【python】socket-传输多个文件、大文件

2023-10-26

0-前言

看过挺多个发文件的例子,但是基本都是发单个,且是 发完连接就结束了

最近正好需要 一个连接 发送 多个文件,根据需求产生以下内容

涉及知识点:socket 的客户端和服务端应用、json、TCP粘包处理

1-发送单个文件流程

  1. 【客户端】获取文件信息

    必备:大小

    可选:文件名、文件绝对路径

  2. 【客户端】准备一个消息,告诉对方我们要发送的内容、属性信息

    {
        "消息类型":"请求发送文件",
        "数据内容":{"大小":123, "文件名":"", "文件绝对路径":"",}
    }
    
  3. 【服务端】收到 “请求发送文件” ,根据文件的内容,做好准备接收,回复可以发送 “request_ack”

  4. 【客户端】开始发送数据

  5. 【服务端】不断接收数据

  6. 【客户端】发送数据结束,发送“quit”

  7. 【服务端】收到“quit”,说明数据发送结束了;回复“quit_ack”表示我方已经接收完毕

在这里插入图片描述

# 自行准备一个文件的信息字典
{
    "文件路径":{各种属性}
}

# 【以下为示范】
data = {
    "\\HCIA-Security.docx": {
            "绝对路径": "D:\\Users\\Desktop\\HCIA\\HCIA-Security.docx",
            "文件全名": "HCIA-Security.docx",
            "文件名": "HCIA-Security",
            "拓展名": ".docx",
            "最近修改时间": 16786228.8041983,
            "大小": 542353,
            "类型": "文件",
		   "判断结果": "服务端不存在此文件"
        },
}
# 客户端发送文件示范,仅提供思路,直接复制不一定跑的起来,因为这只是我程序里的部分代码

def fasong_dange_wenjian(a_data, a_peer):
    """
        发送单个文件
    :param a_data:单个文件的各种属性
    :param a_peer:socket
    :return:
    """
    # 发送 "请求文件传输" 类型消息,让对端 进入接收文件状态
    temp_json = json.dumps({
        'data_type': "请求文件传输",
        "data": a_data})
    socket.send(temp_json.encode('gbk'))

    if a_data["类型"] == "文件":
        print('--1011发送文件--对方的文件', a_data["对比方的绝对路径"])
        _file_path = ''
        # 等待对方回复可以开始发送
        _request_ack = socket.recv(1024)
        if _request_ack == b'request_ack':  # 确认可发送就开始 发送文件
            _file_path = a_data["绝对路径"]  # 获取这个文件的绝对路径
            print(f"获取这个文件的绝对路径{_file_path}")

            with open(_file_path, "rb") as f:
                """
                    1kB = 1024 字节
                    1MB = 1024 * 1024 = 1048576 字节
                    1GB = 1024 * 1024 * 1024 = 1,073,741,824 字节
                """
                _read_size = 1048576 * 500  # 每次读取 500M,以防文件太大,内存不够报错
                while True:
                    _file_data = f.read(_read_size)
                    if _file_data:
                        socket.sendall(_file_data)  # 发送文件
                    else:
                        print("读取的内容为空,读取结束---")
                        break

            socket.send(b'quit')  # 发送 文件传输结束 信息,说明文件传输结束
            print("1027 文件发送结束")

            _quit_ack = socket.recv(1024)
            if _quit_ack == b'quit_ack':
                print('对方已经收到此文件:', _file_path)
    elif a_data["类型"] == "文件夹":  # 如果是文件夹,我们发送就结束了
        print('1030-发送文件夹--对方的文件夹', a_data["对比方的绝对路径"])
        # 等待对方回复可以开始下个循环
        _dir_ack = socket.recv(1024)
        if _dir_ack == b'dir_ack':
            print('对方已经创建此文件夹:', _dir_ack)


# 循环发送单个文件
for x, y in data.items():  # x 文件相对路径,y 文件各种属性
    if y["判断结果"] == "服务端不存在此文件":
        print("--开始发送单个文件", y["对比方的绝对路径"])
        fasong_dange_wenjian(y, peer)  # 传个文件各种属性 给他就行了
