粘包问题

【一】什么是粘包

  • 只有TCP有粘包现象,UDP永远不会粘包

【1】TCP

  • TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
    • 收发两端(客户端和服务器端)都要有一一成对的socket
    • 因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
    • 这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

【2】UDP

  • UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。
    • 不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息)
    • 这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

【3】小结

  • tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头
  • udp的recvfrom是阻塞的
    • 一个recvfrom(x)必须对唯一一个sendinto(y),收完了x个字节的数据就算完成,若是y>x数据就丢失,这意味着udp根本不会粘包,但是会丢数据,不可靠
  • tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。
  • 两种情况下会发生粘包。
    • 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

【二】什么是粘包问题

【1】粘包问题

  • 在 TCP 协议中是流式协议,数据是源源不断的传入到客户端中,但是客户端可以接受到的信息的长度是有限的
  • 当接收到指定长度的信息后,客户端进行打印输出
    • 剩余的其他数据会被缓存到 内存中
  • 当再次执行其他命令时
    • 新的数据的反馈结果,会叠加到上一次没有完全打印完全的信息的后面,造成数据的错乱
  • 当客户端想打印新的命令的数据时,打印的其实是上一次没有打印完的数据
    • 对数据造成的错乱

【2】解决思路

#【1】数据量接受不完整是发生在接收端(因为接收端不知道总的数据量的大小,所以在发送端进行处理)
# (1)获取到原始数据的大小(数据长度)
# (2)定义一个字典(里面存储相应的参数,必须是当前数量的总大小 + 数据哈希值 + 文件的描述信息..)
# (3)在python中,字典转为字符串后再转回字典数据格式就变了
# 借助json模块将字典数据转为json字符串类型
# (4)将json字符串类型转为二进制数据,json二进制数据可能也会非常大(获取到json二进制数据的总长度,借助struct模块使用i模式对长度进行压缩)
# (5)得到压缩后的二进制数据,只有四个字节
# (6)发送四个字节的二进制数据,通过struct模块得到打包后的数据
# (7)json二进制数据打包后的数据就是json二进制数据的长度
# (8)发送原始数据,json数据里面包含了总的数据长度

#【2】接收端要处理发送端发送的数据
# (1)接收四个字节的二进制数据,通过struct模块得到打包后的数据
# (2)对struct模块打包后的数据进行解包,得到json二进制数据的长度
# (3)接收json二进制数据的长度再转成json二进制数据
# (4)json二进制数据再转成json字符串数据
# (5)json字符串数据再转成Python的字典
# (6)Python的字典获取到总的数据长度,包括描述性信息 ...
# (7)可以根据每次接收到的数据的大小,结合总的数据长度进行不断的接收,直到所有数据接收完成
# (8)最后得到处理完整的二进制数据

【三】TCP协议粘包问题演示

  • UDP协议不存在粘包问题就不进行演示了

  • 服务端

import socket

# 【1】服务器端先初始化socket对象
# AF_INET:当前连接是基于网络的套接字
# SOCK_STREAM:连接模式是TCP协议的流式模式
server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ip = '127.0.0.1'
port = 8080
ADDR = (ip, port)
# 【2】然后与本地端口和ip绑定(bind)
server_socket.bind(ADDR)
# 【3】监听连接对象
server_socket.listen(5)

# 【4】连接客户端
conn, addr = server_socket.accept()
while True:
    try:
        # 【5】服务端接收并读取客户端数据
        from_client_data = conn.recv(1024)  # 1024个字节
        if not from_client_data:
            break
        print(f"这是来自客户端的消息:{from_client_data.decode()}")  # 解码二进制数据
        # 【6】服务端返回给客户端数据
        while True:
            # #只能发送二进制数据
            to_client_data = input("请输入发送给客户端的消息:").strip()
            if not to_client_data:
                print("发送的消息不能为空!")
                continue
            if to_client_data == 'q':
                print("该连接已断开!")
                break
            conn.send(to_client_data.encode())  # 编码二进制数据
            break
    except Exception as e:
        break
