粘包问题

粘包问题

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

一、什么是粘包问题

  • 什么时候会发生粘包问题?
    • 当TCP传输和接收的数据并非我们规定的最大数据量时,就会发生粘包
    • 我们日常传输的数据几乎不可能等于我们规定的数据量,所以我们必须要解决这个问题
  • 为什么只有TCP会发生粘包问题?
    • TCP是面向流的协议,就是TCP为了提高传输效率发送数据时往往都是收集到足够多的数据后才发送一个段
    • 大白话讲就是TCP会收集一定的数据然后再发送,但是接收方并没有办法知道发来的数据要按怎么样的方式切分,而无法读取数据
    • UDP则不会使用合并算法,且接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息)
    • 这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

[小结]什么是粘包问题

  • 客户端发送需要执行的代码
  • 服务端接收到客户端传过来的代码
    • 服务端调用方法执行代码并拿到执行后的结果
    • 服务端将执行后的结果进行返回
  • 客户端接收到服务端返回的结果并做打印输出

二、粘包问题的演示

  • 我们来使用客户端发送一段cmd命令给服务端,在服务端运行并返回数据给客户端

  • 客户端接收数据并打印

  • 由于返回的数据长度大于1024所以无法全部显示

  • 服务端

import socket
import subprocess

# 创建服务对象
server = socket.socket(type=socket.SOCK_STREAM)

# 设定默认的IP和端口号
IP = '127.0.0.1'
PORT = 8081

server.bind((IP, PORT))

server.listen(5)

while True:
    conn, addr = server.accept()
    while True:
        # 检测可能出现的异常并进行处理
        try:
        # 取到 客户端发来的cmd命令
            cmd_from_client = conn.recv(1024)

            # 不允许传入的命令为空
            if not cmd_from_client:
                break

            # 对接收的命令进行解码
            cmd_from_client = cmd_from_client.decode('gbk')
            # 执行客户端传过来的cmd命令并获取结果
            msg_server = subprocess.Popen(cmd_from_client,
                                          shell=True,  # 执行shell命令
                                          stdout=subprocess.PIPE,  # 管道一
                                          stderr=subprocess.PIPE,  # 管道二
                                          )
            true_res = msg_server.stdout.read()  # 读取正确结果
            false_res = msg_server.stderr.read()  # 读取错误结果

            conn.send(true_res)
            conn.send(false_res)
        except ConnectionResetError as e:
            break
    conn.close()
  • 客户端
import socket

while True:
    client = socket.socket(type=socket.SOCK_STREAM)

    # 设定默认的IP和端口号
    IP = '127.0.0.1'
    PORT = 8081

    client.connect((IP, PORT))
    cmd_send_to_server = input('请输入想要实现的cmd命令:').strip()

    if not cmd_send_to_server:
        break
    cmd_send_to_server = cmd_send_to_server.encode('utf-8')

    client.send(cmd_send_to_server)

    msg_from_server = client.recv(1024)

    msg_from_server = msg_from_server.decode('gbk')

    print(msg_from_server)

    client.close()

  • 客户端收到的返回的不完整的结果

  • cmd中的结果

  • 可以看出客户端接收的结果并不完全

三、解决方案

[1]解决问题的思路

  • 拿到数据的总大小 recv_total_size
  • recv_size = 0 ,循环接收,每接收一次,recv_size += 接收的长度
  • 直到 recv_size = recv_total_size 表示接受信息完毕,结束循环

[2]解决方案

(1)基础版

  • 直接使用struct模块打包数据的长度,传输到客户端,方便对数据的处理

  • 服务端

import socket
import subprocess
import struct

server = socket.socket(type=socket.SOCK_STREAM)

IP = '127.0.0.1'
PORT = 8083

server.bind((IP, PORT))

server.listen(5)

while True:
    conn, addr = server.accept()
    cmd_from_client = conn.recv(1024)
    cmd_from_client = cmd_from_client.decode('utf-8')

    msg_server = subprocess.Popen(cmd_from_client,
                                  shell=True,
                                  stdout=subprocess.PIPE,
                                  stderr=subprocess.PIPE,
                                  )
    true_msg = msg_server.stdout.read()
    false_msg = msg_server.stderr.read()

    total_len = len(true_msg) + len(false_msg)

    total_len_pack = struct.pack('i', total_len)

    conn.send(total_len_pack)

    conn.send(true_msg)
    conn.send(false_msg)
    conn.close()
    break
server.close()
  • 客户端
import socket
import struct

while True:
    client = socket.socket(type=socket.SOCK_STREAM)

    IP = '127.0.0.1'
    PORT = 8083

    client.connect((IP, PORT))

    cmd_send_to_server = input('输入你想要实现的cmd命令:')

    cmd_send_to_server = cmd_send_to_server.encode('utf8')

    client.send(cmd_send_to_server)

    cmd_from_server_pack = client.recv(4)

    cmd_from_server_unpack = struct.unpack('i', cmd_from_server_pack)

    total_len = cmd_from_server_unpack[0]

    txt = b''
    while total_len > 0:
        msg = client.recv(1024)
        txt += msg
        total_len -= 1024
    print(txt.decode('gbk'))
    break

