gamnit: add more options to `accept_clients` & `broadcast`
[nit.git] / lib / gamnit / network / server.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # Server-side network services for games and such
16 #
17 # The following code creates a server that continuously listen for new clients,
18 # and exchange with them briefly before disconnecting.
19 #
20 # ~~~nitish
21 # redef fun handshake_app_name do return "nitwork_test"
22 # redef fun handshake_app_version do return "1.0"
23 #
24 # Open a server on port 4444
25 # var server = new Server(4444)
26 #
27 # loop
28 # # Accept new clients
29 # var new_clients = server.accept_clients
30 # for client in new_clients do
31 # # A client is connected, communicate!
32 # print ""
33 # print client.reader.deserialize.as(Object)
34 # client.writer.serialize "Goodbye client"
35 #
36 # # Done, close socket
37 # client.socket.close
38 # end
39 #
40 # # `accept_clients` in non-blocking,
41 # # sleep before tying again, or do something else.
42 # 0.5.sleep
43 # printn "."
44 # end
45 # ~~~
46 module server
47
48 intrude import common
49
50 # Game server controller
51 class Server
52
53 # Port for the `listening_socket`
54 var port: Int
55
56 # All connected `RemoteClient`
57 var clients = new Array[RemoteClient]
58
59 # TCP socket accepting new connections
60 #
61 # Opened on the first call to `accept_clients`.
62 var listening_socket: TCPServer is lazy do
63 var socket = new TCPServer(port)
64 socket.listen 8
65 socket.blocking = false
66 return socket
67 end
68
69 # Accept currently waiting clients and return them as an array
70 #
71 # If `add_to_clients`, the default, the new clients are added to `clients`.
72 # Otherwise, the return value of `accept_clients` may be added to `clients`
73 # explicitly by the caller after an extra verification or sorting.
74 fun accept_clients(add_to_clients: nullable Bool): Array[RemoteClient]
75 do
76 add_to_clients = add_to_clients or else true
77 assert not listening_socket.closed
78
79 var new_clients = new Array[RemoteClient]
80 loop
81 var client_socket = listening_socket.accept
82 if client_socket == null then break
83
84 var rc = new RemoteClient(client_socket)
85
86 var handshake_success = rc.handshake
87 if handshake_success then
88 new_clients.add rc
89 print "Server: Client at {client_socket.address} passed the handshake"
90 else
91 print_error "Server Error: Client at {client_socket.address} failed the handshake"
92 client_socket.close
93 end
94 end
95
96 if add_to_clients then clients.add_all new_clients
97
98 return new_clients
99 end
100
101 # Broadcast a `message` to all `clients`, then flush the connection
102 #
103 # The client `except` is skipped and will not receive the `message`.
104 fun broadcast(message: Serializable, except: nullable RemoteClient)
105 do
106 for client in clients do if client != except then
107 client.writer.serialize(message)
108 client.socket.flush
109 end
110 end
111
112 # Respond to pending discovery requests by sending the TCP listening address and port
113 #
114 # Returns the number of valid requests received.
115 #
116 # The response messages includes the TCP listening address and port
117 # for remote clients to connect with TCP using `connect`.
118 # These connections are accepted by the server with `accept_clients`.
119 fun answer_discovery_requests: Int
120 do
121 var count = 0
122 loop
123 var ptr = new Ref[nullable SocketAddress](null)
124 var read = discovery_socket.recv_from(1024, ptr)
125
126 # No sender means there is no discovery request
127 var sender = ptr.item
128 if sender == null then break
129
130 var words = read.split(" ")
131 if words.length != 2 or words[0] != discovery_request_message or words[1] != handshake_app_name then
132 print "Server Warning: Rejected discovery request '{read}' from {sender.address}:{sender.port}"
133 continue
134 end
135
136 var msg = "{discovery_response_message} {handshake_app_name} {self.port}"
137 discovery_socket.send_to(sender.address, sender.port, msg)
138 count += 1
139 end
140 return count
141 end
142
143 # UDP socket responding to discovery requests
144 #
145 # Usually opened on the first call to `answer_discovery_request`.
146 var discovery_socket: UDPSocket is lazy do
147 var s = new UDPSocket
148 s.blocking = false
149 s.bind(null, discovery_port)
150 return s
151 end
152 end
153
154 # Reference to a remote client connected to this server
155 class RemoteClient
156
157 # Communication socket with the client
158 var socket: TCPStream
159
160 # Is this client connected?
161 fun connected: Bool do return socket.connected
162
163 # `BinarySerializer` used to send data to this client through `socket`
164 var writer: BinarySerializer is noinit
165
166 # `BinaryDeserializer` used to receive data from this client through `socket`
167 var reader: BinaryDeserializer is noinit
168
169 init
170 do
171 # Setup serialization
172 writer = new BinarySerializer(socket)
173 writer.cache = new AsyncCache(true)
174 reader = new BinaryDeserializer(socket)
175 writer.link reader
176 end
177
178 # Check for compatibility with the client
179 fun handshake: Bool
180 do
181 print "Server: Handshake initiated by {socket.address}"
182
183 # Make sure it is the same app
184 var server_app = sys.handshake_app_name
185 var client_app = socket.read_string
186 if server_app != client_app then
187 print_error "Server Error: Client app name is '{client_app}'"
188
189 # Send an empty string so the client read it and give up
190 socket.write_string ""
191 socket.close
192 return false
193 end
194
195 socket.write_string server_app
196
197 # App version
198 var app_version = sys.handshake_app_version
199 var client_version = socket.read_string
200 if client_version != app_version then
201 print_error "Handshake Error: client version is different '{client_version}'"
202 socket.write_string ""
203 socket.close
204 return false
205 end
206
207 socket.write_string app_version
208
209 return true
210 end
211 end