# 服务端接收文件示范,仅提供思路,直接复制不一定跑的起来,因为这只是我程序里的部分代码

# 等待接收服务端的消息
recved = socket.recv(102400)

# json.loads()  将已编码的 JSON 字符串解码为 Python 对象
_temp_b = recved.decode(encoding='gbk')
temp_json = json.loads(_temp_b)

if temp_json['data_type'] == '请求文件传输':  # 收到 客户端 要发送给 服务端请求
    while True:
        # 开始接收数据
        recved = socket.recv(10240)
        _temp_b = recved.decode(encoding='gbk')
        print(f"--数据是: {_temp_b}")
        temp_json = json.loads(_temp_b)
        # temp_json = json.loads(recved.decode(encoding='gbk'))

        if temp_json['data_type'] == '请求文件传输':  # 收到 客户端 要发送给 服务端请求
            print("--821--收到 '请求-文件传输'")

            # "data" 是 对方的绝对路径
            # 判断是不是个文件
            if temp_json["data"]["类型"] == "文件":
                print(f'824--这是文件{temp_json["data"]["对比方的绝对路径"]}')
                # 开始接收文件
            elif temp_json["data"]["类型"] == "文件夹":
                print(f'824--这是文件夹{temp_json["data"]["对比方的绝对路径"]}')
                os.makedirs(temp_json["data"]["对比方的绝对路径"])  # 是目录,直接创建就行了
                print("文件夹创建成功", temp_json["data"]["对比方的绝对路径"])
                socket.send(b'dir_ack')  # 发送信息,说明文件夹创建好了

        elif temp_json['data_type'] == '请求文件传输-结束':  # 文件发送接收,退出 文件传输模式
            break

2-关于发送大文件,本地读取时报错 MemoryError

读取一个4G的文件报了个内存不够的错误

解决方式1:设置每次读取的最大大小

with open(_file_path, "rb") as f:
    """
        1kB = 1024 字节
        1MB = 1024 * 1024 = 1048576 字节
        1GB = 1024 * 1024 * 1024 = 1,073,741,824 字节
    """
    _read_size = 1048576 * 500  # 每次读取 500M,以防文件太大,内存不够报错
    while True:
        _file_data = f.read(_read_size)
        if _file_data:
            socket.sendall(_file_data)  # 发送文件
        else:
            print("读取的内容为空,读取结束---")
            break

解决方式2:更换64位的python

某天发现控制面板里的python是32位,突然想起来32位有最大内存限制,果断卸载换了64位
在这里插入图片描述
参考大佬文章:
【已解决】Python MemoryError的问题

3-关于粘包

问题背景

一开始没有限制 socket接收的大小

传小文件没啥问题

然后发现在传输 几个G的文件,传输完成后不会执行后面的写入文件,怀疑【服务端】没收到【客户端】传输结束的 “quit”(自定义的退出标志)

排错过程

考虑是不是发太多,程序反应不过来,在【客户端】新增发送文件数据后 等几秒再发送 “quit”(自定义的退出标志)

测试传1G的文件可以了,但是5G的文件又不行了

干脆记录下 【服务端】一共接收了【客户端】的多少数据

"""
	以下是打印 最后2次 接收数据 的记录
	传输的文件大小是 3.8G多 (4096065536字节)
	【客户端】发送的quit 是 4字节
"""

						#  已经接收的大小/文件总大小(单位字节)
接收到数据------4829680 总大小:4095384680/4096065536 
接收到数据------680860  总大小:4096065540/4096065536 

可以非常清楚的看到

最后一次可以接收的大小: 4095384680(总大小) - 4096065536(已经接受的大小) = 680856

实际上最后一次 接收了 680860,刚好多了4字节,而且 【客户端】发送的quit 是 4字节

所以,最终判断,粘包了

解决方案

单个文件,单个连接场景:

发完连接断开就结束了,可以无视

