count the bytes we receive/send
[nikiroo-utils.git] / src / be / nikiroo / utils / serial / server / Server.java
CommitLineData
79ce1a49 1package be.nikiroo.utils.serial.server;
ce0974c4
NR
2
3import java.io.IOException;
4import java.net.ServerSocket;
5import java.net.Socket;
f4053377 6import java.net.UnknownHostException;
ce0974c4
NR
7import java.util.ArrayList;
8import java.util.List;
9
10import javax.net.ssl.SSLServerSocket;
11import javax.net.ssl.SSLServerSocketFactory;
12import javax.net.ssl.SSLSocket;
13import javax.net.ssl.SSLSocketFactory;
14
08a58812 15import be.nikiroo.utils.TraceHandler;
ce0974c4 16
08a58812
NR
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 */
79ce1a49 26abstract class Server implements Runnable {
ce0974c4
NR
27 static private final String[] ANON_CIPHERS = getAnonCiphers();
28
08a58812
NR
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
ce0974c4 34 private ServerSocket ss;
08a58812
NR
35 private int port;
36
ce0974c4
NR
37 private boolean started;
38 private boolean exiting = false;
39 private int counter;
08a58812 40
4bb7e88e
NR
41 private long bytesReceived;
42 private long bytesSent;
43
08a58812 44 private TraceHandler tracer = new TraceHandler();
ce0974c4 45
79ce1a49
NR
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);
cd0c27d2 55
08a58812
NR
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
f4053377
NR
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
08a58812 74 */
cd0c27d2 75 public Server(int port, boolean ssl) throws IOException {
08a58812
NR
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
f4053377
NR
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
08a58812
NR
97 */
98 public Server(String name, int port, boolean ssl) throws IOException {
99 this.name = name;
ce0974c4
NR
100 this.port = port;
101 this.ssl = ssl;
102 this.ss = createSocketServer(port, ssl);
08a58812
NR
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;
ce0974c4
NR
116 }
117
08a58812
NR
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
79ce1a49
NR
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
08a58812
NR
143 /**
144 * Return the assigned port.
f157aed8
NR
145 *
146 * @return the assigned port
08a58812
NR
147 */
148 public int getPort() {
149 return port;
150 }
151
4bb7e88e
NR
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
08a58812
NR
170 /**
171 * Start the server (listen on the network for new connections).
172 * <p>
173 * Can only be called once.
b08aa70e 174 * <p>
d463d266
NR
175 * This call is asynchronous, and will just start a new {@link Thread} on
176 * itself (see {@link Server#run()}).
08a58812 177 */
ce0974c4 178 public void start() {
d463d266 179 new Thread(this).start();
b08aa70e
NR
180 }
181
182 /**
183 * Start the server (listen on the network for new connections).
184 * <p>
185 * Can only be called once.
d463d266
NR
186 * <p>
187 * You may call it via {@link Server#start()} for an asynchronous call, too.
b08aa70e 188 */
d463d266
NR
189 @Override
190 public void run() {
191 ServerSocket ss = null;
192 boolean alreadyStarted = false;
ce0974c4 193 synchronized (lock) {
d463d266 194 ss = this.ss;
08a58812 195 if (!started && ss != null) {
b08aa70e 196 started = true;
d463d266
NR
197 } else {
198 alreadyStarted = started;
ce0974c4
NR
199 }
200 }
08a58812 201
d463d266 202 if (alreadyStarted) {
08a58812
NR
203 tracer.error(name + ": cannot start server on port " + port
204 + ", it is already started");
d463d266
NR
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 {
f4053377
NR
215 tracer.trace(name + ": server starting on port " + port + " ("
216 + (ssl ? "SSL" : "plain text") + ")");
d463d266 217
d9e2136b
N
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
d463d266
NR
227 while (started && !exiting) {
228 count(1);
8537d55a
NR
229 final Socket s = ss.accept();
230 new Thread(new Runnable() {
231 @Override
232 public void run() {
4bb7e88e 233 ConnectActionServer action = null;
8537d55a 234 try {
4bb7e88e
NR
235 action = createConnectActionServer(s);
236 action.connect();
8537d55a
NR
237 } finally {
238 count(-1);
4bb7e88e
NR
239 if (action != null) {
240 bytesReceived += action.getBytesReceived();
241 bytesSent += action.getBytesSent();
242 }
8537d55a
NR
243 }
244 }
245 }).start();
d463d266
NR
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);
08a58812 270 }
ce0974c4
NR
271 }
272
08a58812
NR
273 /**
274 * Will stop the server, synchronously and without a timeout.
275 */
ce0974c4 276 public void stop() {
08a58812 277 tracer.trace(name + ": stopping server");
ce0974c4
NR
278 stop(0, true);
279 }
280
08a58812
NR
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 */
ce0974c4
NR
291 public void stop(final long timeout, final boolean wait) {
292 if (wait) {
293 stop(timeout);
294 } else {
295 new Thread(new Runnable() {
cd0c27d2 296 @Override
ce0974c4
NR
297 public void run() {
298 stop(timeout);
299 }
300 }).start();
301 }
302 }
303
08a58812
NR
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 */
ce0974c4 311 private void stop(long timeout) {
d463d266 312 tracer.trace(name + ": server stopping on port " + port);
ce0974c4
NR
313 synchronized (lock) {
314 if (started && !exiting) {
315 exiting = true;
316
317 try {
79ce1a49 318 new ConnectActionClientObject(createSocket(null, port, ssl))
f157aed8 319 .connect();
ce0974c4
NR
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 }
0988831f 332 }
ce0974c4 333
0988831f
NR
334 // return only when stopped
335 while (started || exiting) {
336 try {
337 Thread.sleep(10);
338 } catch (InterruptedException e) {
ce0974c4
NR
339 }
340 }
341 }
342
08a58812
NR
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 */
8537d55a 351 private int count(int change) {
ce0974c4
NR
352 synchronized (counterLock) {
353 counter += change;
354 return counter;
355 }
356 }
357
8537d55a
NR
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
08a58812
NR
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
f4053377
NR
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
08a58812 390 */
ce0974c4
NR
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);
0988831f
NR
396 if (s instanceof SSLSocket) {
397 // Should always be the case
398 ((SSLSocket) s).setEnabledCipherSuites(ANON_CIPHERS);
399 }
ce0974c4
NR
400 } else {
401 s = new Socket(host, port);
402 }
403
404 return s;
405 }
406
08a58812
NR
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
f4053377
NR
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
08a58812 424 */
ce0974c4
NR
425 static ServerSocket createSocketServer(int port, boolean ssl)
426 throws IOException {
427 ServerSocket ss;
428 if (ssl) {
429 ss = SSLServerSocketFactory.getDefault().createServerSocket(port);
0988831f
NR
430 if (ss instanceof SSLServerSocket) {
431 // Should always be the case
432 ((SSLServerSocket) ss).setEnabledCipherSuites(ANON_CIPHERS);
433 }
ce0974c4
NR
434 } else {
435 ss = new ServerSocket(port);
436 }
437
438 return ss;
439 }
440
08a58812
NR
441 /**
442 * Return all the supported ciphers that do not use authentication.
443 *
444 * @return the list of such supported ciphers
445 */
d9e2136b 446 public static String[] getAnonCiphers() {
ce0974c4
NR
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}