# 【7】最后关闭连接(close)
conn.close()
server_socket.close()

# 可以看到第一次没有接收完的数据,在返回给客户端时,又把上一次没有接收完的数据接收到了。导致了数据混乱
'''
这是来自客户端的消息:
Windows IP 配置


以太网适配器 以太网:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

无线局域网适配器 本地连接* 9:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

无线局域网适配器 本地连接* 10:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

无线局域网适配器 本地连接* 11:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::1101:1a8a:a264:eaf5%9
   IPv4 地址 . . . . . . . . . . . . : 192.168.137.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 

以太网适配器 VMware Network Adapter VMnet1:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::8c84:5a1c:a223:630a%19
   IPv4 地址 . . . . . . . . . . . . : 192.168.237.
   
请输入发送给客户端的消息:11  
这是来自客户端的消息:1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 

以太网适配器 VMware Network Adapter VMnet8:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::70b8:24ff:7cdc:f31%4
   IPv4 地址 . . . . . . . . . . . . : 192.168.107.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 

无线局域网适配器 WLAN:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::d3c5:d4d4:410b:64a0%21
   IPv4 地址 . . . . . . . . . . . . : 192.168.0.115
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 192.168.0.1

以太网适配器 蓝牙网络连接:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

请输入发送给客户端的消息:
'''
  • 客户端
import socket
import subprocess


def run(command):
    result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='gbk',
                            timeout=1)
    # command:子进程要执行的命令
    # shell=True:执行的是shell的命令
    # stdout=subprocess.PIPE:存放的是执行命令成功的结果
    # stderr=subprocess.PIPE;存放的是执行命令失败的结果
    # returncode属性是run()函数返回结果的状态
    if result.returncode == 0:
        return result.stdout
    else:
        return result.stderr


# 【1】客户端初始化一个Socket
# AF_INET:当前连接是基于网络的套接字
# SOCK_STREAM:连接模式是TCP协议的流式模式
client_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
ip = '127.0.0.1'
port = 8080
ADDR = (ip, port)
# 【2】连接服务器
client_socket.connect(ADDR)
while True:
    # 【3】客户端直接向服务器端发送数据
    command = input('请输入执行的命令:').strip()
    if not command:
        print("发送的数据不能为空!")
        continue
    if command == 'q':
        print("该连接已断开!")
        break
    to_server_data = run(command=command)
    client_socket.send(to_server_data.encode())

    # 【4】客户端接收并读取服务端的数据
    from_server_data = client_socket.recv(1024)
    if from_server_data == 'q':
        break
    print(f"这是来自服务端的消息:{from_server_data.decode()}")

# 【5】最后关闭连接(close)
client_socket.close()

【四】粘包问题的解决办法

  • 服务端
import socket
import json
import struct

# 【1】服务器端先初始化socket对象
# AF_INET:当前连接是基于网络的套接字
# SOCK_STREAM:连接模式是TCP协议的流式模式
server_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ip = '127.0.0.1'
port = 8080
ADDR = (ip, port)
# 【2】然后与本地端口和ip绑定(bind)
server_socket.bind(ADDR)
# 【3】监听连接对象
server_socket.listen(5)