多个文件,只使用一个连接,用for循环 每次发送 单个文件 场景:

  1. 准备3个变量,用于存放以下数据
    1. 获取接收文件的大小信息
    2. 统计已经接收的数据大小
    3. 可接收的剩余数据大小
  2. 实时设置我们还可以接收的大小
    1. 我们接收的缓冲区大小 = 接收文件的总大小 - 已经接收的数据大小
  3. 缓冲区大小,一开始会是文件的大小,但是实际上不可能一次全发完占满,只会一部分一部分发,随着不断接收,会越来越小,直到结束为0
# 【服务端 接收侧】

"""
	准备3个变量
"""
file_size	# 获取接收文件的大小信息
_temp_size = 0   # 统计已经接收的数据大小
recv_size   # 可接收的剩余数据大小


"""
	开始接收
"""
while True:
    # 开始接收文件数据
    if file_size != 0:  # 如果对面 不是个空文件, recv_size != 0
        recv_size = file_size - _temp_size  # 传大文件会发送粘包,导致最后的 quit 黏在 文件数据上,所以实时计算可接收的剩余数据大小
        if recv_size > 0:  # 保证接收的大小在 文件大小之内
            _temp_file_data = socket.recv(recv_size)
            _temp_size += len(_temp_file_data)
            # print(f"接收到数据------{len(_temp_file_data)} 总大小:{_temp_size}/{file_size}  进度:{_temp_size/file_size}")
            print(f"接收到数据------{len(_temp_file_data)}  进度:{(_temp_size / file_size) * 100}")
        elif recv_size == 0:  # 代表文件接收结束,接下来等待 接收 quit
            _temp_file_data = socket.recv(1024)

    elif file_size == 0:  # 如果对面是个空文件, recv_size == 0
        _temp_file_data = socket.recv(1024)
        print("接收到数据------", len(_temp_file_data))

    # 只要不是完成信号,或者数据为0,就一直接收
    if _temp_file_data == b'quit':
        print("接收完毕,quit", "文件总大小 :", file_size)
        break
    elif len(_temp_file_data) == 0:
        print("这是空数据---quit---")
        break

    _file_date += _temp_file_data  # 把数据先存入变量

# 写入文件【你也可以设置收到数据就写入文件,节省内存,但是我不缺内存,选择先存在内存 后写入储存】
with open(_file_path, "wb+") as f:
    f.write(_file_date)
    print(f"{_file_route} 写入成功")

# 接受完成标志
socket.send(b'quit_ack')

4-备注-换算表

换算表【数据源自百度百科】

中文单位 中文简称 英文单位 英文简称 进率(Byte=1)
比特 比特 bit b 0.125
字节 字节 Byte B 1
千字节 千字节 KiloByte KB 2^10
兆字节 MegaByte MB 2^20
吉字节 GigaByte GB 2^30
太字节 Terabyte TB 2^40
拍字节 PetaByte PB 2^50
艾字节 ExaByte EB 2^60
泽字节 ZettaByte ZB 2^70
尧字节 YottaByte YB 2^80
千亿亿亿字节 千亿亿亿字节 BrontByte BB 2^90

若是用普通网速1Mbs计算,约8秒钟能下载1MB的文件。

1b(bit,位 或 比特,b)

1B(Byte,字节,B)=8bit

1KB( Kilobyte,千字节,KB)=1024B

1MB( Megabyte,兆字节,MB)=1024KB

1GB( Gigabyte,吉字节,GB)=1024MB

换算:

1kB = 1024 字节
1MB = 1024 * 1024 = 1048576 字节
1GB = 1024 * 1024 * 1024 = 1073741824 字节

存放的内容:

bit(位 或 比特)
一个二进制位只可以表示0和1两种状态(2的1次方);

Byte(字节)
习惯上用大写的“B”表示。
字节是计算机中数据处理的基本单位。
一个字节由八个二进制位构成,即1个字节等于8个比特 (1Byte=8bit) 。
八位二进制数最小为00000000,最大为11111111;
通常1个字节可以存入一个ASCII码,2个字节可以存放一个汉字国标码。
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【python】socket-传输多个文件、大文件 的相关文章

随机推荐