WebSocket以及ws源码学习

WebSocket简介

HTML5开始提供的一种浏览器与服务器进行全双工通讯的网络技术,属于应用层协议。它基于TCP传输协议,并复用HTTP的握手通道。

特点:

  1. WebSocket可以在浏览器里使用
  2. 支持双向通信
  3. 使用很简单

使用

安装ws库

npm i ws

服务端

var app = require('express')();
var WebSocket = require('ws');

var wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
    // 如果有客户端连接,就会进入这个回调
    console.log('连接成功');
    // 如果有客户端发送消息,就会进入这个回调
    ws.on('message', (msg) => {
        console.log('服务端接收的消息' + msg);
    });
    // 向客户端发送消息
    ws.send('nice to meet you too');
});

app.listen(3000);

客户端

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>

<body>
  <input type="text">
  <button>发送</button>

  <script>
    var ws = new WebSocket('ws://localhost:8080');
    ws.onopen = function () {
      console.log('连接成功');
      // 向服务端发送消息
      ws.send('nice to meet you');
    };
    ws.onmessage = function (msg) {
      // 接收到服务端消息
      console.log('客户端接收的消息: ' + msg.data);
    };
  </script>
</body>

</html>

客户端效果:

服务端效果:

ws模块源码解析

426状态码

426 Upgrade Required 是一种HTTP协议的错误状态代码,表示服务器拒绝处理客户端使用当前协议发送的请求,但是可以接受其使用升级后的协议发送的请求。

这个状态码主要用在WebSockets协议中,表示客户端需要使用WebSockets协议来连接服务器。

什么意思呢?例如我们创建一个HTTP服务如果这么写:

const http = require("http");

const server = http.createServer((req,res)=>{
  const body = http.STATUS_CODES[426]
  // 设置响应头
  res.writeHead(426, {
    "Content-Length": body.length,
    "Content-Type": "text/plain",
  });
  // 发送响应体  
  res.end(body)
})

server.listen(8000)

就是告诉客户端,如果你访问我这边的服务,那么你就要进行升级服务。也就是使用WebSocket对我进行访问!

那有一个问题,如果客户端使用了WebSocket访问,服务端要怎么进行响应呢?

还直接在createServer里面的回调中处理吗?

upgrade事件

在这里面,如果客户端通过WebSocket进行访问服务端,会触发服务端server的upgrade事件,也就是说会进下面的回调函数里。

server.on('upgrade',(req, socket, head) => {
  // 固定格式
  const key = req.headers['sec-websocket-key'];
  const digest = createHash('sha1')
  .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',)
  .digest('base64');

  const headers = [
    'HTTP/1.1 101 Switching Protocols',
    'Upgrade: websocket',
    'Connection: Upgrade',
    `Sec-WebSocket-Accept: ${digest}`
  ];
  socket.write(headers.concat('\r\n').join('\r\n'));

  // 客户端发送的消息
  socket.on('data', (data) => {
    console.log(data.toString());
  })
 
  // 服务端向客户端发送消息
  socket.write('你好')
})

这个回调中,通过socket来进行服务端和客户端之间的双向通信。

转码

但是只有上面的例子,似乎每次拿到的数据都是乱码。这是因为WebSocket之间的通信的报文,不能通过Buffer的toString直接转码。这里提供一下在网上找到的转码方法:

