• socket--粘包


    参考博客:http://www.cnblogs.com/kex1n/p/6502002.html

    一、粘包现象

      在上一篇的socket传输大数据文章中,我们可以顺利的接发数据,似乎做的不错,可以接收了。但是其实还隐藏着一个问题,请看下面的情况:

      

      我们看到出错的位置,具体分析一下是什么情况

      

      这个功能在上篇中介绍是用来判断数据量大小的,即服务端先将其要发送的数据量大小发给客户端,然后客户端再接收数据时,以此来判断是否接收了全部数据。

      我们在来看看服务端的代码: 

    # 服务端先将数据大小发送给客户端,用于对比
    conn.send(str(len('{}'.format(cmd_res).encode('utf-8'))).encode('utf-8'))
    # 发送数据
    conn.send('{}'.format(cmd_res).encode('utf-8'))
    

      第一句代码的作用是发送数据量大小,第二句则是发送数据本身。由错误我们可以判断出,第一次发送的数据当中,不仅仅是数据量大小,即数据长度,还包含了第二次发送的部分数据。所以str格式的字符当然没办法 int了,就报错了。但是为什么发送的数据会混淆呢,why?

    二、Socket粘包

      同志们,这就是粘包了,也就是前后发送的数据在接收端那边是合在一起的。粘包现象不一定每次都出现,我试了二三十次才终于出现了。那么粘包现象是怎么来的呢?我目前知道的还少,只能有一点说一点了,先做个笔记,再慢慢了解了。

      从现象上来看,粘包就是后一个数据紧跟着前一个数据的末尾被接收了,这样的影响就是如果两个包的数据格式不一样,分别要用来处理不同问题,比如上例中一个是int 一个是 str,就会出错。即便是数据格式一致,但前后数据连在一起,可能接收方就不知道其本意了。

      2.1 考虑出现粘包

      1)如果利用tcp每次发送数据,就与对方建立连接,然后双方发送完一段数据后,就关闭连接,这样就不会出现粘包问题。关闭连接主要是要双方都发送close连接,双方在数据发送和接收完毕后不再继续发送和接收新数据,而是双方关闭连接,这样就不用考虑粘包问题。

      2)如果发送数据无结构,如文件传输,这样发送方只管发送,接收方只管接收存储就ok,也不用考虑粘包

           3)UDP发送数据不用考虑

      4)如果双方建立连接,需要在连接后一段时间内发送不同结构数据,如连接后,有好几种结构,需要考虑粘包

      5)双方建立连接后,一段时间内不断发送数据,需要考虑粘包

      2.2 粘包原因

    1. 发送端需要等缓冲区满才发送出去,造成粘包
    2. 接收方不及时接收缓冲区的包,造成多个包接收 

      具体点:

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

      (2)接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

      粘包情况有两种,一种是粘在一起的包都是完整的数据包,另一种情况是粘在一起的包有不完整的包。

    三、如何解决粘包

      具体情况具体分析吧。我们还是以上例来处理

      3.1 利用超时

      socket将数据存入缓冲区后,TCP/IP协议会将数据发送给接收方,我们在两次连续发送数据时加入一个间隔时间,比如说: 

    conn.send(str(len('{}'.format(cmd_res).encode('utf-8'))).encode('utf-8'))
    
    time.sleep(0.5)   # 两次发送数据间隔一段时间
    # 发送数据
     conn.send('{}'.format(cmd_res).encode('utf-8', 'ignore'))
    

      这样确实能解决问题,但是显然有缺陷,你让老司机发车的时候,总是停停顿顿的,他还不砸了硬盘?哦不,是方向盘。

       3.2 接收端收到数据后回复一个确认

      利用socket中的recv()函数是阻塞的,在接收端不返回确认时,就不会继续发送数据。同样在两次发送数据之间添加这个功能,代码如下:

      服务端:

    # -*- coding: UTF-8 -*-
    import os
    import socket
    
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  # TCP/IP协议, tcp ,如果不填写就是默认这个
    
    server.bind(('localhost', 9999))
    
    server.listen()
    
    while True:  # 可以接受多个客户端
    
        conn, addr = server.accept()
    
        while True:
    
            data = conn.recv(1024)
            if not data:   # 防止当接受的客户端数据为空时,程序卡掉
                print('client has lost...')
                break
            print('执行命令:', data.decode())
            cmd_res = os.popen(data.decode()).read()
    
            if len(cmd_res) == 0:
                print('command not found')
            else:
                # 服务端先将数据大小发送给客户端,用于对比
                conn.send(str(len('{}'.format(cmd_res).encode('utf-8'))).encode('utf-8'))
                # 这里等待接收到返回
                clientACK = conn.recv(1024)
                # 发送数据
                conn.send('{}'.format(cmd_res).encode('utf-8', 'ignore'))
                print('发送完成')
    View Code

      客户端:

    # -*- coding: UTF-8 -*-
    import socket
    
    client = socket.socket()
    
    client.connect(('localhost', 9999))
    
    while True:
        cmd = input('>>:').strip()
        # 判断是否发送空数据,如果是就重新发送
        if len(cmd) == 0:
            continue
        else:
            client.send(cmd.encode('utf-8'))
            data_size = client.recv(1024)  # 接收服务端发送的数据大小
            print(data_size)
            data_length = int(data_size.decode())
            # 此处就是给服务端返回确认
            client.send('返回确认收到数据大小,以解决粘包'.encode('utf-8'))
            
            print('返回数据大小:', data_length)
            # 定义已接收数据大小为0
            received_length = 0
            # 定义已接收数据为0
            received_data = b''
            while received_length < data_length:
                r_data = client.recv(1024)  # 接受的数据是bytes类型
                received_length += len(r_data)
                received_data += r_data
            else:
                print('接收数据大小:', received_length)
                print(received_data.decode('utf-8', 'ignore'))   # 不加ignore在windows有时会报错
                print('数据接收完毕!')
    View Code

      其实改的不是很多,就添加了两行代码,但是效果杠杠的,方向盘是保住了。

      

  • 相关阅读:
    Linux基础知识
    redis info
    记录: 解决 pycurl: libcurl link-time ssl backend (openssl) is different from compile-time ssl backend (none/other)
    IOS IAP 自动续订 之 利用rabbitmq延时队列自动轮询检查是否续订成功
    Python3.6 的字典为什么会快
    IAP 订阅后端踩坑总结之 Google 篇
    docker 命令合集
    Python Schema使用说明
    Apache Bench测试
    channels2.X 学习笔记
  • 原文地址:https://www.cnblogs.com/bigberg/p/7749551.html
Copyright © 2020-2023  润新知