1、实时定位

2、轨迹查询

3、告警管理

4、 围栏设置

5、看板与报表

Netty + RocketMQ + PostgreSQL:JT/T 808 协议网关的设计与实现(第 2 篇 · 后端技术)
上篇讲了平台的五层骨架和业务闭环。这篇钻进后端核心:我们自研的两个模块------jt-gateway (808/1078/809 三个协议网关,只翻译协议,不认识业务)和 jt-server(装下全部业务,不碰一个字节)。它们之间唯一的对话方式,是 RocketMQ 信封。
这篇的内容比较硬核:Netty 管线怎么搭、为什么要绕一圈 MQ、下行指令怎么路由到正确的网关实例、1078 流媒体网关为什么有六个端口、94 张表怎么组织。所有设计都有对应的真实教训,不是教科书照搬。
一、模块怎么切:协议面与业务面严格分离

图2-1 业务架构图
整个 JT 后端 = 两个自研模块 + 两个公共模块,边界划得非常"洁癖":
协议接入面(jt-gateway),三个独立进程,一个协议一个:
- 808 网关:Netty 长连接接入 → 拆包 → 转义还原 → 校验和 → 解码 → 30+ 消息处理器分发。它知道 0x0200 是定位,但不知道定位数据最终进了哪张表
- 1078 流媒体网关:RTP 收流 → 解复用 → 转码/转封装 FLV → 按端分发。它知道怎么把一帧 H.265 喂给浏览器,但不知道是谁在看
- 809 网关:作为客户端连上级监管平台,主从双链路、双版本兼容、断链自动重连补传
业务面(jt-server) ,一个服务装四大域:基础域(车辆档案/组织树/设备台账)、核心域(轨迹/监控/报警/围栏)、动作域(指令/视频/推送)、外延域(开放 API/报表/车务/809 转发策略)。它消费协议对象,产出业务结果,从头到尾见不到字节流。
中间靠两个公共模块粘合:
- jt-core:契约模块------MQ 信封结构、Topic 命名、Redis Key 规范,全定义在这。网关和业务都只依赖它,互不依赖
- jt-codec:编解码唯一实现,零三方依赖的纯 jar,被一个依赖扫描单测把守红线
这套切法的好处在上篇说过(发版不断连、独立扩容),这篇讲它具体怎么运转。
二、808 网关内部:一条报文的 IO 之旅
808 网关的 Netty 管线,每个 handler 职责单一,顺序有讲究:
- 帧解码器:按 0x7e 标志位拆包。TCP 是流协议,一条报文可能被切成两个包、也可能三条报文粘在一个包里,这层负责还原出完整帧
- 转义还原:808 规定报文体里出现 0x7e/0x7d 要转义成两字节序列,这层把它们还原回去。顺序不能反------必须先拆包再反转义
- 校验和验证:从消息头到消息尾逐字节异或,对不上直接丢弃并计数。别心疼丢帧,校验不过的帧解析下去只会污染更下游
- codec 解码:校验通过的纯字节交给 jt-codec,按消息 ID 找到对应的协议类,注解驱动地解析成 Java 对象。三个协议版本(2011/2013/2019)的差异全部收敛在 codec 内部,网关无感
- 业务分发:30 多个消息处理器按消息 ID 注册成分发表------定位走定位处理器、报警走报警处理器、应答走应答处理器。处理器的产出统一是"包信封、投 MQ"
两道保险丝贯穿全程:IO 线程零阻塞 (任何可能慢的操作一律扔进 MQ 或异步线程池,慢 SQL、慢 HTTP 都不允许出现在管线上)和空闲连接检测(长时间没心跳的连接主动踢掉,把文件描述符还给活连接)。
终端侧的两条"入门手续"也在网关完成:注册(0x0100,新设备领鉴权码)和鉴权(0x0102,每次上线验明正身)。鉴权通过的那一刻,网关做了一件影响全局的事------往 Redis 写一条会话:tid → 本网关实例 ID。这条数据是第七节下行路由的根基。
三、1078 流媒体网关:平台的"第二心脏"

