Writing a simple WebSocket server in java

Websocket protocol uses a server, which is a simple TCP application (like a webserver) and a client, which may be a browser. There are lot of examples on how to use websockts in javascripts. In this tutorial, I will explain how I wrote a simple Websocket server application. All I had to do was to follow the protocol specification.

Websocket server has two steps when communicating with a client.

  1. Handshake
  2. Data communication

Websocket Handshake

Messaging used for handshake will look similar to HTTP with headers and syntax. Here is the sample request header

GET / HTTP/1.1
Host: myserver.com:8888
Sec-WebSocket-Version: 13
Origin: null
Sec-WebSocket-Key: A2c+44/K5aeYNGgwnpR+sg==
Connection: keep-alive, Upgrade
Upgrade: websocket

and here is the response for the same from server.

HTTP/1.1 101 Web Socket Protocol Handshake
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: AtqoGG6al8jX9KRAVzpC/Y0Zdn8=

Every header will enter with a carriage return and new line. Don’t forget to add carriage return and new line. What is important in this handshake is the Sec-WebSocket-Key in request and Sec-WebSocket-Accept. Rest of the items can be hard coded. As a response to the key sent by client, the server prepares a Sec-WebSocket-Accept using the following logic.

  1. Suffix the string “258EAFA5-E914-47DA-95CA-C5AB0DC85B11” to the key sent by client.
  2. Calculate SHA-1 has for this value.
  3. Create base64 encoding of the resulting value, which will give you the key to be sent back to the client.

Here is the code, which does this job.

private String getAcceptKey(String clientKey) {
    String k = clientKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
    MessageDigest md = null;
    try {
        md = MessageDigest.getInstance("SHA-1");
    } catch (NoSuchAlgorithmException e) {
        System.err.println(e.getMessage());
        return null;
    }
    byte[] o = md.digest(k.getBytes());
    k = Base64.getEncoder().encodeToString(o);
    return k;
}

Websocket Data frame

Once this response is sent, client is ready to send the data. Data format is defined as frame. Data frame format is same for message generated from client and from server. Length of the frame is not fixed. Here is the frame.

Websocket Data Format

Each row is a byte. Lets examine the the format.

  1. First bit in in the first byte is a flag that indicates, if this is the final frame or not. Client can send data in multiple frames. If there are multiple frames, last frame will have this bit set to 1 and all frames before this will have this bit set to 0. If there is only one frame, this bit will be set to 1. In this example, we are assuming that client will send only one frame. So, always set this bit to 1.
  2. We can ignore the next three bits RSV1, RSV2 and RSV3
  3. Next four bits form the opcode tells what kind of data it is
    • 1 is for text
    • 2 is for binary.
  4. First bit in the next byte, if data is masked. Data from client to server is always masked. So, in the server you expect this value to be 1. Message from server to client is not masked. So, set this to zero.
  5. Length field comes next. Here is the logic to interpret the length. Remaining 7 bits of the second byte determines the length. But using this we can have a maximum length of 127. So, there is a mechanism to represent length
    • If value is 125, the use it as it is for length or,
    • if length is 126, read the next two bytes and it it forms the length, or
    • if length is 127, the read next 8 bytes which is the actual length.
  6. After reading the length as mentioned in previous step, read the next four bytes as mask. Message from server to client does not have this field.
  7. After the mask key, the actual data from client follows which is masked. Loop through every byte in data and do an xor with byte at position ( i modulo 4) of mask key

Following code will make it clear.


for(int i=0;i<len;i++) {
   buff[i] = (byte)(buff[i] ^ maskKey[i % 4]);
}

Here is the code for parsing the client message.




private void startTxn() throws IOException {
    //Read first byte.
    int b = _mIs.read();

    //This example suports only single frames. Seqence is not supported.
    boolean finalFrame = (b & 0x80) == 0x80;
    if(!finalFrame) {
        handleError();
    }

    //Read opcode. We support only text messages.
    int opcode = b &0x0F;
    if(opcode != 1) {
        handleError();
    }

    //Second byte
    b = _mIs.read();
    boolean mask = (b & 0x80) == 0x80;

    //We are expecting message from client. So, mask should be 1.
    if(!mask) {
        handleError();
    }

    //Payload length. Lets consider only 7 bits or 16 bits. 
    int len = b & 0x7F;
    if(len  == 127) {
        handleError();
    }
    else if(len == 126) {
        int b1 = _mIs.read();
        int b2 = _mIs.read();

        len = b1 << 8;
        len |= b2;
    }

    //Read mask. 4 bytes
    byte maskKey[] = new byte[4];
    for(int i=0;i<4;i++) {
        maskKey[i] = (byte)_mIs.read();
    }

    byte buff[] = new byte[len];
    int readLen = _mIs.read(buff);
    if(readLen < len) {
        //We could not read enough data.
        handleError();
    }

    //Apply the mask
    for(int i=0;i<len;i++) {
        buff[i] = (byte)(buff[i] ^ maskKey[i % 4]);
    }

    String message = new String(buff);

    System.out.println("Message : \n"+message);
    sendMessage("We got \""+message+"\"");
            
}

Sending the Websocket response to client

When sending the response back to client, similar syntax is followed, with the following exception.

  • Mask bit is set to zero
  • Mask key is not used
  • Data is not masked.

Here is the complete program. Down the file wssample.zip
This program will wait for Websocket connections at port 8888 (use ws://localhost:8888/). Connection close is not implemented. But it can be implemented using the details mentioned here.

To test this app, you can use any client or a chrome Websocket plugin.

Important : If you are using this code in production do that at your own risk.

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *