热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

背景现在写客户端或者网页的时候,越来越多的需要与长连接打交道,尤其是在这个老板动不动就要搞一个聊天系统的时代,后端大哥们于是分分钟就能造一个基于TCP或者WebSockets的消息

背景

现在写客户端或者网页的时候, 越来越多的需要与长连接打交道, 尤其是在这个老板动不动就要搞一个聊天系统的时代, 后端大哥们于是分分钟就能造一个基于TCP或者WebSockets的消息协议出来. 但是问题在于每做一个新项目, 后端大哥们就能造出一个新协议, 而且能有各种神奇的限制. 比如说要在长连接当中保持一个状态机, 发送某条消息后收到的下一条消息一定是XXX, 或者完全一个JSON就直接丢了出来等等. 虽然都能用, 但是却需要在各种地方维护着不同的底层通信库, 没有章法可依, 所以草拟了这个协议.

目前最热门的消息协议莫过于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, 则显得过于庞杂.

简介

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

简单来说, STMP有以下特点:

  • 非常精简的固定头部, 仅有一字节(二进制序列化)
  • 支持二进制序列化(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中消息分类四种类型, 不同的消息类型可能包含的字段及含义有所不同, 详细如下:

心跳消息

双端为了保证对方连接有效性, 必需定期发送一个心跳消息给对方, 此消息一定不包含任何除了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就好了?????.


推荐阅读
  • 从零基础到精通的前台学习路线
    随着互联网的发展,前台开发工程师成为市场上非常抢手的人才。本文介绍了从零基础到精通前台开发的学习路线,包括学习HTML、CSS、JavaScript等基础知识和常用工具的使用。通过循序渐进的学习,可以掌握前台开发的基本技能,并有能力找到一份月薪8000以上的工作。 ... [详细]
  • JavaScript和Python是用于构建各种应用程序的两种有影响力的编程语言。尽管JavaScript多年来一直是占主导地位的编程语言,但Python的迅猛发展有 ... [详细]
  • webui之常用js操作(webui界面是什么)
    本文目录一览:1、web前端开发需要掌握的几个必备技术 ... [详细]
  • 基于Springboot实现Mqtt
    转载:基于Springboot实现MqttJava端开发:pom.xml: ... [详细]
  • JavaScript设计模式之策略模式(Strategy Pattern)的优势及应用
    本文介绍了JavaScript设计模式之策略模式(Strategy Pattern)的定义和优势,策略模式可以避免代码中的多重判断条件,体现了开放-封闭原则。同时,策略模式的应用可以使系统的算法重复利用,避免复制粘贴。然而,策略模式也会增加策略类的数量,违反最少知识原则,需要了解各种策略类才能更好地应用于业务中。本文还以员工年终奖的计算为例,说明了策略模式的应用场景和实现方式。 ... [详细]
  • 本文回顾了3.21开学以来的学习情况,包括javaWeb课程的迷糊感和未预习导致的不知所措,以及对VOJ题目的归类和解答。午饭前完成了阶乘相关的两道题目。下午的数据结构课听懂了队列的讲解,但有几个疑问未能及时复习。设计模式课程因预习效率低而感到困惑,同时也没搞清楚下节课的内容。晚上去图书馆学习。通过反思和总结,对自己的学习收获有了更深刻的认识。 ... [详细]
  • JVM 学习总结(三)——对象存活判定算法的两种实现
    本文介绍了垃圾收集器在回收堆内存前确定对象存活的两种算法:引用计数算法和可达性分析算法。引用计数算法通过计数器判定对象是否存活,虽然简单高效,但无法解决循环引用的问题;可达性分析算法通过判断对象是否可达来确定存活对象,是主流的Java虚拟机内存管理算法。 ... [详细]
  • Oracle优化新常态的五大禁止及其性能隐患
    本文介绍了Oracle优化新常态中的五大禁止措施,包括禁止外键、禁止视图、禁止触发器、禁止存储过程和禁止JOB,并分析了这些禁止措施可能带来的性能隐患。文章还讨论了这些禁止措施在C/S架构和B/S架构中的不同应用情况,并提出了解决方案。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
  • 大厂首发!思源笔记docker
    JVMRedisJVM面试内存模型以及分区,需要详细到每个区放什么?GC的两种判定方法GC的三种收集方法:标记清除、标记整理、复制算法的 ... [详细]
  • 这么多流媒体服务器?你怎么技术选型?
    在上一篇文章里我们介绍了我们介绍了MCU和SFU的优缺点,webRTC通信方案SFU和MCU的区别?下面就来探讨下常见的SFU开源解决方案,当然,你也可以自己实现SFU流媒体服务器 ... [详细]
  • 前端简史之纵横:Node东出
    引💡Ajax的出现,带来了jQuery时代,而jQuery时代也伴随着Node风暴淡淡退出了历史舞台。如果说Ajax给前端带来了从网页静 ... [详细]
  • html css在线便宜,在线HTML、CSS和JS工具汇总
    本文提供了在线HTML、CSS和JS工具汇总,它们都是直接在浏览器上可以使用的在线工具,基本上都是比较简单操作的,只适合简单的调试工作&# ... [详细]
  • HTTP协议之总结展望篇
    文章目录HTTP2HTTP2内核HTTP3Nginx:高性能的Web服务器OpenResty:更灵活的Web服务器网络应用防火墙(WAF)CDN ... [详细]
author-avatar
傅雪莱_
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有