造轮子系列(二): 史上最简单的长链接通讯协议及实现

背景

如今写客户端或者网页的时候, 愈来愈多的须要与长链接打交道, 尤为是在这个老板动不动就要搞一个聊天系统的时代, 后端大哥们因而分分钟就能造一个基于TCP或者WebSockets的消息协议出来. 可是问题在于每作一个新项目, 后端大哥们就能造出一个新协议, 并且能有各类神奇的限制. 好比说要在长链接当中保持一个状态机, 发送某条消息后收到的下一条消息必定是XXX, 或者彻底一个JSON就直接丢了出来等等. 虽然都能用, 可是却须要在各类地方维护着不一样的底层通讯库, 没有章法可依, 因此草拟了这个协议.html

目前最热门的消息协议莫过于MQTT和gRPC了, 前者被定义为A lightweight messaging protocol for small sensors and mobile devices, optimized for high-latency or unreliable networks, 即一个为传感器和移动设备定制的消息协议. 最大的特色莫过于其固定消息头只有2字节, 以及QoS服务质量控制了. 对于前者, 无可厚非, 任何一个长链接的消息协议都应该能够作到如此, 甚至更简单(STMP即是如此), 其次其QoS设计使得通讯层面就变得很复杂, 使得其更像一个消息队列协议, 而不是简单的通讯协议. 而gRPC则是一个基于ProtocolBuffers发展起来的RPC协议以实现. 集成度很高, 底层基于HTTP 2, 因此通用性很好, 若是是作大项目而且团队有必定的技术/运维积累的话, 是很是推荐的选择, 可是这和STMP不冲突, STMP面向的是对协议健壮性要求不高, 只须要一个能用的规范的企业/团队中, 你能够用在Web端, 也能够用在客户端, 或者智能家居等嵌入式设备中, 反观gRPC, 则显得过于庞杂.git

简介

协议取名STMP, 意思是最简单的消息协议(The simplest message protocol). 项目托管在GitHub上, 包含了完整的协议文档以及相关实现, 详细了解请移步GitHub, 同时欢迎提交PR/Issue, 地址是https://github.com/acrazing/stmp.github

简单来讲, STMP有如下特色:json

  • 很是精简的固定头部, 仅有一字节(二进制序列化)
  • 支持二进制序列化(TCP)以及文本序列化(WebSockets), 文本序列化支持消息分包传送(传递二进制数据)
  • 与IP协议掩码相似的上层路由控制
  • 负载编码格式对协议透明
  • 心跳检测
  • 四种消息类型: 心跳, 请求, 通知, 回复
  • 与HTTP协议相似的返回状态码控制

消息字段定义

