zoukankan      html  css  js  c++  java
  • 黏包-黏包的成因、解决方式及struct模块初识、文件的上传和下载

    黏包:


    同时执行多条命令之后,得到的结果很可能只有一部分,在执行其他命令的时候又接收到之前执行的另外一部分结果,这种显现就是黏包。

    只有TCP协议中才会产生黏包,UDP协议中不会有黏包(udp协议中数据会直接丢失,俗称丢包)

    #面试
    #首先只有在TCP协议中才有黏包现象,是因为TCP协议是面向流的协议,
    #在发送的数据传输的过程中还有缓存机制来避免数据丢失
    #因此在连续发送小数据的时候,以及接收大小不符的时候,都容易产生黏包现象
    #本质是不知道客户端发送的数据长度
    面试中解释黏包
    #连续send两个小数据
    #两个recv,第一个recv特别小
    
    #面试
    #首先只有在TCP协议中才有黏包现象,是因为TCP协议是面向流的协议,
    #在发送的数据传输的过程中还有缓存机制来避免数据丢失
    #因此在连续发送小数据的时候,以及接收大小不符的时候,都容易产生黏包现象
    #本质是不知道客户端发送的数据长度
    
    #服务端发送数据,经过系统内存,客户端接收,在TCP协议中,如果服务端发送数据过大,或者客户端接收数据过少,超过的数据就会滞纳在内存中
    #从而产生了黏包。但是在UDP过程中则直接丢包了
    #本质问题,不知道客户端发送的数据长度
    
    #在TCP中  如果发送端发送了3次,3次总数据大小为10,接收端如果只接收一次,接收大小>10,那么这3次会合起来一起被接收
    #以为TCP中的优化算法,当几次数据又小又连续,会合并发送(需要又小又连续)
    #因为一次性接收,只需要经历一次网络延时,提高接收效率,
    #并且只要接收方足够大,它会一次性把这三次数据都给合并接收,即使此处有3次接收
    #比如此处 ret1和ret2接收都为空,连续的小数据被ret接收了
    # ret = conn.recv(1024)
    # print(ret)
    # ret1 = conn.recv(1024) #此时ret1和ret2接收到的空,是因为数据都被ret接收了,本来是阻塞的,等待消息传入。
    # ret2 = conn.recv(1024) #但是此时客户端close了,会默认发送空消息过来(根据windows版本不同,也可能直接报错),刚好被ret1和ret2接收了。
    # print(ret1,ret2)
    
    #多个send小的数据连在一起,会发生黏包现象,是TCP内部优化算法造成的
    #注意这几个send必须又短又连续且发送间隔非常短暂(0.01秒都不行)
    黏包问题总结
    #UDP不会黏包,但是udp会丢包
    #tcp会黏包,但是不会丢包
    
    # 黏包成因:
    # TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。
    # 收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,
    # 使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
    # 这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。
    
    # 当发送端缓冲区的长度大于网卡的MTU时,tcp会将这次发送的数据拆成几个数据包发送出去。
    # MTU是Maximum Transmission Unit的缩写。意思是网络上传送的最大数据包。MTU的单位是字节。
    # 大部分网络设备的MTU都是1500。如果本机的MTU比网关的MTU大,大的数据包就会被拆开来传送,
    # 这样会产生很多数据包碎片,增加丢包率,降低网络速度。
    
    # 1.是因为tcp的拆包机制,使得消息没有边界
    # 2.当发送端缓冲区的长度大于网卡的MTU时,产生了数据包碎片
    黏包成因-蛮看看就好

    黏包的发现:


    socket模块中,TCP协议的黏包问题发现:(用到了subprocess模块)

    import socket
    import subprocess
    ip_port = ('127.0.0.1',8898)
    buffer_size = 10240
    
    sk = socket.socket()
    sk.bind(ip_port)
    sk.listen()
    
    conn,addr = sk.accept()
    while True:
        cmd = input('>>>>> ')
        if cmd == 'q':
            break
        conn.send(cmd.encode('utf-8'))
        ret = conn.recv(buffer_size).decode('utf-8')
        # ret2 = conn.recv(buffer_size).decode('utf-8')
        print(ret)
        # print(ret2)
    
    conn.close()
    sk.close()
    serve端-黏包
    import socket
    import subprocess
    sk = socket.socket()
    sk.connect(('127.0.0.1',8898))
    
    while True:
        cmd = sk.recv(1024).decode('gbk')
        ret = subprocess.Popen(cmd,shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE
                               )
        std_out = 'stdout: '+(ret.stdout.read()).decode('gbk')
        std_err = 'stderr: '+(ret.stderr.read()).decode('gbk')
        print(std_out)
        print(std_err)
        sk.send(std_out.encode('utf-8'))
        sk.send(std_err.encode('utf-8'))
    sk.close()
    client端-黏包

    黏包自己的小分析:

    #服务端发送数据,经过系统内存,客户端接收,在TCP协议中,如果服务端发送数据过大,或者客户端接收数据过少,超过的数据就会滞纳在内存中(内核态用户态)
    #从而产生了黏包。但是在UDP过程中则直接丢包了
    #本质问题,不知道客户端发送的数据长度
    
    
    import socket
    sk = socket.socket()
    sk.bind(('127.0.0.1',8898))
    sk.listen()
    
    conn,addr = sk.accept()
    
    #在TCP中  如果发送端发送了3次,3次总数据大小为10,接收端如果只接收一次,接收大小>10,那么这3次会合起来一起被接收
    #以为TCP中的优化算法,当几次数据又小又连续,会合并发送(需要又小又连续)
    #因为一次性接收,只需要经历一次网络延时,提高接收效率,
    #并且只要接收方足够大,它会一次性把这三次数据都给合并接收,即使此处有3次接收
    #比如此处 ret1和ret2接收都为空,连续的小数据被ret接收了
    ret = conn.recv(1024)
    print(ret)
    ret1 = conn.recv(1024) #此时ret1和ret2接收到的空,是因为数据都被ret接收了,本来是阻塞的,等待消息传入。
    ret2 = conn.recv(1024) #但是此时客户端close了,会默认发送空消息过来(根据windows版本不同,也可能直接报错),刚好被ret1和ret2接收了。
    print(ret1,ret2)
    
    conn.close()
    sk.close()
    
    #多个send小的数据连在一起,会发生黏包现象,是TCP内部优化算法造成的
    #注意这几个send必须又短又连续且发送间隔非常短暂(0.01秒都不行)
    server-黏包问题分析
    import socket,time
    sk = socket.socket()
    sk.connect(('127.0.0.1',8898))
    
    sk.send(b'hello')
    time.sleep(0.01) #此时 hello会单独发送,下面两句才一起发送
    sk.send(b'he22')
    sk.send(b'33llo')
    
    #多个send小的数据连在一起,会发生黏包现象,是TCP内部优化算法造成的
    #注意这几个send必须又短又连续且发送间隔非常短暂(0.01秒都不行)
    
    # import time
    # time.sleep(5)
    sk.close()
    client-黏包问题分析

    黏包的解决


    有两种方式:

    1.比较low的方法:及优劣点分析

    import socket
    
    sk = socket.socket()
    sk.bind(('127.0.0.1',8898))
    sk.listen()
    conn,addr = sk.accept()
    
    while True:
        cmd = input('>>>>> ')
        if cmd == 'q':
            break
        conn.send(cmd.encode('gbk'))
        len_num = conn.recv(1024).decode('utf-8')
        conn.send(b'ok')
        len_num = int(len_num)
        ret = conn.recv(len_num).decode('gbk')
        print(ret)
    
    
    conn.close()
    sk.close()
    
    #好处:确定了我到底要接收多大的数据
        #要在文件中配置了一个配置项:就是每一次recv的大小  buffer = 4096
        #当我们要发送大数据的时候,要明确的告诉接收方要发送多大的数据,以便接收方能够准确的接收到所有数据
        #多用在文件传输的过程中
            #大文件的传输 一定是按照字节读 每一次读固定的字节
            #传输的过程中 发送端一边读一边传,接收端一边收一边写
            #send这个大文件之前,35672字节 send(4096)-....-send(4096)---->0
            #resv这个大文件之前,35672字节 send(4096)-....-send(4096)---->0
    
    #不好的地方:
        #多了一次交互
        #send sendto 在超过一定范围的时候,都会报错
        #程序的内存管理
    server端-方法的好处和坏处
    import socket
    import subprocess
    
    sk = socket.socket()
    sk.connect(('127.0.0.1',8898))
    
    while True:
        cmd = sk.recv(1024).decode('gbk')
        if cmd == 'q':
            break
        ret = subprocess.Popen(cmd,shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
        std_out = ret.stdout.read()  #注意read本身就是bytes类型。
        std_err = ret.stderr.read()  #此处read完后,类似从list中pop,管道中的值就取出来没了,类似队列
        len_num = str(len(std_out) + len(std_err))
        sk.send(len_num.encode('utf-8'))
        sk.recv(1024)
    
        sk.send(std_out)
        sk.send(std_err)
    sk.close()
    client端

    2.用struct模块解决黏包问题,及struct模块初识

    import socket
    import subprocess
    import struct
    sk = socket.socket()
    sk.bind(('127.0.0.1',8898))
    sk.listen()
    conn,addr = sk.accept()
    
    while True:
        cmd = input('>>>>> ')
        if cmd == 'q':
            break
        conn.send(cmd.encode('gbk'))
        len_byte = conn.recv(4)         #通过struct模块,确定知道传过来的int为4个字节,这里只收取4个字节,保证不黏包
        len_num = struct.unpack('i',len_byte)[0]  #获取到传过来的数字,也就是接下来要接收数据的大小
        ret = conn.recv(len_num).decode('gbk')    #通过len,知道接下来要接收多大的数据,从而不管发送端发了几次,这边可以一次性接收
        print(ret)
    
    
    conn.close()
    sk.close()
    server端-struct
    import socket
    import subprocess
    import struct
    sk = socket.socket()
    sk.connect(('127.0.0.1',8898))
    
    while True:
        cmd = sk.recv(1024).decode('gbk')
        if cmd == 'q':
            break
        ret = subprocess.Popen(cmd,shell=True,
                               stdout=subprocess.PIPE,
                               stderr=subprocess.PIPE)
        std_out = ret.stdout.read()  #注意read本身就是bytes类型。
        std_err = ret.stderr.read()  #此处read完后,类似从list中pop,管道中的值就取出来没了,类似队列
        len_num = len(std_out) + len(std_err)
        len_byt = struct.pack('i',len_num)
        sk.send(len_byt)
        sk.send(std_out)
        sk.send(std_err)
    sk.close()
    client端-struct
    import struct
    #模块可以把一个数字 转换为 一个bytes类型 (二进制)  然后再转回数字类型
    #以int为例  int转换后的bytes类型长度为 4字节,以元组的方式返回 (int,)
    #除了int 还有 float double  long  等等
    #其中,int将在以后很长的时间里都只使用int
    
    num_byte = struct.pack('i',4096)
    print(num_byte)      #b'x00x10x00x00'
    print(len(num_byte)) #长度为4
    
    num_tup = struct.unpack('i',num_byte)
    print(num_tup)          #(4096,)
    print(type(num_tup[0])) #<class 'int'>
    
    #通过struct模块,我们就将int转换为长度为4的bytes,就可以通过先recv(4),获取到一个int类型
    # 这个int类型表示接下来要接收的数据大小
    #和黏包问题解决中比较low的方法比较,省去了一个交互步骤,不用我收到len大小后,再send(b'ok')来避免 len 被黏包
    struct模块初识

    练习:


    大文件的上传和下载

    import os
    import json
    import socket
    import struct
    bufer = 1024
    ip_port = ('127.0.0.1',8898)
    sk = socket.socket()
    sk.bind(ip_port)
    sk.listen()
    conn,addr = sk.accept()
    
    len_bytes = conn.recv(4)   #!!!!!这里怎么可以出错,struct传过来的int 就是4字节
    len_head = struct.unpack('i',len_bytes)[0]
    head_json = conn.recv(len_head).decode('utf-8')
    head = json.loads(head_json)
    filesize = head['filesize']
    with open(head['file_name'],'wb') as f:
        while filesize:
            print(filesize)
            if filesize >= bufer:
                file_write = conn.recv(bufer)
                f.write(file_write)
                filesize -= bufer
            else:
                file_write = conn.recv(filesize)
                f.write(file_write)
                break
    conn.close()
    sk.close()
    server
    import os
    import json
    import socket
    import struct
    bufer = 1024
    ip_port = ('127.0.0.1',8898)
    sk = socket.socket()
    sk.connect(ip_port)
    
    head = {'file_path':r'D:python-全栈九期day33',
            'file_name':'04 python fullstack s9day33 ftp作业分析.mp4',
            'filesize':None}
    file_path = os.path.join(head['file_path'],head['file_name']) #join的用法写错了 !!!
    filesize = os.path.getsize(file_path)
    head['filesize'] = filesize
    head_json = json.dumps(head)
    head_bytes = head_json.encode('utf-8')
    
    #开始用struct转换报头长度
    len_head = len(head_bytes)
    head_pack = struct.pack('i',len_head)
    sk.send(head_pack)
    sk.send(head_bytes)
    with open(file_path,'rb') as f:  #!!!这里rb写成了wb
        while filesize:
            print(filesize)
            if filesize >= bufer:
                content = f.read(bufer)
                sk.send(content)
                filesize -= bufer
            else:
                content = f.read(filesize)
                sk.send(content)
                break
    sk.close()
    client
  • 相关阅读:
    Visual Studio 2010使用Visual Assist X的方法
    SQL Server 2000 评估版 升级到 SQL Server 2000 零售版
    双网卡多网络单主机同时访问
    开发即过程!立此纪念一个IT新名词的诞生
    delphi dxBarManager1 目录遍历 转为RzCheckTree2树
    5320 软件集合
    delphi tree 从一个表复制到另一个表
    DELPHI 排课系统课表
    长沙金思维 出现在GOOGLE的 金思维 相关搜索里啦!!
    如何在DBGrid的每一行前加一个单选框?
  • 原文地址:https://www.cnblogs.com/gkx0731/p/9727223.html
Copyright © 2011-2022 走看看