Rust语言的异步运行时Tokio

242 阅读7分钟

什么是Tokio

Tokio是Rust语言的异步运行时。它提供了编写网络应用程序所需的构建块。这意味着您可以使用Tokio来构建可靠的网络应用程序,而不会影响速度。

例如,下面是一个简单的Tokio程序,它创建一个TCP侦听器并在接受到连接时打印消息:

use tokio::net::TcpListener;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let listener = TcpListener::bind("127.0.0.1:8080").await?;

    loop {
        let (socket, _) = listener.accept().await?;
        println!("Accepted connection from {:?}", socket.peer_addr()?);
    }
}

Tokio的特点

Tokio具有许多优秀的特点,包括可靠性、快速性、易用性和灵活性。

  • 可靠性

Tokio的API具有内存安全性、线程安全性和防滥用性。这有助于防止常见错误,例如无界队列、缓冲区溢出和任务饥饿。

  • 快速

基于Rust,Tokio提供了一个多线程、窃取工作的调度程序。应用程序可以以最小的开销每秒处理数十万个请求。

  • 易用性

async/await减少了编写异步应用程序的复杂性。配合Tokio的实用程序和活跃的生态系统,编写应用程序轻而易举。

  • 灵活性

服务器应用程序的需求与嵌入式设备不同。尽管Tokio带有开箱即用的默认值,但它也提供了调整不同情况所需的旋钮。

Tokio的组成部分

Tokio由许多模块组成,提供了实现Rust异步应用程序所需的各种功能。下面是一些主要组件:

运行时

包括I/O、计时器、文件系统、同步和调度设施,Tokio运行时是异步应用程序的基础。它提供了一个多线程、窃取工作的调度程序,可以处理大量并发任务。

例如,下面是一个简单的Tokio程序,它使用运行时来并发执行多个任务:

use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() {
    let task1 = tokio::spawn(async {
        sleep(Duration::from_millis(100)).await;
        println!("Task 1 completed");
    });

    let task2 = tokio::spawn(async {
        sleep(Duration::from_millis(200)).await;
        println!("Task 2 completed");
    });

    let task3 = tokio::spawn(async {
        sleep(Duration::from_millis(300)).await;
        println!("Task 3 completed");
    });

    task1.await.unwrap();
    task2.await.unwrap();
    task3.await.unwrap();
}

在这个示例中,我们使用Tokio运行时来并发执行多个任务。我们使用tokio::spawn函数来创建新的任务,并使用await关键字来等待它们完成。

Hyper

Hyper是一个支持HTTP 1和2协议的HTTP客户端和服务器库。它提供了构建高性能HTTP应用程序所需的所有工具。

例如,下面是一个简单的Hyper服务器程序,它监听HTTP请求并返回响应:

use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};

async fn handle_request(_req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
    Ok(Response::new(Body::from("Hello World!")))
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = ([127, 0, 0, 1], 3000).into();

    let service = make_service_fn(|_| async { Ok::<_, hyper::Error>(service_fn(handle_request)) });

    let server = Server::bind(&addr).serve(service);

    server.await?;

    Ok(())
}

在这个示例中,我们使用Hyper库来创建一个简单的HTTP服务器。我们定义了一个处理函数handle_request来处理HTTP请求,并返回响应。然后,我们使用make_service_fnservice_fn来创建一个服务,并使用Server::bindserve方法来启动服务器。

Tonic

Tonic是一个无样板代码的gRPC客户端和服务器库。它提供了在网络上公开和使用API的最简单方法。

例如,下面是一个简单的Tonic服务器程序,它实现了一个gRPC服务并监听请求:

use tonic::{transport::Server, Request, Response, Status};

tonic::include_proto!("helloworld");

pub struct MyGreeter {}

#[tonic::async_trait]
impl helloworld::greeter_server::Greeter for MyGreeter {
    async fn say_hello(
        &self,
        request: Request<helloworld::HelloRequest>,
    ) -> Result<Response<helloworld::HelloReply>, Status> {
        let reply = helloworld::HelloReply {
            message: format!("Hello {}!", request.into_inner().name),
        };
        Ok(Response::new(reply))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let addr = "0.0.0.0:50051".parse()?;
    let greeter = MyGreeter {};

    Server::builder()
        .add_service(helloworld::greeter_server::GreeterServer::new(greeter))
        .serve(addr)
        .await?;

    Ok(())
}

在这个示例中,我们使用Tonic库来创建一个简单的gRPC服务器。我们定义了一个结构体MyGreeter并实现了gRPC服务定义中的Greeter trait。然后,我们使用Server::builderadd_service方法来创建一个服务器,并使用serve方法来启动它。

Tower

Tower是一个用于构建可靠客户端和服务器的模块化组件库。它包括重试、负载平衡、过滤、请求限制设施等。

例如,下面是一个简单的Tower程序,它使用重试策略来处理失败的请求:

use std::{time::{Duration, Instant}};
use tower::{ServiceBuilder, ServiceExt};
use tower_retry::{RetryPolicy, Budget};
use hyper::{client::{Client, HttpConnector}, Body};

struct MyPolicy {
    budget: Budget,
}

impl<E> RetryPolicy<hyper::Response<Body>, E> for MyPolicy {
    type Future = futures::future::Ready<Self>;

