设计一个在UDP之上提供面向连接服务的可靠传输协议

 http://hi.baidu.com/%B2%BB%D5%FD%D6%B1%B5%C4%C8%CB/blog/item/03927582baaca49df703a625.html

一、关于传输协议

在作者的理解中,目前互联网上常用的传输协议主要有三种。

TCP:最经典的面向连接的传输协议,提供高效无误的字节流传输服务。它可以用来传输海量数据,为适应复杂的网络状况作了专门的设计,可以在各种类型的底层网络上使用,是互联网上最常用的传输协议。

UDP:无连接的面向数据包的协议,仅提供最尽力的传输服务,协议本身十分简单,处理起来很容易,传输数据时没有建立连接的开销。使用UDP的程序需要自行处理可靠性的问题。在有些场合下很有用,比如RTP就是基于UDP实现流媒体的传输的。很多程序使用UDP协议主要就是为了利用其开销低的特点,很适合用来向多个可变的目的地突发性的传送小报文,这个特点被SIP协议发挥得淋漓尽致。

SCTP:一个很新的协议,除了提供高效无误的数据报传输之外,还支持多穴和多流这种高级特性。由于这种特点SCTP特别适合用来传输信令,但是配置安装和编程稍微复杂了点。SIP是可以在SCTP上传输的,且SCTP的设计初衷就是要作为SIGTRAN的传输层。就算不去使用SIGTRAN,我们还是可以把SCTP的这些高级特性用在自己的程序里。目前Linux内核已支持SCTP,Windows还未提供原生的支持。在Windows上的SCTP库运行在用户空间,利用原始套接字接口处理IP报文。

二、设计一个自己的传输协议

作者出于兴趣和实验的目的,自行设计并实现了一款传输协议。该传输协议具有以下特点,面向连接,但又基于消息,提供可靠的传输服务,有流量控制和拥塞控制,工作在用户层,姑且取名为User Message Protocol,简称UMP。在UMP里,作者实践了差错处理、乱序报文重排、滑动窗口、慢启动、拥塞避免、指数回避、快速重传、快速恢复、延迟ACK、主动窗口探察、心跳保活、后备队列、同时打开、同时关闭等机制。这些理论都是平时学习计算机网络教材时会看到的,但动手实现下来以后体会就更深了,同时对自己的设计能力也是一种锻炼。

这个传输协议是通过Socket接口编程,在UDP上附加额外的处理逻辑功能实现的,所以,虽然是一个传输协议,但严格的说,它并不工作在传输层。这并没有减少设计和实现这款协议的挑战性和意义。其实UDP提供的服务是比较精练的,总结起来主要就是数据报的校验和端口的复用,比IP只多了一个端口的概念。如果没有经过大量的研究和实践,一般人是不会懂得操作系统内部TCP/IP协议栈的实现机制的,自己从头到尾往协议栈里加新的协议会很麻烦。而基于Socket和UDP去做协议时既可以避免牵涉到大量的底层细节,又能方便的实现各种算法。其实Windows版的SCTP源码也有一个SCTP over UDP模式,这就是最好的证明。

UMP的底层是UDP的Socket编程,中间层是几个收发线程和各种状态机、算法的实现,对外提供几个简单的函数调用接口,完全用C实现,以Windows的dll形式提供给用户。目前尚未支持IPv6和Linux,如果觉得这个工作比较有意思,作者会加上这些支持。

三、难点与对策

第一个难点就是状态机的设计。作者认为TCP为了适应各种情况,状态机比较复杂。TCP的设计也是经过长期的讨论才逐步完善的,并且还在进一步的改进,实现起来也不简单。由于传输协议涉及到收发两方,所以如果状态机本身比较复杂,协议的设计者就更难预测和理解收发双方的状态和行为。作为一个实验性的协议,可以将状态机设计得尽量简单。

第二个难点是并发和事件的通知。作为一个传输协议,天生就是并发的,例如滑动窗口协议允许在用完窗口之前一直发送数据,而这时完全可收到新的窗口通告,或者早些时候发送的数据发生超时,所以必须实现并发和事件通知机制,而不是光发不收或者光收不发。不止滑动窗口,一般来说传输协议都需要做拥塞控制,一下就涉及到发、收、超时三个并发操作,为了避免糊涂窗口综合症,协议实现者还要考虑延迟ACK和主动窗口探察的情况,有可能的话,应该让发出去的包捎带这些附加信息,这样会让并发操作更复杂。

