2天时间,聊今年最热的 Agent、上下文工程、AI 产品创新等话题。2025 年最后一场~ 了解详情
写点什么

网络报文处理的两个模式

  • 2012-10-23
  • 本文字数:3504 字

    阅读完需:约 11 分钟

在网络软件开发中, 不得不做的一件事是解析和处理从网络上接收到的报文. 一些标准的应用层协议比如 HTTP 等已经有各种开源的可复用的解析器, 但更多的是各种自定义的内部协议, 又或者你本身就要实现某种标准的或行业的协议, 两种情况下你都需要自己写代码来解析和处理报文.

这类软件历史悠久, 按理应该有比较成熟的设计模式. 但在最近几年接触到的几个项目中, 发现实现这些功能的代码依然缺乏必要的设计. 这些代码都是国内著名电信供应商的项目, 因此觉得有必要开始这方面的讨论. 下面两个模式是相对基础和容易想到的两个模式, 算是一个开始.

Navigator Pattern, 导航者模式


模式名称

  • Navigator/ 导航者

意图

  • 封装报文数据复杂的内部结构, 通过提供有业务含义的寻址操作来避免危险的指针运算, 以减少重复和出错的可能, 并提供更清晰的业务意义

动机

在网络通信软件的开发中, 为了传输效率或完整性的考虑, 通常在应用层协议的定义中, 一次可以发送多个单位的净荷数据, 其具体数量可用报文头中的某个字段来描述. 另外一些时候报文体的长度是不定的, 通常也用报文头中的某个字段来表示实际的报文多长.

而此类软件通常以 C 语言开发完成. 经典的实现方案是为报文定义如下的数据结构, 并以指针运算来寻址特定的数据. 而当数据结构有嵌套时, 其指针运算将变的异常复杂和易错:

复制代码
typedef struct SecondLevelPayload {
int field_1;
char field_2;
float field_3;
} SecondLevelPayload;
typedef struct TopLevelPayload {
int some_top_level_field;
int second_level_payload_count;
SecondLevelPayload* payload //payload 之间有嵌套
} TopLevelPayload;
typedef struct Message {
int count;
TopLevelPayload* payload;
} Message;
void print_datagram(Message* message) {
TopLevelPayload* current = message->payload;
for(int i = 0; i < message->count; i++) {
PrintTopLevelPayload(current++);
//or PrintTopLevelPayload(message->payload[i]);
}
}

上面最后的函数试图循环打印所有 TopLevelPayload, 但寻址方式却是错的, 因为第二个 TopLevelPayload 的地址并不是第一个的地址加上其自身结构体的长度. 这里的症结在于 C 语言缺乏描述动态集合的设施, 只有指向首地址的指针, 而指针的大小和其指向的数据的大小是不同的. 又因为数据流是连续的, 因此据此定义的结构体的大小和实际数据的大小是不一致的. 初步的改正如下:

复制代码
void print_datagram(Message* message) {
TopLevelPayload* current = message->payload;
for(int i = 0; i < message->count; i++) {
PrintTopLevelPayload(current);
current = (TopLevelPayload*)((char*)current + sizeof(TopLevelPayload) + current->second_level_payload_count * sizeof(SecondLevelPayload));
}
}

这样代码就变得晦涩, 看不出意图. 而指针运算容易出错, 且当其它代码需要在报文内部寻址的时候需要重复一遍代码来再算一次, 当报文协议 / 结构体定义变化的时候, 需要检查所有现存的指针运算看是否还合适. 我们需要更好的设计.

方案

这里的问题是寻址. 而现实生活中, 当我们需要去某个地址的时候, 我们借助导航. 它可以是一部仪器, 也可以是熟悉当地环境的路人. 但接口是一致的: 我们只需要告诉他我们要去哪, 不需要提前了解地形. 在 C 语言中, 它可以是围绕着报文首地址指针提供的一组有业务含义的接口函数:

复制代码
TopLevelPayload* goto_nth_toplevel_payload(Message* message, int nth_toplevel_payload) {
TopLevelPayload* addr = message->payload;
for(int i = 0; i < nth_toplevel_payload; i++) {
addr = (TopLevelPayload*)((char*)addr + sizeof_toplevel_payload(addr));
}
return addr;
}
SecondLevelPayload* goto_nth_secondlevel_payload(TopLevelPayload* top, int nth_secondlevel_payload) {
return top->payload + nth_secondlevel_payload;
}
static int sizeof_toplevel_payload(TopLevelPayload* payload) {
return sizeof(TopLevelPayload) + payload->second_level_payload_count * sizeof(SecondLevelPayload);
}

