From 8eae4527f39bb98cdd64df1322e1e8f568033d30 Mon Sep 17 00:00:00 2001 From: PhilipRoman Date: Thu, 14 Nov 2024 15:44:52 +0200 Subject: [PATCH 1/2] Add support for creating server from existing Channel See #1440 The main motivation for this feature is the ability to integrate with on-demand socket activation. --- .../server/WebSocketServer.java | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/java_websocket/server/WebSocketServer.java b/src/main/java/org/java_websocket/server/WebSocketServer.java index e4f8790e..8d11bcf4 100644 --- a/src/main/java/org/java_websocket/server/WebSocketServer.java +++ b/src/main/java/org/java_websocket/server/WebSocketServer.java @@ -181,6 +181,30 @@ public WebSocketServer(InetSocketAddress address, int decodercount, List this(address, decodercount, drafts, new HashSet()); } + // Small internal helper function to get around limitations of Java constructors. + private static InetSocketAddress checkAddressOfExistingChannel(ServerSocketChannel existingChannel) { + assert existingChannel.isOpen(); + SocketAddress addr; + try { + addr = existingChannel.getLocalAddress(); + } catch (IOException e) { + throw new IllegalArgumentException("Could not get address of channel passed to WebSocketServer, make sure it is bound", e); + } + if (addr == null) { + throw new IllegalArgumentException("Could not get address of channel passed to WebSocketServer, make sure it is bound"); + } + return (InetSocketAddress)addr; + } + + /** + * @param existingChannel An already open and bound server socket channel, which this server will use. + * For example, it can be System.inheritedChannel() to implement socket activation. + */ + public WebSocketServer(ServerSocketChannel existingChannel) { + this(checkAddressOfExistingChannel(existingChannel)); + this.server = existingChannel; + } + /** * Creates a WebSocketServer that will attempt to bind/listen on the given address, and * comply with Draft version draft. @@ -575,7 +599,10 @@ private void doWrite(SelectionKey key) throws WrappedIOException { private boolean doSetupSelectorAndServerThread() { selectorthread.setName("WebSocketSelector-" + selectorthread.getId()); try { - server = ServerSocketChannel.open(); + if (server == null) { + server = ServerSocketChannel.open(); + // If 'server' is not null, that means WebSocketServer was created from existing channel. + } server.configureBlocking(false); ServerSocket socket = server.socket(); int receiveBufferSize = getReceiveBufferSize(); @@ -583,7 +610,11 @@ private boolean doSetupSelectorAndServerThread() { socket.setReceiveBufferSize(receiveBufferSize); } socket.setReuseAddress(isReuseAddr()); - socket.bind(address, getMaxPendingConnections()); + // Socket may be already bound, if an existing channel was passed to constructor. + // In this case we cannot modify backlog size from pure Java code, so leave it as is. + if (!socket.isBound()) { + socket.bind(address, getMaxPendingConnections()); + } selector = Selector.open(); server.register(selector, server.validOps()); startConnectionLostTimer(); From e5253dc29af0c5640142cb9d0fb4c1c4cbd07c2d Mon Sep 17 00:00:00 2001 From: PhilipRoman Date: Thu, 14 Nov 2024 15:47:26 +0200 Subject: [PATCH 2/2] Add example of using WebSocketServer with systemd socket activation --- src/main/example/SocketActivation.java | 102 ++++++++++++++++++++++++ src/main/example/jws-activation.service | 17 ++++ src/main/example/jws-activation.socket | 9 +++ 3 files changed, 128 insertions(+) create mode 100644 src/main/example/SocketActivation.java create mode 100644 src/main/example/jws-activation.service create mode 100644 src/main/example/jws-activation.socket diff --git a/src/main/example/SocketActivation.java b/src/main/example/SocketActivation.java new file mode 100644 index 00000000..86b0da63 --- /dev/null +++ b/src/main/example/SocketActivation.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2010-2020 Nathan Rajlich + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation + * files (the "Software"), to deal in the Software without + * restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following + * conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES + * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT + * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR + * OTHER DEALINGS IN THE SOFTWARE. + */ + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ServerSocketChannel; +import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; +import org.java_websocket.WebSocket; +import org.java_websocket.drafts.Draft; +import org.java_websocket.drafts.Draft_6455; +import org.java_websocket.handshake.ClientHandshake; +import org.java_websocket.server.WebSocketServer; + +/** + * This is a "smart" chat server which will exit when no more clients are left, in order to demonstrate socket activation + */ +public class SocketActivation extends WebSocketServer { + + AtomicInteger clients = new AtomicInteger(0); + + public SocketActivation(ServerSocketChannel chan) { + super(chan); + } + + @Override + public void onOpen(WebSocket conn, ClientHandshake handshake) { + conn.send("Welcome to the server!"); //This method sends a message to the new client + broadcast("new connection: " + handshake.getResourceDescriptor()); //This method sends a message to all clients connected + if(clients.get() == 0) { + broadcast("You are the first client to join"); + } + System.out.println(conn.getRemoteSocketAddress().getAddress().getHostAddress() + " entered the room!"); + clients.incrementAndGet(); + } + + @Override + public void onClose(WebSocket conn, int code, String reason, boolean remote) { + broadcast(conn + " has left the room!"); + System.out.println(conn + " has left the room!"); + if(clients.decrementAndGet() <= 0) { + System.out.println("No more clients left, exiting"); + System.exit(0); + } + } + + @Override + public void onMessage(WebSocket conn, String message) { + broadcast(message); + System.out.println(conn + ": " + message); + } + + @Override + public void onMessage(WebSocket conn, ByteBuffer message) { + broadcast(message.array()); + System.out.println(conn + ": " + message); + } + + + public static void main(String[] args) throws InterruptedException, IOException { + if(System.inheritedChannel() == null) { + System.err.println("System.inheritedChannel() is null, make sure this program is started with file descriptor zero being a listening socket"); + System.exit(1); + } + SocketActivation s = new SocketActivation((ServerSocketChannel)System.inheritedChannel()); + s.start(); + System.out.println(">>>> SocketActivation started on port: " + s.getPort() + " <<<<"); + } + + @Override + public void onError(WebSocket conn, Exception ex) { + ex.printStackTrace(); + } + + @Override + public void onStart() { + System.out.println("Server started!"); + } + +} diff --git a/src/main/example/jws-activation.service b/src/main/example/jws-activation.service new file mode 100644 index 00000000..0ae3d091 --- /dev/null +++ b/src/main/example/jws-activation.service @@ -0,0 +1,17 @@ +[Unit] +Description=Java-WebSocket systemd activation demo service +After=network.target jws-activation.socket +Requires=jws-activation.socket + +[Service] +Type=simple +# Place the command for running SocketActivation.java in file "$HOME"/jws_activation_command: +ExecStart=/bin/sh %h/jws_activation_run +TimeoutStopSec=5 +StandardError=journal +StandardOutput=journal +# This is very important - systemd will pass the socket as file descriptor zero, which is what Java expects +StandardInput=socket + +[Install] +WantedBy=default.target diff --git a/src/main/example/jws-activation.socket b/src/main/example/jws-activation.socket new file mode 100644 index 00000000..db769c3e --- /dev/null +++ b/src/main/example/jws-activation.socket @@ -0,0 +1,9 @@ +[Unit] +Description=Java-WebSocket systemd activation demo socket +PartOf=jws-activation.service + +[Socket] +ListenStream=127.0.0.1:9999 + +[Install] +WantedBy=sockets.target