在实现UMP时,作者实验过两种处理并发的机制。一种是先只发不收,发了一组数据以后,只收不发,等待确认或超时的到来。这样每次只涉及两种并发操作比较简单,但是传输效率会比较低。第二种是为每个操作都分配一个线程,一个组包线程负责将净荷和捎带的信息组装起来发送出去,一个发送线程负责向组包线程喂净荷并向超时线程登记,一个超时线程负责登记和等待超时并将超时时间通知发送线程,一个接收线程负责接收确认并把有效的确认向发送通知。这种设计涉及的线程太多,太复杂,运行起来传输性能也未必好。

所以这个版本的UMP作了折衷,一个线程专门负责接受数据并把数据分配到各个连接,接着每个连接有自己单独的线程用来处理接收到的报文、发送报文和超时,这些操作在一个线程里用模拟并发的方式去做。所谓模拟并发,就是每次并不是真的同时做好几个操作,其实只做一个操作,但并不是一直做下去,而是做一个阶段就停下来,去做另一个操作,如此往复循环。由于并没有真正的并发,所以各个操作之间不存在并发带来的复杂性,而且还能提供接近真正并发操作的效果。

第三个难点是各种机制和算法的实现。在教科书上,对流量控制、超时、拥塞控制等算法都是分开讨论的,但协议的行为其实是各种算法综合作用的结果。比如确认机制要求接受方向发送方发送确认报文,同时为了避免糊涂窗口综合症,确认可以被延迟,并且最后确认发送出去的时候,又可以被捎带在正常的本方发送数据的报文里。因此在设计完了各种机制以后,具体的设计每个步骤需要做的操作以考虑各个算法的要求就是一件难度很大的事情。按理说,应该把每个算法单独隔离成小模块,最后再组装起来,但是在目前这个UMP的实现版本中,作者没有做到这一点,而是主要以发送/接收/超时逻辑来划分模块,然后将各个算法同时作用到每个模块上去。这种设计思路作者自己也很不满意,最终写出来的代码还比较乱,希望将来有时间能够再重新设计各种算法的作用关系。

第四个难点是可移植性。作者希望同一套代码既能够在Windows下编译又能够在Linux下编译。UMP主要的代码只使用C标准库就行了,需要操作系统支持只有线程和Socket。Linux和Windows的Socket接口差异不大,但是线程接口差异就比较大了,从概念上到实现上都有区别。为了屏蔽这种区别,作者使用了glib库2.20版,glib2提供了许多C程序迫切需要的功能,例如,各种数据结构、可移植的线程、IO等等。由于Linux和Windows的线程同步原语在概念上有所区别,所以对于glib2下同一个线程同步函数,在不同的操作系统上可能有不同的表现,所以作者在glib2之上又自己再封装了一个事件对象,用于同步线程。但是目前UMP程序还是只能在Windows下运行,如果将来有时间,作者会让UMP支持Linux。

第五个难点是测试。由于是自己定义的协议,各种抓包软件都不识别。另外在局域网上测试是不够的,还需要在广域网上测试。由于目前开发者只有一人,也暂时没有条件搭建测试环境,所以目前只能通过程序自己主动丢包来模拟广域网的不可靠环境,至于大时延,在程序中模拟起来比较麻烦,就没有做。

四、设计和实现

在上一节里对难点的讨论的过程中,其实已经初步涉及了协议的设计。这里再稍微再多讨论一下协议的设计和实现。由于作者比较懒,这里只是简单写写,想知道详细情况的朋友请看代码。

分为两种报文,控制报文和数据报文。控制报文只携带控制信息,数据报文只携带数据。两种报文都单独编号。
下面是控制报文的包格式
语法:
|1   2 |3   | 4 | 5 | 6 | 7 | 8 | 9 | 10| 11| 12| 13 14 15 16|
|Version|Type|   |SYN|FIN|RST|   |SEQ|ACK|MSS|WND|               |
|                          SEQ Number                            |
|                          ACK Number                            |
|                          MSS Number                            |
|                          WND Number                            |

