• TCP黏包问题


    什么是黏包?什么情况下会出现黏包的情况?该如何避免黏包的情况?

    首先来看一个例子

    #服务端
    import time
    from socket import *
    server = socket(AF_INET,SOCK_STREAM)
    server.bind(("127.0.0.1",8180))
    server.listen(5)
    conn,addr = server.accept()
    resl = conn.recv(1024)
    print(resl.decode('utf-8'))
    
    #客户端
    from socket import *
    import time
    client = socket(AF_INET,SOCK_STREAM)
    client.connect(("127.0.0.1",8180))
    client.send(b"1234")
    client.send(b'world')

    结果:

    1234world  #为什么会连在一起了

    这种简单的情况其实也好解决:

    #客户端
    from socket import *
    import time
    client = socket(AF_INET,SOCK_STREAM)
    client.connect(("127.0.0.1",8180))
    client.send(b"1234")
    client.send(b'world')
    time.sleep(3)       #用时间隔开就能不让它黏在一起
    client.send(b'world')

    执行结果:

    12345
    world

    还可以这样:

    import time
    from socket import *
    server = socket(AF_INET,SOCK_STREAM)
    server.bind(("127.0.0.1",8180))
    server.listen(5)
    conn,addr = server.accept()
    resl = conn.recv(5)    #指定的字节数就不能黏在一起了
    print(resl.decode('utf-8'))
    res2 = conn.recv(5)
    print(res2.decode('utf-8'))

    我们可以猜测的:当包的大小有限,同时连续发送的时候就可能出现黏包的情况。

    什么是黏包?

    只有TCP有黏包现象,UDP永远不会黏包。

    之前提到过,TCP是面向连接的通信方式,提供了顺序、可靠、不会重复的数据传输,而且不会加上边界。

    这个就意味着,每一个要发送的信息,可能会被拆分成多份,每一份都会不多不少地正确地到达目的地,然后被重新拼装起来,传给正在等待地应用程序。

    问题就出现了,传输地数据没有界限,当传输到另一台地机器的时候,从内存中取值的时候,取值的大小取决于recv地参数。

    如果需要传输地数据比较大,那么recv肯定无法一次取完,在内存中有残留,这样就会出现黏包地现象。

    还有就是为了提高效率,tcp协议在传输地过程中,会将数据包较小的和时间间距比较小的数据包一起发送,这样也会造成黏包。

    黏包是如何产生的?

    黏包出现的时候,表现在有可能并没有取到自己想要的包,为什么没有取到自己的包了?

    难道是内存中没有吗?不是,是因为系统按顺序给你取,而上一个又没有取完,所以就可能取到别人的包,怎么解决了?

    改变recv参数并不能从根本上解决这个问题,recv受制于当前系统可供支配的内存,如果传输的数据比内存大,这样也会产生黏包。

    刚才说了,你之所以会取别人的,是别人没有取完,只要能保证每个人都取到自己的包并且不残留就可以了。

    那为什么就不能取完了?还是TCP的性质决定了,没有边界,不知道取到那里为止,因此只要让它知道了边界的位置,问题也就解决了。

    怎样解决黏包问题?

    在传输之前,我们先给对方添加一个包头,这样它就知道取到那里了;

    #服务方
    from socket import *
    import subprocess
    import struct
    server = socket(AF_INET,SOCK_STREAM)
    server.bind(('127.0.0.1',8888))
    server.listen(5)
    
    while True:
        conn,addr = server.accept()
        print('收到来自%s的访问' % addr[0])
        while True:
            try:
                cmd = conn.recv(1024)
                cmd = cmd.decode("utf-8")   #接受的是二进制的格式,因为下面需要执行这条消息,所以必须解码
                ret = subprocess.Popen(cmd,shell=True,
                                       stdout=subprocess.PIPE,   #正确输出
                                       stderr=subprocess.PIPE,   #错误输出
                                       )
                stdout = ret.stdout.read()
                stderr = ret.stderr.read()
    
                #发送之前先创建一个报头
                total_size = len(stdout) + len(stderr)
                #报头必须是定长,不然发过去,不知道边界也没有意义
                header_size = struct.pack('i',total_size)  #转化为可以直接传出的二进制,而且i就为4
                print(header_size)
                conn.send(header_size)
    
                conn.send(stdout)
                conn.send(stderr)
    
            except ConnectionResetError:
                break
        conn.close()
    server.close()

    接收方:

    #客户端
    from socket import *
    import struct
    client = socket()
    client.connect(('127.0.0.1',8888))
    while True:
        cmd = input(">>>").strip()
        if len(cmd) == 0:continue
        client.send(cmd.encode("utf-8"))
        header_size = client.recv(4)     #先接受报头,这样就能知道这个包的边界了
        total_size = struct.unpack('i',header_size)[0]    #返回的是一个元祖
        fact_size = 0
        fact = b''
        while fact_size < total_size:  #使用循环,只有这个包取完了,才能取下一个,这样就没有遗漏
            data = client.recv(1024)
            fact += data
            fact_size += len(data)
        print(fact.decode('gbk'))  #由于实在window下面执行,所有返回的编码是GBK格式,也只能用GBK解开。
    client.close()

    上面的代码加上一个包头,已经解决了黏包问题,但是如果我想在包头之中添加其它信息,而struct只能接收数字参数,又该如何了?

    我们可以先把需要传输的信息解出来,再在信息中提取边界。

    升级版:

    #服务端
    from socket import *
    import subprocess
    import struct,json
    server = socket(AF_INET,SOCK_STREAM)
    server.bind(('127.0.0.1',8888))
    server.listen(5)
    while True:
        conn,addr = server.accept()
        print('收到来自%s的访问' % addr[0])
        while True:
            try:
                cmd = conn.recv(1024)  #因为一般命令都不会太长,使用1024足矣
                cmd = cmd.decode("utf-8")
                ret = subprocess.Popen(cmd,shell=True,
                                       stdout=subprocess.PIPE,   #正确输出
                                       stderr=subprocess.PIPE,   #错误输出
                                       )
                stdout = ret.stdout.read()
                stderr = ret.stderr.read()
    
                #发送之前先创建一个报头
                total_size = len(stdout) + len(stderr)
                #报头必须是定长,不然发过去,不知道边界也没有意义
                # 先创建一个报头
                header_dict = {'total_size':total_size,   #封装的包头
                               "md5":78978900967890878,
                               "filename":"a.txt"
                               }
                header_str = json.dumps(header_dict)   #字典不能传输,先使用json转化为字符串
                header_bytes = header_str.encode('utf-8')    #转化成二进制格式,不然直接报错
                header_size = struct.pack('i',len(header_bytes))   #第一层数据(包含包头大小)
                conn.send(header_size)   #发送大小给客户端
                conn.send(header_bytes)  #包头信息也要一并发过去
                conn.send(stdout)
                conn.send(stderr)
            except ConnectionResetError:
                break
        conn.close()
    server.close()

    为什么UDP不会出现黏包问题了?

    UDP的数据报会保留边界,这就表示,数据是整个发送的,不会像面向连接的协议那样先被拆分成小块。自然就不会存在残留,所以不会出现黏包现象。

  • 相关阅读:
    《三体》推荐
    低调做人,高调做事
    注意力的培养是学校教学的真正目的
    【RTP.NET入门系列 一】接收第一个RTP包。
    MapX开发日记(三)GPS项目终于有了眉头
    【RTP.NET入门系列 二】接收第一个RTP帧。
    10.04 flash 乱码 问题
    10.04 中文输入发问题。
    通过值类型进行Timer类的线程的同步。
    关于ManualResetEvent信号机制。
  • 原文地址:https://www.cnblogs.com/yangmingxianshen/p/7912921.html
Copyright © 2020-2023  润新知