Article Directory
-
- Introduction
- Server example
- client example
- Additional data attachment for the connection
- Custom request header
- custom response header
- get response header
- Get request URL parameters
- reject handshake connection
- Enable SO_REUSEADDR
- enableTCP_NODELAY
- Multi-endpoint support Endpoint
- Idle Check/Connection Lost Check
- Internal thread Thread
Introduction
Github: https://github.com/TooTallNate/Java-WebSocket
A barebones WebSocket server and client implementation written in 100% Java. Low-level classes are implemented java.nio
that allow for a non-blocking event-driven model (similar to a web browser's WebSocket API ).
The implemented WebSocket protocol versions are:
Using Maven, add this dependency to your pom.xml:
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
Server example
Using Java-Websocket is very similar to using javascript websockets : you just take a client or server class and override its abstract methods by putting your application logic.
These methods are
- onOpen
- onMessage
- onClose
- onError
- onStart (just for the server)
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
import org.java_websocket.server.WebSocketServer;
public class SimpleServer extends WebSocketServer {
public SimpleServer(InetSocketAddress address) {
super(address);
}
@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
System.out.println("new connection to " + conn.getRemoteSocketAddress());
}
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
System.out.println("closed " + conn.getRemoteSocketAddress() + " with exit code " + code + " additional info: " + reason);
}
@Override
public void onMessage(WebSocket conn, String message) {
System.out.println("received message from " + conn.getRemoteSocketAddress() + ": " + message);
}
@Override
public void onMessage( WebSocket conn, ByteBuffer message ) {
broadcast(message.array());
System.out.println("received ByteBuffer from " + conn.getRemoteSocketAddress());
}
@Override
public void onError(WebSocket conn, Exception ex) {
System.err.println("an error occurred on connection " + conn.getRemoteSocketAddress() + ":" + ex);
}
@Override
public void onStart() {
setConnectionLostTimeout(120)
System.out.println("server started successfully");
}
public static void main(String[] args) {
String host = "localhost";
int port = 8887;
WebSocketServer server = new SimpleServer(new InetSocketAddress(host, port));
server.run();
}
}
client example
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
public class EmptyClient extends WebSocketClient {
public EmptyClient(URI serverUri, Draft draft) {
super(serverUri, draft);
}
public EmptyClient(URI serverURI) {
super(serverURI);
}
@Override
public void onOpen(ServerHandshake handshakedata) {
send("Hello, it is me. Mario :)");
System.out.println("new connection opened");
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("closed with exit code " + code + " additional info: " + reason);
}
@Override
public void onMessage(String message) {
System.out.println("received message: " + message);
}
@Override
public void onMessage(ByteBuffer message) {
System.out.println("received ByteBuffer");
}
@Override
public void onError(Exception ex) {
System.err.println("an error occurred:" + ex);
}
public static void main(String[] args) throws URISyntaxException {
WebSocketClient client = new EmptyClient(new URI("ws://localhost:8887"));
client.connect();
}
}
Additional data attachment for the connection
Equivalent to extending properties/fields, attachment stores data directly on the WebSocket instance.
For example, you can use it to keep track of different clients or to store authentication information in an easy way.
code example
public class ServerAttachmentExample extends WebSocketServer {
Integer index = 0;
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
conn.setAttachment(index); //Set the attachment to the current index
index++;
}
@Override
public void onClose(WebSocket conn, int code, String reason, boolean remote) {
// Get the attachment of this connection as Integer
System.out.println(conn + " has left the room! ID: " + conn.<Integer>getAttachment());
}
principle
websocket.java
private Object attachment;
public <T> void setAttachment(T attachment) {
this.attachment = attachment;
}
Custom request header
Map<String, String> httpHeaders = new HashMap<String, String>();
httpHeaders.put("Cookie", "username=nemo");
httpHeaders.put("Access-Control-Allow-Origin", "*");
c = new ExampleClient(new URI("ws://localhost:8887"), httpHeaders);
//Wer expect a successful connection
c.connectBlocking();
c.closeBlocking();
custom response header
Can solve cross-domain problems
public class ServerAdditionalHeaderExample extends WebSocketServer {
@Override
public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft,
ClientHandshake request) throws InvalidDataException {
ServerHandshakeBuilder builder = super
.onWebsocketHandshakeReceivedAsServer(conn, draft, request);
builder.put("Access-Control-Allow-Origin", "*");
return builder;
}
get response header
public void onOpen( WebSocket conn, ClientHandshake handshake ) {
if (!handshake.hasFieldValue( "Cookie" )) {
return;
}
String cookie = handshake.getFieldValue( "Cookie" );
}
Get request URL parameters
public class LakerServer extends WebSocketServer {
@Override
public void onOpen(WebSocket conn, ClientHandshake handshake) {
// 如果请求为 ws://localhost:8887?roomid=1
// 则结果为 /?roomid=1
String queryString = handshake.getResourceDescriptor();
reject handshake connection
Application scenario: For example, when some security checks are performed, if the specification is not met, the connection is refused.
public class ServerRejectHandshakeExample extends WebSocketServer {
@Override
public ServerHandshakeBuilder onWebsocketHandshakeReceivedAsServer(WebSocket conn, Draft draft,
ClientHandshake request) throws InvalidDataException {
ServerHandshakeBuilder builder = super
.onWebsocketHandshakeReceivedAsServer(conn, draft, request);
//In this example we don't allow any resource descriptor ( "ws://localhost:8887/?roomid=1 will be rejected but ws://localhost:8887 is fine)
if (!request.getResourceDescriptor().equals("/")) {
throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, "Not accepted!");
}
//If there are no cookies set reject it as well.
if (!request.hasFieldValue("Cookie")) {
throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, "Not accepted!");
}
//If the cookie does not contain a specific value
if (!request.getFieldValue("Cookie").equals("username=nemo")) {
throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, "Not accepted!");
}
//If there is a Origin Field, it has to be localhost:8887
if (request.hasFieldValue("Origin")) {
if (!request.getFieldValue("Origin").equals("localhost:8887")) {
throw new InvalidDataException(CloseFrame.POLICY_VALIDATION, "Not accepted!");
}
}
return builder;
}
When thrown InvalidDataException
, the server will send an HTTP code 404 to the client, resulting in a shutdown.
The principle is as follows
WebSocketImpl.java
try {
response = wsl.onWebsocketHandshakeReceivedAsServer(this, d, handshake);
} catch (InvalidDataException e) {
log.trace("Closing due to wrong handshake. Possible handshake rejection", e);
// 如果收到的握手不正确,则关闭连接
closeConnectionDueToWrongHandshake(e);
- write(generateHttpResponseDueToError(404));
- flushAndClose(exception.getCloseCode(), exception.getMessage(), false);
return false;
} catch (RuntimeException e) {
log.error("Closing due to internal server error", e);
wsl.onWebsocketError(this, e);
// 如果 RuntimeException 出现服务器错误,则关闭连接
closeConnectionDueToInternalServerError(e);
- write(generateHttpResponseDueToError(500));
- flushAndClose(CloseFrame.NEVER_CONNECTED, exception.getMessage(), false);
return false;
}
Enable SO_REUSEADDR
SO_REUSEADDR is a socket option used to set whether to allow address reuse when binding a socket . It can quickly restart the server program when the server port is occupied.
In a TCP server program, when a connection is closed, the socket is kept in the operating system for a period of time (usually 2-4 minutes) to ensure that any delayed data arrives. During this time, if you start the server program again and try to use the same port number, an error that the address is already in use occurs. Setting the SO_REUSEADDR option solves this problem, allowing rebinding to the same address and port during this time .
It should be noted that if the SO_REUSEADDR option is set on multiple sockets at the same time, it may cause address conflicts between sockets. Therefore, care needs to be taken when using this option to ensure that only one socket is using a given address and port at any one time.
Remember that you cannot enable/disable SO_REUSEADDR when you have already started the connection.
ChatServer s = new ChatServer( port );
s.setReuseAddr( true );
s.start();
ExampleClient c = new ExampleClient( new URI( "ws://localhost:8887" ) );
c.setReuseAddr( true );
c.connect();
enableTCP_NODELAY
TCP_NODELAY is an option that can be set on a TCP socket to disable Nagle's algorithm. Nagle's algorithm is a property that improves network efficiency by reducing the number of small packets sent over the network.
When Nagle's algorithm is enabled, the TCP protocol stack waits for some data to accumulate before sending packets to reduce network overhead. However, this delay can adversely affect some real-time applications, such as online gaming and live video streaming.
By setting the TCP_NODELAY option, you can tell the TCP protocol stack to send data immediately, regardless of the size of the data packet. This reduces latency and improves performance for real-time applications, but may increase network load.
After enabling
- Pros: Reduces latency and improves performance for real-time applications
- Cons: May increase network load
Remember that you cannot enable/disable TCP_NODELAY when you have already initiated the connection. It only affects new connections.
ChatServer s = new ChatServer( port );
s.setTcpNoDelay( true );
s.start();
ExampleClient c = new ExampleClient( new URI( "ws://localhost:8887" ), new Draft_6455() );
c.setTcpNoDelay( true );
c.connect();
Multi-endpoint support Endpoint
There is no built-in support for multiple endpoints , so you'll have to implement it yourself.
-
Callbacks
onOpen
provide access to handshake data. -
handshake.getResourceDescriptor()
You can parse the endpoint string from which the full URI will be obtained. Then you can call methods based on the endpoint string.
sample code
import org.java_websocket.*;
import org.java_websocket.server.*;
import org.java_websocket.client.*;
import org.java_websocket.handshake.*;
import java.util.*;
import java.net.*;
interface Endpoint {
void onOpen(WebSocket socket);
// add other event handlers here
}
public class EndpointServer extends WebSocketServer {
private Map<String, Endpoint> endpoints = Collections.synchronizedMap(new HashMap<>());
public static void main(String... args) {
var server = new EndpointServer();
server.endpoints.put("/greeting", socket -> socket.send("Hello!"));
server.endpoints.put("/chat", socket -> socket.send("You have connected to chat"));
server.start();
var client = new Client("ws://localhost:" + server.getPort() + "/chat");
client.connect();
}
public void onStart() {
// ...
}
public void onOpen(WebSocket socket, ClientHandshake handshake) {
String path = URI.create(handshake.getResourceDescriptor()).getPath();
Endpoint endpoint = endpoints.get(path);
if(endpoint != null)
endpoint.onOpen(socket);
}
public void onMessage(WebSocket socket, String message) {
// ...
}
public void onClose(WebSocket socket, int code, String message, boolean remote) {
// ...
}
public void onError(WebSocket socket, Exception e) {
e.printStackTrace();
}
}
class Client extends WebSocketClient {
public Client(String uri) {
super(URI.create(uri));
}
public void onOpen(ServerHandshake handshake) {
}
public void onMessage(String message) {
System.out.println(this + " received message: " + message);
}
public void onClose(int code, String message, boolean remote) {
}
public void onError(Exception e) {
e.printStackTrace();
}
}
Idle Check/Connection Lost Check
Idle Check or Connection Lost Check
Connection Loss Checking is a feature that detects if the connection to another endpoint is lost, for example due to a loss of wifi or mobile data signal.
To detect lost connections we use a heartbeat implementation.
Instrumentation runs at specified intervals (eg: 60 seconds) and performs the following actions on all connected endpoints:
- Disconnects the endpoint if it has not sent a pong recently. Endpoints are given 1.5 times the time interval to reply to a PONG. Therefore, if the interval is 60 seconds, the endpoint has 90 seconds to respond.
- Send a ping to the endpoint.
Detection is bidirectional, so a server can detect a lost client, and a client can detect a lost connection to the server.
Endpoints SHOULD respond to Ping frames with Pong frames as soon as practicable.
sample code
// 设置间隔为120秒
server.setConnectionLostTimeout(120);
// 间隔小于或等于 0 的值会导致检查被停用。
server.setConnectionLostTimeout( 0 );
Realization principle
// 实现原理 核心代码如下
long minimumPongTime;
synchronized (syncConnectionLost) {
minimumPongTime = (long) (System.nanoTime() - (connectionLostTimeout * 1.5));
}
for (WebSocket webSocket : connections) {
WebSocketImpl webSocketImpl = (WebSocketImpl) webSocket;
if (webSocketImpl.getLastPong() < minimumPongTime) {
// 如果最后一次收到PONG的时间差值 小于了 1.5倍的设置值
// 则关闭连接
log.trace("Closing connection due to no pong received: {}", webSocketImpl);
webSocketImpl.closeConnection(CloseFrame.ABNORMAL_CLOSE,
} else {
// 为客户端发送Ping
if (webSocketImpl.isOpen()) {
webSocketImpl.sendPing();
} else {
log.trace("Trying to ping a non open connection: {}", webSocketImpl);
}
}
}
To verify , listen to Pong on the websocket server, here we set a check interval of 5 seconds.
@Override
public void onWebsocketPong(WebSocket conn, Framedata f) {
logger.info("pong pong pong pong : {}", conn.getRemoteSocketAddress());
}
10:55:32.698 [WebSocketWorker-14] INFO c.c.f.t.LakerTracker - pong pong pong pong : /127.0.0.1:54125
10:55:37.694 [WebSocketWorker-14] INFO c.c.f.t.LakerTracker - pong pong pong pong : /127.0.0.1:54125
10:55:42.695 [WebSocketWorker-14] INFO c.c.f.t.LakerTracker - pong pong pong pong : /127.0.0.1:54125
10:55:47.795 [WebSocketWorker-14] INFO c.c.f.t.LakerTracker - pong pong pong pong : /127.0.0.1:54125
Internal thread Thread
Both websocket clients and servers use nio, although their architectures are completely different.
- The client uses only one thread to perform read and write operations and event delivery
- The server performs these operations in a multi-threaded manner:
- The server has a selector thread selector and one or more worker threads workers .
- The Selector thread performs all nio operations: it registers channels, selects keys for efficient reading and writing to channels.
- The worker thread performs encoding/decoding or event delivery.
- Selector thread selector thread and worker thread worker thread communicate through the queue . There are decoding queue, write queue and buffer queue.
The server-side loop decoding process is :
- The selector thread takes an unused buffer from the " buffer queue ", puts the data of a read-ready channel into it, and puts it into the decoding queue.
- The worker thread dequeues the websocket and consumes all allocated buffer contents.
- It then puts the buffer back into the buffer queue so it can be reused.
Performance can be controlled by the number of worker threads , the number and size of buffers in the buffer queue, and the size of the channel's internal buffers.
WebSocketServer
The number of worker threads can be adjusted using a form of the constructor that takes parameters decodercount
.
public WebSocketServer(InetSocketAddress address) {
this(address, AVAILABLE_PROCESSORS, null);
}
public WebSocketServer(InetSocketAddress address, int decodercount) {
this(address, decodercount, null);
}
WebSocketClient starts the following threads:
WebSocketTimer
- Lost connection detection timerWebSocketWriteThread-*
- A thread that writes messages to another endpointWebSocketConnectReadThread-*
- A thread that connects and reads messages from another endpoint
WebSocketServer starts the following threads:
WebSocketTimer
- Lost connection detection timerWebSocketWorker-*
- Threads to decode incoming messages (number of threads depends on how many decoders you use)WebSocketSelector-*
- The thread on which the server selector runs (single-threaded)