You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

199 lines
9.1 KiB

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