基于UDP的文件传输程序
计算机网络大作业期中报告
项目一览与摘要
项目特色
该项目在应用层实现了TCP
协议中的众多功能以实现传输的可靠和高效:动态调整RTT
,流量控制,阻塞控制,快速重传。
同时,还实现了并发接收和并发传输机制,能够同时接收或发送若干个文件。发送还支持文件夹发送,即保留文件夹结构不变地情况下发送里面的所有文件。
实现了基于md5
的断点续传功能,在传输发生中断时,下次传输时用户可以选择从上次中断之处继续传输,而不用从头传输,大大节省不必要的流量。
该项目还有完善的日志记录功能,会将程序运行过程中产生的正常信息,警告信息,错误信息分别保存下来,方便查错。
该项目实现了服务端和客户端双方收发文件的功能。客户端既能发送文件给服务端,也能从服务端下载文件到本地。
该项目实现了数据传输的报文数据段大小由客户端决定,服务端适应的机制,使得服务端能够适应不同客户端的传输需求。
该项目还有绘图功能,能够将发送过程中的参数变化绘制成图表,方便分析。
经实测实现了可靠和高效传输的效果。
此图为从连接校园网的终端传输一个10M的文件到阿里云服务器的结果,传输期间触发了一次超时重传,236
次快速重传,总耗时34
秒。
本项目基于python语言编写。
项目结构:
. |
需求分析
众所周知,UDP
是不可靠协议,但能够对应用层数据的发送控制更为精准,无连接建立,无连接状态使得UDP
能较少地占用终端资源,且UDP
分组的首部开销小,仅有8
字节。而TCP
是通用的可靠传输协议,适用范围很广,其首部有20
字节的开销。因此对于文件传输来说,有许多信息是没有必要的。因此实现专门适用于文件传输的可靠协议是有必要的。而要用一个不可靠协议实现可靠传输,就如同TCP
一样要在不可靠的IP协议下实现可靠传输一样,我们要将能够确保可靠的机制在UDP
的上一层:应用层实现。于是我们需要参考TCP
实现可靠传输的一些机制,在应用层上实现。同时为了改善用户体验,实现一些基本的需求比如断点续传,文件夹发送等功能。
设计思想
本项目基于建造者(Builder
)模式设计。
服务端(Server
)和客户端(Client
)都有发送和接收文件的功能,两者在这两个功能上行为一致,只有起初的发送请求等行为不同。
于是服务端能够根据自身的身份(发送者还是接收者),创建相应的Sender
或Receiver
进行发送或接收文件。同理客户端也能如此。
同时为实现并发传输及可靠性,服务端有个主进程,监听客户端的请求,并创建一个子服务进程,处理该请求,而主进程继续监听请求。
客户端同样也有个主进程,负责扫描发送的文件(夹),对每一个要发送的文件,创建一个子客户进程向服务端发送请求。当所有子进程结束后,主进程将根据子进程发送文件时产生的数据进行汇总,绘制图表。
子服务进程在处理请求,以及子客户进程在执行请求时,会根据自身的身份(发送者还是接收者),创建相应的类,并调用类方法执行。
核心算法
报文首部
签名sign (16bit) | 窗口大小rwnd(16bit) |
---|---|
报文序号(32bit) | |
数据段(MSS) |
其中rwnd
对于接收方发送的报文而言,就是缓冲区的剩余大小,单位是MSS
.
对于发送方发送的报文而言,用于特殊标记,如终止传输报文,请求端口报文等。
ACK
报文,数据段为空。报文序号表示确认的报文序号。
注意,此处的ACK
报文序号与TCP
中的ACK
不同。此处的ACK序号为接收到的最后一个报文序号,并非TCP
里的接收到的最后一个报文序号加一。可以理解此处的ACK
与TCP
里的ACK
的关系为
除了请求报文的MSS
是双方约定好之外,握手和数据传输报文的MSS
大小由客户端告知服务端,在请求报文中告知。
握手报文中,数据段的数据之间采用分隔符spliter
(定义在config/config.py
文件中)区分。
可靠机制
为实现可靠机制,需要对发出去的每一份报文收到相应的ACK
确认报文,此处实现和TCP
一致。
同时,为防止无关报文的干扰,传输通信的报文都有一个唯一确定的签名sign进行核验,不通过的报文将被丢弃。
报文出了签名外,还标有序号,以确保不被乱序收到。
动态调整RTT
实现自Jacobson / Karels
算法
—— 计算平滑 RTT
——计算平滑 RTT 和真实的差距(加权移动平均)
其中,取自RFC6298。RTT是测量值,为一份报文从发出到接收到ACK报文所经历的时间。
流量控制
考虑到服务端和客户端性能上的差异,接收方收到的包并不会马上写入文件,而是会放到缓冲区里,而当缓冲区满时,接收方就不能接收新的数据包,防止缓冲区溢出,而此时如果发送方继续发送报文则会导致丢失,因此当客户端和服务端性能差异过大时,要避免不必要的发包。
实现机制和TCP
类似,接收方每一次ACK
回复报文中会带有当前缓冲区剩余长度,发送方会根据ACK
报文中的剩余长度,动态调整自己的发送行为,必要时暂停发送。
阻塞控制
除了考虑服务端和客户端之外,还要考虑当前网络状况,如出现频繁丢包现象时不应保持持续发包,以降低发送成功率,加剧网络阻塞。
实现机制和TCP
类似,发送方维护一个发送状态,有诸如慢启动,阻塞避免,快速恢复等状态,以及一个阻塞窗口,和流量控制里的流量窗口(接收方缓冲区剩余长度)共同控制发送方的发送行为。
快速重传
限于接收方采用按序收包的策略,当出现包丢失时,能够通过接收方发送的多次冗余ACK
得知。
实现机制和TCP
类似,当发送方接收到三次及以上冗余ACK
后,会立刻重发仍未被确认的数据包。
断点续传
由于文件是从头开始发送,中间传输故障时,我们能够知晓接收方收到的数据是文件开头一定大小的。
客户端和服务端在进行握手时,接收方会向发送方发送自身文件的大小size和md5码,接收方接收到后,计算发送文件前size
大小的md5
码,与接收方发送的md5
码进行比对。如果一致,说明文件一致,则向用户询问是否续传还是重传。
并发传输
运用多进程,使得每个进程负责一个文件的收发,达到并发机制。
软件架构
本项目共有四个模块,十分清晰简单。其类成员方法如下所示。
classDiagram class Sender{ +file +send() +update_cwnd() +resend() +update_RTO() +receive() +summary() +start() } class Receiver{ +file +receive() +write() +start() } class Server{ +Shakehand() +start() } class Client{ +Getport() +Shakehand() +start() } Sender --* Server Sender --* Client Receiver --* Server Receiver --* Client
流程图
这里展示客户端发送文件给服务端,续传方式。
Participant User |
其余方式与此图非常类似。
关于异常处理,在发送文件前的握手阶段,如果有任何报文丢失的话,接收报文方将触发超时机制,重传上一份报文。连续超时5
次则握手失败,关闭进程。
签名重复情况。
Participant Client |
收到任何签名sign
不对的报文均丢弃。
收到的任何序号不对的报文将丢弃,并重发上一个报文。
发送数据过程中出现的异常处理和TCP
的异常处理一致,上面的流程图有所展示。
Sender
内部的函数调用关系如下图所示。
digraph G { |
Receiver
内部的函数调用关系如下图所示。
digraph F{ |
Sender
和Receiver
的交互如下图所示。
digraph F{ |
测试结果与讨论
发送的MSS
报文大小会影响到传输效果。
在实测中,MSS
太小时,总发包数量过多,尽管一次发包数增加,但无法弥补总包数增多带来的时间上的增加。MSS
太大时,接收方难以快速处理接收到的数据包,导致丢包发生,且包太大时,经过中间路由会导致数据包乱序到达,由于接收方必须按序接收,这容易触发发送方的快速重传机制。
当MSS
过小,比如256Byte
时,两次传输一个3MB
的文件,发送方记录的数据图表如下:
当MSS
为1024Byte
时,其记录如下:
这两张图中cwnd
曲线很好地符合教科书上的曲线特点。
这是发送方最后输出的信息。
这是接收方最后输出的信息,可以看到文件传输无误。
但当MSS
调整为1024 * 10Byte
时,发送方记录的数据图表如下:
可以看到,cwnd
呈周期性震动,在接收方里的错误信息可以看到
接收方在接收数据时出现了周期性地数据包丢失,亦或者说是乱序收到,由于接收方必须按序接收,一旦乱序就会重传ACK
报文,易让发送方产生多次没必要的超时重传。
从发送方的结果可以看出,消耗的时间反而更长了,因为期间产生了过多的快速重传机制。
刚刚传输了一次qwq
文件,再次传输时会提示文件已存在,询问用户是否续传:
输入1
表示续传,而由于文件已经完整,因而就只发了一个终止FIN
包,传输就结束了。
将MSS
调整为1024 * 5Byte
,结果如下:
可以看到,快速重传的次数减少了,但由于发送的数据包的数量增加,总耗时与上面差不多。
仔细观察出现warning
的情况,可以推断出并不是包丢了,而是数据包在传输过程中顺序被打乱了,这就导致了之后一连串数据包的错位,此时cwnd
的作用就出现了,不断减小的窗口能够很快缓解数据包乱序带来的现象,但这在一定程度上消耗了一定的时间和网络资源来解决这个问题。
不过好在本程序支持断点续传,此时我们重新启动传输,也可以解决此问题。
但同样MSS
,在内网传输时,结果如下:
可以看出内网的网络环境明显优于外网。
总结
通过这次在公网进行文件传输的测试,可以得知外网环境较为复杂,当传输数据包大小较大时,由于外网传输路径的复杂性,期间的路由器等网络设备对数据包的处理可能容易造成乱序,进而影响应用层数据包的接收。同时在出现乱序的现象,我们也看到cwnd
阻塞控制所起到的作用:它迅速减少同时发包数量,防止数据包乱序现象的持续。因为此时已经是乱序了,接收端接收到的乱序包混杂了很多重发包,如果这又造成多次ACK
重发的话,会进一步触发发送方的快速重传机制,导致这种现象的加剧。
从实验中我们也发现,丢包现象相对于数据包乱序现象发生次数较小,如果接收方采用选择重传的机制,即不要求数据包要按序收到,允许在一段小的时间内乱序,进而将数据包根据序号重排,这或许能有效减少此种乱序现象导致的回退N步方式的快速重传机制,能有效改善传输的时间。但由于需要缓存收到的数据包,将数据包重组排序,这在一定程度上会占用服务端的CPU
资源,当有许多个并发任务传输时对传输的稳定性可能是有一定的影响。
从这次实验也可以看出来,为了传输的可靠性先人做了许多的措施,在这次代码的编写时,也根据实际情况,对TCP
类似机制的实现做了微调,尽管不能完美的解决问题,我们只能接受一定程度上的缺陷,尽可能实现好的效率。在起初的程序设计中,曾为了实现高效可靠的传输斟酌了各种可能的方法,例如考虑握手的实现方式,考虑每一步如果出现差错如何处理,实现选择重传还是回退N步等,超时重传的时间计量应该如何进行,是每个包一个时间还是一个ACK
接收间隔一个时间等等,虽然没想到一个能完美无缺的方案,但我们还是得选一个缺陷允许接受的方案实现。或许在未来,网络物理条件改善,在物理链路层能保证可靠传输的话,应该就不需要这些了。