More work on implementing networking

This commit is contained in:
2020-05-10 11:05:16 -05:00
parent e94f2582f9
commit cf1e72225c
11 changed files with 253 additions and 118 deletions

View File

@@ -1,26 +1,32 @@
class IMICFPS class IMICFPS
class NetworkManager module Networking
MULTICAST_ADDRESS = "224.0.0.1" MULTICAST_ADDRESS = "224.0.0.1"
MULTICAST_PORT = 30_000 MULTICAST_PORT = 30_000
REMOTE_GAMEHUB = "i-mic.rubyclan.org" REMOTE_GAMEHUB = "i-mic.cyberarm.dev"
REMOTE_GAMEHUB_PORT = 98765 REMOTE_GAMEHUB_PORT = 98765
DEFAULT_SERVER_HOST = "0.0.0.0" DEFAULT_SERVER_HOST = "0.0.0.0"
DEFAULT_SERVER_PORT = 56789 DEFAULT_SERVER_PORT = 56789
DEFAULT_SERVER_QUERY_PORT = 28900 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 end
# https://github.com/jpignata/blog/blob/master/articles/multicast-in-ruby.md # 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 = UDPSocket.open
socket.setsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL, 1) socket.setsockopt(:IPPROTO_IP, :IP_MULTICAST_TTL, 1)
socket.send("IMICFPS_LAN_LOBBY", 0, MULTICAST_ADDRESS, MULTICAST_PORT) socket.send("IMICFPS_LAN_LOBBY", 0, MULTICAST_ADDRESS, MULTICAST_PORT)
socket.close socket.close
end end
def handle_lan_multicast def self.handle_lan_multicast
end end
end end
end end

View File

@@ -1,6 +0,0 @@
class IMICFPS
module Networking
class MemoryConnection < Connection
end
end
end

View File

@@ -1,6 +0,0 @@
class IMICFPS
module Networking
class MemoryServer < Server
end
end
end

View File

@@ -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

View File

@@ -1,16 +1,75 @@
class IMICFPS class IMICFPS
module Networking module Networking
class Connection 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 end
def connect 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 end
def update def update
while(read)
end
write
@read_buffer.reconstruct_packets.each do |packet|
end
end end
def close 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 end
end end

View File

@@ -1,29 +1,24 @@
class IMICFPS class IMICFPS
module Networking module Networking
class Director class Director
attr_reader :mode, :hostname, :port, :tick_rate, :storage attr_reader :address, :port, :tick_rate, :storage, :map
def initialize(mode:, hostname:, port:, interface:, state: nil, tick_rate: 2) def initialize(address: DEFAULT_SERVER_HOST, port: DEFAULT_SERVER_PORT, tick_rate: 2)
@mode = mode @address = address
@hostname = hostname
@port = port @port = port
@state = state
@tick_rate = (1000.0 / tick_rate) / 1000.0 @tick_rate = (1000.0 / tick_rate) / 1000.0
case @mode @server = Server.new(address: @address, port: @port, max_peers: DEFAULT_PEER_LIMIT)
when :server @server.bind
@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
@last_tick_time = milliseconds @last_tick_time = Networking.milliseconds
@directing = true @directing = true
@storage = {} @storage = {}
@map = nil
end
def load_map(map_parser:)
# TODO: send map_change to clients
@map = Map.new(map_parser: map_parser)
end end
def run def run
@@ -33,28 +28,24 @@ class IMICFPS
tick(dt) tick(dt)
@server.update if @server
@connection.update if @connection
@last_tick_time = milliseconds @last_tick_time = milliseconds
sleep(@tick_rate) sleep(@tick_rate)
end end
end 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 end
def shutdown def shutdown
@directing = false @directing = false
@server.close
@clients.each(&:close)
@server.update if @server
@connection.update if @connection
end
def milliseconds
Process.clock_gettime(Process::CLOCK_MONOTONIC)
end end
end end
end end

View File

@@ -1,22 +1,32 @@
class IMICFPS class IMICFPS
module Networking module Networking
class Packet class Packet
HEADER_PACKER = "CnCnC" HEADER_PACKER = "CnCnCC"
HEADER_SIZE = 7 HEADER_SIZE = 8
def self.from_stream(raw) def self.from_stream(raw)
header = raw[ 0..HEADER_SIZE ].unpack(HEADER_PACKER) 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 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 @sequence_number = sequence
@packet_type = type @packet_type = type
@content_length = payload.length
@parity = calculate_parity @parity = calculate_parity
@payload = payload @payload = payload
@content_length = payload.length
end end
def header def header
@@ -26,7 +36,8 @@ class IMICFPS
@packet_type, # char @packet_type, # char
@content_length, # uint16 @content_length, # uint16
@parity, # char @parity, # char
].unpack(HEADER_PACKER) @peer_id, # char
].pack(HEADER_PACKER)
end end
def calculate_parity def calculate_parity
@@ -38,6 +49,7 @@ class IMICFPS
end end
def decode(payload) def decode(payload)
payload
end end
end end
end end

23
lib/networking/peer.rb Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -1,28 +1,102 @@
class IMICFPS class IMICFPS
module Networking module Networking
class Server class Server
MAX_CLIENTS = 32 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
attr_reader :hostname, :port, :max_clients, :clients def initialize(address:, port:, max_peers:)
def initialize(hostname:, port:, max_clients: MAX_CLIENTS) @address = address
@hostname = hostname
@port = port @port = port
@max_clients = max_clients @max_peers = max_peers
@clients = [] @peers = Array.new(@max_peers + 1, nil)
@socket = 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 end
def bind def bind
@socket = UDPSocket.new
@socket.bind(@address, @port)
end 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 end
def update def update
while(read)
end
# "deliver" packets to peers, record stats to peers
@read_buffer.reconstruct_packets.each do |packet|
end
end end
def close 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 end
end end

View File

@@ -9,7 +9,11 @@ class IMICFPS
@player = @map.find_entity_by(name: "character") @player = @map.find_entity_by(name: "character")
@camera = Camera.new(position: @player.position.clone) @camera = Camera.new(position: @player.position.clone)
@camera.attach_to(@player) @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 @crosshair = Crosshair.new
@hud = HUD.new(@player) @hud = HUD.new(@player)
@@ -35,15 +39,13 @@ class IMICFPS
def update def update
update_text update_text
Publisher.instance.publish(:tick, Gosu.milliseconds - window.delta_time)
@map.update
control_player control_player
@hud.update @hud.update
@camera.update @camera.update
@connection.update
@director.tick(window.dt) @director.tick(window.dt)
@map.update
if window.config.get(:debug_options, :stats) if window.config.get(:debug_options, :stats)
@text.text = update_text @text.text = update_text