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

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

View File

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

View File

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

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

View File

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