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

    粘包问题

    注意:只有TCP有粘包现象,UDP永远不会粘包.所谓粘包问题主要还是因为接收方不知道消息之间的界限,不知道一次性提取多少字节的数据所造成的。

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

    TCP发送数据的四种情况

    假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。

    1. 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
    2. 服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
    3. 服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
    4. 服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包。

    特例:如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。

    粘包的两种情况

    1.发送端需要等缓冲区满才发送出去,造成粘包(发送数据时间间隔很短,数据了很小,会合到一起,产生粘包)

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

    为何TCP是可靠传输,udp是不可靠传输

    • 基于TCP的数据传输请参考nickl老师的另一篇文章https://www.cnblogs.com/nickchen121/p/11027575.html,TCP在数据传输时,发送端先把数据发送到自己的缓存中,然后协议控制将缓存中的数据发往对端,对端返回一个ack=1,发送端则清理缓存中的数据,对端返回ack=0,则重新发送数据,所以TCP是可靠的
    • udp发送数据,对端是不会返回确认信息的,因此不可靠

    send(字节流)和recv(1024)及sendall

    • recv里指定的1024意思是从缓存里一次拿出1024个字节的数据
    • send的字节流是先放入己端缓存,然后由协议控制将缓存内容发往对端,如果待发送的字节流大小大于缓存剩余空间,那么数据丢失,用sendall就会循环调用send,数据不会丢失

    解决粘包问题

    问题的根源在于,接收端不知道发送端将要传送的字节流的长度,所以解决粘包的方法就是围绕,如何让发送端在发送数据前,把自己将要发送的字节流总大小让接收端知晓,然后接收端来一个死循环接收完所有数据。

    #服务端
    import socket, subprocess
    
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    server.bind(('127.0.0.1', 8000))
    server.listen(5)
    
    while True:
        conn, addr = server.accept()
    
        print('start...')
        while True:
            cmd = conn.recv(1024)
            print('cmd:', cmd)
    
            obj = subprocess.Popen(cmd.decode('utf8'),
                                   shell=True,
                                   stderr=subprocess.PIPE,
                                   stdout=subprocess.PIPE)
    
            stdout = obj.stdout.read()
    
            if stdout:
                ret = stdout
            else:
                stderr = obj.stderr.read()
                ret = stderr
    
            ret_len = len(ret)
    
            conn.send(str(ret_len).encode('utf8'))
    
            data = conn.recv(1024).decode('utf8')
    
            if data == 'recv_ready':
                conn.sendall(ret)
    
        conn.close()
    
    server.close()
    
    #客户端
    import socket
    
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
    client.connect(('127.0.0.1', 8000))
    
    while True:
        msg = input('please enter your cmd you want>>>').strip()
    
        if len(msg) == 0: continue
    
        client.send(msg.encode('utf8'))
        length = int(client.recv(1024))
    
        client.send('recv_ready'.encode('utf8'))
    
        send_size = 0
        recv_size = 0
    
        data = b''
    
        while recv_size < length:
            data = client.recv(1024)
            recv_size += len(data)
    
        print(data.decode('utf8'))
        
        #此版比较low,程序的运行速度远快于网络传输速度,所以在发送一段字节前,先用send去发送该字节流长度,这种方式会放大网络延迟带来的性能损耗
    
    
    
    解决粘包问题的核心就是:为字节流加上自定义固定长度报头,报头中包含字节流长度,然后一次send到对端,对端在接收时,先从缓存中取出定长的报头,然后再取真实数据
    
    具体版本详情请看 nick老师的博客 https://www.cnblogs.com/nickchen121/p/11032005.html
    

    基于UDP协议的socket 套接字编程

    • UPD协议一般不用于传输大数据。

    • UDP套接字虽然没有粘包问题,但是不能替代TCP套接字,因为UPD协议有一个缺陷:如果数据发送的途中,数据丢失,则数据就丢失了,而TCP协议则不会有这种缺陷,因此一般UPD套接字用户无关紧要的数据发送,例如qq聊天

      #服务器
      #_*_coding:utf-8_*_
      __author__ = 'nick'
      import socket
      ip_port = ('127.0.0.1', 8081)
      UDP_server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)  #买手机
      UDP_server_sock.bind(ip_port)
      
      while True:
          qq_msg, addr = UDP_server_sock.recvfrom(1024)
          print('来自[%s:%s]的一条消息:33[1;44m%s33[0m' %
                (addr[0], addr[1], qq_msg.decode('utf-8')))
          back_msg = input('回复消息: ').strip()
      
          UDP_server_sock.sendto(back_msg.encode('utf-8'), addr)
         
      # 客户端1
      #_*_coding:utf-8_*_
      __author__ = 'nick'
      import socket
      BUFSIZE = 1024
      UDP_client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
      
      qq_name_dic = {
          '狗哥alex': ('127.0.0.1', 8081),
          '瞎驴': ('127.0.0.1', 8081),
          '一棵树': ('127.0.0.1', 8081),
          '武大郎': ('127.0.0.1', 8081),
      }
      
      while True:
          qq_name = input('请选择聊天对象: ').strip()
          while True:
              msg = input('请输入消息,回车发送: ').strip()
              if msg == 'quit': break
              if not msg or not qq_name or qq_name not in qq_name_dic: continue
              UDP_client_socket.sendto(msg.encode('utf-8'), qq_name_dic[qq_name])
      
              back_msg, addr = UDP_client_socket.recvfrom(BUFSIZE)
              print('来自[%s:%s]的一条消息:33[1;44m%s33[0m' %
                    (addr[0], addr[1], back_msg.decode('utf-8')))
      
      UDP_client_socket.close()
      
      #客户端2
      #_*_coding:utf-8_*_
      __author__ = 'nick'
      import socket
      BUFSIZE = 1024
      UDP_client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
      
      qq_name_dic = {
          '狗哥alex': ('127.0.0.1', 8081),
          '瞎驴': ('127.0.0.1', 8081),
          '一棵树': ('127.0.0.1', 8081),
          '武大郎': ('127.0.0.1', 8081),
      }
      
      while True:
          qq_name = input('请选择聊天对象: ').strip()
          while True:
              msg = input('请输入消息,回车发送: ').strip()
              if msg == 'quit': break
              if not msg or not qq_name or qq_name not in qq_name_dic: continue
              UDP_client_socket.sendto(msg.encode('utf-8'), qq_name_dic[qq_name])
      
              back_msg, addr = UDP_client_socket.recvfrom(BUFSIZE)
              print('来自[%s:%s]的一条消息:33[1;44m%s33[0m' %
                    (addr[0], addr[1], back_msg.decode('utf-8')))
      
      UDP_client_socket.close()
      

    基于socketserver实现并发的socket套接字编程

  • 相关阅读:
    171 01 Android 零基础入门 03 Java常用工具类02 Java包装类 01 包装类简介 01 Java包装类内容简介
    170 01 Android 零基础入门 03 Java常用工具类01 Java异常 08 Java异常总结 01 异常总结
    169 01 Android 零基础入门 03 Java常用工具类01 Java异常 07 异常链 01 异常链简介
    168 01 Android 零基础入门 03 Java常用工具类01 Java异常 06 自定义异常 01 自定义异常类
    167 01 Android 零基础入门 03 Java常用工具类01 Java异常 05 使用throw和throws实现异常处理 02 使用throw抛出异常对象
    166 01 Android 零基础入门 03 Java常用工具类01 Java异常 05 使用throw和throws实现异常处理 01 使用throws声明异常类型
    165 01 Android 零基础入门 03 Java常用工具类01 Java异常 04 使用try…catch…finally实现异常处理 05 return关键字在异常处理中的使用
    DevExpress WPF v20.2版本亮点放送:全新升级的PDF Viewer
    界面控件DevExpress使用教程:Dashboard – 自定义导出
    DevExpress WinForms帮助文档:表单控件
  • 原文地址:https://www.cnblogs.com/bladecheng/p/11099187.html
Copyright © 2011-2022 走看看