IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 基于TCP/UDP/FTP实现的PySide2文件传输工具 -> 正文阅读

[网络协议]基于TCP/UDP/FTP实现的PySide2文件传输工具

引言

????????基于PySide2开发可视化文件传输程序界面,用户可以选择TCP/UDP/FTP类型协议在局域网中进行传输文件,用户一端既可以接收文件,也可以发送文件。做这个项目的时候,用了差不多一个多月的时间,目前实现了TCP和UDP,暂不支持传输多个文件(不过拓展起来很方便)!FTP协议后续新增跟上...

项目历程

第一个方案

? ? ? ? UI在主线程运行,开启子线程来进行文件发送和接收。也就是说,子线程中既要等待客户端连接,又要负责文件接收,只能作为客户端或者服务端,因此用户一端只能发数据或者收数据。因为使用到可视化界面设计,在构思功能时,希望UI可以通过某个按钮主动关闭服务端或客户端(只结束子线程,不结束主线程)。

? ? ? ? 然而,子线程中socket的recv、accept方法默认是阻塞的,主线程这边没有办法让子线程结束(子线程在while循环工作,加了标记位,但是子线程没机会判断到),即使把子线程设为守护线程,结果还是关闭不了。进一步,主线程先开启子线程,子线程再开启子线程(孙线程,设为守护线程),由主线程关闭子线程,孙线程还是无法结束(守护线程并非跟随父线程)。

第二个方案

? ? ? ? 为了让子线程有机会判断到全局标记位,让子线程自己结束自己,那么可以把recv、accept方法设成非阻塞状态,即setblocking(0)。服务端accept等待客户端没有连接时,会一直抛出BlockingIOError异常,然后检查标记位,判断是否要结束子线程,直到有客户端连接才接着执行后面的程序;同样把recv方法放在while循环,在没有收到数据的时候,让它有机会判断全局标记位。

self.server_obj.setblocking(0) # socket套接字对象改为非阻塞
# self.server_obj.setblocking(1) # socket套接字对象改为阻塞
        while self.thread_run_flag: # 表示线程在执行
            try:
                if not self.connect_client_flag: # 没有客户端连接
                    self.client_obj,self.client_addr = self.server_obj.accept() 
            except BlockingIOError: # 表示等待连接
                pass
            except ConnectionResetError:
                pass
tmp_data = self.client_obj.recv((SEND_RECV_SIZE))
self.recv_data_buff += tmp_data
if len(tmp_data) == (SEND_RECV_SIZE): # 表示客户端还有数据
   raise BlockingIOError
elif len(tmp_data) == 0: 
   return tmp_data
self.buff.data_from_client = self.recv_data_buff
self.recv_data_buff = b'' # 清空数据缓冲区
return self.buff.data_from_client # 返回接收到的总数据大小

????????最后确认这种方法有效,但是文件传输速度很慢(TCP),80M的文件居然耗时26s,后面经过调整socket的收发缓冲区大小(其实可以不用设置缓冲区大小,默认还是65536B,只是往send方法里传入50M的数据,recv(50M)这样子),从1024B到1M再到50M,花了0.2s的时间就传完了80M的文件,包括文件的读写过程。TCP是基于流通信的,应用端只管把数据提交到系统层,即使是100M也没影响,底层会把数据分成一段一段发过去(最大1500B,网卡的MTU限制),数据最终是能够达到目标地址。

? ? ? ? 在这个过程有很多东西要交互到UI,UI在主线程,而TCP在子线程,子线程想把打印显示到UI,基于这个需求,我自己封装了一个QT Signal类,当子线程有数据打印时,通过Signal信号触发UI对应的槽函数,就能把打印显示到UI了,这在以后文件传输的进度条显示上也使用到了。

# 自定义signal信号,用于子线程通知UI主线程显示消息
class Self_Signal(object):
    def __init__(self):
        self._signal = {}

    # name: 用于标识所定义的信号
    # slot:所定义的信号指向的槽函数
    # type1:信号和槽之间传递的参数类型
    # type2:默认不使用,预留着用于拓展参数个数
    def create(self, name, slot, type1, type2=None):
        class MySignals(QObject):
            if type2 is not None:
                obj = Signal(type1,type2)
            else:
                obj = Signal(type1)
        signal_object = MySignals()
        self._signal.update({name:signal_object})
        signal_object.obj.connect(slot) # 注册信号和槽
        return self._signal[name].obj

