早点时候翻译了篇实现一个websocket服务器-理论篇,简单介绍了下理论基础,本来打算放在一起,但是感觉太长了大家可能都看不下去。不过发现如果拆开的话,还是不可避免的要提及理论部分。用到的地方就简要回顾一下好了。

Websockt 基本通信流程

在具体代码实现之前,我们需要大概理一下思路。回顾一下websocket的理论部分。简单的websocket流程如下(这里就不谈详细的过程了,大概描述一下)

  1. 客户端发送握手请求
  2. 服务器响应、处理握手并返回
  3. 客户端验证通过后,发送数据
  4. 服务器接收、处理数据,然后返回给客户端
  5. 客户端接收服务器的推送

作为一个服务器而言,我们主要的精力需要放在2,4这两个步骤。

响应并处理握手

虽然websocket可以实现服务器推送,前提在于该连接已经建立。第客户端仍然需要发起一个Websocket握手请求。 既然要响应该握手请求,我们需要了解一下该请求。

客户端握手请求

客户端的握手请求是一个标准的HTTP请求,大概像下面的例子。

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

上面列出了实际例子中的请求头,内容由浏览器生成,需要注意的部分如下。

  • HTTP版本必须1.1及以上,请求方式为GET
  • Connection: Upgrade
  • Upgrade: websocket //指定websocket
  • Sec-WebSocket-Key 密钥 服务器处理握手的依据

我们服务器处理握手的时候需要关注的就是上面四点。

响应握手请求

服务器处理根据是否满足websocket的必须请求头,分下面两种情况:

  1. 不满足,作为http请求来响应。
  2. 满足,解析处理按照websocket规定的数据格式来响应

返回格式

    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编码。

代码如下:

// 指定拼接字符
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 ... |
+---------------------------------------------------------------+

每个从客户端发送到服务器的数据帧遵循上面的格式。

  1. MASK位:只表明信息是否已进行掩码处理。来自客户端的消息必须经过处理,因此我们应该将其置为1

  2. opcode字段定义如何解析有效的数据:

    • 0x0 继续处理
    • 0x1 text(必须是UTF-8编码)
    • 0x2 二进制 和其他叫做控制代码的数据。
    • 0x3-0x7 0xB-0xF 该版本的WebSockets无意义
  3. FIN 表明是否是数据集合的最后一段消息,如果为0,服务器继续监听消息,以待消息剩余的部分。否则服务器认为消息已经完全发送。

  4. Payload len:有效数据长度

    • Payload len<126, 即为真实长度
    • 126,说明真实长度大于125,后面2个字节的值为真实长度
    • 127,真实长度大于65535,后面8字节值为真实长度

解析数据

所谓解析数据,肯定是基于上面的格式按照一定规则来进行处理。下面就是处理的规则。

  1. 获取有效数据长度
  2. 获取掩码并依据规则进行反序列化数据

直接看代码应该更加直接。

// 解析接受的数据帧
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库处理各种情况是比较完善的,更推荐大家使用,这里只是简单实践,更多的是满足一下自己的好奇心,知其然,也要知其所以然,希望大家共同学习和进步

参考文章

细说WebSocket - Node篇

WebSocket - 初入探究 与 实现

