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()等内置打印函数时,统一的字体颜色和背景让我们在打印结果较多时比较难以分辨,这时可以在打印的字符串前加上一小段字符实现变色等功能:

  • \033[Display_mode;Forecolor;Backgroundcolor嵌入打印字符串前,可以按照需求修改输出字符
print('\033[2;36mhello world\033[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'{\x00\x00\x00'
    ret3 = struct.pack('i', num3)
    print(ret3)  # b'\x08\x00\x00\x00'
    
    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()
    

猜你喜欢

转载自www.cnblogs.com/raygor/p/13394657.html