gamnit: tweak network doc
[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 fun accept_clients: Array[RemoteClient]
71 do
72 assert not listening_socket.closed
73
74 var new_clients = new Array[RemoteClient]
75 loop
76 var client_socket = listening_socket.accept
77 if client_socket == null then break
78
79 var rc = new RemoteClient(client_socket)
80
81 var handshake_success = rc.handshake
82 if handshake_success then
83 new_clients.add rc
84 print "Server: Client at {client_socket.address} passed the handshake"
85 else
86 print_error "Server Error: Client at {client_socket.address} failed the handshake"
87 client_socket.close
88 end
89 end
90 return new_clients
91 end
92
93 # Broadcast a `message` to all `clients`, then flush the connection
94 fun broadcast(message: Serializable)
95 do
96 for client in clients do
97 client.writer.serialize(message)
98 client.socket.flush
99 end
100 end
101
102 # Respond to pending discovery requests by sending the TCP listening address and port
103 #
104 # Returns the number of valid requests received.
105 #
106 # The response messages includes the TCP listening address and port
107 # for remote clients to connect with TCP using `connect`.
108 # These connections are accepted by the server with `accept_clients`.
109 fun answer_discovery_requests: Int
110 do
111 var count = 0
112 loop
113 var ptr = new Ref[nullable SocketAddress](null)
114 var read = discovery_socket.recv_from(1024, ptr)
115
116 # No sender means there is no discovery request
117 var sender = ptr.item
118 if sender == null then break
119
120 var words = read.split(" ")
121 if words.length != 2 or words[0] != discovery_request_message or words[1] != handshake_app_name then
122 print "Server Warning: Rejected discovery request '{read}' from {sender.address}:{sender.port}"
123 continue
124 end
125
126 var msg = "{discovery_response_message} {handshake_app_name} {self.port}"
127 discovery_socket.send_to(sender.address, sender.port, msg)
128 count += 1
129 end
130 return count
131 end
132
133 # UDP socket responding to discovery requests
134 #
135 # Usually opened on the first call to `answer_discovery_request`.
136 var discovery_socket: UDPSocket is lazy do
137 var s = new UDPSocket
138 s.blocking = false
139 s.bind(null, discovery_port)
140 return s
141 end
142 end
143
144 # Reference to a remote client connected to this server
145 class RemoteClient
146
147 # Communication socket with the client
148 var socket: TCPStream
149
150 # Is this client connected?
151 fun connected: Bool do return socket.connected
152
153 # `BinarySerializer` used to send data to this client through `socket`
154 var writer: BinarySerializer is noinit
155
156 # `BinaryDeserializer` used to receive data from this client through `socket`
157 var reader: BinaryDeserializer is noinit
158
159 init
160 do
161 # Setup serialization
162 writer = new BinarySerializer(socket)
163 writer.cache = new AsyncCache(true)
164 reader = new BinaryDeserializer(socket)
165 writer.link reader
166 end
167
168 # Check for compatibility with the client
169 fun handshake: Bool
170 do
171 print "Server: Handshake initiated by {socket.address}"
172
173 # Make sure it is the same app
174 var server_app = sys.handshake_app_name
175 var client_app = socket.read_string
176 if server_app != client_app then
177 print_error "Server Error: Client app name is '{client_app}'"
178
179 # Send an empty string so the client read it and give up
180 socket.write_string ""
181 socket.close
182 return false
183 end
184
185 socket.write_string server_app
186
187 # App version
188 var app_version = sys.handshake_app_version
189 var client_version = socket.read_string
190 if client_version != app_version then
191 print_error "Handshake Error: client version is different '{client_version}'"
192 socket.write_string ""
193 socket.close
194 return false
195 end
196
197 socket.write_string app_version
198
199 return true
200 end
201 end