gamnit: move up UDP discovery logic from Tinks! to the lib
[nit.git] / lib / gamnit / network / client.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 # Client-side network services for games and such
16 #
17 # The following code implements a client to connect to a local server and
18 # briefly exchange with it.
19 #
20 # ~~~
21 # redef fun handshake_app_name do return "nitwork_test"
22 # redef fun handshake_app_version do return "1.0"
23 #
24 # # Prepare connection with remote server
25 # var config = new RemoteServerConfig("localhost", 4444)
26 # var server = new RemoteServer(config)
27 #
28 # # Try to connect
29 # if not server.connect then return
30 #
31 # # Make sure the server is compatible
32 # if not server.handshake then return
33 #
34 # # Connection up! communicate
35 # server.writer.serialize "hello server"
36 # print server.reader.deserialize.as(Object)
37 #
38 # # Done, close socket
39 # server.socket.close
40 # ~~~
41 module client
42
43 intrude import common
44
45 # Information of the remove server
46 class RemoteServerConfig
47
48 # Address of the remote server, either a domain name or an Internet address
49 var address: Text
50
51 # Listening port of the server
52 var port: Int
53 end
54
55 # Connection to a remote server
56 class RemoteServer
57
58 # `RemoteServerConfig` used to initiate connection to the server
59 var config: RemoteServerConfig
60
61 # Communication socket with the server
62 var socket: nullable TCPStream = null
63
64 # Is this connection connected?
65 fun connected: Bool do return socket != null and socket.connected == true
66
67 # `BinarySerializer` used to send data to this client through `socket`
68 var writer: BinarySerializer is noinit
69
70 # `BinaryDeserializer` used to receive data from this client through `socket`
71 var reader: BinaryDeserializer is noinit
72
73 # Attempt connection with the remote server
74 fun connect: Bool
75 do
76 print "Connecting to {config.address}:{config.port}..."
77 var socket = new TCPStream.connect(config.address.to_s, config.port)
78 self.socket = socket
79
80 if not socket.connected then
81 print "Connection failed: {socket.last_error or else "Internal error"}"
82 return false
83 end
84
85 # Setup serialization
86 writer = new BinarySerializer(socket)
87 writer.cache = new AsyncCache(false)
88 reader = new BinaryDeserializer(socket)
89 writer.link reader
90
91 return true
92 end
93
94 # Attempt handshake with server
95 #
96 # Validates compatibility between `handshake_app_name` and `handshake_app_version`.
97 #
98 # On error, close `socket`.
99 fun handshake: Bool
100 do
101 # The client goes first so that the server doesn't show its hand
102 var socket = socket
103 assert socket != null
104
105 # App name
106 var app_name = sys.handshake_app_name
107 socket.write_string app_name
108
109 var server_app = socket.read_string
110 if server_app != app_name then
111 print_error "Handshake Error: server app name is '{server_app}'"
112 socket.close
113 return false
114 end
115
116 # App version
117 socket.write_string sys.handshake_app_version
118
119 var server_version = socket.read_string
120 if server_version != sys.handshake_app_version then
121 print_error "Handshake Error: server version is different '{server_version}'"
122 socket.close
123 return false
124 end
125
126 return true
127 end
128 end
129
130 # Discover local servers responding on UDP `discovery_port`
131 #
132 # Sends a message in the format `gamnit::network? handshake_app_name` and
133 # looks for the response `gamnit::network! handshake_app_name port_number`.
134 # Waits for `timeout`, or the default 0.1 seconds, after sending the message.
135 #
136 # The server usually responds using the method `answer_discovery_requests`.
137 # When receiving responses, the client may then choose a server and
138 # connect via `new RemoteServer`.
139 #
140 # ~~~
141 # var servers = discover_local_servers
142 # if servers.not_empty then
143 # var server = new RemoteServer(servers.first)
144 # server.connect
145 # server.writer.serialize "hello server"
146 # server.socket.close
147 # end
148 # ~~~
149 fun discover_local_servers(timeout: nullable Float): Array[RemoteServerConfig]
150 do
151 timeout = timeout or else 0.1
152
153 var s = new UDPSocket
154 s.enable_broadcast = true
155 s.blocking = false
156 s.broadcast(discovery_port, "{discovery_request_message} {handshake_app_name}")
157 timeout.sleep
158
159 var r = new Array[RemoteServerConfig]
160 loop
161 var ptr = new Ref[nullable SocketAddress](null)
162 var resp = s.recv_from(1024, ptr)
163 var src = ptr.item
164
165 if resp.is_empty then
166 # No response
167 break
168 else
169 assert src != null
170 var words = resp.split(" ")
171 if words.length == 3 and words[0] == discovery_response_message and
172 words[1] == handshake_app_name and words[2].is_int then
173 var address = src.address
174 var port = words[2].to_i
175 r.add new RemoteServerConfig(address, port)
176 end
177 end
178 end
179 return r
180 end