zoukankan      html  css  js  c++  java
  • 20 网络编程 粘包现象与解决方案

    1、什么是粘包现象

     当发送网络数据时,tcp协议会根据Nagle算法将时间间隔短,数据量小的多个数据包打包成一个数据包,先发送到自己操作系统的缓存中,

    然后操作系统将数据包发送到目标程序所对应操作系统的缓存中,最后将目标程序从缓存中取出,而第一个数据包的长度,应用程序并不知道。

    所以会直接取出数据或者取出部分数据,留部分数据在缓存中,取出的数据可能第一个数据包和第二个数据包粘到一起。

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

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

    参考:https://www.cnblogs.com/liuwei0824/p/7413463.html

    1.发送数据时间间隔很短,数据了很小,会合到一起,产生粘包

     import socket
     import time
     phone=socket(AF_INET,SOCK_STREAM)
     phone.connect(('127.0.0.1',8080))
    
     phone.send('helloworld'.encode('utf-8'))
     phone.send('egon'.encode('utf-8'))

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

    总结
    
    TCP(transport control protocol,传输控制协议)是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket,
    因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。
    这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。 即面向流的通信是无消息保护边界的。

    UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务。不会使用块的合并优化算法,,
    由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,
    在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。

    tcp是基于数据流的,于是收发的消息不能为空,这就需要在客户端和服务端都添加空消息的处理机制,防止程序卡住,
    而udp是基于数据报的,即便是你输入的是空内容(直接回车),那也不是空消息,udp协议会帮你封装上消息头,实验略

    3、粘包的解决方案

    由于应用程序自己发送的数据可以进行打包处理,自己制作协议,对数据进行封装添加报头,然后发送数据部分。

    而报头必须是固定长度,对方接受时可以先接受报头,对报头进行解析,然后根据报头内的封装的数据的长度对数据进行读取,这样收取的数据就是一个完整的数据包

    发送数据包前 对包的长度进行计算

    1. 比较low的方法是 len( package) 然后直接发送给接收端。这样会出现一个问题,就是接收端不知道你的这个 len(package)是几个字节,就也有可能会出现粘包问题。

    2. 利用struct对包的长度打包为固定4个字节或8个字节。

    3. struct.pack format参数为"i" 时只能打包长度为10的数字,那么还可以先将 长度 转换为一个json字符串,再打包。

    服务端:

    import socket
    import subprocess
    import struct
    import json
    
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    # phone.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    phone.bind(('127.0.0.1',9909)) #0-65535:0-1024给操作系统使用
    phone.listen(5)
    
    print('starting...')
    while True: # 链接循环
        conn,client_addr=phone.accept()
        print(client_addr)
    
        while True: #通信循环
            try:
                #1、收命令
                cmd=conn.recv(8096)
                if not cmd:break #适用于linux操作系统
    
                #2、执行命令,拿到结果
                obj = subprocess.Popen(cmd.decode('utf-8'), shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
    
                stdout=obj.stdout.read()
                stderr=obj.stderr.read()
    
                #3、把命令的结果返回给客户端
                #第一步:制作固定长度的报头
                header_dic={
                    'filename':'a.txt',
                    'md5':'xxdxxx',
                    'total_size': len(stdout) + len(stderr)
                }
    
                header_json=json.dumps(header_dic)
    
                header_bytes=header_json.encode('utf-8')
    
                #第二步:先发送报头的长度
                conn.send(struct.pack('i',len(header_bytes))) # len(header_bytes)发送信息给客户端的字节长度
    
                #第三步:再发报头
                conn.send(header_bytes)  # 客户端发两次
    
                #第四步:再发送真实的数据
                conn.send(stdout)
                conn.send(stderr)
    
            except ConnectionResetError: #适用于windows操作系统
                break
        conn.close()
    
    phone.close()

    客户端:

    import socket
    import struct
    import json
    
    phone=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    phone.connect(('127.0.0.1',9909))
    
    while True:
        #1、发命令
        cmd=input('>>: ').strip() #ls /etc
        if not cmd:continue
        phone.send(cmd.encode('utf-8'))
    
        #2、拿命令的结果,并打印
    
        #第一步:先收报头的长度
        obj=phone.recv(4)  #接收服务端传来的  struct.pack('i',len(header_bytes))
    
        header_size=struct.unpack('i',obj)[0] # 解包--得到服务端传给客户端   header_dic字典字节的长度
    
        #第二步:再收报头
        header_bytes=phone.recv(header_size) #   header_size为上一步已经算好的字典字节长度
        # header_bytes 为 接收客户端第二次发过来的header_dic字典转化的成的字节数据
    
    
    
        #第三步:从报头中解析出对真实数据的描述信息
        header_json=header_bytes.decode('utf-8') # class---> str类型
        header_dic=json.loads(header_json)   # 反序列化 服务端原先的 字典
        print(header_dic)
        total_size=header_dic['total_size'] # 服务端的执行后返回给客户端的字节流长度
    
        #第四步:接收真实的数据
        recv_size=0
        recv_data=b''
        while recv_size < total_size:
            res=phone.recv(1024) #1024是一个坑
            recv_data+=res
            recv_size+=len(res)
    
        print(recv_data.decode('gbk'))
    
    phone.close()
  • 相关阅读:
    2012年6月30号PMP考试总结
    安装了VS2010 sp1 后再安装ASP.NET MVC 3.0的问题(Final Result: Installation failed with error code: (0x80070643), "安装时发生严重错误 " (Ela)
    黄光裕方面独家点评国美中报:领先优势即将丧失
    Asp.net 模板页(MasterPage)里的 js脚本路径
    Windows 7 Sp1 x64 无法安装英文语言包(已解决)
    The issue of vertical lines throughing the Report footer or the whole page in Crystal report 11
    Windows Server 2008 R2 with Service Pack 1
    垃圾农业银行网银https://www.95599.cn/PersonalBank/startUpHtmlSessionAction.ebf
    如何填写PMP审计表单,Audit注意事项
    被日军飞机炸毁的上海南火车站铁轨上拍摄了一个父母刚被炸死、正在铁轨上深哀啼号的婴儿
  • 原文地址:https://www.cnblogs.com/foremostxl/p/9657723.html
Copyright © 2011-2022 走看看