? ? ? ? 方案二运行起来唯一缺点就是耗性能,因为在非阻塞模式,如果没有客户端连接或没有数据发送过来,就一直会抛异常,这样并不好。

第三个方案?

? ? ? ? 为了UI能结束TCP子线程,而不用把TCP Socket设为非阻塞,也就是说,我需要的只是结束子线程,主线程(UI)不用结束,经过想了想之后,决定用多进程解决这些问题;

????????用进程的好处,不用等待其他子线程或子进程执行完,就能立即结束掉(子进程结束后,所有的子进程资源都会被销毁),需要工作时再创建开启子进程。比方案二好就是不用再考虑阻不阻塞的问题。

?????????程序运行起来后,只有一个UI主进程,在初始化的时候创建一个UI子线程,用于和TCP子进程交换进程的通信数据。TCP子进程只有在用户点击监听(作为服务端)或点击发送(作为客户端)时,才会被创建和开启。TCP子进程里又开启两个子线程(同时开启运行),一个作为服务端,立即开启等待客户端连接(等待客户端发送文件过来);另一个作为客户端,需要等待UI主进程的发送文件信号,才会开启连接对方的服务端。这两个子线程在接收或发送完文件后,又回到等待连接或发送状态,整个子进程随时都有可能被UI主进程主动结束。

  1. 打印消息队列
    1. 比如TCP子进程中的子线程,如果有消息要打印到UI,消息先从该子线程传递到TCP主线程(queue.Queue),然后TCP主线程在while循环判断到该队列不为空时,就取出信息放到用于进程间通信的打印消息队列(multiprocessing.Queue);
    2. UI子线程while循环判断到打印消息队列不为空,就取出消息并发送signal信号,通知UI主线程响应对应的槽函数,把消息加载到UI。
  2. 连接状态队列
    1. TCP子线程中关于socket的连接状态,比如服务端是否绑定端口失败,客户端是否连接对方服务端失败等,会把对应的状态放到该队列;
    2. 先是TCP主线程从queue.Queue中取出状态,放到multiprocessing.Queue;
    3. 然后UI子线程从多进程队列取出状态,发送signal信号,通知UI主线程响应对应的槽函数去处理;
    4. 比如如果是绑定端口失败,则结束TCP子进程。
  3. 文件进度队列
    1. 在文件传输过程中,更新文件传输的速度状态放到该队列;
    2. 先是TCP主线程从queue.Queue中取出状态,放到multiprocessing.Queue;
    3. 然后UI子线程从多进程队列取出状态,发送signal信号,通知UI主线程响应对应的槽函数更新进度条的状态。
  4. 发送文件信号队列
    1. 在UI主进程中,设置发送目标文件的路径,UI子线程把路径信息放到multiprocessing.Queue;
    2. TCP主线程在while循环判断到该队列不为空时,取出路径信息放到queue.Queue用于和子线程间进行通信;?

???????当用户点击发送按钮时,发送时,先连接对方的服务端(另一端服务是开启监听状态下),接着计算发送的文件MD5; 客户端开始发送文件的头部信息(文件名、文件大小、文件的MD5)过去给服务端;等待服务端的响应后(确认收到文件头部信息后),接着发送文件的内容;

????????服务端监听到有客户端连接了,就准备接收文件头部信息,收到文件头部信息后,答应给客户端确认收到了,接着准备收文件内容;接收文件保存完成后,开始计算保存的文件MD5;

file_hash = hashlib.md5()
with open(path, "rb") as f:
     while chunk := f.read(g_SEND_RECV_SIZE): # chunk表示表达式中的变量
          file_hash.update(chunk) 
     return file_hash.hexdigest()

????????如果保存的文件MD5值和接收文件头部信息里MD5的值一样,则该文件是安全、有效的。

????????TCP一次收发数据的大小(可修改的),是服务端和客户端先固定好的,TCP协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,因为TCP是面向流的协议,是可靠传输的,但是会有粘包的现象;

?????????而UDP一次收发数据大小是有限制(65507B)的,不能一次提取任意字节的数据,这一点和TCP是很不同的,因为UDP传输是面向报文的,udp本身就是不可靠协议,用数据包(消息来源地址,端口等信息)的形式发送,底层会把数据分成一段一段发过去(最大1500B,网卡的MTU限制),一旦被拆包后,有的数据会被分成没有报头信息,所以就会发生丢包现象。??

