else
print "Looking for a server..."
- var s = new UDPSocket
- s.enable_broadcast = true
- s.blocking = false
- s.broadcast(discovery_port, "Server? {handshake_app_name}")
- nanosleep(0, 100_000_000)
-
- var ptr = new Ref[nullable SocketAddress](null)
- var resp = s.recv_from(1024, ptr)
- var src = ptr.item
-
- if not resp.is_empty then
- var words = resp.split(" ")
- if words.length == 3 and words[0] == "Server!" and words[1] == handshake_app_name and words[2].is_numeric then
- address = src.address
- port = words[2].to_i
- end
+ var servers = discover_local_servers
+ if servers.not_empty then
+ address = servers.first.address
+ port = servers.first.port
end
end
# Default listening port of the server
fun default_listening_port: Int do return 18721
- # Port to which clients send discovery requests
- fun discovery_port: Int do return 18722
+ redef fun discovery_port do return 18722
end
end
redef class Server
- # `UDPSocket` to which clients send discovery requests
- var discovery_socket: UDPSocket do
- var s = new UDPSocket
- s.blocking = false
- s.bind(null, discovery_port)
- return s
- end
# The current game
var game = new TGame is lazy, writable
# Do game logic
var turn = game.do_turn
- # Respond to discovery requests
- loop
- var ptr = new Ref[nullable SocketAddress](null)
- var read = discovery_socket.recv_from(1024, ptr)
-
- # No sender means there is no request (an error would also do it)
- var sender = ptr.item
- if sender == null then break
-
- var words = read.split(" ")
- if words.length != 2 or words[0] != "Server?" or words[1] != handshake_app_name then
- print "Server Warning: Rejected discovery request '{read}'"
- continue
- end
-
- discovery_socket.send_to(sender.address, sender.port,
- "Server! {handshake_app_name} {self.port}")
- end
+ # Respond to discovery requests sent over UDP
+ answer_discovery_requests
# Setup clients
var new_clients = accept_clients
# ~~~
module client
-import common
+intrude import common
# Information of the remove server
class RemoteServerConfig
return true
end
end
+
+# Discover local servers responding on UDP `discovery_port`
+#
+# Sends a message in the format `gamnit::network? handshake_app_name` and
+# looks for the response `gamnit::network! handshake_app_name port_number`.
+# Waits for `timeout`, or the default 0.1 seconds, after sending the message.
+#
+# The server usually responds using the method `answer_discovery_requests`.
+# When receiving responses, the client may then choose a server and
+# connect via `new RemoteServer`.
+#
+# ~~~
+# var servers = discover_local_servers
+# if servers.not_empty then
+# var server = new RemoteServer(servers.first)
+# server.connect
+# server.writer.serialize "hello server"
+# server.socket.close
+# end
+# ~~~
+fun discover_local_servers(timeout: nullable Float): Array[RemoteServerConfig]
+do
+ timeout = timeout or else 0.1
+
+ var s = new UDPSocket
+ s.enable_broadcast = true
+ s.blocking = false
+ s.broadcast(discovery_port, "{discovery_request_message} {handshake_app_name}")
+ timeout.sleep
+
+ var r = new Array[RemoteServerConfig]
+ loop
+ var ptr = new Ref[nullable SocketAddress](null)
+ var resp = s.recv_from(1024, ptr)
+ var src = ptr.item
+
+ if resp.is_empty then
+ # No response
+ break
+ else
+ assert src != null
+ var words = resp.split(" ")
+ if words.length == 3 and words[0] == discovery_response_message and
+ words[1] == handshake_app_name and words[2].is_int then
+ var address = src.address
+ var port = words[2].to_i
+ r.add new RemoteServerConfig(address, port)
+ end
+ end
+ end
+ return r
+end
#
# Both client and server refuse connections with a different version.
fun handshake_app_version: String do return "0.0"
+
+# Server port listening for discovery requests
+#
+# This name must be the same between client/server.
+fun discovery_port: Int do return 18722
+
+# First word in discovery requests
+private fun discovery_request_message: String do return "gamnit::network?"
+
+# First word in discovery responses
+private fun discovery_response_message: String do return "gamnit::network!"
# ~~~
module server
-import common
+intrude import common
# Game server controller
class Server
# All connected `RemoteClient`
var clients = new Array[RemoteClient]
- # Socket accepting new connections
+ # TCP socket accepting new connections
+ #
+ # Opened on the first call to `accept_clients`.
var listening_socket: TCPServer is lazy do
- print port
var socket = new TCPServer(port)
socket.listen 8
socket.blocking = false
return socket
end
- init do listening_socket
# Accept currently waiting clients and return them as an array
fun accept_clients: Array[RemoteClient]
client.socket.flush
end
end
+
+ # Respond to pending discovery requests by sending the TCP listening address and port
+ #
+ # Returns the number of valid requests received.
+ #
+ # The response messages includes the TCP listening address and port
+ # for remote clients to connect with TCP using `connect`.
+ # These connections are accepted by the server with `accept_clients`.
+ fun answer_discovery_requests: Int
+ do
+ var count = 0
+ loop
+ var ptr = new Ref[nullable SocketAddress](null)
+ var read = discovery_socket.recv_from(1024, ptr)
+
+ # No sender means there is no discovery request
+ var sender = ptr.item
+ if sender == null then break
+
+ var words = read.split(" ")
+ if words.length != 2 or words[0] != discovery_request_message or words[1] != handshake_app_name then
+ print "Server Warning: Rejected discovery request '{read}' from {sender.address}:{sender.port}"
+ continue
+ end
+
+ var msg = "{discovery_response_message} {handshake_app_name} {self.port}"
+ discovery_socket.send_to(sender.address, sender.port, msg)
+ count += 1
+ end
+ return count
+ end
+
+ # UDP socket responding to discovery requests
+ #
+ # Usually opened on the first call to `answer_discovery_request`.
+ var discovery_socket: UDPSocket is lazy do
+ var s = new UDPSocket
+ s.blocking = false
+ s.bind(null, discovery_port)
+ return s
+ end
end
# Reference to a remote client connected to this server