mirror of
https://github.com/cyberarm/i-mic-fps.git
synced 2025-12-15 15:42:35 +00:00
More work on implementing networking
This commit is contained in:
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
class IMICFPS
|
||||
module Networking
|
||||
class MemoryConnection < Connection
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
class IMICFPS
|
||||
module Networking
|
||||
class MemoryServer < Server
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
23
lib/networking/peer.rb
Normal 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
|
||||
30
lib/networking/read_buffer.rb
Normal file
30
lib/networking/read_buffer.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user