Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[2021-01-19]WebSocket原理及简单实现 #13

Open
Aria486 opened this issue Jan 19, 2021 · 0 comments
Open

[2021-01-19]WebSocket原理及简单实现 #13

Aria486 opened this issue Jan 19, 2021 · 0 comments

Comments

@Aria486
Copy link
Contributor

Aria486 commented Jan 19, 2021

WebSocket

1. 详解

名字: web + socket

1.1 socket(插座,套接字)

image

可以将服务端主机想象成一个布满各种插座的房间,每个插座有一个编号,有的插座提供 220 伏交流电,有的提供固定电话信号,有的则提供有线电视节目。客户端软件将插头接入不同编号的插座,就可以得到不同的服务

image

Socket 其实并不是一个标准的协议,而是应用层与 TCP/IP 协议族通信的中间软件抽象层,它是一组接口,工作位置基本在 OSI 模型会话层(第5层),是为了方便大家直接使用更底层协议(一般是 TCP 或 UDP )而存在的一个抽象层。

image

1.2 webSocket

为 C/S 两端提供了实时交互通信的能力,允许服务器主动发送信息给客户端,是一种区别于 HTTP 的全新双向数据流协议

传统的 TCP Socket 是一套相对标准化的 API,而出现时间不久的 WebSocket 是一种网络协议

1.2.1 解决的问题

在基于 请求/响应 模式的 HTTP/HTTPS 下,如果是对实时性要求较高的场景,客户端就需要不停的询问服务端有无可用的数据,这在各方面都是笨拙而不划算的。

早起解决方式:

  • 短轮询(Polling)

image

借助于定时器等方式,客户端不断的发送请求并得到响应。这种做法比较简单,可以在一定程度上解决问题。不过对于轮询的时间间隔需要进行仔细考虑。轮询的间隔过长,会导致用户不能及时接收到更新的数据;轮询的间隔过短,会导致查询请求过多,增加服务器端的负担。

this.timerCount = setInterval(() => this.xxxxx(), time);
clearInterval(this.timerCount);
  • 长轮询(Long-Polling)

image

这是对轮询的一种改进。客户端发出请求后,服务器端用 while(true) 等方式阻塞住请求,直到有可用数据才发送响应数据或者超时,而客户端收到响应后再发送下一个请求。

function async() {
    fetch(url).then((res) => {
    	async();
    	// success code
	}).catch(() => {
		// 超时
        async();
	})
}
  • HTTP 流 (Streaming)

image

使用 HTTP 1.1 且响应头中包含 Transfer-Encoding: chunked 的情况下,服务器发送给客户端的数据可以分成多个部分,保持打开(while-true, sleep等),并周期性 flush() 分块传输。

客户端只发送一个HTTP连接,在 xhr.readyState==3 状态下,用 xhr.responseText.substring 获取每次的数据。

但是数据响应可能会因 代理服务器 或 防火墙 等中间人造成延迟,所以可能还要额外探测这种情况以切换到长轮询方式。

缺点:

浪费带宽

CPU占用高

1.2.2 webSocket

websocket 参考

实现双向通讯

  • 原理

当客户端要和服务端建立 WebSocket 连接时,在客户端和服务器的握手过程中,客户端首先会向服务端发送一个 HTTP 请求,包含一个 Upgrade 请求头来告知服务端客户端想要建立一个 WebSocket 连接。

  • 和TCP、HTTP协议的关系

WebSocket是基于TCP的独立的协议。
和HTTP的唯一关联就是HTTP服务器需要发送一个“Upgrade”请求,即101 Switching Protocol到HTTP服务器,然后由服务器进行协议转换。

  • 优点
    • 较少的控制开销,在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于 HTTP 请求每次都要携带完整的头部,此项开销显著减少了。
    • 更强的实时性,由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;
    • 长连接,保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
    • 双向通信、更好的二进制支持。与 HTTP 协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP 协议,因此握手时不容易被屏蔽,能通过各种 HTTP 代理服务器。

2. 使用和实现

客户端使用非常简单,服务端难。。。

2.1 WebSocket API

2.1.1 front侧使用

  • 与webSocket服务连接
  • 进行一些交互操作
  • 关闭连接
// front 比较简单
const ws = new WebSocket('wss://xxxx.com/xxxx'); // 与webSocket服务连接

ws.onmessage = function(evt) {
  console.log("Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
};     

2.1.2 服务端(node)

客户端通过 HTTP Upgrade 请求,即 101 Switching Protocol 到 HTTP 服务器,然后由服务器进行协议转换。

const http = require('http');
const WebSocket = require('./websocket');
// HTTP服务器部分
const server = http.createServer(function(req, res) {
  res.end('websocket test');
});

console.log('starting...');

// Upgrade请求处理
server.on('upgrade', callback);

function callback(req, socket, upgradeHead) {
  const ws = new WebSocket(req, socket, upgradeHead);

  ws.on('data', function(opcode, payload) {
    console.log('receive data:', opcode, payload.length);
  });


  ws.on('close', function(code, reason) {
    console.log('close:', code, reason);
  });
}

server.listen(3000);

webSocket实现

接收前台数据并处理:

  • 根据协议规范,写出响应头的内容
const crypto = require('crypto');
const { EventEmitter } = require('events');
const MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

const hashWebSocketKey = (key) => {
  const sha1 = crypto.createHash('sha1');
  sha1.update(key + MAGIC_STRING, 'ascii');
  return sha1.digest('base64');
};

// 继承node内置的EventEmitter ,这样生成的实例才能监听、绑定事件
class WebSocket extends EventEmitter {
  constructor(req, socket, upgradeHead) {
    super();
    const resKey = hashWebSocketKey(req.headers['sec-websocket-key']);

    // 根据协议规范构造响应头
    const resHeaders = [
      'HTTP/1.1 101 Switching Protocols',
      'Upgrade: websocket',
      'Connection: Upgrade',
      'Sec-WebSocket-Accept: ' + resKey
    ]
      .concat('', '')
      .join('\r\n');
		
    // socket 就是 TCP 协议的抽象,直接在上面监听已有的 data 事件和 close 事件这两个事件。
    socket.on('data', data => {
      // 通信数据处理
      //....
      
      this.emit('data', data) // 以供服务端监听处理后的数据
    });

    socket.on('close', had_error => {
      // 
    });

    socket.write(resHeaders);

    this.socket = socket;
  }
}

module.exports = WebSocket;

帧顺序

报文的顺序是由TCP来保证的,所以要求消息的分片必须由发送者按给定的顺序发送给接收者,以此让 TCP给报文打上正确的编号。

  • 处理接受到数据

向前台发送数据(和接收正好相反):

  • 掩码处理,分装成数据帧
  • 发送给前台
  • 前台处理
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant