2020년에 Rust로 단일 바이너리 gRPC 서버-클라이언트를 구축해 봅시다 - 4부

In the previous post, we covered using our protobuf compiled Rust code to implement our gRPC server and include it in our CLI frontend.

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

This is the last 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.




고객



우리는 홈 스트레치에 있습니다. 클라이언트를 구현합니다. remotecli라는 새 모듈을 client.rs 안에 만들 것입니다. 이 모듈은 서버에 대해 설정한 것과 동일한 패턴을 따릅니다.

$ tree
.
├── build.rs
├── Cargo.toml
├── proto
│ └── cli.proto
└── src
    ├── main.rs
    └── remotecli
            ├── client.rs
            ├── mod.rs
            └── server.rs


remotecli/mod.rs




pub mod client;
pub mod server;

mod.rs 내에서 클라이언트 모듈을 선언하고 있습니다.

remotecli/client.rs



우리 고객은 훨씬 더 간단합니다. 그러나 설명을 위해 모듈을 조각으로 나눕니다. 다시 말하지만 전체 파일은 섹션 끝에 있습니다.

수입품




pub mod remotecli_proto {
   tonic::include_proto!("remotecli");
}

// Proto generated client
use remotecli_proto::remote_cli_client::RemoteCliClient;

// Proto message structs
use remotecli_proto::CommandInput;

use crate::RemoteCommandOptions;


서버에서와 마찬가지로 모듈remotecli_proto을 만들고 tonic::include_proto!() 매크로를 사용하여 생성된 코드를 이 모듈에 복사/붙여넣기합니다.

그런 다음 연결하기 위해 생성된 RemoteCliClientCommandInput 구조체를 포함합니다. 이것이 우리가 서버로 보내는 것이기 때문입니다.

마지막 포함은 프론트엔드의 RemoteCommandOptions 구조체이므로 연결하려는 서버 주소를 전달할 수 있습니다.

client_run




pub async fn client_run(rc_opts: RemoteCommandOptions) -> Result<(), Box<dyn std::error::Error>> {
   // Connect to server
   // Use server addr if given, otherwise use default
   let mut client = RemoteCliClient::connect(rc_opts.server_addr).await?;

   let request = tonic::Request::new(CommandInput {
       command: rc_opts.command[0].clone().into(),
       args: rc_opts.command[1..].to_vec(),
   });

   let response = client.shell(request).await?;

   println!("RESPONSE={:?}", response);

   Ok(())
}


도우미 함수client_run()는 우리 서버와 같은 async 함수입니다. 프론트엔드는 서버 주소 정보와 원시 사용자 명령에 대한 RemoteCommandOptions 구조체를 전달합니다.


먼저 client를 생성하고 RemoteCliClient::connect로 서버에 연결하고 .await를 수행합니다.


그런 다음 tonic::RequestCommandInput 구조체를 생성하여 요청을 작성합니다.

사용자 명령은 원시이며 서버가 기대하는 모양에 맞게 슬라이스해야 합니다. 사용자 명령의 첫 번째 단어는 쉘 명령이고 나머지는 인수입니다.


마지막으로 client를 사용하고 요청과 함께 엔드포인트를 호출하고 .await 실행을 완료합니다.

메인.rs


main.rs 의 최종 형태입니다. main.rs에 마지막으로 하는 일은 client_run() 함수를 연결하는 것입니다.

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);
           remotecli::client::client_run(rc_opts).await?;
       }
   }

   Ok(())
}


remotecli/client.rs 모두 함께




pub mod remotecli_proto {
   tonic::include_proto!("remotecli");
}

// Proto generated client
use remotecli_proto::remote_cli_client::RemoteCliClient;

// Proto message structs
use remotecli_proto::CommandInput;

use crate::RemoteCommandOptions;

pub async fn client_run(rc_opts: RemoteCommandOptions) -> Result<(), Box<dyn std::error::Error>> {
   // Connect to server
   // Use server addr if given, otherwise use default
   let mut client = RemoteCliClient::connect(rc_opts.server_addr).await?;

   let request = tonic::Request::new(CommandInput {
       command: rc_opts.command[0].clone().into(),
       args: rc_opts.command[1..].to_vec(),
   });

   let response = client.shell(request).await?;

   println!("RESPONSE={:?}", response);

   Ok(())
}


결론



우리는 사용자 입력을 구문 분석하고 gRPC를 사용하여 gRPC 클라이언트에서 서버로 명령을 전송하여 명령 출력을 실행하고 반환하는 CLI 애플리케이션을 구축하는 과정을 살펴보았습니다.
StructOpt 를 사용하여 프론트엔드 CLI를 구성한 방법에 따라 클라이언트와 서버 모두 단일 바이너리로 컴파일할 수 있습니다.

프로토콜 버퍼(또는 protobuf)는 서버의 인터페이스와 사용된 데이터 구조를 정의하는 데 사용되었습니다. TonicProst 크레이트와 Cargo 빌드 스크립트는 protobuf를 네이티브 비동기 Rust 코드로 컴파일하는 데 사용되었습니다.
Tokio는 비동기 런타임이었습니다. async/await 패턴을 지원하는 데 필요한 코드가 얼마나 적은지 경험했습니다.

이 연습이 백엔드 코드에 gRPC를 사용하는 것에 대한 약간의 궁금증을 해소하기를 바랍니다. 뿐만 아니라 Rust 코드를 작성하는 데 관심이 생겼습니다.

좋은 웹페이지 즐겨찾기