python基础学习-TCP的粘包问题

摘要:
tcp客户端以数据流的形式与服务器通信。服务器将数据发送到服务器的缓存。1.数据包粘连问题。当服务器向客户端返回非常大的数据时,客户端将只接受服务器数据流的前1024个字节。通常,TCP会根据优化算法将这些数据合成为一个TCP段,并一次性发送出去。因此,接收端的skbuff(套接字缓冲区)使用链结构来记录到达的每个UDP数据包。TCP基于数据流。
一、什么是粘包(粘包问题的原因)

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

只有TCP有粘包现象,UDP永远不会粘包,tcp的客户端与服务端进行通信的时候是以一种数据流的形式,

首先需要掌握一个socket收发消息的原理:

python基础学习-TCP的粘包问题第1张

服务端把要发送的数据送到服务端的缓存,通过网卡把数据从服务端的缓存到客户端的缓存,反过来同理。

而且这种数据流的交互是在两个不同的单向链接中进行,本身互不影响。

1.粘包问题

当我们服务端要给客户端回一个非常大的数据,但是客户端的最大接受量只设置了1024,这时候客户端只会接受服务端数据流的前1024个字节。其余的都在客户端的缓存里躺着。

这时候即使是修改最大接受量也行不通,因为数据流在数据传输完之前,我们不知道整个数据流有多大,而且,如果数据流的大小超出了我们缓存大小,我们即使把最大接收量改成无穷大依旧不行,因为当数据流把缓存占满了,其他数据流还在传输的时候,客户端就已经在读取数据了。

此外,发送方引起的粘包是由TCP协议本身造成的,TCP为提高传输效率,发送方往往要收集到足够多的数据后才发送一个TCP段。若连续几次需要send的数据都很少,通常TCP会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

  1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
  2. 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.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

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

二、解决粘包问题的办法(low)

粘包问题的根源在于,接受数据一方不知道这个数据的大小,所以我们可以让发送方在发送数据之前就把数据的大小先发送给接收方,以便接收方通过一个循环去完整的接受所有数据流

服务端.py

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket,subprocess
ip_port=('127.0.0.1',8080)
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)

s.bind(ip_port)
s.listen(5)

while True:
    conn,addr=s.accept()
    print('客户端',addr)
    while True:
        msg=conn.recv(1024)
        if not msg:break
        res=subprocess.Popen(msg.decode('utf-8'),shell=True,
                            stdin=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         stdout=subprocess.PIPE)
        err=res.stderr.read()
        if err:
            ret=err
        else:
            ret=res.stdout.read()
        # 获取要发送的数据长度
        data_length=len(ret)
        # 把数据长度编码成utf8格式,并发送数据长度
        conn.send(str(data_length).encode('utf-8'))
        # 获取接收方发来的准备完毕信息
        data=conn.recv(1024).decode('utf-8')
        if data == 'recv_ready':
            # 发送真实数据
            conn.sendall(ret)
    conn.close()

客户端.py

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket,time
s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    s.send(msg.encode('utf-8'))
    # 接受服务端发送的数据长度
    length=int(s.recv(1024).decode('utf-8'))
    # 向服务端发送准备好接受数据
    s.send('recv_ready'.encode('utf-8'))
    send_size=0
    recv_size=0
    data=b''
    while recv_size < length:
        # 通过循环以每次1024个字节数去接受服务端发回来的数据
        data+=s.recv(1024)
        recv_size+=len(data)


    print(data.decode('utf-8'))

以上解决办法比较low ,low的原因:

程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗

三、egon解决粘包的方法

上面一种办法虽然解决了粘包问题,但是多了一次io操作。为了避免这种多于操作,我们要知道那次io的目的是什么

为了在数据传输之前就知道数据流的大小,但是我们本身不知道数据长度的大小,所以要分开发送。

这里引入struct模块,用来把数字转换成固定长度的bytes

python基础学习-TCP的粘包问题第2张

import json,struct
#假设通过客户端上传1T:1073741824000的文件a.txt

#为避免粘包,必须自定制报头
header={'file_size':1073741824000,'file_name':'/a/b/c/d/e/a.txt','md5':'8f6fbf8347faa4924a76856701edb0f3'} #1T数据,文件路径和md5值

#为了该报头能传送,需要序列化并且转为bytes
head_bytes=bytes(json.dumps(header),encoding='utf-8') #序列化并转成bytes,用于传输

