2020년 Rust로 바이너리 gRPC 서버 클라이언트 구축 - 섹션 3

In the previous post, we covered creating our data schema in the Protocol Buffer (protobuf) format, and using Rust build scripts to compile the protobufs into Rust code.

It is recommended that you follow in order since each post builds off the progress of the previous post.

This is the third post of a 4 part series. If you would like to view this post in a single page, you can follow along on my blog.



서버
이제 우리는 프로토버프가 생성한 코드를 사용하여 서버를 작성할 수 있다.새 모듈에서 서버 (클라이언트) 를 작성할 것입니다.
$ tree
.
├── build.rs
├── Cargo.toml
├── proto
│ └── cli.proto
└── src
    ├── main.rs
    └── remotecli
            ├── mod.rs
            └── server.rs

화물.톰
[package]
name = "cli-grpc-tonic-blocking"
version = "0.1.0"
authors = ["T.J. Telan <[email protected]>"]
edition = "2018"

[dependencies]
# gRPC server/client
tonic = "0.3.0"
prost = "0.6"
# CLI
structopt = "0.3"
# Async runtime
tokio = { version = "0.2", features = ["full"] }

[build-dependencies]
# protobuf->Rust compiler
tonic-build = "0.3.0"
이것은 우리가 화물에 대한 마지막 변경이다.톰.
GRPC 서버/클라이언트를 구현할 때 tonicprost를 추가했습니다.Prost는 Rust에서 프로토콜 버퍼를 실현하는 것으로 패키지의 나머지 부분에 포함할 때 생성된 코드를 컴파일해야 한다.
Tokio는 우리가 사용하고 있는 비동기 운행 시이다.gRPC 서버/클라이언트는 async입니다. 현재 호출된 비동기 함수 코드에서 더 많은 통신을 할 수 있도록 조정main()이 필요합니다.

remotecli/mod.rs
pub mod server;
질서정연하게 실현하기 위해서 우리는 서버와 클라이언트 코드를 각자의 모듈로 한층 더 분리할 것이다.서버부터 시작합니다.

remotecli/server.rs
프런트엔드 CLI 연습과 유사하게 이 파일을 여러 부분으로 나누어 살펴보겠습니다.이 파일 부분의 밑부분에서, 나는 완전한 파일을 복사하거나 붙여넣는 데 사용할 것이다.

수입하다
use tonic::{transport::Server, Request, Response, Status};

// Import the generated rust code into module
pub mod remotecli_proto {
   tonic::include_proto!("remotecli");
}

// Proto generated server traits
use remotecli_proto::remote_cli_server::{RemoteCli, RemoteCliServer};

// Proto message structs
use remotecli_proto::{CommandInput, CommandOutput};

// For the server listening address
use crate::ServerOptions;

// For executing commands
use std::process::{Command, Stdio};
파일의 맨 위에 있는 모듈remotecli_proto을 설명했습니다. 이 모듈의 역할 영역은 이 파일에만 한정됩니다.명칭remotecli_proto은 임의로 명확하게 보기 위해서다.tonic::include_proto!()protobuf가 번역한 녹슨 코드(protobuf 패키지 이름에 따라)를 모듈에 복사/붙여넣기에 효과적입니다.
protobuf 번역의 명칭 약속은 처음에는 약간 혼란스러울 수 있지만 모두 일치합니다.
저희protobufRemoteCLI 서비스는 snake case+_server 또는 _client로 단독 클라이언트와 서버 모듈을 생성합니다.생성된 특징 정의는 Pascal case(알파벳 대문자의 낙타 봉격의 특수한 형식)을 사용한다.
서버에 특정한 생성 코드에서, 우리는traitRemoteCli를 가져왔는데, 이것은 우리에게 같은 함수 서명을 사용하여 gRPC 단점Shell을 실현하도록 요구한다.
그 밖에 우리는 RemoteCliServer를 도입했다. 이것은 생성된 서버로 모든 gRPC 네트워크의 의미를 처리할 수 있지만 실현RemoteCli 특징의 구조를 사용하여 실례화해야 한다.
마지막으로 gRPC 코드에서 가져온 것은 저희protobuf 메시지CommandInputCommandOutput입니다.
서버 탐지 주소를 사용자의 입력을 전달하기 위해 전방에서 ServerOptions 구조를 가져올 것입니다.
마지막으로 우리는 std::process에서 수입한다.CommandStdio - 명령을 실행하고 출력을 캡처하는 데 사용됩니다.

