基于UDP实现简易聊天室(基础版)——Python实现

一、实验目的

目录

  1. 理解UDP无连接通信机制:掌握UDP(User Datagram Protocol)与TCP(Transmission Control Protocol)的核心区别,理解无连接、不可靠但高效的数据传输特点。
  2. 掌握Socket编程基础 :学习使用Python内置的socket模块创建UDP Socket,进行数据的发送(sendto)与接收(recvfrom)。
  3. 实现服务端核心功能
    • 创建服务端Socket并绑定端口,持续接收来自不同客户端的消息。
    • 维护一个在线客户端地址集合,实现简单的用户管理。
    • 实现消息广播功能,将任一客户端发送的消息,按照[IP地址]:消息内容的格式转发给所有其他在线客户端。
  4. 完成客户端功能
    • 搭建客户端程序,实现向服务端发送消息。
    • 开启独立线程持续监听并展示来自服务端的广播消息,实现实时聊天效果。

二、实验原理

UDP(用户数据报协议)是一种面向无连接的传输层协议。与TCP需要建立"三次握手"连接不同,UDP在通信前无需建立连接,直接将数据封装成数据报进行发送。这种机制带来了以下特点:

  • 优点:传输延迟低,开销小,效率高。
  • 缺点:不保证数据包的顺序、可靠性和完整性,可能丢包。

聊天室服务端核心逻辑

  1. 创建与绑定 :服务端创建一个UDP Socket,并绑定到特定的IP地址和端口(如0.0.0.0:8888),开始监听。
  2. 接收与存储 :进入无限循环,使用recvfrom()方法阻塞等待接收客户端发来的数据。该方法会返回数据和客户端的地址(IP, Port)。收到消息后,立即将该客户端地址加入"在线客户端集合"。
  3. 广播消息 :每当收到一条消息,服务端便调用广播函数。该函数将消息封装为[客户端IP]:消息内容的格式,然后遍历"在线客户端集合",将封装后的消息发送给集合中的每一个地址(除了发送者自身,可选)。

客户端核心逻辑

  1. 双线程设计
    • 主线程 :负责从控制台获取用户输入,并通过sendto()方法将消息发送至服务端地址。
    • 子线程(守护线程) :负责循环调用recvfrom()方法,接收服务端广播过来的消息,并实时打印到控制台。两个线程并行,确保发送和接收互不阻塞。

三、实验环境

  • 编程语言:Python 3.6 或更高版本。
  • 核心模块 :仅使用Python标准库中的socketthreading模块,无需安装任何第三方库
  • 操作系统: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。接收线程由于是守护线程,会随主程序退出而自动终止。

六、运行测试步骤

  1. 启动服务端

    打开一个终端(或命令行),导航到代码所在目录,运行:

    bash 复制代码
    python base_server.py

    你将看到输出:UDP聊天室【基础版】服务端启动 0.0.0.0:8888

  2. 启动多个客户端

    • 打开第二个终端 ,运行客户端连接本地服务器:

      bash 复制代码
      python base_client.py
    • 打开第三个终端 ,同样运行客户端:

      bash 复制代码
      python base_client.py
    • 可以重复此步骤,模拟更多用户加入。

  3. 开始聊天

    • 在任意一个客户端终端中输入文字,按回车发送。
    • 观察所有客户端终端 ,都会收到格式为 [发送者IP]:消息内容 的广播消息。
    • 观察服务端终端,会打印出所有接收到的消息和客户端连接信息。
  4. 退出客户端

    在客户端输入 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聊天室测试

八、基础版存在的不足

  1. 无连接状态管理:UDP是无连接的,服务端无法感知客户端是否异常崩溃或离线。本版本中,客户端地址一旦加入集合就永不移除,会导致向无效地址发送消息失败。
  2. 标识单一:仅使用IP地址和端口标识用户,不友好且同一机器多个客户端无法区分。缺乏"昵称"等用户自定义标识功能。
  3. 无管理功能:服务端只是一个简单的消息转发器,没有实现管理员权限,无法执行如"禁言特定用户"、"踢出用户"、"发送系统公告"等操作。
  4. 健壮性不足:代码中异常处理较为基础,网络波动、非法数据包可能导致程序意外终止。未处理消息丢失、乱序等UDP固有问题。
  5. 广播效率:每次广播都遍历整个集合并逐个发送,当在线用户很多时效率较低。

九、拓展方向(进阶需求)

基于此基础版,可以尝试实现以下进阶功能,使其更接近一个实用的聊天室:

  1. 用户昵称 :客户端启动时输入昵称,发送消息时附带昵称而非IP。服务端维护地址->昵称的映射。
  2. 心跳机制:客户端定期向服务端发送心跳包。服务端定时检查,长时间未收到心跳的客户端视为离线,从其地址集合中移除。
  3. 管理员线程 :在服务端开启一个独立的控制台线程,允许管理员输入命令(如 /list 查看用户,/mute 昵称 禁言,/kick 昵称 踢人)。
  4. 禁言/解除禁言:服务端维护一个"禁言列表",广播消息前检查发送者是否被禁言。
  5. 私聊功能 :支持消息格式如 @昵称 消息内容,实现用户间的私密聊天。
  6. 图形化界面 :使用tkinterPyQt为客户端制作图形界面,提升用户体验。
相关推荐
W_326001 小时前
Python 组合数据类型——序列(列表 & 元组)
开发语言·python
砚凝霜1 小时前
软考网络工程师|第 5 章 路由基础、静态路由、RIP、OSPF、BGP、IS-IS 完整备考笔记
网络·笔记
二进制杯莫停2 小时前
A和B环境的python版本相同,B环境无法安装pip依赖,离线安装
开发语言·python·pip
威联通安全存储2 小时前
TS-h1677AXU-RP 在汽车冲压车间高速冲压线检测网中的部署
python·汽车
星恒讯工业路由器2 小时前
4G(LTE)基础技术解析:从频段划分到网络架构的核心知识点
大数据·网络·信息与通信·工业物联网·4g lte·lte网络架构·无线通信基础
RSTJ_16253 小时前
PYTHON+AI LLM DAY ONE HUNDRED AND TWENTY-NINE
人工智能·python
夏天测3 小时前
Python 网络编程从 TCP 三次握手到 Socket 搭建极简 AI 推理服务端(零基础可跑通完整代码)
python·socket·网络通信·tcp 网络编程·零基础网络编程
深念Y3 小时前
ZTE_UZ901_NVRAM经验总结
linux·服务器·网络·嵌入式硬件·嵌入式·随身wifi