背景
我们在提出开发跨平台组件之前, iOS 和 Android 客户端分别使用一套长连接组件,需要双倍的人力开发和维护;在产品需求调整上,为了在实现细节上保持一致性也具有一定的难度;Web 端与客户端长连接的形式不同,前者使用 WebSocket,后者使用 Socket ,无形中也增加了后端的维护成本。为了解决这些问题,我们基于 WebSocket 协议开发了一套跨平台的长连接组件。
架构介绍
组件自上而下分为五层:
- Native 层:负责业务请求封装和数据解析,与原生进行交互
- Chat 层:负责提供底层通信使用的 c 接口,包含连接、读写和关闭
- Websocket 层:实现 websocket 协议及维护心跳
- TLS 层 :基于
mbedTLS
实现 TLS 协议及数据加解密 - TCP 层:基于
libuv
实现 TCP 连接和数据的读写
整体架构如下图所示:
TCP 层
TCP 层我们是基于 libuv 进行开发, libuv 是一个异步 I/O 库,并且支持了多个平台( Linux ,Windows 和 Darwin ),一开始主要应用于开发 Node.js ,后来逐渐在其他项目也开始使用。文件、 网络和管道 等操作是 I/O 操作 ,libuv 为此抽象出了相关的接口,底层使用各平台上最优的 I/O 模型实现。
它的核心是提供了一个 event loop
,每个 event loop 包含了六个阶段:
- timers 阶段:这个阶段执行 timer(
setTimeout
、setInterval
)的回调 - I/O callbacks 阶段:执行一些系统调用错误,比如网络通信的错误回调
- idle , prepare 阶段:仅 node 内部使用
- poll 阶段:获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里
- check 阶段:执行
setImmediate()
的回调 - close callbacks 阶段:执行
socket
的close
事件回调
TLS 层
mbedTLS(前身PolarSSL)是实现了一套易用的加解密算法和 SSL / TLS 库。TLS 以及前身 SSL 是传输层安全协议,给网络通信提供安全和数据完整性的保障,所以它能很好的解决数据明文和劫持篡改的问题。并且其分为记录层和传输层,记录层用来确定传输层数据的封装格式,传输层则用于数据传输,而在传输之前,通信双方需要经过握手,其包含了双方身份验证
,协商加密算法
,交换加密密钥
。
Websocket 层
Websocket 层包含了对协议的实现和心跳的维护。
其最新的协议是 13 RFC 6455。协议的实现分为握手,数据发送/读取,关闭连接。
握手
握手要从请求头去理解。
WebSocket 首先发起一个 HTTP 请求,在请求头加上 Upgrade
字段,该字段用于改变 HTTP 协议版本或者是换用其他协议,这里我们把 Upgrade
的值设为 websocket
,将它升级为 WebSocket 协议。
同时要注意 Sec-WebSocket-Key
字段,它由客户端生成并发给服务端,用于证明服务端接收到的是一个可受信的连接握手,可以帮助服务端排除自身接收到的由非 WebSocket 客户端发起的连接,该值是一串随机经过 base64
编码的字符串。
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
收到请求后,服务端也会做一次响应:
HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
里面重要的是 Sec-WebSocket-Accept
,服务端通过从客户端请求头中读取 Sec-WebSocket-Key
与一串全局唯一的标识字符串(俗称魔串)“258EAFA5-E914-47DA- 95CA-C5AB0DC85B11”做拼接,生成长度为160位的 SHA-1
字符串,然后进行 base64
编码,作为 Sec-WebSocket-Accept
的值回传给客户端,客户端再去解析这个值,与自己加密编码后的字符串进行比较。
处理握手 HTTP 响应解析的时候,可以用 http-paser ,解析方式也比较简单,就是对头信息的逐字读取再处理,具体处理你可以看一下它的状态机实现。解析完成后你需要对其内容进行解析,看返回是否正确,同时去管理你的握手状态。
数据发送/读取
数据的处理需要用帧协议图来说明:
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 ... | +---------------------------------------------------------------+
首先我们来看看