Ridiculous
LFTP是一个java实现的网络应用,支持局域网、互联网上两台主机之间实现大文件传输。
使用UDP协议进行传输:基于 Java
的 DatagramSocket,DatagramPacket
实现
实现100%可靠传输:使用GBN协议回退N步保证所有报文正确接收
实现流量控制:使用接收方通知发送方剩余接收缓存大小反馈发送方的发送窗口大小
实现拥塞控制:使用动态的发送窗口,根据丢包、超时的网络状况调整发送窗口大小
使用客户端-服务器结构,服务器支持多个客户端并行服务
应用提供有意义的输出信息
./code/LFTP/src
├── main # 程序控制
│ ├── LFTP.java # 主程序入口,解析命令行
│ ├── Client.java # Client客户端
│ └── Server.java # Server服务端
│
├── service # 文件收发服务
│ ├── SendThread.java # 文件发送服务线程
│ └── ReceiveThread.java # 文件接收服务线程
│
└── tools # 工具服务
├── FileIO.java # 文件读写IO管理
├── ByteConverter.java # 数据包序列化
├── Percentage.java # 进度条显示
└── Packet.java # 数据包封装类
应用由 LFTP
程序入口进入,解析终端输入的命令参数,根据不同的命令参数决定客户端/服务器的不同行为。
主要有两个核心模块:
这个模块进行基于 UDP
的文件发送。
先从本地文件中分块读取数据进内存,一个区块 10Mb
。将块中数据的每 1kb
大小封装成一个 Packet
包,放入发送窗口中
发送使用流水线协议 ( GBN
),通过 GBM
回退N步,来保证即使出现丢包、失序、超时都可以100%可靠传输
使用 流量控制服务 ,通过解析接收端返回的封装在 Packet
内的 ACK
包,得到 Packet
内的 rwnd
字段,得到接收端接收窗口的空闲空间大小,保证在接受端有空闲空间的时候才发送数据分组,从而控制发送端的发送流量
使用 拥塞控制服务 ,使用 TCP 拥塞控制的方法,维护发送窗口的大小和阈值。当收到3次冗余 ACK 包或者超时需要重传时,就减少发送窗口的大小,尽可能避免拥塞导致不断的丢包
这个模块进行基于 UDP
的文件接收。
接收遵循 流水线协议( GBN
),每当收到正确的下一个报文号的数据分组,发送确认号为这个报文号的ACK包给发送端。如果收到错误的,就发送上一次确认的报文号的ACK包给发送端
使用 流量控制服务 ,维护一个接收窗口,这里设置接收窗口大小为一个区块大小,也即10Mb。每次发送 ACK
包都将接收窗口剩余空间 rwnd
封装进 Packet 包中,通知发送方接受端的空闲缓冲大小。当接收窗口满了的时候,执行 FileIO
对文件进行写入,然后清空接收窗口继续接收数据分组
data
数据是从文件 FileIO
读取出来的 1kb
的 byte[]
数据
使用 GBN 回退N步的协议,实现UDP数据分组100%可靠传输。
我们按照书本GBN 协议拓展FSM 图的描述去实现(如下图所示)
稍有不同的是,我们的base是从0开始,也即我们的发送的数据分组的报文号是zero-base。
发送端:
接收端:
在 GBN协议 下,当收到正确的 ACK 的时候才会将报文从发送中的数据队列移除,发送窗口才会往右滑动,也即下图中的 base ++ 。在下一个超时时间之后会重新发送所有已发送但未确认的分组,也即下图中 [base,nextseqnum)
的分组。
具体的发送案例可以参见下图:
pck0正常接收,数据放入接收窗口缓存中,pck1丢失,接收端将不会将后续的pck2、pck3···放入接收窗口,返回的是错误的 ACK包,也即冗余的ACK0包。
接收端接收到正确的ACK0后,base++
变成1。之后收到冗余的ACK0包,将不会将发送窗口右滑,直到下一次超时重传发生时,才重新将 [1, N)
的数据分组重传,也就是说,这时候丢失的pck1才得以重传,这样接收端能实现100%可靠的接收。
事实上,在后面拥塞控制服务的实现中,我们不必等待所有N-1个冗余ACK包抵达后超时才进行重传,在拥塞控制中,我们只要收到三个冗余的ACK包,我们就将修改发送窗口的大小和阈值,并重传未确认的分组。
接收端维护一个接收窗口,并在返回的ACK包中返回一个指示 rwnd
——该接收窗口还有多少可用的空闲缓存空间。
当 rwnd==0
时,接收端将不再缓存新的数据包的分组数据,相当于丢弃这个包,而同时将接收窗口缓存满的数据写入文件中,写完后清空接收窗口缓存,并发送一个分组到发送端,告知发送端这里的接收窗口又有新的缓存空闲空间了,然后继续接收发送方传来的数据包。
而同时,发送端当收到 rwnd==0
的 ACK包,就会停止发送数据包,避免重复多次的冗余ACK和重传,直到收到接收端传来的通知窗口清空的信息分组,才继续进行数据发送。
然而需要注意的是,因为接收端发送的通知已清空缓存的信息分组可能会丢失,所以发送端有可能并不知道接收端何时有了新的接收窗口空闲空间,这样会导致接发两端都陷入阻塞。所以当 rwnd==0
时,发送端依然要不停发送只有一个字节数据的报文段,相当于在不断的询问接收端是否已经清空缓存完毕,当收到回复 rwnd>0
后,发送端就可以继续传输数据包 。
这样就实现了 流量控制服务
,以消除发送方使接收方缓存溢出的可能性。
拥塞控制服务我们使用的使 Reno 版 TCP拥塞控制的算法。
一共存在两个状态:
SS 慢启动状态,发送窗口大小开始为1,然后以指数型增长到阈值
CA 拥塞避免状态,在阈值之后,以线性增长不断提速,直到丢包或超时事件的发生
丢包事件:
超时事件:
对于很大的文件,例如2Gb以上等大文件,我们不可能一次把文件全部读入内存,因此我们要分批次读取,所以设计了一个区块的概念。区块大小为10Mb,一次对文件的读写使10Mb。
因此我们在 FileIO
中的读文件接口函数设置了一个参数——区块号,用以确认发送端想读出来的是哪一个区块的数据。
而为了让发送端知道有多少个区块,还有新加一个接口函数,getBlockLength()
,用以知道文件总共有多少个区块。
如此,发送端就可以分批地一区块一区块的发送数据,应用运行就不会出现 OutOfMemoryError
使用内存溢出的情况。
使用局域网测试时,我们通过开启wifi,两台主机在同一网络下进行通信
开启服务器:
java -jar LFTP.jar server
查看提示:
java -jar LFTP.jar help
查看服务器上的文件:
java -jar LFTP.jar listall -s 172.18.33.245 -sp 7545
客户端输出:
服务器端输出:
一对一向服务器发送文件:
java -jar LFTP.jar lsend -f test.mp4 -s 172.18.33.245 -sp 7545
客户端输出:
可以看到,传输速度在经过慢启动阶段后逐渐增大,可以达到2Mb/s.
服务器端输出:
多对一向服务器发送文件:
必须指定新开客户端的控制端口-cp,否则造成端口冲突
同时,客户端可以选择不仅局限于当前路径下的文件来进行发送,比如book/book.pdf
java -jar LFTP.jar lsend -f book/book.pdf -s 172.18.33.245 -sp 7545
java -jar LFTP.jar lsend -f test.zip -s 172.18.33.245 -sp 7545
客户端并行发送中:
客户端并行发送完成:
可以从这里看出拥塞控制的作用,并行发送时,两个客户端可以共享带宽,传输速度各接近于单对一发送时的一半
服务器端并行发送开始:
服务器端并行发送完成:
服务器端查看文件结构:
服务器端解压接收压缩文件:
可以看到,解压成功,因此文件传输没有问题
再次listall查看服务器中文件
java -jar LFTP.jar listall -s 172.18.33.245 -sp 7545
客户端输出:
服务器端输出:
准备测试lget时,两台主机之间的连接出现了问题,我们交换了两台主机的角色,并在新的服务器主机上添加了前面传输的3个文件,可以认为只是服务器地址变了
多对一从服务器下载文件
同样,必须使用-cp来给不同的客户端指定不同的控制端口
java -jar LFTP.jar lget -f book.pdf -s 192.168.137.42 -cp 3999
java -jar LFTP.jar lget -f test.zip -s 192.168.137.42 -cp 3999
客户端多对一lget开始:
客户端多对一lget结束:
同样,在下载文件时也可以看到拥塞控制的作用,两个客户端的速度被控制到几乎相同
服务器端输出:
客户端lget结束后文件结构:
客户端解压下载的文件:
可以看到解压成功,因此下载文件没有问题
我们在局域网下进行了大文件的传输测试,bigFile.zip
文件大小为2.7GB
java -jar LFTP.jar lsend -f bigFile.zip -s 192.168.137.42 -sp 7545
客户端发送超大文件开始:
查看发送超大文件时占用的内存:
可以看到,在发送超过2GB的大文件时,客户端占用的内存也只有100多M,没有把整个文件都读入内存中,对文件的分块传输设计成功
客户端发送超大文件完成:
由于中途网络断开了,因此这里显示的传输平均速度只有700kb/s,但实际速度依然是有2Mb/s的
服务器端接收大文件成功:
解压大文件:
大文件解压成功,具有100%传输可靠性
服务器:腾讯云
系统:Windows Sever
配置:1核2G
运行环境:Java JDK11
服务器地址:119.xxx.204.xxx (广州公网IP地址)
注意,在服务器上进行测试时,要把我们需要使用的服务器的端口的防火墙打开,否则无法访问
开启服务器:
默认的端口是7545
多对一向服务器发送文件:
注意要设置客户端控制端口的不同
java -jar LFTP.jar lsend -f jay.mp3 -s 119.29.204.215 -sp 7545
java -jar LFTP.jar lsend -f ppt.zip -s 119.29.204.215 -sp 7545 -cp 20500
可以看到服务器为我们打开的数据端口30306和30700,需要提前打开服务器防火墙中的这两个端口
服务器端输出:
客户端输出:
由于服务器的带宽限制和处理器限制,速度很慢,在存在拥塞控制的情况下,每个客户端的速度只有10kb/s
列出服务器上的文件
可以看到,服务器中存在我们刚刚发送的两个文件
多对一从服务器下载文件
在这里,同样要注意设置客户端的控制端口不同
java -jar LFTP.jar lget -f jay.mp3 -s 119.29.204.21 -sp 7545
java -jar LFTP.jar lget -f ppt.zip -s 119.29.204.21 -sp 7545 -cp 20000
在输出中,我们可以看到服务器为我们打开的数据端口20230和20795,需要打开服务器防火墙的这两个端口
服务器端输出:
服务器端发送成功的输出:
客户端接收成功后输出:
在这里,因为与服务器的连接比较慢,我们又是通过线程来处理输出,因此显示的提示信息有些乱,但还是可以看到文件传输成功的
查看文件哈希值
最后,我们分别在两端查看文件的哈希值,来证明文件传输是没有问题的:
可以看到,文件的大小和内容是完全一样的,因此实现了100%可靠的传输。