goat

package module
v0.1.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 7, 2024 License: MIT Imports: 30 Imported by: 0

README ΒΆ

🐐 GOAT 🐐

GOAT is gRPC Over Any Transport. The standard GRPC implementations are quite closely tied to HTTP/2, but there are cases where such a transport is not optimal or not possible. GOAT is an attempt at a common abstraction layer over the networking portion, allowing GRPC to work over any reliable transport: WebSockets, TCP, a pipe(2), etc.

The basic idea is to serialise a gRPC request into a wrapper protobuf that also includes any metadata needed to describe the request. The wrapper contains the same sort of information provided in HTTP/2 headers, trailers, method name, and stream ID. The libraries in this and linked repositories then use this to provide client and server interfaces.

Language implementations

Golang

Golang is our canonical language for GOAT and is implemented in this repo. See below for more information on using it.

Typescript / ECMAScript

See the goat-es repository.

Kotlin

Work in progress. Come back here later.

GOAT for Golang

GOAT works with the standard GRPC libraries for Go, implementing the ClientConnInterface and a Server (that handles *ServiceDesc) interfaces for clients and servers respectively.

The transport interface is very minimal:

type RpcReadWriter interface {
	Read(context.Context) (*goatorepo.Rpc, error)
	Write(context.Context, *goatorepo.Rpc) error
}

See Client below on how to use a GOAT client. See Server below on how to use a GOAT server.


Concepts

Transports

A transport implementation is one implementing this interface; two example implementations exist in this repo:

  • A simple websocket-based tranport in NewGoatOverWebsocket(). This allows creating a new GOAT transport given a websocket connection from the nhooyr.io/websocket Golang module.
  • The unit testing code has several transport implementations, e.g. NewGoatOverPipe() which works over any net.Conn including those returned by net.Pipe().
  • An example transport over HTTP 1/1.1 in NewGoatOverHttp(). You probably don't want to use this, but it serves as another example of implementing a transport.
Client and server names

GOAT allows clients to include an identifying name, and to specify the name of a server destination:

  • Client source names are not used for anything inside the GOAT implementation, and may be used by users of this library for cases like complex proxying.
  • Destination and server names must match, else GOAT will reject an RPC. If names are not desirable, then the client and server should specify an empty string for the destination and server names respectively.

Names are designed for improved debugging, logging, and ability to create more complex routing or proxying setups.

Client

The client side only requires a transport implementation instance, and then provides regular GRPC usage like normal.

Server

The server side allows binding GRPC services like normal. It is then invoked to serve each client individually.

Proxy

It is possible to build proxies, much like HTTP reverse proxies can be used with GRPC over HTTP.


Client

Consider the example regular Go GRPC example code as follows - taken from the GRPC docs.

var opts []grpc.DialOption
// ...
conn, err := grpc.Dial(*serverAddr, opts...)
if err != nil {
    // ...
}
defer conn.Close()

client := pb.NewRouteGuideClient(conn)

feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})
if err != nil {
    // ...
}

To change this to use a GOAT based transport, we simply change it like so:

websock, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil)
if err != nil {
	// ...
}
defer websock.CloseNow()

var opts []goat.DialOption
// ...
goatConn := goat.NewGoatOverWebsocket(websock)
conn := goat.NewClientConn(goatConn, "", "srv", opts...)

client := pb.NewRouteGuideClient(conn)

feature, err := client.GetFeature(context.Background(), &pb.Point{409146138, -746188906})
if err != nil {
    // ...
}

GOAT is used in two ways then: creating a new RpcReadWriter (in this case via goat.NewGoatOverWebsocket()) and then using goat.NewClientConn(). This returns something that implements ClientConnInterface, satisfying the needs of the generated code that creates a new GRPC service client.

Server

Consider the example regular Go GRPC example code as follows - taken from the GRPC docs.

lis, err := net.Listen("tcp", fmt.Sprintf("localhost:%d", port))
if err != nil {
    log.Fatalf("failed to listen: %v", err)
}
var opts []grpc.ServerOption
// ...
grpcServer := grpc.NewServer(opts...)
pb.RegisterRouteGuideServer(grpcServer, newServer())
grpcServer.Serve(lis)

Rather than grpc.NewServer() we use goat.NewServer().

// ...
grpcServer := goat.NewServer("srv")
pb.RegisterRouteGuideServer(grpcServer, newServer())
// ...
// For each new client connection coming in, we call grpcServer.Serve():
grpcServer.Serve(context.TODO(), clientRpcReadWriter)
Proxy

