ci: tests for macOS on Gitlab CI
[nit.git] / lib / gamnit / network / server.nit
index fcff7a2..0a78d43 100644 (file)
 #
 #     # `accept_clients` in non-blocking,
 #     # sleep before tying again, or do something else.
-#     nanosleep(0, 50000000)
+#     0.5.sleep
 #     printn "."
 # end
 # ~~~
 module server
 
-import common
+intrude import common
 
 # Game server controller
 class Server
@@ -56,19 +56,24 @@ 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]
+       #
+       # If `add_to_clients`, the default, the new clients are added to `clients`.
+       # Otherwise, the return value of `accept_clients` may be added to `clients`
+       # explicitly by the caller after an extra verification or sorting.
+       fun accept_clients(add_to_clients: nullable Bool): Array[RemoteClient]
        do
+               add_to_clients = add_to_clients or else true
                assert not listening_socket.closed
 
                var new_clients = new Array[RemoteClient]
@@ -87,17 +92,63 @@ class Server
                                client_socket.close
                        end
                end
+
+               if add_to_clients then clients.add_all new_clients
+
                return new_clients
        end
 
        # Broadcast a `message` to all `clients`, then flush the connection
-       fun broadcast(message: Serializable)
+       #
+       # The client `except` is skipped and will not receive the `message`.
+       fun broadcast(message: Serializable, except: nullable RemoteClient)
        do
-               for client in clients do
+               for client in clients do if client != except then
                        client.writer.serialize(message)
                        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
@@ -109,51 +160,51 @@ class RemoteClient
        # Is this client connected?
        fun connected: Bool do return socket.connected
 
-       # `BinarySerializer` used to send data to this client through `socket`
-       var writer: BinarySerializer is noinit
+       # `MsgPackSerializer` used to send data to this client through `socket`
+       var writer: MsgPackSerializer is noinit
 
-       # `BinaryDeserializer` used to receive data from this client through `socket`
-       var reader: BinaryDeserializer is noinit
+       # `MsgPackDeserializer` used to receive data from this client through `socket`
+       var reader: MsgPackDeserializer is noinit
 
        init
        do
                # Setup serialization
-               writer = new BinarySerializer(socket)
+               writer = new MsgPackSerializer(socket)
                writer.cache = new AsyncCache(true)
-               reader = new BinaryDeserializer(socket)
+               reader = new MsgPackDeserializer(socket)
                writer.link reader
        end
 
        # Check for compatibility with the client
        fun handshake: Bool
        do
-               print "Server: Handshake requested by {socket.address}"
+               print "Server: Handshake initiated by {socket.address}"
 
                # Make sure it is the same app
                var server_app = sys.handshake_app_name
-               var client_app = socket.read_string
+               var client_app = socket.deserialize_msgpack
                if server_app != client_app then
-                       print_error "Server Error: Client app name is '{client_app}'"
+                       print_error "Server Error: Client app name is '{client_app or else "<invalid>"}'"
 
                        # Send an empty string so the client read it and give up
-                       socket.write_string ""
+                       socket.serialize_msgpack ""
                        socket.close
                        return false
                end
 
-               socket.write_string server_app
+               socket.serialize_msgpack server_app
 
                # App version
                var app_version = sys.handshake_app_version
-               var client_version = socket.read_string
+               var client_version = socket.deserialize_msgpack
                if client_version != app_version then
-                       print_error "Handshake Error: client version is different '{client_version}'"
-                       socket.write_string ""
+                       print_error "Handshake Error: client version is different '{client_version or else "<invalid>"}'"
+                       socket.serialize_msgpack ""
                        socket.close
                        return false
                end
 
-               socket.write_string app_version
+               socket.serialize_msgpack app_version
 
                return true
        end