本文主要讲解《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.rs
(main
函数)
#[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通信时主要会用到TCPLister
和TCPStream
这两种类型。
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服务器的雏形。