定义一个API,用以在网页浏览器和服务器之间创建socket链接,这个链接是持久的,两边能够在任意时间开始发送数据web
优势算法
支持双向通讯,实时性强跨域
更好的二进制支持浏览器
没有同源限制,客户端能够与任意服务器通讯bash
较少的控制开销(建立后,ws客户端\服务端进行数据交换时,协议控制的数据包头部较小)服务器
let socket = new WebSocket('ws://localhost:9999');
socket.onopen = () => { // 链接成功后的回调
socket.send('hello')
}
socket.onmessage = (event) => { };// 接收到服务器数据时的回调
socket.onclose = function(event) { };// 链接关闭时的回调
socket.onerror = function(event) { };// 报错时的回调
复制代码
let webSocketServer = require('ws').Server;
let server = new webSocketServer({port: 8888}); // 支持跨域 端口号不能冲突
server.on('connection', (socket) => { // 链接成功回调
socket.on('message', (msg) => { // 监听客户端发送的消息
socket.send(msg); // 向客户端返回消息
});
});
复制代码
客户端经过HTTP请求与WebSocket服务端协商升级协议。协议升级完成后,后续的数据交换则遵守WebSocket的协议。websocket
GET ws://localhost:8888/ HTTP/1.1
Host: localhost:8888
Connection: Upgrade 表示要升级协议
Upgrade: websocket 要升级的协议
Sec-WebSocket-Version: 13 协议版本
Sec-WebSocket-Key: IHfMdf8a0aQXbwQO1pkGdA== 与服务端响应首部的Sec-WebSocket-Accept是配套的,提供基本的防御,确保服务端理解websocket链接, 避免恶意\无心等非法链接。
复制代码
HTTP/1.1 101 Switching Protocols // 101标识协议转换
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: aWAY+V/uyz5ILZEoWuWdxjnlb7E=
到此完成协议升级,后续的数据交互都按照新的协议来。
复制代码
Sec-WebSocket-Accept计算公式网络
const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; // 常量
const webSocketKey = 'IHfMdf8a0aQXbwQO1pkGdA==';
let websocketAccept = require('crypto').createHash('sha1').update(webSocketKey + CODE).digest('base64'); // crypto提供通用的加密和哈希算法
复制代码
客户端和服务器端通讯的最小单位是帧, 由1或多个帧组成一条完整的消息 客户端: 将消息切割为多个帧发送服务器端 服务器端:接收消息帧, 并将关联的帧从新组装成完整的消息socket
单位是比特
0 1 2 3
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:1个比特 表示是不是消息(message)的最后一个分片(fragment),1表明是,0表明不是tcp
RSV1, RSV2, RSV3:各占1个比特。通常状况下全为0。只有当客户端、服务端协商采用WebSocket扩展时,这三个标志位能够非0,且值的含义由扩展进行定义,不然链接出错。
Opcode: 4个比特。操做代码,即如何解析后续的数据载荷(data payload)。若是操做代码是不认识的,那么接收端应该断开链接(fail the connection)
%x0:延续帧。表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
%x1:文本帧(frame)
%x2:二进制帧(frame)
%x3-7:保留的操做代码,用于后续定义的非控制帧。
%x8:链接断开。
%x9:一个ping操做。
%xA:一个pong操做。
%xB-F:保留的操做代码,用于后续定义的控制帧。
Mask: 1个比特。表示是否要对数据载荷进行掩码操做
从客户端向服务端发送数据时,Mask都是1,即须要对数据进行掩码操做;从服务端向客户端发送数据时,不须要对数据进行掩码操做
若是服务端接收到的数据没有进行过掩码操做,服务端须要断开链接。
若是Mask是1,那么在Masking-key中会定义一个掩码键(masking-key),并用这个掩码键来对数据载荷进行反掩码。
Payload length:数据载荷的长度,单位是字节。为7位,或7+16位,或7+64位。
x为0~125:数据的长度为x字节。
x为126:后续2个字节表明一个16位的无符号整数,该无符号整数的值为数据的长度。
x为127:后续8个字节表明一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。
若是payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian,重要的位在前)
Masking-key:0或4字节(32位),全部从客户端传送到服务端的数据帧,数据载荷都进行了掩码操做,Mask为1,且携带了4字节的Masking-key。若是Mask为0,则没有Masking-key。载荷数据的长度,不包括mask key的长度
Payload data:(x+y) 字节 载荷数据:包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。
扩展数据:若是没有协商使用扩展的话,扩展数据数据为0字节。全部的扩展都必须声明扩展数据的长度,或者能够如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。若是扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。
应用数据:任意的应用数据,在扩展数据以后(若是存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就获得应用数据的长度。
let net = require('net'); // 实现tcp协议
const CODE = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const crypto = require('crypto');
let server = net.createServer((socket) => {
socket.once('data', (data) => { // once只会执行一次回调
data = data.toString(); // data为请求流
if (data.match(/Connection: Upgrade/)) { // 说明须要请求升级协议
let rows = data.split('\r\n'); //按分割符分开
rows = rows.slice(1, -2); //去掉请求行和尾部的二个分隔符
// 获取请求头
let headers = {};
rows.reduce((memo, item) => {
let [key, value] = item.split(': ');
memo[key] = value;
return memo;
}, headers);
// console.log(headers, 'headers');
if(headers['Sec-WebSocket-Version'] === '13'){ // 须要升级为13版本
let SecWebSocketKey = headers['Sec-WebSocket-Key'];
let SecWebSocketAccept = crypto.createHash('sha1').update(SecWebSocketKey + CODE).digest('base64');
let response = [
'HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${SecWebSocketAccept}`,
'\r\n', // 响应头和响应体之间有两个\r\n
].join('\r\n');
socket.write(response); // 返回响应头给客户端 代表握手成功
// 后面格式基于websocket协议
socket.on('data', (buffers) => { // data默认为buffer类型
let fin = buffers[0]&0b10000000 == 0b10000000; // 获取第一个字节的第一位 即结束位的值
let opcode = buffers[0]&0b00001111; // 获取第一个字节的后四位 即操做码
let ismask = buffers[1]&0b10000000; // 是否进行掩码
let payloadLength = buffers[1]&0b01111111;
let payload;
if (payloadLength<=125) {
if (ismask) {
let mask = buffers.slice(2,6); // 掩码键
payload = buffers.slice(6); // 携带的真实数据
// console.log(payload, 'before unmask');
unmask(payload, mask); // 对数据进行反掩码
// console.log(payload.toString(), 'unmask');
} else {
payload = buffers.slice(2);
}
} else if (payloadLength<=126) {
// ....
}
// 拼接响应帧
let res = Buffer.alloc(2+payload.length);
res[0] = 0b10000000|opcode;
res[1] = payloadLength;
payload.copy(res, 2);
socket.write(res);
});
}
}
});
});
function unmask(payload,mask){ // mask为4个字节长度
const length = payload.length;
for (let i=0;i<length;i++) {
payload[i]^=mask[i&3]; // i&3等价于1%4
}
}
server.listen(9999);
复制代码