LoginSignup
16
13

More than 3 years have passed since last update.

[Rust] gRPC in Rustしてみる その1

Last updated at Posted at 2019-07-30

gRPCしたくなってきたので、Rustで試してみることにしました

環境

Rustを使ったふつうのコマンドラインツール開発 - Qiita
参考にさせて頂きました :bow:

.proto ファイルを書いてみる

Language Guide (proto3) | Protocol Buffers | Google Developers
Proto3 Language Guide(和訳) - Qiita

proto/hello.proto
syntax = "proto3";

package example.helloworld;

message Request {
  string name = 1;
  int32 age = 2;
}

message Response {
  string message = 1;
}

コンパイルする

Cargo.toml
[package]
name = "grpc-practice-rs"
version = "0.1.0"
authors = ["yagince <xxx@gmail.com>"]
edition = "2018"

[dependencies]
protobuf = "2.8.0"

[build-dependencies]
grpcio-compiler = "0.5.0-alpha.2"
which = "2.0.1"
build.rs
use std::process::Command;
use which::which;

fn main() {
    println!("gen protoc");
    let grpc_rust_plugin_path = which("grpc_rust_plugin")
        .expect("fail get extension path `grpc_rust_plugin`")
        .to_str()
        .expect("fail path string")
        .to_string();

    let output = Command::new("protoc")
        .arg("--rust_out=./src/")
        .arg("--grpc_out=./src/")
        .arg(format!("--plugin=protoc-gen-grpc={}", grpc_rust_plugin_path).as_str())
        .arg("proto/helloworld.proto")
        .output()
        .expect("fail command protoc");
    println!("{}", output.status);
    println!("{}", String::from_utf8_lossy(&output.stdout));
    println!("{}", String::from_utf8_lossy(&output.stderr));
}
$ cargo build
   Compiling grpc_practice_rs v0.1.0 (/Users/xxx/grpc-practice-rs)
error: failed to run custom build command for `grpc_practice_rs v0.1.0 (/Users/xxx/grpc-practice-rs)`

Caused by:
  process didn't exit successfully: `/Users/xxx/grpc-practice-rs/target/debug/build/grpc_practice_rs-2230aa86ff6e333c/build-script-build` (exit code: 101)
--- stdout
gen protoc

--- stderr
thread 'main' panicked at 'fail command protoc: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/libcore/result.rs:999:5
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.

怒られました。
protoc コマンドはやっぱり必要でしたね。

protocコマンドを入れる

Protocol Buffers 導入メモ Mac/Win - Qiita

$ brew install protobuf
...
$ protoc --version
libprotoc 3.7.1

もう一度コンパイルしてみる

$ cargo build

成功しました

src/helloworld.rs
// This file is generated by rust-protobuf 2.8.0. Do not edit
// @generated

// https://github.com/Manishearth/rust-clippy/issues/702
#![allow(unknown_lints)]
#![allow(clippy::all)]

#![cfg_attr(rustfmt, rustfmt_skip)]

#![allow(box_pointers)]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(trivial_casts)]
#![allow(unsafe_code)]
#![allow(unused_imports)]
#![allow(unused_results)]
//! Generated file from `helloworld.proto`

use protobuf::Message as Message_imported_for_functions;
use protobuf::ProtobufEnum as ProtobufEnum_imported_for_functions;

/// Generated files are compatible only with the same version
/// of protobuf runtime.
const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_2_8_0;

#[derive(PartialEq,Clone,Default)]
pub struct Request {
    // message fields
    pub name: ::std::string::String,
    pub age: i32,
    // special fields
    pub unknown_fields: ::protobuf::UnknownFields,
    pub cached_size: ::protobuf::CachedSize,
}
...

生成されたファイルを読み込んでみる

src/lib.rs
pub mod helloworld;
src/bin/sample.rs
fn main() {
    let mut req = grpc_practice_rs::helloworld::Request::new();
    req.set_name("test".into());
    req.set_age(100);
    dbg!(req);
}
$ cargo run --bin sample
   Compiling grpc_practice_rs v0.1.0 (/Users/xxx/grpc-practice-rs)
    Finished dev [unoptimized + debuginfo] target(s) in 1.23s
     Running `target/debug/sample`
[src/bin/sample.rs:5] req = name: "test"
age: 100

とりあえず、読み込めましたね。

serviceを定義してみる

In/Outのデータだけ定義していたので、Requestを受け取ってResponseを返すserviceを定義してみます。

proto/helloworld.proto
syntax = "proto3";

package example.helloworld;

message Request {
  string name = 1;
  int32 age = 2;
}

message Response {
  string message = 1;
}

// 追加
service Helloworld {
  rpc Call (Request) returns (Response);
}
$ cargo build

src/helloworld_grpc.rs が生成されました

src/hellworld_grpc.rs
// This file is generated. Do not edit
// @generated

// https://github.com/Manishearth/rust-clippy/issues/702
#![allow(unknown_lints)]
#![allow(clippy::all)]

#![cfg_attr(rustfmt, rustfmt_skip)]

#![allow(box_pointers)]
#![allow(dead_code)]
#![allow(missing_docs)]
#![allow(non_camel_case_types)]
#![allow(non_snake_case)]
#![allow(non_upper_case_globals)]
#![allow(trivial_casts)]
#![allow(unsafe_code)]
#![allow(unused_imports)]
#![allow(unused_results)]

const METHOD_HELLOWORLD_CALL: ::grpcio::Method<super::helloworld::Request, super::helloworld::Response> = ::grpcio::Method {
    ty: ::grpcio::MethodType::Unary,
    name: "/example.helloworld.Helloworld/Call",
    req_mar: ::grpcio::Marshaller { ser: ::grpcio::pb_ser, de: ::grpcio::pb_de },
    resp_mar: ::grpcio::Marshaller { ser: ::grpcio::pb_ser, de: ::grpcio::pb_de },
};

#[derive(Clone)]
pub struct HelloworldClient {
    client: ::grpcio::Client,
}

impl HelloworldClient {
    pub fn new(channel: ::grpcio::Channel) -> Self {
        HelloworldClient {
            client: ::grpcio::Client::new(channel),
        }
    }

    pub fn call_opt(&self, req: &super::helloworld::Request, opt: ::grpcio::CallOption) -> ::grpcio::Result<super::helloworld::Response> {
        self.client.unary_call(&METHOD_HELLOWORLD_CALL, req, opt)
    }

    pub fn call(&self, req: &super::helloworld::Request) -> ::grpcio::Result<super::helloworld::Response> {
        self.call_opt(req, ::grpcio::CallOption::default())
    }

    pub fn call_async_opt(&self, req: &super::helloworld::Request, opt: ::grpcio::CallOption) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver<super::helloworld::Response>> {
        self.client.unary_call_async(&METHOD_HELLOWORLD_CALL, req, opt)
    }

    pub fn call_async(&self, req: &super::helloworld::Request) -> ::grpcio::Result<::grpcio::ClientUnaryReceiver<super::helloworld::Response>> {
        self.call_async_opt(req, ::grpcio::CallOption::default())
    }
    pub fn spawn<F>(&self, f: F) where F: ::futures::Future<Item = (), Error = ()> + Send + 'static {
        self.client.spawn(f)
    }
}

pub trait Helloworld {
    fn call(&mut self, ctx: ::grpcio::RpcContext, req: super::helloworld::Request, sink: ::grpcio::UnarySink<super::helloworld::Response>);
}

pub fn create_helloworld<S: Helloworld + Send + Clone + 'static>(s: S) -> ::grpcio::Service {
    let mut builder = ::grpcio::ServiceBuilder::new();
    let mut instance = s.clone();
    builder = builder.add_unary_handler(&METHOD_HELLOWORLD_CALL, move |ctx, req, resp| {
        instance.call(ctx, req, resp)
    });
    builder.build()
}

こんな感じ。

Serverを実装してみる

grpc-rs/server.rs at f0a26b528fb9648253044a8e46aaa62fab72b072 · pingcap/grpc-rs

↑これを参考に実装してみます

Cargo.toml
[dependencies]
grpcio = "0.5.0-alpha.3"
protobuf = "2.8.0"
futures = "0.1.28"
log = "*"
env_logger = "*"

[build-dependencies]
grpcio-compiler = "0.5.0-alpha.2"
which = "2.0.1"
src/server.rs
use super::helloworld;
use super::helloworld_grpc;
use futures::Future;
use grpcio::{RpcContext, UnarySink};
use log::*;

#[derive(Debug, Clone, PartialEq)]
pub struct Server;

impl helloworld_grpc::Helloworld for Server {
    fn call(
        &mut self,
        ctx: RpcContext,
        req: helloworld::Request,
        sink: UnarySink<helloworld::Response>,
    ) {
        let mut res = helloworld::Response::default();
        res.set_message(format!(
            "Hello name: {} age: {}",
            req.get_name(),
            req.get_age()
        ));

        ctx.spawn(
            sink.success(res)
                .map_err(move |e| error!("failed to reply {:#?}: {:?}", req, e)),
        );
    }
}
src/bin/server.rs
use std::io::Read;
use std::sync::Arc;
use std::{io, thread};

