org.java_websocket.server.WebSocketServer Maven / Gradle / Ivy
Show all versions of Java-WebSocket Show documentation
/*
* 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.
*/
package org.java_websocket.server;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import org.java_websocket.AbstractWebSocket;
import org.java_websocket.SocketChannelIOHelper;
import org.java_websocket.WebSocket;
import org.java_websocket.WebSocketFactory;
import org.java_websocket.WebSocketImpl;
import org.java_websocket.WebSocketServerFactory;
import org.java_websocket.WrappedByteChannel;
import org.java_websocket.drafts.Draft;
import org.java_websocket.exceptions.WebsocketNotConnectedException;
import org.java_websocket.exceptions.WrappedIOException;
import org.java_websocket.framing.CloseFrame;
import org.java_websocket.framing.Framedata;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.handshake.Handshakedata;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* WebSocketServer is an abstract class that only takes care of the
* HTTP handshake portion of WebSockets. It's up to a subclass to add functionality/purpose to the
* server.
*/
public abstract class WebSocketServer extends AbstractWebSocket implements Runnable {
private static final int AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors();
/**
* Logger instance
*
* @since 1.4.0
*/
private final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
/**
* Holds the list of active WebSocket connections. "Active" means WebSocket handshake is complete
* and socket can be written to, or read from.
*/
private final Collection connections;
/**
* The port number that this WebSocket server should listen on. Default is
* WebSocketImpl.DEFAULT_PORT.
*/
private final InetSocketAddress address;
/**
* The socket channel for this WebSocket server.
*/
private ServerSocketChannel server;
/**
* The 'Selector' used to get event keys from the underlying socket.
*/
private Selector selector;
/**
* The Draft of the WebSocket protocol the Server is adhering to.
*/
private List drafts;
private Thread selectorthread;
private final AtomicBoolean isclosed = new AtomicBoolean(false);
protected List decoders;
private List iqueue;
private BlockingQueue buffers;
private int queueinvokes = 0;
private final AtomicInteger queuesize = new AtomicInteger(0);
private WebSocketServerFactory wsf = new DefaultWebSocketServerFactory();
/**
* Attribute which allows you to configure the socket "backlog" parameter which determines how
* many client connections can be queued.
*
* @since 1.5.0
*/
private int maxPendingConnections = -1;
/**
* Creates a WebSocketServer that will attempt to listen on port WebSocketImpl.DEFAULT_PORT.
*
* @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here
*/
public WebSocketServer() {
this(new InetSocketAddress(WebSocketImpl.DEFAULT_PORT), AVAILABLE_PROCESSORS, null);
}
/**
* Creates a WebSocketServer that will attempt to bind/listen on the given address.
*
* @param address The address to listen to
* @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here
*/
public WebSocketServer(InetSocketAddress address) {
this(address, AVAILABLE_PROCESSORS, null);
}
/**
* @param address The address (host:port) this server should listen on.
* @param decodercount The number of {@link WebSocketWorker}s that will be used to process the
* incoming network data. By default this will be Runtime.getRuntime().availableProcessors()
* @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here
*/
public WebSocketServer(InetSocketAddress address, int decodercount) {
this(address, decodercount, null);
}
/**
* @param address The address (host:port) this server should listen on.
* @param drafts The versions of the WebSocket protocol that this server instance should comply
* to. Clients that use an other protocol version will be rejected.
* @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here
*/
public WebSocketServer(InetSocketAddress address, List drafts) {
this(address, AVAILABLE_PROCESSORS, drafts);
}
/**
* @param address The address (host:port) this server should listen on.
* @param decodercount The number of {@link WebSocketWorker}s that will be used to process the
* incoming network data. By default this will be Runtime.getRuntime().availableProcessors()
* @param drafts The versions of the WebSocket protocol that this server instance should
* comply to. Clients that use an other protocol version will be rejected.
* @see #WebSocketServer(InetSocketAddress, int, List, Collection) more details here
*/
public WebSocketServer(InetSocketAddress address, int decodercount, List drafts) {
this(address, decodercount, drafts, new HashSet());
}
/**
* Creates a WebSocketServer that will attempt to bind/listen on the given address, and
* comply with Draft version draft.
*
* @param address The address (host:port) this server should listen on.
* @param decodercount The number of {@link WebSocketWorker}s that will be used to process
* the incoming network data. By default this will be
* Runtime.getRuntime().availableProcessors()
* @param drafts The versions of the WebSocket protocol that this server instance
* should comply to. Clients that use an other protocol version will
* be rejected.
* @param connectionscontainer Allows to specify a collection that will be used to store the
* websockets in.
If you plan to often iterate through the
* currently connected websockets you may want to use a collection
* that does not require synchronization like a {@link
* CopyOnWriteArraySet}. In that case make sure that you overload
* {@link #removeConnection(WebSocket)} and {@link
* #addConnection(WebSocket)}.
By default a {@link HashSet} will
* be used.
* @see #removeConnection(WebSocket) for more control over syncronized operation
* @see more about
* drafts
*/
public WebSocketServer(InetSocketAddress address, int decodercount, List drafts,
Collection connectionscontainer) {
if (address == null || decodercount < 1 || connectionscontainer == null) {
throw new IllegalArgumentException(
"address and connectionscontainer must not be null and you need at least 1 decoder");
}
if (drafts == null) {
this.drafts = Collections.emptyList();
} else {
this.drafts = drafts;
}
this.address = address;
this.connections = connectionscontainer;
setTcpNoDelay(false);
setReuseAddr(false);
iqueue = new LinkedList<>();
decoders = new ArrayList<>(decodercount);
buffers = new LinkedBlockingQueue<>();
for (int i = 0; i < decodercount; i++) {
WebSocketWorker ex = new WebSocketWorker();
decoders.add(ex);
}
}
/**
* Starts the server selectorthread that binds to the currently set port number and listeners for
* WebSocket connection requests. Creates a fixed thread pool with the size {@link
* WebSocketServer#AVAILABLE_PROCESSORS}
May only be called once.
*
* Alternatively you can call {@link WebSocketServer#run()} directly.
*
* @throws IllegalStateException Starting an instance again
*/
public void start() {
if (selectorthread != null) {
throw new IllegalStateException(getClass().getName() + " can only be started once.");
}
new Thread(this).start();
}
/**
* Closes all connected clients sockets, then closes the underlying ServerSocketChannel,
* effectively killing the server socket selectorthread, freeing the port the server was bound to
* and stops all internal workerthreads.
*
* If this method is called before the server is started it will never start.
*
* @param timeout Specifies how many milliseconds the overall close handshaking may take
* altogether before the connections are closed without proper close
* handshaking.
* @throws InterruptedException Interrupt
*/
public void stop(int timeout) throws InterruptedException {
if (!isclosed.compareAndSet(false,
true)) { // this also makes sure that no further connections will be added to this.connections
return;
}
List socketsToClose;
// copy the connections in a list (prevent callback deadlocks)
synchronized (connections) {
socketsToClose = new ArrayList<>(connections);
}
for (WebSocket ws : socketsToClose) {
ws.close(CloseFrame.GOING_AWAY);
}
wsf.close();
synchronized (this) {
if (selectorthread != null && selector != null) {
selector.wakeup();
selectorthread.join(timeout);
}
}
}
public void stop() throws InterruptedException {
stop(0);
}
/**
* Returns all currently connected clients. This collection does not allow any modification e.g.
* removing a client.
*
* @return A unmodifiable collection of all currently connected clients
* @since 1.3.8
*/
public Collection getConnections() {
synchronized (connections) {
return Collections.unmodifiableCollection(new ArrayList<>(connections));
}
}
public InetSocketAddress getAddress() {
return this.address;
}
/**
* Gets the port number that this server listens on.
*
* @return The port number.
*/
public int getPort() {
int port = getAddress().getPort();
if (port == 0 && server != null) {
port = server.socket().getLocalPort();
}
return port;
}
/**
* Get the list of active drafts
*
* @return the available drafts for this server
*/
public List getDraft() {
return Collections.unmodifiableList(drafts);
}
/**
* Set the requested maximum number of pending connections on the socket. The exact semantics are
* implementation specific. The value provided should be greater than 0. If it is less than or
* equal to 0, then an implementation specific default will be used. This option will be passed as
* "backlog" parameter to {@link ServerSocket#bind(SocketAddress, int)}
*
* @since 1.5.0
* @param numberOfConnections the new number of allowed pending connections
*/
public void setMaxPendingConnections(int numberOfConnections) {
maxPendingConnections = numberOfConnections;
}
/**
* Returns the currently configured maximum number of pending connections.
*
* @see #setMaxPendingConnections(int)
* @since 1.5.0
* @return the maximum number of pending connections
*/
public int getMaxPendingConnections() {
return maxPendingConnections;
}
// Runnable IMPLEMENTATION /////////////////////////////////////////////////
public void run() {
if (!doEnsureSingleThread()) {
return;
}
if (!doSetupSelectorAndServerThread()) {
return;
}
try {
int shutdownCount = 5;
int selectTimeout = 0;
while (!selectorthread.isInterrupted() && shutdownCount != 0) {
SelectionKey key = null;
try {
if (isclosed.get()) {
selectTimeout = 5;
}
int keyCount = selector.select(selectTimeout);
if (keyCount == 0 && isclosed.get()) {
shutdownCount--;
}
Set keys = selector.selectedKeys();
Iterator i = keys.iterator();
while (i.hasNext()) {
key = i.next();
if (!key.isValid()) {
continue;
}
if (key.isAcceptable()) {
doAccept(key, i);
continue;
}
if (key.isReadable() && !doRead(key, i)) {
continue;
}
if (key.isWritable()) {
doWrite(key);
}
}
doAdditionalRead();
} catch (CancelledKeyException e) {
// an other thread may cancel the key
} catch (ClosedByInterruptException e) {
return; // do the same stuff as when InterruptedException is thrown
} catch (WrappedIOException ex) {
handleIOException(key, ex.getConnection(), ex.getIOException());
} catch (IOException ex) {
handleIOException(key, null, ex);
} catch (InterruptedException e) {
// FIXME controlled shutdown (e.g. take care of buffermanagement)
Thread.currentThread().interrupt();
}
}
} catch (RuntimeException e) {
// should hopefully never occur
handleFatal(null, e);
} finally {
doServerShutdown();
}
}
/**
* Do an additional read
*
* @throws InterruptedException thrown by taking a buffer
* @throws IOException if an error happened during read
*/
private void doAdditionalRead() throws InterruptedException, IOException {
WebSocketImpl conn;
while (!iqueue.isEmpty()) {
conn = iqueue.remove(0);
WrappedByteChannel c = ((WrappedByteChannel) conn.getChannel());
ByteBuffer buf = takeBuffer();
try {
if (SocketChannelIOHelper.readMore(buf, conn, c)) {
iqueue.add(conn);
}
if (buf.hasRemaining()) {
conn.inQueue.put(buf);
queue(conn);
} else {
pushBuffer(buf);
}
} catch (IOException e) {
pushBuffer(buf);
throw e;
}
}
}
/**
* Execute a accept operation
*
* @param key the selectionkey to read off
* @param i the iterator for the selection keys
* @throws InterruptedException thrown by taking a buffer
* @throws IOException if an error happened during accept
*/
private void doAccept(SelectionKey key, Iterator i)
throws IOException, InterruptedException {
if (!onConnect(key)) {
key.cancel();
return;
}
SocketChannel channel = server.accept();
if (channel == null) {
return;
}
channel.configureBlocking(false);
Socket socket = channel.socket();
socket.setTcpNoDelay(isTcpNoDelay());
socket.setKeepAlive(true);
WebSocketImpl w = wsf.createWebSocket(this, drafts);
w.setSelectionKey(channel.register(selector, SelectionKey.OP_READ, w));
try {
w.setChannel(wsf.wrapChannel(channel, w.getSelectionKey()));
i.remove();
allocateBuffers(w);
} catch (IOException ex) {
if (w.getSelectionKey() != null) {
w.getSelectionKey().cancel();
}
handleIOException(w.getSelectionKey(), null, ex);
}
}
/**
* Execute a read operation
*
* @param key the selectionkey to read off
* @param i the iterator for the selection keys
* @return true, if the read was successful, or false if there was an error
* @throws InterruptedException thrown by taking a buffer
* @throws IOException if an error happened during read
*/
private boolean doRead(SelectionKey key, Iterator i)
throws InterruptedException, WrappedIOException {
WebSocketImpl conn = (WebSocketImpl) key.attachment();
ByteBuffer buf = takeBuffer();
if (conn.getChannel() == null) {
key.cancel();
handleIOException(key, conn, new IOException());
return false;
}
try {
if (SocketChannelIOHelper.read(buf, conn, conn.getChannel())) {
if (buf.hasRemaining()) {
conn.inQueue.put(buf);
queue(conn);
i.remove();
if (conn.getChannel() instanceof WrappedByteChannel && ((WrappedByteChannel) conn
.getChannel()).isNeedRead()) {
iqueue.add(conn);
}
} else {
pushBuffer(buf);
}
} else {
pushBuffer(buf);
}
} catch (IOException e) {
pushBuffer(buf);
throw new WrappedIOException(conn, e);
}
return true;
}
/**
* Execute a write operation
*
* @param key the selectionkey to write on
* @throws IOException if an error happened during batch
*/
private void doWrite(SelectionKey key) throws WrappedIOException {
WebSocketImpl conn = (WebSocketImpl) key.attachment();
try {
if (SocketChannelIOHelper.batch(conn, conn.getChannel()) && key.isValid()) {
key.interestOps(SelectionKey.OP_READ);
}
} catch (IOException e) {
throw new WrappedIOException(conn, e);
}
}
/**
* Setup the selector thread as well as basic server settings
*
* @return true, if everything was successful, false if some error happened
*/
private boolean doSetupSelectorAndServerThread() {
selectorthread.setName("WebSocketSelector-" + selectorthread.getId());
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
ServerSocket socket = server.socket();
socket.setReceiveBufferSize(WebSocketImpl.RCVBUF);
socket.setReuseAddress(isReuseAddr());
socket.bind(address, getMaxPendingConnections());
selector = Selector.open();
server.register(selector, server.validOps());
startConnectionLostTimer();
for (WebSocketWorker ex : decoders) {
ex.start();
}
onStart();
} catch (IOException ex) {
handleFatal(null, ex);
return false;
}
return true;
}
/**
* The websocket server can only be started once
*
* @return true, if the server can be started, false if already a thread is running
*/
private boolean doEnsureSingleThread() {
synchronized (this) {
if (selectorthread != null) {
throw new IllegalStateException(getClass().getName() + " can only be started once.");
}
selectorthread = Thread.currentThread();
if (isclosed.get()) {
return false;
}
}
return true;
}
/**
* Clean up everything after a shutdown
*/
private void doServerShutdown() {
stopConnectionLostTimer();
if (decoders != null) {
for (WebSocketWorker w : decoders) {
w.interrupt();
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
log.error("IOException during selector.close", e);
onError(null, e);
}
}
if (server != null) {
try {
server.close();
} catch (IOException e) {
log.error("IOException during server.close", e);
onError(null, e);
}
}
}
protected void allocateBuffers(WebSocket c) throws InterruptedException {
if (queuesize.get() >= 2 * decoders.size() + 1) {
return;
}
queuesize.incrementAndGet();
buffers.put(createBuffer());
}
protected void releaseBuffers(WebSocket c) throws InterruptedException {
// queuesize.decrementAndGet();
// takeBuffer();
}
public ByteBuffer createBuffer() {
return ByteBuffer.allocate(WebSocketImpl.RCVBUF);
}
protected void queue(WebSocketImpl ws) throws InterruptedException {
if (ws.getWorkerThread() == null) {
ws.setWorkerThread(decoders.get(queueinvokes % decoders.size()));
queueinvokes++;
}
ws.getWorkerThread().put(ws);
}
private ByteBuffer takeBuffer() throws InterruptedException {
return buffers.take();
}
private void pushBuffer(ByteBuffer buf) throws InterruptedException {
if (buffers.size() > queuesize.intValue()) {
return;
}
buffers.put(buf);
}
private void handleIOException(SelectionKey key, WebSocket conn, IOException ex) {
// onWebsocketError( conn, ex );// conn may be null here
if (key != null) {
key.cancel();
}
if (conn != null) {
conn.closeConnection(CloseFrame.ABNORMAL_CLOSE, ex.getMessage());
} else if (key != null) {
SelectableChannel channel = key.channel();
if (channel != null && channel
.isOpen()) { // this could be the case if the IOException ex is a SSLException
try {
channel.close();
} catch (IOException e) {
// there is nothing that must be done here
}
log.trace("Connection closed because of exception", ex);
}
}
}
private void handleFatal(WebSocket conn, Exception e) {
log.error("Shutdown due to fatal error", e);
onError(conn, e);
//Shutting down WebSocketWorkers, see #222
if (decoders != null) {
for (WebSocketWorker w : decoders) {
w.interrupt();
}
}
if (selectorthread != null) {
selectorthread.interrupt();
}
try {
stop();
} catch (InterruptedException e1) {
Thread.currentThread().interrupt();
log.error("Interrupt during stop", e);
onError(null, e1);
}
}
@Override
public final void onWebsocketMessage(WebSocket conn, String message) {
onMessage(conn, message);
}
@Override
public final void onWebsocketMessage(WebSocket conn, ByteBuffer blob) {
onMessage(conn, blob);
}
@Override
public final void onWebsocketOpen(WebSocket conn, Handshakedata handshake) {
if (addConnection(conn)) {
onOpen(conn, (ClientHandshake) handshake);
}
}
@Override
public final void onWebsocketClose(WebSocket conn, int code, String reason, boolean remote) {
selector.wakeup();
try {
if (removeConnection(conn)) {
onClose(conn, code, reason, remote);
}
} finally {
try {
releaseBuffers(conn);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
/**
* This method performs remove operations on the connection and therefore also gives control over
* whether the operation shall be synchronized
*
* {@link #WebSocketServer(InetSocketAddress, int, List, Collection)} allows to specify a
* collection which will be used to store current connections in.
Depending on the type on the
* connection, modifications of that collection may have to be synchronized.
*
* @param ws The Websocket connection which should be removed
* @return Removing connection successful
*/
protected boolean removeConnection(WebSocket ws) {
boolean removed = false;
synchronized (connections) {
if (this.connections.contains(ws)) {
removed = this.connections.remove(ws);
} else {
//Don't throw an assert error if the ws is not in the list. e.g. when the other endpoint did not send any handshake. see #512
log.trace(
"Removing connection which is not in the connections collection! Possible no handshake received! {}",
ws);
}
}
if (isclosed.get() && connections.isEmpty()) {
selectorthread.interrupt();
}
return removed;
}
/**
* @param ws the Websocket connection which should be added
* @return Adding connection successful
* @see #removeConnection(WebSocket)
*/
protected boolean addConnection(WebSocket ws) {
if (!isclosed.get()) {
synchronized (connections) {
return this.connections.add(ws);
}
} else {
// This case will happen when a new connection gets ready while the server is already stopping.
ws.close(CloseFrame.GOING_AWAY);
return true;// for consistency sake we will make sure that both onOpen will be called
}
}
@Override
public final void onWebsocketError(WebSocket conn, Exception ex) {
onError(conn, ex);
}
@Override
public final void onWriteDemand(WebSocket w) {
WebSocketImpl conn = (WebSocketImpl) w;
try {
conn.getSelectionKey().interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
} catch (CancelledKeyException e) {
// the thread which cancels key is responsible for possible cleanup
conn.outQueue.clear();
}
selector.wakeup();
}
@Override
public void onWebsocketCloseInitiated(WebSocket conn, int code, String reason) {
onCloseInitiated(conn, code, reason);
}
@Override
public void onWebsocketClosing(WebSocket conn, int code, String reason, boolean remote) {
onClosing(conn, code, reason, remote);
}
public void onCloseInitiated(WebSocket conn, int code, String reason) {
}
public void onClosing(WebSocket conn, int code, String reason, boolean remote) {
}
public final void setWebSocketFactory(WebSocketServerFactory wsf) {
if (this.wsf != null) {
this.wsf.close();
}
this.wsf = wsf;
}
public final WebSocketFactory getWebSocketFactory() {
return wsf;
}
/**
* Returns whether a new connection shall be accepted or not.
Therefore method is well suited
* to implement some kind of connection limitation.
*
* @param key the SelectionKey for the new connection
* @return Can this new connection be accepted
* @see #onOpen(WebSocket, ClientHandshake)
* @see #onWebsocketHandshakeReceivedAsServer(WebSocket, Draft, ClientHandshake)
**/
protected boolean onConnect(SelectionKey key) {
return true;
}
/**
* Getter to return the socket used by this specific connection
*
* @param conn The specific connection
* @return The socket used by this connection
*/
private Socket getSocket(WebSocket conn) {
WebSocketImpl impl = (WebSocketImpl) conn;
return ((SocketChannel) impl.getSelectionKey().channel()).socket();
}
@Override
public InetSocketAddress getLocalSocketAddress(WebSocket conn) {
return (InetSocketAddress) getSocket(conn).getLocalSocketAddress();
}
@Override
public InetSocketAddress getRemoteSocketAddress(WebSocket conn) {
return (InetSocketAddress) getSocket(conn).getRemoteSocketAddress();
}
/**
* Called after an opening handshake has been performed and the given websocket is ready to be
* written on.
*
* @param conn The WebSocket instance this event is occurring on.
* @param handshake The handshake of the websocket instance
*/
public abstract void onOpen(WebSocket conn, ClientHandshake handshake);
/**
* Called after the websocket connection has been closed.
*
* @param conn The WebSocket instance this event is occurring on.
* @param code The codes can be looked up here: {@link CloseFrame}
* @param reason Additional information string
* @param remote Returns whether or not the closing of the connection was initiated by the remote
* host.
**/
public abstract void onClose(WebSocket conn, int code, String reason, boolean remote);
/**
* Callback for string messages received from the remote host
*
* @param conn The WebSocket instance this event is occurring on.
* @param message The UTF-8 decoded message that was received.
* @see #onMessage(WebSocket, ByteBuffer)
**/
public abstract void onMessage(WebSocket conn, String message);
/**
* Called when errors occurs. If an error causes the websocket connection to fail {@link
* #onClose(WebSocket, int, String, boolean)} will be called additionally.
This method will be
* called primarily because of IO or protocol errors.
If the given exception is an
* RuntimeException that probably means that you encountered a bug.
*
* @param conn Can be null if there error does not belong to one specific websocket. For example
* if the servers port could not be bound.
* @param ex The exception causing this error
**/
public abstract void onError(WebSocket conn, Exception ex);
/**
* Called when the server started up successfully.
*
* If any error occurred, onError is called instead.
*/
public abstract void onStart();
/**
* Callback for binary messages received from the remote host
*
* @param conn The WebSocket instance this event is occurring on.
* @param message The binary message that was received.
* @see #onMessage(WebSocket, ByteBuffer)
**/
public void onMessage(WebSocket conn, ByteBuffer message) {
}
/**
* Send a text to all connected endpoints
*
* @param text the text to send to the endpoints
*/
public void broadcast(String text) {
broadcast(text, connections);
}
/**
* Send a byte array to all connected endpoints
*
* @param data the data to send to the endpoints
*/
public void broadcast(byte[] data) {
broadcast(data, connections);
}
/**
* Send a ByteBuffer to all connected endpoints
*
* @param data the data to send to the endpoints
*/
public void broadcast(ByteBuffer data) {
broadcast(data, connections);
}
/**
* Send a byte array to a specific collection of websocket connections
*
* @param data the data to send to the endpoints
* @param clients a collection of endpoints to whom the text has to be send
*/
public void broadcast(byte[] data, Collection clients) {
if (data == null || clients == null) {
throw new IllegalArgumentException();
}
broadcast(ByteBuffer.wrap(data), clients);
}
/**
* Send a ByteBuffer to a specific collection of websocket connections
*
* @param data the data to send to the endpoints
* @param clients a collection of endpoints to whom the text has to be send
*/
public void broadcast(ByteBuffer data, Collection clients) {
if (data == null || clients == null) {
throw new IllegalArgumentException();
}
doBroadcast(data, clients);
}
/**
* Send a text to a specific collection of websocket connections
*
* @param text the text to send to the endpoints
* @param clients a collection of endpoints to whom the text has to be send
*/
public void broadcast(String text, Collection clients) {
if (text == null || clients == null) {
throw new IllegalArgumentException();
}
doBroadcast(text, clients);
}
/**
* Private method to cache all the frames to improve memory footprint and conversion time
*
* @param data the data to broadcast
* @param clients the clients to send the message to
*/
private void doBroadcast(Object data, Collection clients) {
String strData = null;
if (data instanceof String) {
strData = (String) data;
}
ByteBuffer byteData = null;
if (data instanceof ByteBuffer) {
byteData = (ByteBuffer) data;
}
if (strData == null && byteData == null) {
return;
}
Map> draftFrames = new HashMap<>();
List clientCopy;
synchronized (clients) {
clientCopy = new ArrayList<>(clients);
}
for (WebSocket client : clientCopy) {
if (client != null) {
Draft draft = client.getDraft();
fillFrames(draft, draftFrames, strData, byteData);
try {
client.sendFrame(draftFrames.get(draft));
} catch (WebsocketNotConnectedException e) {
//Ignore this exception in this case
}
}
}
}
/**
* Fills the draftFrames with new data for the broadcast
*
* @param draft The draft to use
* @param draftFrames The list of frames per draft to fill
* @param strData the string data, can be null
* @param byteData the byte buffer data, can be null
*/
private void fillFrames(Draft draft, Map> draftFrames, String strData,
ByteBuffer byteData) {
if (!draftFrames.containsKey(draft)) {
List frames = null;
if (strData != null) {
frames = draft.createFrames(strData, false);
}
if (byteData != null) {
frames = draft.createFrames(byteData, false);
}
if (frames != null) {
draftFrames.put(draft, frames);
}
}
}
/**
* This class is used to process incoming data
*/
public class WebSocketWorker extends Thread {
private BlockingQueue iqueue;
public WebSocketWorker() {
iqueue = new LinkedBlockingQueue<>();
setName("WebSocketWorker-" + getId());
setUncaughtExceptionHandler(new UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
log.error("Uncaught exception in thread {}: {}", t.getName(), e);
}
});
}
public void put(WebSocketImpl ws) throws InterruptedException {
iqueue.put(ws);
}
@Override
public void run() {
WebSocketImpl ws = null;
try {
while (true) {
ByteBuffer buf;
ws = iqueue.take();
buf = ws.inQueue.poll();
assert (buf != null);
doDecode(ws, buf);
ws = null;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (RuntimeException e) {
handleFatal(ws, e);
}
}
/**
* call ws.decode on the byteBuffer
*
* @param ws the Websocket
* @param buf the buffer to decode to
* @throws InterruptedException thrown by pushBuffer
*/
private void doDecode(WebSocketImpl ws, ByteBuffer buf) throws InterruptedException {
try {
ws.decode(buf);
} catch (Exception e) {
log.error("Error while reading from remote connection", e);
} finally {
pushBuffer(buf);
}
}
}
}