(2)进阶版

  • 思路

    1. 将数据的名字,长度等信息放入字典中
    2. 通过json模块将字典转换成json字符串并编码成二进制数据(需要传输的数据:json字符串)
    3. 使用struct模块将json字符串的长度打包成四位二级制数据(需要传输的数据:struct包)
    4. 将原始数据(不是二进制格式的转换成二进制)进行传输(需要传输的数据:原始数据)
    5. 客户端可以通过解struct包和json字符串的二进制数据得到之后每一部分数据的长度,并通过循环取出每个部分
  • 服务端

import socket
import struct
import subprocess
import json

server = socket.socket()

server.bind(('127.0.0.1', 8080))

server.listen(5)

while True:
    conn, addr = server.accept()

    msg_from_client = conn.recv(1024)

    msg_from_client = msg_from_client.decode('utf-8')

    if msg_from_client == 'q':
        conn.send(b'q')
        break
    msg_server = subprocess.Popen(msg_from_client,
                                  shell=True,
                                  stdout=subprocess.PIPE,
                                  stderr=subprocess.PIPE,
                                  )
    true_msg = msg_server.stdout.read()
    false_msg = msg_server.stderr.read()

    total_len = len(true_msg) + len(false_msg)

    msg_dict = {
        'name': 'cmd',
        'total_len': total_len
    }

    msg_dict_json = json.dumps(msg_dict)

    msg_dict_json_b = msg_dict_json.encode('utf-8')

    struct_pack = struct.pack('i', len(msg_dict_json_b))

    conn.send(struct_pack)
    conn.send(msg_dict_json_b)
    conn.send(true_msg)
    conn.send(false_msg)

  • 客户端
import json
import socket
import struct

while True:
    client = socket.socket()

    client.connect(('127.0.0.1', 8080))

    while True:
        cmd_send_to_server = input('请输入想要实现的cmd命令:')
        if not cmd_send_to_server:
            print('命令不能是空白!')
            continue
        cmd_send_to_server = cmd_send_to_server.encode('utf-8')
        break
    client.send(cmd_send_to_server)

    struct_pack = client.recv(4)

    if struct_pack == b'q':
        break
    struct_unpack = struct.unpack('i', struct_pack)

    dict_json_b = client.recv(struct_unpack[0])

    dict_json = dict_json_b.decode('utf-8')

    msg_dict = json.loads(dict_json)

    total_len = msg_dict['total_len']

    txt = b''
    while total_len > 0:
        msg = client.recv(1024)
        txt += msg
        total_len -= 1024
    print(txt.decode('gbk'))

client.close()

四、案例

  • 一个文件夹中的将一个视频文件放入另一个文件夹中

  • 服务端

    conn, addr = server.accept()

    # conn.send(json_file_list_b)

    msg_from_client_b = conn.recv(1024)

    if msg_from_client_b == b'q':
        conn.send(b'q')
        break

    msg_from_client = msg_from_client_b.decode('utf-8')

    if msg_from_client not in file_list:
        conn.send('f'.encode('utf-8'))
        continue

    file_path = os.path.join(os.getcwd(), msg_from_client)

    with open(file_path, 'rb') as fp:
        video_data = fp.read()

    video_len = len(video_data)

    headers = {
        'file_name': msg_from_client,
        'video_len': video_len
    }

    headers_json = json.dumps(headers)

    headers_json_b = headers_json.encode('utf-8')

    struct_package = struct.pack('i', len(headers_json_b))

    conn.send(struct_package)

    conn.send(headers_json_b)

    conn.send(video_data)

  • 客户端
    print('以下是文件列表:\n')
    # 将操作的目录转到目标文件夹
    os.chdir(r'D:\Users\21277\Desktop\home')
    # 将文件夹中的所有文件名放入列表
    file_list = os.listdir(os.getcwd())
    # 将文件名打印出来
    for file_name in file_list:
        print(file_name)

    print('\n')

    client = socket.socket()

    client.connect(('127.0.0.1', 8080))

    # json_file_list_b = client.recv(1024)

    # file_list = json.loads(json_file_list_b.decode('utf-8'))
    #
    # for file_name in file_list:
    #     print(file_name)

    while True:
        msg_send_to_server = input('请输入想要传输的文件:')
        if not msg_send_to_server:
            print('不能是空白!')
            continue
        msg_send_to_server = msg_send_to_server.encode('utf-8')
        break

    client.send(msg_send_to_server)

    struct_package = client.recv(4)

    if struct_package == b'q':
        break
    if struct_package == b'f':
        print('未找到该文件!请重新输入文件名。')
        continue

    headers_json_len = struct.unpack('i', struct_package)[0]

    headers_json_b = client.recv(headers_json_len)

    headers = json.loads(headers_json_b.decode('utf-8'))

    video_len = headers['video_len']

    video_data = b''
    while video_len > 0:
        video_data += client.recv(1024)
        video_len -= 1024

    new_file_name = input('请重命名文件(不重命名则跳过):')
    if not new_file_name:
        new_file_name = msg_send_to_server.decode('utf-8')
    else:
        new_file_name = new_file_name + '.mp4'

    with open(os.path.join(r'D:\Users\21277\Desktop\new', new_file_name), 'wb') as fp:
        fp.write(video_data)

    client.close()
posted @ 2024-03-20 09:35  桃源氏  阅读(122)  评论(0)    收藏  举报