server.on('upgrade', (req, socket, head) => {
  const key = req.headers['sec-websocket-key'];
  const digest = createHash('sha1')
  .update(key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',)
  .digest('base64');

  const headers = [
    'HTTP/1.1 101 Switching Protocols',
    'Upgrade: websocket',
    'Connection: Upgrade',
    `Sec-WebSocket-Accept: ${digest}`
  ];
  socket.write(headers.concat('\r\n').join('\r\n'));
  socket.on('data',(data) => {
    console.log(decodeSocketFrame(data).payloadBuf.toString())
    socket.write(encodeSocketFrame({
      fin:1,
      opcode:1,
      payloadBuf:Buffer.from('你好')
  }))
  })
})

function decodeSocketFrame (bufData){
  let bufIndex = 0
  const byte1 = bufData.readUInt8(bufIndex++).toString(2)
  const byte2 = bufData.readUInt8(bufIndex++).toString(2)
  console.log(byte1);
  console.log(byte2);
  const frame =  {
      fin:parseInt(byte1.substring(0,1),2),
      // RSV是保留字段,暂时不计算
      opcode:parseInt(byte1.substring(4,8),2),
      mask:parseInt(byte2.substring(0,1),2),
      payloadLen:parseInt(byte2.substring(1,8),2),
  }
  // 如果frame.payloadLen为126或127说明这个长度不够了,要使用扩展长度了
  // 如果frame.payloadLen为126,则使用Extended payload length同时为16/8字节数
  // 如果frame.payloadLen为127,则使用Extended payload length同时为64/8字节数
  // 注意payloadLen得长度单位是字节(bytes)而不是比特(bit)
  if(frame.payloadLen==126) {
      frame.payloadLen = bufData.readUIntBE(bufIndex,2);
      bufIndex+=2;
  } else if(frame.payloadLen==127) {
      // 虽然是8字节,但是前四字节目前留空,因为int型是4字节不留空int会溢出
      bufIndex+=4;
      frame.payloadLen = bufData.readUIntBE(bufIndex,4);
      bufIndex+=4;
  }
  if(frame.mask){
      const payloadBufList = []
      // maskingKey为4字节数据
      frame.maskingKey=[bufData[bufIndex++],bufData[bufIndex++],bufData[bufIndex++],bufData[bufIndex++]];
      for(let i=0;i<frame.payloadLen;i++) {
          payloadBufList.push(bufData[bufIndex+i]^frame.maskingKey[i%4]);
      }
      frame.payloadBuf = Buffer.from(payloadBufList)
  } else {
      frame.payloadBuf = bufData.slice(bufIndex,bufIndex+frame.payloadLen)
  }
  return frame
}

function encodeSocketFrame (frame){
  const frameBufList = [];
  // 对fin位移七位则为10000000加opcode为10000001
  const header = (frame.fin<<7)+frame.opcode;
  frameBufList.push(header)
  const bufBits = Buffer.byteLength(frame.payloadBuf);
  let payloadLen = bufBits;
  let extBuf;
  if(bufBits>=126) {
      //65536是2**16即两字节数字极限
      if(bufBits>=65536) {
          extBuf = Buffer.allocUnsafe(8);
          buf.writeUInt32BE(bufBits, 4);
          payloadLen = 127;
      } else {
          extBuf = Buffer.allocUnsafe(2);
          buf.writeUInt16BE(bufBits, 0);
          payloadLen = 126;
      }
  }
  let payloadLenBinStr = payloadLen.toString(2);
  while(payloadLenBinStr.length<8){payloadLenBinStr='0'+payloadLenBinStr;}
  frameBufList.push(parseInt(payloadLenBinStr,2));
  if(bufBits>=126) {
      frameBufList.push(extBuf);
  }
  frameBufList.push(...frame.payloadBuf)
  return Buffer.from(frameBufList)
}

WebSocketServer的实现

有了上面的基础,基本知道双向通信是怎么做到的了。就来看一下WebSocketServer的实现。

当我们使用的时候,我们是以这种方式:

const WebSocketServer = require('ws')

var wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (ws) => {
  
})

我们知道,connection是httpServer的回调,为什么在WebSocketServer中可以使用呢?

export default class WebSocketServer {
  constructor(port) {
    this._server = http.createServer((req, res) => {
      const body = http.STATUS_CODES[426];
      res.writeHead('426', {
        'Content-Type': 'text/align',
        'Content-Length': body.length
      })
      res.end(body)
    })

    this._server.listen(port);

    const connectionEmit = this.emit.bind(this, 'connection');
    const closeEmit = this.emit.bind(this, 'close');
    // 其他事件,都是http能监听到的;
    const map = {
      connection: connectionEmit,
      close: closeEmit
    }

    for(let emitName in map) {
      this._server.on(emitName, map[emitName])
    }
  }
}

在WebSocketServer中,如果客户端触发了http的事件时,它便将其转发到WebSocket实例上面。
然后再处理自己的逻辑。

改进后的实现

const http = require('http');
const WebSocket = require('ws');
const EventEmitter = require('events');

