libevent: rename `bind_to` to the more precise `bind_tcp`
[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
66 do
67 var socket = socket
68 return socket != null and socket.connected
69 end
70
71 # `MsgPackSerializer` used to send data to this client through `socket`
72 var writer: MsgPackSerializer is noinit
73
74 # `MsgPackDeserializer` used to receive data from this client through `socket`
75 var reader: MsgPackDeserializer is noinit
76
77 # Attempt connection with the remote server
78 fun connect: Bool
79 do
80 print "Connecting to {config.address}:{config.port}..."
81 var socket = new TCPStream.connect(config.address.to_s, config.port)
82 self.socket = socket
83
84 if not socket.connected then
85 print "Connection failed: {socket.last_error or else "Internal error"}"
86 return false
87 end
88
89 # Setup serialization
90 writer = new MsgPackSerializer(socket)
91 writer.cache = new AsyncCache(false)
92 reader = new MsgPackDeserializer(socket)
93 writer.link reader
94
95 return true
96 end
97
98 # Attempt handshake with server
99 #
100 # Validates compatibility between `handshake_app_name` and `handshake_app_version`.
101 #
102 # On error, close `socket`.
103 fun handshake: Bool
104 do
105 # The client goes first so that the server doesn't show its hand
106 var socket = socket
107 assert socket != null
108
109 # App name
110 var app_name = sys.handshake_app_name
111 socket.serialize_msgpack app_name
112
113 var server_app = socket.deserialize_msgpack("String")
114 if server_app != app_name then
115 print_error "Handshake Error: server app name is '{server_app or else "<invalid>"}'"
116 socket.close
117 return false
118 end
119
120 # App version
121 socket.serialize_msgpack sys.handshake_app_version
122
123 var server_version = socket.deserialize_msgpack("String")
124 if server_version != sys.handshake_app_version then
125 print_error "Handshake Error: server version is different '{server_version or else "<invalid>"}'"
126 socket.close
127 return false
128 end
129
130 return true
131 end
132 end
133
134 # Discover local servers responding on UDP `discovery_port`
135 #
136 # Sends a message in the format `gamnit::network? handshake_app_name` and
137 # looks for the response `gamnit::network! handshake_app_name port_number`.
138 # Waits for `timeout`, or the default 0.1 seconds, after sending the message.
139 #
140 # The server usually responds using the method `answer_discovery_requests`.
141 # When receiving responses, the client may then choose a server and
142 # connect via `new RemoteServer`.
143 #
144 # ~~~
145 # var servers = discover_local_servers
146 # if servers.not_empty then
147 # var server = new RemoteServer(servers.first)
148 # server.connect
149 # server.writer.serialize "hello server"
150 # server.socket.close
151 # end
152 # ~~~
153 fun discover_local_servers(timeout: nullable Float): Array[RemoteServerConfig]
154 do
155 timeout = timeout or else 0.1
156
157 var s = new UDPSocket
158 s.enable_broadcast = true
159 s.blocking = false
160 s.broadcast(discovery_port, "{discovery_request_message} {handshake_app_name}")
161 timeout.sleep
162
163 var r = new Array[RemoteServerConfig]
164 loop
165 var ptr = new Ref[nullable SocketAddress](null)
166 var resp = s.recv_from(1024, ptr)
167 var src = ptr.item
168
169 if resp.is_empty then
170 # No response
171 break
172 else
173 assert src != null
174 var words = resp.split(" ")
175 if words.length == 3 and words[0] == discovery_response_message and
176 words[1] == handshake_app_name and words[2].is_int then
177 var address = src.address
178 var port = words[2].to_i
179 r.add new RemoteServerConfig(address, port)
180 end
181 end
182 end
183 return r
184 end