携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第19天,点击查看活动详情
今天的学习主题是,在 Rust 中如何进行网络开发。主要学习目标是学习如何使用 Rust 标准库及生态系统中的第三方库来进行网络开发,包括创建网络连接、处理网络数据等。让我们开始吧。
01-Rust std::net 简介
Rust 的标准库 std::net 提供了对 TCP/IP 协议栈使用的封装。但 std::net 是同步的,如果需要异步高性能网络,可以使用 tokio::net,而且两者的对外 api 几乎一模一样。
std::net 的内容:
- TCP:TcpListener / TcpStream 分别用来处理服务器的监听和客户端的连接;
- UDP:UdpSocket 用来处理基于 UDP 的 Socket;
- IpAddr 用来处理 IPv4 / IPv6 地址封装;SocketAddr 用来封装 IP + port 信息。
02-处理网络链接的一般方法
TcpListener / TcpStream 的使用方法如下:
/// server 端
// 监听特定端口
let listener = TcpListener::bind("127.0.0.1:9527").await?;
loop {
// 等待客户端连接
let (stream, addr) = listener.accept().await?;
// 通常做法是创建线程来处理客户端连接,避免长时间阻塞主线程
}
/// client 端
// 链接到特定端口
let stream = TcpStream::connect("127.0.0.1:9527").await?;
// 之后就可以与 server 收发消息了
从上面 api 可以看出,Rust 得益于所有权原则和 Drop trait,不需要像 Java 一样,写大量释放资源的样板代码。
02.1-如何处理大量连接?
按照上面使用新线程处理客户端连接的方式,系统存在一个瓶颈,即所谓的”C10K”,当连接数达到万这个级别,系统会遭遇到资源和算力的双重瓶颈。从资源角度,Rust 中栈帧默认为 2M,10k 个连接需要20G;从算力角度,太多线程切换上下文消耗太大。
处理大量连接(超过 C10K,到达 C10M),需要通过用户态协程。Rust 中支持异步处理的无栈协程。
02.2-如何处理共享信息?
对于需要线程间共享的数据,如果是只读数据,可以使用 Arc;如果是需要修改的数据,需要使用 Arc<RwLock>。
使用锁时,被锁的资源会影响系统地吞吐量。一种解决思路是,降低锁力度。这在其他编程语言中也能看到类似思路,例如 Java HashMap 中的分段锁。另一种思路是,改变访问共享资源的方式,使其只被一个特定的线程访问;其他线程或者协程只能通过给其发消息的方式与之交互。Rust 下的 channel 都有非常棒的实现。
03-处理网络数据的一般方法
处理客户端 / 服务端通信时,如果使用 HTTP 协议,JSON 通常作为首选数据结构。Rust 下,对 JSON 序列化和反序列化可以通过第三方库 serde。
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct Hello {
name: String,
}
某些情况可能需要自定义客户端 / 服务端通信使用的数据结构,此时通常会使用 protobuf。由于 protobuf 在传输过程中是二进制流,接收消息是需要某种方式来界定消息帧。可以借助 tokio 提供的 length_delimited codec 搭配 Framed 结构使用。
// 服务端接受
let (stream, addr) = listener.accept().await?;
// LengthDelimitedCodec 默认 4 字节长度
let mut stream = Framed::new(stream, LengthDelimitedCodec::new());
// 客户端发送
let stream = TcpStream::connect("127.0.0.1:9527").await?;
let mut stream = Framed::new(stream, LengthDelimitedCodec::new());
本节课程链接《28|网络开发(上):如何使用Rust处理网络请求?》