# 【4】连接客户端
conn, addr = server_socket.accept()
while True:
    try:
        # 【5】服务端接收并读取客户端数据
        # 接收到struct打包的四个字节数据
        json_pack_data = conn.recv(4)  # 1024个字节
        if not json_pack_data:
            break
        json_bytes_length = struct.unpack('i', json_pack_data)[0]
        # 根据数据长度转为json二进制数据
        json_bytes = conn.recv(json_bytes_length)
        # json二进制数据转为json字符串数据
        json_str = json_bytes.decode()
        # json字符串数据转为字典数据
        data_info = json.loads(json_str)
        # 从字典中获取总的数据长度
        data_length = data_info.get('result_length')
        # 定义参数
        # 总数据
        all_data = b''
        # 每次接收的数据大小
        size = 1024
        count, last_size = divmod(data_length, size)
        # 已经接收的数据大小
        all_size = 0
        while all_size < count+1:
            all_size += 1
            if all_size == count+1:
                all_data += conn.recv(last_size)
            else:
                all_data += conn.recv(size)
        print(f"这是来自客户端的消息:{all_data.decode()}")
        # 【6】服务端返回给客户端数据
        while True:
            # #只能发送二进制数据
            to_client_data = input("请输入发送给客户端的消息:").strip()
            if not to_client_data:
                print("发送的消息不能为空!")
                continue
            if to_client_data == 'q':
                print("该连接已断开!")
                break
            conn.send(to_client_data.encode())  # 编码二进制数据
            break
    except Exception as e:
        break
# 【7】最后关闭连接(close)
conn.close()
server_socket.close()

# 可以看到现在数据是一次性全部接收到了
'''
这是来自客户端的消息:
Windows IP 配置


以太网适配器 以太网:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

无线局域网适配器 本地连接* 9:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

无线局域网适配器 本地连接* 10:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

无线局域网适配器 本地连接* 11:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::1101:1a8a:a264:eaf5%9
   IPv4 地址 . . . . . . . . . . . . : 192.168.137.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 

以太网适配器 VMware Network Adapter VMnet1:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::8c84:5a1c:a223:630a%19
   IPv4 地址 . . . . . . . . . . . . : 192.168.237.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 

以太网适配器 VMware Network Adapter VMnet8:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::70b8:24ff:7cdc:f31%4
   IPv4 地址 . . . . . . . . . . . . : 192.168.107.1
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 

无线局域网适配器 WLAN:

   连接特定的 DNS 后缀 . . . . . . . : 
   本地链接 IPv6 地址. . . . . . . . : fe80::d3c5:d4d4:410b:64a0%21
   IPv4 地址 . . . . . . . . . . . . : 192.168.0.115
   子网掩码  . . . . . . . . . . . . : 255.255.255.0
   默认网关. . . . . . . . . . . . . : 192.168.0.1

以太网适配器 蓝牙网络连接:

   媒体状态  . . . . . . . . . . . . : 媒体已断开连接
   连接特定的 DNS 后缀 . . . . . . . : 

请输入发送给客户端的消息:good
'''
  • 客户端
import hashlib
import json
import socket
import struct
import subprocess
import uuid


def run(command):
    result = subprocess.run(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding='gbk',
                            timeout=1)
    # command:子进程要执行的命令
    # shell=True:执行的是shell的命令
    # stdout=subprocess.PIPE:存放的是执行命令成功的结果
    # stderr=subprocess.PIPE;存放的是执行命令失败的结果
    # returncode属性是run()函数返回结果的状态
    if result.returncode == 0:
        return result.stdout
    else:
        return result.stderr


def encrypted_data(data, salt):
    data = str(data) + str(salt)
    data = data.encode()
    md5 = hashlib.md5(data)
    md5.update(data)
    return md5.hexdigest()