图2-3 技术架构图
如果说 808 网关管的是"车的状态",1078 网关管的就是"车的眼睛"。它是整个后端最重的进程,六个端口各司其职:
|--------|--------------------------|
| 端口 | 职责 |
| 6802 | 实时视频收流(终端主动推 RTP 上来) |
| 6803 | 历史录像回放收流 |
| 6805 | 双向对讲 |
| 6899 | 浏览器播放(WebSocket-FLV) |
| 6810 | 小程序/H5 播放(HTTP-FLV) |
| 6809 | 对内 API(jt-server 来要流、关流) |
一路视频的生命周期是这样的:调度员在页面上点开某辆车的摄像头 → jt-server 通过 6809 告诉流媒体网关"给我拉这台车的 1 号通道" → 网关通过 808 链路向终端下发实时流请求 → 终端开始向 6802 推 RTP 流 → 网关解 RTP、取出 H.264/H.265 裸流、转封装成 FLV → 浏览器从 6899 拉 WS-FLV,小程序从 6810 拉 HTTP-FLV。
为什么收流和播放要分开?因为一对多:一路车载流可能同时被调度员的浏览器、安全员的小程序、还有录像存储三方消费,收流端只收一份,分发端各拉各的。
视频流是最怕被"忘了关"的资源------一路流没人看还挂着,带宽、内存、终端流量全在烧。所以有一条硬规则:一路流 30 秒没有任何消费者,自动回收,同时给终端发停流指令。这条规则上线前,测试环境一晚上被忘关的流吃掉了几十 GB 流量;上线后,这个问题彻底绝迹。
四、809 网关:向上级监管平台汇报
809 是"平台对平台"协议,我们的网关在监管平台面前是客户端。设计要点三个:
- 主从双链路:主链路传定位、报警等业务数据,从链路传链路检测和补传请求。监管平台就靠从链路判断你"活着没有"
- 双版本兼容:不同省份的监管平台 809 版本不一致,网关内部做了版本适配,对上呈现哪种版本可配置
- 断链补传:网络抖一下数据就缺一段,监管会考核。掉线期间的定位数据进补传队列,链路恢复后自动追平
809 网关没什么性能压力,它的难点全在"运维韧性"------重连、补传、对账,全是状态机细节。
五、MQ 信封:跨进程的唯一语言
网关和业务两个进程之间,只允许一种东西流动:统一信封 `JtMessageEnvelope`。它的关键字段:
|---------|---------------------------|
| 字段 | 作用 |
| msgId | 协议消息 ID(如 0x0200) |
| tid | 终端设备号 |
| 网关实例 ID | 这条消息从哪个网关实例来的 / 要发给哪个网关实例 |
| payload | 解码后的协议对象(业务方永远见不到字节) |
| 租户 | 多租户隔离 |
Topic 规划分两类:
- 上行类:按"实例 + 消息类型"命名,如 tp_{实例}_0x0200。定位这种海量消息按网关实例分 topic,消费端可以按实例并行扩容,互不抢
- 下行类 :统一 jt-downstream,信封里带目标实例 ID,所有网关都订阅,只有实例 ID 匹配的才真正下发,其余直接丢弃
为什么下行要"广播 + 过滤"而不是定向投递?因为简单可靠:订阅关系固定,不用维护"哪类消息去哪个 topic"的路由表;网关实例上下线时订阅关系自动就位,不需要额外的注册逻辑。代价是多投递了几份无用消息------相对定位上行的量级,下行的这点浪费可以忽略。
六、上行全链路:为什么 MQ 回环是线程模型的一部分

图2-2 流程架构图
上行链路完整走一遍:终端定位帧 → Netty IO 线程解码 → codec 注解解析 → 包信封 → MQ → 消费线程池三路并行(轨迹入库 / 围栏计算 / WS 推送)。
很多人第一次看这个链路会问:网关解码完直接调业务不行吗,为什么非要绕一圈 RocketMQ?
因为 MQ 在这里不是跨进程通信,是线程模型的一部分。三个理由:
- IO 线程必须零阻塞。Netty 的 IO 线程数量是按 CPU 核数配的,一个线程要伺候上千个连接的读写事件。一次慢 SQL(哪怕只要 200ms)卡住 IO 线程,这上千个连接的读事件全部延迟,终端判断超时,批量断线重连------上篇说的"惊群"就是这么来的
- 削峰。早高峰全城车辆集中上线,每秒数千条定位洪峰冲进来。没有队列缓冲,业务线程池瞬间打满、拒绝、丢数据;有了 MQ,洪峰变平台,消费端按自己的能力匀速拉
- 三路并行。同一条定位要干三件事:存历史(慢,写 PG)、算围栏(中等,空间计算)、推实时(快,写 WS)。三条路耗时差一个数量级,必须拆开并行消费,谁也别拖累谁
一句话:Netty 管"接得住",MQ 管"不堵车",线程池管"干得完"。
七、下行全链路:Redis 会话 + 定向投递
下行比上行难,难在一个根本问题:jt-server 想给终端发指令,但它根本不知道这台终端的 TCP 连接挂在哪个网关实例上。
解法三步:
- 上线记账:终端鉴权通过时,网关在 Redis 会话表写入 tid → 网关实例 ID;断线时删除。这张表永远反映"此刻谁连在哪儿"
- 路由查询:jt-server 组装好指令信封(带上查出来的目标实例 ID),投进 jt-downstream topic
- 过滤下发:所有网关实例都消费这个 topic,信封里的实例 ID 和自己的对上就真正下发,对不上直接丢弃。终端应答(0x0001 通用应答或带结果的拍照应答)走上行链路回来,落库指令台账
这套机制带来一个免费的福利:水平扩容 。车多了要加网关实例,只需要启动新实例、注册到 Nacos,新连上来的终端自然会把会话写到新实例名下,下行路由自动生效------不需要改任何配置,不需要数据迁移。
八、数据设计:信封、Redis 键、月分区、双轨 ORM