一个全双工的通讯系统中, 双端须要有效识别对方发来的消息, 并做出相应的处理, 选择是否回应等操做, 因此除了实际的负载以外, 还须要若干标志字段. STMP中, 完整的消息字段列表以下, 须要注意的是并非每条消息都会包含全部的这些字段, 须要根据网络环境以及消息类型肯定应该包含的字段列表. 可是若是某条消息包含了如下这些字段中的某一些字段的话,排序顺序必定与字段在下面出现的顺序相同.后端

  • 消息类型(KIND): 表示一条消息的类型, 可能的取值有:浏览器

    • 0: 心跳消息(Ping Message)
    • 1: 请求消息(Request Message)
    • 2: 通知消息(Notify Message)
    • 3: 回复消息(Response Message)
  • 消息编码格式(ENCODING): 表示负载的编码格式, 上层应用/编解码层收到消息后, 能够经过此字段对负载进行解码操做, 因为头部长度限制, 可能的取值范围为0-7, 已经约定的编码格式以下:网络

    • 0: 保留格式, 表示不包含负载, 此时消息中必定不存在PS以及PAYLOAD字段
    • 1: Protocol Buffers, 参考 Protocol Buffers
    • 2: JSON, 参考 JSON
    • 3: MessagePack, 参考 MessagePack
    • 4: BSON, 参考 BSON
    • 5: 原始二进制数据
  • 消息ID(ID): 消息的临时ID, 取值范围为0x0000-0xFFFF, 用于请求与回复消息当中, 请求方应该保证在超时的时限内此ID惟一, 回复方在回复时带上此ID以供发送方识别
  • 消息请求动做(ACTION): 请求的动做, 用于上层应用进行路由控制, 取值范围为0x00000000-0xFFFFFFFF, 即32位整型, 上层应用中能够写成xxx.xxx.xxx.xxx的形式, 与IP相似. 接收方在收到相应的动做后必需可以正确识别, 并转交给相应的处理器进行处理. 其中0x00-0xFF为保留动做, 用于协议内部使用. 目前已使用的动做有:运维

    • 0x00: 版本协商(Check Versions)
  • 状态码(STATUS): 处理结果状态码, 用在回复消息中, 代表对请求的处理结果, 取值范围为0x00-0xFF, 其中0x00-0x7F为保留取值, 含义与ACTION无关, 0x80-0xFF为用户定义的状态值, 含义根据ACTION不一样有可能不一样. 目前已定义的状态码有(和HTTP相似, 只不过换了个值而已):性能

    • 0x00: Ok, 200
    • 0x10: MovedPermanently, 301
    • 0x11: Found, 302
    • 0x12: NotModified, 304
    • 0x20: BadRequest, 400
    • 0x21: Unauthorized, 401
    • 0x22: PaymentRequired, 402
    • 0x23: Forbidden, 403
    • 0x24: NotFound, 404
    • 0x25: RequestTimeout, 408
    • 0x26: RequestEntityTooLarge, 413
    • 0x27: TooManyRequests, 429
    • 0x30: InternalServerError, 500
    • 0x31: NotImplemented, 501
    • 0x32: BadGateway, 502
    • 0x33: ServiceUnavailable, 503
    • 0x34: GatewayTimeout, 504
    • 0x35: VersionNotSupported, 505
  • 负载长度(PS): 表示PAYLOAD的长度, 以字节为单位, 取值范围为0x00000000-0xFFFFFFFF, 即负载最大长度为4Gb, 此字段存在与否由网络环境与ENCODING决定, 若是ENCODING0, 或者网络环境可以正确的分包(好比WebSockets环境), 则必定不存在此字段, 不然必定存在此字段.
  • 负载(PAYLOAD): 实际的负载, 长度由PS或者网络分包结果肯定, 编码方式由ENCODING决定, 协议自己不负责负载的编解码, 须要交由上层的应用进行解释.

消息类型

如前所述, STMP中消息分类四种类型, 不一样的消息类型可能包含的字段及含义有所不一样, 详细以下:ui

心跳消息

双端为了保证对方链接有效性, 必需按期发送一个心跳消息给对方, 此消息必定不包含任何除了KIND外的其它任何字段. 同时此消息不须要 回复, 若是一方在约定的时间内没有收到对方发送的心跳消息, 则代表对方已经断开链接或者出现异常, 应该当即断开链接.

请求消息

此消息表示发送方请求接收方返回某一个资源, 若是在指定的时间内未收到接收方的回复, 则放弃等待, 并向上层应用返回一个STATUS0x25的回复, 表示请求超时.
此消息必定包含KIND, ENCODING, ID, ACTION字段, 可能包含PS, PAYLOAD字段, 必定不包含STATUS字段.

通知消息

此消息表示发送方向接收方发送一个通知, 接收方无需回复此消息.

此消息必定包含KIND, ENCODING, ACTION字段, 可能包含PS, PAYLOAD字段, 必定不包含ID, STATUS字段.

回复消息

