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

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 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` 的学习之旅。