vlambda博客
学习文章列表

TCP协议粘包问题的解决

不知名菜鸟
爱分享,爱生活,爱工作。
58篇原创内容
Official Account




基于TCP协议的数据传输在运行的时候会发生粘包问题


服务端
import socket
import subprocess

HOST = '0.0.0.0'        # 服务端绑定0.0.0.0的IP  放行所有的客户端请求
PORT = 8080  # 0-65535  1024前被系统保留使用
BUFSIZE = 1024
ADDR = (HOST, PORT)

# STREAM 流式协议  TCP协议    DGRAM 数据报协议  UDP协议
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(ADDR)  # 将服务端的IP和地址绑定到套接字
server.listen(5)  # 监听链接 定义半链接池大小

print('服务端启动,监听地址:%s :%s' % (ADDR))

while True:  # 链接循环  不停的接受请求 一直提供服务
    # 等待客户端请求
    conn, addr = server.accept()  # 接受客户端链接
    while True:  # 通信循环  不停的收发消息
        # send 和recv都会发起系统调用
        try:
            data = conn.recv(BUFSIZE)
            if not data:
                break
            res = subprocess.Popen(data.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            stdout_data = res.stdout.read()
            stderr_data = res.stderr.read()

            print(len(stderr_data) + len(stdout_data), addr)

            conn.send(stdout_data)
            conn.send(stderr_data)
        except Exception:
            # 针对Windows系统
            break
    conn.close()  # (必选操作)关闭客户端套字节  回收资源的操作
客户端
import socket

HOST = '39.108.102.14'      # 绑定服务器公网IP
PORT = 8080  # 0-65535  1024前被系统保留使用
BUFSIZE = 8096
ADDR = (HOST, PORT)

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client.connect(ADDR)

while True:  # 开启通信循环  不停的收发消息
    cmd = input('cmd>>>').strip()
    if not cmd:  # 不让客户端发送空消息
        continue
    client.send(cmd.encode('utf-8'))
    res_data = client.recv(BUFSIZE)
    if not res_data:
        break
    print(res_data.decode('gbk'))   # Windows采用gbk Unix采用utf-8

client.close()  # 关闭客户端  必要的选择

基于UDP远程执行命令

在运行时永远不会发生粘包

服务端
from socket import *
import subprocess


HOST = '127.0.0.1'
PORT = 8080
BUFSIZE = 1024
ADDR = (HOST, PORT)

server = socket(AF_INET, SOCK_DGRAM)
server.bind(ADDR)

while True:
    cmd, addr = server.recvfrom(BUFSIZE)
    if not cmd:
        break
    print('命令为>>>', cmd, 'from', addr)
    res = subprocess.Popen(cmd.decode('utf-8'),
                           shell=True, stdout=subprocess.PIPE,
                           stderr=subprocess.PIPE)
    stdout_msg = res.stdout.read()
    stderr_msg = res.stderr.read()

    server.sendto(stdout_msg, addr)
    server.sendto(stderr_msg, addr)

server.close()
客户端
from socket import *

HOST = '127.0.0.1'
PORT = 8080
BUFSIZE = 1024
ADDR = (HOST, PORT)

client = socket(AF_INET, SOCK_DGRAM)

while True:
    cmd = input('>>>').strip()
    if not cmd:
        continue
    client.sendto(cmd.encode('utf-8'), ADDR)
    data, addr = client.recvfrom(BUFSIZE)
    if not data:
        break
    print(data.decode('gbk'))   # Windows

    client.close()

粘包问题的解决

由于接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。


发生粘包的情况

  • 发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据量很小,会合到一起,产生粘包)

  • 接收方不及时接收缓冲区的包,造成多个包接收(客户端发送了一段数据,服务端只收了一小部分,服务端下次再收的时候还是从缓冲区拿上次遗留的数据,产生粘包)

TCP协议粘包问题的解决


解决粘包问题思路

  • 先收固定长度的头信息并解析出数据的总大小:total_size

  • recv_size=0,循环接收,每接收一次,recv_size+=接收的长度

  • 直到recv_size=total_size,结束循环

TCP协议粘包问题的解决


struct模块
可以把一个类型,如int类型,转成固定长度的bytes

>>>import struct
>>>x = struct.pack('i'6666)
>>>x
>>>b'\n\x1a\x00\x00'
>>>len(x)
>>>4    # 长度为4字节
>>>struct.unpack('i', x)[0]
>>>6666
定义报头

先发送返回内容的长度信息,再发送真实的返回信息给客户端

服务端
import socket
import subprocess
import struct

HOST = '127.0.0.1'
PORT = 8080  # 0-65535  1024前被系统保留使用
BUFSIZE = 1024
ADDR = (HOST, PORT)

# STREAM 流式协议  TCP协议    DGRAM 数据报协议  UDP协议
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(ADDR)  # 将服务端的IP和地址绑定到套接字
server.listen(5)  # 监听链接 定义半链接池大小

print('服务端启动,监听地址:%s :%s' % (ADDR))

