全双工通信方案 socket.io

Author Avatar
呃哦 9月 27, 2020

一个兼容低版本浏览器的全双工通信方案

简介

众所周知, http 是半双工通信,在一些场景下,我们需要全双工通信,如 im 系统等。以下介绍本人对相关通信协议的理解。

协议

http 1.0/1.1

http 协议是基于 TCP 协议上的一个应用层协议。根据 OSI 七层网络模型,http 协议是第七层的应用层协议,tcp 是第四层传输层协议。

在讲 http 通行方式之前,需要先确定协议版本号,http 1.0/1.1 是都是基于 TCPTCP 本身是全双工的通信,但 http/1 这里只用到了半双工,即只能是客户端请求,服务端答复,答复后 http 就断开了 TCP 链接,服务端无法再次发送消息给客户端。

这里有个混淆点,就是 http/1.1 新增了个头部字段 keepalive,可以达到 TCP 连接复用,即多次 http 请求共用同一个 TCP 连接,避免了多次建立 TCP 隧道。但这跟全双工没有任何关系,本质上还是一个 应-答 模式的半双工通信。

websocket

基于 TCP 的一个全双工通信,为了兼容 http,header 部分跟 http 完全一样。

通常不是直接建立 websocket 连接,而是发送一个 http 请求,带有以下字段,请求服务器彼此将通信协议升级为 websocket,并约定好通信的 Sec-WebSocket-Key

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

其中主要的字段是 Sec-WebSocket-KeySec-WebSocket-Version

Sec-WebSocket-Key 是生成通信摘要用,提供请求的基本保护功能。

Sec-WebSocket-Version 是 彼此用的 websocket 协议版本号。

服务器接受到升级请求后,检查自身是否支持,而后返回握手相应,相应如下。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

请求建立,进行消息传输。

建立传输方式后就是传输内容了,在 websocket 中,数据传输采用帧的概念,区分 二进制帧文本帧

以下不在详细介绍了,毕竟 websocket 不是本文重点。

socket.io

一个支持客户端和服务端全双工通信的网络库。

socket.io 是用 engine.io 作为底层网络通信库,通信过程中,engine.io 有一套自己的数据格式协议,socket.io 也有自己的一套。

关系如下:

Socket.IO dependency graph

功能:

  1. 兼容不支持 websocket 的浏览器,通过 polling 实现,用户层无感知
  2. 支持 一对一,命名空间,房间 的通信方式
  3. 支持 掉线重连
  4. 水平扩展

实现方式:

  1. 浏览器支持

    Sauce Test Status

    对于支持 websocket 浏览器直接使用 websocket 方式通信。

    连接标识为 sid

    对于不支持的,使用 polling 方式通信,client 端发起 pollingGET 请求,如果当前没有数据,则 GET 请求会被 hang 住一个 ping_timeout 的时间。发送数据则通过 POST 方式 。

  1. 提供了命名空间(namespace) 做区分不同的路由端点,对于同一个命名空间下,TCP连接是可复用的。

    默认加入了 / 的命名空间,对于不同的命名空间,每一个命名空间都会建立一个TCP连接,但命名空间不等于是 URL path ,不会体现在 URL 路径上,而是作为参数传递给服务端。

  2. 掉线重连

    客户端对连接做监控,当掉线事件发生时,检查重连。基于连接做监控,所以如果重启浏览器则无效

    Manager.prototype.onclose = function (reason) {
      debug('onclose');
    
      this.cleanup();
      this.backoff.reset();
      this.readyState = 'closed';
      this.emit('close', reason);
    
      if (this._reconnection && !this.skipReconnect) {
        this.reconnect();
      }
    };
    
  3. 水平扩展

    基于 adapter 做房间管理,可以扩展 redis-adapter kafka-adapter,但 adapter 只是做消息的中间件,即双向通信,本质上就是一个 收 和 发的动作,adapter 只是做消息中间件,监听收和发,不做下文提到的会话功能。

FAQ

Q:websocketsocket.io 区别

A:socket.io 借鸡生蛋, websocket 协议升级部分一样,但数据传输部分,socket.io 自定了自己的一套传输协议,基于 websocketopcode

0x1 的数据帧做数据传输,ping/pong 也与 websocket 协议不一样。因此,无法用 websockt 的客户端连接 socket.io 的服务端,反之亦然。

socket.io 踩坑

  1. 部署问题

    socket.io 连接本质上是一种长连接,在服务器上,有多种语言实现,如 node 和 python。

    对于长链接,适合使用异步通信模型,而不是传统的多线程模型,node 是天生异步,python 的 socket.io 可以选择同步模型和异步 async 模型,需要注意的是选择 async 不能很好的利用多核性能。原因是协程的本质导致。

  2. 会话粘性问题

    socket.io 支持 polling 和 websocket 两种协议模式实现全双工通信,并且默认行为是 polling,然后自动升级为 websocket 通信。

    当采用多实例部署时,会遇到在A实例握手后,在B实例升级为websocket,这时候会导致失败,因为 socket.io 通过 sid 做会话唯一标识,而 sid 是进程数据,不同实例的 sid 哈希表不一致。类似于传统的 cookie 多实例会话粘性问题了。

    解决方案:

    1. socket.io 客户端设置为仅支持 websocket 协议实现,毕竟现在的浏览器基本支持 websocket
    2. 修改项目源码,将进程做的会话保持放到中间件如 redis ,但会遇到实例重启的话,需要清理 sid 问题,以及分布式下唯一性 sid 问题。
    3. 网关层实现会话粘性,始终将用户连接分配到同一个实例下