一、实验目的
目录
- 一、实验目的
- 二、实验原理
- 三、实验环境
- 四、代码实现
- [4.1 服务端代码
base_server.py](#4.1 服务端代码 base_server.py) - [4.2 客户端代码
base_client.py](#4.2 客户端代码 base_client.py)
- [4.1 服务端代码
- 五、代码说明
- [1. 服务端核心要点](#1. 服务端核心要点)
- [2. 客户端核心要点](#2. 客户端核心要点)
- 六、运行测试步骤
- 七、运行效果示例
- 八、基础版存在的不足
- 九、拓展方向(进阶需求)
- 理解UDP无连接通信机制:掌握UDP(User Datagram Protocol)与TCP(Transmission Control Protocol)的核心区别,理解无连接、不可靠但高效的数据传输特点。
- 掌握Socket编程基础 :学习使用Python内置的
socket模块创建UDP Socket,进行数据的发送(sendto)与接收(recvfrom)。 - 实现服务端核心功能 :
- 创建服务端Socket并绑定端口,持续接收来自不同客户端的消息。
- 维护一个在线客户端地址集合,实现简单的用户管理。
- 实现消息广播功能,将任一客户端发送的消息,按照
[IP地址]:消息内容的格式转发给所有其他在线客户端。
- 完成客户端功能 :
- 搭建客户端程序,实现向服务端发送消息。
- 开启独立线程持续监听并展示来自服务端的广播消息,实现实时聊天效果。
二、实验原理
UDP(用户数据报协议)是一种面向无连接的传输层协议。与TCP需要建立"三次握手"连接不同,UDP在通信前无需建立连接,直接将数据封装成数据报进行发送。这种机制带来了以下特点:
- 优点:传输延迟低,开销小,效率高。
- 缺点:不保证数据包的顺序、可靠性和完整性,可能丢包。
聊天室服务端核心逻辑:
- 创建与绑定 :服务端创建一个UDP Socket,并绑定到特定的IP地址和端口(如
0.0.0.0:8888),开始监听。 - 接收与存储 :进入无限循环,使用
recvfrom()方法阻塞等待接收客户端发来的数据。该方法会返回数据和客户端的地址(IP, Port)。收到消息后,立即将该客户端地址加入"在线客户端集合"。 - 广播消息 :每当收到一条消息,服务端便调用广播函数。该函数将消息封装为
[客户端IP]:消息内容的格式,然后遍历"在线客户端集合",将封装后的消息发送给集合中的每一个地址(除了发送者自身,可选)。
客户端核心逻辑:
- 双线程设计 :
- 主线程 :负责从控制台获取用户输入,并通过
sendto()方法将消息发送至服务端地址。 - 子线程(守护线程) :负责循环调用
recvfrom()方法,接收服务端广播过来的消息,并实时打印到控制台。两个线程并行,确保发送和接收互不阻塞。
- 主线程 :负责从控制台获取用户输入,并通过
三、实验环境
- 编程语言:Python 3.6 或更高版本。
- 核心模块 :仅使用Python标准库中的
socket和threading模块,无需安装任何第三方库。 - 操作系统:Windows, macOS, Linux 均可。
四、代码实现
4.1 服务端代码 base_server.py
python
import socket
import threading
# ===================== 全局配置 =====================
HOST = "0.0.0.0"
PORT = 8888
BUF_SIZE = 1024
ENCODING = "utf-8"
# UDP套接字
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))
# 多线程共享资源
lock = threading.Lock()
online_clients = set() # 在线客户端地址集合 {(ip, port)}
client_nickname = dict() # key:(ip,port) value:昵称
banned_ip_set = set() # 禁言IP集合
# ===================== 基础功能 =====================
def broadcast(raw_msg: str, sender_addr: tuple):
"""消息群发,禁言校验"""
sender_ip = sender_addr[0]
with lock:
if sender_ip in banned_ip_set:
print(f"[🚫禁言拦截] {sender_ip} 消息不予转发")
return
for client_addr in online_clients:
try:
server_socket.sendto(raw_msg.encode(ENCODING), client_addr)
except Exception as err:
print(f"[⚠️发送失败 {client_addr}] {err}")
# ===================== 管理员命令解析模块(核心优化) =====================
def admin_command_parser(cmd: str):
"""命令解析中心,统一管理所有管理员指令"""
parts = cmd.strip().split()
if not parts:
return
cmd_op = parts[0].lower()
if cmd_op == "help":
help_text = """
==================== 管理员命令列表 ====================
help 查看全部命令
ban [ip] 禁言指定IP,例:ban 127.0.0.1
unban [ip] 解除指定IP禁言,例:unban 127.0.0.1
banlist 查看所有被禁言IP
online 查看当前在线所有用户
clearban 清空全部禁言名单
quit 关闭聊天室服务端
=======================================================
"""
print(help_text.strip())
elif cmd_op == "ban":
if len(parts) < 2:
print("❌ 参数缺失!格式:ban 127.0.0.1")
return
ban_ip = parts[1]
with lock:
banned_ip_set.add(ban_ip)
print(f"✅ 操作成功:已禁言 {ban_ip}")
elif cmd_op == "unban":
if len(parts) < 2:
print("❌ 参数缺失!格式:unban 127.0.0.1")
return
target_ip = parts[1]
with lock:
if target_ip in banned_ip_set:
banned_ip_set.remove(target_ip)
print(f"✅ 操作成功:解除 {target_ip} 禁言")
else:
print(f"⚠️ {target_ip} 不在禁言列表中")
elif cmd_op == "banlist":
with lock:
if banned_ip_set:
print("📋 当前禁言IP列表:")
for ip in banned_ip_set:
print(f" - {ip}")
else:
print("📋 当前无禁言用户")
elif cmd_op == "online":
with lock:
print("📋 当前在线用户:")
if not online_clients:
print(" 暂无在线客户端")
return
for addr in online_clients:
nick = client_nickname.get(addr, "未初始化昵称")
print(f" {addr[0]}:{addr[1]} | {nick}")
elif cmd_op == "clearban":
with lock:
banned_ip_set.clear()
print("✅ 操作成功:已清空全部禁言列表")
elif cmd_op == "quit":
print("🛑 准备关闭服务端...")
server_socket.close()
raise SystemExit # 终止线程循环
else:
print(f"❌ 未知指令 [{cmd_op}],输入 help 查看命令清单")
def admin_console_task():
"""管理员控制台线程"""
print("💡 输入 help 查看所有管理员命令")
while True:
try:
user_input = input("admin > ").strip()
admin_command_parser(user_input)
except SystemExit:
break
except Exception as e:
print(f"[命令执行异常] {e}")
# ===================== 消息接收主循环 =====================
def main_receive_loop():
print(f"✅ UDP聊天室服务端启动成功 {HOST}:{PORT}")
while True:
try:
data, addr = server_socket.recvfrom(BUF_SIZE)
msg_text = data.decode(ENCODING).strip()
client_ip, _ = addr
with lock:
online_clients.add(addr)
# 首次消息设置昵称
if addr not in client_nickname:
with lock:
client_nickname[addr] = msg_text
welcome_msg = f"🎉 新用户上线 ({client_ip}){msg_text}"
print(welcome_msg)
broadcast(welcome_msg, addr)
continue
# 正常聊天消息
nick = client_nickname[addr]
forward_msg = f"({client_ip}){nick}:{msg_text}"
print(forward_msg)
broadcast(forward_msg, addr)
except UnicodeDecodeError:
print("[警告] 收到无法解析的二进制数据,丢弃")
except OSError:
break
except Exception as err:
print(f"[接收异常] {err}")
if __name__ == "__main__":
threading.Thread(target=admin_console_task, daemon=True).start()
main_receive_loop()
4.2 客户端代码 base_client.py
python
import socket
import threading
SERVER_HOST = "127.0.0.1"
SERVER_PORT = 8888
BUF_SIZE = 1024
ENCODING = "utf-8"
client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def receive_message_task():
while True:
try:
data, _ = client_socket.recvfrom(BUF_SIZE)
print(f"\n{data.decode(ENCODING)}")
except OSError:
break
except Exception as e:
print(f"\n[接收异常] {e}")
def main_input_loop():
print("======== UDP聊天室客户端 ========")
print("💡 首次发送内容将作为你的昵称")
print("💡 输入 exit 退出聊天室\n")
threading.Thread(target=receive_message_task, daemon=True).start()
while True:
text = input()
if text.strip().lower() == "exit":
print("👋 退出聊天室")
break
client_socket.sendto(text.encode(ENCODING), (SERVER_HOST, SERVER_PORT))
if __name__ == "__main__":
main_input_loop()
client_socket.close()
五、代码说明
1. 服务端核心要点
- 客户端管理 :使用Python的
set()集合self.clients来存储所有在线客户端的地址(ip, port)。集合的自动去重特性防止同一地址被重复添加。 - 线程安全 :由于广播函数可能在遍历集合时,主循环正在向集合中添加新客户端,因此使用
threading.Lock()锁来确保对self.clients的修改和遍历是线程安全的。 - 广播函数
broadcast():该函数是服务端的核心。它将接收到的原始消息与发送者的IP地址封装成[IP]:消息的格式,然后遍历在线客户端集合,使用sendto()方法将消息发送给除发送者外的每一个客户端。 - 主循环
handle_client():使用recvfrom()在循环中阻塞等待消息。一旦收到消息,立即将发送者地址加入在线集合,然后调用广播函数。该设计简单高效,符合UDP无连接的特性。
2. 客户端核心要点
- 双线程模型 :
- 主线程 :执行
send_messages()函数,负责监听控制台输入,并将用户输入的消息通过sendto()发送至服务端。 - 子线程 :执行
receive_messages()函数,作为一个守护线程(daemon=True)启动,在后台循环调用recvfrom(),专门负责接收和打印服务端广播的消息。这种设计避免了因等待接收消息而阻塞用户输入。
- 主线程 :执行
- 退出机制 :用户输入
exit后,将self.running标志设为False,发送线程循环结束,主线程关闭Socket。接收线程由于是守护线程,会随主程序退出而自动终止。
六、运行测试步骤
-
启动服务端 :
打开一个终端(或命令行),导航到代码所在目录,运行:
bashpython base_server.py你将看到输出:
UDP聊天室【基础版】服务端启动 0.0.0.0:8888 -
启动多个客户端:
-
打开第二个终端 ,运行客户端连接本地服务器:
bashpython base_client.py -
打开第三个终端 ,同样运行客户端:
bashpython base_client.py -
可以重复此步骤,模拟更多用户加入。
-
-
开始聊天:
- 在任意一个客户端终端中输入文字,按回车发送。
- 观察所有客户端终端 ,都会收到格式为
[发送者IP]:消息内容的广播消息。 - 观察服务端终端,会打印出所有接收到的消息和客户端连接信息。
-
退出客户端 :
在客户端输入
exit并按回车,该客户端将退出程序。
七、运行效果示例
服务端控制台输出:
UDP聊天室【基础版】服务端启动 0.0.0.0:8888
新客户端加入: ('127.0.0.1', 65432),当前在线: 1
新客户端加入: ('127.0.0.1', 65433),当前在线: 2
收到('127.0.0.1', 65432)消息:大家好!
收到('127.0.0.1', 65433)消息:UDP聊天室测试
客户端A (端口65432) 控制台输出:
=== UDP聊天室基础客户端 ===
已连接到服务器 127.0.0.1:8888
输入消息并按回车发送,输入 'exit' 退出。
大家好!
[127.0.0.1]:UDP聊天室测试
客户端B (端口65433) 控制台输出:
=== UDP聊天室基础客户端 ===
已连接到服务器 127.0.0.1:8888
输入消息并按回车发送,输入 'exit' 退出。
[127.0.0.1]:大家好!
UDP聊天室测试
八、基础版存在的不足
- 无连接状态管理:UDP是无连接的,服务端无法感知客户端是否异常崩溃或离线。本版本中,客户端地址一旦加入集合就永不移除,会导致向无效地址发送消息失败。
- 标识单一:仅使用IP地址和端口标识用户,不友好且同一机器多个客户端无法区分。缺乏"昵称"等用户自定义标识功能。
- 无管理功能:服务端只是一个简单的消息转发器,没有实现管理员权限,无法执行如"禁言特定用户"、"踢出用户"、"发送系统公告"等操作。
- 健壮性不足:代码中异常处理较为基础,网络波动、非法数据包可能导致程序意外终止。未处理消息丢失、乱序等UDP固有问题。
- 广播效率:每次广播都遍历整个集合并逐个发送,当在线用户很多时效率较低。
九、拓展方向(进阶需求)
基于此基础版,可以尝试实现以下进阶功能,使其更接近一个实用的聊天室:
- 用户昵称 :客户端启动时输入昵称,发送消息时附带昵称而非IP。服务端维护
地址->昵称的映射。 - 心跳机制:客户端定期向服务端发送心跳包。服务端定时检查,长时间未收到心跳的客户端视为离线,从其地址集合中移除。
- 管理员线程 :在服务端开启一个独立的控制台线程,允许管理员输入命令(如
/list查看用户,/mute 昵称禁言,/kick 昵称踢人)。 - 禁言/解除禁言:服务端维护一个"禁言列表",广播消息前检查发送者是否被禁言。
- 私聊功能 :支持消息格式如
@昵称 消息内容,实现用户间的私密聊天。 - 图形化界面 :使用
tkinter或PyQt为客户端制作图形界面,提升用户体验。