Added rubocop config, more work on CyberarmEngine Netcode; basic sending and receiving of packets is now functional

This commit is contained in:
2020-11-30 15:41:20 -06:00
parent 85ec285263
commit ecbbc77ca7
15 changed files with 566 additions and 133 deletions

8
.rubocop.yml Normal file
View File

@@ -0,0 +1,8 @@
Style/StringLiterals:
EnforcedStyle: double_quotes
Metrics/MethodLength:
Max: 40
Style/EmptyMethod:
EnforcedStyle: expanded

View File

@@ -1,4 +1,4 @@
class IMICFPS
module CyberarmEngine
module Networking
MULTICAST_ADDRESS = "224.0.0.1"
MULTICAST_PORT = 30_000
@@ -6,7 +6,7 @@ class IMICFPS
REMOTE_GAMEHUB = "i-mic.cyberarm.dev"
REMOTE_GAMEHUB_PORT = 98765
DEFAULT_SERVER_HOST = "0.0.0.0"
DEFAULT_SERVER_HOSTNAME = "0.0.0.0"
DEFAULT_SERVER_PORT = 56789
DEFAULT_SERVER_QUERY_PORT = 28900

View File

@@ -1,6 +1,8 @@
module CyberarmEngine
module Networking
class Channel
def initialize(id:, mode:)
end
end
end
end
end

View File

@@ -1,31 +1,78 @@
module CyberarmEngine
module Networking
class Connection
def initialize(hostname:, port:, max_clients:, channels: 1)
def initialize(hostname:, port:, channels: 3)
@hostname = hostname
@port = port
@channels = Array(0..channels).map { |id| Channel.new(id: id, mode: :default) }
@peer = Peer.new(id: 0, hostname: "", port: "")
@last_read_time = Networking.milliseconds
@last_write_time = Networking.milliseconds
@total_packets_sent = 0
@total_packets_received = 0
@total_data_sent = 0
@total_data_received = 0
end
# Callbacks #
def connected
end
def disconnected(reason)
def disconnected(reason:)
end
def reconnected
end
def packet_received(message, channel)
def packet_received(message:, channel:)
end
# Functions #
def send_packet(message, reliable, channel = 0)
def send_packet(message:, reliable: false, channel: 0)
end
def broadcast_packet(message, reliable, channel = 0)
def connect(timeout: Protocol::TIMEOUT_PERIOD)
@socket = UDPSocket.new
write(PacketHandler.create_control_packet(peer: @peer, control_type: Protocol::CONTROL_CONNECT))
end
def disconnect(reason = "")
def disconnect(timeout: Protocol::TIMEOUT_PERIOD)
end
def update
while read
end
end
private
def read
data, addr = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE)
pkt = PacketHandler.handle(host: self, raw: data, peer: @peer)
packet_received(message: pkt.message, channel: -1) if pkt.is_a?(RawPacket)
@total_packets_received += 1
@total_data_received += data.length
@last_read_time = Networking.milliseconds
return true
rescue IO::WaitReadable
return false
end
def write(packet)
raw = packet.encode
@socket.send(raw, 0, @hostname, @port)
@total_packets_sent += 1
@total_data_sent += raw.length
@last_write_time = Networking.milliseconds
end
end
end
end
end

View File

@@ -1,19 +1,30 @@
module CyberarmEngine
module Networking
class Packet
attr_reader :protocol_version, :type, :peer_id, :message
attr_reader :protocol_version, :peer_id, :channel, :message
def self.type
raise NotImplementedError, "#{self.class}.type must be defined!"
def self.decode(raw)
header = raw.unpack(CyberarmEngine::Networking::Protocol::PACKET_BASE_HEADER)
Packet.new(protocol_version: header[0], peer_id: header[1], channel: header[2], message: raw[Protocol::PACKET_BASE_HEADER_LENGTH...raw.length])
end
def self.decode(packet)
raise NotImplementedError, "#{self.class}.decode must be defined!"
def initialize(protocol_version:, peer_id:, channel:, message:)
@protocol_version = protocol_version
@peer_id = peer_id
@channel = channel
@message = message
end
def encode
raise NotImplementedError, "#{self.class}#encode must be defined!"
header = [
@protocol_version,
@peer_id,
@channel
].pack(CyberarmEngine::Networking::Protocol::PACKET_BASE_HEADER)
"#{header}#{@message}"
end
end
end
end
end

View File

@@ -1,6 +1,108 @@
module CyberarmEngine
module Networking
module PacketHandler
def self.type_to_name(type:)
Protocol.constants.select { |const| const.to_s.start_with?("PACKET_") }
.find { |const| Protocol.const_get(const) == type }
end
def self.handle(host:, raw:, peer:)
packet = Packet.decode(raw)
type = packet.message.unpack1("C")
puts "#{host.class} received #{type_to_name(type: type)}"
case type
when Protocol::PACKET_CONTROL
handle_control_packet(host, packet, peer)
when Protocol::PACKET_RAW
handle_raw_packet(packet)
else
raise NotImplementedError, "A Packet handler for #{type} is not implmented!"
end
end
def self.handle_control_packet(host, packet, peer)
pkt = ControlPacket.decode(packet.message)
case pkt.control_type
when Protocol::CONTROL_CONNECT # TOSERVER only
if (peer_id = host.available_peer_id)
peer.id = peer_id
host.clients << peer
peer.write_queue << create_control_packet(peer: peer, control_type: Protocol::CONTROL_SET_PEER_ID, message: [peer_id].pack("n"))
host.client_connected(peer: peer)
else
host.write(
peer: peer,
packet: PacketHandler.create_control_packet(
peer: peer,
control_type: Protocol::CONTROL_DISCONNECT,
message: "ERROR: max number of clients already connected"
)
)
end
when Protocol::CONTROL_SET_PEER_ID # TOCLIENT only
peer.id = pkt.message.unpack1("n")
host.connected
when Protocol::CONTROL_DISCONNECT
if host.is_a?(Server)
host.client_disconnected(peer: peer)
else
host.disconnected(reason: pkt.message)
end
when Protocol::CONTROL_HEARTBEAT
when Protocol::CONTROL_PING
when Protocol::CONTROL_PONG
end
nil
end
def self.handle_raw_packet(packet)
RawPacket.decode(packet.message)
end
def self.create_control_packet(peer:, control_type:, message: nil, reliable: false, channel: 0)
message_packet = nil
if reliable
warn "Reliable packets are not yet implemented!"
packet = ControlPacket.new(control_type: control_type, message: message)
message_packet = ReliablePacket.new(sequence_number: peer.next_reliable_sequence_number, message: packet.encode)
else
message_packet = ControlPacket.new(control_type: control_type, message: message)
end
Packet.new(
protocol_version: Protocol::PROTOCOL_VERSION,
peer_id: peer.id,
channel: channel,
message: message_packet.encode
)
end
def self.create_raw_packet(peer:, message:, reliable: false, channel: 0)
message_packet = nil
if reliable
warn "Reliable packets are not yet implemented!"
packet = RawPacket.new(message: message)
message_packet = ReliablePacket.new(sequence_number: peer.next_reliable_sequence_number, message: packet.encode)
else
message_packet = RawPacket.new(message: message)
end
Packet.new(
protocol_version: Protocol::PROTOCOL_VERSION,
peer_id: peer.id,
channel: channel,
message: message_packet.encode
)
end
end
end
end
end

View File

@@ -0,0 +1,32 @@
module CyberarmEngine
module Networking
class ControlPacket
attr_reader :message, :type, :control_type
HEADER_PACKER = "CC"
HEADER_LENGTH = 1 + 1 # bytes
def self.decode(raw_message)
header = raw_message.unpack(HEADER_PACKER)
message = raw_message[HEADER_LENGTH..raw_message.length - 1]
ControlPacket.new(type: header[0], control_type: header[1], message: message)
end
def initialize(control_type:, message: nil, type: Protocol::PACKET_CONTROL)
@type = type
@control_type = control_type
@message = message
end
def encode
header = [
@type,
@control_type
].pack(HEADER_PACKER)
"#{header}#{@message}"
end
end
end
end

View File

@@ -1,37 +0,0 @@
module CyberarmEngine
module Networking
class DataPacket < Packet
HEADER_PACKER = "CCn"
HEADER_LENGTH = 1 + 1 + 4 # bytes
def self.type
Protocol::DATA
end
def self.decode(raw_message)
header = raw_message.unpack(HEADER_PACKER)
message = raw_message[HEADER_LENGTH..raw_message.length - 1]
DataPacket.new(protocol_version: header[0], type: header[1], message: message)
end
def initialize(protocol_version:, type:, peer_id:, message:)
@protocol_version = protocol_version
@type = type
@peer_id = peer_id
@message = message
end
def encode
header = [
Protocol::PROTOCOL_VERSION,
@type,
@peer_id,
].pack(HEADER_PACKER)
"#{header}#{message}"
end
end
end
end

View File

@@ -0,0 +1,30 @@
module CyberarmEngine
module Networking
class RawPacket
attr_reader :message, :type
HEADER_PACKER = "C"
HEADER_LENGTH = 1 # bytes
def self.decode(raw_message)
header = raw_message.unpack(HEADER_PACKER)
message = raw_message[HEADER_LENGTH..raw_message.length - 1]
RawPacket.new(type: header[0], message: message)
end
def initialize(message:, type: Protocol::PACKET_RAW)
@type = type
@message = message
end
def encode
header = [
@type
].pack(HEADER_PACKER)
"#{header}#{@message}"
end
end
end
end

View File

@@ -0,0 +1,32 @@
module CyberarmEngine
module Networking
class ReliablePacket
attr_reader :message, :type, :control_type
HEADER_PACKER = "Cn"
HEADER_LENGTH = 1 + 2 # bytes
def self.decode(raw_message)
header = raw_message.unpack(HEADER_PACKER)
message = raw_message[HEADER_LENGTH..raw_message.length - 1]
ReliablePacket.new(type: header[0], control_type: header[1], message: message)
end
def initialize(sequence_number:, message:, type: Protocol::PACKET_RELIABLE)
@type = type
@sequence_number = sequence_number
@message = message
end
def encode
header = [
@type,
@control_type
].pack(HEADER_PACKER)
"#{header}#{@message}"
end
end
end
end

View File

@@ -1,14 +1,34 @@
module CyberarmEngine
module Networking
class Peer
attr_reader :id, :hostname, :port, :data
attr_reader :id, :hostname, :port, :data, :read_queue, :write_queue
attr_accessor :total_packets_sent, :total_packets_received,
:total_data_sent, :total_data_received,
:last_read_time, :last_write_time
def initialize(id:, hostname:, port:)
@id = id
@hostname = hostname
@port = port
@data = {}
@read_queue = []
@write_queue = []
@last_read_time = Networking.milliseconds
@last_write_time = Networking.milliseconds
@total_packets_sent = 0
@total_packets_received = 0
@total_data_sent = 0
@total_data_received = 0
end
def id=(n)
raise "Peer id must be an integer" unless n.is_a?(Integer)
@id = n
end
end
end
end
end

View File

@@ -1,30 +1,29 @@
module CyberarmEngine
module Networking
module Protocol
MAX_PACKET_SIZE = 1024
PROTOCOL_VERSION = 0 # int
MAX_PACKET_SIZE = 1024 # bytes
PROTOCOL_VERSION = 0 # u32
HEARTBEAT_INTERVAL = 5_000 # ms
TIMEOUT_PERIOD = 30_000 # ms
packet_types = %w{
# protocol packets
reliable
multipart
control
data
PACKET_BASE_HEADER = "NnC" # protocol version (u32), sender peer id (u16), channel (u8)
PACKET_BASE_HEADER_LENGTH = 4 + 2 + 1 # bytes
# control packet types
disconnect
acknowledge
heartbeat
ping
}
# protocol packets
PACKET_RELIABLE = 0
PACKET_FRAGMENT = 1
PACKET_CONTROL = 2
PACKET_RAW = 3
# emulate c-like enum
packet_types.each_with_index do |type, i|
next if type.start_with?("#")
self.const_set(:"#{type.upcase}", i)
end
# control packet types
CONTROL_CONNECT = 30
CONTROL_SET_PEER_ID = 31
CONTROL_DISCONNECT = 32
CONTROL_ACKNOWLEDGE = 33
CONTROL_HEARTBEAT = 34
CONTROL_PING = 35
CONTROL_PONG = 36
CONTROL_SET_PEER_MTU = 37 # In future
end
end
end
end

View File

@@ -1,59 +1,180 @@
class Server
attr_reader :hostname, :port, :max_clients
def initialize(hostname: "0.0.0.0", port: 56789, max_clients: 32, channels: 1)
@hostname = hostname
@port = port
@max_clients = max_clients
module CyberarmEngine
module Networking
class Server
attr_reader :hostname, :port, :max_clients
attr_accessor :total_packets_sent, :total_packets_received,
:total_data_sent, :total_data_received,
:last_read_time, :last_write_time
@socket = UDPSocket.new
@socket.bind(@hostname, @port)
def initialize(
hostname: CyberarmEngine::Networking::DEFAULT_SERVER_HOSTNAME,
port: CyberarmEngine::Networking::DEFAULT_SERVER_PORT,
max_clients: CyberarmEngine::Networking::DEFAULT_PEER_LIMIT,
channels: 3
)
@hostname = hostname
@port = port
@max_clients = max_clients
@channels = Array(0..channels).map { |id| Channel.new(id: id, server: self) }
@peers = []
@channels = Array(0..channels).map { |id| Channel.new(id: id, mode: :default) }
@peers = []
@last_read_time = Networking.milliseconds
@last_write_time = Networking.milliseconds
@total_packets_sent = 0
@total_packets_received = 0
@total_data_sent = 0
@total_data_received = 0
end
# Helpers #
def connected_clients
@peers.size
end
def clients
@peers
end
# Callbacks #
# Called when client connects
def client_connected(peer:)
end
# Called when client times out or explicitly disconnects
def client_disconnected(peer:, reason:)
end
### REMOVE? ###
# Called when client was not sending heartbeats or regular packets for a
# period of time, but was not logically disconnected and removed, and started
# send packets again.
#
# TLDR: Client was temporarily unreachable but did not timeout.
def client_reconnected(peer:)
end
# Called when a (logical) packet is received from client
def packet_received(peer:, message:, channel:)
end
# Functions #
# Bind server
def bind
# TODO: Handle socket errors
@socket = UDPSocket.new
@socket.bind(@hostname, @port)
end
# Send packet to specified peer
def send_packet(peer:, message:, reliable: false, channel: 0)
if (peer = @peers.get(peer))
packet = PacketHandler.create_raw_packet(message, reliable, channel)
peer.write_queue << packet
else
# TODO: Handle no such peer error
end
end
# Send packet to all connected peer
def broadcast_packet(message:, reliable: false, channel: 0)
@peers.each { |peer| send_packet(peer.id, message, reliable, channel) }
end
# Disconnect peer
def disconnect_client(peer:, reason: "")
if (peer = @peers.get(peer))
packet = PacketHandler.create_disconnect_packet(peer.id, reason)
peer.write_now!(packet)
@peers.delete(peer)
end
end
def update
while(read)
end
# handle write queue
# TODO: handle reliable packets differently
@peers.each do |peer|
if Networking.milliseconds - peer.last_read_time > Protocol::TIMEOUT_PERIOD
message = "ERROR: connection timed out"
write(
peer: peer,
packet: PacketHandler.create_control_packet(
peer: peer,
control_type: Protocol::CONTROL_DISCONNECT,
message: message
)
)
client_disconnected(peer: peer, reason: message)
@peers.delete(peer)
next
end
while(packet = peer.write_queue.shift)
write(peer: peer, packet: packet)
end
end
end
# !--- this following functions are meant for internal use only ---! #
def available_peer_id
peer_ids = @peers.map { ||peer| peer.id }
ids = (1..@max_clients).to_a - peer_ids
ids.size.positive? ? ids.first : nil
end
def read
data, addr = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE)
peer = nil
if (peer = @peers.find { |pr| pr.hostname == addr[2] && pr.port == addr[1] })
pkt = PacketHandler.handle(host: self, raw: data, peer: peer)
packet_received(peer: peer, message: pkt.message, channel: 0) if pkt.is_a?(RawPacket)
else
# TODO: Reject packet unless it's a connection request
peer = Peer.new(id: 0, hostname: addr[2], port: addr[1])
pkt = PacketHandler.handle(host: self, raw: data, peer: peer)
if pkt && !pkt.is_a?(ControlPacket) && pkt.control_type != Protocol::CONTROL_CONNECT
write(
peer: peer,
packet: PacketHandler.create_control_packet(
peer: peer,
control_type: Protocol::CONTROL_DISCONNECT,
message: "ERROR: client not connected"
)
)
end
end
@total_packets_received += 1
@total_data_received += data.length
@last_read_time = Networking.milliseconds
true
rescue IO::WaitReadable
false
end
def write(peer:, packet:)
raw = packet.encode
@socket.send(raw, 0, peer.hostname, peer.port)
@total_packets_sent += 1
@total_data_sent += raw.length
@last_write_time = Networking.milliseconds
peer.total_packets_sent += 1
peer.total_data_sent += raw.length
peer.last_write_time = Networking.milliseconds
end
end
end
# Helpers #
def connected_clients
@peers.size
end
def clients
@peers
end
# Callbacks #
# Called when client connects
def client_connected(peer)
end
# Called when client times out or explicitly disconnects
def client_disconnected(peer, reason)
end
### REMOVE? ###
# Called when client was not sending heartbeats or regular packets for a
# period of time, but was not logically disconnected and removed, and started
# send packets again.
#
# TLDR: Client was temporarily unreachable but did not timeout.
def client_reconnected(peer)
end
# Called when a (logical) packet is received from client
def packet_received(peer, message, channel = 0)
end
# Functions #
# Send packet to specified peer
def send_packet(peer, message, reliable, channel = 0)
end
# Send packet to all connected peer
def broadcast_packet(message, reliable, channel = 0)
end
# Disconnect peer
def disconnect_client(peer, reason = "")
end
end
end

66
new_server_test.rb Normal file
View File

@@ -0,0 +1,66 @@
def require_all(directory)
files = Dir["#{directory}/**/*.rb"].sort!
begin
failed = []
first_name_error = nil
files.each do |file|
begin
require_relative file
rescue NameError => name_error
failed << file
first_name_error ||= name_error
end
end
if failed.size == files.size
raise first_name_error
else
files = failed
end
end until( failed.empty? )
end
require "socket"
require_relative "lib/networking"
require_all "lib/networking/backend"
server = CyberarmEngine::Networking::Server.new
def server.client_connected(peer:)
puts "Client connected as peer: #{peer.id}"
end
def server.packet_received(peer:, message:, channel:)
pp "Server received: #{message} [on channel: #{channel} from peer: #{peer&.id}]"
end
Thread.new do
server.bind
loop do
server.update
sleep (1000.0 / 60.0) / 10.0
end
end
connection = CyberarmEngine::Networking::Connection.new(hostname: "localhost", port: CyberarmEngine::Networking::DEFAULT_SERVER_PORT, channels: 3)
def connection.connected
puts "Connection: Connected!"
end
def connection.disconnected(reason:)
puts "Connection: disconnected: #{reason}"
end
def connection.packet_received(message:, channel:)
pp "Connection received: #{message} [on channel: #{channel} from peer: SERVER]"
end
connection.connect(timeout: 1_000)
loop do
connection.update
sleep (1000.0 / 60.0) / 10.0
end
sleep