zoukankan      html  css  js  c++  java
  • 粘包问题

    粘包问题

    213131

    TCP与UDP协议

    1. TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的
    2. UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的
    3. tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略

    粘包现象

    socket收发消息的原理

    服务端可以1kb,1kb地发向客户端送数据,客户端的应用程序可以在缓存当中2kb,2kb地取走数据,当然也可以更多,或都更少。也就是说,应用程序看到的数据是来个整体。或者说是一个流。一条消息有多少字节对应用程序是不可见的,TCP协议是面向流的协议,这就是它容易粘包的问题原因。

    所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一个消息要提取多少字节的数据所造成的。

    tcp协议才会有粘包问题,udp协议没有

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

    粘包情况一:发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据量很小,TCP优化算法会当做一个包发出去,产生粘包)

    1、服务端

    from socket import *
    
    server=socket(AF_INET,SOCK_STREAM)
    server.bind(('127.0.0.1',8080))
    server.listen(5)
    
    conn,client_addr=server.accept()
    
    res1=conn.recv(1024)
    print('第一次:',res1)
    res2=conn.recv(1024)
    print('第二次:',res2)
    
    conn.close()
    server.close()
    

    2、客户端

    
    from socket import *
    
    client=socket(AF_INET,SOCK_STREAM)
    client.connect(('127.0.0.1',8080))
    
    # 这两次发送数据由于数据量小且时间短,所以会被TCP优化到一起
    client.send(b'hello')
    client.send(b'world')
    client.close()
    

    先启动服务端,后再启动客户端,服务端得到的结果为:

    第一次: b'helloworld'
    第二次: b''
    

    粘包情况二:客户端发关了一段数据,服务端只收了一小部分,服务端下次再收的时候不是从缓冲区拿上次遗留的数据,产生粘包。

    情况一的,客户端不变,服务端略作修改,如下:

    from socket import *
    
    server=socket(AF_INET,SOCK_STREAM)
    server.bind(('127.0.0.1',8080))
    server.listen(5)
    
    conn,client_addr=server.accept()
    
    res1=conn.recv(2)	# 只接受两个字节
    print('第一次:',res1)
    res2=conn.recv(3)
    print('第二次:',res2)
    
    conn.close()
    server.close()
    
    第一次: b'he'
    第二次: b'llo'
    

    解决方案

    问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包问题的方法就是在发送端发送前,发一个头文件包,告诉发送的字节流总大小

    第一次情况

    服务端

    from socket import *
    import struct
    
    server=socket(AF_INET,SOCK_STREAM)
    server.bind(('127.0.0.1',8080))
    server.listen(5)
    
    conn,client_addr=server.accept()
    # 因为struct中的i就是接受4个字节
    res_bytes = conn.recv(4)
    count_len = struct.unpack('i', res_bytes)[0]
    res1 = conn.recv(count_len)
    print('第一次:',res1)
    
    res_bytes = conn.recv(4)
    count_len = struct.unpack('i', res_bytes)[0]
    res2 = conn.recv(count_len)
    print('第二次:',res2)
    
    conn.close()
    server.close()
    

    客户端

    from socket import *
    import struct
    
    client=socket(AF_INET,SOCK_STREAM)
    client.connect(('127.0.0.1',8080))
    
    count_len = len('hello')
    
    res_bytes = struct.pack('i', count_len)
    # 先发送字节流总大小,再发送数据
    client.send(res_bytes)
    client.send(b'hello')
    
    count_len = len('world')
    res_bytes = struct.pack('i', count_len)
    client.send(res_bytes)
    client.send(b'world')
    client.close()
    

    第二种情况,就以ssh的tasklist命令为例,本来该命令由于太长,无法获取

    服务端

    import socket
    import subprocess
    import struct
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    phone.bind(("127.0.0.1", 8087))
    phone.listen(5)
    print('wait...')
    while True:
        conn, client_addr = phone.accept()
        print(client_addr)
    
        while True:
            try:
    
                cmd = conn.recv(1024)
                print(cmd)
    
                pipeline = subprocess.Popen(cmd.decode('utf-8'),
                                            shell=True,
                                            stderr=subprocess.PIPE,
                                            stdout=subprocess.PIPE)
    
                stdout = pipeline.stdout.read()
                stderr = pipeline.stderr.read()
    
                print(stderr)
                print(stdout)
                count_len = len(stderr) + len(stdout)
                res_len = struct.pack('i', count_len)
                # 同理,先发送字节流大小,再发送数据
                conn.send(res_len)
                conn.send(stdout + stderr)
            except ConnectionResetError:
                break
    
        conn.close()
    

    客户端

    import socket
    import struct
    
    phone = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    phone.connect(('127.0.0.1', 8087))
    
    while True:
        msg = input('please your enter msg')
        phone.send(msg.encode('utf-8'))
    
        count_len = phone.recv(4)
        res_len = struct.unpack('i', count_len)[0]
    
        data = phone.recv(res_len)
        print(data.decode('gbk'))
    
  • 相关阅读:
    原型链
    原型规则总结
    使用typeof能得到的哪些类型
    eslint 规则中文注释
    两张图片互相切换
    输入框获得焦点与失去焦点、阴影效果
    vue 写table的几种方式
    vue 注意事项
    angular 接口定义封装
    @NgModule
  • 原文地址:https://www.cnblogs.com/lucky75/p/11098937.html
Copyright © 2011-2022 走看看