-
Notifications
You must be signed in to change notification settings - Fork 1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
5bb1a02
commit 5aa8ae1
Showing
4 changed files
with
176 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |