• 黏包问题的成因与解决方案


    一、黏包成因

    tcp协议的拆包机制

    当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。 
    MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。大部分网络设备的MTU都是1500。
    如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,这样会产生很多数据包碎片,增加丢包率,降低网络速度

    面向流的通信特点和Nagle算法

    TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
    收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
    这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。 
    对于空消息:tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),也可以被发送,udp协议会帮你封装上消息头发送过去。 
    可靠黏包的tcp协议:tcp的协议数据不会丢,没有收完包,下次接收,会继续上次继续接收,己端总是在收到ack时才会清除缓冲区内容。数据是可靠的,但是会粘包。

    总结:

    黏包有两种:

    一种是因为发送数据包时,每次发送的包小,因为系统进行优化算法,就将两次的包放在一起发送,减少了资源的重复占用。多次发送会经历多次网络延迟,一起发送会减少网络延迟的次数。因此在发送小数据时会将两次数据一起发送,而客户端接收时,则会一并接收。#即出现多次send会出现黏包

    第二种是因为接收数据时,又多次接收,第一次接收的数据量小,导致数据还没接收完,就停下了,剩余的数据会缓存在内存中,然后等到下次接收时和下一波数据一起接收。

    二、黏包的解决方案

    1,问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

    #_*_coding:utf-8_*_
    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)
            conn.send(str(data_length).encode('utf-8'))
            data=conn.recv(1024).decode('utf-8')
            if data == 'recv_ready':
                conn.sendall(ret)
        conn.close()
    
    服务端
    server
    #_*_coding:utf-8_*_
    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:
            data+=s.recv(1024)
            recv_size+=len(data)
    
    
        print(data.decode('utf-8'))
    
    客户端
    client

    2.使用time模块,在每次send的时候加入一个time.sleep(0.01),这种方法可以有效地隔开两次send,断开系统的优化,此种方法虽然可以解决黏包问题,但是会造成发送数据时间长

    import socket
    sk = socket.socket()
    sk.bind(('127.0.0.1',8090))
    sk.listen()
    
    conn,addr = sk.accept()
    ret1 = conn.recv(12)
    print(ret1)
    ret2 = conn.recv(12)   #
    ret3 = conn.recv(12)   #
    print(ret2)
    print(ret3)
    conn.close()
    sk.close()
    server
    import socket
    
    sk = socket.socket()
    sk.connect(('127.0.0.1',8090))
    
    sk.send(b'hello')
    import time
    time.sleep(0.01)
    sk.send(b'egg')
    
    sk.close()
    client

    3,先读取文件的大小,然后将文件的大小发送给接收端,这样接收端就可以以文件大小来写入数据。

    import json
    import socket
    import struct
    sk =socket.socket()#创建一个socket对象
    sk.bind(('127.0.0.1',8080))#绑定本地ip地址与端口
    sk.listen()#开启监听
    buffer =1024    #设置buffer值大小
    conn,addr =sk.accept()#等待客户端连接服务端,得到地址与双共工通道
    head_len=conn.recv(4)#接收用struck将数字转长度为4的bytes
    head_len =struct.unpack('i',head_len)[0]#调用struct模块来解包,得到原来的数字(数字为报头的长度)
    json_head =conn.recv(head_len).decode('utf-8')#接收json序列化的报头进行解码
    head =json.loads(json_head)#将json序列化的报头进行反序列化
    filesize =head['filesize']#拿到head字典中键filesize所对应的值
    print(filesize)#打印filesize
    with open(r'dir\%s'%head['filename'],'wb')as f:#dir文件名,拿到文件的路径,以wb模式打开
        while filesize:#当filesize(文件内剩余内容的大小)有值时
            if filesize >=buffer:#如果filesize>= buffer值,buffer值是设定的一次接收多少字节的内容
                print(filesize)  #打印filesize大小
                content =conn.recv(buffer)#接收buffer值大小的内容
                f.write(content)#写入文件
                filesize -=buffer#原来的文件大小减去接收的内容,等于剩余文件的大小
            else:#如果文件剩余的内容大小<buffer设定的大小,就全部接收
                content =conn.recv(filesize)
                f.write(content)
                filesize =0
            print('=====>',len(content))
        print(filesize)
    print('服务器端')
    conn.close()
    sk.close()
    server
    import struct
    import os
    import json
    import socket
    sk =socket.socket
    sk.connect(('127.0.0.1',8090))
    buffer =1024
    head ={'filepath':r'D:DocumentsoCam',
           'filename':r'test.mp4',
           'filesize':None}#定义一个报头
    file_path =os.path.join(head['filepath'],head['filename'])#将文件名与文件路径加载进目录中
    filesize = os.path.getsize(file_path)#得到目录中文件的大小
    head['filesize'] =filesize#将文件大小赋值回列表中
    json_head =json.dumps(head)#将head字典序列化
    bytes_head =json_head.encode('utf-8')#将序列化之后的字典进行解码
    head_len =len(bytes_head)#计算转码之后字典的长度
    pack_len =struct.pack('i',head_len)#调用struct模块将长度转换成长度为4的bytes类型
    sk.send(pack_len)#发送pack_len
    sk.send(bytes_head)#发送bytes_head
    with open(file_path,'rb')as f:
        while filesize:
            print(filesize)
            if filesize>=buffer:
                content =f.read(buffer)
                print('====>',len(content))
                sk.send(content)
                filesize-=buffer
            else:
                content =f.read(filesize)
                sk.send(content)
                filesize=0
    sk.close()
    client

    为什么会出现黏包问题?

    首先只有在TCP协议中才会出现黏包现象

    是因为TCP协议是面向流的协议

    在发送的数据传输的过程中还有缓存机制来避免数据丢失

    因为在连续发送小数据的时候、以及接收大小不符的时候都容易出现黏包现象

    本质还是因为我们在接收数据的时候不知道发送的数据的长短

    解决黏包问题

    在传输大量数据之前先告诉数据量的大小。

    4,使用struct解决黏包 

    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()
    服务端(自定制报头)
    #_*_coding:utf-8_*_
    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'))
    
    
    
        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编码
    客户端(自定制报头)
  • 相关阅读:
    POJ 2636:Electrical Outlets
    POJ 2260:Error Correction
    POJ 2080:Calendar
    POJ 2017:Speed Limit
    POJ 1504:Adding Reversed Numbers
    POJ 1477:Box of Bricks
    POJ 1060:Modular multiplication of polynomials
    HDU 1379:DNA Sorting
    HDU 1009:FatMouse' Trade
    mysql修改初始密码
  • 原文地址:https://www.cnblogs.com/kakawith/p/8378425.html
Copyright © 2020-2023  润新知