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:
+13
-3
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user