RemoteCli 기능 구현
#[derive(Default)]
pub struct Cli {}

#[tonic::async_trait]
impl RemoteCli for Cli {
   async fn shell(
       &self,
       request: Request<CommandInput>,
   ) -> Result<Response<CommandOutput>, Status> {
       let req_command = request.into_inner();
       let command = req_command.command;
       let args = req_command.args;

       println!("Running command: {:?} - args: {:?}", &command, &args);

       let process = Command::new(command)
           .args(args)
           .stdout(Stdio::piped())
           .spawn()
           .expect("failed to execute child process");

       let output = process
           .wait_with_output()
           .expect("failed to wait on child process");
       let output = output.stdout;

       Ok(Response::new(CommandOutput {
           output: String::from_utf8(output).unwrap(),
       }))
   }
}
우리는 자신의 구조Cli를 성명한다. 왜냐하면 우리는 필요하기 때문이다impl RemoteCli.
우리가 생성한 코드 사용async 방법.우리는trait impl에 #[tonic::async_trait]를 추가하여 서버를 우리의 방법에서 사용할 수 있도록 했다async fn.우리는 단지 한 가지 방법을 정의할 수 있을 뿐이다. async fn shell()나는 여기서 손을 흔들어 함수 서명을 찾았지만, 처음에 함수 서명을 어떻게 작성하는지 배웠다. 생성된 코드에 들어가서 remote_cli_server 모듈의 코드를 훑어보고, 박스 경로를 수정하는 것이다.
우리가 shell에 들어간 첫 번째 일은 tonic에서 request에서 포장.into_inner()을 벗기는 것이다.데이터 소유권은 추가로 commandargsVAR로 나뉘었습니다.
사용자의 프로세스를 생성하고 stdout을 포획할 수 있도록 process 구조로 구축할 것입니다.
그리고 우리는 std::process::Command가 종료되고 process를 사용하여 출력을 수집할 때까지 기다립니다.우리는 단지 .wait_with_output()만을 원하기 때문에 우리는 이 손잡이를 더욱 가지고 싶다.
마지막으로, 우리는 stdout 을 구축하여 실례화 tonic::Response 할 때 프로세스 stdout을 String 로 전환했다.마지막으로 CommandOutputResponse에 포장하여 클라이언트에게 되돌려줍니다.

서버 시작
pub async fn start_server(opts: ServerOptions) -> Result<(), Box<dyn std::error::Error>> {
   let addr = opts.server_listen_addr.parse().unwrap();
   let cli_server = Cli::default();

   println!("RemoteCliServer listening on {}", addr);

   Server::builder()
       .add_service(RemoteCliServer::new(cli_server))
       .serve(addr)
       .await?;

   Ok(())
}
프런트엔드에서 이 기능을 사용하여 서버를 시작합니다.
감청 주소가 Result를 통해 전송됩니다.이것은 opts 형식으로 전송되지만, 컴파일러는 우리가 나중에 그것을 어떻게 사용하느냐에 따라 String 호출할 때 가리키는 유형을 정합니다.
우리는 .parse() 구조 실례화cli_server를 사용하여protobuf traitCli로 실현했다.RemoteCli GRPC 서버 인스턴스를 생성합니다.tonic::Server::builder() 방법은 생성된 단점via.add_service()와trait implviaRemoteCliServer::new(cli_server)를 포함하는 gRPC 서버를 만들어야 한다.RemoteCliServer 방법은 해석된 탐지 주소를 수신하여 컴파일러가 필요한 유형을 추정하는 데 필요한 힌트를 제공하고 cli_server를 우리에게 사용하도록 되돌려준다serve().

매인.rs-지금까지
서버 모듈을 삽입하기 위해 async Result<>에 약간의 변경을 하고 있습니다.
pub mod remotecli;

use structopt::StructOpt;

// These are the options used by the `server` subcommand
#[derive(Debug, StructOpt)]
pub struct ServerOptions {
   /// The address of the server that will run commands.
   #[structopt(long, default_value = "127.0.0.1:50051")]
   pub server_listen_addr: String,
}

