websocket 是 html5 中的一种新的 Web 通信技术, 它实现了浏览器与服务器之间的双向通信 (full-duplex).
背景
在 Websocket 之前, 实现双向通信的技术有轮询 https://en.wikipedia.org/wiki/Polling_(computer_science) , https://en.wikipedia.org/wiki/Comet_(programming)
技术 | |
---|---|
轮询 | 客户端定时向服务器发送 Ajax 请求,服务器接到请求后马上返回响应信息并关闭连接 |
优点 | 后端容易实现 |
缺点 | 大部分是无用的请求,浪费服务器资源和带宽 |
长轮询 | 客户端向服务器发送 Ajax 请求,服务器接到请求后 hold 住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求 |
优点 | 在无消息的情况下不会频繁的请求,耗费资源小 |
缺点 | 服务器保持连接会消耗资源,返回数据顺序无保证 |
iframe | 在页面里嵌入一个隐蔵 iframe,将这个隐蔵 iframe 的 src 属性设为对一个长连接的请求或是采用 xhr 请求,服务器端就能源源不断地往客户端输入数据 |
优点 | 消息即时到达,不发无用请求;管理起来也相对方便 |
缺点 | 服务器维护一个长连接会增加开销 |
什么是 Websocket
Websocket 协议是基于 TCP https://en.wikipedia.org/wiki/Transmission_Control_Protocol 的一种新的通信协议, 可以在浏览器和服务器之间建立 "套接字 (Socket)" 连接, 简单地说: 客户端和服务器之间存在持久的连接, 而且双方都可以随时开始发送数据.
Websocket 协议有两部分, 握手和数据传输
Websocket 握手
一个典型的 Websocket 握手请求如下:
- GET / HTTP/1.1
- Upgrade: websocket
- Connection: Upgrade
- Host: example.com
- Origin: http://example.com
- Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
- Sec-WebSocket-Version: 13
服务器回应
HTTP/1.1 101 Switching Protocols
- Upgrade: websocket
- Connection: Upgrade
- Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
- Sec-WebSocket-Location: ws://example.com/
Connection 必须设置 Upgrade, 表示客户端希望连接升级.
Upgrade 字段必须设置 Websocket, 表示希望升级到 Websocket 协议.
Sec-WebSocket-Key 是随机的字符串, 服务器端会用这些数据来构造出一个 SHA-1 的信息摘要. 把 "Sec-WebSocket-Key" 加上一个特殊字符串 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11", 然后计算 https://zh.wikipedia.org/wiki/SHA-1 摘要, 之后进行 https://zh.wikipedia.org/w/index.php?title=BASE-64&action=edit&redlink=1 编码, 将结果做为 "Sec-WebSocket-Accept" 头的值, 返回给客户端. 如此操作, 可以尽量避免普通 HTTP 请求被误认为 Websocket 协议.
Sec-WebSocket-Version 表示支持的 Websocket 版本. RFC6455 要求使用的版本是 13, 之前草案的版本均应当弃用.
Origin 字段是可选的, 通常用来表示在浏览器中发起此 Websocket 连接所在的页面, 类似于 Referer https://zh.wikipedia.org/wiki/HTTP来源地址 . 但是, 与 Referer 不同的是, Origin 只包含了协议和主机名称.
其他一些定义在 HTTP 协议中的字段, 如 https://zh.wikipedia.org/wiki/Cookie 等, 也可以在 Websocket 中使用.
在 Node 中可以使用 http 模块实现一个简单的服务器来完成 Websocket 的握手.
服务器要监听 upgrade 的请求.
- server.on('upgrade', (req, socket, head) => {
- });
完成
- Sec-WebSocket-Key -> Sec-WebSocket-Accept
- const guid = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
- const key = crypto
- .createHash('sha1')
- .update(`${req.headers['sec-websocket-key']}${guid}`)
- .digest('base64');
返回必要的头信息
- socket.write(
- 'HTTP/1.1 101 Switching Protocols\r\n' +
- 'Upgrade: webSocket\r\n' +
- 'Connection: upgrade\r\n' +
- `Sec-WebSocket-Accept: ${key}\r\n` +
- '\r\n'
- );
Websocket 数据传输 (frame)
在 Websocket 协议中, 客户端和服务器端都可以互相发送数据.
发送和接受的数据如下面
- 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
- +-+-+-+-+-------+-+-------------+-------------------------------+
|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 ... |
- +---------------------------------------------------------------+
- FIN: 1bit
是否为最后的 frame 标记
FSV: 3bits
保留
opcode: 4bits
payload 数据说明
MASK: 1bit
是否有 mask 标记
Payload len: 7bits
如果 payload 长度 126, 延长 16bits, 如果 payload 长度是 127, 延长到 64bits
- Masking-key: 4 bits
- Payload Data
opcode 的类型
- |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 |
- -+--------+-------------------------------------+-----------|
在 Node 中用 net 这个模块读取 socket 中 frame 的数据
- socket.on('data', (buf: Buffer) => {
- const fro = buf[0]; // 读取第一个字节
- const fin = (fro & 0x80) === 0x80;
- const opcode = fro & 0x0f;
- console.log("fin:", fin);
- console.log("opcode:", opcode);
- const mp = buf[1]; // 读取第二个字节
- const mask = (mp & 0x80) === 0x80;
- const payloadLen = mp & 0x7f;
- console.log("mask:", mask);
- console.log("payloadLen:", payloadLen);
- // 这里做了简化处理, 实际过程中需要判断 mask 和 payloadLen
- const maskKey = buf.slice(2, 6);
- const payload = buf.slice(6, 6+payloadLen);
- const data = payload.map((p, i) => {
- return p ^ maskKey[i%4] // 利用掩码解析数据
- });
- console.log(data.toString('utf8'));
- });
发送数据也是同样的道理, 先组装一个 frame, 然后写入到 socket 中去
- const text = Buffer.from("Hello there");
- const finfo = Buffer.allocUnsafe(2);
- finfo[0] = 0b10000001;
- finfo[1] = text.length;
- const ret = Buffer.concat([finfo, text]);
- socket.write(ret);
完整的代码
浏览器端代码
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <meta http-equiv="X-UA-Compatible" content="ie=edge">
- <title>Document</title>
- </head>
- <body>
- <script>
- var ws = new WebSocket("ws://localhost:2345");
- ws.addEventListener('open', function(e) {
- ws.send("can you hear me?");
- })
- ws.addEventListener('message', function(e) {
- console.log(e.data);
- });
- </script>
- </body>
- </html>
服务器端代码 (用 ts 实现的)
- import * as http from 'http';
- import * as net from 'net';
- import * as crypto from 'crypto';
- const server = http.createServer();
- server.on('upgrade', (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => {
- const key = req.headers['sec-websocket-key'];
- let accept;
- if (key && typeof key !== 'undefined') {
- accept = webSocketAccept(key as string);
- }
- socket.write(
- 'HTTP/1.1 101 Switching Protocols\r\n' +
- 'Upgrade: webSocket\r\n' +
- 'Connection: upgrade\r\n' +
- `Sec-WebSocket-Accept: ${accept}\r\n` +
- '\r\n'
- );
- socket.on('data', (buf: Buffer) => {
- const fro = buf[0];
- const fin = (fro & 0x80) === 0x80;
- const opcode = fro & 0x0f;
- console.log("fin:", fin);
- console.log("opcode:", opcode);
- const mp = buf[1];
- const mask = (mp & 0x80) === 0x80;
- const payloadLen = mp & 0x7f;
- console.log("mask:", mask);
- console.log("payloadLen:", payloadLen);
- const maskKey = buf.slice(2, 6);
- const payload = buf.slice(6, 6+payloadLen);
- const data = payload.map((p, i) => {
- return p ^ maskKey[i%4]
- });
- console.log(data.toString('utf8'));
- const text = Buffer.from("Hello there");
- const finfo = Buffer.allocUnsafe(2);
- finfo[0] = 0b10000001;
- finfo[1] = text.length;
- const ret = Buffer.concat([finfo, text]);
- let i = 3;
- do {
- socket.write(ret);
- i = i -1;
- } while (i <3);
- });
- });
- function webSocketAccept(key: string): string {
- const hash = crypto.createHash('sha1');
- hash.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
- return hash.digest('base64');
- }
- server.listen(2345, () => {
- console.log("server started at 2345");
- });
引用
- https://zh.wikipedia.org/wiki/WebSocket
- https://en.wikipedia.org/wiki/OSI_model
- https://en.wikipedia.org/wiki/Transmission_Control_Protocol
- https://en.wikipedia.org/wiki/Duplex_(telecommunications)
- http://www.52im.net/thread-224-1-1.html
- https://www.html5rocks.com/zh/tutorials/websockets/basics/
来源: http://www.jianshu.com/p/95b189689b04