博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Nodejs教程20:WebSocket之二:用原生实现WebSocket应用
阅读量:7030 次
发布时间:2019-06-28

本文共 10221 字,大约阅读时间需要 34 分钟。

阅读更多系列文章请访问我的,示例代码请访问。

原生实现WebSocket应用

上一节使用了Socket.io实现WebSocket,也是开发中常用的方式。

但这样不利于了解其原理,这一节使用Nodejs的Net模块和Web端的WebSocket API实现WebSocket服务器。

示例代码:/lesson20/server.js,/lesson20/index.html

1. 服务端创建一个Net服务器

// 引入net模块const net = require('net')// 使用net模块创建服务器,返回的是一个原始的socket对象,与Socket.io的socket对象不同。const server = net.createServer((socket) => {  })server.listen(8080)复制代码

2. Web端创建一个WebSocket链接

创建一个WebSocket连接,此时控制台的Network模块可以看到一个处于pending状态的HTTP连接。

这个连接是一个HTTP请求,与普通HTTP请求的请求你头相比,增加了以下内容:

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits // 扩展信息

Sec-WebSocket-Key: O3PKSb95qaSB7/+XfaTg7Q== // 发送一个Key到服务端,用于校验服务端是否支持WebSocket

Sec-WebSocket-Version: 13 // WebSocket版本

Upgrade: websocket // 告知服务器通信协议将会升级到WebSocket若服务器支持则继续下一步

const ws = new WebSocket('ws://localhost:8080/')复制代码

3. 服务端使用socket.once,触发一次data事件处理HTTP请求头数据