语义:控制报文和数据报文的前四个字节是一样的,之后就不同了。Version是UMP的版本号,Type为0,则为控制报文,1则为数据报文。SYN为1表示发起连接,FIN为1表示关闭连接。RST表示工作过程中出现异常。SEQ为1时,会附加16位的SEQ Number,表示为控制报文编号,如果SEQ不为1,则SEQ Number不出现。ACK为1时,出现ACK Number,表示对控制报文的确认。MSS和MSS Number是用来通知最大报文端长度的。WND和WND Number是用来通告接收窗口大小的。SEQ、ACK、MSS和WND,只要置位,则对应的XXX Number会出现,不置位,则对应的XXX Number不出现。每个XXX Number都是16位的,为网络字节序。空的位目前还未使用。

时序:
控制报文的时序比较简单。除了纯粹的ACK报文,每个控制报文都有SEQ编号,接收方收到数据后,需要发送确认(ACK),确认可以捎带在正常报文里。在收到对上一个控制报文的确认后,发送方才开始发送下一个控制报文。如果超过一定时间没有收到确认,就重传原报文。至于连接的建立和拆除,后文在讨论状态机时会专门谈。

下面是数据报文的格式
语法:
|1   2 |3   | 4     | 5 | 6 | 7 | 8 | 9   10 11 12 13 14 15 16|
|Version|Type|REQ_WND|BDR|SEQ|ACK|WND|        SEQ Number Part0       |
|        SEQ Number Part1            |        ACK Number Part0       |
|        ACK Number Part1            |        WND Number Part0       |
|        WND Number Part1            |        User Content……

语义:REQ_WND为1表示主动窗口探察,用来要求接收方通告窗口。BDR表示本报文是一个消息的系列报文中的最后一个。SEQ、SEQ Number是对本报文的编号,ACK、ACK Number则是确认编号,WND、WND Number则是通告窗口大小。同控制报文一样,SEQ、ACK、WND置位时,对应的XXX Number才出现。每个XXX Number都是16位的,为网络字节序。Content为净荷。

时序:数据报文的也有编号、确认、超时、重传机制,但是并不需要等待确认到来以后才能发送下一个报文,而是遵从滑动窗口协议,只要还有窗口,就可以发送报文。除此以外,慢启动、拥塞避免、快速重传、快速恢复、指数退避等算法都会影响数据报文的时序。所以数据报文的时序难以预测,总的来说,遵从基本的编号、确认机制,然后在此基础上,接受各种算法产生的综合影响。

连接状态机:
下面是UMP的连接状态机。



可以看到,状态机十分简单,图中标有1、2的表示只要满足其中一个条件,就发生状态迁移。状态机是帮助我们形式化程序的状态的。实际上UMP的状态要比图中的多,但是作者对一些小状态进行了合并,代码实现完全按此图去做的。

五、测试、发布和改进
好长,有点写不动了。。。
还有很多有意义的内容没有谈,目前为止讨论的都是很简单的内容。。准备虎头蛇尾了。。

——————————————哥是没意义的分割线——————————————————

作者在局域网里作了初步的测试,也作了程序模拟丢包的测试。测试结果是,传输速率和TCP差不多,传输的数据可靠没有差错。

目前项目发布在http://codaset.com/edwardbadboy/user-message-protocol上。读者如果有git,可以通过clone git://codaset.com/edwardbadboy/user-message-protocol.git下载代码。没有git的读者可以浏览网页打包下载代码。代码量不大,四千多行。
浏览http://codaset.com/edwardbadboy可以查看作者发布的其他项目。

需要改进的地方有:
做更多的测试,修复更多的bug,不光修复实现代码的bug,还要消除协议设计上的bug。
重构代码让它不那么乱。
认真处理所有的错误和异常。
支持IPv6。
支持Linux。

六、小结
在实现UMP的过程中收获很大,对各种传输协议的算法了解更深了。用C做设计和编程的水平提高了不少,还学会了消除内存泄露的各种方法。遗憾是因为第三节第三个难点的原因,有几个模块的设计比较乱。实际上,这个传输协议我一共设计和实现了三个完全不同的版本。总的来说,这个版本还算让人满意,作者也没有太多的实现在同一个题目上做下去。下一步应该去阅读Linux等开源操作系统的协议栈源代码。


 

你可能感兴趣的:(网络编程)