Initial websocket

Time:2021-8-22

I’ve always been interested in real-time communication. I took the time to learn about websocket this week.

websocket

Websocket is a network transmission protocol, which can carry out full duplex communication on a single TCP connection. It is located in the application layer of OSI model.

Websocket makes the data exchange between the client and the server easier, and allows the server to actively push data to the client. In the websocket API, the browser and the server only need to complete a handshake to create a persistent connection and conduct two-way data transmission.

Practice is the best teacher in order to understandwebsocketSpecific implementation, intended to usewebsocketCommunicate with ` ` java socket. Java socket uses transport layer protocol, while websocket is application layer protocol, which requires us to process data manually.

The first thing to understand is the handshake process and data frame format of websocket.

Websocket handshake process

request

Websocket uses HTTP protocol to shake hands. First, it uses HTTP protocol to send request message, mainly to ask whether the server supports websocket service. The main information of the request header is as follows:

GET ws://localhost:7000/ HTTP/1.1
Host: localhost:7000
Connection: Upgrade
Sec-WebSocket-Key: kvMm3tIaxXRCmGHuY01eQw==
Sec-WebSocket-Version: 13
Upgrade: websocket

The most important thing here isSec-WebSocket-Key, this is a random string generated by the client and encoded with Base64. The server should respond to this encoding.

response

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
WebSocket-Location: ws://127.0.0.1:9527
Sec-WebSocket-Accept: Mf7ptCXn+TYF9XtDt8w+j9FCBEg=

most important of allSec-WebSocket-Accept, the client will verify this. If it does not comply with the verification rules, it will be regarded as the server rejecting the connection. Build rule for clientSec-WebSocket-KeyRemove the leading and trailing blanks, connect the fixed string (258eafa5-e914-47da-95ca-c5ab0dc85b11), use SHA-1 for hash operation, and then encode the result with Base64.

Java code implementation:

