socket网络编程:粘包现象以及解决方法(代码完善)

Posted py-xiaoqiang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了socket网络编程:粘包现象以及解决方法(代码完善)相关的知识,希望对你有一定的参考价值。

粘包现象

.recv(1024)坑:当传送来的数据超过1024bytes的时候,因为recv只能一次接受1024byte,传输管道就会积压数据 → 下次recv会继续接收积压的数据 → 这回导致本次send的处理结果可能返回的是上次的结果内容的一部分,

粘包现象:TCP协议是流数据协议(传送的是一个整体的流数据),即一条消息对应多少bytes是不可见的。 这导致接收方接受数据的时候不知道怎么断句,使得数据混乱。这就是所谓的粘包现象

注意:只有TCP有粘包现象,UDP永远不会粘包

参考

粘包详解send 和 recv详解

首先我们强调一下:
应用软件不能直接操作硬件,必须借由OS来调用硬件。所以应用软件的send和recv都是针对os的数据传输(应用层)。
其他底层的网络协议的实现都是由os完全接管的。

  • 粘包总结:
    • 无论send还是recv都是针对本地os的内存进行数据交换而不是直接跟目标服务器进行数据交换

      注意:不是一次send就只能对应一次recv,可以任意次对应任意次

    • recv,send处理:
      • recv:
        • wait data:os等待数据 耗时长(需要一直等待)
        • copy data:从os内存拷贝到数据到应用软件
      • send:
        • send data:从应用软件拷贝数据到os内存
      • 粘包:
        • 接收取数据时,一个包数据过大,使数据有一部分残留在了内存里。
        • 接收取数据时,把多个包的数据当成一个包(Nagle导致的)接收了。
    • TCP流传输使用Nagle优化算法把包合并起来发送:这是粘包的一个原因

      Nagle算法:把数据量小并且时间间隔比较短的包合成一个包发送

  • 解决方法:
    • 明确知道包的bytes数的话,就可以避免粘包了 → 事先告诉接收端包的信息就可以了
解决粘包问题

我们用ssh模拟的程序说明:

我么虽然可以recv的参数设定的很大,但是这也是固定值,最大值也不会超过内存的空间,所以也不是个很好的方案

  • 那么我们分析下怎么做:
    • 发送信息的是对方,所以发送数据的时候
      • 把数据长度发送给对方
      • 再把数据发送给对方

server.py

import socket
import subprocess
import struct
import locale

ssh_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ssh_server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
ssh_server.bind(('127.0.0.1', 8080))
ssh_server.listen(3)

while True:
    conn, addr = ssh_server.accept()
    while True:
        try:
            cmd = conn.recv(1024).decode('utf-8')
            if not cmd: break
            print("excute cmd:", cmd)
            obj = subprocess.Popen(cmd, shell=True,
                                   stdout=subprocess.PIPE,
                                   stderr=subprocess.PIPE)
            stdout = obj.stdout.read()
            stderr = obj.stderr.read()

            os_locale = locale.getdefaultlocale()
            os_encode = os_locale[1]
            print(stdout.decode(os_encode))

            # 第一步:生成数据的报头(报头:固定长度):需要用到struct模块
            # 我们首先需要把发送数据的描述信息发送给客户端,但是客户端并不知道这个包是干什么的,而且会和数据的包粘包
            # 所以我们把这段数据作为数据的报头发送
            total_size = len(stdout) + len(stderr)
            header = struct.pack('i', total_size)  # 打包报头,打包返回的数据是一个固定长度(4bytes)的bytes类型
            # struct.pack(模式,报头数据),注意:'i'模式int范围:正负2*10^9之内(因为只有4bytes)
            # 另外有'l'模式:长整型。但是这个模式报头为8bytes。也有范围限制

            # 第二部:发送报头
            conn.send(header)  # 因为是bytes类型所以可以直接发送

            # 把命令结果返回给客户端
            # conn.send(stdout+stderr)  # 这里之前用+连接一起发送,但其实粘包也可以一起发送
            conn.send(stdout)  # 连续发送可以粘包
            conn.send(stderr)  # 与上一行粘包成一个数据包了
        except ConnectionResetError as e:
            print(e)
            break
    conn.close()

ssh_server.close()

client.py

import socket
import struct

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

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

while True:
    # 发命令
    cmd = input(">>>>:").strip()
    if not cmd:continue
    ssh_client.send(cmd.encode('utf-8'))

    # 第一步:先收报头
    header = ssh_client.recv(4)  # 因为报头固定4byte 所以我们收4

    # 第二部:从报头解析出数据的描述(数据的长度)
    total_size = struct.unpack('i', header)[0]  # 解包报头,返回一个元组类型,这里我们第一个元素是长度
    print(total_size)  # 打印一下报头内容

    # 第三部:接受真实收据
    recv_size = 0
    recv_data = b''
    while recv_size < total_size:
        res = ssh_client.recv(1024)
        recv_data += res
        recv_size += len(res)

    print(recv_data.decode('cp932'))  # 因为粘包问题解决了,这里我们直接打印

ssh_client.close()

以上是关于socket网络编程:粘包现象以及解决方法(代码完善)的主要内容,如果未能解决你的问题,请参考以下文章

网络编程基础之粘包现象

socket 通信粘包怎么处理

python网络编程基础之socket粘包现象

python----socket编程

Python之路(十七):网络编程(下)

python基础之socket编程