3. 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
验证步骤与判定标准:
- 服务器终端显示每个客户端注册时的地址信息,如
[服务器] 客户端 127.0.0.1:52340 已注册; - 在客户端A输入任意文本(如
hello)并回车,客户端B终端应显示[127.0.0.1:52340] hello(端口号随实际分配变化); - 在客户端B输入回复,客户端A应能收到同样格式的消息;
- 客户端A发送的消息不会回显到自身终端,仅由服务器转发给其他客户端;
- 启动第三个客户端并发送消息,前两个客户端均能收到,验证多客户端广播能力。
满足以上条件即表示程序正确实现了UDP多客户端聊天中转功能。
UDP消息转发流程
关键说明
- 服务器使用
HashSet存储客户端地址,自动去重,重复注册不会产生冗余记录; - 客户端接收线程使用
try_clone()复制套接字描述符,实现同一套接字同时收发; - UDP不保证消息有序到达,本程序未做消息编号或重排处理,适用于对实时性要求不高的聊天场景;
- 消息最大长度为1024字节,超出部分会被截断,如需支持更长消息可调整缓冲区大小。