#为了让客户端知道报头的长度,用struck将报头长度这个数字转成固定长度:4个字节
head_len_bytes=struct.pack('i',len(head_bytes)) #这4个字节里只包含了一个数字,该数字是报头的长度

#客户端开始发送
conn.send(head_len_bytes) #先发报头的长度,4个bytes
conn.send(head_bytes) #再发报头的字节格式
conn.sendall(文件内容) #然后发真实内容的字节格式

#服务端开始接收
head_len_bytes=s.recv(4) #先收报头4个bytes,得到报头长度的字节格式
x=struct.unpack('i',head_len_bytes)[0] #提取报头的长度

head_bytes=s.recv(x) #按照报头长度x,收取报头的bytes格式
header=json.loads(json.dumps(header)) #提取报头

#最后根据报头的内容提取真实的数据,比如
real_data_len=s.recv(header['file_size'])
s.recv(real_data_len)

服务端.py

import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加

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

phone.listen(5)

while True:
    conn,addr=phone.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)

        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        err=res.stderr.read()
        print(err)
        if err:
            back_msg=err
        else:
            back_msg=res.stdout.read()


        conn.send(struct.pack('i',len(back_msg))) #先发back_msg的长度
        conn.sendall(back_msg) #在发真实的内容

    conn.close()

客户端.py

#_*_coding:utf-8_*_
__author__ = 'Linhaifeng'
import socket,time,struct

s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
res=s.connect_ex(('127.0.0.1',8080))

while True:
    msg=input('>>: ').strip()
    if len(msg) == 0:continue
    if msg == 'quit':break

    s.send(msg.encode('utf-8'))
    # 这是我们是先设定好的已经知道数据头长度为4,取第一个数据流的前四个字节进行解码操作就能得到数据流长度
    l=s.recv(4)
    # 获取反解出的第一个元素(元组的第一个元素为数据流长度)
    x=struct.unpack('i',l)[0]
    print(type(x),x)
    # print(struct.unpack('I',l))
    r_s=0
    data=b''
    while r_s < x:
        r_d=s.recv(1024)
        data+=r_d
        r_s+=len(r_d)

    # print(data.decode('utf-8'))
    print(data.decode('gbk')) #windows默认gbk编码

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

发送时:

先发报头长度

再编码报头内容然后发送

最后发真实内容

接收时:

先手报头长度,用struct取出来

根据取出的长度收取报头内容,然后解码,反序列化

从反序列化的结果中取出待取数据的详细信息,然后去取真实的数据内容 

服务端.py

import socket,struct,json
import subprocess
phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) #就是它,在bind前加

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

phone.listen(5)

while True:
    conn,addr=phone.accept()
    while True:
        cmd=conn.recv(1024)
        if not cmd:break
        print('cmd: %s' %cmd)

        res=subprocess.Popen(cmd.decode('utf-8'),
                             shell=True,
                             stdout=subprocess.PIPE,
                             stderr=subprocess.PIPE)
        err=res.stderr.read()
        print(err)
        if err:
            back_msg=err
        else:
            back_msg=res.stdout.read()
        # 写一个数据头,内含对数据的描述信息,比如数据流长度
        headers={'data_size':len(back_msg)}
        # 序列化数据头
        head_json=json.dumps(headers)
        # 把序列化的结果转换成二进制
        head_json_bytes=bytes(head_json,encoding='utf-8')
        # 第一次发送:把数据头序列化成二进制的长度先发送出去
        conn.send(struct.pack('i',len(head_json_bytes))) 
        # 第二次发送:把数据头发送出去
        conn.send(head_json_bytes) 
        # 第三次发送:真实数据流
        conn.sendall(back_msg) 

    conn.close()

客户端.py

from socket import *
import struct,json

ip_port=('127.0.0.1',8080)
client=socket(AF_INET,SOCK_STREAM)
client.connect(ip_port)

while True:
    cmd=input('>>: ')
    if not cmd:continue
    client.send(bytes(cmd,encoding='utf-8'))
    # 事先先定好我们的数据头长度的接收(第一次接收:数据头长度)
    head=client.recv(4)
    # 把数据头长度反解成真实长度
    head_json_len=struct.unpack('i',head)[0]
    # 接受到数据头展开并反解(第二次接收:数据头)
    head_json=json.loads(client.recv(head_json_len).decode('utf-8'))
    data_len=head_json['data_size']

    recv_size=0
    recv_data=b''
    # 第三次接受,循环接受数据流
    while recv_size < data_len:
        recv_data+=client.recv(1024)
        recv_size+=len(recv_data)

    print(recv_data.decode('utf-8'))
    #print(recv_data.decode('gbk')) #windows默认gbk编码

