You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Socket传输数据包序号错误排查:格式正确但接收乱序

问题:数据包格式正确但接收序号错误

我在完成作业时耗了好几个小时,还是搞不懂问题出在哪:

我的需求是实现以下步骤:

  • 从文件读取500字节数据
  • 将包序号与读取的500字节数据打包
  • 将格式化后的字节串通过Socket发送

我的代码如下:

sender.py

def sending(host, port, path_to_file, package_size):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))
    fp = open(path_to_file, 'rb')
    start_time = time.time()
    package_number = 1
    while True:
        msg = fp.read(package_size)
        if len(msg) < package_size:
            sock.send(pack(f'I{len(msg)}s', package_number, msg))
            break
        final_result = pack(f'I{package_size}s', package_number, msg)
        sock.send(final_result)
        package_number += 1
    end_time = time.time()
    print(f"Time elapsed using {package_size} bytes: {end_time - start_time}")

receiver.py

def receive(host, port, extension, package_size):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind((host, port))
    sock.listen(1)
    conn, addr = sock.accept()
    print("Connection accepted with " + str(addr))
    fp = open(f'received_file_{package_size}.{extension}', 'wb')
    start_time = time.time()
    while True:
        received_package = conn.recv(package_size)
        if len(received_package) < package_size:
            package_number, msg = unpack(f'I{len(received_package)-4}s', received_package)
            print(f"Package number {package_number} received with size {len(received_package)}")
            fp.write(msg)
            break
        package_number, msg = unpack(f'I{package_size-4}s', received_package)
        print(f"Package number {package_number} received with size {len(received_package)}")
        fp.write(msg)
        if not received_package:
            break
    fp.close()
    end_time = time.time()
    print(f"Time elapsed using {package_size} bytes: {end_time - start_time}")

但接收端收到的数据出现异常:

Package number 1 received with size 500: (正确的第一个包序号)
 b'\x01\x00\x00\x00\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x0f ...
Package number 2468486165 received with size 500: (应为第二个包)
 b'\x15\x1c&quot;\x93\x02\x00\x00\x00\xd6\x05\x1ei\x1fv\xb1\xdf\xbb\x1a

可以看到代表序号的字节向右偏移(\x02\x00\x00\x00),导致包序号错误(显示2468486165而非2)。我要传输的是7MB的PNG文件,且必须不能使用FTP库,请问哪里出错了?


问题根源

TCP是流式协议,它没有数据包边界的概念,你不能假设conn.recv(package_size)会正好接收到一个完整的打包后的数据包。

你的发送端每个完整包的结构是4字节整数序号 + 500字节数据,总大小为504字节,但接收端每次只调用recv(500),这会导致:

  1. 第一次recv只拿到前500字节(4字节序号 + 496字节数据)
  2. 剩下的4字节数据会和第二个包的序号、数据混在一起,被下一次recv读取
  3. 解析时错误地把数据字节当成了序号,自然出现序号偏移、数值错误的问题

修复方案

核心是解决TCP的粘包/拆包问题,确保接收端能完整接收每个打包单元。以下是修改后的代码:

修改后的sender.py

def sending(host, port, path_to_file, package_size):
    import socket
    import time
    from struct import pack

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.connect((host, port))
    with open(path_to_file, 'rb') as fp:
        start_time = time.time()
        package_number = 1
        while True:
            msg = fp.read(package_size)
            if not msg:
                break
            data_len = len(msg)
            # 打包格式:序号(4字节) + 数据长度(4字节) + 数据内容
            final_result = pack(f'II{data_len}s', package_number, data_len, msg)
            sock.sendall(final_result)  # 用sendall确保所有数据发送完成
            package_number += 1
        # 发送结束标记:序号0 + 数据长度0,告知接收端传输完成
        sock.sendall(pack('II', 0, 0))
    end_time = time.time()
    print(f"使用{package_size}字节包大小耗时:{end_time - start_time}")

修改后的receiver.py

def receive(host, port, extension, package_size):
    import socket
    import time
    from struct import unpack

    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind((host, port))
    sock.listen(1)
    conn, addr = sock.accept()
    print("已与" + str(addr) + "建立连接")
    with open(f'received_file_{package_size}.{extension}', 'wb') as fp:
        start_time = time.time()

        def recv_all(conn, length):
            # 确保接收指定长度的字节,处理TCP拆包情况
            data = b''
            while len(data) < length:
                chunk = conn.recv(length - len(data))
                if not chunk:
                    return None
                data += chunk
            return data

        while True:
            # 先接收8字节的头部:序号(4字节) + 数据长度(4字节)
            header = recv_all(conn, 8)
            if not header:
                break
            package_number, data_len = unpack('II', header)
            # 收到结束标记则退出循环
            if package_number == 0 and data_len == 0:
                break
            # 接收对应长度的数据内容
            msg = recv_all(conn, data_len)
            if not msg:
                break
            print(f"收到包序号{package_number},总大小{8+data_len}字节")
            fp.write(msg)
    end_time = time.time()
    print(f"使用{package_size}字节包大小耗时:{end_time - start_time}")

关键修改点

  1. 使用sendall()替代send():确保所有数据都被发送,避免因网络原因导致的部分发送。
  2. 增加数据长度字段:每个数据包包含数据长度信息,接收端可以明确知道需要接收多少字节的数据,彻底解决粘包/拆包问题。
  3. 实现recv_all()函数:确保能接收到指定长度的字节,不会因TCP拆包导致接收不完整。
  4. 增加结束标记:通过发送序号0+数据长度0的包,让接收端可以明确判断传输结束,避免依赖数据长度的模糊判断。

内容的提问来源于stack exchange,提问作者Kresnik

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.20 10:10:03