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 实现
-
基本形式
# 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()
-
多次对话
# 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 | 白色 |
五、黏包
-
什么是黏包?
我们现在建立一个服务端和一个客户端:
# 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 形式字符串因为服务端的两次发送时间较为密集连在了一起,这个现象就称为黏包。
-
黏包的原因
- 发送端:发送端内核态在极短的时间内会缓存用户态发送的短消息,从而提高发送的效率;
- 接收端:接收端内核态由于有时可能会没及时接收,而会在较短的时间同时接受多条消息,也会出现黏包;
- UDP 数据传递机制:由于 UDP 是无连接的,高效率的服务;发送的消息要遵循网络最大带宽限制 MTU(Maximum Transmisson Unit) ,一般设定为 1500 字节,因此 UDP 一般不能发送过大的消息(Python 的 UDP 经过优化可以发送大文件);
- TCP 数据传输机制:TCP 是面向连接的,高可靠性的服务;TCP 会对发送的大文件进行拆分,所以 TCP 没有网络最大带宽限制,也正因如此,TCP 信息之间没有标记可言;故只有 TCP 协议才会出现黏包问题。
六、利用 struct 模块解决黏包
-
解决黏包问题的本质:设置 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
的字节数如果超过四位数,就没法很好地实现自定义协议传输了。 -
利用 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()