这样, 通过报文首地址和 goto_nth_toplevel_payload(), goto_nth_secondlevel_payload() 两个函数, 客户代码就可以在报文体中任意巡航, 而无需理会其内部表示, 无需涉及易错和晦涩的指针运算. 当报文协议变化时, 我们也只需要修改 navigator, 无需修改客户代码.

相关模式

Page Object 模式描述了在 web 应用测试领域针对易变的 web 页面进行封装的方法, 其中也涉及对页面不同元素的导航. 其解决的主要问题是减少相对频繁的页面变化对测试代码的稳定性造成的冲击, 并更清晰的描述测试意图.

SAD Pattern: Simple API for Datagram


模式名称

  • SAD, Simple API for Datagram

意图

  • 分离网络报文的解析和处理, 使解析代码和处理代码不再耦合在一起, 便于扩展. 类似 SAX(Simple API for XML) 将 XML 文档的解析和处理分离到不同的单元中

动机

在网络通信软件的开发中, 经常要处理网络上接收到的各种数据报文. 而收到某种报文后, 需要进行的处理逻辑上可能不止一件事情. 处理过程中会用到报文中的数据, 因此需要对报文进行解析. 而报文的结构通常存在动态部分, 而在 C 语言中, 无法定义一个数据结构可以直接将报文映射到该结构. 一个例子参见前面的 Navigator 模式中定义的报文结构.

缺乏考虑的做法通常会把解析和处理放在一起, 一个大函数, 用局部变量甚至全局变量来保存解析出来的数据, 并对其进行各种处理. 这样做的问题是:

  • 难以扩展: 当需要增加新的处理时, 需要在解析过程中多个地方插入处理代码
  • 难以理解: 不同的处理代码混在一起, 和报文解析的逻辑也混在一起, 难以看清楚真正做了什么事
  • 容易出错: 不同的处理共享解析出来的数据, 容易互相影响, 引入错误

另外一种常见的做法是每种不同的处理单独去解析自己需要的内容. 这种方式相对内聚, 但需要解析多遍报文结构, 解析代码也有重复

我们需要更好的设计.

方案

SAX 以事件驱动的方式分离了 XML 文档的解析和处理. 我们可以借鉴. 报文有内部结构, 我们可以使用 Navigator 模式遍历其内部结构, 并在每一个独立的净荷开始和结束时触发回调, 而对报文内容的各种处理可以以回调函数的形式注册到解析过程中, 为每种处理编写单独的回调函数.

例如, 对于 Navigator 模式中定义的报文结构, 可以定义如下的 API:

复制代码
typedef void (*MessageHandler)(Message*);
typedef void (*TopLevelPayloadHandler)(TopLevelPayload*);
typedef void (*SecondLevelPayloadHandler)(SecondLevelPayload*);
typedef struct Handler {
MessageHandler start_handle_message;
MessageHandler end_handle_message;
TopLevelPayloadHandler start_handle_toplevel_payload;
TopLevelPayloadHandler end_handle_toplevel_payload;
SecondLevelPayloadHandler start_handle_secondlevel_payload;
SecondLevelPayloadHandler end_handle_secondlevel_payload;
} Handler;
void parse(Message* message, Handler* handlers, int handler_count) {
for(int i=0; i < handler_count; i++) {
handlers[i]->start_handle_message(message);
}
// 遍历 Message 内部嵌套的 payload, 并调用对应的 handler, 比如:
//handlers[i]->start_handle_toplevel_payload(toplevel_payload_pointer);
//handlers[i]->end_handle_toplevel_payload(toplevel_payload_pointer);
for(int i=0; i < handler_count; i++) {
handlers[i]->end_handle_message(message);
}
}

而每种不同的处理, 只需提供自己的 handler 即可. 比如可以有打印报文内容的 handler, 有根据报文操作硬件的 handler, 有持久化报文数据的 handler 等:

