9 月 13 日,2025 Inclusion・外滩大会「开源嘉年华」正在限量报名中! 了解详情
写点什么

网络报文处理的两个模式

  • 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:007040

评论

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

Wi-Fi 6 提升了哪些方面?

BUG侦探

wifi MU-MIMO Wi-Fi 6 协商速率

恒源云gpushare.com_Byte-Pair Encoding算法超详细讲解

恒源云

自然语言处理 深度学习 NLP 大模型

Apache APISIX 社区双周报 | 1.28 线上直播预约开启

API7.ai 技术团队

后端 社区周报

有了小程序还要不要做app?

石云升

小程序 1月月更

来自开发者的点赞!网易云信揽获三大技术奖项

网易云信

资讯

有道技术团队入选 2021思否中国技术先锋年度评选两项榜单

有道技术团队

获奖

【OpenMLDB Meetup #1】会议纪要

第四范式开发者社区

机器学习 第四范式 OpenMLDB 特征平台

百亿级监控场景大数据分位值计算实践

百度Geek说

大数据 后端

政法委跨单位重点人员联防联控系统开发,重点人员管理平台

a13823115807

XSS跨站脚本攻击:获取键盘记录

喀拉峻

深入分析H2数据库控制台中无需身份验证的RCE漏洞

H

数据库 网络安全 漏洞

架构训练营 - 模块五作业

伊静西蒙

微信业务架构图&学生管理系统毕设架构

blazar

「架构实战营」

一图看懂 | 2021阿里云混合云的高能时刻

科技

投稿有奖丨阿里云云服务器ECS开发实践征文活动

阿里云弹性计算

阿里云 ECS 征文活动

架构实战营模块五作业

zhongwy

架构实战营

Hive底层 explain 执行计划详解

五分钟学大数据

hive 1月月更

WPS最大的败笔是“免费用,广告弹窗”,难怪用户纷纷使用office

淋雨

Office

阿里云EMAS 12月产品动态更新

移动研发平台EMAS

阿里云 移动研发平台 emas

Hyperf结合Redis异步队列任务async-queue实现后台操作日志写入

Owen Zhang

hyperf async-queue Redis异步队列任务

.Net Minimal API 介绍

MASA技术团队

C# .net 微软 接口 API

javaagent

淡泊明志、宁静致远

javaagent

中国AIOps们,你们究竟是在骗谁?

码农一米

云计算 云服务

深入浅出Apache Pulsar(3):Pulsar Schema

云智慧AIOps社区

云原生 消息中间件 schema Apache Pulsar 社区 java 编程

浅析安全反序列化漏洞

网络安全学海

黑客 网络安全 信息安全 渗透测试 安全漏洞

一文带你快速拆解云智慧前端技术架构

云智慧AIOps社区

前端 前端开发 可视化 框架 技术干货

零基础如何上手APICloud App、小程序多端开发

YonBuilder低代码开发平台

前端开发 APP开发 APICloud 多端开发 小程序开发

火山引擎MARS-APMPlus专栏——iOS Heimdallr 卡死卡顿监控方案与优化之路

字节跳动终端技术

ios 字节跳动 性能调优 应用性能监控产品 运维监控

详解策略梯度算法

行者AI

人工智能 强化学习

网络安全kali渗透学习 web渗透入门 Layer子域名挖掘机收集信息

学神来啦

第五周作业

cqyanbo

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