tokio 初印象
又到了喜闻乐见的初印象环节,这个环节决定了你心中的那 24 盏灯最终是全亮还是全灭。
在本文中,我们将看看本专题的学习目标、tokio
该怎么引入以及如何实现一个 Hello Tokio
项目,最终亮灯还是灭灯的决定权留给各位看官。但我提前说好,如果你全灭了,但却找不到更好的,未来还是得回来真香 :P
专题目标
通过 API 学项目无疑是无聊的,因此我们采用一个与众不同的方式:边学边练,在本专题的最后你将拥有一个 redis
客户端和服务端,当然不会实现一个完整版本的 redis
,只会提供基本的功能和部分常用的命令。
mini-redis
redis
的项目源码可以在这里访问,本项目是从官方地址 fork
而来,在未来会提供注释和文档汉化。
再次声明:该项目仅仅用于学习目的,因此它的文档注释非常全,但是它完全无法作为 redis
的替代品。
环境配置
首先,我们假定你已经安装了 Rust 和相关的工具链,例如 cargo
。其中 Rust 版本的最低要求是 1.45.0
,建议使用最新版 1.58
:
sunfei@sunface $ rustc --version
rustc 1.58.0 (02072b482 2022-01-11)
接下来,安装 mini-redis
的服务器端,它可以用来测试我们后面将要实现的 redis
客户端:
$ cargo install mini-redis
如果下载失败,也可以通过这个地址下载源码,然后在本地通过
cargo run
运行。
下载成功后,启动服务端:
$ mini-redis-server
然后,再使用客户端测试下刚启动的服务端:
$ mini-redis-cli set foo 1
OK
$ mini-redis-cli get foo
"1"
不得不说,还挺好用的,先自我陶醉下 :) 此时,万事俱备,只欠东风,接下来是时候亮"箭"了:实现我们的 Hello Tokio
项目。
Hello Tokio
与简单无比的 Hello World
有所不同(简单?还记得本书开头时,湖畔边的那个多国语言版本的你好,世界
嘛~~),Hello Tokio
它承载着"非常艰巨"的任务,那就是向刚启动的 redis
服务器写入一个 key=hello, value=world
,然后再读取出来,嗯,使用 mini-redis
客户端 :)
分析未到,代码先行
在详细讲解之前,我们先来看看完整的代码,让大家有一个直观的印象。首先,创建一个新的 Rust
项目:
$ cargo new my-redis
$ cd my-redis
然后在 Cargo.toml
中添加相关的依赖:
[dependencies]
tokio = { version = "1", features = ["full"] }
mini-redis = "0.4"
接下来,使用以下代码替换 main.rs
中的内容:
use mini_redis::{client, Result}; #[tokio::main] async fn main() -> Result<()> { // 建立与mini-redis服务器的连接 let mut client = client::connect("127.0.0.1:6379").await?; // 设置 key: "hello" 和 值: "world" client.set("hello", "world".into()).await?; // 获取"key=hello"的值 let result = client.get("hello").await?; println!("从服务器端获取到结果={:?}", result); Ok(()) }
不知道你之前启动的 mini-redis-server
关闭没有,如果关了,记得重新启动下,否则我们的代码就是意大利空气炮。
最后,运行这个项目:
$ cargo run
从服务器端获取到结果=Some("world")
Perfect, 代码成功运行,是时候来解释下其中蕴藏的至高奥秘了。
原理解释
代码篇幅虽然不长,但是还是有不少值得关注的地方,接下来我们一起来看看。
#![allow(unused)] fn main() { let mut client = client::connect("127.0.0.1:6379").await?; }
client::connect
函数由mini-redis
包提供,它使用异步的方式跟指定的远程 IP
地址建立 TCP 长连接,一旦连接建立成功,那 client
的赋值初始化也将完成。
特别值得注意的是:虽然该连接是异步建立的,但是从代码本身来看,完全是同步的代码编写方式,唯一能说明异步的点就是 .await
。
什么是异步编程
大部分计算机程序都是按照代码编写的顺序来执行的:先执行第一行,然后第二行,以此类推(当然,还要考虑流程控制,例如循环)。当进行同步编程时,一旦程序遇到一个操作无法被立即完成,它就会进入阻塞状态,直到该操作完成为止。
因此同步编程非常符合我们人类的思维习惯,是一个顺其自然的过程,被几乎每一个程序员所喜欢(本来想说所有,但我不敢打包票,毕竟总有特立独行之士)。例如,当建立 TCP 连接时,当前线程会被阻塞,直到等待该连接建立完成,然后才往下继续进行。
而使用异步编程,无法立即完成的操作会被切到后台去等待,因此当前线程不会被阻塞,它会接着执行其它的操作。一旦之前的操作准备好可以继续执行后,它会通知执行器,然后执行器会调度它并从上次离开的点继续执行。但是大家想象下,如果没有使用 await
,而是按照这个异步的流程使用通知 -> 回调的方式实现,代码该多么的难写和难读!
好在 Rust 为我们提供了 async/await
的异步编程特性,让我们可以像写同步代码那样去写异步的代码,也让这个世界美好依旧。
编译时绿色线程
一个函数可以通过async fn
的方式被标记为异步函数:
#![allow(unused)] fn main() { use mini_redis::Result; use mini_redis::client::Client; use tokio::net::ToSocketAddrs; pub async fn connect<T: ToSocketAddrs>(addr: T) -> Result<Client> { // ... } }
在上例中,redis
的连接函数 connect
实现如上,它看上去很像是一个同步函数,但是 async fn
出卖了它。
async fn
异步函数并不会直接返回值,而是返回一个 Future
,顾名思义,该 Future
会在未来某个时间点被执行,然后最终获取到真实的返回值 Result<Client>
。
async/await 的原理就算大家不理解,也不妨碍使用
tokio
写出能用的服务,但是如果想要更深入的用好,强烈建议认真读下本书的async/await
异步编程章节,你会对 Rust 的异步编程有一个全新且深刻的认识。
由于 async
会返回一个 Future
,因此我们还需要配合使用 .await
来让该 Future
运行起来,最终获得返回值:
async fn say_to_world() -> String { String::from("world") } #[tokio::main] async fn main() { // 此处的函数调用是惰性的,并不会执行 `say_to_world()` 函数体中的代码 let op = say_to_world(); // 首先打印出 "hello" println!("hello"); // 使用 `.await` 让 `say_to_world` 开始运行起来 println!("{}", op.await); }
上面代码输出如下:
hello
world
而大家可能很好奇 async fn
到底返回什么吧?它实际上返回的是一个实现了 Future
特征的匿名类型: impl Future<Output = String>
。
async main
在代码中,使用了一个与众不同的 main
函数 : async fn main
,而且是用 #[tokio::main]
属性进行了标记。异步 main
函数有以下意义:
.await
只能在async
函数中使用,如果是以前的fn main
,那它内部是无法直接使用async
函数的!这个会极大的限制了我们的使用场景- 异步运行时本身需要初始化
因此 #[tokio::main]
宏在将 async fn main
隐式的转换为 fn main
的同时还对整个异步运行时进行了初始化。例如以下代码:
#[tokio::main] async fn main() { println!("hello"); }
将被转换成:
fn main() { let mut rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { println!("hello"); }) }
最终,Rust 编译器就愉快地执行这段代码了。
cargo feature
在引入 tokio
包时,我们在 Cargo.toml
文件中添加了这么一行:
tokio = { version = "1", features = ["full"] }
里面有个 features = ["full"]
可能大家会比较迷惑,当然,关于它的具体解释在本书的 Cargo 详解专题 有介绍,这里就简单进行说明。
Tokio
有很多功能和特性,例如 TCP
,UDP
,Unix sockets
,同步工具,多调度类型等等,不是每个应用都需要所有的这些特性。为了优化编译时间和最终生成可执行文件大小、内存占用大小,应用可以对这些特性进行可选引入。
而这里为了演示的方便,我们使用 full
,表示直接引入所有的特性。
总结
大家对 tokio
的初印象如何?可否 24 灯全亮通过?
总之,tokio
做的事情其实是细雨润无声的,在大多数时候,我们并不能感觉到它的存在,但是它确实是异步编程中最重要的一环(或者之一),深入了解它对我们的未来之路会有莫大的帮助。
接下来,正式开始 tokio
的学习之旅。