GOAT is designed to be able to build proxies in the same manner as HTTP reverse proxies. This Golang library supports such use-cases via the goat.NewProxy() function.

This is an advanced feature and not yet documented. It is recommended to read the code and tests in this area.

Protocol

GRPC is canonically transported over HTTP/2, making use of HTTP/2's ability to provide framing and stream management.

Three concepts from HTTP/2 are core to understanding GRPC's use there (we're glossing over detail like CONTINUATION frames and more; the following suffices):

  1. HEADERS frames
  2. DATA frames
  3. Stream Identifiers and END_STREAM flag

HEADERS frames are used to begin a request, to begin answering a request, and to finalise a response by including "trailers" and a status code. DATA frames contain the actual payload RPC data.

These two types of frames are logically associated by a Stream Identifier, a 31-bit integer used to demarcate different streams. And to signal the end of request or response, the END_STREAM flag is specified.

Here we provide a generic framing and stream management protocol, allowing GRPC to be serialised over any reliable transport. We take inspiration from the HTTP/2 protocol and provide minimal protocol with complementary semantics.

Data format

Data is formatted in Protobuf messages: this means RPC requests can effectively have two layers of protobuf encoding: the information used here to identify a request in the GOAT protocol, and the usual RPC data itself (that found in the DATA frame normally). The precise format is referenced in the source here, but it approximately looks like this:

message Rpc {
    // Analogous to a "Stream Identifier" in HTTP/2: allocated by the initator of an RPC, a
    // unique identifier used to group requests and responses specific to this request.
    uint64 id = 1;
    // Information identifying the request to be made; hence always set in the initial
    // request. At a minimum in such a request, the RPC method to be invoked is set. This
    // information is transferred in the URL path in HTTP/2.
    RequestHeader header = 2;
    // When a request finishes, it is explicitly marked with a status code, and optionally
    // with trailers. In HTTP/2 this is communicated with a HEADERS frame following the
    // response DATA frame, with certain canonical headers set like `grpc-status`.
    ResponseStatus status = 3;
    // The actual RPC request or response data: just some opaque bytes, this is usually
    // protobuf-serialised bytes.
    Body body = 4;
    // Like status, this is sent as part of a response, and allows arbitrary key/values
    // to be communicated. This maps onto "Trailers" in the HTTP/2 encoding, but without
    // status-code, which is encoded explicitly above.
    Trailer trailer = 5;
}

Hence a GOAT communication includes a series of Rpc messages in both directions, multiple RPCs multiplexed by the id parameter which is always set.

Unary

A unary request will contain:

  • id: always specified
  • header: itself with method always set
  • body: itself always specified, but may be empty if the request contains no data

And a response:

  • id: always specified
  • header: at a minimal echoes back the method called
  • status: usually only set on error
  • body: always specified, but may be empty, if the response contains no data
  • trailer: always specified, but may be empty, but its presence signals the request has finished

Streaming

Streaming RPCs need to additionally be able to signal streaming-start and streaming-stop concepts:

  • Starting is explicit by virtue of a new id being used for a method, and the initial request having no body and no trailers
  • Stopping is always indicated -- in both directions -- by presence of the trailer

Because streaming can be in any of three configurations (server, client, bidir), there are now cases where a request or response does not need to carry a body. Included below are examples of what requests and responses might flow in each case. Note that exact ordering of messages can differ from the examples below.

Server stream

In this case the client sends just a single message, and initiates a streaming download. Therefore the requests/responses look something like:

  1. Client ⟢ Server: id, header
  2. Client ⟢ Server: id, header, body?, trailer
  3. Client ⟡ Server: id, header, body (repeated)
  4. Client ⟡ Server: id, header, body?, status?, trailer

It is valid for a server to respond without ever specifying a body, and only ever specifying trailer (and probably status).

Client stream

In this case the client uploads data to the server, which itself does not respond with any data:

  1. Client ⟢ Server: id, header
  2. Client ⟢ Server: id, header, body (repeated)
  3. Client ⟢ Server: id, header, status?, trailer
  4. Client ⟡ Server: id, header, body, status?, trailer

It is valid for the client to specify a status to the server when it stops streaming, allowing the server to disambiguate expected and unexpected cases of the streaming stopping.

Once the client finishes streaming by specifying a trailer, the server can respond with body and trailer.

Bidirectional stream