class WebSocketServer extends EventEmitter {
  constructor(port) {
    super();
    this._server = http.createServer((req, res) => {
      const body = http.STATUS_CODES[426];
      res.writeHead(426, {
        'Content-Type': 'text/plain',
        'Content-Length': body.length
      });
      res.end(body);
    });

    this._wss = new WebSocket.Server({ noServer: true });

    this._server.on('upgrade', (request, socket, head) => {
      this._wss.handleUpgrade(request, socket, head, (ws) => {
        this.emit('connection', ws, request);
      });
    });

    this._server.listen(port);
  }
}

module.exports = WebSocketServer;

解释

继承 EventEmitter

  • WebSocketServer 继承自 EventEmitter,使得 WebSocketServer 类能够使用事件机制。

创建 HTTP 服务器

  • HTTP 服务器在接收到普通 HTTP 请求时,返回状态码 426,表示需要升级协议。

创建 WebSocket 服务器

  • 使用 ws 模块创建一个 WebSocket 服务器实例,并配置为不绑定任何端口({ noServer: true })。

处理升级请求

  • 监听 HTTP 服务器的 upgrade 事件,该事件在接收到带有 Upgrade 头部的 HTTP 请求时触发。
  • 调用 WebSocket 服务器的 handleUpgrade 方法处理 WebSocket 握手过程。
  • 当握手成功时,触发 connection 事件,并传递 WebSocket 实例 ws 和 HTTP 请求 request。

事件机制

  • this.emit('connection', ws, request) 将 WebSocket 实例和请求传递到 connection 事件的监听器。
  • 21
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
WebSocket客户端和服务端实例源码 WebSocket ws实例 HTML5 用java实现的服务端 Websocket与服务器的正常通信 众所周知,Web 应用的交互过程通常是客户端通过浏览器发出一个请求,服务器端接收请求后进行处理并返回结果给客户端,客户端浏览器将信息呈现,这种机制对于信息变化不是特别频繁的应用尚可,但对于实时要求高、海量并发的应用来说显得捉襟见肘,尤其在当前业界移动互联网蓬勃发展的趋势下,高并发与用户实时响应是 Web 应用经常面临的问题,比如金融证券的实时信息,Web 导航应用中的地理位置获取,社交网络的实时消息推送等。 传统的请求-响应模式的 Web 开发在处理此类业务场景时,通常采用实时通讯方案,常见的是: 轮询,原理简单易懂,就是客户端通过一定的时间间隔以频繁请求的方式向服务器发送请求,来保持客户端和服务器端的数据同步。问题很明显,当客户端以固定频率向服务器端发送请求时,服务器端的数据可能并没有更新,带来很多无谓请求,浪费带宽,效率低下。 基于 Flash,AdobeFlash 通过自己的 Socket 实现完成数据交换,再利用 Flash 暴露出相应的接口为 JavaScript 调用,从而达到实时传输目的。此方式比轮询要高效,且因为 Flash 安装率高,应用场景比较广泛,但在移动互联网终端上 Flash 的支持并不好。IOS 系统中没有 Flash 的存在,在 Android 中虽然有 Flash 的支持,但实际的使用效果差强人意,且对移动设备的硬件配置要求较高。2012 年 Adobe 官方宣布不再支持 Android4.1+系统,宣告了 Flash 在移动终端上的死亡。 从上文可以看出,传统 Web 模式在处理高并发及实时性需求的时候,会遇到难以逾越的瓶颈,我们需要一种高效节能的双向通信机制来保证数据的实时传输。在此背景下,基于 HTML5 规范的、有 Web TCP 之称的 WebSocket 应运而生。 早期 HTML5 并没有形成业界统一的规范,各个浏览器和应用服务器厂商有着各异的类似实现,如 IBM 的 MQTT,Comet 开源框架等,直到 2014 年,HTML5 在 IBM、微软、Google 等巨头的推动和协作下终于尘埃落地,正式从草案落实为实际标准规范,各个应用服务器及浏览器厂商逐步开始统一,在 JavaEE7 中也实现了 WebSocket 协议,从而无论是客户端还是服务端的 WebSocket 都已完备,读者可以查阅HTML5 规范,熟悉新的 HTML 协议规范及 WebSocket 支持。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值