while True:  # 链接循环  不停的接受请求 一直提供服务
    # 等待客户端请求
    conn, addr = server.accept()  # 接受客户端链接
    while True:  # 通信循环  不停的收发消息
        # send 和recv都会发起系统调用
        try:
            data = conn.recv(BUFSIZE)
            if not data:
                break
            res = subprocess.Popen(data.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            stdout_data = res.stdout.read()
            stderr_data = res.stderr.read()

            print(len(stderr_data) + len(stdout_data), addr)
            total_size = len(stdout_data) + len(stderr_data)

            # 先发头--->是对数据的描述信息
            header = struct.pack('i', total_size)
            conn.send(header)
            # 再发真实的信息
            conn.send(stdout_data)
            conn.send(stderr_data)
        except Exception:
            # 针对Windows系统
            break
    conn.close()  # (必选操作)关闭客户端套字节  回收资源的操作
客户端
import socket
import struct

HOST = '127.0.0.1'
PORT = 8080  # 0-65535  1024前被系统保留使用
BUFSIZE = 8096
ADDR = (HOST, PORT)

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client.connect(ADDR)

while True:  # 开启通信循环  不停的收发消息
    cmd = input('cmd>>>').strip()
    if not cmd:  # 不让客户端发送空消息
        continue
    client.send(cmd.encode('utf-8'))
    # 服务端知道数据的总大小
    header = client.recv(4# 接收4字节的大小
    total_size = struct.unpack('i', header)[0]  # 取小元组的0索引就是数据的大小
    recv_size = 0
    while recv_size < total_size:
        recv_data = client.recv(BUFSIZE)
        recv_size += len(recv_data)
        print(recv_data.decode('gbk'), end='')
    else:
        print()
client.close()  # 关闭客户端  必要的选择

最终解决方案

可以把报头做成字典,字典里包含将要发送的真实数据的详细信息,然后json序列化,然后用struck将序列化后的数据长度打包成4个字节。

  • 发送时

先发报头长度
再编码报头内容然后发送
最后发真实内容

  • 接收时

先接收报头长度,用struct模块的unpack方法取出来
根据取出的长度收取报头内容,然后解码,反序列化
从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容。

服务端
import socket
import subprocess
import struct
import json

HOST = '127.0.0.1'
PORT = 8080  # 0-65535  1024前被系统保留使用
BUFSIZE = 1024
ADDR = (HOST, PORT)

# STREAM 流式协议  TCP协议    DGRAM 数据报协议  UDP协议
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(ADDR)  # 将服务端的IP和地址绑定到套接字
server.listen(5)  # 监听链接 定义半链接池大小

print('服务端启动,监听地址:%s :%s' % (ADDR))

while True:  # 链接循环  不停的接受请求 一直提供服务
    # 等待客户端请求
    conn, addr = server.accept()  # 接受客户端链接
    while True:  # 通信循环  不停的收发消息
        # send 和recv都会发起系统调用
        try:
            data = conn.recv(BUFSIZE)
            if not data:
                break
            res = subprocess.Popen(data.decode('utf-8'), shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            stdout_data = res.stdout.read()
            stderr_data = res.stderr.read()
            total_size = len(stdout_data) + len(stderr_data)

            print(len(stderr_data) + len(stdout_data), addr)

            # 制作报头字典
            header_dic = {
                "total_size": total_size
            }

            json_header_str = json.dumps(header_dic)
            json_header_str_bytes = json_header_str.encode('utf-8')

            # 先发头--->是对数据的描述信息
            # 发送头的长度信息
            conn.send(struct.pack('i', len(json_header_str_bytes)))
            # 发头信息
            conn.send(json_header_str_bytes)

            # 再发真实的信息
            conn.send(stdout_data)
            conn.send(stderr_data)

        except Exception:
            # 针对Windows系统
            break
    conn.close()  # (必选操作)关闭客户端套字节  回收资源的操作
客户端
import socket
import struct
import json

HOST = '127.0.0.1'
PORT = 8080  # 0-65535  1024前被系统保留使用
BUFSIZE = 8096
ADDR = (HOST, PORT)

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

client.connect(ADDR)

while True:  # 开启通信循环  不停的收发消息
    cmd = input('cmd>>>').strip()
    if not cmd:  # 不让客户端发送空消息
        continue
    client.send(cmd.encode('utf-8'))

    # 接收端
    # 先接收4字节,提取头信息的长度
    header_len = struct.unpack('i', client.recv(4))[0]

    # 接收头信息并解析
    json_header_str_bytes = client.recv(header_len)
    json_header_str = json_header_str_bytes.decode('utf-8')
    header_dic = json.loads(json_header_str)
    print('报头信息:', header_dic)
    total_size = header_dic['total_size']

    recv_size = 0
    while recv_size < total_size:
        recv_data = client.recv(BUFSIZE)
        recv_size += len(recv_data)
        print(recv_data.decode('gbk'), end='')
    else:
        print()
client.close()  # 关闭客户端  必要的选择



***************************************************************