From 5aa8ae1fec27377cd4c2a41d309945d7e38087d0 Mon Sep 17 00:00:00 2001 From: David Pedersen Date: Fri, 9 Jul 2021 18:02:43 +0200 Subject: [PATCH] feat(examples): add grpc-web example (#710) Creates a server using tonic-web and a client that uses a regular `hyper::Client` and issues a regular HTTP/1.1 request. --- examples/Cargo.toml | 16 ++++-- examples/src/grpc-web/client.rs | 90 +++++++++++++++++++++++++++++++++ examples/src/grpc-web/server.rs | 49 ++++++++++++++++++ tonic-web/README.md | 56 +++++++++----------- 4 files changed, 176 insertions(+), 35 deletions(-) create mode 100644 examples/src/grpc-web/client.rs create mode 100644 examples/src/grpc-web/server.rs diff --git a/examples/Cargo.toml b/examples/Cargo.toml index c68e58ed8..3e7f0ea00 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -162,9 +162,16 @@ path = "src/compression/client.rs" name = "mock" path = "src/mock/mock.rs" +[[bin]] +name = "grpc-web-server" +path = "src/grpc-web/server.rs" + +[[bin]] +name = "grpc-web-client" +path = "src/grpc-web/client.rs" [dependencies] -tonic = { path = "../tonic", features = ["tls"] } +tonic = { path = "../tonic", features = ["tls", "compression"] } prost = "0.8" tokio = { version = "1.0", features = ["rt-multi-thread", "time", "fs", "macros", "net"] } tokio-stream = { version = "0.1", features = ["net"] } @@ -183,7 +190,7 @@ tracing-futures = "0.2" # Required for wellknown types prost-types = "0.8" # Hyper example -hyper = "0.14" +hyper = { version = "0.14", features = ["full"] } warp = "0.3" http = "0.2" http-body = "0.4.2" @@ -193,6 +200,9 @@ tonic-health = { path = "../tonic-health" } # Reflection example tonic-reflection = { path = "../tonic-reflection" } listenfd = "0.3" +# grpc-web example +tonic-web = { path = "../tonic-web" } +bytes = "1" [build-dependencies] -tonic-build = { path = "../tonic-build", features = ["prost"] } +tonic-build = { path = "../tonic-build", features = ["prost", "compression"] } diff --git a/examples/src/grpc-web/client.rs b/examples/src/grpc-web/client.rs new file mode 100644 index 000000000..f4b9728ab --- /dev/null +++ b/examples/src/grpc-web/client.rs @@ -0,0 +1,90 @@ +use bytes::{Buf, BufMut, Bytes, BytesMut}; +use hello_world::{HelloReply, HelloRequest}; +use http::header::{ACCEPT, CONTENT_TYPE}; + +pub mod hello_world { + tonic::include_proto!("helloworld"); +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let msg = HelloRequest { + name: "Bob".to_string(), + }; + + // a good old http/1.1 request + let request = http::Request::builder() + .version(http::Version::HTTP_11) + .method(http::Method::POST) + .uri("http://127.0.0.1:3000/helloworld.Greeter/SayHello") + .header(CONTENT_TYPE, "application/grpc-web") + .header(ACCEPT, "application/grpc-web") + .body(hyper::Body::from(encode_body(msg))) + .unwrap(); + + let client = hyper::Client::new(); + + let response = client.request(request).await.unwrap(); + + assert_eq!( + response.headers().get(CONTENT_TYPE).unwrap(), + "application/grpc-web+proto" + ); + + let body = response.into_body(); + let reply = decode_body::(body).await; + + println!("REPLY={:?}", reply); + + Ok(()) +} + +// one byte for the compression flag plus four bytes for the length +const GRPC_HEADER_SIZE: usize = 5; + +fn encode_body(msg: T) -> Bytes +where + T: prost::Message, +{ + let mut buf = BytesMut::with_capacity(1024); + + // first skip past the header + // cannot write it yet since we don't know the size of the + // encoded message + buf.reserve(GRPC_HEADER_SIZE); + unsafe { + buf.advance_mut(GRPC_HEADER_SIZE); + } + + // write the message + msg.encode(&mut buf).unwrap(); + + // now we know the size of encoded message and can write the + // header + let len = buf.len() - GRPC_HEADER_SIZE; + { + let mut buf = &mut buf[..GRPC_HEADER_SIZE]; + + // compression flag, 0 means "no compression" + buf.put_u8(0); + + buf.put_u32(len as u32); + } + + buf.split_to(len + GRPC_HEADER_SIZE).freeze() +} + +async fn decode_body(body: hyper::Body) -> T +where + T: Default + prost::Message, +{ + let mut body = hyper::body::to_bytes(body).await.unwrap(); + + // ignore the compression flag + body.advance(1); + + let len = body.get_u32(); + let msg = T::decode(&mut body.split_to(len as usize)).unwrap(); + + msg +} diff --git a/examples/src/grpc-web/server.rs b/examples/src/grpc-web/server.rs new file mode 100644 index 000000000..08883fa79 --- /dev/null +++ b/examples/src/grpc-web/server.rs @@ -0,0 +1,49 @@ +use tonic::{transport::Server, Request, Response, Status}; + +use hello_world::greeter_server::{Greeter, GreeterServer}; +use hello_world::{HelloReply, HelloRequest}; + +pub mod hello_world { + tonic::include_proto!("helloworld"); +} + +#[derive(Default)] +pub struct MyGreeter {} + +#[tonic::async_trait] +impl Greeter for MyGreeter { + async fn say_hello( + &self, + request: Request, + ) -> Result, Status> { + println!("Got a request from {:?}", request.remote_addr()); + + let reply = hello_world::HelloReply { + message: format!("Hello {}!", request.into_inner().name), + }; + Ok(Response::new(reply)) + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + + let addr = "127.0.0.1:3000".parse().unwrap(); + + let greeter = MyGreeter::default(); + let greeter = GreeterServer::new(greeter); + let greeter = tonic_web::config() + .allow_origins(vec!["127.0.0.1"]) + .enable(greeter); + + println!("GreeterServer listening on {}", addr); + + Server::builder() + .accept_http1(true) + .add_service(greeter) + .serve(addr) + .await?; + + Ok(()) +} diff --git a/tonic-web/README.md b/tonic-web/README.md index 71e459c5d..318061374 100644 --- a/tonic-web/README.md +++ b/tonic-web/README.md @@ -1,46 +1,38 @@ # tonic-web -Enables tonic servers to handle requests from `grpc-web` clients directly, without the need of an -external proxy. +Enables tonic servers to handle requests from `grpc-web` clients directly, +without the need of an external proxy. ## Getting Started - ```toml - [dependencies] - tonic_web = "0.1" - ``` +```toml +[dependencies] +tonic_web = "0.1" +``` - ## Enabling tonic services +## Enabling tonic services - The easiest way to get started, is to call the function with your tonic service and allow the tonic - server to accept HTTP/1.1 requests: +The easiest way to get started, is to call the function with your tonic service +and allow the tonic server to accept HTTP/1.1 requests: - ```rust - #[tokio::main] - async fn main() -> Result<(), Box> { - let addr = "[::1]:50051".parse().unwrap(); - let greeter = GreeterServer::new(MyGreeter::default()); +```rust +#[tokio::main] +async fn main() -> Result<(), Box> { + let addr = "[::1]:50051".parse().unwrap(); + let greeter = GreeterServer::new(MyGreeter::default()); - Server::builder() - .accept_http1(true) - .add_service(tonic_web::enable(greeter)) - .serve(addr) - .await?; + Server::builder() + .accept_http1(true) + .add_service(tonic_web::enable(greeter)) + .serve(addr) + .await?; - Ok(()) - } - ``` + Ok(()) +} +``` ## Examples -[tonic-web-demo][1]: React+Typescript app that talking to a tonic-web enabled service using HTTP/1 or TLS. +See [the examples folder][example] for a server and client example. -[conduit][2]: An (in progress) implementation of the [realworld][3] demo in Tonic+Dart+Flutter. This app shows how -the same client implementation can talk to the same tonic-web enabled server using both `grpc` and `grpc-web` protocols -just by swapping the channel implementation. - -When the client is compiled for desktop, ios or android, a grpc `ClientChannel` implementation is used. -When compiled for the web, a `GrpcWebClientChannel.xhr` implementation is used instead.`` -[1]: https://github.com/alce/tonic-web-demo -[2]: https://github.com/alce/conduit -[3]: https://github.com/gothinkster/realworld +[example]: https://github.com/hyperium/tonic/tree/master/examples/src/tower