用Rust实现TCP Echo服务器

349 阅读1分钟

本文主要讲解《Rustで始めるネットワークプログラミング》一书1.4节的内容——用Rust实现一个TCP Echo服务器。

准备工作

首先通过cargo命令新建一个项目。

$ cargo new tcp-echo
     Created binary (application) `tcp-echo` package

接下来编辑项目目录中的Cargo.toml文件,添加所需的caret。

[dependencies]
log = "0.4"
env_logger = "0.6.1"
failure = "0.1.5"

至此,准备工作就绪。在开始分析TCP Echo服务器的代码之前,我们先来确认一下该服务器的功能。

TCP Echo服务器的主要功能

TCP Echo服务器的主要功能如下:

  • 可以同时处理来自多个客户端的连接
  • 将接收到的数据(消息)输出到标准输出
  • 将接收到的数据原样发回客户端

TCP Echo服务器的代码

代码清单 tcp-echo/src/main.rsmain函数)

#[macro_use]
extern crate log;
use std::env;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::{str, thread};
​
fn main() {
    env::set_var("RUST_LOG", "debug");
    env_logger::init();
​
    let address = "127.0.0.1:1234";
    serve(address).unwrap_or_else(|e| error!("{}", e));
}

main()函数中,我们初始化了日志模块,并指定了TCP Echo服务器的地址(127.0.0.1:1234),最后调用server()函数启动该服务器。

server()函数返回值的类型是Result,这意味着要么返回一个成功的结果,要么返回一个错误。所以这里使用unwrap_or_else(fallback_fn)来处理该返回值(只有在得到错误结果时才会调用fallback_fn)。

代码清单 tcp-echo/src/main.rs(serve函数)

/**
 * 监听指定套接字地址上的TCP连接
 */
pub fn serve(address: &str) -> Result<(), failure::Error> {
    let listener = TcpListener::bind(address)?;
    loop {
        let (stream, _) = listener.accept()?;
        // 生成一个线程来处理连接
        thread::spawn(move || {
            handler(stream).unwrap_or_else(|error| error!("{:?}", error));
        });
    }
}

处理TCP通信时主要会用到TCPListerTCPStream这两种类型。

TcpListener::bind(address)?将创建一个监听TCP连接的套接字(socket)。结尾处的?说明bind()也将返回一个Result值,并且我们不希望在这里捕捉并处理错误,而是直接沿着函数调用链向上传播可能的错误。

? 的行为取决于函数是返回了成功结果还是错误。如果是成功结果,那么它会获取其中的成功值,此时listener变量的类型不是Result<TcpListener, io::Error> ,而是简单的TcpListener。而如果是错误,那么?会立即从所在函数返回,将错误结果向上传播到调用链中。

注意⚠️ ?只能作用于返回类型为Result的函数。

listener变量(类型为TcpListener)所存储的套接字用于等待来自客户端的连接建立请求。当客户端与服务器通过三次握手建立好连接后,内核就会相应地在内部队列中创建一个已建立连接的套接字。

accept()的作用是返回(从队列中取出)一个已建立连接的套接字(类型为TcpStream),这个套接字负责客户端与服务器之间的数据交换。accept()后面的?的作用与bind()之后的?作用相同。

若内核中的队列为空,accept()将会阻塞。

每建立好一个连接,我们就通过thread::spawn()启动一个线程,并调用handler()处理该连接,以此来同时处理来自多个客户端的连接。thread::spawn()的参数是一个闭包,这里的move表示将值的所有权(ownership)移交给线程。

下面我们再来看一看handler()函数的功能。

关于bind()函数的返回类型

虽然从函数定义pub fn bind<A: ToSocketAddrs>(addr: A) -> io::Result<TcpListener>来看,bind()的返回值类型似乎应该是io::Result<TcpListener>,但由于标准库的io模块包括下面这行代码:

pub type Result<T> = result::Result<T, Error>;

所以io::Result<TcpListener>实际上是io::Result<TcpListener, io::Error>的别名。

其实这里还有一个问题:bind()可能返回错误类型io::Error,而该函数所在的serve()函数返回的错误类型却是failure::Error,那么前者是如何转换成后者的呢?

简单来说,这是因为实现了failure::Fail trait的类型(如io::Error)只要使用?操作符就可以变为failure::Error类型。

代码清单 tcp-echo/src/main.rs(handler函数)

/**
 * 等待来自客户端的数据(消息)并在收到后原样返回相同的内容
 */
fn handler(mut stream: TcpStream) -> Result<(), failure::Error> {
    debug!("Handling data from {}", stream.peer_addr()?);
    let mut buffer = [0u8; 1024];
    loop {
        let nbytes = stream.read(&mut buffer)?;
        if nbytes == 0 {
            debug!("Connection closed.");
            return Ok(());
        }
        print!("{}", str::from_utf8(&buffer[..nbytes])?);
        stream.write_all(&buffer[..nbytes])?;
    }
}

stream.read() 用于等待读取来自客户端的数据(等待来自客户端的数据”流入“)并返回已读取的字节数。 read() 读到EOF时(意味着连接断开)返回0,此时通过return退出loop循环,终止线程(退出线程处理函数)。另外,退出循环后就离开了stream的作用域,导致stream被drop,进而关闭连接。

write_all()则用于将读取到的字节序列原样返回给客户端。

运行并调试TCP Echo服务器

通过执行以下命令即可启动TCP Echo服务器。

$ cargo run

我们可以先使用telnet来调试该服务器。

$ telnet 127.0.0.1 1234

在telent中,随意输入一些字符,按下回车键后就可以看到屏幕上原样输出了刚刚输入的内容。

至此,只需不到50行Rust代码,我们就实现了一个TCP Echo服务器的雏形。