zoukankan      html  css  js  c++  java
  • 网络编程基础之粘包现象与UDP协议

    一、粘包现象原理分析

      1、我们先来看几行代码,从现象来分析:

        测试程序分为两部分,分别是服务端和客户端

        服务端.py 

     1 #!/usr/bin/env python3
     2 #-*- coding:utf-8 -*-
     3 # write by congcong
     4 
     5 
     6 import socket
     7 
     8 server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
     9 
    10 server.bind(('127.0.0.1',3306))
    11 
    12 server.listen(5)
    13 
    14 conn,client_addres = server.accept()
    15 
    16 # data1 = conn.recv(1024)
    17 # print('客户端消息1:',data1) # 客户端消息1: b'helloworld'
    18 # data2 = conn.recv(1024)
    19 # print('客户端消息2:',data2) # 客户端消息2: b''  客户端两次发送的信息被服务端第一次就全部接受了,即粘包

      客户端.py

     1 #!/usr/bin/env python3
     2 #-*- coding:utf-8 -*-
     3 # write by congcong
     4 
     5 import socket
     6 
     7 client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
     8 
     9 client.connect(('127.0.0.1',3306))
    10 
    11 # client.send('hello'.encode('utf-8'))
    12 # client.send('world'.encode('utf-8'))
    13 #
    14 # data1 = client.recv(3) # 接受3个字节的信息
    15 # print('服务端消息1:',data1) # 服务端消息1: b'hel'
    16 # data2 = client.recv(1024)
    17 # print('服务端消息2:',data2) # 服务端消息2: b'loserver'  客户端两次接收信息混在一起,也发生了粘包

      由以上程序代码,我们不难发现,粘包发生的情况主要有两种:一种是第一次接收的字节数据小于发送的数据量,再次接收时,便会粘包;另一种是第

    一次准备接收的字节数超过了发送的数据量,再次发送数据时,便会和第一次数据累积在一起,造成粘包。

      2、那么这到底是原因导致的呢?--->原因在于内部机制。

        主要有以下几点: 

    1、不管 recv 还是 send 都不是直接接受对方的数据,而是操作自己的操作系统内存。
        --> 不是一定一个send对应一个recv(可以1对n,或者n对1)
    
    2、recv:
            wait data 耗时长(一是等待程序发送,二是网络延迟)
       send:
            copy data 时间短
    3、优化算法(Nagle算法),将多次时间间隔较短且数据量小的数据,合并成一个大的数据块, 然后封包发送。这样接收方就收到了粘包数据。

        从原理上看:程序实际上无权直接操作网卡的,你操作网卡都是通过操作系统给用户程序暴露出来的接口,那每次你的程序要给远程发数据时,其实是先把数据从用户态copy到内核态,这样的操作是耗资源和时间的,频繁的在内核态和用户态之前交换数据势必会导致发送效率降低, 因此socket 为提高传输效率,发送方往往要收集到足够多的数据后才发送一次数据给对方。若连续几次需要send的数据都很少,通常TCP socket 会根据优化算法把这些数据合成一个TCP段后一次发送出去,这样接收方就收到了粘包数据。

        如下图所示:

        

        那么,我们有什么解决方法没呢?

        答案肯定是有的,而且有好几种,接着往下看,任君挑选!

    二、解决粘包问题的几种方法

        按照方法的适用范围,可以分为以下几个阶段:

        1、凡人阶段

          粘包最简单解决方法,接收信息前事先已知信息的长度,指定接收长度。 

          服务端.py   

    #!/usr/bin/env python3
    #-*- coding:utf-8 -*-
    # write by congcong
    
    
    import socket
    
    server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
    
    server.bind(('127.0.0.1',3306))
    
    server.listen(5)
    
    conn,client_addres = server.accept()
    
    conn.send('hello'.encode('utf-8'))
    conn.send('server'.encode('utf-8'))

          客户端.py

    #!/usr/bin/env python3
    #-*- coding:utf-8 -*-
    # write by congcong
    
    import socket
    
    client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
    
    client.connect(('127.0.0.1',3306))
    
    data1 = client.recv(5)
    print('服务端消息1:',data1) # 服务端消息1: b'hello'
    data2 = client.recv(1024)
    print('服务端消息2:',data2) # 服务端消息2: b'server'

        2、修仙阶段

          预备知识了解

    #!/usr/bin/env python3
    #-*- coding:utf-8 -*-
    # write by congcong
    
    import struct
    
    # struct.pack() 封装固定长度的报头,封装后的类型为bytes类型
    res = struct.pack('i',2056) # i 表示数据类型,2056 表示真实数据的长度
    print(res,type(res),len(res)) # b'x08x08x00x00' <class 'bytes'> 4
    res2 = struct.pack('i',1024)
    print(res2,type(res2),len(res2)) # b'x00x04x00x00' <class 'bytes'> 4
    
    # struct.unpack() 解析报头数据,解析后的数据存储在一个元组内
    data = struct.unpack('i',res) # i 仍然为数据类型,res为封装的包头
    # print(data) # (2056,)
    print(data[0]) # 2056 获取真实数据的长度

        服务端.py

    #!/usr/bin/env python3
    #-*- coding:utf-8 -*-
    # write by congcong
    
    import socket
    import subprocess
    import struct
    server = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM) # 实例化一个套接字对象
    server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 端口重用
    server.bind(('127.0.0.1',3231))  #  绑定IP和端口
    server.listen(5)  # 监听
    
    while True: # 链接循环
        conn,client_addres = server.accept()
        while True: # 通信循环
            try: # windows适用
                #1 接收命令
                cmd = conn.recv(8096)
                if not cmd:break # linux适用(未收到命令)
                # 2 执行命令,拿到结果
                obj = subprocess.Popen(cmd.decode('gbk'),shell=True,
                                       stdout=subprocess.PIPE, # 命令正确时创建一条通道
                                       stderr=subprocess.PIPE) # 命令错误时创建另一条通道
                stdout = obj.stdout.read() # 命令正确执行返回的信息
                stderr = obj.stderr.read() # 命令错误时返回的信息
                # print('from client',cmd)
                # 3 把结果返回给客户端
                # 3.1 制定固定长度的报头
                total_size = len(stdout) + len(stderr)
                header = struct.pack('i',total_size)
                # 3.2 发送报头信息
                conn.send(header)
                # 3.3 发送真实数据信息
                conn.send(stdout)
                conn.send(stderr)
                # conn.send('from server'.encode('utf-8')) # 发命令
    
            except ConnectionResetError:
                break
        conn.close()
    
    server.close()
    View Code

        客户端.py

    #!/usr/bin/env python3
    #-*- coding:utf-8 -*-
    # write by congcong
    import socket
    import struct
    
    client = socket.socket(family=socket.AF_INET,type=socket.SOCK_STREAM)
    
    client.connect(('127.0.0.1',3231))
    
    while True: # 通信循环
        cmd = input('输入命令:').strip() # 输入命令
        if not cmd:continue
        client.send(cmd.encode('gbk')) # 发送命令
        # 1、接收报头信息
        header = client.recv(4)
        # 2、获取真实数据的长度
        total_size = struct.unpack('i',header)[0]
        recv_size = 0   # 初始化接收的信息长度
        recv_data = b'' # 初始化接收的数据
        while recv_size < total_size: # 判断已接收的数据长度是否大于需要接收的数据
            data = client.recv(1024)   # 每次接收的数据量
            recv_data += data       # 已经接收的总数居
            recv_size += len(data)  # 已经统计的数据长度
        print(recv_data.decode('gbk'))
    
    client.close()
    View Code

        3、成仙阶段

          服务端.py

    #!/usr/bin/env python3
    #-*- coding:utf-8 -*-
    # write by congcong
    
    import socket
    import subprocess
    import struct
    import json
    
    server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
    server.bind(('127.0.0.1',3301))
    server.listen(5)
    
    while True:
        conn,client_adress = server.accept()
        while True:
            try:
                cmd = conn.recv(1024)
                obj = subprocess.Popen(cmd.decode('gbk'),shell=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
                stdout = obj.stdout.read()
                stderr = obj.stderr.read()
                # 把执行结果返回给客户端
                # 1、制定固定长度的报头
                head_dic = {
                    'file_name':'head.txt',
                    'md5':'xxxxxxxx',
                    'total_size':len(stdout)+len(stderr)
                } # 报头
                header_json = json.dumps(head_dic)  # json转成字符串
                header_bytes = header_json.encode('gbk') # 字符串转成bytes类型
                # 2、先发送报头长度
                conn.send(struct.pack('i',len(header_bytes)))
                # 3、发送报头
                conn.send(header_bytes)
                # 4、发送执行后的信息
                conn.send(stdout)
                conn.send(stderr)
            except ConnectionResetError:
                break
        conn.close()
    
    server.close()
    View Code

          客户端.py

    #!/usr/bin/env python3
    #-*- coding:utf-8 -*-
    # write by congcong
    
    import socket
    import struct
    import json
    
    client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    
    client.connect(('127.0.0.1',3301))
    
    while True:
        cmd = input('输入命令>>:').strip()
        if not cmd:continue
        client.send(cmd.encode('gbk'))
        # 获取服务器返回的信息
        # 1、获取报头长度
        obj = client.recv(4)
        header_size = struct.unpack('i',obj)[0]
        # 2、获取报头
        header_bytes = client.recv(header_size)
        # 3、从报头解析出对真实数据的描述(数据长度)
        header_json = header_bytes.decode('gbk') # 转成字符串
        header_dic = json.loads(header_json)  # 转成报头原数据类型
        total_size = header_dic['total_size'] # 获取真实数据长度
        # 4、获取真实数据
        recv_size = 0
        recv_data = b''
        while recv_size < total_size:
            data = client.recv(1024)
            recv_data += data
            recv_size += len(data)
        print(recv_data.decode('gbk'))
    
    client.close()
    View Code

    三、UDP协议 

      1、含义: UDP(user datagram protocol,用户数据报协议)是无连接的,面向消息的,提供高效率服务的协议。  

      2、特点:不使用块的合并优化算法,, 由于UDP支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了。 即面向消息的通信是有消息保护边界的。UDP是面向消息的协议,每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据,这也决定了UDP不存在粘包现象。

      3、与TCP协议的区别

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

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

      4、UDP通信示例:

        服务端.py

    #!/usr/bin/env python3
    #-*- coding:utf-8 -*-
    # write by congcong
    
    import socket
    server = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM)
    server.bind(('127.0.0.1',3361))
    
    while True:
        res,client_adds = server.recvfrom(1024)
        print(res,client_adds)
        server.sendto('hello,from server!'.encode('gbk'),client_adds)

        客户端.py

    #!/usr/bin/env python3
    #-*- coding:utf-8 -*-
    # write by congcong
    
    import socket
    client = socket.socket(family=socket.AF_INET,type=socket.SOCK_DGRAM)
    while True:
        res = input('cmd:').strip()
        client.sendto(res.encode('gbk'),('127.0.0.1',3361))
        data,adds = client.recvfrom(1024)
        print("from server:",data,adds)

      5、测试UDP通信是否粘包

        服务端.py   

    #!/usr/bin/env python3
    #-*- coding:utf-8 -*-
    # write by congcong
    
    from socket import *
    
    server = socket(AF_INET,SOCK_DGRAM)
    
    server.bind(('127.0.0.1',3321))
    
    data1 = server.recvfrom(1024)
    print('data1>>:',data1) # data1>>: (b'this is data1', ('127.0.0.1', 62204))
    data2 = server.recvfrom(1024)
    print('data2>>:',data2) # data2>>: (b'this is data2', ('127.0.0.1', 62204))
    View Code

        客户端.py

    #!/usr/bin/env python3
    #-*- coding:utf-8 -*-
    # write by congcong
    
    from socket import *
    
    client = socket(AF_INET,SOCK_DGRAM)
    
    mes = input('>>:').strip()
    client.sendto(mes.encode('gbk'),('127.0.0.1',3321))
    mes = input('>>:').strip()
    client.sendto(mes.encode('gbk'),('127.0.0.1',3321))
    client.close()
    
    '''
        UDP协议不会发生粘包现象,不需要建立链接,所以发送信息时需要指定IP和端口,
    接收信息时会返回发送方的IP和端口。
    '''
    View Code
  • 相关阅读:
    Delphi中WebBbrowser的编程 转
    博客园设置目录
    iTerm
    python
    谷歌浏览器插件的导出导入
    Chapter10 属性
    WPF之Binding
    ASP.NET 路由系统
    Silverlight中使用Application.GetResourceStream方法加载资源时得到的总是null
    基于IoC的ControllerFactory
  • 原文地址:https://www.cnblogs.com/schut/p/8672447.html
Copyright © 2011-2022 走看看