3. UDP聊天程序

UDP聊天程序 学习卡片

任务说明

本案例实现一个基于UDP的聊天程序,采用客户端-服务器中转架构。服务器监听固定端口,接收任意客户端的消息并将其广播给其他所有在线客户端;客户端通过命令行输入消息并实时接收来自其他客户端的消息。程序使用Rust标准库实现,不引入第三方依赖。

实现方案

服务器端使用std::net::UdpSocket绑定本地端口,维护一个客户端地址集合。收到消息后,将发送者地址加入集合,并将消息连同发送者标识转发给集合中除发送者外的所有地址。客户端同样使用UdpSocket,绑定一个随机可用端口,向服务器发送注册消息和聊天内容,同时在一个独立线程中循环接收服务器转发的消息并打印。

代码实现

创建新项目:

cargo new udp_chat --bin
cd udp_chat

将以下代码写入src/main.rs:

use std::collections::HashSet;
use std::io::{self, BufRead};
use std::net::{SocketAddr, UdpSocket};
use std::sync::{Arc, Mutex};
use std::thread;

const SERVER_ADDR: &str = "127.0.0.1:8888";

// ---------- 服务器模式 ----------
fn run_server() -> io::Result<()> {
    let socket = UdpSocket::bind(SERVER_ADDR)?;
    println!("[服务器] 已启动,监听 {}", SERVER_ADDR);

    let clients: Arc<Mutex<HashSet<SocketAddr>>> = Arc::new(Mutex::new(HashSet::new()));
    let mut buf = [0u8; 1024];

    loop {
        let (len, peer) = socket.recv_from(&mut buf)?;
        let msg = String::from_utf8_lossy(&buf[..len]).to_string();

        // 注册消息:客户端首次发送 "REGISTER" 时加入集合
        if msg == "REGISTER" {
            clients.lock().unwrap().insert(peer);
            println!("[服务器] 客户端 {} 已注册", peer);
            continue;
        }

        // 转发消息给除发送者外的所有客户端
        let targets: Vec<SocketAddr> = clients
            .lock()
            .unwrap()
            .iter()
            .filter(|&&addr| addr != peer)
            .cloned()
            .collect();

        let formatted = format!("[{}] {}", peer, msg);
        for target in targets {
            socket.send_to(formatted.as_bytes(), target)?;
        }
    }
}

// ---------- 客户端模式 ----------
fn run_client() -> io::Result<()> {
    // 绑定随机端口,由操作系统分配
    let socket = UdpSocket::bind("127.0.0.1:0")?;
    let server: SocketAddr = SERVER_ADDR.parse().unwrap();

    // 向服务器注册
    socket.send_to(b"REGISTER", server)?;
    println!("[客户端] 已连接服务器 {},输入消息回车发送,Ctrl+C 退出", SERVER_ADDR);

    // 接收线程:打印服务器转发的消息
    let recv_socket = socket.try_clone()?;
    thread::spawn(move || {
        let mut buf = [0u8; 1024];
        loop {
            match recv_socket.recv_from(&mut buf) {
                Ok((len, _)) => {
                    let msg = String::from_utf8_lossy(&buf[..len]);
                    println!("{}", msg);
                }
                Err(e) => {
                    eprintln!("[客户端] 接收错误: {}", e);
                    break;
                }
            }
        }
    });

    // 主线程:读取标准输入并发送
    let stdin = io::stdin();
    for line in stdin.lock().lines() {
        let line = line?;
        if line.trim().is_empty() {
            continue;
        }
        socket.send_to(line.as_bytes(), server)?;
    }
    Ok(())
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 2 {
        eprintln!("用法: {} server | client", args[0]);
        std::process::exit(1);
    }

    let result = match args[1].as_str() {
        "server" => run_server(),
        "client" => run_client(),
        _ => {
            eprintln!("未知模式: {} (可用: server, client)", args[1]);
            std::process::exit(1);
        }
    };

    if let Err(e) = result {
        eprintln!("运行错误: {}", e);
        std::process::exit(1);
    }
}

运行与验证

编译并启动服务器:

cargo build
cargo run -- server

打开两个新终端,分别启动两个客户端:

cargo run -- client

验证步骤与判定标准:

  1. 服务器终端显示每个客户端注册时的地址信息,如[服务器] 客户端 127.0.0.1:52340 已注册;
  2. 在客户端A输入任意文本(如hello)并回车,客户端B终端应显示[127.0.0.1:52340] hello(端口号随实际分配变化);
  3. 在客户端B输入回复,客户端A应能收到同样格式的消息;
  4. 客户端A发送的消息不会回显到自身终端,仅由服务器转发给其他客户端;
  5. 启动第三个客户端并发送消息,前两个客户端均能收到,验证多客户端广播能力。

满足以上条件即表示程序正确实现了UDP多客户端聊天中转功能。

UDP消息转发流程

UDP聊天程序消息转发时序 客户端 A 客户端 B 客户端 C UDP 服务器 127.0.0.1:8888 REGISTER REGISTER REGISTER "hello" 转发 "[A] hello" 转发 "[A] hello" 注册消息 客户端→服务器 服务器→客户端广播

关键说明

  • 服务器使用HashSet存储客户端地址,自动去重,重复注册不会产生冗余记录;
  • 客户端接收线程使用try_clone()复制套接字描述符,实现同一套接字同时收发;
  • UDP不保证消息有序到达,本程序未做消息编号或重排处理,适用于对实时性要求不高的聊天场景;
  • 消息最大长度为1024字节,超出部分会被截断,如需支持更长消息可调整缓冲区大小。