Skip to content

Commit

Permalink
Add client/bidi streaming support with WebSockets (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
RTann authored Aug 19, 2020
1 parent 73e63e7 commit ceaf053
Show file tree
Hide file tree
Showing 19 changed files with 1,543 additions and 60 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
/.idea/
/deps
/.gobin/
31 changes: 18 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
grpc-http1: A gRPC via HTTP/1 Enabling Library for Go
====================================================

This library enables using a subset of the functionality of a gRPC server even if it is exposed behind
a reverse proxy that does not support HTTP/2, or only supports it for clients (such as Amazon's ALB).
This is accomplished via adaptive downgrading to the gRPC-web response format. This library allows
instrumenting both clients and servers to that extent.
This library enables using all the functionality of a gRPC server even if it is exposed behind
a reverse proxy which does not support HTTP/2, or only supports it for clients (such as Amazon's ALB).
This is accomplished via either adaptive downgrading to the gRPC-Web response format or utilizing WebSockets.

For a high-level overview, see [this Medium post](https://medium.com/stackrox-engineering/how-to-expose-grpc-services-behind-almost-any-load-balancer-e9ebf8e6d12a).

**Stay tuned for a high-level overview article to the WebSocket solution.**

Connection Compatibility Overview
---------------------------------

Expand All @@ -19,18 +20,18 @@ when accessing it via a reverse proxy not supporting HTTP/2.
<tr><th></th><th colspan="2">Plain Old gRPC Server</th><th colspan="2">HTTP/1 Downgrading gRPC Server</th></tr>
<tr><th></th><th>direct</th><th>behind reverse proxy</th><th>direct</th><th>behind reverse proxy</th></tr>
<tr><td>Plain Old gRPC Client</td><td>:white_check_mark:</td><td>:x:</td><td>:white_check_mark:</td><td>:x:</td></tr>
<tr><td>HTTP/1 downgrading gRPC client</td><td>:white_check_mark:</td><td>:x:</td><td>:white_check_mark:</td><td>(:white_check_mark:)</td></tr>
<tr><td>gRPC-Web downgrade client mode</td><td>:white_check_mark:</td><td>:x:</td><td>:white_check_mark:</td><td>(:white_check_mark:)</td></tr>
<tr><td>gRPC-WebSocket client mode</td><td>:x:</td><td>:x:</td><td>:white_check_mark:</td><td>:white_check_mark:</td></tr>
</table>

The (:white_check_mark:) in the bottom right cell indicates that a subset of gRPC calls will be possible, but not
The (:white_check_mark:) for the gRPC-Web downgrading client indicates a subset of gRPC calls will be possible, but not
all. These include all calls that do not rely on client-side streaming (i.e., all unary and server-streaming calls).
Support for client-side streaming calls is active work in progress.

As you can see, it is possible to instrument the client or the server only without any (functional) regressions - there
As you can see, when using the client in gRPC-Web downgrade mode, it is possible to instrument the client **or** the server without any (functional) regressions - there
may be a small but fairly negligible performance penalty. This means rolling this feature out to your clients and
servers does not need to happen in a strictly synchronous fashion. However, you will only be able to work with a server
behind an HTTP/2-incompatible reverse proxy if both the client and the server have been instrumented via
this library.
behind an HTTP/2-incompatible reverse proxy if both the client **and** the server have been instrumented via
this library. To use the client in gRPC-WebSocket mode, both the client **and** server must be instrumented via this library.


Usage
Expand All @@ -48,7 +49,7 @@ and instead use the `ServeHTTP` method of the `*grpc.Server` object -- it is exp
to be fairly stable and reliable.

The only exported function in the `golang.grpc.io/grpc-http1/server` package is `CreateDowngradingHandler`,
which returns a `http.Handler` that can be served by a Go HTTP server. It is crucial that this server is
which returns a `http.Handler` that can be served by a Go HTTP server. It is crucial this server is
configured to support HTTP/2; otherwise, your clients using the vanilla gRPC client will no longer be able
to talk to it. You can find an example of how to do so in the `_integration-tests/` directory.

Expand All @@ -66,6 +67,10 @@ connection to the server, pass a `nil` TLS config; however, this does *not* free
`grpc.WithInsecure()` gRPC dial option.

The last (variadic) parameter specifies options that modify the dialing behavior. You can pass any gRPC dial
options via `client.DialOpts(...)`. Another important option is `client.ForceHTTP2()`, which needs to be used for
a plaintext connection to a server that is *not* HTTP/1.1 capable (e.g., the vanilla gRPC server). Again, check out the
options via `client.DialOpts(...)`; however, the `grpc.WithTransportCredentials` option will not be needed.
By default, adaptive gRPC-Web downgrading is used. To use WebSockets, pass `true` to the `client.UseWebSocket` option.

Another important option is `client.ForceHTTP2()`, which needs to be used for
a plaintext connection to a server that is *not* HTTP/1.1 capable (e.g., the vanilla gRPC server).
This option is ignored when WebSockets are used. Again, check out the
code in the `_integration-tests` directory.
56 changes: 56 additions & 0 deletions _integration-tests/echo_service.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
// Copyright (c) 2020 StackRox Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License

package integrationtests

import (
Expand All @@ -6,14 +20,19 @@ import (
"strings"

"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/examples/features/proto/echo"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)

var (
_ = echo.EchoServer(echoService{})
)

// echoService implements an echo server, which also sets headers and trailers.
// Given the 'ERROR:' keyword in the message or 'error' in the header, the call will trigger an error.
// This allows for testing for errors during various stages of the response.
type echoService struct{}

func (echoService) echoHeadersAndTrailers(ctx context.Context) error {
Expand All @@ -33,6 +52,10 @@ func (echoService) echoHeadersAndTrailers(ctx context.Context) error {
}
}

if errMsg := md.Get("error"); len(errMsg) > 0 {
return status.Error(codes.InvalidArgument, errMsg[0])
}

return nil
}

Expand All @@ -41,6 +64,10 @@ func (s echoService) UnaryEcho(ctx context.Context, req *echo.EchoRequest) (*ech
return nil, err
}

if strings.HasPrefix(req.GetMessage(), "ERROR:") {
return nil, status.Error(codes.InvalidArgument, req.GetMessage()[6:])
}

return &echo.EchoResponse{
Message: req.GetMessage(),
}, nil
Expand All @@ -54,6 +81,15 @@ func (s echoService) ServerStreamingEcho(req *echo.EchoRequest, server echo.Echo
lines := strings.Split(req.GetMessage(), "\n")

for _, line := range lines {
if strings.HasPrefix(line, "ERROR:") {
return status.Error(codes.InvalidArgument, line[6:])
}
if line == "HEADERS" {
if err := server.SendHeader(metadata.MD{}); err != nil {
return err
}
continue
}
resp := &echo.EchoResponse{Message: line}
if err := server.Send(resp); err != nil {
return err
Expand All @@ -79,6 +115,16 @@ func (s echoService) ClientStreamingEcho(server echo.Echo_ClientStreamingEchoSer
return err
}

if strings.HasPrefix(msg.GetMessage(), "ERROR:") {
return status.Error(codes.InvalidArgument, msg.GetMessage()[6:])
}
if msg.GetMessage() == "HEADERS" {
if err := server.SendHeader(metadata.MD{}); err != nil {
return err
}
continue
}

msgs = append(msgs, msg.GetMessage())
}

Expand All @@ -103,6 +149,16 @@ func (s echoService) BidirectionalStreamingEcho(server echo.Echo_BidirectionalSt
return err
}

if strings.HasPrefix(msg.GetMessage(), "ERROR:") {
return status.Error(codes.InvalidArgument, msg.GetMessage()[6:])
}
if msg.GetMessage() == "HEADERS" {
if err := server.SendHeader(metadata.MD{}); err != nil {
return err
}
continue
}

resp := &echo.EchoResponse{
Message: msg.GetMessage(),
}
Expand Down
Loading

0 comments on commit ceaf053

Please sign in to comment.