复制代码
Handler handlers[3] = {
DataPrinter,
HardwareManipulator,
DataPersister};
parse(message, handlers, sizeof(handlers)/sizeof(handlers[0]));

效果

  • 报文解析和报文处理的代码彻底分开, 不再纠缠在一起
  • 可以很容易的扩展新的报文处理逻辑
  • 报文只需解析一遍
  • 其约束在于不同的 handler 之间不应该有依赖

相关模式

  • SAX 是处理 XML 的一种类似的模式, 但其最初的出发点是源于 DOM 的性能太差, 不过它也有分离解析和处理的效果
  • Visitor 模式用于在不改变层次结构的情况下增加对这个层次结构的处理, 并且自动分发正确的处理到正确的节点. 它客观上也分离了数据的解析和对数据的处理.

感谢张凯峰对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ )或者腾讯微博( @InfoQ )关注我们,并与我们的编辑和其他读者朋友交流。

2012-10-23 12:007130

评论

发布
暂无评论
发现更多内容

深圳企业要知道的:堡垒机就选行云管家!

行云管家

网络安全 堡垒机

如何评价OpenAi发布的视频生成模型Sora?

算法的秘密

终于有篇文章把后管权限系统设计讲清楚了

越长大越悲伤

Java spring 权限 权限控制 后台管理

Spring Security权限控制框架使用指南

越长大越悲伤

Java Spring Boot spring security

大数据时代来了

小齐写代码

软件测试学习笔记丨Docker容器镜像制作

测试人

软件测试 测试开发

怎样建立健康的绩效管理体系?聊聊专家看到的误区与疑问

思码逸研发效能

Stable Diffusion解析:探寻AI绘画背后的科技神秘

极限实验室

GAN model AI绘画 Diffusion Stable Diffusion

多租户篇 | MatrixOne与MySQL全面对比

MatrixOrigin

数据库 分布式 云原生

苹果上架App被拒绝的原因

如何确保团队协作中,项目Node版本的一致性?

秃头小帅oi

node.js 团队协作 低代码

智能护航:人工智能引领软件测试新革命

测试人

人工智能 软件测试

助力春节精准营销,火山引擎ByteHouse加速数据分析效率

字节跳动数据平台

数据库 大数据 云原生 数仓 企业号 2 月 PK 榜

教不会你算我输系列 | 手把手教你HarmonyOS应用开发

百度Geek说

HarmonyOS 鸿蒙开发 ArkTS

干货 | 如何通过度量研发效能,多角度洞察百人敏捷团队的价值交付?

思码逸研发效能

站在大模型加速带,重新审视办公提效

飞桨PaddlePaddle

百度 百度飞桨 AI应用 文心大模型 飞桨星河社区

低代码平台运营效果评估模型:AICE

鲸品堂

低代码 企业号 2 月 PK 榜

Apifox 2月版本更新:常用参数优化,自动化测试持续优化

Apifox

开发工具 Apifox 测试工具

Databend 开源周报第 133 期

Databend

NFT支持的ICO开发:开创众筹的未来

区块链软件开发推广运营

dapp开发 区块链开发 链游开发 NFT开发 公链开发

深入解析 Java 面向对象编程与类属性应用

伤感汤姆布利柏

Java js java

AI与人类联手,智能排序人类决策:RLHF标注工具打造协同标注新纪元,重塑AI训练体验

汀丶人工智能

大模型 智能标注 RLHF

第40期 | GPTSecurity周报

云起无垠

产品更新 | 如何利用思码逸DevInsight 度量代码评审效率、质量与瓶颈?

思码逸研发效能

等保测评与合规性检查定义以及区别简单了解

行云管家

等级保护 等保测评 合规性检查

CertiK CSO Dr. Kang Li 确认出席Hack .Summit() 香港区块链盛会

TechubNews

获奖!科技进步奖一等奖!成果贡献奖金奖!

天翼云开发者社区

云计算 云服务 云平台

小程序框架(概念、工作原理、发展及应用)

天津汇柏科技有限公司

小程序开发 定制软件开发 软件开发定制

我是如何参与 Apache Calcite 社区并成为 Committer 的

LakeShen

大数据 开源 Apache Calcite apache 社区 Calcite

网络报文处理的两个模式_架构_李光磊_InfoQ精选文章