# 基于python实现的udp可靠文件传输
# 一、项目说明
- 本项目使用Python进行实现
- 采用Client-Server架构,源代码有两个文件client.py和server.py,两个文件需要放在同一目录下才能运行客户端/服务端功能,支持大文件传输(下载/上传)。服务端监听端口限定为10000,传输端口在10001-10009之间,监听端口用于与用户建立连接,传输端口用于与用户进行文件数据的传输,最大支持9个线程同时工作(此处可以修改,以更改端口号或者最大连接数)
- 服务端使用方法:命令行输入 python server.py即可
- 客户端使用方法:命令行输入python client.py lget/lsend ipv6_hostname filename
- 使用UDP作为传输层,使用IPV6作为网络层,所以传输速度比较快
- 实现了100%可靠传输
- 实现类似TCP的流式控制功能
- 实现类似TCP的拥塞控制功能
- 服务端支持多用户同时下载/上传
关于参数改进:如果使用的时候发现大量丢包,需要更改下面的常量值,以此优化传输:
Client.py中:
![](http://www.writebug.com/myres/static/uploads/2021/10/19/a5948d70b37966c261424c9cadeb87e8.writebug)
Server.py中:
![](http://www.writebug.com/myres/static/uploads/2021/10/19/79d7ab18fc361069e73abc5b9a58099b.writebug)
网络越拥堵,MAX_DATA_LENGTH、RECV_WINDOW两个常量值越应该设置的小一点:
如果网络状况非常拥堵,这两个参数推荐(client.py和server.py都需要更改这两个参数):
- 将MAX_DATA_LENGTH设置为300(每个UDP数据包大小)
- 将RECV_WINDOW设置为400(接收方拥塞窗口大小)
# 二、结构设计
客户端具有从服务器下载和上传的功能,因此我们分别设计Recv_Unit和Send_Unit两个类分别完成下载和上传功能。
客户端下载文件时实例化Recv_Unit并调用该类的recv_file方法接收服务端发来的文件,同时服务端实例化Send_Unit并调用send_file向用户发送文件。
客户端上传文件时则相反,由客户端实例化Send_Unit并调用该类的send_file方法向服务器发送文件,同时服务端实例化Recv_Unit并调用recv_file方法接收用户发来的文件。
因此主要设计包含:class Send_Unit 上传类、class Recv_Unit 下载类、class Fragmentation分片类,下面分别介绍:
## 2.1 class Send_Unit 上传类
![](http://www.writebug.com/myres/static/uploads/2021/10/19/b2344638d41ff5a0e662af1a95002fdd.writebug)
该类用于向远程主机发送数据,其实例变量与各自的说明如上图注释中所示,有端口、套接字、目的IP地址、总数据段数量、用于模拟TCP的send base、send window、rwnd、cwnd、ssthresh、计时器、冗余ACK计数和表示拥塞控制中三个状态的变量等。
下面简要说明该类的4种方法。
**send方法**
调用该方法向远程主机发送文件。
![](http://www.writebug.com/myres/static/uploads/2021/10/19/ae162e69ce0bc8d918bed4177dd230d8.writebug)
首先创建套接字,并绑定端口(这里还另外调整了UDP的发送缓存)。
![](http://www.writebug.com/myres/static/uploads/2021/10/19/a2b9327e90bcee3d89a001ecbeb40e05.writebug)
这里获取远程主机想要下载的文件(文件路径),并设计超时,若3s内对方没有发送这个信息到发送方,则假定连接未建立,此时关闭套接字,切断连接。
![](http://www.writebug.com/myres/static/uploads/2021/10/19/f9c92d753de1b25c6e5b7e39a59c3bd9.writebug)
计算要传输的文件大小与总数据段数量,获取远程主机IP地址。
接下来开始正式传输文件:
![](http://www.writebug.com/myres/static/uploads/2021/10/19/64055562d1cb32bee6b77ad6cfc3a780.writebug)
这里单线程模仿流水线实现传输,为此,首先将套接字设为非阻塞模式。
传输文件时发送方接收文件只会接收到接收方发送的ACK,所以我们首先从UDP缓存读取数据(ACK),若发现无法读取到任何数据,则转到异常处理部分;若能够读取到数据,说明发送方这边收到了ACK,则转入ACK处理函数处理ACK。
- 异常处理部分:该部分用于发送数据。由于ACK还没有传到,故继续传输数据给接收方,此时由于流控制和阻塞控制,我们应保证发送到连接中但未被确认的数据量(最后发送的数据段序号 next_seq_num – 最早未被确认的数据段序号 send_base)小于等于接收窗口rwnd和拥塞窗口cwnd的两者的最小值。若条件满足,我们就读取读取要发送文件的一段发送给接收方,并将这段数据放入send_window(重传时使用)。当计数器未启动时,启动计时器。另外,若rwnd为0,则根据教材所说方案,发送方这边继续发送只有一个字节数据的报文段到接收方以维持两方的通信
- 接收到ACK:进入ACK处理函数
**handle_ack方法**
调用该方法处理接收到的ACK。
![](http://www.writebug.com/myres/static/uploads/2021/10/19/89a72dd891266f96f6421a1627abe89f.writebug)
首先解析收到的ACK数据,更新rwnd,并打印一些相关信息。
![](http://www.writebug.com/myres/static/uploads/2021/10/19/fe0ae717ccb658aa69ef88efcd4d94f0.writebug)
正式处理ACK,通过判断ACK和send_base的大小关系来判断是否是冗余ACK。
- 不是冗余ACK:更新send_base的值,移动send_window,若当前还有未确认的数据段,则重启定时器。重置冗余ACK计数。根据当前所处状态(拥塞控制),相应更新cwnd
- 是冗余ACK:根据当前所处状态相应更新cwnd。若当前为慢启动或拥塞避免状态,则增加冗余ACK计数,当该计数等于3时触发快速重传,调用重传函数
**retransmit方法**
调用该方法重传数据段(选择重传)
![](http://www.writebug.com/myres/static/uploads/2021/10/19/00024481f1a29d0a7c319977ba2fa7bd.writebug)
首先通过传入的参数判断这次重传是超时触发的还是快速重传,据此更新当前状态。
然后判断send_window是否为空,rwnd是否为0,若是则可以进行重传。进行重传时取出send_window中的第一个元素,该元素的data成员即为最早发送未确认的数据段,再次向接收方发送该数据段。
**enter_slow_start方法**
![](http://www.writebug.com/myres/static/uploads/2021/10/19/bf05c962316be7f48c8a6104f968e33d.writebug)
调用该方法初始化进入慢启动状态时的各变量。
阻塞控制主要是根据教材中对TCP拥塞控制的FSM描述设计,即下图:
![](http://www.writebug.com/myres/static/uploads/2021/10/19/78a786e91054d648ef46294f7ce9bd27.writebug)
## 2.2 class Recv_Unit 下载类
**成员变量**
![](http://www.writebug.com/myres/static/uploads/2021/10/19/9e17ad58639c18e7ab68502fdce6f7e4.writebug)
- **self.ack_send_interval**:接收方并不会收到数据包就立即发送,而是使用RFC [5681]的建议:当接收到按序的报文,将最多等待500ms发送一次ack给发送方 ,self.ack_send_interval设置为不超过500ms。如果收到乱序报文则马上回复一个ack给发送方,以示按序报文仍未收到,三次这样的冗余ack将引起快速重传
- **self.client_socket**:该类使用的套接字,使用ipv6作为网络层,并使用setsockopt设置UDP套接字缓存大小为1M
- **self.port = port**:使用传入的port进行初始化,表示后续接收数据通过该端口进行
- **self.top**:当前还未收到的报文的最小序号,小于该序号的报文段都已收到并写入磁盘
- **self.last_byte_received**:当前已经收到的报文数目
- **self.clock_using = False**:用于指示计时器是否在使用,该计时器即按照(1)中变量计时,每间隔 ack_send_interval发送一次ack
- **self.send_ack_operation**:ack_send_interval的计时器
- **self.package_num**:指示整个文件大小
- **self.have_received**:是一个boo