四、实现并发

4.1 TCP:

服务端.py

import socketserver

# 自定义的类
class MyRequestHandle(socketserver.BaseRequestHandler):
    # 必须要写的handle方法 在使用这个类的时候会直接执行这个方法
    def handle(self):
        try:
            while True:
                # self.request == conn()
                data = self.request.recv(1024)
                if len(data) == 0: break
                print('客户端send:', data.decode('utf-8'))
                self.request.send(data.upper())
        except Exception:
            self.request.close()
# 固定写法,在接受到一个客户端发来的请求时,启动一个线程去为他服务
s=socketserver.ThreadingTCPServer(('127.0.0.1',8888),MyRequestHandle)
s.serve_forever()

客户端.py

from socket import *

client = socket(AF_INET,SOCK_STREAM)

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

while True:
    msg = input(">>>:")
    if msg == 'q':break
    if len(msg) == 0:continue
    client.send(msg.encode('utf-8'))
    rec = client.recv(1024)
    print(rec)
client.close()

4.2 UDP:

服务端.py

import socketserver


class MyRequestHandle(socketserver.BaseRequestHandler):
    def handle(self):
        data = self.request[0]
        sever = self.request[1]
        addr = self.client_address
        print('客户端send:', data.decode('utf-8'))
        sever.sendto(data.upper(),addr)


s = socketserver.ThreadingUDPServer(('127.0.0.1',8888),MyRequestHandle)
s.serve_forever()

客户端.py

from socket import *

client = socket(AF_INET,SOCK_DGRAM)



while True:
    msg = input(">>>:")
    if msg == 'q':break
    if len(msg) == 0:continue
    client.sendto(msg.encode('utf-8'),('127.0.0.1',8888))
    rec = client.recvfrom(1024)
    print(rec)
client.close()

免责声明:文章转载自《python基础学习-TCP的粘包问题》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇linux操作提示:“Can't open file for writing”或“operation not permitted”的解决办法css3 flex流动自适应响应式布局实例 转下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

numpy和matplotlib读书笔记

1.numpy笔记:获取numpy.array中最后一列的数据1.y = to_nparray[:,-1] # 最后一列2.X = to_nparray[:,0:-1] # 从第一列开始到倒数第二列numpy.dtype:int转字符串1.# np.dtype: int转字符串2.res = np.char.mod('%d', res)numpy拼接字符串...

eslint 的基本配置介绍

eslint 这个代码规则,是在用webpack +vue-cli这个脚手架时候接触的,默认的规则可能不太习惯我们日常平时的代码开发,需要对这个规则稍加改造。 下面的是 eslintrc.js的基本规则(语句分号结尾,支持空格和tab的混合缩进) // https://eslint.org/docs/user-guide/configuring mod...

BUUCTF PWN [HarekazeCTF2019]baby_rop2

今天的最后一题 int __cdecl main(int argc, const char **argv, const char **envp) { char buf[28]; // [rsp+0h] [rbp-20h] BYREF int v5; // [rsp+1Ch] [rbp-4h] setvbuf(stdout, 0LL, 2,...

spider爬虫练习,爬取顶点小说网,小说内容。

------------恢复内容开始------------ 我这里练习爬虫的网站是顶点小说网,地址如下: https://www.booktxt.net/ 我这里以爬取顶点小说网里面的凡人修仙传为例子: 首先观察界面: 第一章: 第二章: 由上面可得出规律,每点一次下一章,url后面的数字就会自动加1。 爬虫主要分为3步: 第一步:构建url列表...

Layui数据表格动态加载操作按钮

效果:  方法一:绑定模版选择器 <div class="layui-card"> <div class="layui-card-body layui-row layui-col-space10"> <table lay-filter="deliveryTable"></table...

mysql的log_output参数

log_output背景 log_output参数是什么意思?表示慢日志输出到文件还是表中。 show global variables like 'log_output'; 默认参数,log_output='FILE',表示慢日志输出到了文件中。 set global log_output='TABLE'; 此时慢日志就输出到了mysql数据库的系统表...