最新发布《数智时代的AI人才粮仓模型解读白皮书(2024版)》,立即领取! 了解详情
写点什么

网络报文处理的两个模式

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

评论

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

TiDB分布式事务—写写冲突

TiDB 社区干货传送门

故障排查/诊断

观测云产品更新|新增基础设施 YAML 显示;新增日志查看器 DQL 搜索模式;优化应用性能监测等

观测云

微软 × 灵雀云 × 中建信息 联合推出基于Azure的云原生全栈解决方案

York

alauda 云原生 azure 数字化转型 虚拟化

AI加速器与机器学习算法:协同设计与进化

OneFlow

机器学习 深度学习 AI 加速器

TiDB部署----openEuler2203/2003 单机部署TiDB 6.1.1

TiDB 社区干货传送门

安装 & 部署

理解elasticsearch的post_filter

程序员欣宸

elasticsearch 9月月更

十年大厂资深面试官告诉你,现在Java面试八股文都已经卷到什么程度了

Java永远的神

程序员 程序人生 设计模式 中间件 java面试

云安全是什么?是哪个企业提出的概念?

行云管家

云计算 网络安全 安全 云安全

C++学习---cstdio的源码学习分析01-类型定义

桑榆

c++ 源码阅读 9月月更

高并发之限流

源字节1号

软件开发 后端开发

Vite构建的Vue3项目打包部署到Gitee —— 全网最详细系列

海底烧烤店ai

前端 vite Vue3 部署 9月月更

VUE3中watch与watchEffect —— 全网最详细系列

海底烧烤店ai

前端 响应式编程 Vue3 9月月更

数字化转型的认识偏见十宗罪

博文视点Broadview

玩转ApiFox脚本实现自动化

Liam

测试 Postman API 测试自动化 脚本自动化

什么是RTMP 和 RTSP?它们之间有什么区别?

wljslmz

音视频 流媒体 RTMP RTSP 9月月更

Vite+Vue3+Vue-Router+Vuex+CSS预处理器(less/sass) 配置指南 —— 全网最详细系列

海底烧烤店ai

前端 Vue3 9月月更 项目搭建

StarlingX 7.0 已发布!进一步强化可扩展性、安全性及灵活性

Geek_2d6073

Qualcomm IPQ5018 solution application wifi6 wallys ,QCN9074, 2. 4G/5G

wallys-wifi6

QCN9074 ipq5018' ipq5015

使用Rust开发后端——Actix-Web

CodeWithBuff

后端 actix-web ​Rust

依据TIdb执行计划的sql调优案例分享

TiDB 社区干货传送门

性能调优 管理与运维 HTAP 场景实践 大数据场景实践

MASA Framework的分布式锁设计

MASA技术团队

.net 分布式锁 MASA Framewrok MASA

大数据问题排查系列 - 开启 Kerberos 安全的大数据环境中,Yarn Container 启动失败导致 spark/hive 作业失败

明哥的IT随笔

大数据 spark hive kerberos

跨链自动化中心 OAK Network,构建安全高效 Web3 基础设施

One Block Community

Substrate defi 跨链 区块链、

一文带你了解隐私 Layer1

TinTinLand

区块链 隐私

数字机器人及超级自动化产品和方案提供商朗思科技加入龙蜥社区

OpenAnolis小助手

AI 龙蜥社区 CLA 朗思科技

羊了个羊区块链挖矿游戏系统开发模式玩法

开发微hkkf5566

等保2.0是什么意思?谁能详细解释一下!

行云管家

云计算 等保 等级保护 等保2.0

TiDB生命周期

TiDB 社区干货传送门

实践案例 集群管理 管理与运维 数据库架构设计

MyBatis-Plus(二、常用注解)

注解 MyBatisPlus 9月月更

Dapp系统开发智能合约部署

薇電13242772558

智能合约

Rust异步初探

CodeWithBuff

异步 ​Rust

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