55·模块十二:网络编程进阶

网络编程:UDP 协议与简单应用

networkingudp

第55课 - 网络编程:UDP协议与简单应用

学习目标

  1. 理解UDP协议的核心特点及其与TCP协议的主要区别。
  2. 掌握使用Python socket模块创建UDP套接字并实现基本数据收发。
  3. 能够编写一个简单的UDP客户端/服务器程序,实现双向通信。
  4. 了解UDP协议的典型应用场景及使用时的注意事项。

核心概念

在上一课中,我们学习了基于TCP的可靠通信。今天,我们将探索它的"兄弟"——UDP。如果说TCP像打电话(先建立连接,确认对方听到,信息按顺序送达),那么UDP就像寄明信片:你直接把写好的内容丢出去,它尽力送达,但不保证对方一定能收到,也不保证顺序。

UDP(用户数据报协议) 的核心特点是:

  • 无连接:发送数据前不需要建立连接,可以直接向目标地址发送数据包。
  • 不可靠:数据报可能丢失、重复或乱序,协议本身不提供重传或排序机制。
  • 轻量高效:由于省去了建立连接、确认、重传等机制,开销小,传输速度快。
  • 面向消息:保留消息边界,发送几次数据,接收方就会收到几个完整的数据块(而TCP是流式的)。

这种"无连接、不可靠"的特性,在特定场景下反而是优势,例如:

  • 实时音视频、在线游戏:偶尔丢一个数据包(比如一帧画面)可以接受,但绝不能因等待重传而导致卡顿。
  • DNS查询:简单的请求-应答模式。
  • 广播或多播:向网络中的多个设备发送信息。

代码示例

示例1:基础UDP客户端与服务器

udp_server.py - UDP服务器

import socket

# 1. 创建UDP套接字
#    AF_INET: 使用IPv4地址族
#    SOCK_DGRAM: 指定套接字类型为数据报套接字(即UDP)
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 2. 绑定地址和端口
#    '0.0.0.0' 表示监听所有可用的网络接口
#    12345 是监听的端口号
server_address = ('0.0.0.0', 12345)
server_socket.bind(server_address)

print(f"UDP服务器启动,正在监听 {server_address}...")

# 3. 循环等待并接收数据
while True:
    # recvfrom() 是UDP的关键函数,它返回两个值:
    #   data: 接收到的数据(字节)
    #   client_address: 发送方(客户端)的地址和端口元组
    data, client_address = server_socket.recvfrom(1024)  # 1024是缓冲区大小
    message = data.decode('utf-8')
    
    print(f"收到来自 {client_address} 的消息: {message}")
    
    # 4. 向客户端回复消息
    #    sendto() 函数需要指定目标地址和端口
    reply = f"服务器已收到: {message}".encode('utf-8')
    server_socket.sendto(reply, client_address)

# 注意:在实际应用中,需要添加退出循环的逻辑(例如收到特定消息时)

udp_client.py - UDP客户端

import socket

# 1. 创建UDP套接字
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# 2. 指定服务器地址
server_address = ('localhost', 12345)  # 如果服务器在另一台机器,替换'localhost'为服务器IP

# 3. 发送数据
#    无需建立连接,直接发送
message = "Hello, UDP Server!"
client_socket.sendto(message.encode('utf-8'), server_address)
print(f"客户端已发送: {message}")

# 4. 接收服务器的回复
data, address = client_socket.recvfrom(1024)
print(f"收到来自服务器 {address} 的回复: {data.decode('utf-8')}")

# 5. 关闭套接字
client_socket.close()

运行步骤

  1. 先运行 python udp_server.py,服务器开始监听。
  2. 再运行 python udp_client.py,观察双方控制台的输出。

示例2:简单的UDP聊天程序

这个示例扩展了基本通信,使客户端和服务器可以进行多次交互。

udp_chat_server.py

import socket

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('0.0.0.0', 12346)
server_socket.bind(server_address)

print("简易聊天服务器已启动。等待消息...")

# 用于记录当前对话的客户端地址
current_client = None

