对于初涉网络编程的开发人员来讲,在通讯协议的设计上通常会有所困惑。通常的网络编程书籍上也较少涉及这方面的内容。估计是以为太简单了。这块确实是不难,但若是不了解,又很容易出篓子或者绕弯路。下面我就来谈谈基于TCP/UDP的协议设计。
一、基于TCP的协议设计
TCP是基于流的协议。但大部分网络应用通常会有个更小的处理单元,咱们称之为帧(FRAME)。编程
也就是说,应用层在处理数据的通讯的时候必定要按照Frame为单位来处理。json
如上所述,大部分网络应用是须要分帧的。举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)。下面重点讨论基于长度的分帧方式。安全
通常来讲,咱们会将帧分为帧头(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的,这种脑残到爆:兼容性渣不说,类对齐(能够用pragma pack避免不一致)、byte order、机器字长都会形成麻烦。
比较推荐的作法:骚年,用Google Protobuf吧!若是要可读性好,json相比XML更省带宽。
二、基于UDP的协议设计
通常来讲,UDP的服务器要比TCP简单得多(不过若是要实现基于UDP的可靠消息传输,就当我没说)。并且udp原本就是基于数据包的协议。write/read是能够一一对应的(不考虑丢包),因此不须要有长度字段/终结符。
可是要注意:为了不丢包率太高,udp包的长度通常不该该大于1500字节(大概,为了安全起见,我通常保证小于1K嘿),若是数据量较大,就须要分包了,这是比TCP麻烦的地方。
典型的UDP的协议设计就是:Type-Message。Type长度固定,用于说明消息类型;Message是消息体,和tcp的帧体设计一样便可。网络