设计文档
LFTP是一个在任意两台计算机之间传输任意大小文件的工具
- 基于UDP
- 基于
Java
的DatagramSocket
实现
- 基于
- 100%可靠性
- 使用GBN协议保证所有报文都正确接收
- 流量控制
- 使用接收者通知发送者接收缓存大小来反馈发送窗口大小
- 拥堵控制
- 使用动态的发送窗口大小,根据网络情况及时调整。
- 并发传输
- 使用多线程多端口保证多个用户同时上传或者下载
项目树:
.
├── cmd # 命令行控制
│ ├── CmdParameter.java # 解析命令行参数
│ ├── Get.java # 从服务器获取文件
│ ├── GetList.java # 从服务器获取文件列表
│ ├── Main.java # 程序主入口
│ ├── Send.java # 向服务器发送文件
│ ├── Server.java # 开启服务器
│ └── Util.java # 解析数据
├── net
│ ├── ByteConverter.java # 封包序列化转换
│ ├── FileChunk.java # 文件块封包
│ ├── FileData.java # 文件块管理
│ ├── NetSocket.java # 网络通讯管理
│ └── UDPPacket.java # 基本封包
└── service
├── FileIO.java # 文件IO管理
├── FileNet.java # 文件收发管理
├── Percentage.java # 进度条显示
├── ReceiveThread.java # 接收文件线程
└── SendThread.java # 发送文件线程
架构图:
程序由Picocli
接收命令行的各种参数,然后交给CmdParameter
解析,根据参数执行不同的命令。
主要有两个核心模块:
这个模块管理UDP的封包、接收和发送。
发送的时候,将数据封包并放入发送缓冲当中。
发送使用流水线协议,并且使用GBN协议保证数据一定发送到对方并且是有序的,保证可靠的数据传输
使用流量控制服务,通过在UDPPacket
封包的ACK回执中附带当前接收窗口的剩余空间,发送方控制自己的发送速率和窗口大小,达到流量控制的效果
使用TCP的拥塞控制算法,维护一个发送窗口大小和阈值,当收到错误的包或者重复三次同样的ACK包的时候,就减少窗口大小,防止拥塞发生。
一些普通的请求,可以直接调用NetSocket
进行收发,而对于文件块的收发,就需要通过FileNet
进行统一管理。将文件数据放入FileChunk
再次封包放入UDPPacket
的data
字段,又FileNet
进行解析和封装。
同时,对于本地文件进行操作,分块读取或者写入文件
本程序基于UDP报文段,在UDP基础上封装了两层结构。
UDP报文段
UDPPacket基本报文段(存放于UDP的应用数据部分):
FileChunk 文件分块报文段(存放于UDPPacket的Data部分):
这里的流程参考FTP的被动模式,首先客户端向服务端指定的端口发送请求命令,然后服务端新开一个数据传输端口,并且把端口号发送给客户端,然后客户端向这个数据传输端口获取文件数据或者发送文件数据。这种方式保证了多个客户端可以同时连接服务器进行数据传输。
具体工作过程:
- 客户端向服务端请求发送文件,并且把这次会话的ID发送给服务端
- 服务端接收到发送文件请求,从空闲地址池中选择一个端口,在这个端口上监听接收的数据
- 客户端接收到服务端的数据端口,然后从自己的数据端口中发送文件各个分块的数据到服务器的数据端口,并且附带这次的会话ID
- 服务端接收到文件分块之后写入硬盘,并且向客户端发送确认回执
- 客户端接收到所有文件分块的回执之后,向服务端发送结束信号
- 服务端接收到结束信号(或者超时10s)之后关闭端口,放回空闲地址池
- 客户端收到结束信号之后同样关闭端口
下载的过程和发送的过程类似,只是在数据传输部分将上面的客户端和服务端交换过来。
具体工作过程:
- 客户端向服务端请求获取文件,并且把这次会话的ID发送给服务端
- 服务端接收到获取文件请求,从空闲地址池中选择一个端口,等待客户端的数据端口的连接,并把端口号发送给客户端。
- 客户端向服务端的数据端口发送第一次请求(用于确认客户端的地址,使得UDP可以穿透内网),附带SessionID
- 如果SeesionID正确,服务端就逐一发送文件块到客户端
- 客户端接收到文件分块之后写入硬盘,并且向服务端端发送确认回执
- 服务端接收到所有文件分块的回执之后,向客户端发送结束信号
- 客户端接收到结束信号之后关闭端口,也想服务端发送结束信号
- 服务端收到结束信号之后关闭端口,把数据端口放回地址池中,等待下次的使用
这个比较简单,只需要对服务器发出一个简单的请求就可以实现,没有必要专门建立连接管理状态。
为了提高传输速度,本项目使用移动窗口模式发送数据。
简单来说,就是同时发送和窗口大小相同的数据包个数,然后根据实际情况(拥塞算法)增加或者减少窗口大小。
发送数据一般使用非阻塞模式来接收ACK报文。
这里使用单独的发送线程和接收线程来收发数据包
流程图:
使用GBN协议 ,当收到相应的ACK的时候才会将报文从发送中的数据队列移除,否则下一个超时时间之后会重新发送,直到收到相应的ACK。
在接收到ACK的时候:
- 正确的ACK,增加窗口大小:
cwnd <= ssthresh
:cwnd = cwnd * 2
cwnd > ssthresh
:cwnd = cwnd + 1
- 错误的ACK,减少窗口大小
ssthresh
变成之前的cwnd
的一半- 重复三次以上:
cwnd
调成1 - 重复三次以下:
cwnd
调成ssthresh + 1
- 超时,减少窗口大小
ssthresh
变成之前的cwnd
的一半cwnd
调成ssthresh + 1
在接收到ACK的时候:
- 接收缓存小于
100
:减少窗口大小 - 接收缓存小于
10
:窗口大小降低为1
根据接收方的缓存大小,及时调整发送方的发送速率,以控制流量大小
根据过去的RTT
和估计的RTT
更新当前的估计RTT
使用指数加权移动平均来估计当前的RTT
然后计算出重传超时间隔:
接收数据使用阻塞模式,流程比较简单。
接收到正确序号时候,就交给客户端处理,否则返回之前已确认的ACK序号。
流程图:
因为本项目是专门支持大文件传输的,因此对于大文件需要特别处理。
由于文件过大,不可能一次性把文件读取到内存当中,因此我们需要把文件划分成多个块,然后读取文件的指定部分,发送给接收方。
为了防止过多的文件同时读取到内存中,这里使用了Semaphore
信号量来限制读入缓冲中的数据的数量。
在发送过程图的准备发送数据队列中,在插入数据之前,首先需要获取一个信号量,然后在发送线程将一个数据包从准备发送数据队列移动到发送中的数据队列的时候,释放要给信号量。这样就限制了读入数据的数量始终不会超过指定的值。
每个文件块由以下的结构传输:
当接收方收到这个数据的时候,就会将指定的数据通过RandomAccessFile
写入文件的指定位置,这样就不需要将数据存储在内存当中。
流程图