while True:
    data, client_address = server_socket.recvfrom(1024)
    message = data.decode('utf-8')
    
    # 如果是新客户端,记录其地址
    if current_client is None or client_address != current_client:
        current_client = client_address
        print(f"新客户端接入: {client_address}")
    
    print(f"收到({client_address[0]}:{client_address[1]}): {message}")
    
    # 服务器端输入回复
    if message.lower() == 'quit':
        print("客户端要求结束会话。")
        continue
    
    reply = input("服务器回复: ")
    server_socket.sendto(reply.encode('utf-8'), client_address)

udp_chat_client.py

import socket

client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_address = ('localhost', 12346)

print("连接到聊天服务器。输入'quit'结束会话。")

while True:
    msg_to_send = input("你: ")
    client_socket.sendto(msg_to_send.encode('utf-8'), server_address)
    
    if msg_to_send.lower() == 'quit':
        print("会话结束。")
        break
    
    # 接收服务器回复(这里会阻塞,直到收到数据)
    try:
        data, address = client_socket.recvfrom(1024)
        print(f"服务器: {data.decode('utf-8')}")
    except socket.timeout:
        print("接收服务器回复超时。")
    
client_socket.close()

实践练习

练习1:修改服务器回复

修改第一个示例中的 udp_server.py,使其在收到消息后,不是回复原始消息,而是返回服务器的当前时间。格式为:"当前服务器时间:YYYY-MM-DD HH:MM:SS"。 提示:使用 datetime 模块。

练习2:实现简易时间服务器

编写一个UDP服务器 time_server.py,当客户端发送任何消息时,服务器返回一个简单的字符串 TIME,但其内容是当前的 Unix 时间戳(一个浮点数)。编写对应的客户端 time_client.py 来获取并显示这个时间戳。 预期输出示例

客户端发送: get time
客户端收到: 1715348465.123456

练习3:UDP Ping程序

编写一个程序 udp_ping_client.py,模拟 ping 命令。

  • 向一个指定的 UDP 服务器(可以自行编写一个简单的回显服务器)发送10个数据包。
  • 每个数据包包含序号和发送时间戳。
  • 服务器收到后应原样返回。
  • 客户端计算并输出每个数据包往返时间(RTT)。
  • 最后报告发送的包数、接收的包数和丢包率。 预期输出示例
向 ('localhost', 12347) 发送 10 个数据包...
收到来自服务器的回复,RTT: 1.23 ms
收到来自服务器的回复,RTT: 0.98 ms
... (丢失一个)
统计: 发送 10,接收 9,丢包率 10.0%

常见错误

  1. 绑定地址错误:在服务器中绑定 'localhost''127.0.0.1' 会导致只能被本机客户端访问。如果希望被局域网内其他机器访问,应绑定 '0.0.0.0'
  2. 数据丢失与阻塞:默认情况下,recvfrom() 是阻塞的。如果客户端发送了消息但服务器未运行或未收到,客户端的 recvfrom() 会一直等待。需要为套接字设置超时:client_socket.settimeout(5)
  3. 误把UDP当TCP用:不要期望UDP能保证数据送达。如果应用需要可靠性(如文件传输),必须使用TCP或自行在应用层实现确认和重传机制。
  4. 缓冲区大小recvfrom(1024) 中的1024字节可能不足以接收所有数据。对于大数据,需要设计协议来分片或增大缓冲区,并注意MTU限制。
  5. 未处理异常:网络操作容易出错(如地址不可达),应使用 try...except socket.error 来捕获异常,使程序更健壮。

小结

本节课我们学习了UDP协议的编程。关键要点如下:

  • UDP特性:无连接、不可靠、轻量、保留消息边界。
  • Socket操作:使用 socket.socket(AF_INET, SOCK_DGRAM) 创建UDP套接字;服务器需 bind(),客户端直接 sendto();使用 sendto()recvfrom() 收发数据,必须携带地址信息
  • 适用场景:对实时性要求高、能容忍少量丢包的应用,如视频流、实时游戏、简单查询。
  • 核心权衡:用"可靠性"和"顺序保证"换取了"速度"和"灵活性"。在实际项目中,选择TCP还是UDP取决于你的具体需求。

理解UDP是掌握网络编程的重要一步,它为你处理实时网络通信提供了基础工具。在下一课中,我们将学习基于TCP的、更高级的应用层协议——HTTP。

练习编辑器

python
Loading...

继续学习

完成本课后,建议继续学习下一课「HTTP 请求与 requests 库」 以巩固所学知识。