この記事はRust 3 Advent Calendar 2020の 20 日目の記事です。
前日の記事はhibi221b様の『top 500 Rustlang repositories』です。分野問わず様々なリポジトリがリストアップされております。
本記事のまとめ
Rust で下記 API を用いるサーバーの実装を試しました。本記事ではそれらのサンプルコードおよび参考にした記事を共有します。
- gRPC(RPC 系)
- REST
- GraphQL
こちらのリポジトリに、本記事に関連するコードが含まれております。
本編の目次
今回対象とする API の参考記事
Web API の一般的な説明については、例えば下記の記事が参考になります。
Web 関係で使用される API は多数存在します。altexsoft 社の記事によれば、API のアーキテクチャーのスタイルは下のように分類されるようです。
今回実装する gRPC, REST, GraphQL については下の Qiita 記事に詳しくありますので、そちらをご参照ください。
- 『REST APIの設計で消耗している感じたときのgRPC入門』(disc99様)
- 『gRPCって何?』(oohira様)
- 『0からREST APIについて調べてみた』(masato44gm様)
- 『GraphQLの全体像とWebApp開発のこれから』(saboyutaka様)
- 『GraphQLはサーバーサイド実装のベストプラクティスとなるか』(saboyutaka様)
実装例
gRPC
gRPC のライブラリは grpc-ecosystem/awesome-grpc リポジトリに掲載されています。
上記リポジトリを 2020/12/19 に確認した限りでは、Rust で gRPC の機能を提供しているクレートは下記のようです。
- 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(())
}
上記でコンパイルされると、下記 client.rs
や server.rs
のように proto で定義されたものを利用できるようになります。
※コンパイル自体は 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
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
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 には client サーバーとして QraphiQL が実装されていますので、それを利用します。
テスト方法
-
graphql_server/
内でcargo run --bin graphql-server
を実行。 -
http://127.0.0.1:8080/graphiql
にアクセスして GET/POST を行う。
終わりに
本記事では API サーバーを Rust で立てる例を紹介しました。ほかにもいろいろな機能(並列処理など)が実装されているので、それらを有効に活用すると、サーバー実装がより楽しく、効率的になると思われます。