§1 tokio
前言
本次主要是通过你好 Tokio | Tokio - 一个异步 Rust 运行时来学习。
开始是,但是后面变味了。
步骤
首先应该下载tokio和mini-redis,这里的mini-redis是一个数据库。
新建项目并配置依赖项。
tokio = { version = "1", features = ["full"] }
mini-redis = "0.4"
在main.rs中填入:
use mini_redis::{client, Result};
#[tokio::main]
async fn main() -> Result<()> {
// Open a connection to the mini-redis address.
let mut client = client::connect("127.0.0.1:6379").await?;
// Set the key "hello" with value "world"
client.set("hello", "world".into()).await?;
// Get key "hello"
let result = client.get("hello").await?;
println!("got value from the server; result={:?}", result);
Ok(())
}
要运行该项目,首先要打开mini-redis数据库服务。在终端中执行mini-redis-server以打开它。
运行项目使用cargo run。
同步异步、串行并行、IO/CPU密集
同步阻塞,就是完全没有使用这些高级特性的程序,它的执行方式。总共只有一个线程,所有函数顺序执行。一个函数在执行时会阻塞线程,其他函数需要等待该函数执行完毕。
可以考虑在Java中发生的情况,附加到事件上的函数,它们的执行就默认是同步阻塞的。
tokio在只使用async、future、await时,是异步执行的,还是只有一个线程,但是一个函数执行到await时,会切换到其他函数已经等待到的await,执行它的后面的任务。于是在等待future的时间里不会阻塞。
上面两者都是串行的,只使用单线程执行单个或多个任务。
并行需要多个线程,于是在tokio函数中必须要出现管理线程的#[tokio::main],并且在其中有handle.push或者tokio::spawn等传递闭包到新线程执行的操作。
IO密集,CPU仅作很少的计算,大多数时间都在等待信息输入,如命令行窗口输入、HTTP请求等,内部可以想象成是一个条件循环。IO密集型任务,特点是使用异步编程多个IO任务时,消耗时间基本相同。
CPU密集,基本上是CPU在进行计算,通常使用并行编程。
实例:server和client传递字符串
并没有很好读的风格。
// client.rs
use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
#[tokio::main]
async fn main() -> io::Result<()> {
let addr = "127.0.0.1:8080";
println!("Connecting to server at {}...", addr);
let mut stream = TcpStream::connect(addr).await?;
let local_addr = stream.local_addr()?;
println!("Connected from local port: {}", local_addr.port());
let stdin = io::stdin();
let mut reader = BufReader::new(stdin);
loop {
let mut message = String::new();
reader.read_line(&mut message).await?;
// println!("message is: {:?}", message);
stream.write_all(message.as_bytes()).await?;
}
}
// server.rs
use tokio::io::{self, AsyncBufReadExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
async fn process_socket(stream: TcpStream) -> io::Result<()> {
let mut buf = BufReader::new(stream);
loop {
let mut string = String::new();
println!("try to read line ...");
buf.read_line(&mut string).await?;
if string.is_empty() {
println!("client closed connection");
return Ok(());
}
println!("read line: {:?}", string);
}
}
#[tokio::main]
async fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:8080").await?;
println!("server listen on 127.0.0.1");
loop {
let (stream, peek_addr) = listener.accept().await?;
println!("connect established with {}", peek_addr);
process_socket(socket).await?;
}
}
从这里可以看出一些通用的结构。
-
Listen:
TcpListener::bind创建一个TcpListener实例(对象),用于监听网络上的TCP连接请求。~~或者说,Rust程序请求了一个socket用于建立TCP连接。~~实际上的等待过程,是在accept被调用时,即下面。 -
connect和accept:客户端向服务端发送连接请求使用
TcpStream::connect(Addr),连接成功则创建一个TcpStream实例,并使一个TCP连接正式建立。每接受一个连接请求,就能建立一个新的TCP连接。通过let (stream, peek_addr) = listener.accept().await?得到一个TcpStream实例。 -
send和recv:发送通常不需要缓存,但是接收需要缓存。所以客户端发送文本可以直接调用
stream.write系列方法,以明确要发送的文本,把文本传给客户端;而服务端要接收文本,则需要用BufRead::new(stream)创建一个BufRead实例,再调用reader.read系列方法,从缓冲区中读取文本。 -
close:技术力不够写不出。
和《计算机网络》书中相同。所以接下来是尝试弄清socket的含义。
Socket含义分析
目前TCP/IP协议都在操作系统中,并且通过操作系统的Socket API完全封装。要使用TCP/IP协议传输数据,应用进程要使用Socket API。Socket API是在系统上的API(应用编程接口)。
应用进程发出一个系统调用,即socket系统调用(关闭socket,通过close系统调用),操作系统为进程创建一个socket数据结构,并给进程一个对应的socket描述符。socket数据结构里有本地/远地IP/端口等的信息。socket描述符会存在进程的socket描述符表中,与socket数据结构对应。
操作系统在处理进程的socket系统调用时,操作系统创建一个socket给进程,这是一个总的说法。此时的socket包括SocketAddr、指向分配的系统资源的句柄(或者类似的东西)等含义。
由于socket包含本地远地的SocketAddr(四元组),因此一个socket会对应一个TCP连接。但是正在accept的服务端也有一个socket。通常会被叫做监听socket和已连接socket。
§2 reqwest
reqwest是Rust生态的一个HTTP客户端库。它的主要作用仅是发起HTTP请求,而不能提供。
HTTP指令的传递,有发送请求、接收响应(客户端),有接收请求、发送响应(服务端)。这建立在TCP连接已经建立的基础上。
comment 评论区
star_outline 咱快来抢个沙发吧!