• Python基础学习(28)TCP协议的Python实现 UDP协议的Python实现 黏包 利用struct模块解决黏包


    Python基础学习(28)TCP协议的Python实现 UDP协议的Python实现 黏包 利用struct模块解决黏包

    一、今日内容

    • TCP(Transport Control Protocol) 协议的 Python 实现
    • UDP(User Datagram Protocol) 协议的 Python 实现
    • 输出变色字体
    • 黏包
    • 利用 struct 模块解决黏包

    二、TCP 协议的 Python 实现

    1. 基本形式

      # server.py
      
      import socket
      sk = socket.socket()
      sk.bind(('192.168.1.100', 9000))  # 申请操作系统端口资源
      sk.listen()
      print('sk:', sk)
      # sk: <socket.socket fd=456, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('192.168.1.100', 9000)>
      
      conn, addr = sk.accept()  # conn存储的是一个客户端和server端的连接信息
      print('conn:', conn)
      # conn: <socket.socket fd=460, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('192.168.1.100', 9000), raddr=('192.168.1.100', 53673)>
      conn.send(b'hello')
      msg = conn.recv(1024)
      print(msg)
      conn.close()  # 挥手 断开连接
      
      sk.close()  # 归还申请的操作系统资源
      
      # client.py
      
      import socket
      sk = socket.socket()
      sk.connect(('192.168.1.100', 9000))
      
      msg = sk.recv(1024)
      print(msg)
      sk.send(b'byebye')
      
      sk.close()
      
    2. 多次对话

      # server.py
      
      import socket
      sk = socket.socket()
      sk.bind(('192.168.1.100', 9000))  # 申请操作系统端口资源
      sk.listen()
      
      
      while True:
          conn, addr = sk.accept()  # conn存储的是一个客户端和server端的连接信息
          print('conn:', conn)
          # 每次运行客户端发现不是一个端口
          # raddr=('192.168.1.100', 53830)
          # raddr=('192.168.1.100', 53831)
          # raddr=('192.168.1.100', 53832)
          # raddr=('192.168.1.100', 53833)
          # ....
          while True:
              send_msg = input('>>>')
      
              conn.send(send_msg.encode('utf-8'))
              if send_msg.upper() == 'Q':
                  break
              msg = conn.recv(1024).decode('utf-8')
              if msg.upper() == 'Q': break
              print(msg)
          conn.close()  # 挥手 断开连接
      
      
      sk.close()  # 归还申请的操作系统资源
      
      # client.py
      
      import socket
      sk = socket.socket()
      sk.connect(('192.168.1.100', 9000))
      
      while True:
          msg = sk.recv(1024).decode('utf-8')
          if msg.upper() == 'Q':
              break
          print(msg)
          send_msg = input('>>>')
          sk.send(send_msg.encode('utf-8'))
          if send_msg.upper() == 'Q':
              break
      
      sk.close()
      

    三、UDP 协议的 Python 实现

    # server.py
    
    import socket
    
    sk = socket.socket(type=socket.SOCK_DGRAM)  # UDP协议
    # socket.socket(type=socket.SOCK_STREAM)  # TCP协议(默认)
    sk.bind(('127.0.0.1', 9000))
    
    # 非全双工通信,服务端不可以先发送消息
    # sk.recv(1024)  # 不可以使用recv,因为这样无法获知发送端
    while True:
        msg, addr = sk.recvfrom(1024)
        print(msg.decode('utf-8'))
        s_msg = input('>>>')
        sk.sendto(s_msg.encode('utf-8'), addr)
    
    # client.py
    
    import socket
    
    sk = socket.socket(type=socket.SOCK_DGRAM)
    
    server = ('127.0.0.1', 9000)
    while True:
        s_msg = input('>>>')
        sk.sendto(s_msg.encode('utf-8'), server)
        msg = sk.recv(1024).decode('utf-8')  # 地址已知,不用recvfrom
        if msg.upper() == 'Q':  # 服务端发送q退出发送的客户端,只需要单方面推出
            break
        print(msg)
    

    四、输出变色字体

    当日常在使用print()等内置打印函数时,统一的字体颜色和背景让我们在打印结果较多时比较难以分辨,这时可以在打印的字符串前加上一小段字符实现变色等功能:

    • 33[Display_mode;Forecolor;Backgroundcolor嵌入打印字符串前,可以按照需求修改输出字符
    print('33[2;36mhello world33[0m')  # 前面经过设置后,如不修改回来,后续输出的所有字符都会按照此设置输出
    

    具体功能说明:

    前景色 Forecolor 背景色 Backgroundcolor 颜色 显示方式 Display mode 意义
    30 40 黑色 0 终端默认设置
    31 41 红色 1 高亮显示
    32 42 绿色 4 使用下划线
    33 43 黄色 7 反白显示
    34 44 蓝色
    35 45 紫红色
    35 46 青蓝色
    37 47 白色

    五、黏包

    1. 什么是黏包?

      我们现在建立一个服务端和一个客户端:

      # server.py
      
      import socket
      
      sk = socket.socket()
      sk.bind(('127.0.0.1', 9000))
      sk.listen()
      
      conn, addr = sk.accept()
      conn.send(b'alex')
      conn.send(b'hahaha')
      conn.close()
      
      sk.close()
      
      # client.py
      
      import time
      import socket
      
      sk = socket.socket()
      sk.connect(('127.0.0.1', 9000))
      
      time.sleep(0.1)
      msg1 = sk.recv(1024)
      print(msg1)  # b'alexhahaha' 粘包现象
      msg2 = sk.recv(1024)
      print(msg2)  # b''
      
      sk.close()
      

      我们在客户端取到的 byte 形式字符串因为服务端的两次发送时间较为密集连在了一起,这个现象就称为黏包。

    2. 黏包的原因

      • 发送端:发送端内核态在极短的时间内会缓存用户态发送的短消息,从而提高发送的效率;
      • 接收端:接收端内核态由于有时可能会没及时接收,而会在较短的时间同时接受多条消息,也会出现黏包;
      • UDP 数据传递机制:由于 UDP 是无连接的,高效率的服务;发送的消息要遵循网络最大带宽限制 MTU(Maximum Transmisson Unit) ,一般设定为 1500 字节,因此 UDP 一般不能发送过大的消息(Python 的 UDP 经过优化可以发送大文件);
      • TCP 数据传输机制:TCP 是面向连接的,高可靠性的服务;TCP 会对发送的大文件进行拆分,所以 TCP 没有网络最大带宽限制,也正因如此,TCP 信息之间没有标记可言;故只有 TCP 协议才会出现黏包问题。

    六、利用 struct 模块解决黏包

    1. 解决黏包问题的本质:设置 TCP 的边界,即设置接受的字节数。

      我们设置了 4 个字节的数据来负责解决传输中接收和发送文件大小的问题,这又被称为自定义协议:

      # server.py
      
      import socket
      
      sk = socket.socket()
      sk.bind(('127.0.0.1', 9001))
      sk.listen()
      
      conn, addr = sk.accept()
      msg1 = input('>>>').encode('utf-8')
      msg2 = input('>>>').encode('utf-8')
      num = str(len(msg1))
      ret = num.zfill(4).encode('utf-8')
      conn.send(ret)
      conn.send(msg1)
      conn.send(msg2)
      
      sk.close()
      
      # client.py
      
      import socket
      
      sk = socket.socket()
      sk.connect(('127.0.0.1', 9001))
      
      ret = sk.recv(4).decode('utf-8')
      msg1 = sk.recv(ret)
      msg2 = sk.recv(1024)
      print(msg1.decode('utf-8'))
      print(msg2.decode('utf-8'))
      
      sk.close()
      

      但是四个字节有时满足不了更大的文件要求,server.py的字节数如果超过四位数,就没法很好地实现自定义协议传输了。

    2. 利用 struct 模块解决黏包

      struct 模块的基本功能如下:

      import struct
      
      # 将所有此数字范围内的数字打包为四个字节
      # 能表示所有2**32个数字,即(2**31 + 1) ~ 2**31
      num1 = 321321312
      num2 = 123
      num3 = 8
      
      ret1 = struct.pack('i', num1)  
      print(ret1)  # b'`xf9&x13'
      ret2 = struct.pack('i', num2)
      print(ret2)  # b'{x00x00x00'
      ret3 = struct.pack('i', num3)
      print(ret3)  # b'x08x00x00x00'
      
      print(struct.unpack('i', ret1))  # (321321312,)
      print(struct.unpack('i', ret2))  # (123,)
      print(struct.unpack('i', ret3))  # (8,)
      

      这时我们就可以用四个字节来表示足够大的数字了:

      # server.py
      
      import socket
      import struct
      
      sk = socket.socket()
      sk.bind(('127.0.0.1', 9001))
      sk.listen()
      
      conn, addr = sk.accept()
      msg1 = input('>>>').encode('utf-8')
      msg2 = input('>>>').encode('utf-8')
      # num = str(len(msg1))
      # ret = num.zfill(4)
      ret = struct.pack('i', len(msg1))
      # print(ret)
      conn.send(ret)
      conn.send(msg1)
      conn.send(msg2)
      
      import socket
      import struct
      
      sk = socket.socket()
      sk.connect(('127.0.0.1', 9001))
      ret = sk.recv(4)
      length = struct.unpack('i', ret)[0]
      msg1 = sk.recv(length)
      msg2 = sk.recv(1024)
      print(msg1.decode('utf-8'))
      print(msg2.decode('utf-8'))
      
      sk.close()
      
  • 相关阅读:
    java 原子量Atomic举例(AtomicReference)
    Android 用LinkedList实现队列
    Android常用组件,太全了
    java io系列20之 PipedReader和PipedWriter
    Android ExecutorService 的理解与使用
    Android Thread.setDaemon设置说明
    Hadoop HDFS编程 API入门系列之简单综合版本1(四)
    Hadoop HDFS编程 API入门系列之合并小文件到HDFS(三)
    Hadoop HDFS编程 API入门系列之路径过滤上传多个文件到HDFS(二)
    Hadoop HDFS编程 API入门系列之从本地上传文件到HDFS(一)
  • 原文地址:https://www.cnblogs.com/raygor/p/13394657.html
Copyright © 2020-2023  润新知