Class: Doom::Net::Transport

Inherits:
Object
  • Object
show all
Defined in:
lib/doom/net/transport.rb

Overview

Non-blocking UDP for both multiplayer stacks: the peer-to-peer lockstep mesh (send_to/broadcast over the peer table) and the authoritative star server (send_to_addr, replying straight to whoever a packet came from).

UDP rather than TCP because a late ticcmd is worthless: head-of-line blocking would make one lost packet stall everything behind it, which is the opposite of what we want when the payload is already resent by the next packet anyway.

Nothing here retries or acknowledges. Loss is handled a layer up, by Lockstep resending recent commands, and ordering is handled by the tic numbers inside the packets.

Defined Under Namespace

Classes: Peer

Constant Summary collapse

MAX_DATAGRAM =
2048

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(port: 0, host: '0.0.0.0') ⇒ Transport

Returns a new instance of Transport.



30
31
32
33
34
# File 'lib/doom/net/transport.rb', line 30

def initialize(port: 0, host: '0.0.0.0')
  @socket = UDPSocket.new
  @socket.bind(host, port)
  @peers = {}
end

Instance Attribute Details

#peersObject (readonly)

Returns the value of attribute peers.



28
29
30
# File 'lib/doom/net/transport.rb', line 28

def peers
  @peers
end

Instance Method Details

#add_peer(host, port, player_id: nil) ⇒ Object



40
41
42
43
44
# File 'lib/doom/net/transport.rb', line 40

def add_peer(host, port, player_id: nil)
  peer = Peer.new(host, port, player_id)
  @peers[peer.key] = peer
  peer
end

#broadcast(bytes) ⇒ Object



73
74
75
# File 'lib/doom/net/transport.rb', line 73

def broadcast(bytes)
  @peers.each_value { |peer| send_to(peer, bytes) }
end

#closeObject



101
102
103
# File 'lib/doom/net/transport.rb', line 101

def close
  @socket.close unless @socket.closed?
end

#closed?Boolean

Returns:

  • (Boolean)


105
106
107
# File 'lib/doom/net/transport.rb', line 105

def closed?
  @socket.closed?
end

#local_portObject



36
37
38
# File 'lib/doom/net/transport.rb', line 36

def local_port
  @socket.addr[1]
end

#peer_for(host, port) ⇒ Object



46
47
48
# File 'lib/doom/net/transport.rb', line 46

def peer_for(host, port)
  @peers["#{host}:#{port}"]
end

#poll(max: 64) ⇒ Object

Drain what has arrived, decoded. Returns [[message, host, port], ...]. max bounds the work per frame so a flood cannot starve rendering.

Undecodable packets are dropped silently: on an open port anyone can send anything, and it is not worth a log line per stray datagram.



82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/doom/net/transport.rb', line 82

def poll(max: 64)
  return [] if closed?

  messages = []
  max.times do
    begin
      data, addr = @socket.recvfrom_nonblock(MAX_DATAGRAM)
    rescue IO::WaitReadable, Errno::EWOULDBLOCK, Errno::EAGAIN
      break
    rescue SystemCallError, IOError
      break
    end

    msg = Protocol.decode(data)
    messages << [msg, addr[3], addr[1]] if msg
  end
  messages
end

#send_to(peer, bytes) ⇒ Object



50
51
52
53
54
55
56
57
58
59
# File 'lib/doom/net/transport.rb', line 50

def send_to(peer, bytes)
  return false if closed?

  @socket.send(bytes, 0, peer.host, peer.port)
  true
rescue SystemCallError, IOError
  # A peer that has gone away must not take the session down with it;
  # the lockstep layer already notices them by stalling.
  false
end

#send_to_addr(host, port, bytes) ⇒ Object

Send to a bare address, without registering it as a peer. The game server tracks its clients itself and replies to wherever a packet came from, so it does not want the peer table.



64
65
66
67
68
69
70
71
# File 'lib/doom/net/transport.rb', line 64

def send_to_addr(host, port, bytes)
  return false if closed?

  @socket.send(bytes, 0, host, port)
  true
rescue SystemCallError, IOError
  false
end