搜索
bottom↓
回复: 13

谈谈网络编程中应用层(基于TCP/UDP)的协议设计

[复制链接]

出0入0汤圆

发表于 2014-4-20 17:58:13 | 显示全部楼层 |阅读模式
对于初涉网络编程的开发人员来说,在通信协议的设计上一般会有所困惑。一般的网络编程书籍上也较少涉及这方面的内容。估计是觉得太简单了。这块确实是不难,但如果不了解,又很容易出篓子或者绕弯路。下面我就来谈谈基于TCP/UDP的协议设计。
1、基于TCP的协议设计
    TCP是基于流的协议。但大部分网络应用一般会有个更小的处理单元,我们称之为帧(FRAME)。

    是否分帧

    如上所述,大部分网络应用是需要分帧的。举IM为例,用户登录是一个帧,用户发送文本信息是一个帧。少部分应用可以不需要分帧,比如:echo服务器,接收到什么直接回复即可;转发服务器,同样是接收到数据直接转给目标机器;更常见的情况是一个TCP连接只发送/处理一个请求之后就直接关闭,这种也就没必要分帧了。
   考虑到除了学习网络编程,没人做echo server。所以只要服务端不是一次连接只处理一个请求,或者纯转发,就应该采用分帧的设计。

    如何分帧

    注意:帧是业务处理的单元,是具体应用Care的,但这不关TCP的事情!初学者往往认为tcp这端 write一次,tcp那端就会read一次,然后惊呼“粘包”、“丢包”,其实这都是程序处理不当。在这边推荐一本书籍《TCP/IP协议详解 卷1》,挺薄的,看完可以减少很多对TCP的错误认识。实际上发送方发送一帧,接收方可能要N次才能读取完成,而且可能同时读到下帧的数据。那要怎么在接收方把一帧数据不多不少的读取出来呢?
   常用做法有两个:基于长度和基于终结符(Delimiter)。基于长度,就是在帧前先发送帧的长度,一般用固定长度的字节来发送此长度,比如2个字节(最大帧长不能大于65535),4个字节。(ps:我也见过使用可变长度的字节来发送此长度,比如netty中的ProtobufVarint32FrameDecoder,看代码那是相当的蛋疼,我觉得完全是折腾自己,强烈不推荐。)使用基于长度的分帧方式,接受方处理流程一般是这样:“读取固定长度的字节 -> 解析出帧长 -> 读取帧长字节 -> 处理帧”。
   基于终结符(Delimiter),最典型的应用就是HTTP协议了,使用/r/n/r/n作为终结符。使用基于终结符的分帧方式,接收方的处理流程一般是这样:“读数据 -> 在读取的数据中定位终结符 -> 没找到,将数据缓存 -> 继续读数据 -> 定位终结符 -> 找到终结符,将终结符之前的数据作为一帧进行处理”。
   使用终结符的方式务必要考虑转义问题,不然在帧的数据中出现终结符,乐子就大了。
   注意不管采用哪种方式,在开发的时候都需要考虑最大帧长的问题。不然如果对方说要发送4G长度的帧(恶意or程序错误),真的去new 4G字节的缓存;或者对方一直发送数据,没有终结符。都可能造成程序内存耗尽。
   一般来说,基于长度的分帧方式。开发更简单,程序执行效率也更高,使用更广泛些。基于终结符也不是一无是处:可读性更好,容易模拟和测试(如用telnet)。下面重点讨论基于长度的分帧方式。

    基于长度的的帧设计(length based frame design)

   一般来说,我们会将帧分为帧头(frame header,一般是固定长度)和帧体(frame body,一般是可变长度,也有固定长度的)。如上所述,最简单的帧头只要一个字段——帧长。但在实际应用中,一个典型的帧头可能还有以下字段:
   a)消息类型(message type):在一个网络应用中,往往有多种类型的帧。比如对于IM,有登陆/登出/发送消息/……。接收方需要根据帧头的消息类型字段,解码出不同种类的消息,交给相应处理模块进行处理。也就是帧的结构是Length-Type-Message,Length-Type可以视为帧头,Message是帧体。消息类型一般也是使用固定长度,比如Length 4个字节,Type 4个字节,那么帧头的长度就是8个字节。接收方处理流程:“读帧头长度字节数据 - 解码帧头获得长度和消息类型 - 读帧体长度字节数据 - 根据消息类型解码消息 - 处理消息”。Length-Type-Message结构的帧设计是使用最广泛的,普适性最好也最精简的设计。
   b)请求序列号(serials):这个不是必选项,但我觉得对于非echo式的服务(echo式的服务:总是客户端发送请求-服务端针对该请求应答,应答保证严格按照请求顺序),加上这个字段肯定不后悔。这样对于乱序(如果有消息队列后台线程池,很正常)的执行结果,才能够和请求对上号,从而做出正确的处理。一般来说,高性能的服务端要保证响应的严格有序,是比较麻烦和影响性能的。
   c)版本号(version):很多人这么用,但我觉得大部分情况下这不是个好主意。帧头应该放大部分/全部帧都需要的字段。而版本号可能只有少数包如登录会用到,所以放到登录包体里可能更合适。单独维护每个协议的版本工作量会比较大,开发起来会比较繁琐易错。至于担心解码失败,更好的方式是采用类似Protobuf这种可以向下兼容的编解码方案。
   注意:在帧头设计时应该要尽可能的精简和通用,因为帧头长度是每个帧都需要的额外开销。如果某个字段(如序列号)只有少数帧会使用到,完全可以放在帧体里去。反之,如果某个字段大部分包都有,却不定义在包头,会导致难以统一处理,增加开发工作量。这些需要根据具体业务需求来进行权衡,没有统一的答案。举个例子,Length-Type-Message结构适用于大部分情况,但如果业务要求每个帧都需要表明操作者,在帧头增加UID字段变成Length-Type-UID-Message,程序的开发会更简单。

    帧体的设计

    帧体就是字段的集合,举个例子,登录帧体包含用户名、密码这两个字段(只是举例,现实的登录包往往复杂得多)。在帧体设计上,大家往往也是八仙过海各显神通。比如基于XML、json,基于字段Pos(举登录包为例,就先写/读用户名,再写/读密码。这种方式不是太好,很难向下兼容:比如登录包需要在用户名和密码间加一个用户状态,如果服务端/客户端没有同步升级,就会斯巴达)。我甚至见过狂野得离谱的直接使用C struct的,这种NC到爆:兼容性渣不说,类对齐(可以用pragma pack避免不一致)、byte order、机器字长都会造成麻烦。
    比较推荐的做法:骚年,用Google Protobuf吧!如果要可读性好,json相比XML更省带宽。
    2、基于UDP的协议设计
   一般来说,UDP的服务器要比TCP简单得多(不过如果要实现基于UDP的可靠消息传输,就当我没说)。而且udp本来就是基于数据包的协议。write/read是可以一一对应的(不考虑丢包),所以不需要有长度字段/终结符。
   但是要注意:为了避免丢包率过高,udp包的长度一般不应该大于1500字节(大概,为了安全起见,我一般保证小于1K嘿),如果数据量较大,就需要分包了,这是比TCP麻烦的地方。
   典型的UDP的协议设计就是:Type-Message。Type长度固定,用于说明消息类型;Message是消息体,和tcp的帧体设计同样即可。