图2-4 数据架构图
数据层四个关键决策,每个都对应一类真实事故:
1. 统一信封消灭"方言"。 早期没有信封概念,网关给业务传数据各传各的,字段名、单位、坐标口径全靠口头约定,接一个新消息类型要两边对着改。信封统一后,新增消息类型 = codec 里加一个协议类 + 业务侧加一个消费者,网关代码一行不动。
2. Redis 键分两类,各管一件事。 终端会话键管下行路由(第七节),最后位置快照键管"看车不查库"(上篇第四节)。两类键的 TTL 策略、淘汰策略完全不同,绝不混用。
3. PG 月分区管住数据量。 轨迹、报警两张表按月分区:jt_track_202607、jt_track_202608......XXL-Job 每月 1 号凌晨自动预建下月分区,永不断档;查询历史轨迹按时间范围自动只扫相关分区;超期分区整月 detach 归档。对比传统的 DELETE WHERE create_time < ...:快几个数量级、不产生死元组、不撑爆 WAL。
4. ORM 双轨制。 低频 CRUD(档案、组织、规则,一天几百次)走 MyBatis-Plus,保留租户拦截器和 CRUD 脚手架;高频写入(轨迹,每秒数百上千条)走 JdbcTemplate 直写,绕过整个拦截器链。工具没有高低级,匹配频率的才是对的。
九、高可用:每个组件怎么"挂了也不出事"
- 808/1078 网关:无状态多实例。挂一个实例,终端 TCP 断开自动重连到存活实例,新会话重新写 Redis,分钟级自愈。唯一代价是重连瞬间的小洪峰,MQ 正好接住
- jt-server:无状态多实例,MQ 消费天然负载均衡,挂一个实例消费组自动重平衡
- RocketMQ / Redis / PG:走中间件自己的高可用方案(主从/哨兵/流复制),不在应用层发明轮子
- 809 链路:断链自动重连 + 补传队列兜底,监管侧无感知
原则一句话:自研的部分全部无状态,有状态的事全交给专业中间件。
十、运维与排障经验
三个真实好用的手段:
- 信封就是最好的日志。 全链路只认信封,排查"某台车某时刻的数据到哪了",拿 tid + 时间点顺着网关日志 → MQ 消息轨迹 → 消费日志 → 台账一路查下去,五分钟定位。链路里每多一种数据格式,排障成本翻一倍
- 分区表预建要监控。 XXL-Job 预建分区这事"忘了就出人命"(跨月那一刻写入全失败),所以给预建任务本身加了告警------调度器也可能挂,兜底的任务也要有兜底
- 压测用模拟器,别用真车。 多车压测场景全部跑在 PC 模拟器上(第 3 篇细讲),几千台虚拟终端并发上报,削峰、分区写入、WS 推送全链路压到目标水位才敢上线
十一、本篇小结
后端的核心思想一句话:协议面/业务面分离管住复杂度,MQ 信封管住通信,Redis 会话管住路由,月分区管住数据量。 四个"管住"背后,是四次真实事故的学费。
下篇《前端与测试工程篇》:数据到达前端之后的故事------上万辆车在地图上实时动是怎么渲染不卡的?自研 WS 协议长什么样?没有真车怎么做联调和验收?两台"以假乱真"的终端模拟器是答案。
文中部署地址、密钥、域名等敏感信息均已脱敏;架构图为作者基于实际项目整理绘制。