Python网络编程之粘包

一 什么是粘包

在了解什么是粘包前,我们需要先简单回顾一下TCP和UDP,如下:

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

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

知道了TCP和UDP的原理,我们就容易的知道:TCP才会存在粘包现象,UDP永远不会粘包。

那什么是粘包呢?下面简单介绍一下:

基于TCP的特性,它没有数据包的概念,是完全流式的,它会开辟一个缓冲区,发送端往其中写入数据,每过一段时间就发送出去,然后接收端接收到这些数据,但是并不是说我发送了一次数据就肯定发送出去了,数据会在缓冲区中,有可能后续发送的数据和之前发送的数据同时存在缓冲区中随后一起发送,这就是粘包的一种形式;

接收端也有产生粘包的情况,如果应用程序没有及时处理缓冲区中的数据 ,那么后续到达的数据会继续存放到缓冲区中,也就是2次接收的数据同时存在缓冲区中,下次取缓冲区的时候就会取出2次粘包后的数据,这是粘包的另外一种形式;

还有其他许多形式,比如:填充缓冲区到一半缓冲区满了直接发送了,但是其实那个包还没填充完全,这个就是不完整的粘包了,剩余数据会在下次发送的时候补上。

粘包主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据造成的。

二 模拟粘包的发生

2.1 发送端需要等缓冲区满才发送数据,造成粘包(因为优化方法(Nagle算法)将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包发送,产生粘包)

from socket import *
ip_port = ('127.0.0.1',8080)

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(5)

conn,addr = tcp_server.accept()

recv_data1 = conn.recv(10)
recv_data2 = conn.recv(10)

print('-->',recv_data1.decode('utf-8'))
print('-->',recv_data2.decode('utf-8'))

conn.close()
服务端
import socket
buf_size = 1024
ip_port = ('127.0.0.1',8080)

tcp_client = socket.socket()
tcp_client.connect(ip_port)

tcp_client.send('hello'.encode('utf-8'))
tcp_client.send('world!'.encode('utf-8'))
客户端

执行结果:

image

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

from socket import *
ip_port = ('127.0.0.1',8080)

tcp_server = socket(AF_INET,SOCK_STREAM)
tcp_server.bind(ip_port)
tcp_server.listen(5)

conn,addr = tcp_server.accept()

recv_data1 = conn.recv(2)  # 一次没有收完整
recv_data2 = conn.recv(10)  # 下次收的时候会先取旧的数据,再取新的数据

print('-->',recv_data1.decode('utf-8'))
print('-->',recv_data2.decode('utf-8'))

conn.close()
服务端
import socket
buf_size = 1024
ip_port = ('127.0.0.1',8080)

tcp_client = socket.socket()
tcp_client.connect(ip_port)

tcp_client.send('hello world!'.encode('utf-8'))
客户端

执行结果:

image

三 粘包的处理

粘包问题的根源在于接收端不知道发送端将要传送的字节流的长度,解决粘包的方法就是围绕以怎样的方式让接收端知道发送数据的大小,从而完成数据的正确接收。现在列举出以下几种方式:

3.1 低端处理方式

在发送端将要发送数据之前,先将数据大小发送给接收端,接收端收到大小后,再发送一条消息给发送端,然后发送端再发送数据。因为程序的运行速度远快于网络传送速度,所以在发送一段字节前,先send该字节长度,这种方式会放大网络延迟带来的性能损耗,不建议使用此方式。

#! /usr/bin/python
# _*_ coding:utf-8 _*_

import socket,subprocess
ip_port = ("127.0.0.1",8080)
buf_size = 1024

tcp_server_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
tcp_server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)    # 加入一条socket配置,重用ip和端口
tcp_server_socket.bind(ip_port)
tcp_server_socket.listen(5)

while True:
    conn,addr = tcp_server_socket.accept()
    while True:
        try:
            cmd = conn.recv(buf_size)
            if len(cmd) == 0:break
            print(cmd.decode("utf-8"))
            res = subprocess.Popen(cmd.decode("utf-8"),
                                   shell=True,
                                   stderr=subprocess.PIPE,
                                   stdout=subprocess.PIPE)
            out_res = res.stdout.read()
            err_res = res.stderr.read()
            data_size = len(out_res) + len(err_res)
            # 发送数据长度
            conn.send(str(data_size).encode("utf-8"))
            if conn.recv(buf_size).decode("utf-8") == "ready":
                # 发送数据部分
                conn.send(out_res)
                conn.send(err_res)
        except Exception:
            break
    conn.close()
tcp_server_socket.close()
服务端
#! /usr/bin/python
# _*_ coding:utf-8 _*_
__author__ = "Joe"

import socket
ip_port = ("127.0.0.1",8080)
buf_size = 20

tcp_client_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
tcp_client_socket.connect(ip_port)

while True:
    cmd = input(">>:").strip()
    if len(cmd) == 0:
        continue
    if cmd == "q" or cmd == "q":
        break
    tcp_client_socket.send(cmd.encode("utf-8"))
    # 收数据长度
    length = int(tcp_client_socket.recv(buf_size).decode("utf-8"))
    # 发送收到数据长度,已进入收数据数据准备阶段
    tcp_client_socket.send("ready".encode("utf-8"))
    # 收数据
    recv_size = 0
    recv_data = b""
    while recv_size < length:
        data = tcp_client_socket.recv(buf_size)
        recv_size += len(data)
        recv_data += data
    print(recv_data.decode("gbk"))   # win为“gbk”
    print(len(recv_data))
tcp_client_socket.close()
客户端

3.2 借助struct模块简单处理

为真实数据加上自定义固定长度报头,报头中包含数据长度,然后一次send到接收端,接收端在接收时,先从缓存中取出定长的报头,然后再取真实数据。

