早点时候翻译了篇实现一个websocket服务器-理论篇 ,简单介绍了下理论基础,原本打算放在一块儿,可是感受太长了你们可能都看不下去。不过发现若是拆开的话,仍是不可避免的要说起理论部分。用到的地方就简要回顾一下好了。git
在具体代码实现以前,咱们须要大概理一下思路。回顾一下websocket的理论部分。简单的websocket流程以下(这里就不谈详细的过程了,大概描述一下)github
做为一个服务器而言,咱们主要的精力须要放在2,4这两个步骤。web
虽然websocket能够实现服务器推送,前提在于该链接已经创建。客户端仍然须要发起一个Websocket握手请求。 既然要响应该握手请求,咱们须要了解一下该请求。浏览器
客户端的握手请求是一个标准的HTTP请求,大概像下面的例子。bash
GET / HTTP/1.1 //HTTP版本必须1.1及以上,请求方式为GET
Host: localhost:8081 //本地项目
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket //指定websocket协议
Origin: http://192.168.132.170:8000
Sec-WebSocket-Version: 13 //版本
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.94 Safari/537.36
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
Cookie: optimizelyEndUserId=oeu1505722530441r0.5993643212774391; _ga=GA1.1.557695983.1505722531
Sec-WebSocket-Key: /2R6uuzPqLT/6z8fnZfN3w== //握手返回基于该密钥
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
复制代码
上面列出了实际例子中的请求头,内容由浏览器生成,须要注意的部分以下。服务器
咱们服务器处理握手时须要关注的就是上面四点。websocket
服务器根据是否websocket的必须请求头,分下面两种状况:socket
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
复制代码
请注意每个header以\r\n结尾而且在最后一个后面加入额外的\r\n。学习
这里的Sec-WebSocket-Accept 就是基于请求头中Sec-WebSocket-Key来生成。规则以下:
Sec-WebSocket-Key 和"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"连接,经过SHA-1 hash得到结果,而后返回该结果的base64编码。 代码以下:ui
// 指定拼接字符
var ws_key = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
// 生成相应key
function getAccpectKey(rSWKey) {
return crypto.createHash('sha1').update(rSWKey + ws_key).digest('base64')
}
function handShake(socket, headers) {
var reqSWKey = headers['Sec-WebSocket-Key'],
resSWKey = getAccpectKey(reqSWKey)
socket.write('HTTP/1.1 101 Switching Protocols\r\n');
socket.write('Upgrade: websocket\r\n');
socket.write('Connection: Upgrade\r\n');
socket.write('Sec-WebSocket-Accept: ' + resSWKey + '\r\n');
socket.write('\r\n');
}
复制代码
这样咱们的握手协议就算完成了,此时会触发客户端websocket的onopen事件,即websocket打开,能够进行通讯
握手协议完成以后,咱们就该解析数据了,仍是要把这张帧格式拿出来。
帧格式:
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 ... |
+---------------------------------------------------------------+
复制代码
每一个从客户端发送到服务器的数据帧都遵循上面的格式。
MASK位:只代表信息是否已进行掩码处理。来自客户端的消息必须通过处理,所以咱们应该将其置为1
opcode字段定义如何解析有效的数据:
FIN 代表是不是数据集合的最后一段消息,若是为0,服务器继续监听消息,以待消息剩余的部分。不然服务器认为消息已经彻底发送。
Payload len:有效数据长度
所谓解析数据,确定是基于上面的格式按照必定规则来进行处理。下面就是处理的规则。
直接看代码应该更加清晰。
// 解析接受的数据帧
function decodeFrame(buffer) {
/** * >>> 7 右移操做,即字节右移7位,目的是为了即只取第一位的值 * 10010030 ====> 00000001 * & 按位与 同1为1 * 15二进制表示为:00001111 ,运算以后前四位即为0,获得后四位的值 * 11011000 & 00001111 ===》 00001000 * */
var fBite = buffer[0],
/** * 获取Fin的值, * 1传输结束 * 0 继续监听 */
Fin = fBite >>> 7,
/** * 获取opcode的值,opcode为fBite的4-7位 * & 按位与 同1为1 * 15二进制表示为:00001111 ,运算以后前四位即为0,获得后四位的值 */
opcode = buffer[0] & 15,
/** * 获取有效数据长度 */
len = buffer[1] & 127,
// 是否进行掩码处理,客户端请求必须为1
Mask = buffer[1] >>> 7,
maskKey = null
// 获取数据长度
//真实长度大于125,读取后面2字节
if (len == 126) {
len = buffer.readUInt16BE(2)
} else if (len == 127) {
// 真实长度大于65535,读取后面8字节
len = buffer.readUInt64BE(2)
}
// 判断是否进行掩码处理
Mask && (maskKey = buffer.slice(2,5))
/** * 反掩码处理 * 循环遍历加密的字节(octets,text数据的单位)而且将其与第(i%4)位掩码字节(即i除以4取余)进行异或运算 */
if(Mask){
for (var i = 2;i<len ;i++){
buffer[i] = maskKey[(i - 2) % 4] ^ buffer[i];
}
}
var data = buffer.slice(2)
return {
Fin:Fin,
opcode:opcode,
data:data
}
}
复制代码
处理完接收到的数据以后,下面就是发送响应了。 响应数据不须要进行掩码运算,只须要根据帧的格式(即上面的帧),将数据进行组装就好
// 加密发送数据
function encodeFrame(data){
var len = Buffer.byteLength(data),
// 2的64位
payload_len = len > 65535 ?10:(len > 125 ? 4 : 2),
buf = new Buffer(len+payload_len)
/** * 首个字节,0x81 = 10000001 *对应的Fin 为1 opcode为001 mask 为0 * 即代表 返回数据为txt文本已经结束并未使用掩码处理 */
buf[0] = 0x81
/** * 根据真实数据长度设置payload_len位 */
if(payload_len == 2){
buf[1] = len
}else if(payload_len == 4){
buf[1] = 126;
buf.writeUInt16BE(payload_len, 2);
}else {
buf[1] = 127;
buf.writeUInt32BE(payload_len >>> 32, 2);
buf.writeUInt32BE(payload_len & 0xFFFFFFFF, 6);
}
buf.write(data, payload_len);
return buf;
}
复制代码
当收到opcode 为 9时即ping请求,直接返回具备彻底相同有效数据的pong便可。 Pings的opcode为0x9,pong是0xA,因此能够直接以下
// ping请求
if(opcode == 9){
console.log("ping相应");
/** * ping pong最大长度为125,因此能够直接拼接 * 前两位数据为10001010+数据长度 * 即传输完毕的pong响应,数据确定小于125 */
socke.write(Buffer.concat([new Buffer([0x8A, data.length]), data]))
}
复制代码
至此,一个websocket服务器的简单实现就完成了更多细节请查看。固然成熟的websocket库处理各类状况是比较完善的,更推荐你们使用,这里只是简单实践,更多的是知足一下本身的好奇心,知其然,也要知其因此然,但愿你们共同窗习和进步