// These are the options used by the `run` subcommand
#[derive(Debug, StructOpt)]
pub struct RemoteCommandOptions {
   /// The address of the server that will run commands.
   #[structopt(long = "server", default_value = "http://127.0.0.1:50051")]
   pub server_addr: String,
   /// The full command and arguments for the server to execute
   pub command: Vec<String>,
}

// These are the only valid values for our subcommands
#[derive(Debug, StructOpt)]
pub enum SubCommand {
   /// Start the remote command gRPC server
   #[structopt(name = "server")]
   StartServer(ServerOptions),
   /// Send a remote command to the gRPC server
   #[structopt(setting = structopt::clap::AppSettings::TrailingVarArg)]
   Run(RemoteCommandOptions),
}

// This is the main arguments structure that we'll parse from
#[derive(StructOpt, Debug)]
#[structopt(name = "remotecli")]
struct ApplicationArguments {
   #[structopt(flatten)]
   pub subcommand: SubCommand,
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
   let args = ApplicationArguments::from_args();

   match args.subcommand {
       SubCommand::StartServer(opts) => {
           println!("Start the server on: {:?}", opts.server_listen_addr);
           remotecli::server::start_server(opts).await?;
       }
       SubCommand::Run(rc_opts) => {
           println!("Run command: '{:?}'", rc_opts.command);

       }
   }

   Ok(())
}
이제 .await 모듈을 가져옵니다.main.rs 기능도 약간 달라졌다.우선, 우리는 함수를 remotecli로 바꿀 것이다.
실행할 비동기 함수를 표시하기 위해 main() 속성을 추가합니다.
사용자가 async 하위 명령을 실행할 때, 우리는 서버를 실제적으로 시작하기 위해 새로운 #[tokio::main]를 호출합니다.

remotecli/server.다 같이 있어요.
다음은 서버 모듈의 최종 형식입니다.
use tonic::{transport::Server, Request, Response, Status};

// Import the generated rust code into module
pub mod remotecli_proto {
   tonic::include_proto!("remotecli");
}

// Proto generated server traits
use remotecli_proto::remote_cli_server::{RemoteCli, RemoteCliServer};

// Proto message structs
use remotecli_proto::{CommandInput, CommandOutput};

// For the server listening address
use crate::ServerOptions;

// For executing commands
use std::process::{Command, Stdio};

#[derive(Default)]
pub struct Cli {}

#[tonic::async_trait]
impl RemoteCli for Cli {
   async fn shell(
       &self,
       request: Request<CommandInput>,
   ) -> Result<Response<CommandOutput>, Status> {
       let req_command = request.into_inner();
       let command = req_command.command;
       let args = req_command.args;

       println!("Running command: {:?} - args: {:?}", &command, &args);

       let process = Command::new(command)
           .args(args)
           .stdout(Stdio::piped())
           .spawn()
           .expect("failed to execute child process");

       let output = process
           .wait_with_output()
           .expect("failed to wait on child process");
       let output = output.stdout;

       Ok(Response::new(CommandOutput {
           output: String::from_utf8(output).unwrap(),
       }))
   }
}

pub async fn start_server(opts: ServerOptions) -> Result<(), Box<dyn std::error::Error>> {
   let addr = opts.server_listen_addr.parse().unwrap();
   let cli_server = Cli::default();

   println!("RemoteCliServer listening on {}", addr);

   Server::builder()
       .add_service(RemoteCliServer::new(cli_server))
       .serve(addr)
       .await?;

   Ok(())
}
이것이 바로 서버가 서버를 실현하고 시작하는 전단 코드다.이것은 놀라운 작은 코드량이다.
다음 명령을 실행하여 서버 인스턴스를 시작할 수 있습니다.
$ cargo run -- server
[...]
Start the server on: "127.0.0.1:50051"
RemoteCliServer listening on 127.0.0.1:50051

We just covered using protobuf compiled Rust code and using it to implement our gRPC server module. Then we wrote the server startup code and plugged it into our now async CLI frontend.

In our final post, we'll cover writing our gRPC client and plugging it into our CLI frontend.

I hope you'll follow along!

좋은 웹페이지 즐겨찾기