写点什么

网络报文处理的两个模式

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

评论

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

也谈“中年焦虑”

程序员架构进阶

方法论 职业规划 中年危机

太平金科助力“开局之战”顺利启动,博睿数据“A+N”一体化解决方案全力护航

博睿数据

APM npm AIOPS

博睿数据支持腾讯云函数监控,Serverless时代已来临

博睿数据

Serverless APM 监控

架构师训练营 1 期第 12 周:数据应用(一) - 作业

灵霄

极客大学架构师训练营

智慧警务平台搭建,大数据时代下的警务模式

t13823115967

大数据

架构师训练营第六周总结

Geek_xq

网易有道 iOS二面经验分享

iOSer

ios 面试 网易

架构师训练营第五周”技术选型一“总结

随秋

极客大学架构师训练营

简明设计模式—创建型

2970

设计模式 Go 语言

一代版本一代神:利用Docker在Win10系统极速体验Django3.1真实异步(Async)任务

刘悦的技术博客

django python3.x 异步 异步任务

Devil Fruit恶魔果实APP系统软件开发

系统开发

像用户一样测试:不妨犯傻

QualityFocus

软件测试 体验 可用性 用户体验

架构师训练营 1 期第 11 周:安全稳定 - 作业

灵霄

极客大学架构师训练营

AI技术在音视频领域的发展

anyRTC开发者

人工智能 ios android AI WebRTC

数字资产钱包系统开发及介绍

互联网寒冬下,程序员如何化解危机?答案全在这份阿里Java知识地图里

比伯

Java 编程 程序员 面试 计算机

架构师训练营 1 期第 8 周:性能优化(二)- 作业

灵霄

极客大学架构师训练营

架构师训练营 1 期第 9 周:性能优化(三)

灵霄

极客大学架构师训练营

【Java并发编程】阿里最喜欢问的几道线程池的面试题?

root

Java 面试 线程池

生产环境全链路压测建设历程 22:FAQ 1&2

数列科技杨德华

全链路压测 七日更

即构低延迟直播产品L3,打造更优质的实时互动体验

ZEGO即构

LeetCode题解:剑指 Offer 40. 最小的k个数,二叉堆,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

架构师训练营 1 期第 10 周:模块分解 - 作业

灵霄

极客大学架构师训练营

架构师训练营 1 期第 13 周:数据应用(二) - 作业

灵霄

极客大学架构师训练营

IDEA插件:多线程文件下载插件开发

Silently9527

Java 多线程 idea插件 文件传输

智慧公安情报研判大数据系统分析平台搭建

t13823115967

智慧公安

咨询师的诱惑

escray

程序员 面试 面经

架构师训练营第六周作业

Geek_xq

京东将上线社区团购“京喜拼拼”:社区团购是否是一次泡沫大战

石头IT视角

【Java并发编程】面试必备之线程池

root

线程池

大数据ETL批量调度,这几款工具都需要去掌握了解

敏捷调度TASKCTL

大数据 kettle 运维自动化 海豚调度 ETL算法

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