Bidirectional streams allow data-flow in both directions between clients and servers:

  1. Client ⟢ Server: id, header
  2. Client ⟢ Server: id, header, body (repeated)
  3. Client ⟡ Server: id, header, body (repeated)
  4. Client ⟡ Server: id, header, status?, trailer
  5. Client ⟢ Server: id, header, status?, trailer

The order of closing is up to the application.

Documentation ΒΆ

Overview ΒΆ

*
* Copyright 2017 gRPC authors.
*
* 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 GOAT (gRPC Over Any Transport) is a gRPC client and server implementation which runs over any transport which implements RpcReadWriter.

The idea is that gRPC requests and responses are serialised into a wrapper protobuf (wrapped.Rpc).

Index ΒΆ

Constants ΒΆ

This section is empty.

Variables ΒΆ

This section is empty.

Functions ΒΆ

This section is empty.

Types ΒΆ

type ClientConn ΒΆ

type ClientConn struct {
	// contains filtered or unexported fields
}

func NewClientConn ΒΆ

func NewClientConn(conn RpcReadWriter, source, dest string, opts ...DialOption) *ClientConn

func (*ClientConn) Close ΒΆ added in v0.1.4

func (cc *ClientConn) Close()

func (*ClientConn) Invoke ΒΆ

func (cc *ClientConn) Invoke(
	ctx context.Context,
	method string,
	args interface{},
	reply interface{},
	opts ...grpc.CallOption,
) error

Invoke performs a unary RPC and returns after the response is received into reply.

func (*ClientConn) NewStream ΒΆ

func (cc *ClientConn) NewStream(
	ctx context.Context,
	desc *grpc.StreamDesc,
	method string,
	opts ...grpc.CallOption,
) (grpc.ClientStream, error)

NewStream begins a streaming RPC.

type ClientDisconnect ΒΆ

type ClientDisconnect func(id string, reason error)

type Demux ΒΆ

type Demux struct {
	// contains filtered or unexported fields
}

Wraps a Goat Server, demultiplexing IO.

func NewDemux ΒΆ

func NewDemux(
	ctx context.Context,
	rw RpcReadWriter,
	demuxOn func(*Rpc) string,
	onNewConnection func(RpcReadWriter),
) *Demux

NewDemux returns a new Demux which demultiplexex RPCs from the given |rw| into |onNewConnection| based the identity returned from |demuxOn|.

func (*Demux) Cancel ΒΆ

func (gsd *Demux) Cancel(id string)

func (*Demux) Run ΒΆ

func (gsd *Demux) Run()

func (*Demux) Stop ΒΆ

func (gsd *Demux) Stop()

type DialOption ΒΆ

type DialOption interface {
	// contains filtered or unexported methods
}

DialOption is an option used when constructing a NewClientConn.

func WithStatsHandler ΒΆ added in v0.1.4

func WithStatsHandler(handler stats.Handler) DialOption

func WithStreamInterceptor ΒΆ

func WithStreamInterceptor(i grpc.StreamClientInterceptor) DialOption

WithStreamInterceptor returns a DialOption that specifies the interceptor for streaming RPCs.

WARNING: the interceptor will receive a nil *grpc.ClientConn. See pkg doc.

func WithUnaryInterceptor ΒΆ

func WithUnaryInterceptor(i grpc.UnaryClientInterceptor) DialOption

WithUnaryInterceptor returns a DialOption that specifies the interceptor for unary RPCs.

WARNING: the interceptor will receive a nil *grpc.ClientConn. See pkg doc.

type GoatOverHttp ΒΆ

type GoatOverHttp struct {
	// contains filtered or unexported fields
}

GoatOverHttp manages GOAT connections over HTTP 1.0, yielding a new HTTPRpcReadWriter whenever it receives an Rpc from a new source (via ServeHTTP) or when prompted to establish a NewConnection.

func NewGoatOverHttp ΒΆ

func NewGoatOverHttp(
	onConnect OnConnect,
	sourceToAddress SourceToAddress,
	opts ...GoatOverHttpOption,
) *GoatOverHttp

func (*GoatOverHttp) Cancel ΒΆ

func (goh *GoatOverHttp) Cancel()

func (*GoatOverHttp) NewConnection ΒΆ

func (goh *GoatOverHttp) NewConnection(destination string) RpcReadWriter

func (*GoatOverHttp) ServeHTTP ΒΆ

func (goh *GoatOverHttp) ServeHTTP(w http.ResponseWriter, r *http.Request)

type GoatOverHttpOption ΒΆ

type GoatOverHttpOption interface {
	// contains filtered or unexported methods
}

func WithClock ΒΆ