node实现一个WEBSOCKET服务器的更多相关文章

  1. 实现一个websocket服务器-理论篇

    本文是Writing WebSocket servers的中文文档,翻译自MDNWriting WebSocket servers.篇幅略长,个人能力有限难免有所错误,抛砖引玉共同进步. websoc ...

  2. 使用node新建一个socket服务器连接Telnet客户端并且进行输入的显示

    最近在看node的socket,这个很有趣,这个可以很清晰的得到网络http请求的一个过程.首先我们需要一个Telnet的客户端,node(博主为8.0+版本) Telnet客户端的开启过程 有的系统 ...

  3. 4 —— node —— 启动一个 http 服务器

    const http = require('http'); const server = http.createServer(); // 绑定客户端请求事件 // on => 绑定事件 // r ...

  4. 根据Unix哲学来编写你的HTML5 Websocket服务器来实现全双工通信

    websocketd代表WebSocket的守护进程 websocketd处理的是浏览器和服务器之间的WebSocket连接,它会启动你所指定的服务器端应用来对WebSockets进行处理,然后在浏览 ...

  5. Erlang cowboy websocket 服务器

    Erlang cowboy websocket 服务器 原文见于: http://marcelog.github.io/articles/erlang_websocket_server_cowboy_ ...

  6. 【Netty】(7)---搭建websocket服务器

    [Netty](7)---搭建websocket服务器 说明:本篇博客是基于学习某网有关视频教学. 目的:创建一个websocket服务器,获取客户端传来的数据,同时向客户端发送数据 一.服务端 1. ...

  7. HTML5 - 开发一个自己的websocket服务器

    应用:node.js 主要步骤: 创建文件夹 创建app.js(server入口,app为自定义命名) npm init -y (快速创建一个package.json文件) 依赖包安装:nodejs- ...

  8. 前端使用node.js的http-server开启一个本地服务器

    前端使用node.js的http-server开启一个本地服务器 在写前端页面中,经常会在浏览器运行HTML页面,从本地文件夹中直接打开的一般都是file协议,当代码中存在http或https的链接时 ...

  9. 使用node.js 文档里的方法写一个web服务器

    刚刚看了node.js文档里的一个小例子,就是用 node.js 写一个web服务器的小例子 上代码 (*^▽^*) //helloworld.js// 使用node.js写一个服务器 const h ...

随机推荐

  1. 基于 webpack 的 chrome 扩展开发探索

    起 最近利用闲暇时间在进行一款 chrome 扩展 V2EX-HELPER 的开发(如果巧遇 V 友欢迎试用),今天把它彻底改成了用 webpack 打包依赖的模式,不由得感概 webpack 的强大 ...

  2. 解决 iframe 在 ios 上不能滚动的问题

    HTML代码在使用IFRAME或者其他HTML元素时,你需要使用一个元素(如DIV)来包装他们: <div class="scroll-wrapper">  <i ...

  3. 在windows下,将mysql离线数据文件导入本地mysql数据库

    1. 查看mysql路径 SELECT @@basedir AS basePath FROM DUAL 其实mysql5.6 的数据文件在 C:\ProgramData\MySQL\MySQL Ser ...

  4. Angular通信$q服务和promise对象

    promise 约定(promise)是一个对象,表示在未来时间点会发生的某件事情,约定可以是三种状态之一:等待.完成或拒绝.约定将从等待状态开始,然后可以转换为完成或者拒绝状态,一旦约定完成或者被拒 ...

  5. 将本地web服务映射到公网访问

    本文始发于我的个人博客,如需转载请注明出处. 为了更好的阅读体验,可以直接进去我的个人博客看. 项目部署 之前在学习前端的时候项目都只是在本地测试,永远的都是类似 http://localhost/x ...

  6. 关于01背包求第k优解

    引用:http://szy961124.blog.163.com/blog/static/132346674201092775320970/ 求次优解.第K优解 对于求次优解.第K优解类的问题,如果相 ...

  7. Jacobi symbol(裸雅可比符号)

    Jacobi symbol Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Tot ...

  8. float 浮动

    浮动最开始的目的是为了让文字环绕图片(一个图片和多行文字对齐)   1.包裹性:元素添加 float 属性之后 自动变成 inline-block 元素,能设置 宽高 2.破坏性:破坏自身高度,还会使 ...

  9. 串口接收模块(verilog) 波特率115200

    我来分享一下uart协议之接收verilog代码 顶层实例化 `timecale 1ns / 1ps////////////////////////////////////////////////// ...

  10. Remove Untagged Images From Docker

    I've been playing around a lot with docker. It's awesome, and it creates a whole new world of possib ...