    fn retry(&self, _: &Request<Body>, result: Result<&hyper::Response<Body>, &E>) -> Option<Self::Future> {
        if let Ok(response) = result {
            if response.status().is_server_error() {
                if self.budget.check() {
                    return Some(futures::future::ready(MyPolicy {
                        budget: self.budget.clone(),
                    }));
                }
            }
        }

        None
    }

    fn clone_request(&self, request: &Request<Body>) -> Option<Request<Body>> {
        request.clone().into()
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let client = Client::new();

    let policy = MyPolicy {
        budget: Budget::new(5, Duration::from_secs(10)),
    };

    let mut svc = ServiceBuilder::new()
        .retry(policy)
        .service(client);

    let uri = "http://httpbin.org/status/500".parse()?;
    let response = svc.ready().await?.call(uri).await?;

    println!("Response: {:?}", response);

    Ok(())
}

在这个示例中,我们使用ServiceBuilder来构建一个带有重试策略的服务。我们定义了一个自定义的重试策略MyPolicy,它使用Budget来限制重试次数。当服务器返回错误时,我们检查预算是否允许重试,并相应地返回一个新的策略实例。

Mio

Mio是一个在操作系统的事件I/O API之上的最小可移植API。它提供了一个统一的接口,用于在不同的操作系统上处理事件驱动的I/O。

例如,下面是一个简单的Mio程序,它使用事件循环来监听TCP连接:

use mio::{Events, Interest, Poll, Token};
use mio::net::TcpListener;
use std::time::Duration;

const SERVER: Token = Token(0);

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut poll = Poll::new()?;
    let mut events = Events::with_capacity(128);

    let addr = "127.0.0.1:8080".parse()?;
    let mut server = TcpListener::bind(addr)?;

    poll.registry().register(&mut server, SERVER, Interest::READABLE)?;

    loop {
        poll.poll(&mut events, Some(Duration::from_millis(100)))?;

        for event in events.iter() {
            match event.token() {
                SERVER => {
                    let (connection, address) = server.accept()?;
                    println!("Accepted connection from: {}", address);
                }
                _ => unreachable!(),
            }
        }
    }
}

在这个示例中,我们使用Mio库来创建一个简单的事件驱动的TCP服务器。我们使用Poll::newEvents::with_capacity方法来创建一个事件循环,并使用TcpListener::bind方法来创建一个TCP侦听器。然后,我们使用poll.registry().register方法将侦听器注册到事件循环中,并使用poll.poll方法来监听事件。当接受到新的连接时,我们打印一条消息。

Tracing

Tracing是一个用于统一洞察应用程序和库的工具。它提供了结构化、基于事件的数据收集和日志记录。

例如,下面是一个简单的Tracing程序,它使用跟踪器来记录应用程序中发生的事件:

use tracing::{debug, error, info, span, warn, Level};

fn main() {
    let subscriber = tracing_subscriber::fmt()
        .with_max_level(Level::TRACE)
        .finish();

    tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");

    let root_span = span!(Level::TRACE, "app_start", work_units = 2);
    let _enter = root_span.enter();

    info!("starting app");
    debug!("debugging some stuff");
    warn!("warning about something");
    error!("an error occurred");

    drop(_enter);
}

在这个示例中,我们使用Tracing库来记录应用程序中发生的事件。我们首先创建一个订阅者并将其设置为全局默认订阅者。然后,我们创建一个根跟踪器并进入它。在根跟踪器内部,我们可以使用不同级别的日志记录函数(如info、debug、warn和error)来记录事件。

Bytes

Bytes是一个用于处理字节流的实用程序库。它提供了丰富的字节数组操作实用程序。

例如,下面是一个简单的Bytes程序,它使用Bytes缓冲区来处理字节数据:

use bytes::{BufMut, BytesMut};

fn main() {
    let mut buffer = BytesMut::with_capacity(1024);

    buffer.put_u8(0x01);
    buffer.put_u16(0x0203);
    buffer.put_u32(0x04050607);

    assert_eq!(buffer[..], [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07]);
}

在这个示例中,我们使用Bytes库来处理字节数据。我们首先创建一个具有指定容量的Bytes缓冲区。然后,我们可以使用各种put方法(如put_u8、put_u16和put_u32)将数据写入缓冲区。

Tokio学习资源

如果您想学习 Rust 的 Tokio 库,可以参考以下资源:

  • Tokio 官方教程:这是 Tokio 官方网站上的教程,涵盖了 Tokio 的基础知识和使用方法。
  • Hello Tokio:这是一个简单的入门教程,介绍了如何使用 Tokio 来编写一个简单的异步应用程序。
  • Async in depth:这篇文章深入探讨了 Rust 的异步运行时模型,可以帮助您更好地理解 Tokio 的工作原理。 from刘金,转载请注明原文链接。感谢!