此消息表示发送方向接收方发送一个回复消息以回复对方曾经发送的某一条请求消息, 此消息的ID为接收方发送的此条请求消息ID. 若是上层应用在指定的时间内未返回消息, 则向发送方发送一个STATUS0x34的回复消息, 代表上层应用处理超时.

此消息必定包含KIND, ENCODING, ID, STATUS字段, 可能包含PS, PAYLOAD字段, 必定不包含ACTION字段.

消息序列化

针对不一样的网络环境, 协议制定了两套不一样的序列化方式以应对, 主要缘由是浏览器环境中将字符串转换成ArrayBuffer再经过WebSockets发送性能实在没法直视(实现方式能够参考stmp/impl/js/stmp/text.ts, 主要是将UTF-16编码和字符串转换成UTF-8的Uint8Array), 同时为了更好的Web端调试, 因此制定了一套文本序列化方案.

二进制序列化

二进制序列化中, 固定头部占一个字节, 包含KIND以及ENCODING字段, 若是KIND0, 则ENOCDING也必需为0, 表示一个心跳消息. 完整的结构以下:

|   0 ... 7   |  8 ... 15  |  16 ... 23  |  24 ... 31  |
| FixedHeader |           ID             |    ACTION   |
|               ACTION                   |    STATUS   |
|                         PS                           |
|                 PAYLOAD    ...                       |

其中的多字节字段, 包括ID, ACTION, PS字段, 若是存在的话, 必定BigEndian的方式传递. 此外, 固定头部以下:

|   0   |   1   |   2   |   3   |   4   |   5   |   6   |   7   |
|     KIND      |       ENCODING        |   0   |   0   |   0   |

最后三个位为保留位(未用到), 所有置零.

文本就序列化

全部的字段经过字符|链接, 即:

KIND(1)|ENCODING(1)|ID?(1-5)|ACTION?(1-10)|STATUS?(1-3)|PS?(1-10)|PAYLOAD?(...)

消息分割, 在使用文本序列化方式传递二进制数据时, 浏览器环境不能高效的将两者混杂在一块儿, 因此容许分红两个包进行传送, 前者传递头部信息, 后者传递实际的二进制PAYLOAD, 此时ENCODING必定不0, 同时, PAYLOAD在头部包中不存在. WebSockets自身保证了包的有序性.

对于一个心跳消息, 只有一个KIND字段, 因此其结果必定为"0".

区分文本消息与二进制消息

这是比较有趣的地方, 文本消息和二进制消息能够经过首字节彻底区别开来: 对于文本消息, 首字节为'0', '1', '2', '3'中的一个, 即0x30-0x33, 而对于二进制消息, 要么为0x00(心跳消息), 要么大于或者等于0x40, 由于KIND不为0时其值必定大于0b01000000.

版本协商

协议版本有两个字段, 分别为MAJORMINOR, 两者取值范围均为015, 即0x00xF, 能够序列化为MAJOR.MINOR的形式.

当前协议版本为0.1.

客户端在发起链接成功后, 须要发送一个ACTION为0x00的消息给服务端, 消息ID必需为0, 负载编码方式为Raw, 负载为客户端可接受的版本号
列表. 服务端在收到此消息后, 若是能够处理客户端发送过来的版本列表中的某一个, 则回复一个STATUS为Ok的回复消息, 负载为所选择的协议版本
号, 若是不能处理, 则返回一个VersionNotSupported错误消息, 负载为空, 而且关闭链接.

版本号序列化

在二进制消息中, 一个版本号序列化为1字节长度的信息, 其中前4位为MAJOR, 后4位为MINOR值. 多个版本号直接链接在一块儿. 在文本消息中, 一个版本号序列化为2字节长度的信息, 其中前1字节为MAJOR, 后1字节为MINOR值, 多个版本号直接相连.

实现

目前仅实现了Golang和JS的简单的消息编解码部分, 地址在: go版本, js版本, 还有不少工做要作T_T, 若是有人提PR就行了?????.

相关文章
相关标签/搜索