socket.once('data', (buffer) => {  // 接收到HTTP请求头数据  const str = buffer.toString()  console.log(str)})复制代码

打印结果如下:

GET / HTTP/1.1Host: localhost:8080Connection: UpgradePragma: no-cacheCache-Control: no-cacheUpgrade: websocketOrigin: file://Sec-WebSocket-Version: 13User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML,like Gecko) Chrome/72.0.3626.121 Safari/537.36Accept-Encoding: gzip, deflate, brAccept-Language: zh-CN,zh;q=0.9Cookie: _ga=GA1.1.1892261700.1545540050; _gid=GA1.1.774798563.1552221410; io=7X0VY8jhwRTdRHBfAAABSec-WebSocket-Key: JStOineTIKaQskxefzer7Q==Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits复制代码

将回车符转换为\r\n显示,结果如下:

GET / HTTP/1.1\r\nHost: localhost:8080\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Control: no-cache\r\nUpgrade: websocket\r\nOrigin: file://\r\nSec-WebSocket-Version: 13\r\nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36\r\nAccept-Encoding: gzip, deflate, br\r\nAccept-Language: zh-CN,zh;q=0.9\r\nCookie: _ga=GA1.1.1892261700.1545540050; _gid=GA1.1.774798563.1552221410; io=7X0VY8jhwRTdRHBfAAAB\r\nSec-WebSocket-Key: dRB1xDJ/vV+IAGnG7TscNQ==\r\nSec-WebSocket-Extensions: permessage-deflate; client_max_window_bits\r\n\r\n复制代码

通过观察请求头数据,可以发现数据是以key: value的形式显示,可以通过字符串切割,将其转换为对象格式。

4. 将请求头字符串转换为对象

创建一个parseHeader方法处理请求头。

function parseHeader(str) {  // 将请求头数据按回车符切割为数组,得到每一行数据  let arr = str.split('\r\n').filter(item => item)  // 第一行数据为GET / HTTP/1.1,可以丢弃。  arr.shift()  console.log(arr)  /*     处理结果为:    [ 'Host: localhost:8080',      'Connection: Upgrade',      'Pragma: no-cache',      'Cache-Control: no-cache',      'Upgrade: websocket',      'Origin: file://',      'Sec-WebSocket-Version: 13',      'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36',      'Accept-Encoding: gzip, deflate, br',      'Accept-Language: zh-CN,zh;q=0.9',      'Cookie: _ga=GA1.1.1892261700.1545540050; _gid=GA1.1.774798563.1552221410; io=7X0VY8jhwRTdRHBfAAAB',      'Sec-WebSocket-Key: jqxd7P0Xx9TGkdMfogptRw==',      'Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits' ]  */  let headers = {}  // 存储最终处理的数据  arr.forEach((item) => {    // 需要用":"将数组切割成key和value    let [name, value] = item.split(':')    // 去除无用的空格,将属性名转为小写    name = name.replace(/^\s|\s+$/g, '').toLowerCase()    value = value.replace(/^\s|\s+$/g, '')    // 获取所有的请求头属性    headers[name] = value  })  return headers}复制代码

打印结果如下:

{ host: 'localhost',  connection: 'Upgrade',  pragma: 'no-cache',  'cache-control': 'no-cache',  upgrade: 'websocket',  origin: 'file',  'sec-websocket-version': '13',  'user-agent':   'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36',  'accept-encoding': 'gzip, deflate, br',  'accept-language': 'zh-CN,zh;q=0.9',  cookie:   '_ga=GA1.1.1892261700.1545540050; _gid=GA1.1.585339125.1552405260',  'sec-websocket-key': 'TipyPZNW+KNvV3fePNpriw==',  'sec-websocket-extensions': 'permessage-deflate; client_max_window_bits' }复制代码

5. 根据请求头参数,判断是否WebSocket请求

根据headers['upgrade'] !== 'websocket',判断该HTTP连接是否可升级为WebSocket,若可以升级,表示为WebSocket请求。

根据headers['sec-websocket-version'] !== '13',判断WebSocket的版本是否为13,以免因为版本不同出现兼容问题。

socket.once('data', (buffer) => {  // 接收到HTTP请求头数据  const str = buffer.toString()  console.log(str)  // 4. 将请求头数据转为对象  const headers = parseHeader(str)  console.log(headers)  // 5. 判断请求是否为WebSocket连接  if (headers['upgrade'] !== 'websocket') {    // 若当前请求不是WebSocket连接,则关闭连接    console.log('非WebSocket连接')    socket.end()  } else if (headers['sec-websocket-version'] !== '13') {    // 判断WebSocket版本是否为13,防止是其他版本,造成兼容错误    console.log('WebSocket版本错误')    socket.end()  } else {    // 请求为WebSocket连接时,进一步处理  }})复制代码

6. 校验Sec-WebSocket-Key,完成连接

根据协议规定的方式,向前端返回一个请求头,完成建立WebSocket连接的过程。

可参考:

若客户端校验结果正确,在控制台的Network模块可以看到HTTP请求的状态码变为101 Switching Protocols,同时客户端的ws.onopen事件被触发。

socket.once('data', (buffer) => {  // 接收到HTTP请求头数据  const str = buffer.toString()  console.log(str)  // 4. 将请求头数据转为对象  const headers = parseHeader(str)  console.log(headers)  // 5. 判断请求是否为WebSocket连接  if (headers['upgrade'] !== 'websocket') {    // 若当前请求不是WebSocket连接,则关闭连接    console.log('非WebSocket连接')    socket.end()  } else if (headers['sec-websocket-version'] !== '13') {    // 判断WebSocket版本是否为13,防止是其他版本,造成兼容错误    console.log('WebSocket版本错误')    socket.end()  } else {      // 6. 校验Sec-WebSocket-Key,完成连接      /*         协议中规定的校验用GUID,可参考如下链接:        https://tools.ietf.org/html/rfc6455#section-5.5.2        https://stackoverflow.com/questions/13456017/what-does-258eafa5-e914-47da-95ca-c5ab0dc85b11-means-in-websocket-protocol      */      const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'      const key = headers['sec-websocket-key']      const hash = crypto.createHash('sha1')  // 创建一个签名算法为sha1的哈希对象      hash.update(`${key}${GUID}`)  // 将key和GUID连接后,更新到hash      const result = hash.digest('base64') // 生成base64字符串	  const header = `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: ${result}\r\n\r\n` // 生成供前端校验用的请求头      socket.write(header)  // 返回HTTP头,告知客户端校验结果,HTTP状态码101表示切换协议:https://httpstatuses.com/101。      // 若客户端校验结果正确,在控制台的Network模块可以看到HTTP请求的状态码变为101 Switching Protocols,同时客户端的ws.onopen事件被触发。      console.log(header)            // 处理聊天数据    }  })复制代码

7. 建立连接后,通过data事件接收客户端的数据并处理

连接开始后,可以在控制台的Network模块看到,该连接会一直保留在pending状态,直到连接断开。

此时可以通过data事件处理客户端的数据,但此时双方通信的数据为二进制,需要按照其格式进行处理后才可以正常使用。

格式如下:

处理收到的数据:

function decodeWsFrame(data) {  let start = 0;  let frame = {    isFinal: (data[start] & 0x80) === 0x80,    opcode: data[start++] & 0xF,    masked: (data[start] & 0x80) === 0x80,    payloadLen: data[start++] & 0x7F,    maskingKey: '',    payloadData: null  };  if (frame.payloadLen === 126) {    frame.payloadLen = (data[start++] << 8) + data[start++];  } else if (frame.payloadLen === 127) {    frame.payloadLen = 0;    for (let i = 7; i >= 0; --i) {      frame.payloadLen += (data[start++] << (i * 8));    }  }  if (frame.payloadLen) {    if (frame.masked) {      const maskingKey = [        data[start++],        data[start++],        data[start++],        data[start++]      ];      frame.maskingKey = maskingKey;      frame.payloadData = data        .slice(start, start + frame.payloadLen)        .map((byte, idx) => byte ^ maskingKey[idx % 4]);    } else {      frame.payloadData = data.slice(start, start + frame.payloadLen);    }  }  console.dir(frame)  return frame;}复制代码

处理发出的数据:

function encodeWsFrame(data) {  const isFinal = data.isFinal !== undefined ? data.isFinal : true,    opcode = data.opcode !== undefined ? data.opcode : 1,    payloadData = data.payloadData ? Buffer.from(data.payloadData) : null,    payloadLen = payloadData ? payloadData.length : 0;  let frame = [];  if (isFinal) frame.push((1 << 7) + opcode);  else frame.push(opcode);  if (payloadLen < 126) {    frame.push(payloadLen);  } else if (payloadLen < 65536) {    frame.push(126, payloadLen >> 8, payloadLen & 0xFF);  } else {    frame.push(127);    for (let i = 7; i >= 0; --i) {      frame.push((payloadLen & (0xFF << (i * 8))) >> (i * 8));    }  }  frame = payloadData ? Buffer.concat([Buffer.from(frame), payloadData]) : Buffer.from(frame);  console.dir(decodeWsFrame(frame));  return frame;}复制代码

对聊天数据进行处理:

socket.once('data', (buffer) => {  // 接收到HTTP请求头数据  const str = buffer.toString()  console.log(str)  // 4. 将请求头数据转为对象  const headers = parseHeader(str)  console.log(headers)  // 5. 判断请求是否为WebSocket连接  if (headers['upgrade'] !== 'websocket') {    // 若当前请求不是WebSocket连接,则关闭连接    console.log('非WebSocket连接')    socket.end()  } else if (headers['sec-websocket-version'] !== '13') {    // 判断WebSocket版本是否为13,防止是其他版本,造成兼容错误    console.log('WebSocket版本错误')    socket.end()  } else {      // 6. 校验Sec-WebSocket-Key,完成连接      /*         协议中规定的校验用GUID,可参考如下链接:        https://tools.ietf.org/html/rfc6455#section-5.5.2        https://stackoverflow.com/questions/13456017/what-does-258eafa5-e914-47da-95ca-c5ab0dc85b11-means-in-websocket-protocol      */      const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'      const key = headers['sec-websocket-key']      const hash = crypto.createHash('sha1')  // 创建一个签名算法为sha1的哈希对象      hash.update(`${key}${GUID}`)  // 将key和GUID连接后,更新到hash      const result = hash.digest('base64') // 生成base64字符串	  const header = `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-Websocket-Accept: ${result}\r\n\r\n` // 生成供前端校验用的请求头      socket.write(header)  // 返回HTTP头,告知客户端校验结果,HTTP状态码101表示切换协议:https://httpstatuses.com/101。      // 若客户端校验结果正确,在控制台的Network模块可以看到HTTP请求的状态码变为101 Switching Protocols,同时客户端的ws.onopen事件被触发。      console.log(header)            // 7. 建立连接后,通过data事件接收客户端的数据并处理      socket.on('data', (buffer) => {        const data = decodeWsFrame(buffer)        console.log(data)        console.log(data.payloadData && data.payloadData.toString())        // opcode为8,表示客户端发起了断开连接        if (data.opcode === 8) {          socket.end()  // 与客户端断开连接        } else {          // 接收到客户端数据时的处理,此处默认为返回接收到的数据。          socket.write(encodeWsFrame({ payloadData: `服务端接收到的消息为:${data.payloadData ? data.payloadData.toString() : ''}` }))        }      })    }  })复制代码

这样,一个简单的基于WebSocket的聊天应用就创建完成了,在启动服务器后,可以打开index.html看到效果。

转载地址:http://fnwal.baihongyu.com/

你可能感兴趣的文章
BZOJ 1084 最大子矩阵
查看>>
2018杭电多校第三场1007(凸包,极角排序)
查看>>
django中orm的简单操作
查看>>
Mybatis知识(1)
查看>>
[CentOS] 7 不执行文件 /etc/rc.d/rc.local
查看>>
模态窗口的各个属性
查看>>
10.28 (上午) 开课一个月零二十四天 (数据访问)
查看>>
为什么你应该(从现在开始就)写博客
查看>>
小技巧积累
查看>>
Java JDBC链接Oracle数据库
查看>>
Moss2010 部署命令
查看>>
Git 操作分支
查看>>
Grid search in the tidyverse
查看>>
hdu 三部曲 Contestants Division
查看>>
day22——创建表、增加数据、查询数据
查看>>
css伪元素实现tootip提示框
查看>>
关于函数指针的总结
查看>>
采用PHP函数uniqid生成一个唯一的ID
查看>>
Centos7安装32位库用来安装32位软件程序
查看>>
【HMOI】小C的填数游戏 DP+线段树维护
查看>>