Skip to content

Commit

Permalink
feat(examples): add grpc-web example (#710)
Browse files Browse the repository at this point in the history
Creates a server using tonic-web and a client that uses a regular
`hyper::Client` and issues a regular HTTP/1.1 request.
  • Loading branch information
davidpdrsn committed Jul 9, 2021
1 parent 5bb1a02 commit 5aa8ae1
Show file tree
Hide file tree
Showing 4 changed files with 176 additions and 35 deletions.
16 changes: 13 additions & 3 deletions examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand All @@ -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"
Expand All @@ -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 changes: 90 additions & 0 deletions examples/src/grpc-web/client.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 49 additions & 0 deletions examples/src/grpc-web/server.rs
Original file line number Diff line number Diff line change
@@ -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(())
}
56 changes: 24 additions & 32 deletions tonic-web/README.md
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 5aa8ae1

Please sign in to comment.