WebSocket protocol

Origin

WebSocket is design for intercommuncation[full duplex].
The http protocol is build in TCP[Transfer level], only one side can send or recevie.
When job is done, request will close the TCP connection.

If we want a long time alive connection.

  1. Http’s keep-alive head property is not design for long connection.
    It’s design for increase effciency, in avoid 3 times handclasp.
    When out of time, server will still close it.
  2. Http loop request. it’s very common strategy, but very low effciency.
  3. Make a TCP like connection, and keep it not be break.

WebSocket is just like TCP UDP work in Transfer level, a socket in origin.

Connection

Create a server service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let http = require("http");
const hostname = "127.0.0.1";
const port = "9090";

// create a service
let server = http.createServer((req, res) => {
console.log("recv request");
console.log(req.headers);
});

// create a listener
server.listen(port, hostname, () => {
console.log(`Server running at ${hostname}:${port}`);
});

Ceate a broser client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!DOCType html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script>
!function() {
const socket = new WebSocket('ws://127.0.0.1:9090');
socket.onopen = function (event) {
console.log('opened');
socket.send('hello, this is from client tiger');
};
}();
</script>
</body>
</html>


Service

Nodejs do not execute the recall function. Because another upgrade event.

1
2
3
server.on("upgrade", (request, socket, head) => {
console.log(request.headers);
}
  • If make no response then connection closed before recevie headshake:

In request header, sec-websocket-key is used to verify server is legal.

1
2
3
4
5
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: \r\n` +
'\r\n');

  • If the server response sec-websocket-key is not right, then data is dirty:

Calculate

Sec-WebSocket-Accept = base64(sha1(Sec-Websocket-key + GUID))

1
2
3
# command
npm init
npm install sha1 --save
1
2
3
4
5
6
7
8
9
10
11
12
13
14
let sha1 = require('sha1');
server.on("upgrade", (request, socket, head) => {
let secKey = request.headers['sec-websocket-key'];
// RFC 6455 (GUID)
const UNIQUE_IDENTIFIER = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
// calc sha1 base64
let shaValue = sha1(secKey + UNIQUE_IDENTIFIER),
base64Value = Buffer.from(shaValue, 'hex').toString('base64');
socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
'Upgrade: WebSocket\r\n' +
'Connection: Upgrade\r\n' +
`Sec-WebSocket-Accept: ${base64Value}\r\n` +
'\r\n');
});

ps. Look at the eyes and meet the right person :)

Recevie

Frame in documents is like this :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0               1               2               3              
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+

  1. first level is the frame’s head
    • FIN finish if the bit is 1
    • RSV1 RSV2 RSV3 reserve bit some application use it to show wheather data is compressed.
    • opcode operation code 0001 is text
    • MASK mask the data space if the bit is 1
    • Payload len data’s byte length
    • Extended ... if the length cross 2^7[127], use other 8 bytes to store length.
  2. second level is masking-key space 4 bytes
  3. third lecel is data space ? bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|Opcode  | Meaning                             | Reference |
-+--------+-------------------------------------+-----------|
| 0 | Continuation Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 1 | Text Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 2 | Binary Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 8 | Connection Close Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 9 | Ping Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|
| 10 | Pong Frame | RFC 6455 |
-+--------+-------------------------------------+-----------|

In browser if refresh the window, websocket will send the close frame:

1
2
3
4
5
6
7
buffer len =  8
<Buffer 88 82 34 b3 e3 25 37 5a>
maskFlag = 1
pLength = 2
maskKey = 52,179,227,37[34 B3 E3 25]
payloadHex = 03 E9
payloadText = Ȃ

Analysis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
socket.on('data', buffer => {
console.log('buffer len = ', buffer.length);
console.log(buffer);

// ------------------------

let bitBuffer = new BitBuffer(buffer);
let maskFlag = bitBuffer._getBit(8);
console.log('maskFlag = ' + maskFlag);

let payloadLen = bitBuffer.getBit(9, 7),
maskKeys = bitBuffer.getMaskingKey(16);

console.log('pLength = ' + payloadLen);
console.log('maskKey = ' + maskKeys +
'[' + bitBuffer.bytesToHexString(maskKeys[0]) + ' ' +
bitBuffer.bytesToHexString(maskKeys[1]) + ' ' +
bitBuffer.bytesToHexString(maskKeys[2]) + ' ' +
bitBuffer.bytesToHexString(maskKeys[3]) + ']');

let payloadText = bitBuffer.getXorString(48 / 8, payloadLen, maskKeys);
console.log('payloadText = ' + payloadText);

});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
class BitBuffer {

constructor (buffer) {
this.buffer = buffer;
this.hexChar = ['0', '1', '2', '3', '4', '5',
'6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
}
// fetch xth bit content
_getBit (offset) {
let byteIndex = offset / 8 >> 0, // target utf-8 character
byteOffset = offset % 8; // target bit offset in character
// readUInt8 read the nth character data
let num = this.buffer.readUInt8(byteIndex) & (1 << (7 - byteOffset));
return num >> (7 - byteOffset);
}

// [from, length]
getBit (offset, len = 1) {
let result = 0;
for (let i = 0; i < len; i++)
result += this._getBit(offset + i) << (len - i - 1);
return result;
}

// masking-key
getMaskingKey (offset) {
const BYTE_COUNT = 4;
let masks = [];
for (let i = 0; i < BYTE_COUNT; i++)
masks.push(this.getBit(offset + i * 8, 8));
return masks;
}

// get plaintext
getXorString (byteOffset, byteCount, maskingKeys) {
let text = '';
let hex = '';
for (let i = 0; i < byteCount; i++) {
let j = i % 4;
// get origin utf-8 encoded data though exclusive or
let transformedByte = this.buffer.readUInt8(byteOffset + i)
^ maskingKeys[j];
// put utf-8 bytes to ascii char
text += String.fromCharCode(transformedByte);
hex += this.bytesToHexString(transformedByte) + ' ';
}
console.log('payloadHex = ' + hex);
return text;
}

// char to 2-hex String
bytesToHexString(num) {
let text = '';
text += this.hexChar[num >>> 4 & 0xf];
text += this.hexChar[num & 0xf];
return text;
}
}

socket.send(‘hello, this is from client tiger’);

1
2
3
4
5
6
7
8
9
buffer len =  38
<Buffer 81 a0 f1 3d 90 39 99 58 fc 55 9e 11 b0 4d 99 54 e3 19 9
8 4e b0 5f 83 52 fd 19 92 51 f9 5c 9f 49 b0 4d 98 5a f5 4b>
maskFlag = 1
pLength = 32
maskKey = 241,61,144,57[F1 3D 90 39]
payloadHex = 68 65 6C 6C 6F 2C 20 74 68 69 73 20 69 73 20 66 7
2 6F 6D 20 63 6C 69 65 6E 74 20 74 69 67 65 72
payloadText = hello, this is from client tiger

Toke last 4 bytes for example:
origin data is 98 5a f5 4b, masking key is F1 3D 90 39

1
2
3
4
5
98 5a f5 4b XOR F1 3D 90 39 = 69 67 65 72
69 = 'i'
67 = 'g'
65 = 'e'
72 = 'r'

Last issue

In application websocket implements ping/pong[9/10] is used to make sure connection alive.
Client ping server every 30s, server response pong message frame.
If server not recevie client’s ping frame in 1 minute, it will close the connnection.

Broser websocket modual not implemented this mechanism.
If long connection is needed, you need loop ping frame by yourself.

https://www.w3.org/TR/websockets/
https://www.w3.org/TR/2011/WD-websockets-20110419/
https://tools.ietf.org/html/rfc6455

http://www.open-open.com/lib/view/open1527469228211.html
https://www.yinchengli.com/2018/05/27/chrome-websocket/
https://blog.jcoglan.com/2015/03/30/websocket-extensions-as-plugins/

https://docs.spring.io/spring-framework/docs/5.0.0.BUILD-SNAPSHOT/spring-framework-reference/html/websocket.html