add an ID for the server requests
[fanfix.git] / src / be / nikiroo / utils / serial / server / Server.java
1 package be.nikiroo.utils.serial.server;
2
3 import java.io.IOException;
4 import java.net.ServerSocket;
5 import java.net.Socket;
6 import java.net.UnknownHostException;
7
8 import be.nikiroo.utils.TraceHandler;
9
10 /**
11 * This class implements a simple server that can listen for connections and
12 * send/receive objects.
13 * <p>
14 * Note: this {@link Server} has to be discarded after use (cannot be started
15 * twice).
16 *
17 * @author niki
18 */
19 abstract class Server implements Runnable {
20 protected final String key;
21 protected long id = 0;
22
23 private final String name;
24 private final Object lock = new Object();
25 private final Object counterLock = new Object();
26
27 private ServerSocket ss;
28 private int port;
29
30 private boolean started;
31 private boolean exiting = false;
32 private int counter;
33
34 private long bytesReceived;
35 private long bytesSent;
36
37 private TraceHandler tracer = new TraceHandler();
38
39 /**
40 * Create a new {@link ConnectActionServer} to handle a request.
41 *
42 * @param s
43 * the socket to service
44 *
45 * @return the action
46 */
47 abstract ConnectActionServer createConnectActionServer(Socket s);
48
49 /**
50 * Create a new server that will start listening on the network when
51 * {@link Server#start()} is called.
52 *
53 * @param port
54 * the port to listen on, or 0 to assign any unallocated port
55 * found (which can later on be queried via
56 * {@link Server#getPort()}
57 * @param key
58 * an optional key to encrypt all the communications (if NULL,
59 * everything will be sent in clear text)
60 *
61 * @throws IOException
62 * in case of I/O error
63 * @throws UnknownHostException
64 * if the IP address of the host could not be determined
65 * @throws IllegalArgumentException
66 * if the port parameter is outside the specified range of valid
67 * port values, which is between 0 and 65535, inclusive
68 */
69 public Server(int port, String key) throws IOException {
70 this((String) null, port, key);
71 }
72
73 /**
74 * Create a new server that will start listening on the network when
75 * {@link Server#start()} is called.
76 * <p>
77 * All the communications will happen in plain text.
78 *
79 * @param name
80 * the server name (only used for debug info and traces)
81 * @param port
82 * the port to listen on
83 *
84 * @throws IOException
85 * in case of I/O error
86 * @throws UnknownHostException
87 * if the IP address of the host could not be determined
88 * @throws IllegalArgumentException
89 * if the port parameter is outside the specified range of valid
90 * port values, which is between 0 and 65535, inclusive
91 */
92 public Server(String name, int port) throws IOException {
93 this(name, port, null);
94 }
95
96 /**
97 * Create a new server that will start listening on the network when
98 * {@link Server#start()} is called.
99 *
100 * @param name
101 * the server name (only used for debug info and traces)
102 * @param port
103 * the port to listen on
104 * @param key
105 * an optional key to encrypt all the communications (if NULL,
106 * everything will be sent in clear text)
107 *
108 * @throws IOException
109 * in case of I/O error
110 * @throws UnknownHostException
111 * if the IP address of the host could not be determined
112 * @throws IllegalArgumentException
113 * if the port parameter is outside the specified range of valid
114 * port values, which is between 0 and 65535, inclusive
115 */
116 public Server(String name, int port, String key) throws IOException {
117 this.name = name;
118 this.port = port;
119 this.key = key;
120 this.ss = new ServerSocket(port);
121
122 if (this.port == 0) {
123 this.port = this.ss.getLocalPort();
124 }
125 }
126
127 /**
128 * The traces handler for this {@link Server}.
129 *
130 * @return the traces handler
131 */
132 public TraceHandler getTraceHandler() {
133 return tracer;
134 }
135
136 /**
137 * The traces handler for this {@link Server}.
138 *
139 * @param tracer
140 * the new traces handler
141 */
142 public void setTraceHandler(TraceHandler tracer) {
143 if (tracer == null) {
144 tracer = new TraceHandler(false, false, false);
145 }
146
147 this.tracer = tracer;
148 }
149
150 /**
151 * The name of this {@link Server} if any.
152 * <p>
153 * Used for traces and debug purposes only.
154 *
155 * @return the name or NULL
156 */
157 public String getName() {
158 return name;
159 }
160
161 /**
162 * Return the assigned port.
163 *
164 * @return the assigned port
165 */
166 public int getPort() {
167 return port;
168 }
169
170 /**
171 * The total amount of bytes received.
172 *
173 * @return the amount of bytes received
174 */
175 public long getBytesReceived() {
176 return bytesReceived;
177 }
178
179 /**
180 * The total amount of bytes sent.
181 *
182 * @return the amount of bytes sent
183 */
184 public long getBytesSent() {
185 return bytesSent;
186 }
187
188 /**
189 * Start the server (listen on the network for new connections).
190 * <p>
191 * Can only be called once.
192 * <p>
193 * This call is asynchronous, and will just start a new {@link Thread} on
194 * itself (see {@link Server#run()}).
195 */
196 public void start() {
197 new Thread(this).start();
198 }
199
200 /**
201 * Start the server (listen on the network for new connections).
202 * <p>
203 * Can only be called once.
204 * <p>
205 * You may call it via {@link Server#start()} for an asynchronous call, too.
206 */
207 @Override
208 public void run() {
209 ServerSocket ss = null;
210 boolean alreadyStarted = false;
211 synchronized (lock) {
212 ss = this.ss;
213 if (!started && ss != null) {
214 started = true;
215 } else {
216 alreadyStarted = started;
217 }
218 }
219
220 if (alreadyStarted) {
221 tracer.error(name + ": cannot start server on port " + port
222 + ", it is already started");
223 return;
224 }
225
226 if (ss == null) {
227 tracer.error(name + ": cannot start server on port " + port
228 + ", it has already been used");
229 return;
230 }
231
232 try {
233 tracer.trace(name + ": server starting on port " + port + " ("
234 + (key != null ? "encrypted" : "plain text") + ")");
235
236 while (started && !exiting) {
237 count(1);
238 final Socket s = ss.accept();
239 new Thread(new Runnable() {
240 @Override
241 public void run() {
242 ConnectActionServer action = null;
243 try {
244 action = createConnectActionServer(s);
245 action.connect();
246 } finally {
247 count(-1);
248 if (action != null) {
249 bytesReceived += action.getBytesReceived();
250 bytesSent += action.getBytesSent();
251 }
252 }
253 }
254 }).start();
255 }
256
257 // Will be covered by @link{Server#stop(long)} for timeouts
258 while (counter > 0) {
259 Thread.sleep(10);
260 }
261 } catch (Exception e) {
262 if (counter > 0) {
263 onError(e);
264 }
265 } finally {
266 try {
267 ss.close();
268 } catch (Exception e) {
269 onError(e);
270 }
271
272 this.ss = null;
273
274 started = false;
275 exiting = false;
276 counter = 0;
277
278 tracer.trace(name + ": client terminated on port " + port);
279 }
280 }
281
282 /**
283 * Will stop the server, synchronously and without a timeout.
284 */
285 public void stop() {
286 tracer.trace(name + ": stopping server");
287 stop(0, true);
288 }
289
290 /**
291 * Stop the server.
292 *
293 * @param timeout
294 * the maximum timeout to wait for existing actions to complete,
295 * or 0 for "no timeout"
296 * @param wait
297 * wait for the server to be stopped before returning
298 * (synchronous) or not (asynchronous)
299 */
300 public void stop(final long timeout, final boolean wait) {
301 if (wait) {
302 stop(timeout);
303 } else {
304 new Thread(new Runnable() {
305 @Override
306 public void run() {
307 stop(timeout);
308 }
309 }).start();
310 }
311 }
312
313 /**
314 * Stop the server (synchronous).
315 *
316 * @param timeout
317 * the maximum timeout to wait for existing actions to complete,
318 * or 0 for "no timeout"
319 */
320 private void stop(long timeout) {
321 tracer.trace(name + ": server stopping on port " + port);
322 synchronized (lock) {
323 if (started && !exiting) {
324 exiting = true;
325
326 try {
327 new ConnectActionClientObject(new Socket((String) null,
328 port), key).connect();
329 long time = 0;
330 while (ss != null && timeout > 0 && timeout > time) {
331 Thread.sleep(10);
332 time += 10;
333 }
334 } catch (Exception e) {
335 if (ss != null) {
336 counter = 0; // will stop the main thread
337 onError(e);
338 }
339 }
340 }
341 }
342
343 // return only when stopped
344 while (started || exiting) {
345 try {
346 Thread.sleep(10);
347 } catch (InterruptedException e) {
348 }
349 }
350 }
351
352 /**
353 * Change the number of currently serviced actions.
354 *
355 * @param change
356 * the number to increase or decrease
357 *
358 * @return the current number after this operation
359 */
360 private int count(int change) {
361 synchronized (counterLock) {
362 counter += change;
363 return counter;
364 }
365 }
366
367 /**
368 * This method will be called on errors.
369 * <p>
370 * By default, it will only call the trace handler (so you may want to call
371 * super {@link Server#onError} if you override it).
372 *
373 * @param e
374 * the error
375 */
376 protected void onError(Exception e) {
377 tracer.error(e);
378 }
379
380 /**
381 * Return the next ID to use.
382 *
383 * @return the next ID
384 */
385 protected synchronized long getNextId() {
386 return id++;
387 }
388
389 /**
390 * Method called when
391 * {@link ServerObject#onRequest(ConnectActionServerObject, Object, long)}
392 * has successfully finished.
393 * <p>
394 * Can be used to know how much data was transmitted.
395 *
396 * @param id
397 * the ID used to identify the request
398 * @param bytesReceived
399 * the bytes received during the request
400 * @param bytesSent
401 * the bytes sent during the request
402 */
403 @SuppressWarnings("unused")
404 protected void onRequestDone(long id, long bytesReceived, long bytesSent) {
405 }
406
407 /**
408 * Create a {@link Socket}.
409 *
410 * @param host
411 * the host to connect to
412 * @param port
413 * the port to connect to
414 *
415 * @return the {@link Socket}
416 *
417 * @throws IOException
418 * in case of I/O error
419 * @throws UnknownHostException
420 * if the host is not known
421 * @throws IllegalArgumentException
422 * if the port parameter is outside the specified range of valid
423 * port values, which is between 0 and 65535, inclusive
424 */
425 @Deprecated
426 static Socket createSocket(String host, int port) throws IOException {
427 return new Socket(host, port);
428 }
429
430 /**
431 * Create a {@link ServerSocket}.
432 *
433 * @param port
434 * the port to accept connections on
435 *
436 * @return the {@link ServerSocket}
437 *
438 * @throws IOException
439 * in case of I/O error
440 * @throws UnknownHostException
441 * if the IP address of the host could not be determined
442 * @throws IllegalArgumentException
443 * if the port parameter is outside the specified range of valid
444 * port values, which is between 0 and 65535, inclusive
445 */
446 @Deprecated
447 static ServerSocket createSocketServer(int port) throws IOException {
448 return new ServerSocket(port);
449 }
450 }