4. 非阻塞TCP服务器

任务说明

实现一个基于非阻塞socket与poll系统调用的并发TCP回显服务器。服务器监听指定端口,使用单个线程通过poll同时管理多个客户端连接,对每个客户端收到的数据原样返回。该案例用于演示Rust中非阻塞I/O与事件驱动编程模型,不引入async/await或第三方异步运行时。

核心实现

use std::io::{self, Read, Write};
use std::net::{TcpListener, TcpStream};
use std::os::unix::io::{AsRawFd, RawFd};
use std::collections::HashMap;
use libc::pollfd;

const POLL_TIMEOUT_MS: i32 = 1000;

fn set_nonblocking(stream: &TcpStream) -> io::Result<()> {
    let fd = stream.as_raw_fd();
    let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
    if flags == -1 {
        return Err(io::Error::last_os_error());
    }
    let result = unsafe { libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) };
    if result == -1 {
        return Err(io::Error::last_os_error());
    }
    Ok(())
}

fn main() -> io::Result<()> {
    let addr = "127.0.0.1:8080";
    let listener = TcpListener::bind(addr)?;
    listener.set_nonblocking(true)?;
    println!("非阻塞TCP服务器监听 {}", addr);

    // fd -> (TcpStream, 读缓冲区)
    let mut clients: HashMap<RawFd, (TcpStream, Vec<u8>)> = HashMap::new();
    let listener_fd = listener.as_raw_fd();

    loop {
        // 构建pollfd数组
        let mut pollfds: Vec<pollfd> = Vec::new();
        pollfds.push(pollfd {
            fd: listener_fd,
            events: libc::POLLIN,
            revents: 0,
        });
        for fd in clients.keys() {
            pollfds.push(pollfd {
                fd: *fd,
                events: libc::POLLIN,
                revents: 0,
            });
        }

        // 等待事件
        let ret = unsafe { libc::poll(pollfds.as_mut_ptr(), pollfds.len() as libc::nfds_t, POLL_TIMEOUT_MS) };
        if ret == -1 {
            return Err(io::Error::last_os_error());
        }
        if ret == 0 {
            continue; // 超时,继续循环
        }

        // 处理监听socket上的新连接
        if pollfds[0].revents & libc::POLLIN != 0 {
            loop {
                match listener.accept() {
                    Ok((stream, peer_addr)) => {
                        set_nonblocking(&stream)?;
                        let fd = stream.as_raw_fd();
                        println!("新连接: {} (fd={})", peer_addr, fd);
                        clients.insert(fd, (stream, Vec::new()));
                    }
                    Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break,
                    Err(e) => return Err(e),
                }
            }
        }

        // 处理客户端socket上的数据
        for pollfd in pollfds.iter().skip(1) {
            if pollfd.revents & libc::POLLIN != 0 {
                let fd = pollfd.fd;
                if let Some((stream, buf)) = clients.get_mut(&fd) {
                    let mut tmp = [0u8; 1024];
                    loop {
                        match stream.read(&mut tmp) {
                            Ok(0) => {
                                // 连接关闭
                                println!("连接关闭: fd={}", fd);
                                clients.remove(&fd);
                                break;
                            }
                            Ok(n) => {
                                buf.extend_from_slice(&tmp[..n]);
                                // 回显所有已读数据
                                if let Err(e) = stream.write_all(buf) {
                                    eprintln!("写入错误 fd={}: {}", fd, e);
                                    clients.remove(&fd);
                                    break;
                                }
                                buf.clear();
                            }
                            Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => break,
                            Err(e) => {
                                eprintln!("读取错误 fd={}: {}", fd, e);
                                clients.remove(&fd);
                                break;
                            }
                        }
                    }
                }
            }
        }
    }
}

依赖配置

在Cargo.toml中添加libc依赖:

[dependencies]
libc = "0.2"

运行与验证

启动服务器:

cargo run

预期输出:非阻塞TCP服务器监听 127.0.0.1:8080

验证并发能力:打开两个终端,分别执行:

# 终端1
nc 127.0.0.1 8080
# 输入 hello,应收到 hello

# 终端2
nc 127.0.0.1 8080
# 输入 world,应收到 world

判定标准:

  1. 两个客户端可同时保持连接,互不阻塞;
  2. 任一客户端发送数据,服务器立即原样返回;
  3. 关闭任一客户端,服务器不退出,继续服务其他连接;
  4. 服务器终端打印新连接的地址与fd编号,以及连接关闭时的fd编号。
poll事件循环与连接管理 主循环 poll(fds, timeout) 有事件? 超时 继续循环 listener可读? accept() 新连接 加入clients表 client可读? read() → write_all() 回显数据 clients: HashMap<fd, (TcpStream, Vec<u8>)> fd=7 → 127.0.0.1:52340 读缓冲: Vec<u8> fd=8 → 127.0.0.1:52352 读缓冲: Vec<u8> fd=9 → 127.0.0.1:52368 读缓冲: Vec<u8> ··· 连接关闭时 remove(&fd) 事件就绪 关键机制 • 非阻塞: O_NONBLOCK • WouldBlock → 继续循环 • read()==0 → 连接关闭 • 单线程事件驱动 • 无async/await pollfd 结构 {fd, events: POLLIN, revents} 超时: 1000ms 无事件时阻塞等待 有事件立即处理

代码说明

服务器启动后将监听socket设为非阻塞,主循环每次构建包含监听fd与所有客户端fd的pollfd数组,调用poll等待事件。当监听fd可读时,循环accept直到返回WouldBlock,确保接收完所有待处理连接。当客户端fd可读时,循环读取直到WouldBlock,将读到的数据原样写回。连接关闭(读返回0)或发生错误时,从clients表中移除对应fd。整个服务器仅使用单线程,通过poll实现并发,无第三方异步依赖。