zoukankan      html  css  js  c++  java
  • 基于tcp协议的粘包问题(subprocess、struct)

    要点:

    1. 报头  固定长度bytes类型

    1、粘包现象

      粘包就是在获取数据时,出现数据的内容不是本应该接收的数据,如:对方第一次发送hello,第二次发送world,
    我放接收时,应该收两次,一次是hello,一次是world,但事实上是一次收到helloworld,一次收到空,这种现象
    叫粘包

      只有TCP有粘包现象,TCP协议是面向流的协议,这也是容易出现粘包问题的原因。例如基于tcp的套接字客户端往服务端上传文件,发送时文件内容是按照一段一段的字节流发送的,在接收方看了,根本不知道该文件的字节流从何处开始,在何处结束。所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

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

    2、两种情况下会发生粘包。

      发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

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

    3.粘包的解决

      是通过设置报头,在传信息之前,先把该信息的报头传给对方,在传该信息,就不会出现粘包
      报头是一个字典,它含有信息的大小,还有可以有文件的名称,文件的hash值,如下
      head_dic={'size':len(data),'filename':filenaem,'hash':hash值},如果还有,可以继续在字典中加

    1、基于远程执行命令的程序

      需要用到subprocess模块

    服务端

    #1、执行客户端发送的指令
    import socket
    import subprocess
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.bind(('127.0.0.1',8090))
    phone.listen(5)
    while True:
        conn,addr=phone.accept()
        print('IP:%s PORT:%s' %(addr[0],addr[1]))
        while True:
            try:
                cmd=conn.recv(1024)
                if not cmd:break
                #执行命令
                obj=subprocess.Popen(cmd.decode('utf-8'),shell=True,
                                     stdout=subprocess.PIPE,
                                     stderr=subprocess.PIPE
                                     )
                stdout=obj.stdout.read()
                stderr=obj.stderr.read()
                conn.send(stdout+stderr)
            except Exception:
                break
        conn.close()
    phone.close()

    客户端:

    import socket
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    phone.connect(('127.0.0.1',8090))
    while True:
        cmd=input('>>:').strip()
        if not cmd:continue
        phone.send(cmd.encode('utf-8'))
        res=phone.recv(1024)
        print(res.decode('gbk'))
    phone.close()

    注意注意注意:

    res=subprocess.Popen(cmd.decode('utf-8'),
    shell=True,
    stderr=subprocess.PIPE,
    stdout=subprocess.PIPE)

    的结果的编码是以当前所在的系统为准的,如果是windows,那么res.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码,且只能从管道里读一次结果。

    subprocess模块

    从python2.4版本开始,可以用subprocess这个模块来产生子进程,并连接到子进程的标准输入/输出/错误中去,还可以得到子进程的返回值。
    subprocess意在替代其他几个老的模块或者函数,比如:os.system os.spawn* os.popen* popen2.* commands.*

    subprocess模块定义了一个类: Popen
    class subprocess.Popen( args, 
          bufsize=0, 
          executable=None,
          stdin=None,
          stdout=None, 
          stderr=None, 
          preexec_fn=None, 
          close_fds=False, 
          shell=False, 
          cwd=None, 
          env=None, 
          universal_newlines=False, 
          startupinfo=None, 
          creationflags=0)

    参数意思

     

    struct模块

    struct.pack(fmt, v1, v2, ...)

    struct.pack用于将Python的值根据格式符,转换为字符串,准确来说是Byte。

    Python3内的unicode和bytes,在Py3内文本总是Unicode,由str类型表示,二进制数据则由bytes类型表示。

    Py2是没有Byte这么个东西的。参数fmt是格式字符串,v1, v2, ...表示要转换的python值

    struct.unpack(fmt, buffer)

    struct.unpack做的工作刚好与struct.pack相反,用于将字节流转换成python数据类型。它的函数原型为:struct.unpack(fmt, string),该函数返回一个tuple。

     struct.calcsize(fmt)

    struct.calcsize用于计算格式字符串所对应的结果的长度,如:struct.calcsize('ii'),返回8。因为两个int类型所占用的长度是8个字节。

    import struct
    
    #帮我们把数字转成固定长度的bytes类型
    res=struct.pack('i',123123)
    print(res,len(res))  # b'xf3xe0x01x00' 4
    
    res1=struct.unpack('i',res)
    print(res1[0])  # 123123

    格式符"i"表示转换为int,'ii'表示有两个int变量。进行转换后的结果长度为8个字节(int类型占用4个字节,两个int为8个字节)

    import struct
    res=struct.pack('ii',1220,520)
    print(res,len(res))  # b'xc4x04x00x00x08x02x00x00' 8

    小端:较高的有效字节存放在较高的存储器地址,较低的有效字节存放在较低的存储器地址。
    大端:较高的有效字节存放在较低的存储器地址,较低的有效字节存放在较高的存储器地址。

    采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。

    res=struct.pack('>ii',1220,520)
    print(res,len(res)) # b'x00x00x04xc4x00x00x02x08' 8  大端保存
     
    res=struct.pack('<ii',1220,520)
    print(res,len(res)) # b'xc4x04x00x00x08x02x00x00' 8  小端保存

    解决粘包问题

    办法1

    服务端

    import subprocess
    import struct
    from socket import *
    server=socket(AF_INET,SOCK_STREAM)
    server.bind(('127.0.0.1',8084))
    # print(server)
    server.listen(5)
    while True:
        conn,addr=server.accept()
        # print(conn)
        print(addr)
        while True:
            try:
                cmd=conn.recv(8096)
                if not cmd:break #针对linux
    
                #执行命令
                cmd=cmd.decode('utf-8')
                #调用模块,执行命令,并且收集命令的执行结果,而不是打印
                obj = subprocess.Popen(cmd, shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE,
                                       )
                stdout=obj.stdout.read()
                stderr=obj.stderr.read()
                #第一步:制作报头:
                total_size=len(stdout)+len(stderr)
    
                header=struct.pack('i',total_size)
                #第二步:先发报头(固定长度)
                conn.send(header)
    
                #第三步:发送命令的结果
                conn.send(stdout)
                conn.send(stderr)
            except ConnectionResetError:
                break
        conn.close()
    
    server.close()

    客户端

    import struct
    from socket import *
    
    client=socket(AF_INET,SOCK_STREAM)
    client.connect(('127.0.0.1',8084))
    
    while True:
        cmd=input('>>: ').strip()
        if not cmd:continue
        client.send(cmd.encode('utf-8'))
    
        #第一步:收到报头
        header=client.recv(4)
        total_size=struct.unpack('i',header)[0]
    
        #第二步:收完整真实的数据
        recv_size=0
        res=b''
        while recv_size < total_size:
            recv_data=client.recv(1024)
            res+=recv_data
            recv_size+=len(recv_data)
        print(res.decode('gbk'))
    
    client.close()

    由于模块struct的pack方法中使用的i类型存在整数无限大时会出现报错的弊端,故提出如下解决方案:

    办法二(终极版)

    服务端

    import subprocess
    import struct
    import json
    from socket import *
    server=socket(AF_INET,SOCK_STREAM)
    server.bind(('127.0.0.1',8086))
    # print(server)
    server.listen(5)
    while True:
        conn,addr=server.accept()
        # print(conn)
        print(addr)
        while True:
            try:
                cmd=conn.recv(8096)
                if not cmd:break #针对linux
    
                #执行命令
                cmd=cmd.decode('utf-8')
                #调用模块,执行命令,并且收集命令的执行结果,而不是打印
                obj = subprocess.Popen(cmd, shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE,
                                       )
                stdout=obj.stdout.read()
                stderr=obj.stderr.read()
    
    
                # 1:先制作报头,报头里放:数据大小, md5, 文件
                header_dic = {
                    'total_size':len(stdout)+len(stderr),
                    'md5': 'xxxxxxxxxxxxxxxxxxx',
                    'filename': 'xxxxx',
                    'xxxxx':'123123'
                }
                header_json = json.dumps(header_dic)
                header_bytes = header_json.encode('utf-8')
                header_size = struct.pack('i', len(header_bytes))
    
                # 2: 先发报头的长度
                conn.send(header_size)
    
                # 3:先发报头
                conn.send(header_bytes)
    
                # 4:再发送真实数据
                conn.send(stdout)
                conn.send(stderr)
            except ConnectionResetError:
                break
        conn.close()
    
    server.close()

    客户端

    import struct
    import json
    from socket import *
    
    client=socket(AF_INET,SOCK_STREAM)
    client.connect(('127.0.0.1',8086))
    
    while True:
        cmd=input('>>: ').strip()
        if not cmd:continue
        client.send(cmd.encode('utf-8'))
    
        # 1:先收报头长度
        obj = client.recv(4)
        header_size = struct.unpack('i', obj)[0]
    
        # 2:先收报头,解出报头内容
        header_bytes = client.recv(header_size)
        header_json = header_bytes.decode('utf-8')
        header_dic = json.loads(header_json)
    
        print(header_dic)
        total_size = header_dic['total_size']
    
        # 3:循环收完整数据
        recv_size=0
        res=b''
        while recv_size < total_size:
            recv_data=client.recv(1024)
            res+=recv_data
            recv_size+=len(recv_data)
        print(res.decode('gbk'))
    
    client.close()

     能下载视频

    服务端

    import subprocess
    import struct
    import json
    import os
    from socket import *
    server=socket(AF_INET,SOCK_STREAM)
    server.bind(('127.0.0.1',8087))
    # print(server)
    server.listen(5)
    def get(filename):
        # 1:先制作报头,报头里放:数据大小, md5, 文件
        header_dic = {
            'total_size': os.path.getsize(filename),
            'md5': 'xxxxxxxxxxxxxxxxxxx',
            'filename': 'xxxxx',
            'xxxxx': '123123'
        }
        header_json = json.dumps(header_dic)
        header_bytes = header_json.encode('utf-8')
        header_size = struct.pack('i', len(header_bytes))
    
        # 2: 先发报头的长度
        conn.send(header_size)
    
        # 3:先发报头
        conn.send(header_bytes)
    
        # 4:再发送真实数据
        with open(filename,'rb') as f:
            for line in f:
                conn.send(line)
    
    
    while True:
        conn,addr=server.accept()
        # print(conn)
        print(addr)
        while True:
            try:
                cmd=conn.recv(8096) #get C:\a.txt
                if not cmd:break #针对linux
    
                cmd,filename=cmd.split()
                if cmd == 'get':
                    get(filename)
            except ConnectionResetError:
                break
        conn.close()
    
    server.close()

    客户端

    import struct
    import json
    from socket import *
    
    client=socket(AF_INET,SOCK_STREAM)
    client.connect(('127.0.0.1',8087))
    
    while True:
        cmd=input('>>: ').strip()
        if not cmd:continue
        client.send(cmd.encode('utf-8'))
    
        # 1:先收报头长度
        obj = client.recv(4)
        header_size = struct.unpack('i', obj)[0]
    
        # 2:先收报头,解出报头内容
        header_bytes = client.recv(header_size)
        header_json = header_bytes.decode('utf-8')
        header_dic = json.loads(header_json)
    
        print(header_dic)
        total_size = header_dic['total_size']
        filename = header_dic['filename']
        md5 = header_dic['md5']
    
        # 3:循环收完整数据
        recv_size=0
        with open(filename,'wb') as f:
            while recv_size < total_size:
                recv_data=client.recv(1024)
                f.write(recv_data)
                recv_size+=len(recv_data)
    
    
    client.close()
  • 相关阅读:
    反正我是猜错,关于javascript包装对象的一个坑
    《编写可维护的javascript》推荐的编码规范之——编程实践
    《编写可维护的javascript》推荐的编码规范之——编程风格
    35行的山寨版jQuery
    Css进度条
    nginx的location和proxy_pass是否带斜杠的区别
    mongodb笔记
    在安装完 docker for windows 之后,hype-v打开,之后再关闭,vmware无法使用,虚拟机报错:传输(vmdb)错误-32:pipe:read failed
    awk笔记备忘
    sed命令笔记备忘
  • 原文地址:https://www.cnblogs.com/jassin-du/p/7910378.html
Copyright © 2011-2022 走看看