python socket基于TCP的文件传输程序
TCP,Transmission Control Protoco
TCP,英文Transmission Control Protocol,简称传输控制协议。是HTTP协议中的一种,TCP/IP协议族是网络运作的基础。
图源自《图解HTTP》 [(日)上野宣著]
TCP的特点
- 面向连接
- 通信的双方必须先建立好连接才能进行数据的传输,数据传输完成后,双方必须断开此连接,以释放系统资源
- 可靠传输
- TCP采用发送应答机制
- 超时重传
- 错误校验
- 流量控制和阻塞管理
TCP程序的开发
TCP是面向连接的,那么就必须要有连接的双方,即客户端与服务端。
TCP网络应用程序开发分为:
- 客户端应用程序开发
- 服务端应用程序开发
- 客户端指运行在用户设备上的程序;服务端是运行在服务器设备的程序,专门为客户端提供数据服务。
服务端是处于被动状态的,从服务端的运行开始,就一直等待客户端的连接,服务端会一直阻塞在等待客户端连接的状态,直到有客户端进行连接。
客户端发起主动连接请求,直到服务端回应请求,便会建立连接,否则也会处于阻塞状态。
一个客户端只能在一个时间内与一个服务端建立连接。要与其他服务端建立连接,必须要先断开现有的连接。
python socket
socket是python自带的库,可以实现进程之间的网络通信。socket简称套接字,我还是比较喜欢读成socket。
那么怎么使用socket实现进程间的网络通信呢?
socket客户端的开发
根据前面的开发流程,客户端的开发流程比较简陋。
- 创建客户端的socket
- 和服务端socket建立连接
- 传输数据(发送和接收)
- 关闭客户端的socket
import socket
# 创建tcp客户端套接字
# 1. AF_INET:表示ipv4
# 2. SOCK_STREAM: tcp传输协议
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM
# 和服务端应用程序建立连接
# connect方法需要传入一个元组对象,元组的元素为服务端的IP地址,服务端的端口号
tcp_client_socket.connect(("192.168.131.62", 8080))
# 客户端与服务端建立连接的过程是一直处于阻塞的,直到建立连接
# 代码执行到此,说明连接建立成功
# 准备发送的数据
send_data = "你好服务端,我是客户端小黑!".encode("gbk")
# 发送数据
tcp_client_socket.send(send_data)
# 接收数据, 这次接收的数据最大字节数是1024
recv_data = tcp_client_socket.recv(1024)
# 返回的直接是服务端程序发送的二进制数据
print(recv_data)
# 对数据进行解码
recv_content = recv_data.decode("gbk")
print("接收服务端的数据为:", recv_content)
# 关闭套接字
tcp_client_socket.close()
TCP只支持传输字节流数据。
那么这里就可以进行文件的传输了。
socket服务端程序的开发
服务端程序开发流程:
- 创建服务端socket对象
- 绑定服务器设备的端口号(设置端口重用)
- 设置监听
- 等待客户端连接请求
- 传输数据
- 关闭socket
import socket
# 创建tcp服务端套接字
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置端口号复用,让程序退出端口号立即释放
tcp_server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
# 给程序绑定端口号
tcp_server_socket.bind(("", 8989))
# 设置监听
# 128:最大等待建立连接的个数, 提示: 目前是单任务的服务端,同一时刻只能服务与一个客户端,后续使用多任务能够让服务端同时服务于多个客户端
# 不需要让客户端进行等待建立连接
# listen后的这个套接字只负责接收客户端连接请求,不能收发消息,收发消息使用返回的这个新套接字来完成
tcp_server_socket.listen(128)
# 等待客户端建立连接的请求, 只有客户端和服务端建立连接成功代码才会解阻塞,代码才能继续往下执行
# 1. 专门和客户端通信的套接字: service_client_socket
# 2. 客户端的ip地址和端口号: ip_port
service_client_socket, ip_port = tcp_server_socket.accept()
# 代码执行到此说明连接建立成功
print("客户端的ip地址和端口号:", ip_port)
# 接收客户端发送的数据, 这次接收数据的最大字节数是1024
recv_data = service_client_socket.recv(1024)
# 获取数据的长度
recv_data_length = len(recv_data)
print("接收数据的长度为:", recv_data_length)
# 对二进制数据进行解码
recv_content = recv_data.decode("gbk")
print("接收客户端的数据为:", recv_content)
# 准备发送的数据
send_data = "ok, 问题正在处理中...".encode("gbk")
# 发送数据给客户端
service_client_socket.send(send_data)
# 关闭服务与客户端的套接字, 终止和客户端通信的服务
service_client_socket.close()
# 关闭服务端的套接字, 终止和客户端提供建立连接请求的服务
tcp_server_socket.close()
这里需要注意的一点就是,当客户端和服务端建立连接后,服务端程序退出后端口不会立即释放,需要等待1-2分钟。
我们可以每次关闭后短时间内再次开启服务端程序,可以选择更换端口号,防止端口被占用导致程序异常停止;
除了这种方法之外,我们可以设置端口号复用,这样在退出服务端程序后端口会立即被释放,就不需要重复设置端口号了。
了解了简单的客户端与服务端的简单数据传输之后,来回归标题,怎么实现文件传输。
TCP的文件传输程序
文件,包括图片、视频、压缩文件等在计算机中都是以二进制的形式存储的,而TCP传输的数据也是字节流,那么就可以将读取到的文件内容(数据)直接通过socket传输到另一端(客户端或服务端)。
但是由于socket的限制,在同一时间只能进行传输的一种,即一端只能进行文件的发送而另一端进行文件的接收,如果想要反过来,则需要在一次传输任务结束后再次进行传输。
这里的程序与上面的开发流程一致,只是在发送之前需要将文件读取出来,接收后把接收到的文件存放到磁盘中。
传输处理程序
传输处理程序分为两种,一种为文件接收的处理程序,一种为文件发送的处理程序。
文件接收的处理程序受制于文件发送的处理程序(文件怎么发送决定了要怎么进行接收)。
将传输处理程序封装成函数,那么客户端跟服务端都可以复用。
首先是文件的发送处理程序:
import socket
import os
import time
# Resource 文件夹保存接收的文件
def creat_folder(path):
if os.path.exists(path):
return
else:
os.mkdir(path)
def send_handle(file_name, file_size, client_server):
"""
处理传输文件数据,将文件读取并发送到接收端,只允许单次发送
单次发送失败后需要进行重连再重新发送
:param file_name: 要发送的文件名
:param file_size: 要发送的文件的大小
:param client_server: 用于传输数据的socket,发送端的socket
:return: 发送文件的结果,1为发送成功,0为发送失败
"""
al_read_size = 0 # 保存已读取的文件大小,显示读取的进度
if file_name and file_size:
# 判断传入的文件信息是否空
client_server.send(b"starting send file")
with open(file_name, "rb") as f:
while True:
# 循环读取文件
file_content = f.read(1048576) # 每次从文件种读取1M数据
al_read_size += len(file_content) # 计算总共读取的数据的大小
if file_content: # 判断文件是否读取完了
print("{}%".format(al_read_size/file_size)) # 输出读取文件的进度
client_server.send(file_content) # 将读取的文件发送到服务端
else:
print("100%") # 判断文件读取完了,输出读取的进度
return 1 # 文件读取发送完了,返回处理情况
else:
print("Can't find the file or the file is empty.") # 打开文件失败,文件或文件名为空,则退出发送服务
client_server.send(b'cancel send file.') # 通知服务端取消文件的发送
return 0 # 文件未发送成功,返回0
def send_server(client_server):
# 输入需要发送的文件名,包括文件后缀。仅限二进制文件,包括图片、视频、压缩文件等
file_name = input("Please enter the file path or the file name:")
if os.path.exists(file_name) and (not os.path.isdir(file_name)): # 判断文件是否存在,是否文件夹
# 获取文件的大小
file_size = os.path.getsize(file_name)
file_message = file_name + "|" + str(file_size)
# 与服务端建立连接后,先将文件名字与文件的大小发送给服务端
client_server.send(file_message.encode())
# 对方接收到了file_message的信息后返回一个“copy”,接收不成功会返回别的信息
recv_data = client_server.recv(1024)
# 判断对方是否接收信息成功
if recv_data.decode() == "copy":
print("start to send data...")
start_time = time.time() # 计算发送文件的开始时间
send_flag = send_handle(file_name, file_size, client_server) # 发送文件的请求处理,返回处理结果
end_time = time.time() # 计算发送文件的结束时间
spend_time = end_time - start_time # 计算发送文件的耗时
print("sending file spend {} s".format(spend_time)) # 在控制台输出发送文件的耗时
if send_flag: # 判断文件是否发送成功
recv_message = client_server.recv(1024)
if recv_message == "ok":
# 文件发送成功
print("send file successful, close the client server.")
client_server.close()
return 1
else:
# 对方文件接收不成功
print("server recv file failed.")
client_server.close()
return 0
else:
# 文件发送不成功
print("Error,failed to send the file.")
client_server.close()
return 0
else:
# 对方没有接收到文件名及文件大小,或者对方断开了连接,取消发送文件,并关闭socket,退出发送服务
print("Can't recv the server answer.")
print("The client don't send the file data and close the server.")
client_server.close()
return 0
try:
client_server.close() # 尝试关闭本方的socket,防止前面没有进行关闭,如果前面已经关闭了,直接退出函数
except Exception:
pass
接收处理程序:
def recv_handle(file_path, file_size, client_server):
"""
接收文件的处理函数,只允许单次接收,一次接收失败后需要重新建立连接后重新发送
:param file_path: 保存文件的路径
:param file_size: 要接收的文件的大小
:param client_server: 传输服务的socket
:return: 接收文件的结果,1表示接收成功,0表示接收失败
"""
print("Start to recv th file...")
recv_size = 0 # 保存接收的文件的大小
start_time = time.time() # 保存开始接收文件的时间
with open(file_path, "ab") as f:
while True:
# 循环接收文件数据
file_content = client_server.recv(1048576)
if file_content: # 判断文件是否接收完了
recv_size += len(file_content) # 累计接收的文件大小
f.write(file_content) # 将接收的数据保存到文件中
else:
# 如果文件接收完了,则退出循环
end_time = time.time() # 保存文件接收结束的时间
print("spend time:{}".format(end_time - start_time))
break
if recv_size == file_size: # 判断接收的文件大小与对方发送的文件大小是否一致
print("文件全部接收完毕,耗时:{}".format(end_time - start_time))
client_server.send(b'ok')
return 1
else:
print("文件未接收完成,只接收了{}%".format(recv_size/file_size))
print("Failed to recv the file.")
client_server.send(b'fail')
return 0
def recv_server(client_server):
print("Ready to recv the file...")
# 接收发送端发送的文件名及文件大小
file_name, file_size = client_server.recv(1024).decode().split("|")
creat_folder("Resource")
file_path = os.path.join("Resource", file_name)
# 判断文件名及文件大小是否为空
if file_name and file_size:
client_server.send(b'copy') # 反馈文件发送端,已收到文件名及文件大小
start_flag = client_server.recv(1024).decode()
if start_flag == "starting send file":
recv_flag = recv_handle(file_path, file_size, client_server) # 启用文件接收服务
# 判断文件的接收结果
if recv_flag:
client_server.close()
print("文件接收成功,断开连接")
return
else:
print("文件接收失败,断开连接")
client_server.close()
else:
print("对方拒绝发送文件,取消连接")
client_server.close()
return
else:
# 文件名或文件大小为空,拒绝接收文件,断开连接
client_server.send(b'refuse')
client_server.colse()
return
主要的发送跟接收的代码不需要这么多,我在其中增加了很多发送跟接收的结果判断。
这里使用了循环去读取、发送、接收文件,这是因为socket的发送跟接收机制决定的,这样的好处就是可以发送大文件而不会导致内存溢出。
socket的发送接收机制
socket的send都会先将数据传给缓冲区,缓冲区再传给网卡,通过网卡将数据发送出去;recv方法接收通过网卡将数据接收到接收缓冲区,再从缓冲区中将数据接收。
如下图所示:
缓冲区的大小是由内存分配的,所以如果一次发送过多、或接收过多的数据会造成内存溢出。这一点需要注意。
还可以使用面对对象来实现。
这里就不再赘述了。
总结
这是我写的第一篇博客,谢谢你能看到最后。
目前正在学习中。后续也会将学习的内容写成博客的。emmm有空的话。
我写这个的时候,也参考了CSDN另一位博主的代码,先将文件名跟文件大小用“|”拼接并发送过去就是参考了他的做法,当初忘记保存他的链接地址了。非常不好意思。
如果在上面发现了问题欢迎与我联系。