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.
This commit is contained in:
David Pedersen
2021-07-09 18:02:43 +02:00
committed by GitHub
parent 5bb1a029a1
commit 5aa8ae1fec
4 changed files with 176 additions and 35 deletions
+13 -3
View File
@@ -162,9 +162,16 @@ path = "src/compression/client.rs"
name = "mock" name = "mock"
path = "src/mock/mock.rs" 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] [dependencies]
tonic = { path = "../tonic", features = ["tls"] } tonic = { path = "../tonic", features = ["tls", "compression"] }
prost = "0.8" prost = "0.8"
tokio = { version = "1.0", features = ["rt-multi-thread", "time", "fs", "macros", "net"] } tokio = { version = "1.0", features = ["rt-multi-thread", "time", "fs", "macros", "net"] }
tokio-stream = { version = "0.1", features = ["net"] } tokio-stream = { version = "0.1", features = ["net"] }
@@ -183,7 +190,7 @@ tracing-futures = "0.2"
# Required for wellknown types # Required for wellknown types
prost-types = "0.8" prost-types = "0.8"
# Hyper example # Hyper example
hyper = "0.14" hyper = { version = "0.14", features = ["full"] }
warp = "0.3" warp = "0.3"
http = "0.2" http = "0.2"
http-body = "0.4.2" http-body = "0.4.2"
@@ -193,6 +200,9 @@ tonic-health = { path = "../tonic-health" }
# Reflection example # Reflection example
tonic-reflection = { path = "../tonic-reflection" } tonic-reflection = { path = "../tonic-reflection" }
listenfd = "0.3" listenfd = "0.3"
# grpc-web example
tonic-web = { path = "../tonic-web" }
bytes = "1"
[build-dependencies] [build-dependencies]
tonic-build = { path = "../tonic-build", features = ["prost"] } tonic-build = { path = "../tonic-build", features = ["prost", "compression"] }
+90
View File
@@ -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<dyn std::error::Error>> {
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::<HelloReply>(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<T>(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<T>(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
}
+49
View File
@@ -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<HelloRequest>,
) -> Result<Response<HelloReply>, 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<dyn std::error::Error>> {
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(())
}
+24 -32
View File
@@ -1,46 +1,38 @@
# tonic-web # tonic-web
Enables tonic servers to handle requests from `grpc-web` clients directly, without the need of an Enables tonic servers to handle requests from `grpc-web` clients directly,
external proxy. without the need of an external proxy.
## Getting Started ## Getting Started
```toml ```toml
[dependencies] [dependencies]
tonic_web = "0.1" 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 The easiest way to get started, is to call the function with your tonic service
server to accept HTTP/1.1 requests: and allow the tonic server to accept HTTP/1.1 requests:
```rust ```rust
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> { async fn main() -> Result<(), Box<dyn std::error::Error>> {
let addr = "[::1]:50051".parse().unwrap(); let addr = "[::1]:50051".parse().unwrap();
let greeter = GreeterServer::new(MyGreeter::default()); let greeter = GreeterServer::new(MyGreeter::default());
Server::builder() Server::builder()
.accept_http1(true) .accept_http1(true)
.add_service(tonic_web::enable(greeter)) .add_service(tonic_web::enable(greeter))
.serve(addr) .serve(addr)
.await?; .await?;
Ok(()) Ok(())
} }
``` ```
## Examples ## 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 [example]: https://github.com/hyperium/tonic/tree/master/examples/src/tower
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