gRPCしたくなってきたので、Rustで試してみることにしました
環境
- Rust: 1.36
- protoc: 3.7.1
- ライブラリ
- 今回は pingcap/grpc-rs を使うことにしました。
- ( stepancheg/grpc-rustを使わなかったのは、最新バージョンが2018年だったからです。 )
- protoファイルのコンパイルは grpc-rs/compiler at master · pingcap/grpc-rs
- 今回は pingcap/grpc-rs を使うことにしました。
Rustを使ったふつうのコマンドラインツール開発 - Qiita
参考にさせて頂きました
.proto
ファイルを書いてみる
Language Guide (proto3) | Protocol Buffers | Google Developers
Proto3 Language Guide(和訳) - Qiita
syntax = "proto3";
package example.helloworld;
message Request {
string name = 1;
int32 age = 2;
}
message Response {
string message = 1;
}
コンパイルする
[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"
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
成功しました
// 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,
}
...
生成されたファイルを読み込んでみる
pub mod helloworld;
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を定義してみます。
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
が生成されました
// 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
↑これを参考に実装してみます
[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"
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)),
);
}
}
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
こちらを参考に実装してみます。
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からも呼んでみようかと思います。
# 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
# 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
# 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
こんな感じで出来ました。
呼んでみます
$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...
今回は以上で、次回へつづく、、、かも。