struct模块简单了解

该模块可以把一个类型,比如数字,转换成固定长度的bytes。基本的pack和unpack:

import struct

s_p = struct.pack("i",11111111)   # 将数字11111111装换成固定长度
s_u = struct.unpack("i",s_p)      # 解包

print(s_p)    # 结果:b'\xc7\x8a\xa9\x00'
print(s_u)    # 结果:(11111111,),需取我们想要的11111111,s_u[0]即可
struct简单示例
import socket,subprocess,struct
ip_port = ("127.0.0.1",8080)
buf_size = 1024

tcp_server_socket = socket.socket()
tcp_server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
tcp_server_socket.bind(ip_port)
tcp_server_socket.listen(5)

while True:
    conn,addr = tcp_server_socket.accept()
    while True:
        try:
            cmd = conn.recv(buf_size)
            if len(cmd) == 0:break
            print(cmd.decode("utf-8"))
            res = subprocess.Popen(cmd.decode("utf-8"),
                                   shell=True,
                                   stderr=subprocess.PIPE,
                                   stdout=subprocess.PIPE)
            out_res = res.stdout.read()
            err_res = res.stderr.read()
            data_size = len(out_res) + len(err_res)
            # 发送数据长度
            length = struct.pack("i",data_size)   # 利用struct模块将数据长度转换为固定位的bytes
            conn.send(length)
            conn.send(out_res)
            conn.send(err_res)
        except Exception:
            break
    conn.close()
tcp_server_socket.close()
服务端
import socket,struct
ip_port = ("127.0.0.1",8080)
buf_size = 20

tcp_client_socket = socket.socket()
tcp_client_socket.connect(ip_port)

while True:
    cmd = input(">>:").strip()
    if len(cmd) == 0:
        continue
    if cmd == "q" or cmd == "q":
        break
    tcp_client_socket.send(cmd.encode("utf-8"))
    # 收报头,获取数据长度
    length = struct.unpack("i",tcp_client_socket.recv(4))[0]
    # 收数据
    recv_size = 0
    recv_data = b""
    while recv_size < length:
        data = tcp_client_socket.recv(buf_size)
        recv_size += len(data)
        recv_data += data
    print(recv_data.decode("gbk"))
tcp_client_socket.close()
客户端

3.3 借助struct模块json序列化处理

发送准备:将报头做成字典,字典里包含将要发送的真实数据的详细信息(如数据大小)----->将字典json序列化----->编码----->用struck将编码序列化后的数据长度打包成4个字节

发送时:先发报头长度----->再发送编码后报头内容----->最后发真实数据

接收时:先收报头长度,用struct取出来----->根据取出的长度收取报头内容,然后解码,反序列化----->从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容

import socket,subprocess,struct,json
ip_port = ("127.0.0.1",8080)
buf_size = 1024

tcp_server_socket = socket.socket()
tcp_server_socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
tcp_server_socket.bind(ip_port)
tcp_server_socket.listen(5)

while True:
    conn,addr = tcp_server_socket.accept()
    while True:
        try:
            cmd = conn.recv(buf_size)
            if len(cmd) == 0:break
            print(cmd.decode("utf-8"))
            res = subprocess.Popen(cmd.decode("utf-8"),
                                   shell=True,
                                   stderr=subprocess.PIPE,
                                   stdout=subprocess.PIPE)
            out_res = res.stdout.read()
            err_res = res.stderr.read()
            data_size = len(out_res) + len(err_res)

            head_dict ={"length":data_size,"others":None}         # 报头字典
            head_json = json.dumps(head_dict)                     # 报头序列化
            head_json_bytes = head_json.encode("utf-8")           # 报头编码
            head_struct = struct.pack("i",len(head_json_bytes))   # 利用struct模块将数据长度转换为固定位的bytes
            # 发送报头长度
            conn.send(head_struct)
            # 发送已编码报头数据
            conn.send(head_json_bytes)
            # 发送真实数据
            conn.send(out_res)
            conn.send(err_res)
        except Exception:
            break
    conn.close()
tcp_server_socket.close()
服务端
import socket,struct,json
ip_port = ("127.0.0.1",8080)
buf_size = 20

tcp_client_socket = socket.socket()
tcp_client_socket.connect(ip_port)

while True:
    cmd = input(">>:").strip()
    if len(cmd) == 0:
        continue
    if cmd == "q" or cmd == "q":
        break
    tcp_client_socket.send(cmd.encode("utf-8"))

    # 收报头长度,获取数据长度
    head_length = struct.unpack("i",tcp_client_socket.recv(4))[0]
    # 收报头数据
    head_json = tcp_client_socket.recv(head_length)
    # 反序列化,解码,获取数据长度
    head_dict = json.loads(head_json.decode("utf-8"))
    length = head_dict["length"]

    # 收数据
    recv_size = 0
    recv_data = b""
    while recv_size < length:
        data = tcp_client_socket.recv(buf_size)
        recv_size += len(data)
        recv_data += data
    print(recv_data.decode("gbk"))
tcp_client_socket.close()
客户端

补充--为什么分包?

TCP是是以段(Segment)为单位发送数据的,建立TCP链接后,有一个最大消息长度(MSS)。如果应用层数据包超过MSS,就会把应用层数据包拆分,分成两个段来发送。这个时候接收端的应用层就要拼接这两个TCP包,才能正确处理数据。例如,网卡有一个MTU( 最大传输单元),当应用层数据超过它时,TCP会分多个数据包来发送,造成分包。

 

参考:http://www.cnblogs.com/linhaifeng/articles/6129246.html#_label1

posted @ 2018-08-02 22:37  Joe1991  阅读(168)  评论(0)    收藏  举报