? ? ? ? ?为了UDP在传输文件的过程中,另一端收到的文件是完整的、有效的;所以让UDP通过建立传输收发确认机制,因为UDP服务端收到客户端发来的第一次数据后,还要进行写入文件,过程还要花点时间,客户端可能已经发送第二次数据过去了,服务端还没来的及接收第二次数据,这样就会有大量的数据丢失,所以收发一次就应答一次,保证了每一次发送和接收的数据都能达到。UDP通过这种机制来传输文件,文件传输的速度也受到影响,UDP和TCP传输速度相比,都是差不多。

# 作为客户端
def send_file_content(file_path, dest_addr, udp_socket):
    tmp_len = 0
    with open(file_path,'rb') as f:
        while True:
            recv_data = f.read(SEND_RECV_SIZE)
            tmp_len += len(recv_data)
            if not recv_data:
                udp_socket.sendto(''.encode(),dest_addr)
                print('发送完成-->')
                udp_socket.close()
                break
            udp_socket.sendto(recv_data,dest_addr)
            # 等待服务端应答一次,客户端再接着发送第二次
            recv_data = udp_socket.recvfrom(2) 
    print('发送出去的总文件大小:{}'.format(get_file_size(tmp_len)))



# 作为服务端
def save_file(file_name, udp_socket, client_addr):
    tmp_len = 0
    with open(f'save_file/{file_name}','wb') as f:
        while True:
            recv_data = udp_socket.recvfrom(SEND_RECV_SIZE)
            tmp_len += len(recv_data[0])
            if len(recv_data[0]) == 0:
                print('保存文件成功!')
                udp_socket.sendto(''.encode(),client_addr)
                udp_socket.close()
                break
            f.write(recv_data[0])
            # 接收到客户端发来的数据后,就向客户端回复一次
            udp_socket.sendto(''.encode(),client_addr) 
    print('接收后的文件大小:{}'.format(get_file_size(tmp_len)))

?????????子进程有事务,需要先放到队列里,由 UI主进程里的 子线程去取出事务,子线程再通过 signal 通知 UI 主线程处理。

 self.log_signal = self.signal.create('log', self.slot_print_log, str)

????????父进程让子进程结束。

self.child_process_obj.terminate() 

???????使用multiprocessing的队列进程间进行通信。

multiprocessing.Queue(10)

? ? ? ? 考虑有些情况下,发送端发送的文件名过于太长,超过给定固定的文件名长度(60个Byte长度),文件头部信息解析就有问题(收发文件都是转用Byte类型),因为中文encode()的长度和len(str)的长度不一样

a = '你好'
print(len(a)) # 2
print(len(a.encode())) # 6

????????所以发送文件头部信息前,先判断文件名是否含有中文,含用中文的情况,以下代码实现

# 文件名含有中文,并且超过60个Byte固定长度
if len(param_1) < 60 and len(param_1.encode())>60: 
        tmp = param_1 # 用一个临时变量接收
        p_len = int(len(param_1)/2) # 以中间的元素下标为基准
        while True:
            tmp = tmp[:p_len] + tmp[p_len+1:] # 开始从中间的元素下标开始取
            if len(tmp.encode()) <= 60: # 转成Byte类型,不超过60个Byte长度
                break
            # print(len(tmp.encode()))
            p_len = int(len(tmp)/2)
        param_1 = tmp 

?运行程序

?????????前提准备:电脑有安装python3.9+,支持Windows/Linux

?????????依赖库:pip install PySide2

?????????同时可以多开几个终端,运行脚本

?????????运行脚本:python ui_main.py

文件传输效果图

????????TCP协议传输

????????UDP协议传输?

具体代码

????????链接:https://pan.baidu.com/s/1SH9UBA2rEkKuuZRa6TMM9Q?
????????提取码:linv

今天就先到这里啦,如果对你有帮助的,赶紧收藏下来吧!觉得代码哪里有问题或者有建议的,都可以打在评论上,我会留意的,互相学习,互相探讨,你我皆黑马。

后续我将继续新增FTP协议...

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-09-21 01:01:36  更:2022-09-21 01:04:27 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 20:24:44-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码