use futures::{sync::oneshot, Future};
use grpc_practice::{helloworld_grpc::create_helloworld, Server};
use grpcio::{Environment, ServerBuilder};
use log::*;

fn main() {
    std::env::set_var("RUST_LOG", "server=info");
    env_logger::init();

    let env = Arc::new(Environment::new(1));
    let service = create_helloworld(Server);

    let mut server = ServerBuilder::new(env)
        .register_service(service)
        .bind("127.0.0.1", 50_051)
        .build()
        .unwrap();

    server.start();
    for &(ref host, port) in server.bind_addrs() {
        info!("listening on {}:{}", host, port);
    }
    let (tx, rx) = oneshot::channel();

    thread::spawn(move || {
        info!("Press ENTER to exit...");
        let _ = io::stdin().read(&mut [0]).unwrap();
        tx.send(())
    });
    let _ = rx.wait();
    let _ = server.shutdown().wait();
}
$ cargo run --bin server
...
[2019-07-29T13:07:34Z INFO  server] listening on 127.0.0.1:50051
[2019-07-29T13:07:34Z INFO  server] Press ENTER to exit...

とりあえず、起動しました。

Clientを実装してみる

呼ぶ側を作らないことには動作してる様子がわからないので、書いてみます。

grpc-rs/client.rs at f0a26b528fb9648253044a8e46aaa62fab72b072 · pingcap/grpc-rs

こちらを参考に実装してみます。

src/bin/client.rs
use std::sync::Arc;

use grpc_practice::helloworld::Request;
use grpc_practice::helloworld_grpc::HelloworldClient;
use grpcio::{ChannelBuilder, EnvBuilder};
use log::*;

fn main() {
    std::env::set_var("RUST_LOG", "client=info");
    env_logger::init();

    let env = Arc::new(EnvBuilder::new().build());
    let ch = ChannelBuilder::new(env).connect("localhost:50051");

    let client = HelloworldClient::new(ch);

    let mut req = Request::default();
    req.set_name("world".to_owned());
    req.set_age(100);
    let reply = client.call(&req).expect("rpc");
    info!("Helloworld received: {}", reply.get_message());
}
$ cargo run --bin client
[2019-07-30T12:48:55Z INFO  client] Helloworld received: Hello name: world age: 100

とりあえず、疎通確認はできました。

おまけ: Rubyから使う

せっかくなので、別の言語のClientからも呼んでみようかと思います。

Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "grpc"
gem "grpc-tools"
$ mkdir ruby && cd ruby
$ grpc_tools_ruby_protoc -I ../proto --ruby_out=. --grpc_out=. ../proto/helloworld.proto
helloworld_pb.rb
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# source: helloworld.proto

require 'google/protobuf'

Google::Protobuf::DescriptorPool.generated_pool.build do
  add_file("helloworld.proto", :syntax => :proto3) do
    add_message "example.helloworld.Request" do
      optional :name, :string, 1
      optional :age, :int32, 2
    end
    add_message "example.helloworld.Response" do
      optional :message, :string, 1
    end
  end
end

module Example
  module Helloworld
    Request = Google::Protobuf::DescriptorPool.generated_pool.lookup("example.helloworld.Request").msgclass
    Response = Google::Protobuf::DescriptorPool.generated_pool.lookup("example.helloworld.Response").msgclass
  end
end
helloworld_services_pb.rb
# Generated by the protocol buffer compiler.  DO NOT EDIT!
# Source: helloworld.proto for package 'example.helloworld'

require 'grpc'
require 'helloworld_pb'

module Example
  module Helloworld
    module Helloworld
      class Service

        include GRPC::GenericService

        self.marshal_class_method = :encode
        self.unmarshal_class_method = :decode
        self.service_name = 'example.helloworld.Helloworld'

        rpc :Call, Request, Response
      end

      Stub = Service.rpc_stub_class
    end
  end
end

こんな感じで出来ました。
呼んでみます

client.rb
$LOAD_PATH.unshift(File.expand_path("../", __FILE__))
require 'helloworld_services_pb'

stub = Example::Helloworld::Helloworld::Stub.new('localhost:50051', :this_channel_is_insecure)

req = Example::Helloworld::Request.new(name: "hoge", age: 20000)

pp stub.call(req)
$ bundle exec ruby client.rb
<Example::Helloworld::Response: message: "Hello name: hoge age: 20000">

TODO

  • 認証まわりどうやるんだろうか
  • DBアクセスするような場合のベストプラクティス
  • etc...

今回は以上で、次回へつづく、、、かも。

16
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
13