一.项目要求
Please choose one of following programing languages: C, C++, Java, Python;
本项目采用的是python3.6
LFTP should use a client-server service model;
本项目使用客户端-服务器的模式
LFTP must include a client side program and a server side program; Client side program can not only send a large file to the server but also download a file from the server.
Sending file should use the following format:
LFTP lsend myserver mylargefile
Getting file should use the following format
LFTP lget myserver mylargefile
The parameter myserver can be a url address or an IP address.
本项目,客户端不仅可以向服务器上传大文件,也可以从服务器下载大文件
LFTP should use UDP as the transport layer protocol.
本项目利用UDP来作为传输层协议
LFTP must realize 100% reliability as TCP;
本项目实现类似TCP的100%可靠性,处理了丢包,超时,数据包顺序不一致等问题
LFTP must implement flow control function similar as TCP;
本项目实现了类似TCP的流控制,在接收方维护一个rwnd接收窗口
LFTP must implement congestion control function similar as TCP;
本项目实现了类似TCP的阻塞控制,在发送方维护一个cwnd阻塞窗口
LFTP server side must be able to support multiple clients at the same time;
本项目支持多个用户同时向服务器收发文件,使用了多线程的机制
LFTP should provide meaningful debug information when programs are
executed.
本项目提供了有意义的debug消息来显示发送情况,包括丢包,阻塞等事件的处理
二. 设计思路
基于UDP的传输过程如下图所示:
基于UDP来实现类似TCP的大文件传输,代码构建过程:
利用socket实现简单字符串传送
对于文件进行处理,将1的代码改造成文件的传送
对于文件进行分段,打包发送
发送过程使用多线程
服务器与客户端连接的建立与断开,三方握手,四次挥手
处理命令行输入命令,与服务器进行交互
添加ACK反馈包,建立判断重复ACK机制
丢包事件的判断与处理
接收方的序列号是否正确,要求重发
接收方的流控制,构建接收窗口
发送方的阻塞控制
三. 模块设计
3.1 设置传送数据包的数据结构,以及反馈包的数据结构
这里的数据包与反馈包结构我采用的是struct结构体,其中一个数据包所带有文件数据为1024bytes,而头文件包括序列号,确认号,文件结束标志,为24bytes。
反馈包包括ack确认,rwnd接收窗口的大小。
传送一个包的结构,包含序列号,确认号,文件结束标志,数据包
packet_struct = struct.Struct(‘III1024s’)
接收后返回的信息结构,包括ACK确认,rwnd
feedback_struct = struct.Struct(‘II’)
BUF_SIZE = 1024+24
FILE_SIZE = 1024
3.2 服务器部分:处理客户端传输的命令,并使用多线程来处理
服务器先建立一个主的线程来监听接收客户端到来的命令,将命令接收后传送到server_thread来处理,传递的参数包括客户端的命令参数以及客户端的地址。
···
IP = ‘192.168.88.129’
SERVER_PORT = 7777
···
def main():
s = socket.socket(socket.AF_INET,socket.SOCK_DGRAM)
# 绑定端口:
s.bind((IP, SERVER_PORT))
while True:
data,client_addr = s.recvfrom(BUF_SIZE)
# 多线程处理
my_thread = threading.Thread(target=server_thread,args=(client_addr,data))
my_thread.start()
3.3 服务器部分:多线程处理函数
该函数要为每一个客户端新建一个socket,并将命令解析得到客户端所需要的操作
def server_thread(client_addr,string):
# 处理传输过来的str,得到文件名,命令
order = ‘’
try:
order = string.decode(‘utf-8’).split(’,’)[0]
file_name = string.decode(‘utf-8’).split(’,’)[1]
except Exception as e:
return
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
···
除此以外,还需要像TCP建立连接一样来进行三方握手,确认连接才开始发送。
客户端先申请连接
三方握手
发送请求建立连接
s.sendto(data,server_addr)
接收连接允许
print(data.decode(‘utf-8’))
data,server_addr = s.recvfrom(BUF_SIZE)
print(‘来自服务器’, server_addr, '的数据是: ', data.decode(‘utf-8’))
服务器确认连接
文件存在,返回确认信号
s.sendto(‘连接就绪’.encode(‘utf-8’),client_addr)
data,client_addr = s.recvfrom(BUF_SIZE)
print(‘来自’,client_addr,‘的数据是:’,data.decode(‘utf-8’))
客户端再发送确认包,这时连接就建立完毕
第三次握手,确认后就开始接收
data=‘ACK’.encode(‘utf-8’)
s.sendto(data,server_addr)
下面是四次挥手的过程:
当服务器执行完客户端的命令后,要像tcp一样来执行断开连接操作,断开连接后可以把socket释放掉,即调用close函数。
服务器:
print(’n开始中断连接’)
中断连接,四次挥手
data,server_addr = s.recvfrom(BUF_SIZE)
print(data.decode(‘utf-8’))
data = ‘Server allows disconnection’
s.sendto(data.encode(‘utf-8’),client_addr)
print(data)
data = ‘Server requests disconnection’
s.sendto(data.encode(‘utf-8’),client_addr)
print(data)
data,server_addr = s.recvfrom(BUF_SIZE)
print(data.decode(‘utf-8’))
print(‘The connection between client and server has been interrupted’)
s.close()
客户端:
中断连接,四次挥手
data = ‘Client requests disconnection’
print(data)
s.sendto(data.encode(‘utf-8’),server_addr)
data,client_addr = s.recvfrom(BUF_SIZE)
print(data.decode(‘utf-8’))
data,client_addr = s.recvfrom(BUF_SIZE)
print(data.decode(‘utf-8’))
data = ‘Client allows disconnection’
s.sendto(data.encode(‘utf-8’),server_addr)
print(data)
print(‘The connection between client and server has been interrupted’)
s.close()
说完建立连接与断开连接,回到多线程处理函数,对于lsend,lget函数的判断:当命令是lget时,要判断服务器是否存在该文件,如果不存在则直接返回信息后,关闭socket;若存在该文件直接进入lget函数来进行处理。当命令是lsend时,直接进入lsend函数来进行处理。
if order == ‘lget’:
# 处理文件不存在的情况
if os.path.exists(file_name) is False:
data = ‘FileNotFound’.encode(‘utf-8’)
s.sendto(data, client_addr)
# 关闭socket
s.close()
return
# 文件存在,返回确认信号
s.sendto(‘连接就绪’.encode(‘utf-8’),client_addr)
data,client_addr = s.recvfrom(BUF_SIZE)
print(‘来自’,client_addr,‘的数据是:’,data.decode(‘utf-8’))
lget(s,client_addr,file_name)
elif order == ‘lsend’:
s.sendto(‘是否可以连接’.encode(‘utf-8’),client_addr)
# 等待确认
data,client_addr = s.recvfrom(BUF_SIZE)
print(‘来自’,client_addr,‘的数据是:’,data.decode(‘utf-8’))
lsend(s,client_addr,file_name)
完整的源码和详细的文档,上传到了 WRITE-BUG技术共享平台 上,需要的请自取:
https://www.write-bug.com/article/2998.html