From 531d64a32e411a009a22444e597f692e013df16b Mon Sep 17 00:00:00 2001 From: sunface Date: Sun, 23 Jan 2022 20:27:38 +0800 Subject: [PATCH] =?UTF-8?q?add=20tokio-=E5=88=9D=E5=8D=B0=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- book/contents/SUMMARY.md | 3 +- book/contents/tokio/getting-startted.md | 198 ++++++++++++++++++++++++ book/contents/tokio/overview.md | 2 +- 3 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 book/contents/tokio/getting-startted.md diff --git a/book/contents/SUMMARY.md b/book/contents/SUMMARY.md index 2df43393..a00c9c05 100644 --- a/book/contents/SUMMARY.md +++ b/book/contents/SUMMARY.md @@ -103,7 +103,8 @@ - [实践应用:Async Web服务器](async/web-server.md) - [tokio使用指南 doing](tokio/intro.md) - - [tokio简介](tokio/overview.md) + - [tokio概览](tokio/overview.md) + - [tokio初印象](tokio/getting-startted.md) diff --git a/book/contents/tokio/getting-startted.md b/book/contents/tokio/getting-startted.md new file mode 100644 index 00000000..605bc253 --- /dev/null +++ b/book/contents/tokio/getting-startted.md @@ -0,0 +1,198 @@ +# 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 长连接,一旦连接建立成功,那 `clien` 的赋值初始化也将完成。 + +特别值得注意的是:虽然该连接是异步建立的,但是从代码本身来看,完全是**同步的代码编写方式**,唯一能说明异步的点就是 `.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(addr: T) -> Result { + // ... +} +``` + +在上例中,`redis` 的连接函数 `connect` 实现如上,它看上去很像是一个同步函数,但是 `async fn` 出卖了它。 +`async fn` 异步函数并不会直接返回值,而是返回一个 `Future`,顾名思义,该 `Future` 会在未来某个时间点被执行,然后最终获取到真实的返回值 `Result`。 + +> 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("hello, world") +} + +#[tokio::main] +async fn main() { + // 此处的函数调用是惰性的,并不会执行 `say_world()` 函数体中的代码 + let op = say_to_world(); + + // 首先打印出 "hello" + println!("hello"); + + // 使用 `.await` 让 `say_world` 开始运行起来 + op.await; +} +``` + +上面代码输出如下: +```shell +hello +world +``` + +而大家可能很好奇 `async fn` 到底返回什么吧?它实际上返回的是一个实现了 `Future` 特征的匿名类型: `impl Future`。 + +#### 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详解专题]` 有介绍,这里就简单进行说明, + +`Tokio` 有很多功能和特性,例如 `TCP`,`UDP`,`Unix sockets`,同步工具,多调度类型等等,不是每个应用都需要所有的这些特性。为了优化编译时间和最终生成可执行文件大小、内存占用大小,应用可以对这些特性进行可选引入。 + +而这里为了演示的方便,我们使用 `full` ,表示直接引入所有的特性。 + +## 总结 +大家对 `tokio` 的初印象如何?可否24灯全绿通过? + +总之,`tokio` 做的事情其实是细雨润无声的,在大多数时候,我们并不能感觉到它的存在,但是它确实是异步编程中最重要的一环(或者之一),深入了解它对我们的未来之路会有莫大的帮助。 + +接下来,正式开始 `tokio` 的学习之旅。 \ No newline at end of file diff --git a/book/contents/tokio/overview.md b/book/contents/tokio/overview.md index af0a587a..3be6bd95 100644 --- a/book/contents/tokio/overview.md +++ b/book/contents/tokio/overview.md @@ -1,4 +1,4 @@ -# tokio简介 +# tokio概览 对于 Async Rust,最最重要的莫过于底层的异步运行时,它提供了执行器、任务调度、异步API等核心服务。简单来说,使用 Rust 提供的 `async/.await` 特性编写的异步代码要运行起来,就必须依赖于异步运行时,否则这些代码将毫无用处。 ## 异步运行时