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