使用Rust语言构建REST/gRPC/GraphQL的API服务器
这篇文章是 Rust 3 降临日历 2020 年第 20 天的文章。
前一天的文章是 hibi221b 的《前 500 个 Rustlang 仓库》。各个领域都列出了各种各样的仓库。
本文的概要
我在 Rust 中尝试了使用以下 API 进行服务器实现。本文将分享这些示例代码以及参考的文章。
-
- gRPC(RPC 系)
-
- REST
- GraphQL
本文对应的代码已经包含在这个仓库中。
本章的目录
-
- 以下是API相關參考文章的選項:
gRPC
REST
GraphQL
最後
本次讨论的API参考文章
关于Web API的一般描述,可以参考下面的文章。
『WebAPIについての説明』(busyoumono99様)
『翻訳: WebAPI 設計のベストプラクティス』(mserizawa様)
根据Altexsoft公司的文章,用于网络相关的API有许多种类。据说API的架构风格可以按照以下方式进行分类。
-
- RPC 系:XML-RPC, JSON-RPC, gRPC など
-
- SOAP
-
- REST 系:OData など
- GraphQL
请参考以下 Qiita 文章,了解关于本次实施的 gRPC、REST 和 GraphQL 的详细信息。
『REST APIの設計で消耗している感じたときのgRPC入門』(disc99様)
『gRPCって何?』(oohira様)
『0からREST APIについて調べてみた』(masato44gm様)
『GraphQLの全体像とWebApp開発のこれから』(saboyutaka様)
『GraphQLはサーバーサイド実装のベストプラクティスとなるか』(saboyutaka様)
实施示例
gRPC
根据我在2020年12月19日查看的情况,通过grpc-ecosystem/awesome-grpc存储库提供了gRPC的Rust功能的crate如下所示。
grpc-rs – The gRPC library for Rust built on C Core library and futures
grpc-rust – Rust implementation of gRPC
tower-grpc – A client and server gRPC implementation based on Tower
tonic – A native gRPC client & server implementation with async/await support
这次我们将在这些中使用 tonic。我参考了 tonic 仓库中的示例和这篇 Qiita 文章。
文件结构等
文件结构如下。
grpc_hello_server/
proto/
hello_server.proto
src/
client.rs
server.rs
build.rs
Cargo.toml
以下是 Cargo.toml 文件的内容。依赖包包括 tonic、prost 和 tokio。
[package]
name = "grpc-hello-server"
version = "0.1.0"
authors = ["XXXX"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "grpc-hello-server"
path = "src/server.rs"
[[bin]]
name = "grpc-hello-client"
path = "src/client.rs"
[dependencies]
tonic = "0.3.1"
prost = "^0.6"
tokio = { version = "^0.2.13", features = ["macros"] }
[build-dependencies]
tonic-build = "0.3"
以下是gRPC中定义API规范的proto文件。
我会在下面展示实现的hello_server.proto文件内容。
syntax = "proto3";
package hello_server;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloReply);
}
message HelloRequest {
string name = 1;
}
message HelloReply {
string message = 1;
}
我們將編譯已定義的 proto 檔案,並在其他檔案中呼叫它。
我們在 build.rs 檔案中定義了一個函式,用於編譯。
fn main() -> Result<(), Box<dyn std::error::Error>> {
tonic_build::compile_protos("proto/hello_server.proto")?;
Ok(())
}
当上述代码被编译后,就可以使用在proto中定义的client.rs或者server.rs中使用的东西。
请注意,编译本身对于创建client.rs等文件没有问题。
以下是client.rs的内容。
// hello_server.proto 内のアイテムをモジュールとしてインポート
pub mod hello_server {
tonic::include_proto!("hello_server");
}
// 上記モジュール内のアイテムを呼び出す
use hello_server::greeter_client::GreeterClient;
use hello_server::HelloRequest;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut client = GreeterClient::connect("http://[::1]:50051").await?;
let request = tonic::Request::new(HelloRequest {
name: "Tonic".into(),
});
let response = client.say_hello(request).await?;
println!("RESPONSE={:?}", response);
Ok(())
}
以下是server.rs文件的内容。
use tonic::{transport::Server, Request, Response, Status};
// hello_server.proto 内のアイテムをモジュールとしてインポート
pub mod hello_server {
tonic::include_proto!("hello_server");
}
// 上記モジュール内のアイテムを呼び出す
use hello_server::greeter_server::{Greeter, GreeterServer};
use hello_server::{HelloReply, HelloRequest};
#[derive(Debug, Default)]
pub struct MyGreeter {}
#[tonic::async_trait]
impl Greeter for MyGreeter {
// Return an instance of type HelloReply
async fn say_hello(
&self,
// Accept request of type HelloRequest
request: Request<HelloRequest>,
) -> Result<Response<HelloReply>, Status> {
println!("Got a request: {:?}", request);
let reply = HelloReply {
// We must use .into_inner() as the fields of gRPC requests and responses are private
message: format!("Hello {}!", request.into_inner().name).into(),
};
// Send back our formatted greeting
Ok(Response::new(reply))
}
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse().unwrap();
let greeter = MyGreeter::default();
println!("GreeterServer listening on {}", addr);
Server::builder()
.add_service(GreeterServer::new(greeter))
.serve(addr)
.await?;
Ok(())
}
考试方式
-
- 在grpc_hello_server/中运行cargo run –bin grpc-hello-server。
- 在另一个命令提示符中运行cargo run –bin grpc-hello-client。
休息
REST API 可以仅使用 Actix Web 进行实现。我参考了官方网站上的文章。
文件结构等
文件的构成如下所示。
rest_hello_server/
src/
client.rs
server.rs
Cargo.toml
以下是 Cargo.toml 文件的内容。它包含以下依赖项:actix-web、actix-http 和 awc。
[package]
name = "rest-hello-server"
version = "0.1.0"
authors = ["XXXX"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "3.3.2"
actix-http = "^2.2.0"
awc = "^2.0.3"
[[bin]]
name = "rest-hello-server"
path = "src/server.rs"
[[bin]]
name = "rest-hello-client"
path = "src/client.rs"
以下是client.rs文件的内容。
use actix_http::Error;
#[actix_web::main]
async fn main() -> Result<(), Error> {
let client = awc::Client::new();
// Create request builder, configure request and send
let addr: &str = "http://localhost:8080/";
let mut response = client
.get(addr)
.send()
.await?;
// server http response
println!("Response: {:?}", response);
// read response body
let body = response.body().await?;
println!("Downloaded: {:?} bytes, {:?}", body.len(), body);
// Get a response from the second endpoint
let addr: &str = "http://localhost:8080/hey";
let mut response = client
.get(addr)
.send()
.await?;
// server http response
println!("Response: {:?}", response);
// read response body
let body = response.body().await?;
println!("Downloaded: {:?} bytes, {:?}", body.len(), body);
Ok(())
}
以下是server.rs的内容。
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
#[get("/")]
async fn hello() -> impl Responder {
HttpResponse::Ok().body("Hello world!")
}
#[post("/echo")]
async fn echo(req_body: String) -> impl Responder {
HttpResponse::Ok().body(req_body)
}
async fn manual_hello() -> impl Responder {
HttpResponse::Ok().body("Hey there!")
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(hello)
.service(echo)
.route("/hey", web::get().to(manual_hello))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
测试方法
-
- 在 rest_hello_server 目录下运行 cargo run –bin rest-hello-server。
- 在另一个命令提示符下运行 cargo run –bin rest-hello-client。
GraphQL-请使用汉语进行释义。
Juniper 在 GraphQL 的官方网站上展示了以下 GraphQL in Rust 库。这次我们使用了 juniper。
graphql-rust/juniper – GraphQL server library for Rust
async-graphql/async-graphql – A GraphQL server library implemented in Rust
在实施时,我们参考了actix/examples/juniper。
文件结构等
文件结构如下。
graphql_server/
src/
scheme.rs
server.rs
Cargo.toml
我将以下内容展示在Cargo.toml文件中。
[package]
name = "graphql-server"
version = "0.1.0"
authors = ["XXXX"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix-web = "3"
actix-cors = "0.4.0"
serde = "1.0.103"
serde_json = "1.0.44"
serde_derive = "1.0.103"
juniper = "0.14.2"
[[bin]]
name = "graphql-server"
path = "src/server.rs"
在GraphQL中,我们定义了用于交互的数据模式。这次我们在scheme.rs文件中定义了以下模式。
use juniper::FieldResult;
use juniper::RootNode;
use juniper::{GraphQLInputObject, GraphQLObject};
// オブジェクトの定義
#[derive(GraphQLObject)]
#[graphql(description = "Hello struct")]
struct Hello {
id: String,
message: String,
}
#[derive(GraphQLInputObject)]
#[graphql(description = "NewHello struct")]
struct NewHello {
message: String,
}
// クエリの定義(Get用)
pub struct QueryRoot;
#[juniper::object]
impl QueryRoot {
fn human(id: String) -> FieldResult<Hello> {
Ok(Hello {
id: "0".to_owned(),
message: "Hello GraphQL!".to_owned(),
})
}
}
// ミューテーションの定義(Post用)
pub struct MutationRoot;
#[juniper::object]
impl MutationRoot {
fn create_hello(new_hello: NewHello) -> FieldResult<Hello> {
Ok(Hello {
id: "1234".to_owned(),
message: new_hello.message,
})
}
}
pub type Schema = RootNode<'static, QueryRoot, MutationRoot>;
pub fn create_schema() -> Schema {
Schema::new(QueryRoot {}, MutationRoot {})
}
以下是server.rs的内容。这几乎完全是actix/examples/juniper内的示例代码。
use std::io;
use std::sync::Arc;
use actix_cors::Cors;
use actix_web::{web, App, Error, HttpResponse, HttpServer};
use juniper::http::graphiql::graphiql_source;
use juniper::http::GraphQLRequest;
mod schema;
use crate::schema::{create_schema, Schema};
async fn graphiql() -> HttpResponse {
let html = graphiql_source("http://127.0.0.1:8080/graphql");
HttpResponse::Ok()
.content_type("text/html; charset=utf-8")
.body(html)
}
async fn graphql(
st: web::Data<Arc<Schema>>,
data: web::Json<GraphQLRequest>,
) -> Result<HttpResponse, Error> {
let user = web::block(move || {
let res = data.execute(&st, &());
Ok::<_, serde_json::error::Error>(serde_json::to_string(&res)?)
})
.await?;
Ok(HttpResponse::Ok()
.content_type("application/json")
.body(user))
}
#[actix_web::main]
async fn main() -> io::Result<()> {
// Create Juniper schema
let schema = std::sync::Arc::new(create_schema());
// Start http server
HttpServer::new(move || {
App::new()
.data(schema.clone())
.wrap(
Cors::new()
.allowed_methods(vec!["POST", "GET"])
.supports_credentials()
.max_age(3600)
.finish(),
)
.service(web::resource("/graphql").route(web::post().to(graphql)))
.service(web::resource("/graphiql").route(web::get().to(graphiql)))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
有关客户端服务器的问题,GraphQL 实现了 GraphiQL 作为客户端服务器,因此我们将使用它。
考试方式
-
- 在graphql_server文件夹下,执行cargo run –bin graphql-server命令。
- 访问http://127.0.0.1:8080/graphiql,并进行GET/POST操作。
最后
本文介绍了如何使用Rust搭建API服务器的示例。由于还实现了其他各种功能(如并行处理),因此如果能有效利用它们,服务器的实现将更加有趣和高效。