View Javadoc
1   /*
2    * junixsocket
3    *
4    * Copyright 2009-2026 Christian Kohlschütter
5    *
6    * Licensed under the Apache License, Version 2.0 (the "License");
7    * you may not use this file except in compliance with the License.
8    * You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing, software
13   * distributed under the License is distributed on an "AS IS" BASIS,
14   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15   * See the License for the specific language governing permissions and
16   * limitations under the License.
17   */
18  package org.newsclub.net.unix.server;
19  
20  import java.io.IOException;
21  import java.io.InterruptedIOException;
22  import java.net.ServerSocket;
23  import java.net.Socket;
24  import java.net.SocketAddress;
25  import java.net.SocketException;
26  import java.net.SocketTimeoutException;
27  import java.util.Objects;
28  import java.util.concurrent.Callable;
29  import java.util.concurrent.ExecutorService;
30  import java.util.concurrent.Executors;
31  import java.util.concurrent.ForkJoinPool;
32  import java.util.concurrent.Future;
33  import java.util.concurrent.ScheduledExecutorService;
34  import java.util.concurrent.ScheduledFuture;
35  import java.util.concurrent.TimeUnit;
36  import java.util.concurrent.atomic.AtomicBoolean;
37  import java.util.concurrent.atomic.AtomicInteger;
38  
39  import org.eclipse.jdt.annotation.NonNull;
40  import org.newsclub.net.unix.AFServerSocket;
41  import org.newsclub.net.unix.AFSocketAddress;
42  
43  import com.kohlschutter.annotations.compiletime.SuppressFBWarnings;
44  import com.kohlschutter.annotations.compiletime.SuppressLint;
45  
46  /**
47   * A base implementation for a simple, multi-threaded socket server.
48   *
49   * @author Christian Kohlschütter
50   * @see AFSocketServer
51   * @param <A> The supported address type.
52   * @param <S> The supported {@link Socket} type.
53   * @param <V> The supported {@link ServerSocket} type.
54   */
55  public abstract class SocketServer<A extends SocketAddress, S extends Socket, V extends ServerSocket> {
56    private static final ScheduledExecutorService TIMEOUTS = Executors.newScheduledThreadPool(1);
57  
58    private final @NonNull A listenAddress;
59  
60    private int maxConcurrentConnections = Runtime.getRuntime().availableProcessors();
61    private int serverTimeout = 0; // by default, the server doesn't timeout.
62    private final AtomicInteger socketTimeout = new AtomicInteger((int) TimeUnit.SECONDS.toMillis(
63        60));
64    private final AtomicInteger serverBusyTimeout = new AtomicInteger((int) TimeUnit.SECONDS.toMillis(
65        1));
66  
67    private Thread listenThread = null;
68    private V serverSocket;
69    private final AtomicBoolean stopRequested = new AtomicBoolean(false);
70    private final AtomicBoolean ready = new AtomicBoolean(false);
71  
72    private final Object connectionsMonitor = new Object();
73    private ForkJoinPool connectionPool;
74  
75    private ScheduledFuture<IOException> timeoutFuture;
76    private final V reuseSocket;
77  
78    /**
79     * Creates a server using the given, bound {@link ServerSocket}.
80     *
81     * @param serverSocket The server socket to use (must be bound).
82     */
83    @SuppressWarnings("all") // unchecked, null
84    @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
85    public SocketServer(V serverSocket) {
86      this((A) Objects.requireNonNull(serverSocket).getLocalSocketAddress(), serverSocket);
87    }
88  
89    /**
90     * Creates a server using the given {@link SocketAddress}.
91     *
92     * @param listenAddress The address to bind the socket on.
93     */
94    @SuppressWarnings("null")
95    @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
96    public SocketServer(A listenAddress) {
97      this(listenAddress, null);
98    }
99  
100   @SuppressWarnings("null")
101   @SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
102   private SocketServer(A listenAddress, V preboundSocket) {
103     Objects.requireNonNull(listenAddress, "listenAddress");
104     this.reuseSocket = preboundSocket;
105 
106     this.listenAddress = listenAddress;
107   }
108 
109   /**
110    * Returns the maximum number of concurrent connections.
111    *
112    * @return The maximum number of concurrent connections.
113    */
114   public int getMaxConcurrentConnections() {
115     return maxConcurrentConnections;
116   }
117 
118   /**
119    * Sets the maximum number of concurrent connections.
120    *
121    * @param maxConcurrentConnections The new maximum.
122    */
123   @SuppressFBWarnings("AT_STALE_THREAD_WRITE_OF_PRIMITIVE")
124   public void setMaxConcurrentConnections(int maxConcurrentConnections) {
125     if (isRunning()) {
126       throw new IllegalStateException("Already configured");
127     }
128     this.maxConcurrentConnections = maxConcurrentConnections;
129   }
130 
131   /**
132    * Returns the server timeout (in milliseconds).
133    *
134    * @return The server timeout in milliseconds (0 = no timeout).
135    */
136   public int getServerTimeout() {
137     return serverTimeout;
138   }
139 
140   /**
141    * Sets the server timeout (in milliseconds).
142    *
143    * @param timeout The new timeout in milliseconds (0 = no timeout).
144    */
145   @SuppressFBWarnings("AT_STALE_THREAD_WRITE_OF_PRIMITIVE")
146   public void setServerTimeout(int timeout) {
147     if (isRunning()) {
148       throw new IllegalStateException("Already configured");
149     }
150     this.serverTimeout = timeout;
151   }
152 
153   /**
154    * Returns the socket timeout (in milliseconds).
155    *
156    * @return The socket timeout in milliseconds (0 = no timeout).
157    */
158   public int getSocketTimeout() {
159     return socketTimeout.get();
160   }
161 
162   /**
163    * Sets the socket timeout (in milliseconds).
164    *
165    * @param timeout The new timeout in milliseconds (0 = no timeout).
166    */
167   public void setSocketTimeout(int timeout) {
168     this.socketTimeout.set(timeout);
169   }
170 
171   /**
172    * Returns the server-busy timeout (in milliseconds).
173    *
174    * @return The server-busy timeout in milliseconds (0 = no timeout).
175    */
176   public int getServerBusyTimeout() {
177     return serverBusyTimeout.get();
178   }
179 
180   /**
181    * Sets the server-busy timeout (in milliseconds).
182    *
183    * @param timeout The new timeout in milliseconds (0 = no timeout).
184    */
185   public void setServerBusyTimeout(int timeout) {
186     this.serverBusyTimeout.set(timeout);
187   }
188 
189   /**
190    * Checks if the server is running.
191    *
192    * @return {@code true} if the server is alive.
193    */
194   public boolean isRunning() {
195     synchronized (this) {
196       return (listenThread != null && listenThread.isAlive());
197     }
198   }
199 
200   /**
201    * Checks if the server is running and accepting new connections.
202    *
203    * @return {@code true} if the server is alive and ready to accept new connections.
204    */
205   public boolean isReady() {
206     return ready.get() && !stopRequested.get() && isRunning();
207   }
208 
209   /**
210    * Starts the server, and returns immediately.
211    *
212    * @see #startAndWaitToBecomeReady(long, TimeUnit)
213    */
214   public void start() {
215     synchronized (this) {
216       if (isRunning()) {
217         return;
218       }
219       if (connectionPool == null) {
220         connectionPool = new ForkJoinPool(maxConcurrentConnections,
221             ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true);
222       }
223 
224       @SuppressWarnings("deprecation")
225       Thread t = new Thread(() -> {
226         try {
227           listen();
228         } catch (Exception e) {
229           onListenException(e);
230         } catch (Throwable e) { // NOPMD
231           onListenException(e);
232         }
233       }, SocketServer.this.toString() + " listening thread");
234       t.start();
235 
236       listenThread = t;
237     }
238 
239   }
240 
241   /**
242    * Starts the server and waits until it is ready or had to stop due to an error.
243    *
244    * @throws InterruptedException If the wait was interrupted.
245    */
246   public void startAndWaitToBecomeReady() throws InterruptedException {
247     synchronized (this) {
248       start();
249       while (!ready.get() && !stopRequested.get()) {
250         this.wait(1000);
251       }
252     }
253   }
254 
255   /**
256    * Starts the server and waits until it is ready or had to stop due to an error.
257    *
258    * @param duration The duration wait.
259    * @param unit The duration's time unit.
260    * @return {@code true} if the server is ready to serve requests.
261    * @throws InterruptedException If the wait was interrupted.
262    */
263   public boolean startAndWaitToBecomeReady(long duration, TimeUnit unit)
264       throws InterruptedException {
265     synchronized (this) {
266       start();
267       long timeStart = System.currentTimeMillis();
268       while (duration > 0) {
269         if (isReady()) {
270           return true;
271         }
272         this.wait(unit.toMillis(duration));
273         duration -= (System.currentTimeMillis() - timeStart);
274       }
275       return isReady();
276     }
277   }
278 
279   /**
280    * Returns a new server socket.
281    *
282    * @return The new socket (an {@link AFServerSocket} if the listen address is an
283    *         {@link AFSocketAddress}).
284    * @throws IOException on error.
285    */
286   protected abstract V newServerSocket() throws IOException;
287 
288   @SuppressWarnings("null")
289   private void listen() throws IOException {
290     V server = null;
291     try {
292       synchronized (this) {
293         if (reuseSocket != null) {
294           server = reuseSocket;
295         } else {
296           server = null;
297         }
298       }
299       if (server == null) {
300         server = newServerSocket();
301       }
302       synchronized (this) {
303         if (serverSocket != null) {
304           throw new IllegalStateException("The server is already listening");
305         }
306         serverSocket = server;
307       }
308       onServerStarting();
309 
310       if (!server.isBound()) {
311         server.bind(listenAddress);
312         onServerBound(listenAddress);
313       }
314       server.setSoTimeout(serverTimeout);
315 
316       acceptLoop(server);
317     } catch (SocketException e) {
318       onSocketExceptionDuringAccept(e);
319     } finally {
320       stop();
321       onServerStopped(server);
322     }
323   }
324 
325   @SuppressWarnings("PMD.CognitiveComplexity")
326   @SuppressFBWarnings("NN_NAKED_NOTIFY")
327   @SuppressLint("RESOURCE_LEAK")
328   private void acceptLoop(V server) throws IOException {
329     long busyStartTime = 0;
330     acceptLoop : while (!stopRequested.get() && !Thread.interrupted()) {
331       try {
332         while (!stopRequested.get() && connectionPool
333             .getActiveThreadCount() >= maxConcurrentConnections) {
334           if (busyStartTime == 0) {
335             busyStartTime = System.currentTimeMillis();
336           }
337           onServerBusy(busyStartTime);
338 
339           synchronized (connectionsMonitor) {
340             try {
341               connectionsMonitor.wait(getServerBusyTimeout());
342             } catch (InterruptedException e) {
343               throw (InterruptedIOException) new InterruptedIOException(
344                   "Interrupted while waiting on server resources").initCause(e);
345             }
346           }
347         }
348         busyStartTime = 0;
349 
350         if (stopRequested.get() || server == null) {
351           break;
352         }
353 
354         synchronized (SocketServer.this) {
355           SocketServer.this.notifyAll();
356         }
357         ready.set(true);
358         onServerReady(connectionPool.getActiveThreadCount());
359 
360         final S socket;
361         try {
362           @SuppressWarnings("unchecked")
363           S theSocket = (S) server.accept();
364           socket = theSocket;
365         } catch (SocketException e) {
366           if (server.isClosed()) {
367             // already closed, ignore
368             break acceptLoop;
369           } else {
370             throw e;
371           }
372         }
373         try {
374           socket.setSoTimeout(getSocketTimeout());
375         } catch (SocketException e) {
376           // Connection closed before we could do anything
377           onSocketExceptionAfterAccept(socket, e);
378           socket.close();
379 
380           continue acceptLoop;
381         }
382 
383         onSubmitted(socket, submit(socket, connectionPool));
384       } catch (SocketTimeoutException e) {
385         if (!connectionPool.isQuiescent()) {
386           continue acceptLoop;
387         } else {
388           onServerShuttingDown();
389           connectionPool.shutdown();
390           break acceptLoop;
391         }
392       }
393     }
394   }
395 
396   /**
397    * Stops the server.
398    *
399    * @throws IOException If there was an error.
400    */
401   @SuppressWarnings("null")
402   public void stop() throws IOException {
403     stopRequested.set(true);
404     ready.set(false);
405 
406     synchronized (this) {
407       V theServerSocket = serverSocket;
408       serverSocket = null;
409       try {
410         if (theServerSocket == null) {
411           return;
412         }
413         ScheduledFuture<IOException> future = this.timeoutFuture;
414         if (future != null) {
415           future.cancel(false);
416           this.timeoutFuture = null;
417         }
418 
419         theServerSocket.close();
420       } finally {
421         SocketServer.this.notifyAll();
422       }
423     }
424   }
425 
426   private Future<?> submit(final S socket, ExecutorService executor) {
427     Objects.requireNonNull(socket);
428     return executor.submit(new Runnable() {
429       @SuppressWarnings("deprecation")
430       @SuppressFBWarnings("NN_NAKED_NOTIFY")
431       @Override
432       public void run() {
433         onBeforeServingSocket(socket);
434 
435         try { // NOPMD
436           doServeSocket(socket);
437         } catch (Exception e) { // NOPMD
438           onServingException(socket, e); // NOPMD
439         } catch (Throwable t) { // NOPMD
440           onServingException(socket, t); // NOPMD
441         } finally {
442           // Notify the server's accept thread that we handled the connection
443           synchronized (connectionsMonitor) {
444             connectionsMonitor.notifyAll();
445           }
446 
447           doSocketClose(socket);
448           onAfterServingSocket(socket);
449         }
450       }
451     });
452   }
453 
454   /**
455    * Called upon closing a socket after serving the connection.
456    * <p>
457    * The default implementation closes the socket directly, ignoring any {@link IOException}s. You
458    * may override this method to close the socket in a separate thread, for example.
459    *
460    * @param socket The socket to close.
461    */
462   @SuppressWarnings("null")
463   protected void doSocketClose(S socket) {
464     try {
465       socket.close();
466     } catch (IOException e) {
467       // ignore
468     }
469   }
470 
471   /**
472    * Requests that the server will be stopped after the given time delay. If the server is not
473    * started yet (and {@link #stop()} was not called yet, it will be started first.
474    *
475    * @param delay The delay.
476    * @param unit The time unit for the delay.
477    * @return A scheduled future that can be used to monitor progress / cancel the request. If there
478    *         was a problem with stopping, an IOException is returned as the value (not thrown). If
479    *         stop was already requested, {@code null} is returned.
480    */
481   public ScheduledFuture<IOException> startThenStopAfter(long delay, TimeUnit unit) {
482     if (stopRequested.get()) {
483       return null;
484     }
485     synchronized (this) {
486       start();
487       ScheduledFuture<?> existingFuture = this.timeoutFuture;
488       if (existingFuture != null) {
489         existingFuture.cancel(false);
490       }
491 
492       return (this.timeoutFuture = TIMEOUTS.schedule(new Callable<IOException>() {
493         @Override
494         public IOException call() throws Exception {
495           try {
496             stop();
497             return null;
498           } catch (IOException e) {
499             return e;
500           }
501         }
502       }, delay, unit));
503     }
504   }
505 
506   /**
507    * Called when a socket is ready to be served.
508    *
509    * @param socket The socket to serve.
510    * @throws IOException If there was an error.
511    */
512   protected abstract void doServeSocket(S socket) throws IOException;
513 
514   /**
515    * Called when the server is starting up.
516    */
517   protected void onServerStarting() {
518   }
519 
520   /**
521    * Called when the server has been bound to a socket.
522    *
523    * This is not called when you instantiated the server with a pre-bound socket.
524    *
525    * @param address The bound address.
526    */
527   protected void onServerBound(A address) {
528   }
529 
530   /**
531    * Called when the server is ready to accept a new connection.
532    *
533    * @param activeCount The current number of active tasks (= serving sockets).
534    */
535   protected void onServerReady(int activeCount) {
536   }
537 
538   /**
539    * Called when the server is busy / not ready to accept a new connection.
540    *
541    * The frequency on how often this method is called when the server is busy is determined by
542    * {@link #getServerBusyTimeout()}.
543    *
544    * @param busyStartTime The time stamp since the server became busy.
545    */
546   protected void onServerBusy(long busyStartTime) {
547   }
548 
549   /**
550    * Called when the server has been stopped.
551    *
552    * @param socket The server's socket that stopped, or {@code null}.
553    */
554   protected void onServerStopped(V socket) {
555   }
556 
557   /**
558    * Called when a socket gets submitted into the process queue.
559    *
560    * @param socket The socket.
561    * @param submission The {@link Future} referencing the submission; it's "done" after the socket
562    *          has been served.
563    */
564   protected void onSubmitted(S socket, Future<?> submission) {
565   }
566 
567   /**
568    * Called when the server is shutting down.
569    */
570   protected void onServerShuttingDown() {
571   }
572 
573   /**
574    * Called when a {@link SocketException} was thrown during "accept".
575    *
576    * @param e The exception.
577    */
578   protected void onSocketExceptionDuringAccept(SocketException e) {
579   }
580 
581   /**
582    * Called when a {@link SocketException} was thrown during "accept".
583    *
584    * @param socket The socket.
585    * @param e The exception.
586    */
587   protected void onSocketExceptionAfterAccept(S socket, SocketException e) {
588   }
589 
590   /**
591    * Called before serving the socket.
592    *
593    * @param socket The socket.
594    */
595   protected void onBeforeServingSocket(S socket) {
596   }
597 
598   /**
599    * Called when an exception was thrown while serving a socket.
600    *
601    * @param socket The socket.
602    * @param e The exception.
603    * @deprecated Use {@link #onServingException(Socket, Throwable)}
604    * @see #onServingException(Socket, Throwable)
605    */
606   @Deprecated
607   protected void onServingException(S socket, Exception e) {
608     onServingException(socket, (Throwable) e);
609   }
610 
611   /**
612    * Called when a throwable was thrown while serving a socket.
613    *
614    * @param socket The socket.
615    * @param t The throwable.
616    */
617   protected void onServingException(S socket, Throwable t) {
618   }
619 
620   /**
621    * Called after the socket has been served.
622    *
623    * @param socket The socket.
624    */
625   protected void onAfterServingSocket(S socket) {
626   }
627 
628   /**
629    * Called when an exception was thrown while listening on the server socket.
630    *
631    * @param e The exception.
632    * @deprecated Use {@link #onListenException(Throwable)}
633    * @see #onListenException(Throwable)
634    */
635   @Deprecated
636   protected void onListenException(Exception e) {
637     onListenException((Throwable) e);
638   }
639 
640   /**
641    * Called when an exception was thrown while listening on the server socket.
642    *
643    * @param t The throwable.
644    */
645   protected void onListenException(Throwable t) {
646   }
647 
648   /**
649    * Returns the address the server listens to.
650    *
651    * @return The listen address.
652    */
653   protected @NonNull A getListenAddress() {
654     return listenAddress;
655   }
656 }