Skip to content

Commit ceaf053

Browse files
authored
Add client/bidi streaming support with WebSockets (#1)
1 parent 73e63e7 commit ceaf053

File tree

19 files changed

+1543
-60
lines changed

19 files changed

+1543
-60
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
/.idea/
22
/deps
3+
/.gobin/

README.md

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
grpc-http1: A gRPC via HTTP/1 Enabling Library for Go
22
====================================================
33

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

98
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).
109

10+
**Stay tuned for a high-level overview article to the WebSocket solution.**
11+
1112
Connection Compatibility Overview
1213
---------------------------------
1314

@@ -19,18 +20,18 @@ when accessing it via a reverse proxy not supporting HTTP/2.
1920
<tr><th></th><th colspan="2">Plain Old gRPC Server</th><th colspan="2">HTTP/1 Downgrading gRPC Server</th></tr>
2021
<tr><th></th><th>direct</th><th>behind reverse proxy</th><th>direct</th><th>behind reverse proxy</th></tr>
2122
<tr><td>Plain Old gRPC Client</td><td>:white_check_mark:</td><td>:x:</td><td>:white_check_mark:</td><td>:x:</td></tr>
22-
<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>
23+
<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>
24+
<tr><td>gRPC-WebSocket client mode</td><td>:x:</td><td>:x:</td><td>:white_check_mark:</td><td>:white_check_mark:</td></tr>
2325
</table>
2426

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

29-
As you can see, it is possible to instrument the client or the server only without any (functional) regressions - there
30+
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
3031
may be a small but fairly negligible performance penalty. This means rolling this feature out to your clients and
3132
servers does not need to happen in a strictly synchronous fashion. However, you will only be able to work with a server
32-
behind an HTTP/2-incompatible reverse proxy if both the client and the server have been instrumented via
33-
this library.
33+
behind an HTTP/2-incompatible reverse proxy if both the client **and** the server have been instrumented via
34+
this library. To use the client in gRPC-WebSocket mode, both the client **and** server must be instrumented via this library.
3435

3536

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

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

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

6869
The last (variadic) parameter specifies options that modify the dialing behavior. You can pass any gRPC dial
69-
options via `client.DialOpts(...)`. Another important option is `client.ForceHTTP2()`, which needs to be used for
70-
a plaintext connection to a server that is *not* HTTP/1.1 capable (e.g., the vanilla gRPC server). Again, check out the
70+
options via `client.DialOpts(...)`; however, the `grpc.WithTransportCredentials` option will not be needed.
71+
By default, adaptive gRPC-Web downgrading is used. To use WebSockets, pass `true` to the `client.UseWebSocket` option.
72+
73+
Another important option is `client.ForceHTTP2()`, which needs to be used for
74+
a plaintext connection to a server that is *not* HTTP/1.1 capable (e.g., the vanilla gRPC server).
75+
This option is ignored when WebSockets are used. Again, check out the
7176
code in the `_integration-tests` directory.

_integration-tests/echo_service.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
// Copyright (c) 2020 StackRox Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License
14+
115
package integrationtests
216

317
import (
@@ -6,14 +20,19 @@ import (
620
"strings"
721

822
"google.golang.org/grpc"
23+
"google.golang.org/grpc/codes"
924
"google.golang.org/grpc/examples/features/proto/echo"
1025
"google.golang.org/grpc/metadata"
26+
"google.golang.org/grpc/status"
1127
)
1228

1329
var (
1430
_ = echo.EchoServer(echoService{})
1531
)
1632

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

1938
func (echoService) echoHeadersAndTrailers(ctx context.Context) error {
@@ -33,6 +52,10 @@ func (echoService) echoHeadersAndTrailers(ctx context.Context) error {
3352
}
3453
}
3554

55+
if errMsg := md.Get("error"); len(errMsg) > 0 {
56+
return status.Error(codes.InvalidArgument, errMsg[0])
57+
}
58+
3659
return nil
3760
}
3861

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

67+
if strings.HasPrefix(req.GetMessage(), "ERROR:") {
68+
return nil, status.Error(codes.InvalidArgument, req.GetMessage()[6:])
69+
}
70+
4471
return &echo.EchoResponse{
4572
Message: req.GetMessage(),
4673
}, nil
@@ -54,6 +81,15 @@ func (s echoService) ServerStreamingEcho(req *echo.EchoRequest, server echo.Echo
5481
lines := strings.Split(req.GetMessage(), "\n")
5582

5683
for _, line := range lines {
84+
if strings.HasPrefix(line, "ERROR:") {
85+
return status.Error(codes.InvalidArgument, line[6:])
86+
}
87+
if line == "HEADERS" {
88+
if err := server.SendHeader(metadata.MD{}); err != nil {
89+
return err
90+
}
91+
continue
92+
}
5793
resp := &echo.EchoResponse{Message: line}
5894
if err := server.Send(resp); err != nil {
5995
return err
@@ -79,6 +115,16 @@ func (s echoService) ClientStreamingEcho(server echo.Echo_ClientStreamingEchoSer
79115
return err
80116
}
81117

118+
if strings.HasPrefix(msg.GetMessage(), "ERROR:") {
119+
return status.Error(codes.InvalidArgument, msg.GetMessage()[6:])
120+
}
121+
if msg.GetMessage() == "HEADERS" {
122+
if err := server.SendHeader(metadata.MD{}); err != nil {
123+
return err
124+
}
125+
continue
126+
}
127+
82128
msgs = append(msgs, msg.GetMessage())
83129
}
84130

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

152+
if strings.HasPrefix(msg.GetMessage(), "ERROR:") {
153+
return status.Error(codes.InvalidArgument, msg.GetMessage()[6:])
154+
}
155+
if msg.GetMessage() == "HEADERS" {
156+
if err := server.SendHeader(metadata.MD{}); err != nil {
157+
return err
158+
}
159+
continue
160+
}
161+
106162
resp := &echo.EchoResponse{
107163
Message: msg.GetMessage(),
108164
}

0 commit comments

Comments
 (0)