public static final String RESPONSE_HEADERS = "HTTP/1.1 101 Switching Protocols\r\n" +
            "Upgrade: websocket\r\n" +
            "Connection: Upgrade\r\n" +
            "WebSocket-Location: ws://127.0.0.1:9527\r\n";

 ServerSocket serverSocket = new ServerSocket(7000);

        while (true) {
            Socket socket = serverSocket.accept();
            //Start a new thread
            Thread thread = new Thread(() -> {
                //Response handshake information
                try {
                    //Read request header
                    byte[] bytes = new byte[10000000];
                    socket.getInputStream().read(bytes);
                    String requestHeaders = new String(bytes, StandardCharsets.UTF_8);

                    //Gets the in the request header
                    String webSocketKey = "";
                    for (String header : requestHeaders.split("\r\n")) {
                        if (header.startsWith("Sec-WebSocket-Key")) {
                            webSocketKey = header.split(":")[1].trim();
                        }
                    }

                    //The websocketkey and magickey are spliced and encrypted with SHA1 before Base64 coding
                    String value = webSocketKey + magicKey;
                    String webSocketAccept = new String(Base64.encodeBase64(DigestUtils.sha1(value.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);

                    //The write return header handshake ends and the connection is successfully established
                    String responseHeaders = RESPONSE_HEADERS + "Sec-WebSocket-Accept: " + webSocketAccept + "\r\n\r\n";
                    socket.getOutputStream().write(responseHeaders.getBytes(StandardCharsets.UTF_8));
                    System.out.println ("handshake successful, connection successfully established");
                }
            }
        }

First, create a new ServerSocket listening port 7000. When the TCP connection is established, read the HTTP byte array sent by the client from the InputStream and convert it into a string. At this time, the value of requestheaders should be the HTTP request header.
Initial websocket
Extract fromSec-WebSocket-KeyAnd then generated according to the ruleswebSocketAccept , splice to definedRESPONSE_HEADERS, write this data to the of the socketoutputStream, after the client receives and passes the verification, the connection is successfully established.

Data frame format

After the successful handshake, in order to communicate normally, you also need to understand the data frame format of websocket:
Initial websocket

  • FIN:1 bit
    Indicates whether this is the last frame of the message. The first frame may also be the last frame. 0: there are subsequent frames. 1: Last frame
  • RSV1、RSV2、RSV3:1 bit
    The extension field must be 0 unless an extension is given some meaning of non-zero value through negotiation
  • opcode:4 bit
    Explain the type of payload data. If you receive an unrecognized opcode, disconnect it directly. The classification values are as follows: 0: continuous frame. 1: text frame. 2: binary frame. 3 – 7: reserved for non control frame. 8: close handshake frame. 9: ping frame. A: Pong frame. B – F: reserved for non control frame
  • MASK:1 bit
    Identifies whether payload data has been masked. If it is 1, the data in masking key field is the mask key, which is used to decode payload data. The protocol stipulates that the client data needs to be masked, so this bit is 1
  • Payload len:7 bit | 7+16 bit | 7+64 bit
    Represents “payload data”, in bytes: – if it is 0 ~ 125, it directly represents the payload length – if it is 126 (binary 111 1110), store 0x7e (= 126) first, and the value of the 16 bit unsigned integer represented by the next two bytes is the payload length – if it is 127, Then, the value of 64 bit unsigned integer represented by 0x7F (= 127) and the next eight bytes is the payload length
  • Masking key: 0 | 4 bytes mask key. All frames sent from the client to the server contain a 32bits mask (if the mask is set to 1), otherwise it is 0. Once the mask is set, all received payload data must be XORed with the value by an algorithm to obtain the real value.
  • Data information sent by payload data application

For the data frame based on websocket, we need to implement two methods: one is to extract the data in the data frame, and the other is to convert the data into data frame.

Decode data frame

The core idea is to determine the reading mode of the data field according to the control field, read and decode it.

/**
     *Decodes a byte array into a string
     *@ param bytes websocket frame byte array
     *@ return resolve to string
     */
    public static String decodeMessage(byte[] bytes) {
        int col = 0;
        boolean isMask = false;
        int dataStart = 2;

        //Extract mask bit in websocket frame
        if ((bytes[1] & 0x80) == 0x80) {
            isMask = true;
        }

        //Extract payload len
        int len = bytes[1] & 0x7f;

        byte[] maskKey = new byte[4];
        if (len == 126) {
            //If it is 126, continue to read the next two bytes as payload len
            len = bytes[2];
            len = (len << 8) + bytes[3];
            //If the mask is 1, read 4 bytes backward as the maskkey
            if (isMask) {
                maskKey[0] = bytes[4];
                maskKey[1] = bytes[5];
                maskKey[2] = bytes[6];
                maskKey[3] = bytes[7];
                //The starting position of payload data is after the maskkey
                dataStart = 8;
            } else {
                dataStart = 4;
            }
        } else if (len == 127) {
            //If it is 126, continue to read the next eight bytes as payload len
            //Skip bytes [2] ~ bytes [5]
            len = bytes[6];
            len = (len << 8) + bytes[7];
            len = (len << 8) + bytes[8];
            len = (len << 8) + bytes[9];

            if (isMask) {
                maskKey[0] = bytes[10];
                maskKey[1] = bytes[11];
                maskKey[2] = bytes[12];
                maskKey[3] = bytes[13];
                dataStart = 14;
            } else {
                dataStart = 10;
            }
        } else {
            //Neither 126 nor 127 indicates that the length only occupies seven bits and does not need to be processed
            if (isMask) {
                maskKey[0] = bytes[2];
                maskKey[1] = bytes[3];
                maskKey[2] = bytes[4];
                maskKey[3] = bytes[5];
                dataStart = 6;
            } else {
                dataStart = 2;
            }
        }

        //Read the payload data and judge whether to perform mask encryption according to ismack
        for (int i = 0, count = 0; i < len; i++) {
            byte t1 = maskKey[count];
            byte t2 = bytes[i + dataStart]; //  Read data from datastart
            char c = isMask ? ( char) (((~t1) & t2) | (t1 & (~t2))) : (char) t2; //  If ismack is true, perform mask encryption
            bufferRes[col++] = c;
            count = (count + 1) % 4;
        }
        bufferRes[col++] = '
/**
*Decodes a byte array into a string
*@ param bytes websocket frame byte array
*@ return resolve to string
*/
public static String decodeMessage(byte[] bytes) {
int col = 0;
boolean isMask = false;
int dataStart = 2;
//Extract mask bit in websocket frame
if ((bytes[1] & 0x80) == 0x80) {
isMask = true;
}
//Extract payload len
int len = bytes[1] & 0x7f;
byte[] maskKey = new byte[4];
if (len == 126) {
//If it is 126, continue to read the next two bytes as payload len
len = bytes[2];
len = (len << 8) + bytes[3];
//If the mask is 1, read 4 bytes backward as the maskkey
if (isMask) {
maskKey[0] = bytes[4];
maskKey[1] = bytes[5];
maskKey[2] = bytes[6];
maskKey[3] = bytes[7];
//The starting position of payload data is after the maskkey
dataStart = 8;
} else {
dataStart = 4;
}
} else if (len == 127) {
//If it is 126, continue to read the next eight bytes as payload len
//Skip bytes [2] ~ bytes [5]
len = bytes[6];
len = (len << 8) + bytes[7];
len = (len << 8) + bytes[8];
len = (len << 8) + bytes[9];
if (isMask) {
maskKey[0] = bytes[10];
maskKey[1] = bytes[11];
maskKey[2] = bytes[12];
maskKey[3] = bytes[13];
dataStart = 14;
} else {
dataStart = 10;
}
} else {
//Neither 126 nor 127 indicates that the length only occupies seven bits and does not need to be processed
if (isMask) {
maskKey[0] = bytes[2];
maskKey[1] = bytes[3];
maskKey[2] = bytes[4];
maskKey[3] = bytes[5];
dataStart = 6;
} else {
dataStart = 2;
}
}
//Read the payload data and judge whether to perform mask encryption according to ismack
for (int i = 0, count = 0; i < len; i++) {
byte t1 = maskKey[count];
byte t2 = bytes[i + dataStart]; //  Read data from datastart
char c = isMask ? ( char) (((~t1) & t2) | (t1 & (~t2))) : (char) t2; //  If ismack is true, perform mask encryption
bufferRes[col++] = c;
count = (count + 1) % 4;
}
bufferRes[col++] = '\0';
return new String(bufferRes);
}
'; return new String(bufferRes); }

The encoded information is a data frame

The core idea is to convert the data to be sent into websocket data frame according to the information format.

/**
     *Encode the message as a websocket frame
     *@ param message character message
     *@ param ismack whether to use mask encryption
     *@ param result save byte array of frames
     *@ return byte length
     */
    public static int encodeMessage(String message, boolean isMask, byte[] result) {
        int dataEnd = 0;
        //The first byte of the frame is of type, and the default type is text frame
        result[dataEnd++] = (byte) 0x81;
        byte[] maskKey = new byte[4];

        //Gets the byte array of message
        byte[] messageBytes = message.getBytes(StandardCharsets.UTF_8);

        //Ismask is true. Set the mask bit to 1
        if (isMask) {
            result[dataEnd] = (byte) 0x80;
        }
        
        //Judge the length of data
        long dataLen = messageBytes.length;
        
        if (dataLen < 126) {
            //Direct assignment less than 126 bytes
            result[dataEnd++] |= dataLen & 0x7f;
        } else if (dataLen < 65536) {
            //Less than 65536 bytes, assign two bytes later
            result[dataEnd++] |= 0x7E;
            result[dataEnd++] = (byte) ((dataLen >> 8) & 0xFF);
            result[dataEnd++] = (byte) ((dataLen >> 0) & 0xFF);
        } else if (dataLen < 0xFFFFFFFF) {
            //Less than 0xFFFFFFFF bytes, assign eight bytes later
            //Avoid skipping 4 bytes if the data is too large
            result[dataEnd++] |= 0x7F;
            result[dataEnd++] |= 0;
            result[dataEnd++] |= 0;
            result[dataEnd++] |= 0;
            result[dataEnd++] |= 0;

            result[dataEnd++] = (byte) ((dataLen >> 24) & 0xFF);
            result[dataEnd++] = (byte) ((dataLen >> 16) & 0xFF);
            result[dataEnd++] = (byte) ((dataLen >> 8) & 0xFF);
            result[dataEnd++] = (byte) ((dataLen >> 0) & 0xFF);
        }

        if (isMask) {
            //If ismack is true, mask the data and save it to the frame
            new Random().nextBytes(maskKey);
            result[dataEnd++] = maskKey[0];
            result[dataEnd++] = maskKey[1];
            result[dataEnd++] = maskKey[2];
            result[dataEnd++] = maskKey[3];
            for (int i = 0, count = 0; i < dataLen; i++) {
                byte t1 = maskKey[count];
                byte t2 = messageBytes[i];
                result[dataEnd++] = (byte) (((~t1) & t2) | (t1 & (~t2)));
                count = (count + 1) % 4;
            }
        } else {
            //Save directly to frame
            for (int i = 0; i < dataLen; i++) {
                result[dataEnd++] = messageBytes[i];
            }
        }
        return dataEnd;
    }

experiment

javaEnd supplyServerSocketIt is used to establish a socket connection. After the connection is successful, it makes a handshake response to websocket and readsWebsocket frameDecode and read information when sending information, and convert it intoWebsocket frame

public static int userCount = 0;
    public static char[] bufferRes = new char[131072];
    public static Scanner sc = new Scanner(System.in);

    public static final String RESPONSE_HEADERS = "HTTP/1.1 101 Switching Protocols\r\n" +
            "Upgrade: websocket\r\n" +
            "Connection: Upgrade\r\n" +
            "WebSocket-Location: ws://127.0.0.1:9527\r\n";

    public static String magicKey = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(7000);

        while (true) {
            Socket socket = serverSocket.accept();
            userCount++;
            //Start a new thread
            Thread thread = new Thread(() -> {
                //Response handshake information
                try {
                    //Read request header
                    byte[] bytes = new byte[10000000];
                    socket.getInputStream().read(bytes);
                    String requestHeaders = new String(bytes, StandardCharsets.UTF_8);

                    //Gets the in the request header
                    String webSocketKey = "";
                    for (String header : requestHeaders.split("\r\n")) {
                        if (header.startsWith("Sec-WebSocket-Key")) {
                            webSocketKey = header.split(":")[1].trim();
                        }
                    }

                    //The websocketkey and magickey are spliced and encrypted with SHA1 before Base64 coding
                    String value = webSocketKey + magicKey;
                    String webSocketAccept = new String(Base64.encodeBase64(DigestUtils.sha1(value.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);

                    //The write return header handshake ends and the connection is successfully established
                    String responseHeaders = RESPONSE_HEADERS + "Sec-WebSocket-Accept: " + webSocketAccept + "\r\n\r\n";
                    socket.getOutputStream().write(responseHeaders.getBytes(StandardCharsets.UTF_8));
                    System.out.println ("handshake successful, connection successfully established");

                    //Accept client information
                    while (true) {
                        System.out.println ("read information");
                        socket.getInputStream().read(bytes);
                        String message = decodeMessage(bytes);
                        System. Out. Println ("the information read is:" + message ");

                        System.out.println ("please reply to the information");
                        String res = sc.next();
                        byte[] result = new byte[10000000];
                        int len = encodeMessage(res, false, result);
                        socket.getOutputStream().write(result, 0, len);
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
                System.out.println("finish Read");
            });
            Thread.setname ("user" + usercount);
            thread.start();
        }

    }

The client is simple to usejsCode implementation (thanks to Zhao Kaiqiang for providing).

<!DOCTYPE html>
<html>
<head>
    <title>websocket test</title>
</head>
<body>

    <ul id="ul">
    </ul>
    <input id="input" type="input" />
    < button onclick = "onclick()" > send < / button >

    <script type="text/javascript">
        var ws = new WebSocket("ws://localhost:7000");

        ws.onopen = function(evt) {
              console.log("Connection open ..."); 
        };

        ws.onmessage = function(evt) {
              console.log( "Received Message: " + evt.data);
              Addli ('accept: '+ EVT. Data);
        };

        ws.onclose = function(evt) {
              console.log("Connection closed.");
        };

        //Click send to read the value of input and print it to the console
        function onClick() {
            VaR value = 'send:' + document.getelementbyid ("input"). Value;
            ws.send(value);
            document.getElementById("input").value = '';
            addLi(value);
        }

        //A method called sring inserts a string into the list
        function addLi(value) {
            document.getElementById("ul").innerHTML += "<li>" + value + "</li>";
        }   
    </script>
</body>
</html>

effect:
Initial websocket
Initial websocket

https://zhuanlan.zhihu.com/p/…