# 【1】客户端初始化一个Socket对象
# AF_INET:当前连接是基于网络的套接字
# SOCK_STREAM:连接模式是TCP协议的流式模式
client_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
ip = '127.0.0.1'
port = 8080
ADDR = (ip, port)
# 【2】连接服务器
client_socket.connect(ADDR)
while True:
    # 【3】客户端直接向服务器端发送数据
    command = input('请输入执行的命令:').strip()
    if not command:
        print("发送的数据不能为空!")
        continue
    if command == 'q':
        print("该连接已断开!")
        break
    # 【1】执行本地的命令,获取到当前命令结果
    result = run(command=command)
    # 【2】对命令结果编码成二进制数据
    result_bytes = result.encode()
    # 【3】计算长度
    result_length = len(result_bytes)
    # 【4】增加一个数据概览
    salt = uuid.uuid4().hex
    encrypted_data = encrypted_data(data=result_bytes,salt=salt)
    send_data_info = {
        'command': command,
        'result_length': result_length,
        'salt': salt,
        'encrypted_data': encrypted_data
    }
    # 【5】将打包好的数据送给服务端
    # 利用json将字典转为字符串
    # dump : 处理文件数据
    # dumps : 做格式转换的
    json_str = json.dumps(send_data_info)
    # 将字符串转为二进制数据
    json_bytes = json_str.encode()
    # 【6】利用struct模块将二进制数据变短
    json_length_pack = struct.pack('i', len(json_bytes))
    # 【7】发送struct打包的四个字节数据+json数据+原始数据
    # 服务端接受的顺序取决于客户端发送的顺序
    # 先发送struct打包的数据
    client_socket.send(json_length_pack)
    # 再发送json打包的数据
    client_socket.send(json_bytes)
    # 最后发送原始数据
    client_socket.send(result_bytes)
    from_server_data = client_socket.recv(1024)
    if from_server_data == 'q':
        break
    print(f"这是来自服务端的消息:{from_server_data.decode()}")
# 【5】最后关闭连接(close)
client_socket.close()

# 可以看到客户端也可以接收到服务端返回的信息
'''
请输入执行的命令:ipconfig
这是来自服务端的消息:good
请输入执行的命令:
'''

【补充】struct模块

  • struct.pack()是Python内置模块struct中的一个函数

    • 它的作用是将指定的数据按照指定的格式进行打包,并将打包后的结果转换成一个字节序列(byte string)
    • 可以用于在网络上传输或者储存于文件中。
  • struct.pack(fmt, v1, v2, ...)

    • 其中,fmt为格式字符串,指定了需要打包的数据的格式,后面的v1,v2,...则是需要打包的数据。
    • 这些数据会按照fmt的格式被编码成二进制的字节串,并返回这个字节串。
  • fmt的常用格式符如下:

    • x --- 填充字节
    • c --- char类型,占1字节
    • b --- signed char类型,占1字节
    • B --- unsigned char类型,占1字节
    • h --- short类型,占2字节
    • H --- unsigned short类型,占2字节
    • i --- int类型,占4字节
    • I --- unsigned int类型,占4字节
    • l --- long类型,占4字节(32位机器上)或者8字节(64位机器上)
    • L --- unsigned long类型,占4字节(32位机器上)或者8字节(64位机器上)
    • q --- long long类型,占8字节
    • Q --- unsigned long long类型,占8字节
    • f --- float类型,占4字节
    • d --- double类型,占8字节
    • s --- char[]类型,占指定字节个数,需要用数字指定长度
    • p --- char[]类型,跟s一样,但通常用来表示字符串
    • ? --- bool类型,占1字节
    import struct
    
    # 定义一个包含不同类型字段的格式字符串
    format_string = 'i'
    
    # 示例数据:整数、四个字节的原始数据、短整数
    data_to_pack = '十七dasdadsad asd 撒大撒多所adsaddasdadsa da dsa asad撒大大带我去大青蛙大大大大大萨达去问问恰饭恰饭放散阀昂发昂发沙发阿发发发放上千万请发送方三房启发法阿发发发ad sada dsa dsa dsa sa dsa dsa as ad sad ad ada顿撒大大三大撒打我前端'
    data_to_pack_bytes = data_to_pack.encode()
    
    data_to_pack_len = len(data_to_pack_bytes)
    print(data_to_pack_len)
    # 使用 struct.pack 将数据打包成二进制字节串
    packed_data = struct.pack(format_string, data_to_pack_len)
    
    # 41000000
    # 64000000
    # 19010000
    print("Packed data:", len(packed_data))  # 打印打包后的十六进制表示
    
    # 解析二进制字节串,恢复原始数据
    unpacked_data = struct.unpack(format_string, packed_data)
    #
    print("Unpacked data:", unpacked_data)  # 打印解析后的数据
    
posted @ 2024-05-28 16:30  Ligo6  阅读(110)  评论(0)    收藏  举报