zoukankan      html  css  js  c++  java
  • python 粘包现象

    一. 粘包现象

    1. 粘包现象的由来
    (1)TCP属于长连接,当服务端与一个客户端进行了连接以后,其他客户端需要(排队)等待.若服务端想要连接另一个客户端,必须首先断开与第一个客户端的连接.

    (2)缓冲区(参考资料):
    a. 缓冲区(buffer),它是内存空间的一部分.也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区,显然缓冲区是具有一定大小的.
    b. 每个socket(套接字)被创建后,都会分配两个缓冲区: 输入缓冲区和输出缓冲区.
    c. write()/send()并不立即向网络中传输数据,而是先将数据写入缓冲区中,再由TCP协议将数据从缓冲区发送到目标机器. 一旦将数据写入到缓冲区,函数就已经完成任务可以成功返回了,而不用去考虑数据何时被发送到网络,也不用去考虑数据是否已经到达目标机器,因为这些后续操作都是TCP协议负责的事情.
    d. TCP协议独立于 write()/send() 函数,数据有可能刚被写入缓冲区就发送到网络,也可能在缓冲区中不断积压,多次写入的数据被一次性发送到网络,这取决于当时的网络情况,当前线程是否空闲等诸多因素,不由程序员控制.
    e. read()/recv() 函数也是如此,也从输入缓冲区中读取数据,而不是直接从网络中读取.
    f. 这些I/O缓冲区特性可整理如下:
    1). I/O缓冲区在每个TCP套接字中单独存在
    2). I/O缓冲区在创建套接字时自动生成
    3). 即使关闭套接字也会继续传送输出缓冲区中遗留的数据
    4). 关闭套接字将丢失输入缓冲区中的数据
    5). 输入/输出缓冲区的默认大小一般是8K(了解:可以通过getsockopt()函数获取)
    g. 如果一次性最多输入/输出的数据量超出了缓冲区的大小,系统就会报错.例如,在UDP协议下,一个数据包的大小超过了一次recv()方法能接受数据量大小,就会报错.

    2. subprocess模块 的简单用法
    import subprocess
    cmd = imput("请输入指令>>>")
    result = subprocess.Popen(
        cmd,                    # 字符串指令: "dir","ipconfig",等等
        shell=True,             # 使用shell,就相当于使用cmd窗口
        stderr=subprocess.PIPE, # 标准错误输出,凡是输入错误指令,错误指令输出的报错信息就会被它拿到
        stdout=subprocess.PIPE, # 标准输出,正确指令的输出结果会被它拿到
    )
    print(result.stdout.read().decode("gbk"))
    print(result.stderr.read().decode("gbk"))
    注意: 如果是在windows操作系统下,那么result.stdout.read()读出的就是GBK编码的,在接收端需要用GBK解码且只能从管道里读一次结果,PIPE称为管道.


    3. 粘包现象模拟(tcp协议下)
    (1)发送方连续发送较小的数据,并且每次发送之间的时间间隔很短,此时,两个消息在输出缓冲区黏在一起了.原因是TCP为了传输效率,做了一个优化算法(Nagle),减少连续的小包发送(因为每个消息被包裹以后,都会有两个过程:组包和拆包,这两个过程是极其消耗时间的,优化算法Magle的目的就是为了减少传输时间)

    服务端(接收方):
    import socket
    server = socket.socket()
    ip_port = ("192.168.15.28", 8001)
    server.bind(ip_port)
    server.listen()
    conn, addr = server.accept()
    
    # 连续接收两次消息
    from_client_msg1 = conn.recv(1024).decode("utf-8")
    print("第一次接收到的消息>>>", from_client_msg1)
    from_client_msg2 = conn.recv(1024).decode("utf-8")
    print("第二次接收到的消息>>>", from_client_msg2)
    
    conn.close()
    server.close()
    客户端(发送方):
    import socket
    client = socket.socket()
    server_ip_port = ("192.168.15.28", 8001)
    client.connect(server_ip_port)
    
    # 连续发送两次消息
    client.send('Hello'.encode('utf-8'))
    client.send('World'.encode('utf-8'))
    
    client.close()
    先运行服务端,再运行客户端,最终在服务端看到的执行结果是:
    第一次接收到的消息>>> HelloWorld
    第二次接收到的消息>>>
    客户端连续发送了两次消息,第一次发送"Hello",第二次发送"World".双方执行程序后却发现,服务端本应接收到两个消息,然而实际情况是:服务端在第一次就收到了所有消息,并且两个消息紧紧靠在一起,出现了"粘包现象"!
    (2)发送方第一次发送的数据比接收方设置的"一次接收消息的大小"要大,于是会出现一次接收不完的情况.因此,接收方第二次再接收的时候,就会将第一次剩余的消息接收到,从而与后续消息粘结住,产生粘包现象.


    二. 粘包的解决方案
    解决方案(一):粘包问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕"如何让发送端在发送数据前,把自己将要发送的字节流总长度让接收端知晓".
    解决步骤:
    a. 发送端把"数据长度"传输给接收端
    b. 接收端把"确认信息"传输给发送端
    c. 发送端把"全部数据"传输给接收端
    d. 接收端使用一个死循环接收完所有数据.

    服务端:
    代码流程: (服务端是发送端,客户端是接收端)
    a. 服务端接收客户端的cmd指令
    b. 服务端通过subprocess模块,从电脑系统中,拿到cmd指令返回值
    c. 服务端拿到cmd指令返回值的"字节流长度", 并将其传输给客户端
    d. 服务端接收来自客户端的"确认信息"
    e. 服务端把"cmd指令返回值"传输给客户端
    import socket
    import subprocess
    
    server = socket.socket()
    ip_port = ('192.168.15.28',8001)
    server.bind(ip_port)
    server.listen()
    conn,addr = server.accept()
    
    while 1:
        from_client_cmd = conn.recv(1024).decode('utf-8')   # a.接收来自客户端的cmd指令
        sub_obj = subprocess.Popen(
            from_client_cmd,    # 客户端的指令
            shell=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
        )
    
        server_cmd_msg = sub_obj.stdout.read()              # b.拿到cmd指令返回值 --> stdout接受到的返回值是bytes类型的,并且windows系统的默认编码为gbk
        cmd_msg_len = str(len(server_cmd_msg))              # c.拿到返回值的长度
        print("cmd返回的正确信息的长度>>>",cmd_msg_len)
        conn.send(cmd_msg_len.encode('gbk'))                # c.把"长度"传输给客户端
        from_client_ack = conn.recv(1024).decode('utf-8')   # d.拿到"确认信息"
    
        if from_client_ack == "确认":
            conn.send(server_cmd_msg)                       # e.把"cmd指令返回值"传输给客户端
        else:
            continue
    客户端:
    代码流程: (服务端是发送端,客户端是接收端)
    a. 用户输入cmd指令
    b. 客户端把"cmd指令"传输给服务端
    c. 客户端接收cmd指令返回值的"字节流长度"
    d. 客户端把"确认信息"传输给服务端
    e. 客户端通过"字节流长度"设置最大可接收数据量,同时接收"cmd指令返回值"
    import socket
    client = socket.socket()
    server_ip_port = ('192.168.15.28',8001)
    client.connect(server_ip_port)
    
    while 1:
        cmd = input('请输入要执行的指令>>>')                                    # a.用户输入cmd指令
        client.send(cmd.encode('utf-8'))                                     # b.把"cmd指令"传输给服务端
        from_server_msglen = int(client.recv(1024).decode('gbk'))            # c.接收cmd指令返回值的"字节流长度"
        print('接收到的信息长度是>>>', from_server_msglen)
        client.send('确认'.encode('utf-8'))                                   # d.把"确认信息"传输给服务端
        from_server_stdout = client.recv(from_server_msglen).decode('gbk')   # e.设置最大可接收数据量,同时接收"cmd指令返回值"
        print('接收到的指令返回值是>>>', from_server_stdout)

     

    2. 解决方案(二):通过struct模块将数据实体(要传输的数据)的长度打包成一个"4bytes字符串",并将其传输给接收端.接收端取出这个"4bytes字符串",对其进行解包,解包后的内容就是"数据实体的长度",接收端再通过这个长度来继续接收数据实体.

    (1)首先简单认识一下struct模块:
    struct模块中最重要的两个函数是:
    pack() -- 具有"打包"功能
    unpack() -- 具有"解包"功能

    1)bytes = struct.pack(format, values)
    描述: pack() --> 把"int类型的数据"打包成一个"4bytes字符串"
    参数: format --> 数据格式(类型),在这里我们指定该格式为"i"(即int数据类型).
    values --> 整数
    返回值: 4bytes字符串

    2)(values,) = struct.unpack(format, bytes)
    描述: unpack() --> 把"4bytes字符串"解包成"int类型的数据"
    参数: format --> 数据格式(类型),在这里我们指定该格式为"i"(即int数据类型).
    bytes --> "4bytes字符串"
    返回值: unpack()函数以元组的形式返回int类型的数据

    举例说明:
    import struct
    num = 100

    # 把"int类型的数据"打包成"4bytes字符串"
    num_stru = struct.pack("i", num) # "i"代表int数据类型
    print(len(num_stru)) # 打印结果: 4
    print(num_stru) # 打印结果: b'dx00x00x00'

    # 把"4bytes字符串"解包成"int类型的数据"
    num2 = struct.unpack("i", num_stru) # "i"代表int数据类型
    print(num2) # 打印结果: (100,) --> 返回结果是元组!!


    (2)结合struct模块解决粘包现象

    服务端(发送端):
    代码流程:
    a.拿到数据实体的长度
    b.将长度打包成"4bytes字符串"
    c.将"4bytes字符串"发送给客户端
    d.发送数据实体

    import socket
    import subprocess
    import struct
    
    server  = socket.socket()
    ip_port = ("127.0.0.1", 8001)
    server.bind(ip_port)
    server.listen()
    conn, addr = server.accept()
    
    while 1:
        from_client_cmd = conn.recv(1024).decode("utf-8")
        print("来自客户端的指令是>>>")
    
        # 通过subprocess模块拿到指令的返回值
        sub_obj = subprocess.Popen(
           from_client_cmd,         # 客户端的指令
           shell=True,
           stdout=subprocess.PIPE,  # 标准输出:接收正确指令的执行结果
           stderr=subprocess.PIPE,  # 标准错误输出:接收错误指令的执行结果
        )
    
        # 通过stdout拿到正确指令的执行结果,即需要发送的"数据实体"
        server_cmd_msg = sub_obj.stdout.read()
        # a.拿到数据实体的长度
        cmd_msg_len = len(server_cmd_msg)
        # b.将长度打包成"4bytes字符串"
        msg_len_stru = struct.pack('i',cmd_msg_len)
        # c.将"4bytes字符串"发送给客户端
        conn.send(msg_len_stru)
        # d.发送数据实体 --> sendall() 循环发送数据,直到数据全部发送成功
        conn.sendall(server_cmd_msg)

    客户端(接收端):
    代码流程:
    a.接收打包后的"4bytes字符串"
    b.解包,拿到"数据实体的长度"
    c.循环接收数据实体,通过"数据实体的长度"来确定跳出循环的条件

    import socket
    import struct
    
    client = socket.socket()
    server_ip_port = ("127.0.0.1", 8001)
    client.connect(server_ip_port)
    
    while 1:
        cmd = input("请输入要执行的指令>>>")
        client.send(cmd.encode("utf-8"))
        # a.接收打包后的"4bytes字符串"
        from_server_msglen = client.recv(4)
        # b.解包,拿到"数据实体的长度",即unpack_msglen
        unpack_msglen = struct.unpack('i', from_server_msglen)[0]
    
        # c.循环接收数据实体,通过"数据实体的长度"来确定跳出循环的条件
        recv_msg_len = 0    # 统计"数据长度"
        all_msg = b''       # 统计"数据实体"
        while recv_msg_len < unpack_msglen:
            every_recv_data = client.recv(1024)
            # 将每次接收到的"数据实体"进行拼接
            all_msg += every_recv_data
            # 将每次接收到的"数据实体的长度"进行累加
            recv_msg_len += len(every_recv_data)
        print(all_msg.decode("gbk"))
  • 相关阅读:
    Encryption (hard) CodeForces
    cf 1163D Mysterious Code (字符串, dp)
    AC日记——大整数的因子 openjudge 1.6 13
    AC日记——计算2的N次方 openjudge 1.6 12
    Ac日记——大整数减法 openjudge 1.6 11
    AC日记——大整数加法 openjudge 1.6 10
    AC日记——组合数问题 落谷 P2822 noip2016day2T1
    AC日记——向量点积计算 openjudge 1.6 09
    AC日记——石头剪刀布 openjudge 1.6 08
    AC日记——有趣的跳跃 openjudge 1.6 07
  • 原文地址:https://www.cnblogs.com/haitaoli/p/9807287.html
Copyright © 2011-2022 走看看