func WithClock(cl clock.Clock) GoatOverHttpOption

WithClock sets the clock of the GoatOverHttp.

func WithConnectionCleanupInterval ΒΆ

func WithConnectionCleanupInterval(i time.Duration) GoatOverHttpOption

WithConnectionCleanupInterval sets the interval that the GoatOverHttp will wait between firings of the connection cleanup routine, which will close any stale connections.

func WithConnectionTimeout ΒΆ

func WithConnectionTimeout(t time.Duration) GoatOverHttpOption

WithConnectionTimeout sets the duration after which the GoatOverHttp will consider a given connection as stale and in need of being closed.

type NewConnection ΒΆ

type NewConnection func(id string) (RpcReadWriter, error)

type OnConnect ΒΆ

type OnConnect func(id string, rw RpcReadWriter)

OnConnect can be called by the GoatOverHttp to indicate that a new client has connected.

type Proxy ΒΆ

type Proxy struct {
	// contains filtered or unexported fields
}

func NewProxy ΒΆ

func NewProxy(
	ctx context.Context,
	id string,
	newConnection NewConnection,
	rpcIntercepter RpcIntercepter,
	onClientDisconnect ClientDisconnect,
) *Proxy

func (*Proxy) AddClient ΒΆ

func (p *Proxy) AddClient(id string, conn RpcReadWriter)

func (*Proxy) Serve ΒΆ

func (p *Proxy) Serve()

type Rpc ΒΆ

type Rpc = proto.Rpc

Rpc is the fundamental type in Goat: the generic protobuf structure into which all goat messages are serialised.

type RpcHeader ΒΆ

type RpcHeader = proto.RequestHeader

RpcHeader represents the header of a Goat Rpc.

type RpcIntercepter ΒΆ

type RpcIntercepter func(hdr *goatorepo.RequestHeader) error

Is free to modify the passed in header, e.g. changing the Destination. If an error is returned, the RPC is dropped.

type RpcReadWriter ΒΆ

type RpcReadWriter = types.RpcReadWriter

RpcReadWriter is the generic interface used by Goat's client and servers. It utilises the wrapped.Rpc protobuf format for generically wrapping gRPC calls and their metadata.

func NewGoatOverChannel ΒΆ

func NewGoatOverChannel(inQ chan *Rpc, outQ chan *Rpc) RpcReadWriter

NewGoatOverChannel turns an input and an output channel into an RpcReadWriter.

func NewGoatOverWebsocket ΒΆ

func NewGoatOverWebsocket(ws *websocket.Conn) RpcReadWriter

type Server ΒΆ

type Server struct {
	// contains filtered or unexported fields
}

func NewServer ΒΆ

func NewServer(id string, opts ...ServerOption) *Server

func (*Server) RegisterService ΒΆ

func (s *Server) RegisterService(sd *grpc.ServiceDesc, ss interface{})

grpc.ServiceRegistrar

func (*Server) Serve ΒΆ

func (s *Server) Serve(ctx context.Context, rw RpcReadWriter) error

func (*Server) Stop ΒΆ

func (s *Server) Stop()

type ServerOption ΒΆ

type ServerOption interface {
	// contains filtered or unexported methods
}

ServerOption is an option used when constructing a NewServer.

func ChainStreamInterceptor ΒΆ added in v0.1.2

func ChainStreamInterceptor(interceptors ...grpc.StreamServerInterceptor) ServerOption

Same as grpc.ChainStreamInterceptor()

func ChainUnaryInterceptor ΒΆ added in v0.1.2

func ChainUnaryInterceptor(interceptors ...grpc.UnaryServerInterceptor) ServerOption

Same as grpc.ChainUnaryInterceptor()

func StatsHandler ΒΆ added in v0.1.4

func StatsHandler(h stats.Handler) ServerOption

func StreamInterceptor ΒΆ

func StreamInterceptor(i grpc.StreamServerInterceptor) ServerOption

StreamInterceptor returns a ServerOption that sets the StreamServerInterceptor for the server. Only one stream interceptor can be installed.

func UnaryInterceptor ΒΆ

func UnaryInterceptor(i grpc.UnaryServerInterceptor) ServerOption

UnaryInterceptor returns a ServerOption that sets the UnaryServerInterceptor for the server. Only one unary interceptor can be installed. The construction of multiple interceptors (e.g., chaining) can be implemented at the caller.

type SourceToAddress ΒΆ

type SourceToAddress func(src string) (string, error)

SourceToAddress maps an Rpc source into an addressable entity.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL