From cf1e72225cf0ef44847bfabbd2fd958204dbb003 Mon Sep 17 00:00:00 2001 From: Cyberarm Date: Sun, 10 May 2020 11:05:16 -0500 Subject: [PATCH] More work on implementing networking --- .../network_manager.rb => networking.rb} | 16 +++- lib/networking/backends/memory_connection.rb | 6 -- lib/networking/backends/memory_server.rb | 6 -- lib/networking/client.rb | 50 ---------- lib/networking/connection.rb | 61 +++++++++++- lib/networking/director.rb | 49 ++++------ lib/networking/packet.rb | 26 ++++-- lib/networking/peer.rb | 23 +++++ lib/networking/read_buffer.rb | 30 ++++++ lib/networking/server.rb | 92 +++++++++++++++++-- lib/states/game_states/game.rb | 12 ++- 11 files changed, 253 insertions(+), 118 deletions(-) rename lib/{managers/network_manager.rb => networking.rb} (64%) delete mode 100644 lib/networking/backends/memory_connection.rb delete mode 100644 lib/networking/backends/memory_server.rb delete mode 100644 lib/networking/client.rb create mode 100644 lib/networking/peer.rb create mode 100644 lib/networking/read_buffer.rb diff --git a/lib/managers/network_manager.rb b/lib/networking.rb similarity index 64% rename from lib/managers/network_manager.rb rename to lib/networking.rb index 310f7f1..a5b4f6c 100644 --- a/lib/managers/network_manager.rb +++ b/lib/networking.rb @@ -1,26 +1,32 @@ class IMICFPS - class NetworkManager + module Networking MULTICAST_ADDRESS = "224.0.0.1" MULTICAST_PORT = 30_000 - REMOTE_GAMEHUB = "i-mic.rubyclan.org" + REMOTE_GAMEHUB = "i-mic.cyberarm.dev" REMOTE_GAMEHUB_PORT = 98765 DEFAULT_SERVER_HOST = "0.0.0.0" DEFAULT_SERVER_PORT = 56789 DEFAULT_SERVER_QUERY_PORT = 28900 - def initialize + + RESERVED_PEER_ID = 0 + DEFAULT_PEER_LIMIT = 32 + HARD_PEER_LIMIT = 254 + + def self.milliseconds + Process.clock_gettime(Process::CLOCK_MONOTONIC) end # https://github.com/jpignata/blog/blob/master/articles/multicast-in-ruby.md - def broadcast_lan_lobby + def self.broadcast_lan_lobby socket = UDPSocket.open socket.setsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL, 1) socket.send("IMICFPS_LAN_LOBBY", 0, MULTICAST_ADDRESS, MULTICAST_PORT) socket.close end - def handle_lan_multicast + def self.handle_lan_multicast end end end \ No newline at end of file diff --git a/lib/networking/backends/memory_connection.rb b/lib/networking/backends/memory_connection.rb deleted file mode 100644 index 363945e..0000000 --- a/lib/networking/backends/memory_connection.rb +++ /dev/null @@ -1,6 +0,0 @@ -class IMICFPS - module Networking - class MemoryConnection < Connection - end - end -end \ No newline at end of file diff --git a/lib/networking/backends/memory_server.rb b/lib/networking/backends/memory_server.rb deleted file mode 100644 index 3f80f66..0000000 --- a/lib/networking/backends/memory_server.rb +++ /dev/null @@ -1,6 +0,0 @@ -class IMICFPS - module Networking - class MemoryServer < Server - end - end -end \ No newline at end of file diff --git a/lib/networking/client.rb b/lib/networking/client.rb deleted file mode 100644 index 93d492d..0000000 --- a/lib/networking/client.rb +++ /dev/null @@ -1,50 +0,0 @@ -class IMICFPS - module Networking - class Client - attr_reader :packets_sent, :packets_received, - :data_sent, :data_received - def initialize(socket:) - @socket = socket - @write_queue = [] - @read_queue = [] - - @packets_sent = 0 - @packets_received = 0 - @data_sent = 0 - @data_received = 0 - end - - def read - data = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE) - @read_queue << Packet.decode(data) - - @packets_received += 1 - @data_received += data.length - rescue IO::WaitReadable - end - - def write - @write_queue.each do |packet| - raw = Packet.encode - @socket.send(raw, 0) - @write_queue.delete(packet) - - @packets_sent += 1 - @data_sent += raw.length - end - end - - def puts(packet) - @write_queue << packet - end - - def gets - @read_queue.shift - end - - def close - @socket.close if @socket - end - end - end -end \ No newline at end of file diff --git a/lib/networking/connection.rb b/lib/networking/connection.rb index b9896a6..07434f0 100644 --- a/lib/networking/connection.rb +++ b/lib/networking/connection.rb @@ -1,16 +1,75 @@ class IMICFPS module Networking class Connection - def initialize(hostname:, port:) + attr_reader :address, :port + attr_accessor :total_packets_sent, :total_packets_received, :total_data_sent, :total_data_received, :last_read_time, :last_write_time + def initialize(address:, port:) + @address = address + @port = port + + + @read_buffer = ReadBuffer.new + @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 + + @socket = nil end def connect + @socket = UDPSocket.new + @socket.connect(@address, @port) + + send_packet( + Packet.new( + peer_id: 0, + sequence: 0, + type: Protocol::CONNECT, + payload: "Hello World!" + ) + ) + end + + def send_packet( packet ) end def update + while(read) + end + + write + + @read_buffer.reconstruct_packets.each do |packet| + end end def close + @socket.close if @socket + end + + private + def read + data, addr = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE) + @read_buffer.add(data, addr ) + + @total_packets_received += 1 + @total_data_received += data.length + @last_read_time = Networking.milliseconds + return true + rescue IO::WaitReadable + return false + end + + def write + while(packet = @write_queue.shift) + @socket.send( packet.encode, 0, @address, @port ) + end end end end diff --git a/lib/networking/director.rb b/lib/networking/director.rb index 2a0fd19..eaf25db 100644 --- a/lib/networking/director.rb +++ b/lib/networking/director.rb @@ -1,29 +1,24 @@ class IMICFPS module Networking class Director - attr_reader :mode, :hostname, :port, :tick_rate, :storage - def initialize(mode:, hostname:, port:, interface:, state: nil, tick_rate: 2) - @mode = mode - @hostname = hostname + attr_reader :address, :port, :tick_rate, :storage, :map + def initialize(address: DEFAULT_SERVER_HOST, port: DEFAULT_SERVER_PORT, tick_rate: 2) + @address = address @port = port - @state = state @tick_rate = (1000.0 / tick_rate) / 1000.0 - case @mode - when :server - @server = interface.new(hostname: @hostname, port: @port) - when :connection - @connection = interface.new(hostname: @hostname, port: @port) - when :memory - @server = interface[:server].new(hostname: @hostname, port: @port) - @connection = interface[:connection].new(hostname: @hostname, port: @port) - else - raise ArgumentError, "Expected mode to be :server, :connection, or :memory, not #{mode.inspect}" - end + @server = Server.new(address: @address, port: @port, max_peers: DEFAULT_PEER_LIMIT) + @server.bind - @last_tick_time = milliseconds + @last_tick_time = Networking.milliseconds @directing = true @storage = {} + @map = nil + end + + def load_map(map_parser:) + # TODO: send map_change to clients + @map = Map.new(map_parser: map_parser) end def run @@ -33,28 +28,24 @@ class IMICFPS tick(dt) - @server.update if @server - @connection.update if @connection - @last_tick_time = milliseconds sleep(@tick_rate) end end end - def tick(dt) + def tick(delta_time) + if @map + Publisher.instance.publish(:tick, delta_time * 1000.0) + + @map.update + @server.update + end end def shutdown @directing = false - - @clients.each(&:close) - @server.update if @server - @connection.update if @connection - end - - def milliseconds - Process.clock_gettime(Process::CLOCK_MONOTONIC) + @server.close end end end diff --git a/lib/networking/packet.rb b/lib/networking/packet.rb index e662ed7..a3ad16f 100644 --- a/lib/networking/packet.rb +++ b/lib/networking/packet.rb @@ -1,22 +1,32 @@ class IMICFPS module Networking class Packet - HEADER_PACKER = "CnCnC" - HEADER_SIZE = 7 + HEADER_PACKER = "CnCnCC" + HEADER_SIZE = 8 def self.from_stream(raw) header = raw[ 0..HEADER_SIZE ].unpack(HEADER_PACKER) - payload = raw[HEADER_SIZE + 1..raw.length - 1] + payload = raw[HEADER_SIZE..raw.length - 1] - new(header[1], [2], payload) + new(peer_id: header.last, sequence: header[1], type: header[2], payload: payload) end - def initialize(sequence:, type:, payload:) + # TODO: Handle splitting big packets into smaller ones + def self.splinter(packet) + packets = [packet] + + return packets + end + + attr_reader :peer_id, :sequence_number, :packet_type, :parity, :payload, :content_length + def initialize(peer_id:, sequence:, type:, payload:) + @peer_id = peer_id @sequence_number = sequence @packet_type = type - @content_length = payload.length @parity = calculate_parity @payload = payload + + @content_length = payload.length end def header @@ -26,7 +36,8 @@ class IMICFPS @packet_type, # char @content_length, # uint16 @parity, # char - ].unpack(HEADER_PACKER) + @peer_id, # char + ].pack(HEADER_PACKER) end def calculate_parity @@ -38,6 +49,7 @@ class IMICFPS end def decode(payload) + payload end end end diff --git a/lib/networking/peer.rb b/lib/networking/peer.rb new file mode 100644 index 0000000..785b657 --- /dev/null +++ b/lib/networking/peer.rb @@ -0,0 +1,23 @@ +class IMICFPS + module Networking + class Peer + attr_reader :packet_read_queue, :packet_write_queue + attr_accessor :packets_sent, :packets_received, :data_sent, :data_received, :last_read_time, :last_write_time + def initialize(peer_id:, address:, port:) + @address, @port = address, port + @peer_id = peer_id + + @packet_write_queue = [] + @packet_read_queue = [] + + @last_read_time = Networking.milliseconds + @last_write_time = Networking.milliseconds + + @packets_sent = 0 + @packets_received = 0 + @data_sent = 0 + @data_received = 0 + end + end + end +end \ No newline at end of file diff --git a/lib/networking/read_buffer.rb b/lib/networking/read_buffer.rb new file mode 100644 index 0000000..4e1d2b0 --- /dev/null +++ b/lib/networking/read_buffer.rb @@ -0,0 +1,30 @@ +class IMICFPS + module Networking + class ReadBuffer + def initialize + @buffer = [] + end + + def add(buffer, addr_info) + @buffer << { buffer: buffer, addr_info: addr_info } + end + + def reconstruct_packets + packets = [] + + @buffer.each do |buffer, addr_info| + packet = Packet.from_stream(buffer) + + if packet.valid? + @buffer.delete(buffer) + else + puts "Invalid packet: #{packet}" + @buffer.delete(buffer) + end + end + + return packets + end + end + end +end \ No newline at end of file diff --git a/lib/networking/server.rb b/lib/networking/server.rb index adfa774..8e6323e 100644 --- a/lib/networking/server.rb +++ b/lib/networking/server.rb @@ -1,28 +1,102 @@ class IMICFPS module Networking class Server - MAX_CLIENTS = 32 - - attr_reader :hostname, :port, :max_clients, :clients - def initialize(hostname:, port:, max_clients: MAX_CLIENTS) - @hostname = hostname + attr_reader :address, :port, :max_peers, :peers + attr_accessor :total_packets_sent, :total_packets_received, :total_data_sent, :total_data_received, :last_read_time, :last_write_time + def initialize(address:, port:, max_peers:) + @address = address @port = port - @max_clients = max_clients + @max_peers = max_peers - @clients = [] - @socket = nil + @peers = Array.new(@max_peers + 1, nil) + + @read_buffer = ReadBuffer.new + + @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 + + # Peer ID 0 is reserved for unconnected peers to pass packet validation + def create_peer(address:, port:) + @peers.each_with_index do |peer, i| + unless peer + new_peer = Peer.new(peer_id: i + 1, address: address, port: port) + @peers[i + 1] = new_peer + + return new_peer + end + end + end + + def get_peer(peer_id) + @peers[peer_id + 1] + end + + def remove_peer(peer_id) + @peers[peer_id] = nil end def bind + @socket = UDPSocket.new + @socket.bind(@address, @port) end - def broadcast(packet) + def send_packet( peer_id, packet ) + if peer = get_peer(peer_id) + packets = Packet.splinter(packet) + + packets.each { |pkt| peer.write_queue.add(pkt) } + end + end + + def broadcast_packet(packet) + @peers.each do |peer| + send_packet(peer.peer_id, packet) + end end def update + while(read) + end + + # "deliver" packets to peers, record stats to peers + @read_buffer.reconstruct_packets.each do |packet| + end end def close + @socket.close if @socket + end + + private + def read + data, addr = @socket.recvfrom_nonblock(Protocol::MAX_PACKET_SIZE) + @read_buffer.add(data, addr ) + + @total_packets_received += 1 + @total_data_received += data.length + @last_read_time = Networking.milliseconds + return true + rescue IO::WaitReadable + return false + end + + def write(peer, packet) + raw = packet.encode + @socket.send( raw, 0, peer.address, 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 diff --git a/lib/states/game_states/game.rb b/lib/states/game_states/game.rb index d8760dc..b11434b 100644 --- a/lib/states/game_states/game.rb +++ b/lib/states/game_states/game.rb @@ -9,7 +9,11 @@ class IMICFPS @player = @map.find_entity_by(name: "character") @camera = Camera.new(position: @player.position.clone) @camera.attach_to(@player) - @director = Networking::Director.new(mode: :memory, hostname: "i-mic.rubyclan.org", port: 56789, interface: { server: Networking::MemoryServer, connection: Networking::MemoryConnection }, state: self) + @director = Networking::Director.new + @director.load_map(map_parser: @options[:map_parser]) + + @connection = Networking::Connection.new(address: "localhost", port: Networking::DEFAULT_SERVER_PORT) + @connection.connect @crosshair = Crosshair.new @hud = HUD.new(@player) @@ -35,15 +39,13 @@ class IMICFPS def update update_text - Publisher.instance.publish(:tick, Gosu.milliseconds - window.delta_time) - - @map.update - control_player @hud.update @camera.update + @connection.update @director.tick(window.dt) + @map.update if window.config.get(:debug_options, :stats) @text.text = update_text