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"
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"] }
+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
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<dyn std::error::Error>> {
let addr = "[::1]:50051".parse().unwrap();
let greeter = GreeterServer::new(MyGreeter::default());
```rust
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
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