zoukankan      html  css  js  c++  java
  • Python网络编程04 /recv工作原理、展示收发问题、粘包现象

    Python网络编程04 /recv工作原理、展示收发问题、粘包现象

    1. recv工作原理

    • 源码解释:

      Receive up to buffersize bytes from the socket.
      # 接收来自socket缓冲区的字节数据,
      For the optional flags argument, see the Unix manual.
      # 对于这些设置的参数,可以查看Unix手册。
      When no data is available, block untilat least one byte is available or until the remote end is closed.
      # 当缓冲区没有数据可取时,recv会一直处于阻塞状态,直到缓冲区至少有一个字节数据可取,或者远程端关闭。
      When the remote end is closed and all data is read, return the empty string.
      # 关闭远程端并读取所有数据后,返回空字符串。
      
    • 验证recv工作原理

      1.验证服务端缓冲区数据没有取完,又执行了recv执行,recv会继续取值

      # server服务端
      import socket
      
      server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
      server.bind(('127.0.0.1',8080))
      server.listen(5)
      
      conn, client_addr = server.accept()
      from_client_data1 = conn.recv(2)
      print(from_client_data1)
      from_client_data2 = conn.recv(2)
      print(from_client_data2)
      from_client_data3 = conn.recv(1)
      print(from_client_data3)
      
      conn.close()
      server.close()
      # client客户端
      import socket
      client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
      client.connect(('127.0.0.1',8080))
      client.send('hello'.encode('utf-8'))
      client.close()
      

      2.验证服务端缓冲区取完了,又执行了recv执行,此时客户端20秒内不关闭的前提下,recv处于阻塞状态

      # server服务端
      import socket
      
      server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
      server.bind(('127.0.0.1',8080))
      server.listen(5)
      
      conn, client_addr = server.accept()
      from_client_data = conn.recv(1024)
      print(from_client_data)
      print(111)
      conn.recv(1024)  # 此时程序阻塞20秒左右,因为缓冲区的数据取完了,并且20秒内,客户端没有关闭。
      print(222)
      
      conn.close()
      server.close()
      
      # client客户端
      import socket
      import time
      client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
      client.connect(('127.0.0.1',8080))
      client.send('hello'.encode('utf-8'))
      time.sleep(20)
      
      client.close()
      

      3.验证服务端缓冲区取完了,又执行了recv执行,此时客户端处于关闭状态,则recv会取到空字符串

      # server服务端
      import socket
      
      server = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
      server.bind(('127.0.0.1',8080))
      server.listen(5)
      
      conn, client_addr = server.accept()
      from_client_data1 = conn.recv(1024)
      print(from_client_data1)
      from_client_data2 = conn.recv(1024)
      print(from_client_data2)
      from_client_data3 = conn.recv(1024)
      print(from_client_data3)
      
      conn.close()
      server.close()
      
      # client客户端
      import socket
      import time
      client = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
      client.connect(('127.0.0.1',8080))
      client.send('hello'.encode('utf-8'))
      client.close()
      
      # recv空字符串: 对方客户端关闭了,且服务端的缓冲区没有数据了,我再recv取到空bytes.
      

    2. 展示收发问题示例

    • 发多次收一次

      # server服务端
      import socket
      
      server = socket.socket()
      server.bind(('127.0.0.1',8848))
      server.listen(5)
      
      conn,addr = server.accept()
      
      from_client_data = conn.recv(1024)
      print(f'来自客户端{addr}消息:{from_client_data.decode("utf-8")}')
      
      conn.close()
      server.close()
      
      # client客户端
      import socket
      
      phone = socket.socket()
      phone.connect(('127.0.0.1',8848))
      
      phone.send(b'he')
      phone.send(b'llo')
      
      phone.close()
      
    • 发一次收多次

      # server服务端
      import socket
      
      server = socket.socket()
      server.bind(('127.0.0.1',8848))
      server.listen(5)
      
      conn,addr = server.accept() 
      
      from_client_data = conn.recv(3)
      print(f'来自客户端{addr}消息:{from_client_data.decode("utf-8")}')
      
      from_client_data = conn.recv(3)
      print(f'来自客户端{addr}消息:{from_client_data.decode("utf-8")}')
      
      from_client_data = conn.recv(3)
      print(f'来自客户端{addr}消息:{from_client_data.decode("utf-8")}')
      
      from_client_data = conn.recv(3)
      print(f'来自客户端{addr}消息:{from_client_data.decode("utf-8")}')
      
      conn.close()
      server.close()
      
      # client客户端
      import socket
      
      client = socket.socket()
      client.connect(('127.0.0.1',8848))
      
      client.send(b'hello world')
      
      client.close()
      

    3. 粘包现象

    • 粘包现象概述:

      发送方发送的若干包数据到接收方接收时粘成一包,从接收缓冲区看,后一包数据的头紧接着前一包数据的尾
      
    • 粘包第一种:

      send的数据过大,大于对方recv的上限时,对方第二次recv时,会接收上一次没有recv完的剩余的数据。

      # server服务端
      import socket
      import subprocess
      
      server = socket.socket()
      server.bind(('127.0.0.1',8848))
      server.listen(2)
      
      while 1:
          server,addr = server.accept()  # 等待客户端链接我,阻塞状态中
      
          while 1:
              try:
                  from_client_data = conn.recv(1024)
                  if from_client_data.upper() == b'Q':
                      print('客户端正常退出聊天了')
                      break
      
                  obj = subprocess.Popen(from_client_data.decode('utf-8'),
                                         shell=True,
                                         stdout=subprocess.PIPE,
                                         stderr=subprocess.PIPE,
      
                                         )
                  result = obj.stdout.read() + obj.stderr.read()
                  print(f'总字节数:{len(result)}')
                  conn.send(result)
              except ConnectionResetError:
                  print('客户端链接中断了')
                  break
          conn.close()
      server.close()
      
      
      # client客户端
      import socket
      
      client = socket.socket()
      client.connect(('127.0.0.1',8848))
      
      while 1:
          to_server_data = input('>>>输入q或者Q退出').strip().encode('utf-8')
          if not to_server_data:
              print('发送内容不能为空')
              continue
          client.send(to_server_data)
          
          if to_server_data.upper() == b'Q':
              break
              
          from_server_data = client.recv(300)  # 最多接受1024字节
          # 这里可能出现一个字符没有接受完整,进而在解码的时候会报错
          # print(f'{from_server_data.decode("gbk")}')
          print(len(from_server_data))
      
      client.close()
      
    • 粘包第二种:

      连续短暂的send多次(数据量很小),的数据会统一发送出去.

      # server服务端
      import socket
      
      server = socket.socket()
      server.bind(('127.0.0.1',8848))
      server.listen(5)
      
      conn,addr = server.accept()
      
      from_client_data = conn.recv(1024)
      print(f'来自客户端{addr}消息:{from_client_data.decode("utf-8")}')
      conn.close()
      server.close()
      
      # client客户端
      import socket
      
      client = socket.socket()
      client.connect(('127.0.0.1',8848))
      
      client.send(b'he')
      client.send(b'll')
      client.send(b'o')
      
      client.close()
      
      # Nagle算法:就是为了尽可能发送大块数据,避免网络中充斥着许多小数据块。
      # Nagle算法的规则:
      # 1.如果包长度达到MSS,则允许发送;
      # 2.如果该包含有FIN,则允许发送;
      # 3.设置了TCP_NODELAY选项,则允许发送;
      # 4.未设置TCP_CORK选项时,若所有发出去的小数据包(包长度小于MSS)均被确认,则允许发送;
      # 5.上述条件都未满足,但发生了超时(一般为200ms),则立即发送。
      

    3. 解决粘包现象

    • 解决粘包现象的思路:

      服务端发一次数据 5000字节,  
      客户端接收数据时,循环接收,每次(至多)接收1024个字节,直至将所有的字节全部接收完毕.将接收的数据拼接在一起,最后解码.
      
      1. 遇到的问题: recv的次数无法确定.
         发送总具体数据之前,先发一个总数据的长度:5000个字节。然后在发送总数据。
         客户端: 先接收一个总数据的长度,再根据总数据的长度接收相应长度的字节。
         然后再循环recv 控制循环的条件就是只要接收的数据< 5000 一直接收。
          
      2. 遇到的问题: 如何将总数据的长度转化成固定的字节数
      
      3.将不固定长度的int类型转化成固定长度的bytes并且还可以翻转回来:struct模块
      

      struct

    • instruct模块的使用:

      import struct
      
      # 将一个数字转化成等长度的bytes类型。
      ret = struct.pack('i', 183346)
      print(ret, type(ret), len(ret))
      # 结果:b'2xccx02x00' <class 'bytes'> 4
      
      # 通过unpack反解回来
      ret1 = struct.unpack('i',ret)[0]
      print(ret1, type(ret1))
      # 结果:183346 <class 'int'>
      
      
      # 但是通过struct 处理不能处理太大
      ret = struct.pack('l', 4323241232132324)
      print(ret, type(ret), len(ret))  # 报错
      # struct.error: argument out of range
      

    4. low版解决粘包现象

    • server服务端

      import socket
      import subprocess
      import struct
      
      server = socket.socket()
      server.bind(('127.0.0.1',8848))
      server.listen(2)
      
      while 1:
          conn,addr = server.accept()
          while 1:
              try:
                  from_client_data = conn.recv(1024)  # 接收命令
      
                  if from_client_data.upper() == b'Q':
                      print('客户端正常退出聊天了')
                      break
      
                  obj = subprocess.Popen(from_client_data.decode('utf-8'),
                                         shell=True,
                                         stdout=subprocess.PIPE,
                                         stderr=subprocess.PIPE,
      
                                         )
                  result = obj.stdout.read() + obj.stderr.read()
                  total_size = len(result)
                  
                  print(f'总字节数:{total_size}')
      
                  # 1. 制作固定长度的报头
                  head_bytes = struct.pack('i',total_size)
      
                  # 2. 发送固定长度的报头
                  conn.send(head_bytes)
      
                  # 3. 发送总数据
                  conn.send(result)
              except ConnectionResetError:
                  print('客户端链接中断了')
                  break
          conn.close()
      server.close()
      
    • client客户端

      import socket
      import struct
      
      phone = socket.socket()
      phone.connect(('127.0.0.1',8848))
      
      while 1:
          to_server_data = input('>>>输入q或者Q退出').strip().encode('utf-8')
          
          if not to_server_data:
              print('发送内容不能为空')
              continue
          phone.send(to_server_data)
          
          if to_server_data.upper() == b'Q':
              break
      
          # 1. 接收报头
          head_bytes = phone.recv(4)
          
          # 2. 反解报头
          total_size = struct.unpack('i',head_bytes)[0]
      
          total_data = b''
          while len(total_data) < total_size:
              total_data += phone.recv(1024)
      
          print(len(total_data))
          print(total_data.decode('gbk'))
      
      phone.close()
      

    5. 高级版解决粘包方式(自定制报头)

    • 解决思路

      制作固定的报头,现在有两段不固定长度的bytes类型,需要固定的报头,所以
      1. 获取不固定报头的长度,总数据的长度放在报头中(字典)
      2. 利用struct模块将不固定的长度转化成固定的字节数 4个字节
      3. 先发4个字节,再发报头数据,再发总数据
      
    • server服务端

      import socket
      import subprocess
      import struct
      import json
      
      server = socket.socket()
      server.bind(('127.0.0.1',8848))
      server.listen(2)
      
      while 1:
          conn,addr = server.accept()  
      
          while 1:
              try:
      
                  from_client_data = conn.recv(1024)  # 接收命令
      
      
                  if from_client_data.upper() == b'Q':
                      print('客户端正常退出聊天了')
                      break
      
                  obj = subprocess.Popen(from_client_data.decode('utf-8'),
                                         shell=True,
                                         stdout=subprocess.PIPE,
                                         stderr=subprocess.PIPE,
      
                                         )
                  result = obj.stdout.read() + obj.stderr.read()
                  total_size = len(result)
      
                  # 1. 自定义报头
                  head_dic = {
                      'file_name': 'test1',
                      'md5': 6567657678678,
                      'total_size': total_size,
      
                  }
                  # 2. json形式的报头
                  head_dic_json = json.dumps(head_dic)
      
                  # 3. bytes形式报头
                  head_dic_json_bytes = head_dic_json.encode('utf-8')
      
                  # 4. 获取bytes形式的报头的总字节数
                  len_head_dic_json_bytes = len(head_dic_json_bytes)
      
                  # 5. 将不固定的int总字节数变成固定长度的4个字节
                  four_head_bytes = struct.pack('i',len_head_dic_json_bytes)
      
                  # 6. 发送固定的4个字节
                  conn.send(four_head_bytes)
      
                  # 7. 发送报头数据
                  conn.send(head_dic_json_bytes)
      
                  # 8. 发送总数据
                  conn.send(result)
      
              except ConnectionResetError:
                  print('客户端链接中断了')
                  break
          conn.close()
      server.close()
      
    • client客户端

      import socket
      import struct
      import json
      
      client = socket.socket()
      client.connect(('127.0.0.1',8848))
      
      while 1:
          to_server_data = input('>>>输入q或者Q退出').strip().encode('utf-8')
                                                               
          if not to_server_data:
              print('发送内容不能为空')
              continue
          client.send(to_server_data)
                                                               
          if to_server_data.upper() == b'Q':
              break
      
          # 1. 接收固定长度的4个字节
          head_bytes = client.recv(4)
      
          # 2. 获得bytes类型字典的总字节数
          len_head_dic_json_bytes = struct.unpack('i',head_bytes)[0]
      
          # 3. 接收bytes形式的dic数据
          head_dic_json_bytes = client.recv(len_head_dic_json_bytes)
      
          # 4. 转化成json类型dic
          head_dic_json = head_dic_json_bytes.decode('utf-8')
      
          # 5. 转化成字典形式的报头
          head_dic = json.loads(head_dic_json)
          '''
          head_dic = {
                      'file_name': 'test1',
                      'md5': 6567657678678,
                      'total_size': total_size,
                  }
          '''
          total_data = b''
          while len(total_data) < head_dic['total_size']:
              total_data += client.recv(1024)
      
          # print(len(total_data))
          print(total_data.decode('gbk'))
      
      client.close()
      
    • 总结:

      1. 高大上版: 自定制报头
      dic = {'filename': XX, 'md5': 654654676576776, 'total_size': 26743}
      2. 高大上版:可以解决文件过大的问题.
      
      
  • 相关阅读:
    Optional int parameter 'resourceState' is present but cannot be translated into a null value
    创建第一个react项目
    后台接收参数报错 Required String parameter 'id' is not present
    sql查询条件参数为空
    斐波那契数列
    Map获取key值
    Java8之集合排序
    Android学习笔记(4)----Rendering Problems(The graphics preview in the layout editor may not be accurate)
    LeetCode赛题395----Longest Substring with At Least K Repeating Characters
    LeetCode赛题394----Decode String
  • 原文地址:https://www.cnblogs.com/liubing8/p/11366648.html
Copyright © 2011-2022 走看看