阿莫论坛20周年了!感谢大家的支持与爱护!!

一只鸟敢站在脆弱的枝条上歇脚,它依仗的不是枝条不会断,而是自己有翅膀,会飞。

出0入0汤圆

发表于 2014-4-20 18:36:12 | 显示全部楼层
mark........

出0入0汤圆

发表于 2014-4-20 19:57:50 | 显示全部楼层
谢谢分享!

出0入0汤圆

发表于 2014-4-21 09:04:46 来自手机 | 显示全部楼层
根据应用实现最好。

出0入42汤圆

发表于 2014-4-21 10:00:41 | 显示全部楼层
帖子有点意思

出0入264汤圆

发表于 2014-4-21 11:09:44 | 显示全部楼层
会串口协议帧的设计,就会这个,从应用层面上面来看,都是一个东西。

出0入0汤圆

 楼主| 发表于 2014-4-21 11:57:21 | 显示全部楼层
mcu_lover 发表于 2014-4-21 11:09
会串口协议帧的设计,就会这个,从应用层面上面来看,都是一个东西。

TCP 和串口都是流式通信
TCP是保证数据完整的 ,串口不保证

出0入0汤圆

发表于 2014-4-21 16:14:56 | 显示全部楼层
GOOD,很不错,我才开始看一点。

出0入0汤圆

发表于 2014-4-23 15:27:54 | 显示全部楼层
慢慢看,分析和串口通信的相同点。谢谢。

出0入0汤圆

发表于 2014-4-24 09:28:57 | 显示全部楼层
UDP 效率是不错,就是得自己在应用协议里加心跳等完善机制

出0入0汤圆

发表于 2014-4-24 09:48:57 | 显示全部楼层
gongxd 发表于 2014-4-21 11:57
TCP 和串口都是流式通信
TCP是保证数据完整的 ,串口不保证

hi,请教一下lz,

现在我做一个网络通讯的东西,
有个上位机,通过TCP协议发送命令数据给下位机,
下位机是tcp服务端, 上位机tcp客户端,
在通讯过程中,始终保持连接,
现在有个问题是上位机发送命令一段时间,
下位机就没反应了, 需要重新初始化,
调试过程中发现,上位机发送过来的命令过一段时间,
下位机返回相应命令的时候,数据堵塞了,上位机不接收数据,

只要上位机断开,重新连接,就会把堵塞的数据收到,然后又可以正常收发数据了,

不知道这样情况是什么原因造成的,需要注意什么,

谢谢!

出0入0汤圆

 楼主| 发表于 2014-4-24 19:36:05 | 显示全部楼层
xiaomu 发表于 2014-4-24 09:48
hi,请教一下lz,

现在我做一个网络通讯的东西,

计算机一般是不会出问题的,最可能是你自己的协议栈有问题
你先从最简单的udp做吧,那个好调试,确认硬件没有问题了

出0入0汤圆

发表于 2016-3-4 10:25:03 | 显示全部楼层
谢谢分享经验

出60入0汤圆

发表于 2016-3-22 12:24:18 | 显示全部楼层
协议写的不好,会发生死锁。比如服务器发命令给客户端,然后等待客户端响应。 但是客户端刚刚发了一个请求到服务器,等待服务器响应。
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-6-16 12:19

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表