Websockets
Bidirectional Communication
What if we wanted a system that could send notifications to the client asynchronously. This is exactly what WebSockets do, they set up a bidirectional channel using HTTP/TCP and enable server-driven, full-duplex messaging.
Specification
The WebSocket protocol specification defines ws
(WebSocket) and wss
(WebSocket Secure) as two new uniform resource identifier (URI) schemes[8] that are used for unencrypted and encrypted connections respectively. To start a WebSocket Channel there needs to be an initial handshake which is done over HTTPS using the upgrade
header.
The return key verifies, that the server understood the request and is calculated like the following:
String KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
String computeReturnKey(String key) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-1");
byte[] res = md.digest((key+KEY_SUFFIX).getBytes(Charset.forName("ascii")));
return Base64.encodeBytes(res);
}
A message has the following format:
The fields having the following meaning:
- FIN marks the final fragment in a message
- RSV = 000
- OPCODE, operation code
- 0x0 continuation frame
- 0x1 text frame
- 0x2 binary frame
- 0x8 close
- 0x9 ping
- 0xA pong – MASK indicates content obfuscation (XOR masking)
A message could look something like this:
0x81 1000 0001 Final Fragment | Text frame
0x85 1 000 0101 Masked / length = 5
0x96 1001 0110 Masking Key
0xa7 1010 0111 Masking Key
0x2b 0010 1011 Masking Key
0x38 0011 1000 Masking Key
0xde 1101 1110 xor 10010110 = 0100 1000 H
0xc2 1100 0010 xor 10100111 = 0110 0101 e
0x47 0100 0111 xor 00101011 = 0110 1100 l
0x54 0101 0100 xor 00111000 = 0110 1100 l
0xf9 1111 1001 xor 10010110 = 0110 1111 o
An implementation of this process could be:
byte[] maskingKey; // random masking key, 4 bytes
byte[] payloadData; // data to be transmitted
byte[] maskedData; // masked data to be generated
// mask (on client)
for(int i = 0; i < payloadData.length; i++)
maskedData[i] = payloadData[i] ^ maskingKey[i%4];
}
//unmask (on server)
for(int i = 0; i < maskedData.length; i++)
payloadData[i] = maskedData[i] ^ maskingKey[i%4];
}
Sub-Protocols
WebSockets also offer the option for the client and server to agree on a protocol with which the transmitted data will be formatted and interpreted. Examples of sub-protocols are JSON, XML, MQTT, WAMP, STOMP, SOAP. These protocols can ensure agreement not only about the way the data is structured but also about the way communication must commence, continue and eventually terminate. As long as it is defined in the handshake with the Sec-WebSocket-Protocol
header and both parties understand what the protocol entails, anything goes.
JSR 356 Example
@ClientEndpoint
public class EchoClient {
private static CountDownLatch latch = new CountDownLatch(1);
@OnOpen
public void onOpen(Session session) throws IOException {
System.out.println("onOpen " + Thread.currentThread());
session.getBasicRemote().sendText("Hello");
// session.getBasicRemote().sendBinary(ByteBuffer.wrap(new byte[]{'h', 'e', 'l', 'o'})); // sends a binary message
// session.getBasicRemote().sendText("Hello", false);
// session.getBasicRemote().sendText("World", true);
}
@OnMessage
public void onMessage(Session session, String message) throws IOException {
System.out.println("onMessage " + message + " " + Thread.currentThread());
session.close();
}
@OnClose
public void onClose(Session session, CloseReason closeReason) {
System.out.printf("[%s] Session %s closed because of %s\n", Thread.currentThread(), session.getId(), closeReason);
latch.countDown();
}
@OnError
public void onError(Throwable exception, Session session) {
System.out.println("an error occured on connection " + session.getId() + ":" + exception);
}
public static void main(String[] args) throws Exception {
// URI url = new URI("ws://86.119.38.130:8080/websockets/echo");
URI url = new URI("ws://localhost:2222/websockets/echo");
//System.out.println(Thread.currentThread());
ClientManager client = ClientManager.createClient();
client.connectToServer(EchoClient.class, url);
latch.await();
}
}
@ServerEndpoint("/echo")
public class EchoServer {
{
System.out.println("EchoServer created " + this);
}
public static void main(String[] args) throws Exception {
Server server = new Server("localhost", 2222, "/websockets", null, EchoServer.class);
server.start();
System.out.println("Server started, press a key to stop the server");
System.in.read();
}
@OnOpen
public void onOpen(Session session) {
System.out.printf("New session %s\n", session.getId());
}
@OnClose
public void onClose(Session session, CloseReason closeReason) {
System.out.printf("Session %s closed because of %s\n", session.getId(), closeReason);
}
@OnMessage
public String onMessage(String message, Session session) {
System.out.println("received message form " + session.getBasicRemote() + ": " + message);
return "echo " + message;
}
@OnError
public void onError(Throwable exception, Session session) {
System.out.println("an error occured on connection " + session.getId() + ":" + exception);
}
}