使用Rust语言构建REST/gRPC/GraphQL的API服务器

这篇文章是 Rust 3 降临日历 2020 年第 20 天的文章。
前一天的文章是 hibi221b 的《前 500 个 Rustlang 仓库》。各个领域都列出了各种各样的仓库。

本文的概要

我在 Rust 中尝试了使用以下 API 进行服务器实现。本文将分享这些示例代码以及参考的文章。

    • gRPC(RPC 系)

 

    • REST

 

    GraphQL

本文对应的代码已经包含在这个仓库中。

本章的目录

    1. 以下是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(())
}

考试方式

    1. 在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
}

测试方法

    1. 在 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 作为客户端服务器,因此我们将使用它。

考试方式

    1. 在graphql_server文件夹下,执行cargo run –bin graphql-server命令。

 

    访问http://127.0.0.1:8080/graphiql,并进行GET/POST操作。

最后

本文介绍了如何使用Rust搭建API服务器的示例。由于还实现了其他各种功能(如并行处理),因此如果能有效利用它们,服务器的实现将更加有趣和高效。

广告
将在 10 秒后关闭
bannerAds