├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── README.md ├── api ├── test.proto └── testpb │ ├── test.pb.go │ └── test_grpc.pb.go ├── benchmarks ├── README.md ├── api │ ├── library.proto │ ├── librarypb │ │ ├── library.pb.go │ │ ├── library.pb.gw.go │ │ ├── library.twirp.go │ │ ├── library_grpc.pb.go │ │ └── librarypbconnect │ │ │ └── library.connect.go │ └── proto.pb ├── bench.txt ├── benchmain │ └── main.go ├── connect_svc.go ├── gen.sh ├── go.mod ├── go.sum ├── grpc-bench.txt ├── main.go ├── main_test.go ├── server │ └── server.go ├── svc.go ├── testdata │ └── envoy.yaml └── twirp_test.go ├── docs ├── .gitignore ├── LARK_PROTOCOL.md ├── intro.md ├── larking.svg └── main.go ├── examples ├── BUILD.bazel ├── WORKSPACE ├── api │ ├── BUILD.bazel │ ├── hellostream.proto │ ├── helloworld.proto │ └── helloworld │ │ ├── helloworld.pb.go │ │ └── helloworld_grpc.pb.go ├── cpp │ └── helloworld │ │ ├── BUILD.bazel │ │ └── hello_world.cc ├── deps.bzl ├── gen.sh ├── go.mod ├── go │ └── helloworld │ │ ├── BUILD.bazel │ │ └── main.go ├── java │ └── com │ │ └── example │ │ └── helloworld │ │ ├── BUILD.bazel │ │ └── HelloWorldServer.java └── proto │ └── hellostream │ ├── hellostream.pb.go │ └── hellostream_grpc.pb.go ├── gen.go ├── gen.sh ├── go.mod ├── go.sum ├── health └── health.go └── larking ├── bench.txt ├── code.go ├── codec.go ├── codec_test.go ├── compress.go ├── grpc.go ├── grpc_test.go ├── handler.go ├── handler_test.go ├── http.go ├── http_test.go ├── lexer.go ├── lexer_test.go ├── log.go ├── mux.go ├── mux_test.go ├── negotiate.go ├── negotiate_test.go ├── proxy.go ├── proxy_test.go ├── rules.go ├── rules_test.go ├── server.go ├── server_test.go ├── stats.go ├── web.go ├── web_test.go ├── websocket.go └── websocket_test.go /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | - main 9 | pull_request: 10 | jobs: 11 | test: 12 | strategy: 13 | matrix: 14 | go-version: [1.23.x] 15 | os: [ubuntu-latest] #, macos-latest, windows-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - name: Install Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ matrix.go-version }} 22 | - name: Checkout code 23 | uses: actions/checkout@v4 24 | - name: Test 25 | run: go test -race ./larking 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bazel-* 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Edward McFarlane. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the 13 | distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived 17 | from this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | _, 3 | ( '> Welcome to larking.io 4 | / ) ) 5 | /|^^ 6 | ``` 7 | [![Go Reference](https://pkg.go.dev/badge/larking.io.svg)](https://pkg.go.dev/larking.io/larking) 8 | 9 | Larking is a [protoreflect](https://pkg.go.dev/google.golang.org/protobuf/reflect/protoreflect) gRPC-transcoding implementation with support for gRPC, gRPC-web and twirp protocols. 10 | Bind [`google.api.http`](https://github.com/googleapis/googleapis/blob/master/google/api/http.proto) annotations to gRPC services without code generation. 11 | Works with existing go-protobuf generators 12 | [`protoc-gen-go`](https://pkg.go.dev/google.golang.org/protobuf@v1.30.0/cmd/protoc-gen-go) and 13 | [`protoc-gen-go-grpc`](https://pkg.go.dev/google.golang.org/grpc/cmd/protoc-gen-go-grpc) 14 | and Go's std library net/http stack. 15 | Bind to local services or proxy to other gRPC servers using gRPC server reflection. 16 | Use Google's [API design guide](https://cloud.google.com/apis/design) to design beautiful RESTful APIs for your gRPC services. 17 | 18 | - Supports [gRPC](https://grpc.io) clients 19 | - Supports [gRPC-transcoding](https://cloud.google.com/endpoints/docs/grpc/transcoding) clients 20 | - Supports [gRPC-web](https://github.com/grpc/grpc-web) clients 21 | - Supports [twirp](https://github.com/twitchtv/twirp) clients 22 | - Proxy gRPC servers with gRPC [server reflection](https://github.com/grpc/grpc/blob/master/doc/server-reflection.md) 23 | - Implicit `/GRPC_SERVICE_FULL_NAME/METHOD_NAME` for all methods 24 | - Google API service configuration [syntax](https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#using-grpc-api-service-configuration) 25 | - Websocket streaming with `websocket` kind annotations 26 | - Content streaming with `google.api.HttpBody` 27 | - Streaming support with [StreamCodec](https://github.com/emcfarlane/larking#streaming-codecs) 28 | - Fast with low allocations: see [benchmarks](https://github.com/emcfarlane/larking/tree/main/benchmarks) 29 | 30 |
31 | 32 |
33 | 34 | ## Install 35 | 36 | ``` 37 | go get larking.io@latest 38 | ``` 39 | 40 | ## Quickstart 41 | 42 | Compile protobuffers with go and go-grpc libraries. Follow the guide [here](https://grpc.io/docs/languages/go/quickstart/#prerequisites). No other pre-compiled libraries are required. We need to create a `larking.Mux` and optionally `larking.Server` to serve both gRPC and REST. 43 | 44 | This example builds a server with the health service: 45 | 46 | ```go 47 | package main 48 | 49 | import ( 50 | "log" 51 | "net" 52 | 53 | "google.golang.org/genproto/googleapis/api/serviceconfig" 54 | healthpb "google.golang.org/grpc/health/grpc_health_v1" 55 | "larking.io/health" 56 | "larking.io/larking" 57 | ) 58 | 59 | func main() { 60 | // Create a health service. The health service is used to check the status 61 | // of services running within the server. 62 | healthSvc := health.NewServer() 63 | healthSvc.SetServingStatus("example.up.Service", healthpb.HealthCheckResponse_SERVING) 64 | healthSvc.SetServingStatus("example.down.Service", healthpb.HealthCheckResponse_NOT_SERVING) 65 | 66 | serviceConfig := &serviceconfig.Service{} 67 | // AddHealthz adds a /v1/healthz endpoint to the service binding to the 68 | // grpc.health.v1.Health service: 69 | // - get /v1/healthz -> grpc.health.v1.Health.Check 70 | // - websocket /v1/healthz -> grpc.health.v1.Health.Watch 71 | health.AddHealthz(serviceConfig) 72 | 73 | // Mux impements http.Handler and serves both gRPC and HTTP connections. 74 | mux, err := larking.NewMux( 75 | larking.ServiceConfigOption(serviceConfig), 76 | ) 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | // RegisterHealthServer registers a HealthServer to the mux. 81 | healthpb.RegisterHealthServer(mux, healthSvc) 82 | 83 | // Server creates a *http.Server. 84 | svr, err := larking.NewServer(mux) 85 | if err != nil { 86 | log.Fatal(err) 87 | } 88 | 89 | // Listen on TCP port 8080 on all interfaces. 90 | lis, err := net.Listen("tcp", "localhost:8080") 91 | if err != nil { 92 | log.Fatalf("failed to listen: %v", err) 93 | } 94 | defer lis.Close() 95 | 96 | // Serve starts the server and blocks until the server stops. 97 | // http://localhost:8080/v1/healthz 98 | log.Println("gRPC & HTTP server listening on", lis.Addr()) 99 | if err := svr.Serve(lis); err != nil { 100 | log.Fatalf("failed to serve: %v", err) 101 | } 102 | ``` 103 | 104 | Running the service we can check the health endpoints with curl: 105 | ```sh 106 | > curl localhost:8080/grpc.health.v1.Health/Check 107 | {"status":"SERVING"} 108 | ``` 109 | 110 | To filter by service you can post the body `{"service": "example.down.Service"}` or use query params: 111 | ```sh 112 | > curl 'localhost:8080/grpc.health.v1.Health/Check?service=example.down.Service' 113 | {"status":"NOT_SERVING"} 114 | ``` 115 | 116 | We can also use the `/v1/healthz` endpoint added by `health.AddHealthz`: 117 | ```sh 118 | > curl 'localhost:8080/v1/healthz?service=example.up.Service' 119 | {"status":"SERVING"} 120 | ``` 121 | 122 | ## Features 123 | 124 | Transcoding provides methods to bind gRPC endpoints to HTTP methods. 125 | An example service `Library`: 126 | ```protobuf 127 | package larking.example 128 | 129 | service Library { 130 | // GetBook returns a book from a shelf. 131 | rpc GetBook(GetBookRequest) returns (Book) {}; 132 | } 133 | ``` 134 | 135 | Implicit bindings are provided on all methods bound to the URL 136 | `/GRPC_SERVICE_FULL_NAME/METHOD_NAME`. 137 | 138 | For example to get the book with curl we can send a `POST` request: 139 | ``` 140 | curl -XPOST http://domain/larking.example.Library/GetBook -d '{"name":"shelves/1/books/2"} 141 | ``` 142 | 143 | We can also use any method with query parameters. The equivalent `GET` request: 144 | ``` 145 | curl http://domain/larking.example.Library/GetBook?name=shelves/1/books/2 146 | ``` 147 | 148 | As an annotation this syntax would be written as a custom http option: 149 | ```protobuf 150 | rpc GetBook(GetBookRequest) returns (Book) { 151 | option (google.api.http) = { 152 | custom : {kind : "*" path : "/larking.example.Library/GetBook"} 153 | body : "*" 154 | }; 155 | }; 156 | ``` 157 | 158 | To get better URL semantics lets define a custom annotation: 159 | ```protobuf 160 | rpc GetBook(GetBookRequest) returns (Book) { 161 | option (google.api.http) = { 162 | get : "/v1/{name=shelves/*/books/*}" 163 | }; 164 | }; 165 | ``` 166 | 167 | Now the equivalent `GET` request would be: 168 | ``` 169 | curl http://domain/v1/shelves/1/books/2 170 | ``` 171 | 172 | See the reference docs for [google.api.HttpRule](https://cloud.google.com/endpoints/docs/grpc-service-config/reference/rpc/google.api#google.api.HttpRule) type for all features. 173 | 174 | ### Extensions 175 | It aims to be a superset of the gRPC transcoding spec with better support for streaming. The implementation also aims to be simple and fast. 176 | API's should be easy to use and have low overhead. 177 | See the `benchmarks/` for details and comparisons. 178 | 179 | #### Arbitrary Content 180 | 181 | Send any content type with the protobuf type `google.api.HttpBody`. 182 | Request bodies are unmarshalled from the body with the `ContentType` header. 183 | Response bodies marshal to the body and set the `ContentType` header. 184 | For large requests streaming RPCs support chunking the file into multiple messages. 185 | 186 | ```protobuf 187 | import "google/api/httpbody.proto"; 188 | 189 | service Files { 190 | // HTTP | gRPC 191 | // -----|----- 192 | // `POST /files/cat.jpg ` | `UploadDownload(filename: "cat.jpg", file: 193 | // { content_type: "image/jpeg", data: })"` 194 | rpc UploadDownload(UploadFileRequest) returns (google.api.HttpBody) { 195 | option (google.api.http) = { 196 | post : "/files/{filename}" 197 | body : "file" 198 | }; 199 | } 200 | rpc LargeUploadDownload(stream UploadFileRequest) 201 | returns (stream google.api.HttpBody) { 202 | option (google.api.http) = { 203 | post : "/files/large/{filename}" 204 | body : "file" 205 | }; 206 | } 207 | } 208 | message UploadFileRequest { 209 | string filename = 1; 210 | google.api.HttpBody file = 2; 211 | } 212 | ``` 213 | 214 | To better support stream uploads use the `AsHTTPBodyReader` and `AsHTTPBodyWriter` methods. 215 | The returned `io.Reader` and `io.Writer` efficiently stream bytes for large messages. 216 | This methods only works on streaming requests or streaming responses for gRPC-transcoding streams. 217 | ```go 218 | // LargeUploadDownload echoes the request body as the response body with contentType. 219 | func (s *asHTTPBodyServer) LargeUploadDownload(stream testpb.Files_LargeUploadDownloadServer) error { 220 | var req testpb.UploadFileRequest 221 | r, _ := larking.AsHTTPBodyReader(stream, &req) 222 | log.Printf("got %s!", req.Filename) 223 | 224 | rsp := httpbody.HttpBody{ 225 | ContentType: req.File.GetContentType(), 226 | } 227 | w, _ := larking.AsHTTPBodyWriter(stream, &rsp) 228 | 229 | _, err := io.Copy(w, r) 230 | return err 231 | } 232 | ``` 233 | 234 | #### Websockets Annotations 235 | Annotate a custom method kind `websocket` to enable clients to upgrade connections. This enables streams to be bidirectional over a websocket connection. 236 | ```protobuf 237 | // Chatroom shows the websocket extension. 238 | service ChatRoom { 239 | rpc Chat(stream ChatMessage) returns (stream ChatMessage) { 240 | option (google.api.http) = { 241 | custom : {kind : "websocket" path : "/v1/{name=rooms/*}"} 242 | body : "*" 243 | }; 244 | } 245 | } 246 | ``` 247 | 248 | #### Streaming Codecs 249 | Streaming requests will upgrade the codec interface to read and write marshalled messages to the stream. 250 | Control of framing is given to the application on a per content type basis. 251 | If the underlying protocol has it's own message framing, like 'websockets', streaming codecs won't be used. 252 | Unlike gRPC streams where compression is _per message_ here the compression is per _stream_ so only a message delimiter is needed. 253 | See the [StreamCodec](https://pkg.go.dev/larking.io/larking#StreamCodec) docs for implementation details. 254 | - Protobuf messages use a varint delimiter encoding: ``. 255 | - JSON messages are delimited on the outer JSON braces `{}`. 256 | - Arbitrary content is delimited by the message size limit, chunking into bytes slices of length limit. 257 | 258 | To stream json we can append payloads together as a single payload: 259 | ``` 260 | curl -XPOST http://larking.io/v1/streaming -d '{"message":"one"}{"message":"two"}' 261 | ``` 262 | The above creates an input stream of two messages. 263 | (N.B. when using HTTP/1 fully bidirectional streaming is not possible. All stream messages must be written before receiving a response) 264 | 265 | To stream protobuf we can use [protodelim](https://pkg.go.dev/google.golang.org/protobuf@v1.30.0/encoding/protodelim) to read and write varint streams of messages. Similar libraries are found in other [languages](https://github.com/protocolbuffers/protobuf/issues/10229). 266 | 267 | #### Twirp 268 | Twirp is supported through gRPC-transcoding with the implicit methods. 269 | The implicit methods cover `POST /package.Service/Method/` matching the content types for both `application/json` and `application/proto`. 270 | Before the [v7 spec](https://twitchtv.github.io/twirp/docs/spec_v7.html) the URL required the `/twirp` prefix. 271 | We can encode the server to use a ServerOption: 272 | ```go 273 | svr, _ := larking.NewServer(mux, 274 | larking.MuxHandleOption("/", "/twirp"), // Serve mux on '/' and '/twirp' 275 | ) 276 | ``` 277 | 278 | Twirp [errors](https://twitchtv.github.io/twirp/docs/errors.html) are created from `*grpc.Status` errors. 279 | Code enums are mapped to twirp error strings and messages. Currently meta fields aren't supported. 280 | Errors will only be converted to twirp errors when the header `Twirp-Version` is set. 281 | This is used to identify a twirp request. 282 | 283 | #### External Configuration 284 | Mux can be configured with external rules using [`*serviceconfig.Service`](https://pkg.go.dev/google.golang.org/genproto/googleapis/api/serviceconfig). Load the file from yaml defintions or declare in Go. 285 | 286 | ```go 287 | mux, _ := larking.NewMux( 288 | larking.ServiceConfigOption(sc), // sc type of *serviceconfig.Service 289 | ) 290 | ``` 291 | -------------------------------------------------------------------------------- /benchmarks/README.md: -------------------------------------------------------------------------------- 1 | # Benchmarks 2 | 3 | Comparisons against other popular services. See `bench.txt` for results. 4 | Benchmarks should be taken with a grain of salt, please add more if you'd like to expand the test cases! 5 | 6 | ## Larking 7 | Larking serves both JSON and Protobuf encoded requests, tests marked with `+pb` are protobuf encoded. 8 | 9 | ### Optimisations 10 | - https://www.emcfarlane.com/blog/2023-04-18-profile-lexer 11 | - https://www.emcfarlane.com/blog/2023-05-01-bufferless-append 12 | 13 | ## gRPC-Gateway 14 | 15 | [gRPC-Gateway](https://github.com/grpc-ecosystem/grpc-gateway) 16 | generated `api/librarypb/library.pb.gw.go` file to proxy JSON requests. 17 | 18 | ## Envoy 19 | 20 | [gRPC-JSON transcoder](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter) with configuration in testdata/envoy.yaml. 21 | Slower than both larking and gRPC-Gateway, price for proxying requests. 22 | 23 | ## Gorilla Mux 24 | 25 | [Gorilla Mux](https://github.com/gorilla/mux) is a popular routing library for HTTP services in Go. 26 | Here we write a custom mux that replicates the gRPC annotations and binds the mux to the gRPC server. 27 | Compares speed with writing the annotations binding by hand, useful for compairsons with Go routing libraries. 28 | 29 | ## Connect-go 30 | 31 | [Connect-go](https://github.com/bufbuild/connect-go) is a slim library with gRPC compatible HTTP APIs. 32 | 33 | ## Twirp 34 | 35 | [Twirp](https://github.com/twitchtv/twirp) is a simple RPC protocol based on HTTP and Protocol Buffers (proto). 36 | 37 | 38 | ## gRPC 39 | 40 | Compare gRPC server benchmarks with the `mux.ServeHTTP`. 41 | We use an altered version of go-gRPC's [benchmain](https://github.com/grpc/grpc-go/blob/master/Documentation/benchmark.md) 42 | tool to run a benchmark and compare it to gRPC internal server. 43 | 44 | ``` 45 | go run benchmain/main.go -benchtime=10s -workloads=all \ 46 | -compression=gzip -maxConcurrentCalls=1 -trace=off \ 47 | -reqSizeBytes=1,1048576 -respSizeBytes=1,1048576 -networkMode=Local \ 48 | -cpuProfile=cpuProf -memProfile=memProf -memProfileRate=10000 -resultFile=result.bin 49 | ``` 50 | 51 | ``` 52 | go run google.golang.org/grpc/benchmark/benchresult grpc_result.bin result.bin 53 | ``` 54 | 55 | See `grpc-bench.txt` for gRPC results. 56 | -------------------------------------------------------------------------------- /benchmarks/api/library.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | syntax = "proto3"; 6 | 7 | package larking.benchmarks.librarypb; 8 | 9 | import "google/api/annotations.proto"; 10 | import "google/protobuf/timestamp.proto"; 11 | import "google/protobuf/duration.proto"; 12 | import "google/protobuf/field_mask.proto"; 13 | import "google/protobuf/empty.proto"; 14 | import "google/protobuf/wrappers.proto"; 15 | 16 | option go_package = "larking.io/benchmarks/api/librarypb;librarypb"; 17 | 18 | service LibraryService { 19 | rpc GetBook(GetBookRequest) returns (Book) { 20 | option (google.api.http) = { 21 | get : "/v1/{name=shelves/*/books/*}" 22 | }; 23 | }; 24 | rpc CreateBook(CreateBookRequest) returns (Book) { 25 | option (google.api.http) = { 26 | post : "/v1/{parent=shelves/*}/books" 27 | body : "book" 28 | }; 29 | }; 30 | 31 | // Lists books in a shelf. 32 | rpc ListBooks(ListBooksRequest) returns (ListBooksResponse) { 33 | // List method maps to HTTP GET. 34 | option (google.api.http) = { 35 | // The `parent` captures the parent resource name, such as 36 | // "shelves/shelf1". 37 | get : "/v1/{parent=shelves/*}/books" 38 | }; 39 | } 40 | 41 | // Updates a book. 42 | rpc UpdateBook(UpdateBookRequest) returns (Book) { 43 | // Update maps to HTTP PATCH. Resource name is mapped to a URL path. 44 | // Resource is contained in the HTTP request body. 45 | option (google.api.http) = { 46 | // Note the URL template variable which captures the resource name of the 47 | // book to update. 48 | patch : "/v1/{book.name=shelves/*/books/*}" 49 | body : "book" 50 | }; 51 | } 52 | 53 | // Deletes a book. 54 | rpc DeleteBook(DeleteBookRequest) returns (google.protobuf.Empty) { 55 | // Delete maps to HTTP DELETE. Resource name maps to the URL path. 56 | // There is no request body. 57 | option (google.api.http) = { 58 | // Note the URL template variable capturing the multi-segment name of the 59 | // book resource to be deleted, such as "shelves/shelf1/books/book2" 60 | delete : "/v1/{name=shelves/*/books/*}" 61 | }; 62 | } 63 | } 64 | 65 | message Book { 66 | // Resource name of the book. It must have the format of "shelves/*/books/*". 67 | // For example: "shelves/shelf1/books/book2". 68 | string name = 1; 69 | // The title of the book. 70 | string title = 2; 71 | // The author of the book. 72 | string author = 3; 73 | // The number of pages in the book. 74 | int32 page_count = 4; 75 | // The date the book was published. 76 | google.protobuf.Timestamp publish_time = 5; 77 | // The duration of the book. 78 | google.protobuf.Duration duration = 6; 79 | // The price of the book. 80 | google.protobuf.DoubleValue price = 7; 81 | } 82 | 83 | message GetBookRequest { 84 | // Resource name of a book. For example: "shelves/shelf1/books/book2". 85 | string name = 1; 86 | } 87 | 88 | message CreateBookRequest { 89 | // Resource name of the parent resource where to create the book. 90 | // For example: "shelves/shelf1". 91 | string parent = 1; 92 | // The Book resource to be created. Client must not set the `Book.name` field. 93 | Book book = 2; 94 | } 95 | 96 | message ListBooksRequest { 97 | // The parent resource name, for example, "shelves/shelf1". 98 | string parent = 1; 99 | 100 | // The maximum number of items to return. 101 | int32 page_size = 2; 102 | 103 | // The next_page_token value returned from a previous List request, if any. 104 | string page_token = 3; 105 | } 106 | 107 | message ListBooksResponse { 108 | // The field name should match the noun "books" in the method name. There 109 | // will be a maximum number of items returned based on the page_size field 110 | // in the request. 111 | repeated Book books = 1; 112 | 113 | // Token to retrieve the next page of results, or empty if there are no 114 | // more results in the list. 115 | string next_page_token = 2; 116 | } 117 | 118 | message UpdateBookRequest { 119 | // The book resource which replaces the resource on the server. 120 | Book book = 1; 121 | 122 | // The update mask applies to the resource. For the `FieldMask` definition, 123 | // see 124 | // https://developers.google.com/protocol-buffers/docs/reference/google.protobuf#fieldmask 125 | google.protobuf.FieldMask update_mask = 2; 126 | } 127 | 128 | message DeleteBookRequest { 129 | // The resource name of the book to be deleted, for example: 130 | // "shelves/shelf1/books/book2" 131 | string name = 1; 132 | } 133 | -------------------------------------------------------------------------------- /benchmarks/api/librarypb/library_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 6 | // versions: 7 | // - protoc-gen-go-grpc v1.3.0 8 | // - protoc v3.21.12 9 | // source: api/library.proto 10 | 11 | package librarypb 12 | 13 | import ( 14 | context "context" 15 | grpc "google.golang.org/grpc" 16 | codes "google.golang.org/grpc/codes" 17 | status "google.golang.org/grpc/status" 18 | emptypb "google.golang.org/protobuf/types/known/emptypb" 19 | ) 20 | 21 | // This is a compile-time assertion to ensure that this generated file 22 | // is compatible with the grpc package it is being compiled against. 23 | // Requires gRPC-Go v1.32.0 or later. 24 | const _ = grpc.SupportPackageIsVersion7 25 | 26 | const ( 27 | LibraryService_GetBook_FullMethodName = "/larking.benchmarks.librarypb.LibraryService/GetBook" 28 | LibraryService_CreateBook_FullMethodName = "/larking.benchmarks.librarypb.LibraryService/CreateBook" 29 | LibraryService_ListBooks_FullMethodName = "/larking.benchmarks.librarypb.LibraryService/ListBooks" 30 | LibraryService_UpdateBook_FullMethodName = "/larking.benchmarks.librarypb.LibraryService/UpdateBook" 31 | LibraryService_DeleteBook_FullMethodName = "/larking.benchmarks.librarypb.LibraryService/DeleteBook" 32 | ) 33 | 34 | // LibraryServiceClient is the client API for LibraryService service. 35 | // 36 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 37 | type LibraryServiceClient interface { 38 | GetBook(ctx context.Context, in *GetBookRequest, opts ...grpc.CallOption) (*Book, error) 39 | CreateBook(ctx context.Context, in *CreateBookRequest, opts ...grpc.CallOption) (*Book, error) 40 | // Lists books in a shelf. 41 | ListBooks(ctx context.Context, in *ListBooksRequest, opts ...grpc.CallOption) (*ListBooksResponse, error) 42 | // Updates a book. 43 | UpdateBook(ctx context.Context, in *UpdateBookRequest, opts ...grpc.CallOption) (*Book, error) 44 | // Deletes a book. 45 | DeleteBook(ctx context.Context, in *DeleteBookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) 46 | } 47 | 48 | type libraryServiceClient struct { 49 | cc grpc.ClientConnInterface 50 | } 51 | 52 | func NewLibraryServiceClient(cc grpc.ClientConnInterface) LibraryServiceClient { 53 | return &libraryServiceClient{cc} 54 | } 55 | 56 | func (c *libraryServiceClient) GetBook(ctx context.Context, in *GetBookRequest, opts ...grpc.CallOption) (*Book, error) { 57 | out := new(Book) 58 | err := c.cc.Invoke(ctx, LibraryService_GetBook_FullMethodName, in, out, opts...) 59 | if err != nil { 60 | return nil, err 61 | } 62 | return out, nil 63 | } 64 | 65 | func (c *libraryServiceClient) CreateBook(ctx context.Context, in *CreateBookRequest, opts ...grpc.CallOption) (*Book, error) { 66 | out := new(Book) 67 | err := c.cc.Invoke(ctx, LibraryService_CreateBook_FullMethodName, in, out, opts...) 68 | if err != nil { 69 | return nil, err 70 | } 71 | return out, nil 72 | } 73 | 74 | func (c *libraryServiceClient) ListBooks(ctx context.Context, in *ListBooksRequest, opts ...grpc.CallOption) (*ListBooksResponse, error) { 75 | out := new(ListBooksResponse) 76 | err := c.cc.Invoke(ctx, LibraryService_ListBooks_FullMethodName, in, out, opts...) 77 | if err != nil { 78 | return nil, err 79 | } 80 | return out, nil 81 | } 82 | 83 | func (c *libraryServiceClient) UpdateBook(ctx context.Context, in *UpdateBookRequest, opts ...grpc.CallOption) (*Book, error) { 84 | out := new(Book) 85 | err := c.cc.Invoke(ctx, LibraryService_UpdateBook_FullMethodName, in, out, opts...) 86 | if err != nil { 87 | return nil, err 88 | } 89 | return out, nil 90 | } 91 | 92 | func (c *libraryServiceClient) DeleteBook(ctx context.Context, in *DeleteBookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { 93 | out := new(emptypb.Empty) 94 | err := c.cc.Invoke(ctx, LibraryService_DeleteBook_FullMethodName, in, out, opts...) 95 | if err != nil { 96 | return nil, err 97 | } 98 | return out, nil 99 | } 100 | 101 | // LibraryServiceServer is the server API for LibraryService service. 102 | // All implementations must embed UnimplementedLibraryServiceServer 103 | // for forward compatibility 104 | type LibraryServiceServer interface { 105 | GetBook(context.Context, *GetBookRequest) (*Book, error) 106 | CreateBook(context.Context, *CreateBookRequest) (*Book, error) 107 | // Lists books in a shelf. 108 | ListBooks(context.Context, *ListBooksRequest) (*ListBooksResponse, error) 109 | // Updates a book. 110 | UpdateBook(context.Context, *UpdateBookRequest) (*Book, error) 111 | // Deletes a book. 112 | DeleteBook(context.Context, *DeleteBookRequest) (*emptypb.Empty, error) 113 | mustEmbedUnimplementedLibraryServiceServer() 114 | } 115 | 116 | // UnimplementedLibraryServiceServer must be embedded to have forward compatible implementations. 117 | type UnimplementedLibraryServiceServer struct { 118 | } 119 | 120 | func (UnimplementedLibraryServiceServer) GetBook(context.Context, *GetBookRequest) (*Book, error) { 121 | return nil, status.Errorf(codes.Unimplemented, "method GetBook not implemented") 122 | } 123 | func (UnimplementedLibraryServiceServer) CreateBook(context.Context, *CreateBookRequest) (*Book, error) { 124 | return nil, status.Errorf(codes.Unimplemented, "method CreateBook not implemented") 125 | } 126 | func (UnimplementedLibraryServiceServer) ListBooks(context.Context, *ListBooksRequest) (*ListBooksResponse, error) { 127 | return nil, status.Errorf(codes.Unimplemented, "method ListBooks not implemented") 128 | } 129 | func (UnimplementedLibraryServiceServer) UpdateBook(context.Context, *UpdateBookRequest) (*Book, error) { 130 | return nil, status.Errorf(codes.Unimplemented, "method UpdateBook not implemented") 131 | } 132 | func (UnimplementedLibraryServiceServer) DeleteBook(context.Context, *DeleteBookRequest) (*emptypb.Empty, error) { 133 | return nil, status.Errorf(codes.Unimplemented, "method DeleteBook not implemented") 134 | } 135 | func (UnimplementedLibraryServiceServer) mustEmbedUnimplementedLibraryServiceServer() {} 136 | 137 | // UnsafeLibraryServiceServer may be embedded to opt out of forward compatibility for this service. 138 | // Use of this interface is not recommended, as added methods to LibraryServiceServer will 139 | // result in compilation errors. 140 | type UnsafeLibraryServiceServer interface { 141 | mustEmbedUnimplementedLibraryServiceServer() 142 | } 143 | 144 | func RegisterLibraryServiceServer(s grpc.ServiceRegistrar, srv LibraryServiceServer) { 145 | s.RegisterService(&LibraryService_ServiceDesc, srv) 146 | } 147 | 148 | func _LibraryService_GetBook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 149 | in := new(GetBookRequest) 150 | if err := dec(in); err != nil { 151 | return nil, err 152 | } 153 | if interceptor == nil { 154 | return srv.(LibraryServiceServer).GetBook(ctx, in) 155 | } 156 | info := &grpc.UnaryServerInfo{ 157 | Server: srv, 158 | FullMethod: LibraryService_GetBook_FullMethodName, 159 | } 160 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 161 | return srv.(LibraryServiceServer).GetBook(ctx, req.(*GetBookRequest)) 162 | } 163 | return interceptor(ctx, in, info, handler) 164 | } 165 | 166 | func _LibraryService_CreateBook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 167 | in := new(CreateBookRequest) 168 | if err := dec(in); err != nil { 169 | return nil, err 170 | } 171 | if interceptor == nil { 172 | return srv.(LibraryServiceServer).CreateBook(ctx, in) 173 | } 174 | info := &grpc.UnaryServerInfo{ 175 | Server: srv, 176 | FullMethod: LibraryService_CreateBook_FullMethodName, 177 | } 178 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 179 | return srv.(LibraryServiceServer).CreateBook(ctx, req.(*CreateBookRequest)) 180 | } 181 | return interceptor(ctx, in, info, handler) 182 | } 183 | 184 | func _LibraryService_ListBooks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 185 | in := new(ListBooksRequest) 186 | if err := dec(in); err != nil { 187 | return nil, err 188 | } 189 | if interceptor == nil { 190 | return srv.(LibraryServiceServer).ListBooks(ctx, in) 191 | } 192 | info := &grpc.UnaryServerInfo{ 193 | Server: srv, 194 | FullMethod: LibraryService_ListBooks_FullMethodName, 195 | } 196 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 197 | return srv.(LibraryServiceServer).ListBooks(ctx, req.(*ListBooksRequest)) 198 | } 199 | return interceptor(ctx, in, info, handler) 200 | } 201 | 202 | func _LibraryService_UpdateBook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 203 | in := new(UpdateBookRequest) 204 | if err := dec(in); err != nil { 205 | return nil, err 206 | } 207 | if interceptor == nil { 208 | return srv.(LibraryServiceServer).UpdateBook(ctx, in) 209 | } 210 | info := &grpc.UnaryServerInfo{ 211 | Server: srv, 212 | FullMethod: LibraryService_UpdateBook_FullMethodName, 213 | } 214 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 215 | return srv.(LibraryServiceServer).UpdateBook(ctx, req.(*UpdateBookRequest)) 216 | } 217 | return interceptor(ctx, in, info, handler) 218 | } 219 | 220 | func _LibraryService_DeleteBook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 221 | in := new(DeleteBookRequest) 222 | if err := dec(in); err != nil { 223 | return nil, err 224 | } 225 | if interceptor == nil { 226 | return srv.(LibraryServiceServer).DeleteBook(ctx, in) 227 | } 228 | info := &grpc.UnaryServerInfo{ 229 | Server: srv, 230 | FullMethod: LibraryService_DeleteBook_FullMethodName, 231 | } 232 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 233 | return srv.(LibraryServiceServer).DeleteBook(ctx, req.(*DeleteBookRequest)) 234 | } 235 | return interceptor(ctx, in, info, handler) 236 | } 237 | 238 | // LibraryService_ServiceDesc is the grpc.ServiceDesc for LibraryService service. 239 | // It's only intended for direct use with grpc.RegisterService, 240 | // and not to be introspected or modified (even as a copy) 241 | var LibraryService_ServiceDesc = grpc.ServiceDesc{ 242 | ServiceName: "larking.benchmarks.librarypb.LibraryService", 243 | HandlerType: (*LibraryServiceServer)(nil), 244 | Methods: []grpc.MethodDesc{ 245 | { 246 | MethodName: "GetBook", 247 | Handler: _LibraryService_GetBook_Handler, 248 | }, 249 | { 250 | MethodName: "CreateBook", 251 | Handler: _LibraryService_CreateBook_Handler, 252 | }, 253 | { 254 | MethodName: "ListBooks", 255 | Handler: _LibraryService_ListBooks_Handler, 256 | }, 257 | { 258 | MethodName: "UpdateBook", 259 | Handler: _LibraryService_UpdateBook_Handler, 260 | }, 261 | { 262 | MethodName: "DeleteBook", 263 | Handler: _LibraryService_DeleteBook_Handler, 264 | }, 265 | }, 266 | Streams: []grpc.StreamDesc{}, 267 | Metadata: "api/library.proto", 268 | } 269 | -------------------------------------------------------------------------------- /benchmarks/api/librarypb/librarypbconnect/library.connect.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Code generated by protoc-gen-connect-go. DO NOT EDIT. 6 | // 7 | // Source: api/library.proto 8 | 9 | package librarypbconnect 10 | 11 | import ( 12 | context "context" 13 | errors "errors" 14 | connect_go "github.com/bufbuild/connect-go" 15 | emptypb "google.golang.org/protobuf/types/known/emptypb" 16 | librarypb "larking.io/benchmarks/api/librarypb" 17 | http "net/http" 18 | strings "strings" 19 | ) 20 | 21 | // This is a compile-time assertion to ensure that this generated file and the connect package are 22 | // compatible. If you get a compiler error that this constant is not defined, this code was 23 | // generated with a version of connect newer than the one compiled into your binary. You can fix the 24 | // problem by either regenerating this code with an older version of connect or updating the connect 25 | // version compiled into your binary. 26 | const _ = connect_go.IsAtLeastVersion0_1_0 27 | 28 | const ( 29 | // LibraryServiceName is the fully-qualified name of the LibraryService service. 30 | LibraryServiceName = "larking.benchmarks.librarypb.LibraryService" 31 | ) 32 | 33 | // These constants are the fully-qualified names of the RPCs defined in this package. They're 34 | // exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. 35 | // 36 | // Note that these are different from the fully-qualified method names used by 37 | // google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to 38 | // reflection-formatted method names, remove the leading slash and convert the remaining slash to a 39 | // period. 40 | const ( 41 | // LibraryServiceGetBookProcedure is the fully-qualified name of the LibraryService's GetBook RPC. 42 | LibraryServiceGetBookProcedure = "/larking.benchmarks.librarypb.LibraryService/GetBook" 43 | // LibraryServiceCreateBookProcedure is the fully-qualified name of the LibraryService's CreateBook 44 | // RPC. 45 | LibraryServiceCreateBookProcedure = "/larking.benchmarks.librarypb.LibraryService/CreateBook" 46 | // LibraryServiceListBooksProcedure is the fully-qualified name of the LibraryService's ListBooks 47 | // RPC. 48 | LibraryServiceListBooksProcedure = "/larking.benchmarks.librarypb.LibraryService/ListBooks" 49 | // LibraryServiceUpdateBookProcedure is the fully-qualified name of the LibraryService's UpdateBook 50 | // RPC. 51 | LibraryServiceUpdateBookProcedure = "/larking.benchmarks.librarypb.LibraryService/UpdateBook" 52 | // LibraryServiceDeleteBookProcedure is the fully-qualified name of the LibraryService's DeleteBook 53 | // RPC. 54 | LibraryServiceDeleteBookProcedure = "/larking.benchmarks.librarypb.LibraryService/DeleteBook" 55 | ) 56 | 57 | // LibraryServiceClient is a client for the larking.benchmarks.librarypb.LibraryService service. 58 | type LibraryServiceClient interface { 59 | GetBook(context.Context, *connect_go.Request[librarypb.GetBookRequest]) (*connect_go.Response[librarypb.Book], error) 60 | CreateBook(context.Context, *connect_go.Request[librarypb.CreateBookRequest]) (*connect_go.Response[librarypb.Book], error) 61 | // Lists books in a shelf. 62 | ListBooks(context.Context, *connect_go.Request[librarypb.ListBooksRequest]) (*connect_go.Response[librarypb.ListBooksResponse], error) 63 | // Updates a book. 64 | UpdateBook(context.Context, *connect_go.Request[librarypb.UpdateBookRequest]) (*connect_go.Response[librarypb.Book], error) 65 | // Deletes a book. 66 | DeleteBook(context.Context, *connect_go.Request[librarypb.DeleteBookRequest]) (*connect_go.Response[emptypb.Empty], error) 67 | } 68 | 69 | // NewLibraryServiceClient constructs a client for the larking.benchmarks.librarypb.LibraryService 70 | // service. By default, it uses the Connect protocol with the binary Protobuf Codec, asks for 71 | // gzipped responses, and sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply 72 | // the connect.WithGRPC() or connect.WithGRPCWeb() options. 73 | // 74 | // The URL supplied here should be the base URL for the Connect or gRPC server (for example, 75 | // http://api.acme.com or https://acme.com/grpc). 76 | func NewLibraryServiceClient(httpClient connect_go.HTTPClient, baseURL string, opts ...connect_go.ClientOption) LibraryServiceClient { 77 | baseURL = strings.TrimRight(baseURL, "/") 78 | return &libraryServiceClient{ 79 | getBook: connect_go.NewClient[librarypb.GetBookRequest, librarypb.Book]( 80 | httpClient, 81 | baseURL+LibraryServiceGetBookProcedure, 82 | opts..., 83 | ), 84 | createBook: connect_go.NewClient[librarypb.CreateBookRequest, librarypb.Book]( 85 | httpClient, 86 | baseURL+LibraryServiceCreateBookProcedure, 87 | opts..., 88 | ), 89 | listBooks: connect_go.NewClient[librarypb.ListBooksRequest, librarypb.ListBooksResponse]( 90 | httpClient, 91 | baseURL+LibraryServiceListBooksProcedure, 92 | opts..., 93 | ), 94 | updateBook: connect_go.NewClient[librarypb.UpdateBookRequest, librarypb.Book]( 95 | httpClient, 96 | baseURL+LibraryServiceUpdateBookProcedure, 97 | opts..., 98 | ), 99 | deleteBook: connect_go.NewClient[librarypb.DeleteBookRequest, emptypb.Empty]( 100 | httpClient, 101 | baseURL+LibraryServiceDeleteBookProcedure, 102 | opts..., 103 | ), 104 | } 105 | } 106 | 107 | // libraryServiceClient implements LibraryServiceClient. 108 | type libraryServiceClient struct { 109 | getBook *connect_go.Client[librarypb.GetBookRequest, librarypb.Book] 110 | createBook *connect_go.Client[librarypb.CreateBookRequest, librarypb.Book] 111 | listBooks *connect_go.Client[librarypb.ListBooksRequest, librarypb.ListBooksResponse] 112 | updateBook *connect_go.Client[librarypb.UpdateBookRequest, librarypb.Book] 113 | deleteBook *connect_go.Client[librarypb.DeleteBookRequest, emptypb.Empty] 114 | } 115 | 116 | // GetBook calls larking.benchmarks.librarypb.LibraryService.GetBook. 117 | func (c *libraryServiceClient) GetBook(ctx context.Context, req *connect_go.Request[librarypb.GetBookRequest]) (*connect_go.Response[librarypb.Book], error) { 118 | return c.getBook.CallUnary(ctx, req) 119 | } 120 | 121 | // CreateBook calls larking.benchmarks.librarypb.LibraryService.CreateBook. 122 | func (c *libraryServiceClient) CreateBook(ctx context.Context, req *connect_go.Request[librarypb.CreateBookRequest]) (*connect_go.Response[librarypb.Book], error) { 123 | return c.createBook.CallUnary(ctx, req) 124 | } 125 | 126 | // ListBooks calls larking.benchmarks.librarypb.LibraryService.ListBooks. 127 | func (c *libraryServiceClient) ListBooks(ctx context.Context, req *connect_go.Request[librarypb.ListBooksRequest]) (*connect_go.Response[librarypb.ListBooksResponse], error) { 128 | return c.listBooks.CallUnary(ctx, req) 129 | } 130 | 131 | // UpdateBook calls larking.benchmarks.librarypb.LibraryService.UpdateBook. 132 | func (c *libraryServiceClient) UpdateBook(ctx context.Context, req *connect_go.Request[librarypb.UpdateBookRequest]) (*connect_go.Response[librarypb.Book], error) { 133 | return c.updateBook.CallUnary(ctx, req) 134 | } 135 | 136 | // DeleteBook calls larking.benchmarks.librarypb.LibraryService.DeleteBook. 137 | func (c *libraryServiceClient) DeleteBook(ctx context.Context, req *connect_go.Request[librarypb.DeleteBookRequest]) (*connect_go.Response[emptypb.Empty], error) { 138 | return c.deleteBook.CallUnary(ctx, req) 139 | } 140 | 141 | // LibraryServiceHandler is an implementation of the larking.benchmarks.librarypb.LibraryService 142 | // service. 143 | type LibraryServiceHandler interface { 144 | GetBook(context.Context, *connect_go.Request[librarypb.GetBookRequest]) (*connect_go.Response[librarypb.Book], error) 145 | CreateBook(context.Context, *connect_go.Request[librarypb.CreateBookRequest]) (*connect_go.Response[librarypb.Book], error) 146 | // Lists books in a shelf. 147 | ListBooks(context.Context, *connect_go.Request[librarypb.ListBooksRequest]) (*connect_go.Response[librarypb.ListBooksResponse], error) 148 | // Updates a book. 149 | UpdateBook(context.Context, *connect_go.Request[librarypb.UpdateBookRequest]) (*connect_go.Response[librarypb.Book], error) 150 | // Deletes a book. 151 | DeleteBook(context.Context, *connect_go.Request[librarypb.DeleteBookRequest]) (*connect_go.Response[emptypb.Empty], error) 152 | } 153 | 154 | // NewLibraryServiceHandler builds an HTTP handler from the service implementation. It returns the 155 | // path on which to mount the handler and the handler itself. 156 | // 157 | // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf 158 | // and JSON codecs. They also support gzip compression. 159 | func NewLibraryServiceHandler(svc LibraryServiceHandler, opts ...connect_go.HandlerOption) (string, http.Handler) { 160 | mux := http.NewServeMux() 161 | mux.Handle(LibraryServiceGetBookProcedure, connect_go.NewUnaryHandler( 162 | LibraryServiceGetBookProcedure, 163 | svc.GetBook, 164 | opts..., 165 | )) 166 | mux.Handle(LibraryServiceCreateBookProcedure, connect_go.NewUnaryHandler( 167 | LibraryServiceCreateBookProcedure, 168 | svc.CreateBook, 169 | opts..., 170 | )) 171 | mux.Handle(LibraryServiceListBooksProcedure, connect_go.NewUnaryHandler( 172 | LibraryServiceListBooksProcedure, 173 | svc.ListBooks, 174 | opts..., 175 | )) 176 | mux.Handle(LibraryServiceUpdateBookProcedure, connect_go.NewUnaryHandler( 177 | LibraryServiceUpdateBookProcedure, 178 | svc.UpdateBook, 179 | opts..., 180 | )) 181 | mux.Handle(LibraryServiceDeleteBookProcedure, connect_go.NewUnaryHandler( 182 | LibraryServiceDeleteBookProcedure, 183 | svc.DeleteBook, 184 | opts..., 185 | )) 186 | return "/larking.benchmarks.librarypb.LibraryService/", mux 187 | } 188 | 189 | // UnimplementedLibraryServiceHandler returns CodeUnimplemented from all methods. 190 | type UnimplementedLibraryServiceHandler struct{} 191 | 192 | func (UnimplementedLibraryServiceHandler) GetBook(context.Context, *connect_go.Request[librarypb.GetBookRequest]) (*connect_go.Response[librarypb.Book], error) { 193 | return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("larking.benchmarks.librarypb.LibraryService.GetBook is not implemented")) 194 | } 195 | 196 | func (UnimplementedLibraryServiceHandler) CreateBook(context.Context, *connect_go.Request[librarypb.CreateBookRequest]) (*connect_go.Response[librarypb.Book], error) { 197 | return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("larking.benchmarks.librarypb.LibraryService.CreateBook is not implemented")) 198 | } 199 | 200 | func (UnimplementedLibraryServiceHandler) ListBooks(context.Context, *connect_go.Request[librarypb.ListBooksRequest]) (*connect_go.Response[librarypb.ListBooksResponse], error) { 201 | return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("larking.benchmarks.librarypb.LibraryService.ListBooks is not implemented")) 202 | } 203 | 204 | func (UnimplementedLibraryServiceHandler) UpdateBook(context.Context, *connect_go.Request[librarypb.UpdateBookRequest]) (*connect_go.Response[librarypb.Book], error) { 205 | return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("larking.benchmarks.librarypb.LibraryService.UpdateBook is not implemented")) 206 | } 207 | 208 | func (UnimplementedLibraryServiceHandler) DeleteBook(context.Context, *connect_go.Request[librarypb.DeleteBookRequest]) (*connect_go.Response[emptypb.Empty], error) { 209 | return nil, connect_go.NewError(connect_go.CodeUnimplemented, errors.New("larking.benchmarks.librarypb.LibraryService.DeleteBook is not implemented")) 210 | } 211 | -------------------------------------------------------------------------------- /benchmarks/api/proto.pb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/emcfarlane/larking/191f5a2ad1102341ec7a4c7849ac417589b864c9/benchmarks/api/proto.pb -------------------------------------------------------------------------------- /benchmarks/bench.txt: -------------------------------------------------------------------------------- 1 | goos: darwin 2 | goarch: arm64 3 | pkg: larking.io/benchmarks 4 | BenchmarkLarking/GRPC_GetBook-8 19376 60966 ns/op 13877 B/op 193 allocs/op 5 | BenchmarkLarking/HTTP_GetBook-8 25690 46071 ns/op 9230 B/op 144 allocs/op 6 | BenchmarkLarking/HTTP_UpdateBook-8 24384 49030 ns/op 11228 B/op 171 allocs/op 7 | BenchmarkLarking/HTTP_DeleteBook-8 31225 38384 ns/op 8002 B/op 96 allocs/op 8 | BenchmarkLarking/HTTP_GetBook+pb-8 30403 39225 ns/op 8351 B/op 102 allocs/op 9 | BenchmarkLarking/HTTP_UpdateBook+pb-8 26089 45797 ns/op 9929 B/op 132 allocs/op 10 | BenchmarkLarking/HTTP_DeleteBook+pb-8 31750 37533 ns/op 7773 B/op 93 allocs/op 11 | BenchmarkGRPCGateway/GRPC_GetBook-8 29191 40917 ns/op 9498 B/op 178 allocs/op 12 | BenchmarkGRPCGateway/HTTP_GetBook-8 25154 46624 ns/op 11200 B/op 179 allocs/op 13 | BenchmarkGRPCGateway/HTTP_UpdateBook-8 23169 51382 ns/op 16404 B/op 230 allocs/op 14 | BenchmarkGRPCGateway/HTTP_DeleteBook-8 31365 38172 ns/op 9160 B/op 119 allocs/op 15 | BenchmarkEnvoyGRPC/GRPC_GetBook-8 9067 131554 ns/op 10960 B/op 177 allocs/op 16 | BenchmarkEnvoyGRPC/HTTP_GetBook-8 8090 148991 ns/op 9974 B/op 163 allocs/op 17 | BenchmarkEnvoyGRPC/HTTP_UpdateBook-8 7807 150534 ns/op 10648 B/op 166 allocs/op 18 | BenchmarkEnvoyGRPC/HTTP_DeleteBook-8 8866 133976 ns/op 9026 B/op 126 allocs/op 19 | BenchmarkGorillaMux/HTTP_GetBook-8 26590 45013 ns/op 9874 B/op 143 allocs/op 20 | BenchmarkGorillaMux/HTTP_UpdateBook-8 26713 44912 ns/op 11791 B/op 166 allocs/op 21 | BenchmarkGorillaMux/HTTP_DeleteBook-8 32792 36645 ns/op 8143 B/op 89 allocs/op 22 | BenchmarkConnectGo/GRPC_GetBook-8 18492 64467 ns/op 13437 B/op 194 allocs/op 23 | BenchmarkConnectGo/HTTP_GetBook-8 24248 49106 ns/op 10996 B/op 176 allocs/op 24 | BenchmarkConnectGo/HTTP_UpdateBook-8 25184 47556 ns/op 11232 B/op 183 allocs/op 25 | BenchmarkConnectGo/HTTP_DeleteBook-8 28917 41493 ns/op 9299 B/op 122 allocs/op 26 | BenchmarkConnectGo/Connect_GetBook-8 15196 79021 ns/op 77348 B/op 151 allocs/op 27 | BenchmarkConnectGo/Connect_UpdateBook-8 16684 73370 ns/op 79413 B/op 151 allocs/op 28 | BenchmarkConnectGo/Connect_DeleteBook-8 18038 66268 ns/op 69503 B/op 140 allocs/op 29 | BenchmarkTwirp/HTTP_GetBook-8 23119 49839 ns/op 12679 B/op 196 allocs/op 30 | BenchmarkTwirp/HTTP_UpdateBook-8 23336 52310 ns/op 13456 B/op 214 allocs/op 31 | BenchmarkTwirp/HTTP_DeleteBook-8 28702 41848 ns/op 10766 B/op 140 allocs/op 32 | PASS 33 | ok larking.io/benchmarks 47.310s 34 | -------------------------------------------------------------------------------- /benchmarks/connect_svc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | connect_go "github.com/bufbuild/connect-go" 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/codes" 10 | "google.golang.org/protobuf/types/known/durationpb" 11 | "google.golang.org/protobuf/types/known/emptypb" 12 | "google.golang.org/protobuf/types/known/timestamppb" 13 | "google.golang.org/protobuf/types/known/wrapperspb" 14 | "larking.io/benchmarks/api/librarypb" 15 | cpb "larking.io/benchmarks/api/librarypb/librarypbconnect" 16 | ) 17 | 18 | type testConnectService struct { 19 | cpb.UnimplementedLibraryServiceHandler 20 | } 21 | 22 | func (testConnectService) GetBook(ctx context.Context, req *connect_go.Request[librarypb.GetBookRequest]) (*connect_go.Response[librarypb.Book], error) { 23 | if req.Msg.Name != "shelves/1/books/1" { 24 | return nil, grpc.Errorf(codes.NotFound, "not found") 25 | } 26 | return connect_go.NewResponse(&librarypb.Book{ 27 | Name: "shelves/1/books/1", 28 | Title: "The Great Gatsby", 29 | Author: "F. Scott Fitzgerald", 30 | PageCount: 180, 31 | PublishTime: timestamppb.New(time.Now()), 32 | Duration: durationpb.New(1 * time.Hour), 33 | Price: wrapperspb.Double(9.99), 34 | }), nil 35 | } 36 | 37 | func (testConnectService) ListBooks(ctx context.Context, req *connect_go.Request[librarypb.ListBooksRequest]) (*connect_go.Response[librarypb.ListBooksResponse], error) { 38 | if req.Msg.Parent != "shelves/1" { 39 | return nil, grpc.Errorf(codes.NotFound, "not found") 40 | } 41 | return connect_go.NewResponse(&librarypb.ListBooksResponse{ 42 | Books: []*librarypb.Book{ 43 | { 44 | Name: "shelves/1/books/1", 45 | Title: "The Great Gatsby", 46 | }, 47 | { 48 | Name: "shelves/1/books/2", 49 | Title: "The Catcher in the Rye", 50 | }, 51 | { 52 | Name: "shelves/1/books/3", 53 | Title: "The Grapes of Wrath", 54 | }, 55 | }, 56 | }), nil 57 | } 58 | 59 | func (testConnectService) CreateBook(ctx context.Context, req *connect_go.Request[librarypb.CreateBookRequest]) (*connect_go.Response[librarypb.Book], error) { 60 | return connect_go.NewResponse(req.Msg.Book), nil 61 | } 62 | func (testConnectService) UpdateBook(ctx context.Context, req *connect_go.Request[librarypb.UpdateBookRequest]) (*connect_go.Response[librarypb.Book], error) { 63 | if req.Msg.Book.GetName() != "shelves/1/books/1" { 64 | return nil, grpc.Errorf(codes.NotFound, "not found") 65 | } 66 | if req.Msg.UpdateMask.Paths[0] != "book.title" { 67 | return nil, grpc.Errorf(codes.InvalidArgument, "invalid field mask") 68 | } 69 | return connect_go.NewResponse(req.Msg.Book), nil 70 | } 71 | func (testConnectService) DeleteBook(ctx context.Context, req *connect_go.Request[librarypb.DeleteBookRequest]) (*connect_go.Response[emptypb.Empty], error) { 72 | if req.Msg.Name != "shelves/1/books/1" { 73 | return nil, grpc.Errorf(codes.NotFound, "not found") 74 | } 75 | return connect_go.NewResponse(&emptypb.Empty{}), nil 76 | } 77 | -------------------------------------------------------------------------------- /benchmarks/gen.sh: -------------------------------------------------------------------------------- 1 | protoc -I ~/src/github.com/googleapis/api-common-protos/ -I. \ 2 | --go_out=module=larking.io/benchmarks:. \ 3 | --go-grpc_out=module=larking.io/benchmarks:. \ 4 | --grpc-gateway_out=module=larking.io/benchmarks:. \ 5 | --connect-go_out=module=larking.io/benchmarks:. \ 6 | --twirp_out=module=larking.io/benchmarks:. \ 7 | --include_imports --include_source_info \ 8 | --descriptor_set_out=api/proto.pb \ 9 | ./api/*.proto 10 | -------------------------------------------------------------------------------- /benchmarks/go.mod: -------------------------------------------------------------------------------- 1 | module larking.io/benchmarks 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/bufbuild/connect-go v1.7.0 7 | github.com/gorilla/mux v1.8.0 8 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 9 | github.com/soheilhy/cmux v0.1.5 10 | github.com/twitchtv/twirp v8.1.3+incompatible 11 | golang.org/x/net v0.9.0 12 | golang.org/x/sync v0.1.0 13 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 14 | google.golang.org/grpc v1.54.0 15 | google.golang.org/protobuf v1.30.1-0.20230501154320-cf06b0c33cda 16 | larking.io v0.0.0-20230415140254-4fbc95c206cd 17 | ) 18 | 19 | require ( 20 | github.com/gobwas/httphead v0.1.0 // indirect 21 | github.com/gobwas/pool v0.2.1 // indirect 22 | github.com/gobwas/ws v1.2.0 // indirect 23 | github.com/golang/protobuf v1.5.3 // indirect 24 | github.com/pkg/errors v0.9.1 // indirect 25 | golang.org/x/sys v0.7.0 // indirect 26 | golang.org/x/text v0.9.0 // indirect 27 | ) 28 | 29 | replace larking.io => ../ 30 | -------------------------------------------------------------------------------- /benchmarks/go.sum: -------------------------------------------------------------------------------- 1 | github.com/bufbuild/connect-go v1.7.0 h1:MGp82v7SCza+3RhsVhV7aMikwxvI3ZfD72YiGt8FYJo= 2 | github.com/bufbuild/connect-go v1.7.0/go.mod h1:GmMJYR6orFqD0Y6ZgX8pwQ8j9baizDrIQMm1/a6LnHk= 3 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 4 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 5 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 6 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 7 | github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I= 8 | github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= 9 | github.com/golang/glog v1.0.0 h1:nfP3RFugxnNRyKgeWd4oI1nYvXpxrx8ck8ZrcizshdQ= 10 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 11 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 12 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 13 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 14 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 15 | github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= 16 | github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= 17 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2 h1:gDLXvp5S9izjldquuoAhDzccbskOL6tDC5jMSyx3zxE= 18 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.15.2/go.mod h1:7pdNwVWBBHGiCxa9lAszqCJMbfTISJ7oMftp8+UGV08= 19 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 20 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 21 | github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= 22 | github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= 23 | github.com/twitchtv/twirp v8.1.3+incompatible h1:+F4TdErPgSUbMZMwp13Q/KgDVuI7HJXP61mNV3/7iuU= 24 | github.com/twitchtv/twirp v8.1.3+incompatible/go.mod h1:RRJoFSAmTEh2weEqWtpPE3vFK5YBhA6bqp2l1kfCC5A= 25 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 26 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 27 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 28 | golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 29 | golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= 30 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 31 | golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= 32 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 33 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 34 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 35 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 36 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 37 | golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= 38 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 39 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 40 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 41 | golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= 42 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 43 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 44 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 45 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= 46 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= 47 | google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag= 48 | google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= 49 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 50 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 51 | google.golang.org/protobuf v1.30.1-0.20230501154320-cf06b0c33cda h1:CGKs/jtLmFiQ0tmmt8ykIoaKqn+yi8T/reVFvwOR5aY= 52 | google.golang.org/protobuf v1.30.1-0.20230501154320-cf06b0c33cda/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 53 | -------------------------------------------------------------------------------- /benchmarks/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | "net/http" 7 | 8 | "google.golang.org/grpc" 9 | "google.golang.org/grpc/credentials/insecure" 10 | "larking.io/benchmarks/api/librarypb" 11 | ) 12 | 13 | //go:generate sh gen.sh 14 | 15 | func run() error { 16 | svc := &testService{} 17 | gs := grpc.NewServer(grpc.Creds(insecure.NewCredentials())) 18 | librarypb.RegisterLibraryServiceServer(gs, svc) 19 | 20 | lis, err := net.Listen("tcp", "localhost:5050") 21 | if err != nil { 22 | return err 23 | } 24 | defer lis.Close() 25 | 26 | log.Printf("listening on %s", lis.Addr().String()) 27 | if err := gs.Serve(lis); err != nil && err != http.ErrServerClosed { 28 | return err 29 | } 30 | return nil 31 | } 32 | 33 | func main() { 34 | if err := run(); err != nil { 35 | log.Fatal(err) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /benchmarks/server/server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * Copyright 2014 gRPC authors. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | * 17 | */ 18 | 19 | /* 20 | Package server implements the building blocks to setup end-to-end gRPC benchmarks. 21 | */ 22 | package server 23 | 24 | import ( 25 | "context" 26 | "fmt" 27 | "io" 28 | "log" 29 | "net" 30 | 31 | "google.golang.org/grpc" 32 | "google.golang.org/grpc/codes" 33 | "google.golang.org/grpc/grpclog" 34 | "google.golang.org/grpc/metadata" 35 | "google.golang.org/grpc/status" 36 | 37 | testpb "google.golang.org/grpc/interop/grpc_testing" 38 | "larking.io/larking" 39 | ) 40 | 41 | var logger = grpclog.Component("benchmark") 42 | 43 | // Allows reuse of the same testpb.Payload object. 44 | func setPayload(p *testpb.Payload, t testpb.PayloadType, size int) { 45 | if size < 0 { 46 | logger.Fatalf("Requested a response with invalid length %d", size) 47 | } 48 | body := make([]byte, size) 49 | switch t { 50 | case testpb.PayloadType_COMPRESSABLE: 51 | default: 52 | logger.Fatalf("Unsupported payload type: %d", t) 53 | } 54 | p.Type = t 55 | p.Body = body 56 | } 57 | 58 | // NewPayload creates a payload with the given type and size. 59 | func NewPayload(t testpb.PayloadType, size int) *testpb.Payload { 60 | p := new(testpb.Payload) 61 | setPayload(p, t, size) 62 | return p 63 | } 64 | 65 | type testServer struct { 66 | testpb.UnimplementedBenchmarkServiceServer 67 | } 68 | 69 | func (s *testServer) UnaryCall(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) { 70 | return &testpb.SimpleResponse{ 71 | Payload: NewPayload(in.ResponseType, int(in.ResponseSize)), 72 | }, nil 73 | } 74 | 75 | // UnconstrainedStreamingHeader indicates to the StreamingCall handler that its 76 | // behavior should be unconstrained (constant send/receive in parallel) instead 77 | // of ping-pong. 78 | const UnconstrainedStreamingHeader = "unconstrained-streaming" 79 | 80 | func (s *testServer) StreamingCall(stream testpb.BenchmarkService_StreamingCallServer) error { 81 | if md, ok := metadata.FromIncomingContext(stream.Context()); ok && len(md[UnconstrainedStreamingHeader]) != 0 { 82 | return s.UnconstrainedStreamingCall(stream) 83 | } 84 | response := &testpb.SimpleResponse{ 85 | Payload: new(testpb.Payload), 86 | } 87 | in := new(testpb.SimpleRequest) 88 | for { 89 | // use ServerStream directly to reuse the same testpb.SimpleRequest object 90 | err := stream.(grpc.ServerStream).RecvMsg(in) 91 | if err == io.EOF { 92 | // read done. 93 | return nil 94 | } 95 | if err != nil { 96 | return err 97 | } 98 | setPayload(response.Payload, in.ResponseType, int(in.ResponseSize)) 99 | if err := stream.Send(response); err != nil { 100 | return err 101 | } 102 | } 103 | } 104 | 105 | func (s *testServer) UnconstrainedStreamingCall(stream testpb.BenchmarkService_StreamingCallServer) error { 106 | in := new(testpb.SimpleRequest) 107 | // Receive a message to learn response type and size. 108 | err := stream.RecvMsg(in) 109 | if err == io.EOF { 110 | // read done. 111 | return nil 112 | } 113 | if err != nil { 114 | return err 115 | } 116 | 117 | response := &testpb.SimpleResponse{ 118 | Payload: new(testpb.Payload), 119 | } 120 | setPayload(response.Payload, in.ResponseType, int(in.ResponseSize)) 121 | 122 | go func() { 123 | for { 124 | // Using RecvMsg rather than Recv to prevent reallocation of SimpleRequest. 125 | err := stream.RecvMsg(in) 126 | switch status.Code(err) { 127 | case codes.Canceled: 128 | return 129 | case codes.OK: 130 | default: 131 | log.Fatalf("server recv error: %v", err) 132 | } 133 | } 134 | }() 135 | 136 | go func() { 137 | for { 138 | err := stream.Send(response) 139 | switch status.Code(err) { 140 | case codes.Unavailable, codes.Canceled: 141 | return 142 | case codes.OK: 143 | default: 144 | log.Fatalf("server send error: %v", err) 145 | } 146 | } 147 | }() 148 | 149 | <-stream.Context().Done() 150 | return stream.Context().Err() 151 | } 152 | 153 | // byteBufServer is a gRPC server that sends and receives byte buffer. 154 | // The purpose is to benchmark the gRPC performance without protobuf serialization/deserialization overhead. 155 | type byteBufServer struct { 156 | testpb.UnimplementedBenchmarkServiceServer 157 | respSize int32 158 | } 159 | 160 | // UnaryCall is an empty function and is not used for benchmark. 161 | // If bytebuf UnaryCall benchmark is needed later, the function body needs to be updated. 162 | func (s *byteBufServer) UnaryCall(ctx context.Context, in *testpb.SimpleRequest) (*testpb.SimpleResponse, error) { 163 | return &testpb.SimpleResponse{}, nil 164 | } 165 | 166 | func (s *byteBufServer) StreamingCall(stream testpb.BenchmarkService_StreamingCallServer) error { 167 | for { 168 | var in []byte 169 | err := stream.(grpc.ServerStream).RecvMsg(&in) 170 | if err == io.EOF { 171 | return nil 172 | } 173 | if err != nil { 174 | return err 175 | } 176 | out := make([]byte, s.respSize) 177 | if err := stream.(grpc.ServerStream).SendMsg(&out); err != nil { 178 | return err 179 | } 180 | } 181 | } 182 | 183 | // ServerInfo contains the information to create a gRPC benchmark server. 184 | type ServerInfo struct { 185 | // Type is the type of the server. 186 | // It should be "protobuf" or "bytebuf". 187 | Type string 188 | 189 | // Metadata is an optional configuration. 190 | // For "protobuf", it's ignored. 191 | // For "bytebuf", it should be an int representing response size. 192 | Metadata interface{} 193 | 194 | // Listener is the network listener for the server to use 195 | Listener net.Listener 196 | } 197 | 198 | // StartServer starts a gRPC server serving a benchmark service according to info. 199 | // It returns a function to stop the server. 200 | func StartServer(info ServerInfo, _ ...grpc.ServerOption) func() { 201 | m, err := larking.NewMux() 202 | if err != nil { 203 | logger.Fatalf("failed to StartServer, NewMux failed: %v", err) 204 | } 205 | 206 | //s := grpc.NewServer(opts...) 207 | switch info.Type { 208 | case "protobuf": 209 | testpb.RegisterBenchmarkServiceServer(m, &testServer{}) 210 | case "bytebuf": 211 | respSize, ok := info.Metadata.(int32) 212 | if !ok { 213 | logger.Fatalf("failed to StartServer, invalid metadata: %v, for Type: %v", info.Metadata, info.Type) 214 | } 215 | testpb.RegisterBenchmarkServiceServer(m, &byteBufServer{respSize: respSize}) 216 | default: 217 | logger.Fatalf("failed to StartServer, unknown Type: %v", info.Type) 218 | } 219 | 220 | s, err := larking.NewServer(m) 221 | if err != nil { 222 | logger.Fatalf("failed to StartServer, NewServer failed: %v", err) 223 | } 224 | 225 | go s.Serve(info.Listener) 226 | return func() { 227 | s.Close() 228 | } 229 | } 230 | 231 | // DoUnaryCall performs an unary RPC with given stub and request and response sizes. 232 | func DoUnaryCall(tc testpb.BenchmarkServiceClient, reqSize, respSize int) error { 233 | pl := NewPayload(testpb.PayloadType_COMPRESSABLE, reqSize) 234 | req := &testpb.SimpleRequest{ 235 | ResponseType: pl.Type, 236 | ResponseSize: int32(respSize), 237 | Payload: pl, 238 | } 239 | if _, err := tc.UnaryCall(context.Background(), req); err != nil { 240 | return fmt.Errorf("/BenchmarkService/UnaryCall(_, _) = _, %v, want _, ", err) 241 | } 242 | return nil 243 | } 244 | 245 | // DoStreamingRoundTrip performs a round trip for a single streaming rpc. 246 | func DoStreamingRoundTrip(stream testpb.BenchmarkService_StreamingCallClient, reqSize, respSize int) error { 247 | pl := NewPayload(testpb.PayloadType_COMPRESSABLE, reqSize) 248 | req := &testpb.SimpleRequest{ 249 | ResponseType: pl.Type, 250 | ResponseSize: int32(respSize), 251 | Payload: pl, 252 | } 253 | if err := stream.Send(req); err != nil { 254 | return fmt.Errorf("/BenchmarkService/StreamingCall.Send(_) = %v, want ", err) 255 | } 256 | if _, err := stream.Recv(); err != nil { 257 | // EOF is a valid error here. 258 | if err == io.EOF { 259 | return nil 260 | } 261 | return fmt.Errorf("/BenchmarkService/StreamingCall.Recv(_) = %v, want ", err) 262 | } 263 | return nil 264 | } 265 | 266 | // DoByteBufStreamingRoundTrip performs a round trip for a single streaming rpc, using a custom codec for byte buffer. 267 | func DoByteBufStreamingRoundTrip(stream testpb.BenchmarkService_StreamingCallClient, reqSize, respSize int) error { 268 | out := make([]byte, reqSize) 269 | if err := stream.(grpc.ClientStream).SendMsg(&out); err != nil { 270 | return fmt.Errorf("/BenchmarkService/StreamingCall.(ClientStream).SendMsg(_) = %v, want ", err) 271 | } 272 | var in []byte 273 | if err := stream.(grpc.ClientStream).RecvMsg(&in); err != nil { 274 | // EOF is a valid error here. 275 | if err == io.EOF { 276 | return nil 277 | } 278 | return fmt.Errorf("/BenchmarkService/StreamingCall.(ClientStream).RecvMsg(_) = %v, want ", err) 279 | } 280 | return nil 281 | } 282 | 283 | // NewClientConn creates a gRPC client connection to addr. 284 | func NewClientConn(addr string, opts ...grpc.DialOption) *grpc.ClientConn { 285 | return NewClientConnWithContext(context.Background(), addr, opts...) 286 | } 287 | 288 | // NewClientConnWithContext creates a gRPC client connection to addr using ctx. 289 | func NewClientConnWithContext(ctx context.Context, addr string, opts ...grpc.DialOption) *grpc.ClientConn { 290 | conn, err := grpc.DialContext(ctx, addr, opts...) 291 | if err != nil { 292 | logger.Fatalf("NewClientConn(%q) failed to create a ClientConn: %v", addr, err) 293 | } 294 | return conn 295 | } 296 | -------------------------------------------------------------------------------- /benchmarks/svc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "google.golang.org/grpc" 8 | "google.golang.org/grpc/codes" 9 | "google.golang.org/protobuf/types/known/durationpb" 10 | "google.golang.org/protobuf/types/known/emptypb" 11 | "google.golang.org/protobuf/types/known/timestamppb" 12 | "google.golang.org/protobuf/types/known/wrapperspb" 13 | "larking.io/benchmarks/api/librarypb" 14 | ) 15 | 16 | type testService struct { 17 | librarypb.UnimplementedLibraryServiceServer 18 | } 19 | 20 | func (testService) GetBook(ctx context.Context, req *librarypb.GetBookRequest) (*librarypb.Book, error) { 21 | if req.Name != "shelves/1/books/1" { 22 | return nil, grpc.Errorf(codes.NotFound, "not found") 23 | } 24 | return &librarypb.Book{ 25 | Name: "shelves/1/books/1", 26 | Title: "The Great Gatsby", 27 | Author: "F. Scott Fitzgerald", 28 | PageCount: 180, 29 | PublishTime: timestamppb.New(time.Now()), 30 | Duration: durationpb.New(1 * time.Hour), 31 | Price: wrapperspb.Double(9.99), 32 | }, nil 33 | } 34 | 35 | func (testService) ListBooks(ctx context.Context, req *librarypb.ListBooksRequest) (*librarypb.ListBooksResponse, error) { 36 | if req.Parent != "shelves/1" { 37 | return nil, grpc.Errorf(codes.NotFound, "not found") 38 | } 39 | return &librarypb.ListBooksResponse{ 40 | Books: []*librarypb.Book{ 41 | { 42 | Name: "shelves/1/books/1", 43 | Title: "The Great Gatsby", 44 | }, 45 | { 46 | Name: "shelves/1/books/2", 47 | Title: "The Catcher in the Rye", 48 | }, 49 | { 50 | Name: "shelves/1/books/3", 51 | Title: "The Grapes of Wrath", 52 | }, 53 | }, 54 | }, nil 55 | } 56 | 57 | func (testService) CreateBook(ctx context.Context, req *librarypb.CreateBookRequest) (*librarypb.Book, error) { 58 | return req.Book, nil 59 | } 60 | func (testService) UpdateBook(ctx context.Context, req *librarypb.UpdateBookRequest) (*librarypb.Book, error) { 61 | if req.Book.GetName() != "shelves/1/books/1" { 62 | return nil, grpc.Errorf(codes.NotFound, "not found") 63 | } 64 | if req.UpdateMask.Paths[0] != "book.title" { 65 | return nil, grpc.Errorf(codes.InvalidArgument, "invalid field mask") 66 | } 67 | return req.Book, nil 68 | } 69 | func (testService) DeleteBook(ctx context.Context, req *librarypb.DeleteBookRequest) (*emptypb.Empty, error) { 70 | if req.Name != "shelves/1/books/1" { 71 | return nil, grpc.Errorf(codes.NotFound, "not found") 72 | } 73 | return &emptypb.Empty{}, nil 74 | } 75 | -------------------------------------------------------------------------------- /benchmarks/testdata/envoy.yaml: -------------------------------------------------------------------------------- 1 | admin: 2 | address: 3 | socket_address: 4 | address: 127.0.0.1 5 | port_value: 9901 6 | 7 | static_resources: 8 | listeners: 9 | - name: listener_0 10 | address: 11 | socket_address: 12 | address: 0.0.0.0 13 | port_value: 10000 14 | filter_chains: 15 | - filters: 16 | - name: envoy.filters.network.http_connection_manager 17 | typed_config: 18 | "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager 19 | stat_prefix: grpc_json 20 | codec_type: AUTO 21 | route_config: 22 | name: local_route 23 | virtual_hosts: 24 | - name: local_service 25 | domains: ["*"] 26 | routes: 27 | # NOTE: by default, matching happens based on the gRPC route, and not on the incoming request path. 28 | # Reference: https://envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter#route-configs-for-transcoded-requests 29 | - match: {prefix: "/larking.benchmarks.librarypb.LibraryService"} 30 | route: {cluster: grpc, timeout: 60s} 31 | http_filters: 32 | - name: envoy.filters.http.grpc_json_transcoder 33 | typed_config: 34 | "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder 35 | proto_descriptor: "api/proto.pb" 36 | services: ["larking.benchmarks.librarypb.LibraryService"] 37 | print_options: 38 | add_whitespace: true 39 | always_print_primitive_fields: true 40 | always_print_enums_as_ints: false 41 | preserve_proto_field_names: false 42 | - name: envoy.filters.http.router 43 | typed_config: 44 | "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router 45 | 46 | clusters: 47 | - name: grpc 48 | type: LOGICAL_DNS 49 | lb_policy: ROUND_ROBIN 50 | dns_lookup_family: V4_ONLY 51 | typed_extension_protocol_options: 52 | envoy.extensions.upstreams.http.v3.HttpProtocolOptions: 53 | "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions 54 | explicit_http_config: 55 | http2_protocol_options: {} 56 | load_assignment: 57 | cluster_name: grpc 58 | endpoints: 59 | - lb_endpoints: 60 | - endpoint: 61 | address: 62 | socket_address: 63 | address: localhost 64 | port_value: 5050 65 | -------------------------------------------------------------------------------- /benchmarks/twirp_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "net" 7 | "net/http" 8 | "testing" 9 | 10 | "github.com/twitchtv/twirp" 11 | "golang.org/x/sync/errgroup" 12 | "larking.io/benchmarks/api/librarypb" 13 | "larking.io/larking" 14 | ) 15 | 16 | func TestTwirp(t *testing.T) { 17 | ctx := context.Background() 18 | svc := &testService{} 19 | 20 | mux, err := larking.NewMux() 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | librarypb.RegisterLibraryServiceServer(mux, svc) 25 | 26 | ts, err := larking.NewServer(mux, 27 | larking.MuxHandleOption("/", "/twirp"), 28 | ) 29 | if err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | lis, err := net.Listen("tcp", "localhost:0") 34 | if err != nil { 35 | t.Fatalf("failed to listen: %v", err) 36 | } 37 | defer lis.Close() 38 | 39 | var g errgroup.Group 40 | defer func() { 41 | if err := g.Wait(); err != nil { 42 | t.Fatal(err) 43 | } 44 | t.Log("all server shutdown") 45 | }() 46 | g.Go(func() (err error) { 47 | if err := ts.Serve(lis); err != nil && err != http.ErrServerClosed { 48 | return err 49 | } 50 | return nil 51 | }) 52 | defer func() { 53 | t.Log("shutdown server") 54 | if err := ts.Shutdown(ctx); err != nil { 55 | t.Fatal(err) 56 | } 57 | }() 58 | 59 | { // proto 60 | ccproto := librarypb.NewLibraryServiceProtobufClient("http://"+lis.Addr().String(), &http.Client{}) 61 | 62 | book, err := ccproto.GetBook(ctx, &librarypb.GetBookRequest{ 63 | Name: "shelves/1/books/1", 64 | }) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | t.Log(book, err) 69 | 70 | // 404 71 | if _, err = ccproto.GetBook(ctx, &librarypb.GetBookRequest{ 72 | Name: "shelves/1/books/404", 73 | }); err == nil { 74 | t.Fatal("should be 404") 75 | } else { 76 | var twerr twirp.Error 77 | if errors.As(err, &twerr) { 78 | t.Log(twerr.Code(), twerr.Msg()) 79 | if twerr.Code() != twirp.NotFound { 80 | t.Errorf("should be %s, but got %s", twirp.NotFound, twerr.Code()) 81 | } 82 | } else { 83 | t.Error(err) 84 | } 85 | } 86 | 87 | if _, err := ccproto.CreateBook(ctx, &librarypb.CreateBookRequest{ 88 | Parent: "shelves/1", 89 | Book: &librarypb.Book{ 90 | Name: "shelves/1/books/2", 91 | Title: "book2", 92 | }, 93 | }); err != nil { 94 | t.Error(err) 95 | } 96 | 97 | if _, err := ccproto.DeleteBook(ctx, &librarypb.DeleteBookRequest{ 98 | Name: "shelves/1/books/1", 99 | }); err != nil { 100 | t.Error(err) 101 | } 102 | } 103 | 104 | { // json 105 | ccjson := librarypb.NewLibraryServiceJSONClient("http://"+lis.Addr().String(), &http.Client{}) 106 | book, err := ccjson.GetBook(ctx, &librarypb.GetBookRequest{ 107 | Name: "shelves/1/books/1", 108 | }) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | t.Log(book, err) 113 | 114 | // 404 115 | if _, err = ccjson.GetBook(ctx, &librarypb.GetBookRequest{ 116 | Name: "shelves/1/books/404", 117 | }); err == nil { 118 | t.Fatal("should be 404") 119 | } else { 120 | var twerr twirp.Error 121 | if errors.As(err, &twerr) { 122 | t.Log(twerr.Code(), twerr.Msg()) 123 | if twerr.Code() != twirp.NotFound { 124 | t.Errorf("should be %s, but got %s", twirp.NotFound, twerr.Code()) 125 | } 126 | } else { 127 | t.Error(err) 128 | } 129 | } 130 | 131 | if _, err := ccjson.CreateBook(ctx, &librarypb.CreateBookRequest{ 132 | Parent: "shelves/1", 133 | Book: &librarypb.Book{ 134 | Name: "shelves/1/books/2", 135 | Title: "book2", 136 | }, 137 | }); err != nil { 138 | t.Error(err) 139 | } 140 | 141 | if _, err := ccjson.DeleteBook(ctx, &librarypb.DeleteBookRequest{ 142 | Name: "shelves/1/books/1", 143 | }); err != nil { 144 | t.Error(err) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/LARK_PROTOCOL.md: -------------------------------------------------------------------------------- 1 | # lark (alpha) 2 | 3 | ## Overview 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: intro 3 | title: Getting Started 4 | sidebar_label: Getting Started 5 | --- 6 | 7 | TODO 8 | 9 | --- 10 | 11 | ## Setup 12 | 13 | ## Debugging 14 | 15 | Checkout [protobuf](https://github.com/golang/protobuf) at the latest v2 relase. 16 | Go install each protoc generation bin. 17 | 18 | Regenerate protoc buffers: 19 | 20 | ``` 21 | protoc --go_out=paths=source_relative:. --go-grpc_out=paths=source_relative:. testpb/test.proto 22 | ``` 23 | 24 | ### Protoc 25 | 26 | Must have googleapis protos avaliable. 27 | Just link API to `/usr/local/include/google` so protoc can find it. 28 | ``` 29 | ln -s ~/src/github.com/googleapis/googleapis/google/api/ /usr/local/include/google/ 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net" 6 | 7 | "google.golang.org/genproto/googleapis/api/serviceconfig" 8 | healthpb "google.golang.org/grpc/health/grpc_health_v1" 9 | "larking.io/health" 10 | "larking.io/larking" 11 | ) 12 | 13 | func main() { 14 | // Create a health service. The health service is used to check the status 15 | // of services running within the server. 16 | healthSvc := health.NewServer() 17 | healthSvc.SetServingStatus("example.up.Service", healthpb.HealthCheckResponse_SERVING) 18 | healthSvc.SetServingStatus("example.down.Service", healthpb.HealthCheckResponse_NOT_SERVING) 19 | 20 | serviceConfig := &serviceconfig.Service{} 21 | // AddHealthz adds a /v1/healthz endpoint to the service binding to the 22 | // grpc.health.v1.Health service: 23 | // - get /v1/healthz -> grpc.health.v1.Health.Check 24 | // - websocket /v1/healthz -> grpc.health.v1.Health.Watch 25 | health.AddHealthz(serviceConfig) 26 | 27 | // Mux impements http.Handler and serves both gRPC and HTTP connections. 28 | mux, err := larking.NewMux( 29 | larking.ServiceConfigOption(serviceConfig), 30 | ) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | // RegisterHealthServer registers a HealthServer to the mux. 35 | healthpb.RegisterHealthServer(mux, healthSvc) 36 | 37 | // Server creates a *http.Server. 38 | svr, err := larking.NewServer(mux) 39 | if err != nil { 40 | log.Fatal(err) 41 | } 42 | 43 | // Listen on TCP port 8080 on all interfaces. 44 | lis, err := net.Listen("tcp", "localhost:8080") 45 | if err != nil { 46 | log.Fatalf("failed to listen: %v", err) 47 | } 48 | defer lis.Close() 49 | 50 | // Serve starts the server and blocks until the server stops. 51 | // http://localhost:8080/v1/healthz 52 | log.Println("gRPC & HTTP server listening on", lis.Addr()) 53 | if err := svr.Serve(lis); err != nil { 54 | log.Fatalf("failed to serve: %v", err) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /examples/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@bazel_gazelle//:def.bzl", "gazelle") 2 | 3 | # gazelle:prefix larking.io/examples 4 | gazelle(name = "gazelle") 5 | -------------------------------------------------------------------------------- /examples/WORKSPACE: -------------------------------------------------------------------------------- 1 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 2 | 3 | http_archive( 4 | name = "com_google_protobuf", 5 | sha256 = "782160a6eae4bddfa4061ff5f7dcf04c9ed1494a0f0c6408b4af6b4a31ab9876", 6 | strip_prefix = "protobuf-22.0", 7 | urls = [ 8 | "https://mirror.bazel.build/github.com/protocolbuffers/protobuf/archive/v22.0.tar.gz", 9 | "https://github.com/protocolbuffers/protobuf/archive/v22.0.tar.gz", 10 | ], 11 | ) 12 | 13 | load("@com_google_protobuf//:protobuf_deps.bzl", "protobuf_deps") 14 | 15 | protobuf_deps() 16 | 17 | # Download rules_go ruleset. 18 | # Bazel makes a https call and downloads the zip file, and then 19 | # checks the sha. 20 | http_archive( 21 | name = "io_bazel_rules_go", 22 | sha256 = "6b65cb7917b4d1709f9410ffe00ecf3e160edf674b78c54a894471320862184f", 23 | urls = [ 24 | "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.39.0/rules_go-v0.39.0.zip", 25 | "https://github.com/bazelbuild/rules_go/releases/download/v0.39.0/rules_go-v0.39.0.zip", 26 | ], 27 | ) 28 | 29 | # Download the bazel_gazelle ruleset. 30 | http_archive( 31 | name = "bazel_gazelle", 32 | sha256 = "efbbba6ac1a4fd342d5122cbdfdb82aeb2cf2862e35022c752eaddffada7c3f3", 33 | urls = [ 34 | "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.27.0/bazel-gazelle-v0.27.0.tar.gz", 35 | "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.27.0/bazel-gazelle-v0.27.0.tar.gz", 36 | ], 37 | ) 38 | 39 | # Load rules_go ruleset and expose the toolchain and dep rules. 40 | load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies") 41 | 42 | # the line below instructs gazelle to save the go dependency definitions 43 | # in the deps.bzl file. Located under '//'. 44 | load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies", "go_repository") 45 | load("//:deps.bzl", "go_dependencies") 46 | 47 | # gazelle:repository_macro deps.bzl%go_dependencies 48 | go_dependencies() 49 | 50 | go_rules_dependencies() 51 | 52 | go_register_toolchains(version = "1.20.2") 53 | 54 | gazelle_dependencies() 55 | 56 | rules_python_version = "740825b7f74930c62f44af95c9a4c1bd428d2c53" # Latest @ 2021-06-23 57 | 58 | http_archive( 59 | name = "rules_python", 60 | # Bazel will print the proper value to add here during the first build. 61 | # sha256 = "FIXME", 62 | strip_prefix = "rules_python-{}".format(rules_python_version), 63 | url = "https://github.com/bazelbuild/rules_python/archive/{}.zip".format(rules_python_version), 64 | ) 65 | -------------------------------------------------------------------------------- /examples/api/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_library") 2 | 3 | #load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_proto_library") 4 | load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") 5 | load("@rules_proto//proto:defs.bzl", "proto_library") 6 | load("@com_github_grpc_grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library") 7 | load("@io_grpc_grpc_java//:java_grpc_library.bzl", "java_grpc_library") 8 | 9 | proto_library( 10 | name = "helloworld_proto", 11 | srcs = ["helloworld.proto"], 12 | visibility = ["//visibility:public"], 13 | deps = ["@go_googleapis//google/api:annotations_proto"], 14 | ) 15 | 16 | cc_proto_library( 17 | name = "helloworld_cc_proto", 18 | visibility = ["//visibility:public"], 19 | deps = [":helloworld_proto"], 20 | ) 21 | 22 | cc_grpc_library( 23 | name = "helloworld_cc_grpc", 24 | srcs = [":helloworld_proto"], 25 | grpc_only = True, 26 | visibility = ["//visibility:public"], 27 | deps = [":helloworld_cc_proto"], 28 | ) 29 | 30 | java_proto_library( 31 | name = "helloworld_java_proto", 32 | visibility = ["//visibility:public"], 33 | deps = [":helloworld_proto"], 34 | ) 35 | 36 | java_grpc_library( 37 | name = "helloworld_java_grpc", 38 | srcs = [":helloworld_proto"], 39 | visibility = ["//visibility:public"], 40 | deps = [":helloworld_java_proto"], 41 | ) 42 | 43 | proto_library( 44 | name = "hellostream_proto", 45 | srcs = ["hellostream.proto"], 46 | visibility = ["//visibility:public"], 47 | deps = ["@go_googleapis//google/api:annotations_proto"], 48 | ) 49 | 50 | cc_proto_library( 51 | name = "hellostream_cc_proto", 52 | visibility = ["//visibility:public"], 53 | deps = [":hellostream_proto"], 54 | ) 55 | 56 | cc_grpc_library( 57 | name = "hellostream_cc_grpc", 58 | srcs = [":hellostream_proto"], 59 | grpc_only = True, 60 | visibility = ["//visibility:public"], 61 | deps = [":hellostream_cc_proto"], 62 | ) 63 | 64 | java_proto_library( 65 | name = "hello_streaming_java_proto", 66 | visibility = ["//visibility:public"], 67 | deps = [":hello_streaming_proto"], 68 | ) 69 | 70 | java_grpc_library( 71 | name = "hello_streaming_java_grpc", 72 | srcs = [":hello_streaming_proto"], 73 | visibility = ["//visibility:public"], 74 | deps = [":hello_streaming_java_proto"], 75 | ) 76 | 77 | go_proto_library( 78 | name = "annotations_go_proto", 79 | importpath = "", 80 | protos = ["@googleapis//google/api:annotations_proto"], 81 | ) 82 | 83 | go_proto_library( 84 | name = "helloworld_go_proto", 85 | compilers = ["@io_bazel_rules_go//proto:go_grpc"], 86 | importpath = "github.com/emcfarlane/larking/examples/proto/helloworld", 87 | protos = [ 88 | #":hellostream_proto", 89 | ":helloworld_proto", 90 | ], 91 | visibility = ["//visibility:public"], 92 | deps = [ 93 | "@go_googleapis//google/api:annotations_go_proto", 94 | "@go_googleapis//google/api:httpbody_go_proto", 95 | "@org_golang_google_grpc//:go_default_library", 96 | "@org_golang_google_grpc//codes:go_default_library", 97 | #"@org_golang_google_grpc//status:go_default_library", 98 | "@org_golang_google_protobuf//reflect/protoreflect:go_default_library", 99 | "@org_golang_google_protobuf//runtime/protoimpl:go_default_library", 100 | ], 101 | ) 102 | 103 | # TODO: https://github.com/bazelbuild/rules_go/pull/2740 104 | #go_library( 105 | # name = "go_default_library", 106 | # srcs = [ 107 | # "hellostream.pb.go", 108 | # "hellostream_grpc.pb.go", 109 | # "helloworld.pb.go", 110 | # "helloworld_grpc.pb.go", 111 | # ], 112 | # importpath = "github.com/emcfarlane/larking/examples/proto", 113 | # visibility = ["//visibility:public"], 114 | # deps = [ 115 | # "@com_github_golang_protobuf//proto:go_default_library", 116 | # "@go_googleapis//google/api:annotations_go_proto", 117 | # "@org_golang_google_grpc//:go_default_library", 118 | # "@org_golang_google_grpc//codes:go_default_library", 119 | # "@org_golang_google_grpc//status:go_default_library", 120 | # "@org_golang_google_protobuf//reflect/protoreflect:go_default_library", 121 | # "@org_golang_google_protobuf//runtime/protoimpl:go_default_library", 122 | # ], 123 | #) 124 | 125 | go_proto_library( 126 | name = "proto_go_proto", 127 | compilers = ["@io_bazel_rules_go//proto:go_grpc"], 128 | importpath = "larking.io/example/proto/hellostream", 129 | proto = ":hellostream_proto", 130 | visibility = ["//visibility:public"], 131 | deps = ["@go_googleapis//google/api:annotations_go_proto"], 132 | ) 133 | 134 | go_library( 135 | name = "helloworld", 136 | embed = [":proto_go_proto"], 137 | importpath = "larking.io/example/proto/helloworld", 138 | visibility = ["//visibility:public"], 139 | ) 140 | 141 | go_library( 142 | name = "hellostream", 143 | embed = [":proto_go_proto"], 144 | importpath = "larking.io/example/proto/hellostream", 145 | visibility = ["//visibility:public"], 146 | ) 147 | -------------------------------------------------------------------------------- /examples/api/hellostream.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_multiple_files = true; 4 | option java_package = "com.examples.hellostream"; 5 | option java_outer_classname = "HelloStreamProto"; 6 | option objc_class_prefix = "HLWS"; 7 | option go_package = "larking.io/examples/proto/hellostream;proto"; 8 | 9 | package hellostream; 10 | 11 | import "google/api/annotations.proto"; 12 | 13 | // The greeting service definition. 14 | service StreamingGreeter { 15 | // Streams a many greetings 16 | rpc SayHelloStreaming(stream HelloRequest) returns (stream HelloReply) { 17 | option (google.api.http) = { 18 | post : "/hello" 19 | }; 20 | } 21 | } 22 | 23 | // The request message containing the user's name. 24 | message HelloRequest { string name = 1; } 25 | 26 | // The response message containing the greetings 27 | message HelloReply { string message = 1; } 28 | -------------------------------------------------------------------------------- /examples/api/helloworld.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_multiple_files = true; 4 | option java_package = "com.examples.helloworld"; 5 | option java_outer_classname = "HelloWorldProto"; 6 | option objc_class_prefix = "HLW"; 7 | option go_package = "larking.io/examples/api/helloworld;proto"; 8 | 9 | package helloworld; 10 | 11 | import "google/api/annotations.proto"; 12 | 13 | // The greeting service definition. 14 | service Greeter { 15 | // Sends a greeting 16 | rpc SayHello(HelloRequest) returns (HelloReply) { 17 | option (google.api.http) = { 18 | get : "/hello/{name}" 19 | }; 20 | } 21 | } 22 | 23 | // The request message containing the user's name. 24 | message HelloRequest { string name = 1; } 25 | 26 | // The response message containing the greetings 27 | message HelloReply { string message = 1; } 28 | -------------------------------------------------------------------------------- /examples/api/helloworld/helloworld.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.30.0 4 | // protoc v3.21.12 5 | // source: api/helloworld.proto 6 | 7 | package proto 8 | 9 | import ( 10 | _ "google.golang.org/genproto/googleapis/api/annotations" 11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | // The request message containing the user's name. 25 | type HelloRequest struct { 26 | state protoimpl.MessageState 27 | sizeCache protoimpl.SizeCache 28 | unknownFields protoimpl.UnknownFields 29 | 30 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 31 | } 32 | 33 | func (x *HelloRequest) Reset() { 34 | *x = HelloRequest{} 35 | if protoimpl.UnsafeEnabled { 36 | mi := &file_api_helloworld_proto_msgTypes[0] 37 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 38 | ms.StoreMessageInfo(mi) 39 | } 40 | } 41 | 42 | func (x *HelloRequest) String() string { 43 | return protoimpl.X.MessageStringOf(x) 44 | } 45 | 46 | func (*HelloRequest) ProtoMessage() {} 47 | 48 | func (x *HelloRequest) ProtoReflect() protoreflect.Message { 49 | mi := &file_api_helloworld_proto_msgTypes[0] 50 | if protoimpl.UnsafeEnabled && x != nil { 51 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 52 | if ms.LoadMessageInfo() == nil { 53 | ms.StoreMessageInfo(mi) 54 | } 55 | return ms 56 | } 57 | return mi.MessageOf(x) 58 | } 59 | 60 | // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. 61 | func (*HelloRequest) Descriptor() ([]byte, []int) { 62 | return file_api_helloworld_proto_rawDescGZIP(), []int{0} 63 | } 64 | 65 | func (x *HelloRequest) GetName() string { 66 | if x != nil { 67 | return x.Name 68 | } 69 | return "" 70 | } 71 | 72 | // The response message containing the greetings 73 | type HelloReply struct { 74 | state protoimpl.MessageState 75 | sizeCache protoimpl.SizeCache 76 | unknownFields protoimpl.UnknownFields 77 | 78 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 79 | } 80 | 81 | func (x *HelloReply) Reset() { 82 | *x = HelloReply{} 83 | if protoimpl.UnsafeEnabled { 84 | mi := &file_api_helloworld_proto_msgTypes[1] 85 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 86 | ms.StoreMessageInfo(mi) 87 | } 88 | } 89 | 90 | func (x *HelloReply) String() string { 91 | return protoimpl.X.MessageStringOf(x) 92 | } 93 | 94 | func (*HelloReply) ProtoMessage() {} 95 | 96 | func (x *HelloReply) ProtoReflect() protoreflect.Message { 97 | mi := &file_api_helloworld_proto_msgTypes[1] 98 | if protoimpl.UnsafeEnabled && x != nil { 99 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 100 | if ms.LoadMessageInfo() == nil { 101 | ms.StoreMessageInfo(mi) 102 | } 103 | return ms 104 | } 105 | return mi.MessageOf(x) 106 | } 107 | 108 | // Deprecated: Use HelloReply.ProtoReflect.Descriptor instead. 109 | func (*HelloReply) Descriptor() ([]byte, []int) { 110 | return file_api_helloworld_proto_rawDescGZIP(), []int{1} 111 | } 112 | 113 | func (x *HelloReply) GetMessage() string { 114 | if x != nil { 115 | return x.Message 116 | } 117 | return "" 118 | } 119 | 120 | var File_api_helloworld_proto protoreflect.FileDescriptor 121 | 122 | var file_api_helloworld_proto_rawDesc = []byte{ 123 | 0x0a, 0x14, 0x61, 0x70, 0x69, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 124 | 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0a, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 125 | 0x6c, 0x64, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x61, 126 | 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 127 | 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 128 | 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 129 | 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 130 | 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 131 | 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x5e, 0x0a, 0x07, 132 | 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x12, 0x53, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 133 | 0x6c, 0x6c, 0x6f, 0x12, 0x18, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 134 | 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 135 | 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 136 | 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x15, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x0f, 0x12, 0x0d, 0x2f, 137 | 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2f, 0x7b, 0x6e, 0x61, 0x6d, 0x65, 0x7d, 0x42, 0x5c, 0x0a, 0x17, 138 | 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 0x68, 0x65, 0x6c, 139 | 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x42, 0x0f, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x57, 0x6f, 140 | 0x72, 0x6c, 0x64, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x28, 0x6c, 0x61, 0x72, 0x6b, 141 | 0x69, 0x6e, 0x67, 0x2e, 0x69, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2f, 142 | 0x61, 0x70, 0x69, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x77, 0x6f, 0x72, 0x6c, 0x64, 0x3b, 0x70, 143 | 0x72, 0x6f, 0x74, 0x6f, 0xa2, 0x02, 0x03, 0x48, 0x4c, 0x57, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 144 | 0x6f, 0x33, 145 | } 146 | 147 | var ( 148 | file_api_helloworld_proto_rawDescOnce sync.Once 149 | file_api_helloworld_proto_rawDescData = file_api_helloworld_proto_rawDesc 150 | ) 151 | 152 | func file_api_helloworld_proto_rawDescGZIP() []byte { 153 | file_api_helloworld_proto_rawDescOnce.Do(func() { 154 | file_api_helloworld_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_helloworld_proto_rawDescData) 155 | }) 156 | return file_api_helloworld_proto_rawDescData 157 | } 158 | 159 | var file_api_helloworld_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 160 | var file_api_helloworld_proto_goTypes = []interface{}{ 161 | (*HelloRequest)(nil), // 0: helloworld.HelloRequest 162 | (*HelloReply)(nil), // 1: helloworld.HelloReply 163 | } 164 | var file_api_helloworld_proto_depIdxs = []int32{ 165 | 0, // 0: helloworld.Greeter.SayHello:input_type -> helloworld.HelloRequest 166 | 1, // 1: helloworld.Greeter.SayHello:output_type -> helloworld.HelloReply 167 | 1, // [1:2] is the sub-list for method output_type 168 | 0, // [0:1] is the sub-list for method input_type 169 | 0, // [0:0] is the sub-list for extension type_name 170 | 0, // [0:0] is the sub-list for extension extendee 171 | 0, // [0:0] is the sub-list for field type_name 172 | } 173 | 174 | func init() { file_api_helloworld_proto_init() } 175 | func file_api_helloworld_proto_init() { 176 | if File_api_helloworld_proto != nil { 177 | return 178 | } 179 | if !protoimpl.UnsafeEnabled { 180 | file_api_helloworld_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 181 | switch v := v.(*HelloRequest); i { 182 | case 0: 183 | return &v.state 184 | case 1: 185 | return &v.sizeCache 186 | case 2: 187 | return &v.unknownFields 188 | default: 189 | return nil 190 | } 191 | } 192 | file_api_helloworld_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 193 | switch v := v.(*HelloReply); i { 194 | case 0: 195 | return &v.state 196 | case 1: 197 | return &v.sizeCache 198 | case 2: 199 | return &v.unknownFields 200 | default: 201 | return nil 202 | } 203 | } 204 | } 205 | type x struct{} 206 | out := protoimpl.TypeBuilder{ 207 | File: protoimpl.DescBuilder{ 208 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 209 | RawDescriptor: file_api_helloworld_proto_rawDesc, 210 | NumEnums: 0, 211 | NumMessages: 2, 212 | NumExtensions: 0, 213 | NumServices: 1, 214 | }, 215 | GoTypes: file_api_helloworld_proto_goTypes, 216 | DependencyIndexes: file_api_helloworld_proto_depIdxs, 217 | MessageInfos: file_api_helloworld_proto_msgTypes, 218 | }.Build() 219 | File_api_helloworld_proto = out.File 220 | file_api_helloworld_proto_rawDesc = nil 221 | file_api_helloworld_proto_goTypes = nil 222 | file_api_helloworld_proto_depIdxs = nil 223 | } 224 | -------------------------------------------------------------------------------- /examples/api/helloworld/helloworld_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc v3.21.12 5 | // source: api/helloworld.proto 6 | 7 | package proto 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | const ( 22 | Greeter_SayHello_FullMethodName = "/helloworld.Greeter/SayHello" 23 | ) 24 | 25 | // GreeterClient is the client API for Greeter service. 26 | // 27 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 28 | type GreeterClient interface { 29 | // Sends a greeting 30 | SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) 31 | } 32 | 33 | type greeterClient struct { 34 | cc grpc.ClientConnInterface 35 | } 36 | 37 | func NewGreeterClient(cc grpc.ClientConnInterface) GreeterClient { 38 | return &greeterClient{cc} 39 | } 40 | 41 | func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) { 42 | out := new(HelloReply) 43 | err := c.cc.Invoke(ctx, Greeter_SayHello_FullMethodName, in, out, opts...) 44 | if err != nil { 45 | return nil, err 46 | } 47 | return out, nil 48 | } 49 | 50 | // GreeterServer is the server API for Greeter service. 51 | // All implementations must embed UnimplementedGreeterServer 52 | // for forward compatibility 53 | type GreeterServer interface { 54 | // Sends a greeting 55 | SayHello(context.Context, *HelloRequest) (*HelloReply, error) 56 | mustEmbedUnimplementedGreeterServer() 57 | } 58 | 59 | // UnimplementedGreeterServer must be embedded to have forward compatible implementations. 60 | type UnimplementedGreeterServer struct { 61 | } 62 | 63 | func (UnimplementedGreeterServer) SayHello(context.Context, *HelloRequest) (*HelloReply, error) { 64 | return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") 65 | } 66 | func (UnimplementedGreeterServer) mustEmbedUnimplementedGreeterServer() {} 67 | 68 | // UnsafeGreeterServer may be embedded to opt out of forward compatibility for this service. 69 | // Use of this interface is not recommended, as added methods to GreeterServer will 70 | // result in compilation errors. 71 | type UnsafeGreeterServer interface { 72 | mustEmbedUnimplementedGreeterServer() 73 | } 74 | 75 | func RegisterGreeterServer(s grpc.ServiceRegistrar, srv GreeterServer) { 76 | s.RegisterService(&Greeter_ServiceDesc, srv) 77 | } 78 | 79 | func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 80 | in := new(HelloRequest) 81 | if err := dec(in); err != nil { 82 | return nil, err 83 | } 84 | if interceptor == nil { 85 | return srv.(GreeterServer).SayHello(ctx, in) 86 | } 87 | info := &grpc.UnaryServerInfo{ 88 | Server: srv, 89 | FullMethod: Greeter_SayHello_FullMethodName, 90 | } 91 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 92 | return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest)) 93 | } 94 | return interceptor(ctx, in, info, handler) 95 | } 96 | 97 | // Greeter_ServiceDesc is the grpc.ServiceDesc for Greeter service. 98 | // It's only intended for direct use with grpc.RegisterService, 99 | // and not to be introspected or modified (even as a copy) 100 | var Greeter_ServiceDesc = grpc.ServiceDesc{ 101 | ServiceName: "helloworld.Greeter", 102 | HandlerType: (*GreeterServer)(nil), 103 | Methods: []grpc.MethodDesc{ 104 | { 105 | MethodName: "SayHello", 106 | Handler: _Greeter_SayHello_Handler, 107 | }, 108 | }, 109 | Streams: []grpc.StreamDesc{}, 110 | Metadata: "api/helloworld.proto", 111 | } 112 | -------------------------------------------------------------------------------- /examples/cpp/helloworld/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@rules_cc//cc:defs.bzl", "cc_binary") 2 | 3 | cc_binary( 4 | name = "helloworld", 5 | srcs = ["hello_world.cc"], 6 | defines = ["BAZEL_BUILD"], 7 | deps = [ 8 | "//examples/api:helloworld_cc_grpc", 9 | "//examples/api:helloworld_cc_proto", 10 | "@com_github_grpc_grpc//:grpc++", 11 | "@com_github_grpc_grpc//:grpc++_reflection", 12 | ], 13 | ) 14 | -------------------------------------------------------------------------------- /examples/cpp/helloworld/hello_world.cc: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include 6 | #include 7 | #include 8 | 9 | #ifdef BAZEL_BUILD 10 | #include "examples/proto/helloworld.grpc.pb.h" 11 | #else 12 | #include "helloworld.grpc.pb.h" 13 | #endif 14 | 15 | using grpc::Server; 16 | using grpc::ServerBuilder; 17 | using grpc::ServerContext; 18 | using grpc::Status; 19 | using helloworld::Greeter; 20 | using helloworld::HelloReply; 21 | using helloworld::HelloRequest; 22 | 23 | // Logic and data behind the server's behavior. 24 | class GreeterServiceImpl final : public Greeter::Service { 25 | Status SayHello(ServerContext *context, const HelloRequest *request, 26 | HelloReply *reply) override { 27 | std::string prefix("Hello "); 28 | reply->set_message(prefix + request->name()); 29 | return Status::OK; 30 | } 31 | }; 32 | 33 | void RunServer() { 34 | std::string server_address("0.0.0.0:50051"); 35 | GreeterServiceImpl service; 36 | 37 | grpc::EnableDefaultHealthCheckService(true); 38 | grpc::reflection::InitProtoReflectionServerBuilderPlugin(); 39 | ServerBuilder builder; 40 | // Listen on the given address without any authentication mechanism. 41 | builder.AddListeningPort(server_address, grpc::InsecureServerCredentials()); 42 | // Register "service" as the instance through which we'll communicate with 43 | // clients. In this case it corresponds to an *synchronous* service. 44 | builder.RegisterService(&service); 45 | // Finally assemble the server. 46 | std::unique_ptr server(builder.BuildAndStart()); 47 | std::cout << "Server listening on " << server_address << std::endl; 48 | 49 | // Wait for the server to shutdown. Note that some other thread must be 50 | // responsible for shutting down the server for this call to ever return. 51 | server->Wait(); 52 | } 53 | 54 | int main(int argc, char **argv) { 55 | RunServer(); 56 | 57 | return 0; 58 | } 59 | -------------------------------------------------------------------------------- /examples/deps.bzl: -------------------------------------------------------------------------------- 1 | load("@bazel_gazelle//:deps.bzl", "go_repository") 2 | 3 | def go_dependencies(): 4 | go_repository( 5 | name = "com_github_soheilhy_cmux", 6 | importpath = "github.com/soheilhy/cmux", 7 | sum = "h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=", 8 | version = "v0.1.5", 9 | ) 10 | 11 | go_repository( 12 | name = "org_golang_google_grpc", 13 | importpath = "google.golang.org/grpc", 14 | sum = "h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=", 15 | version = "v1.54.0", 16 | ) 17 | -------------------------------------------------------------------------------- /examples/gen.sh: -------------------------------------------------------------------------------- 1 | protoc -I ~/src/github.com/googleapis/api-common-protos/ --go_out=module=larking.io/examples:. --go-grpc_out=module=larking.io/examples:. -I. ./api/*.proto 2 | -------------------------------------------------------------------------------- /examples/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/emcfarlane/larking/examples 2 | 3 | replace larking.io/ => ../ 4 | 5 | -------------------------------------------------------------------------------- /examples/go/helloworld/BUILD.bazel: -------------------------------------------------------------------------------- 1 | load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") 2 | 3 | go_library( 4 | name = "go_default_library", 5 | srcs = ["main.go"], 6 | importpath = "github.com/emcfarlane/larking/examples/go/helloworld", 7 | visibility = ["//visibility:private"], 8 | deps = [ 9 | "//:go_default_library", 10 | "//examples/proto:helloworld_go_proto", 11 | "@com_github_soheilhy_cmux//:go_default_library", 12 | "@org_golang_google_grpc//:go_default_library", 13 | "@org_golang_google_grpc//reflection:go_default_library", 14 | ], 15 | ) 16 | 17 | go_binary( 18 | name = "helloworld", 19 | embed = [":helloworld_lib"], 20 | visibility = ["//visibility:public"], 21 | ) 22 | 23 | go_library( 24 | name = "helloworld_lib", 25 | srcs = ["main.go"], 26 | importpath = "larking.io/examples/go/helloworld", 27 | visibility = ["//visibility:private"], 28 | deps = [ 29 | "@com_github_soheilhy_cmux//:cmux", 30 | "@org_golang_google_grpc//:go_default_library", 31 | "@org_golang_google_grpc//reflection", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /examples/go/helloworld/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/http" 10 | 11 | "github.com/soheilhy/cmux" 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/reflection" 14 | pb "larking.io/examples/proto/helloworld" 15 | "larking.io/larking" 16 | ) 17 | 18 | var ( 19 | flagPort = flag.String("port", "8000", "Port") 20 | flagHost = flag.String("host", "", "Host") 21 | ) 22 | 23 | type Server struct { 24 | pb.UnimplementedGreeterServer 25 | } 26 | 27 | func (s *Server) SayHello(ctx context.Context, req *pb.HelloRequest) (*pb.HelloReply, error) { 28 | return &pb.HelloReply{ 29 | Message: fmt.Sprintf("Hello, %s! <3 Go", req.Name), 30 | }, nil 31 | } 32 | 33 | func (s *Server) serve(l net.Listener) error { 34 | m := cmux.New(l) 35 | 36 | grpcL := m.Match(cmux.HTTP2HeaderField("content-type", "application/grpc")) 37 | httpL := m.Match(cmux.Any()) 38 | 39 | gs := grpc.NewServer() 40 | pb.RegisterGreeterServer(gs, s) 41 | reflection.Register(gs) 42 | 43 | // Register HTTP handler in-process to server both the GRPC server and 44 | // the HTTP server on one port. 45 | hd := &larking.Handler{} 46 | if err := hd.RegisterServiceByName("helloworld.Greeter", s); err != nil { 47 | return err 48 | } 49 | 50 | hs := &http.Server{ 51 | Handler: hd, 52 | } 53 | 54 | errs := make(chan error, 3) 55 | 56 | go func() { errs <- gs.Serve(grpcL) }() 57 | defer gs.Stop() 58 | 59 | go func() { errs <- hs.Serve(httpL) }() 60 | defer hs.Close() 61 | 62 | go func() { errs <- m.Serve() }() 63 | 64 | return <-errs 65 | } 66 | 67 | func run() error { 68 | flag.Parse() 69 | 70 | l, err := net.Listen("tcp", fmt.Sprintf("%s:%s", *flagHost, *flagPort)) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | s := &Server{} 76 | return s.serve(l) 77 | } 78 | 79 | func main() { 80 | flag.Parse() 81 | 82 | if err := run(); err != nil { 83 | log.Fatal(err) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/java/com/example/helloworld/BUILD.bazel: -------------------------------------------------------------------------------- 1 | java_library( 2 | name = "helloworld", 3 | testonly = 1, 4 | srcs = glob( 5 | ["*.java"], 6 | ), 7 | runtime_deps = [ 8 | "@io_grpc_grpc_java//netty", 9 | ], 10 | deps = [ 11 | "//examples/api:helloworld_java_grpc", 12 | "//examples/api:helloworld_java_proto", 13 | "@com_google_protobuf//:protobuf_java", 14 | "@com_google_protobuf//:protobuf_java_util", 15 | "@io_grpc_grpc_java//api", 16 | "@io_grpc_grpc_java//protobuf", 17 | "@io_grpc_grpc_java//services:reflection", 18 | "@io_grpc_grpc_java//stub", 19 | "@maven//:com_google_api_grpc_proto_google_common_protos", 20 | "@maven//:com_google_code_findbugs_jsr305", 21 | "@maven//:com_google_code_gson_gson", 22 | "@maven//:com_google_guava_guava", 23 | ], 24 | ) 25 | 26 | java_binary( 27 | name = "HelloWorldServer", 28 | testonly = 1, 29 | main_class = "com.examples.helloworld.HelloWorldServer", 30 | runtime_deps = [ 31 | ":helloworld", 32 | ], 33 | ) 34 | -------------------------------------------------------------------------------- /examples/java/com/example/helloworld/HelloWorldServer.java: -------------------------------------------------------------------------------- 1 | package com.examples.helloworld; 2 | 3 | import io.grpc.Server; 4 | import io.grpc.ServerBuilder; 5 | import io.grpc.protobuf.services.ProtoReflectionService; 6 | import io.grpc.stub.StreamObserver; 7 | import java.io.IOException; 8 | import java.util.concurrent.TimeUnit; 9 | import java.util.logging.Logger; 10 | 11 | /** 12 | * Server that manages startup/shutdown of a {@code Greeter} server. 13 | */ 14 | public class HelloWorldServer { 15 | private static final Logger logger = Logger.getLogger(HelloWorldServer.class.getName()); 16 | 17 | private Server server; 18 | 19 | private void start() throws IOException { 20 | /* The port on which the server should run */ 21 | int port = 50051; 22 | server = ServerBuilder.forPort(port) 23 | .addService(new GreeterImpl()) 24 | .addService(ProtoReflectionService.newInstance()) 25 | .build() 26 | .start(); 27 | logger.info("Server started, listening on " + port); 28 | Runtime.getRuntime().addShutdownHook(new Thread() { 29 | @Override 30 | public void run() { 31 | // Use stderr here since the logger may have been reset by its JVM shutdown hook. 32 | System.err.println("*** shutting down gRPC server since JVM is shutting down"); 33 | try { 34 | HelloWorldServer.this.stop(); 35 | } catch (InterruptedException e) { 36 | e.printStackTrace(System.err); 37 | } 38 | System.err.println("*** server shut down"); 39 | } 40 | }); 41 | } 42 | 43 | private void stop() throws InterruptedException { 44 | if (server != null) { 45 | server.shutdown().awaitTermination(30, TimeUnit.SECONDS); 46 | } 47 | } 48 | 49 | /** 50 | * Await termination on the main thread since the grpc library uses daemon threads. 51 | */ 52 | private void blockUntilShutdown() throws InterruptedException { 53 | if (server != null) { 54 | server.awaitTermination(); 55 | } 56 | } 57 | 58 | /** 59 | * Main launches the server from the command line. 60 | */ 61 | public static void main(String[] args) throws IOException, InterruptedException { 62 | final HelloWorldServer server = new HelloWorldServer(); 63 | server.start(); 64 | server.blockUntilShutdown(); 65 | } 66 | 67 | static class GreeterImpl extends GreeterGrpc.GreeterImplBase { 68 | 69 | @Override 70 | public void sayHello(HelloRequest req, StreamObserver responseObserver) { 71 | HelloReply reply = HelloReply.newBuilder().setMessage("Hi " + req.getName()).build(); 72 | responseObserver.onNext(reply); 73 | responseObserver.onCompleted(); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/proto/hellostream/hellostream.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.30.0 4 | // protoc v3.21.12 5 | // source: api/hellostream.proto 6 | 7 | package proto 8 | 9 | import ( 10 | _ "google.golang.org/genproto/googleapis/api/annotations" 11 | protoreflect "google.golang.org/protobuf/reflect/protoreflect" 12 | protoimpl "google.golang.org/protobuf/runtime/protoimpl" 13 | reflect "reflect" 14 | sync "sync" 15 | ) 16 | 17 | const ( 18 | // Verify that this generated code is sufficiently up-to-date. 19 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 20 | // Verify that runtime/protoimpl is sufficiently up-to-date. 21 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 22 | ) 23 | 24 | // The request message containing the user's name. 25 | type HelloRequest struct { 26 | state protoimpl.MessageState 27 | sizeCache protoimpl.SizeCache 28 | unknownFields protoimpl.UnknownFields 29 | 30 | Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` 31 | } 32 | 33 | func (x *HelloRequest) Reset() { 34 | *x = HelloRequest{} 35 | if protoimpl.UnsafeEnabled { 36 | mi := &file_api_hellostream_proto_msgTypes[0] 37 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 38 | ms.StoreMessageInfo(mi) 39 | } 40 | } 41 | 42 | func (x *HelloRequest) String() string { 43 | return protoimpl.X.MessageStringOf(x) 44 | } 45 | 46 | func (*HelloRequest) ProtoMessage() {} 47 | 48 | func (x *HelloRequest) ProtoReflect() protoreflect.Message { 49 | mi := &file_api_hellostream_proto_msgTypes[0] 50 | if protoimpl.UnsafeEnabled && x != nil { 51 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 52 | if ms.LoadMessageInfo() == nil { 53 | ms.StoreMessageInfo(mi) 54 | } 55 | return ms 56 | } 57 | return mi.MessageOf(x) 58 | } 59 | 60 | // Deprecated: Use HelloRequest.ProtoReflect.Descriptor instead. 61 | func (*HelloRequest) Descriptor() ([]byte, []int) { 62 | return file_api_hellostream_proto_rawDescGZIP(), []int{0} 63 | } 64 | 65 | func (x *HelloRequest) GetName() string { 66 | if x != nil { 67 | return x.Name 68 | } 69 | return "" 70 | } 71 | 72 | // The response message containing the greetings 73 | type HelloReply struct { 74 | state protoimpl.MessageState 75 | sizeCache protoimpl.SizeCache 76 | unknownFields protoimpl.UnknownFields 77 | 78 | Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` 79 | } 80 | 81 | func (x *HelloReply) Reset() { 82 | *x = HelloReply{} 83 | if protoimpl.UnsafeEnabled { 84 | mi := &file_api_hellostream_proto_msgTypes[1] 85 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 86 | ms.StoreMessageInfo(mi) 87 | } 88 | } 89 | 90 | func (x *HelloReply) String() string { 91 | return protoimpl.X.MessageStringOf(x) 92 | } 93 | 94 | func (*HelloReply) ProtoMessage() {} 95 | 96 | func (x *HelloReply) ProtoReflect() protoreflect.Message { 97 | mi := &file_api_hellostream_proto_msgTypes[1] 98 | if protoimpl.UnsafeEnabled && x != nil { 99 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 100 | if ms.LoadMessageInfo() == nil { 101 | ms.StoreMessageInfo(mi) 102 | } 103 | return ms 104 | } 105 | return mi.MessageOf(x) 106 | } 107 | 108 | // Deprecated: Use HelloReply.ProtoReflect.Descriptor instead. 109 | func (*HelloReply) Descriptor() ([]byte, []int) { 110 | return file_api_hellostream_proto_rawDescGZIP(), []int{1} 111 | } 112 | 113 | func (x *HelloReply) GetMessage() string { 114 | if x != nil { 115 | return x.Message 116 | } 117 | return "" 118 | } 119 | 120 | var File_api_hellostream_proto protoreflect.FileDescriptor 121 | 122 | var file_api_hellostream_proto_rawDesc = []byte{ 123 | 0x0a, 0x15, 0x61, 0x70, 0x69, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x73, 0x74, 0x72, 0x65, 0x61, 124 | 0x6d, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0b, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x73, 0x74, 125 | 0x72, 0x65, 0x61, 0x6d, 0x1a, 0x1c, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x61, 0x70, 0x69, 126 | 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 127 | 0x74, 0x6f, 0x22, 0x22, 0x0a, 0x0c, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 128 | 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 129 | 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x26, 0x0a, 0x0a, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 130 | 0x65, 0x70, 0x6c, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 131 | 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x32, 0x6f, 132 | 0x0a, 0x10, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x47, 0x72, 0x65, 0x65, 0x74, 133 | 0x65, 0x72, 0x12, 0x5b, 0x0a, 0x11, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x53, 0x74, 134 | 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x19, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x73, 135 | 0x74, 0x72, 0x65, 0x61, 0x6d, 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 136 | 0x73, 0x74, 0x1a, 0x17, 0x2e, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 137 | 0x2e, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x0e, 0x82, 0xd3, 0xe4, 138 | 0x93, 0x02, 0x08, 0x22, 0x06, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x28, 0x01, 0x30, 0x01, 0x42, 139 | 0x62, 0x0a, 0x18, 0x63, 0x6f, 0x6d, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x73, 0x2e, 140 | 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x42, 0x10, 0x48, 0x65, 0x6c, 141 | 0x6c, 0x6f, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 142 | 0x2b, 0x6c, 0x61, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x2e, 0x69, 0x6f, 0x2f, 0x65, 0x78, 0x61, 0x6d, 143 | 0x70, 0x6c, 0x65, 0x73, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 144 | 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x3b, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0xa2, 0x02, 0x04, 0x48, 145 | 0x4c, 0x57, 0x53, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 146 | } 147 | 148 | var ( 149 | file_api_hellostream_proto_rawDescOnce sync.Once 150 | file_api_hellostream_proto_rawDescData = file_api_hellostream_proto_rawDesc 151 | ) 152 | 153 | func file_api_hellostream_proto_rawDescGZIP() []byte { 154 | file_api_hellostream_proto_rawDescOnce.Do(func() { 155 | file_api_hellostream_proto_rawDescData = protoimpl.X.CompressGZIP(file_api_hellostream_proto_rawDescData) 156 | }) 157 | return file_api_hellostream_proto_rawDescData 158 | } 159 | 160 | var file_api_hellostream_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 161 | var file_api_hellostream_proto_goTypes = []interface{}{ 162 | (*HelloRequest)(nil), // 0: hellostream.HelloRequest 163 | (*HelloReply)(nil), // 1: hellostream.HelloReply 164 | } 165 | var file_api_hellostream_proto_depIdxs = []int32{ 166 | 0, // 0: hellostream.StreamingGreeter.SayHelloStreaming:input_type -> hellostream.HelloRequest 167 | 1, // 1: hellostream.StreamingGreeter.SayHelloStreaming:output_type -> hellostream.HelloReply 168 | 1, // [1:2] is the sub-list for method output_type 169 | 0, // [0:1] is the sub-list for method input_type 170 | 0, // [0:0] is the sub-list for extension type_name 171 | 0, // [0:0] is the sub-list for extension extendee 172 | 0, // [0:0] is the sub-list for field type_name 173 | } 174 | 175 | func init() { file_api_hellostream_proto_init() } 176 | func file_api_hellostream_proto_init() { 177 | if File_api_hellostream_proto != nil { 178 | return 179 | } 180 | if !protoimpl.UnsafeEnabled { 181 | file_api_hellostream_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 182 | switch v := v.(*HelloRequest); i { 183 | case 0: 184 | return &v.state 185 | case 1: 186 | return &v.sizeCache 187 | case 2: 188 | return &v.unknownFields 189 | default: 190 | return nil 191 | } 192 | } 193 | file_api_hellostream_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 194 | switch v := v.(*HelloReply); i { 195 | case 0: 196 | return &v.state 197 | case 1: 198 | return &v.sizeCache 199 | case 2: 200 | return &v.unknownFields 201 | default: 202 | return nil 203 | } 204 | } 205 | } 206 | type x struct{} 207 | out := protoimpl.TypeBuilder{ 208 | File: protoimpl.DescBuilder{ 209 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 210 | RawDescriptor: file_api_hellostream_proto_rawDesc, 211 | NumEnums: 0, 212 | NumMessages: 2, 213 | NumExtensions: 0, 214 | NumServices: 1, 215 | }, 216 | GoTypes: file_api_hellostream_proto_goTypes, 217 | DependencyIndexes: file_api_hellostream_proto_depIdxs, 218 | MessageInfos: file_api_hellostream_proto_msgTypes, 219 | }.Build() 220 | File_api_hellostream_proto = out.File 221 | file_api_hellostream_proto_rawDesc = nil 222 | file_api_hellostream_proto_goTypes = nil 223 | file_api_hellostream_proto_depIdxs = nil 224 | } 225 | -------------------------------------------------------------------------------- /examples/proto/hellostream/hellostream_grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go-grpc. DO NOT EDIT. 2 | // versions: 3 | // - protoc-gen-go-grpc v1.3.0 4 | // - protoc v3.21.12 5 | // source: api/hellostream.proto 6 | 7 | package proto 8 | 9 | import ( 10 | context "context" 11 | grpc "google.golang.org/grpc" 12 | codes "google.golang.org/grpc/codes" 13 | status "google.golang.org/grpc/status" 14 | ) 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the grpc package it is being compiled against. 18 | // Requires gRPC-Go v1.32.0 or later. 19 | const _ = grpc.SupportPackageIsVersion7 20 | 21 | const ( 22 | StreamingGreeter_SayHelloStreaming_FullMethodName = "/hellostream.StreamingGreeter/SayHelloStreaming" 23 | ) 24 | 25 | // StreamingGreeterClient is the client API for StreamingGreeter service. 26 | // 27 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. 28 | type StreamingGreeterClient interface { 29 | // Streams a many greetings 30 | SayHelloStreaming(ctx context.Context, opts ...grpc.CallOption) (StreamingGreeter_SayHelloStreamingClient, error) 31 | } 32 | 33 | type streamingGreeterClient struct { 34 | cc grpc.ClientConnInterface 35 | } 36 | 37 | func NewStreamingGreeterClient(cc grpc.ClientConnInterface) StreamingGreeterClient { 38 | return &streamingGreeterClient{cc} 39 | } 40 | 41 | func (c *streamingGreeterClient) SayHelloStreaming(ctx context.Context, opts ...grpc.CallOption) (StreamingGreeter_SayHelloStreamingClient, error) { 42 | stream, err := c.cc.NewStream(ctx, &StreamingGreeter_ServiceDesc.Streams[0], StreamingGreeter_SayHelloStreaming_FullMethodName, opts...) 43 | if err != nil { 44 | return nil, err 45 | } 46 | x := &streamingGreeterSayHelloStreamingClient{stream} 47 | return x, nil 48 | } 49 | 50 | type StreamingGreeter_SayHelloStreamingClient interface { 51 | Send(*HelloRequest) error 52 | Recv() (*HelloReply, error) 53 | grpc.ClientStream 54 | } 55 | 56 | type streamingGreeterSayHelloStreamingClient struct { 57 | grpc.ClientStream 58 | } 59 | 60 | func (x *streamingGreeterSayHelloStreamingClient) Send(m *HelloRequest) error { 61 | return x.ClientStream.SendMsg(m) 62 | } 63 | 64 | func (x *streamingGreeterSayHelloStreamingClient) Recv() (*HelloReply, error) { 65 | m := new(HelloReply) 66 | if err := x.ClientStream.RecvMsg(m); err != nil { 67 | return nil, err 68 | } 69 | return m, nil 70 | } 71 | 72 | // StreamingGreeterServer is the server API for StreamingGreeter service. 73 | // All implementations must embed UnimplementedStreamingGreeterServer 74 | // for forward compatibility 75 | type StreamingGreeterServer interface { 76 | // Streams a many greetings 77 | SayHelloStreaming(StreamingGreeter_SayHelloStreamingServer) error 78 | mustEmbedUnimplementedStreamingGreeterServer() 79 | } 80 | 81 | // UnimplementedStreamingGreeterServer must be embedded to have forward compatible implementations. 82 | type UnimplementedStreamingGreeterServer struct { 83 | } 84 | 85 | func (UnimplementedStreamingGreeterServer) SayHelloStreaming(StreamingGreeter_SayHelloStreamingServer) error { 86 | return status.Errorf(codes.Unimplemented, "method SayHelloStreaming not implemented") 87 | } 88 | func (UnimplementedStreamingGreeterServer) mustEmbedUnimplementedStreamingGreeterServer() {} 89 | 90 | // UnsafeStreamingGreeterServer may be embedded to opt out of forward compatibility for this service. 91 | // Use of this interface is not recommended, as added methods to StreamingGreeterServer will 92 | // result in compilation errors. 93 | type UnsafeStreamingGreeterServer interface { 94 | mustEmbedUnimplementedStreamingGreeterServer() 95 | } 96 | 97 | func RegisterStreamingGreeterServer(s grpc.ServiceRegistrar, srv StreamingGreeterServer) { 98 | s.RegisterService(&StreamingGreeter_ServiceDesc, srv) 99 | } 100 | 101 | func _StreamingGreeter_SayHelloStreaming_Handler(srv interface{}, stream grpc.ServerStream) error { 102 | return srv.(StreamingGreeterServer).SayHelloStreaming(&streamingGreeterSayHelloStreamingServer{stream}) 103 | } 104 | 105 | type StreamingGreeter_SayHelloStreamingServer interface { 106 | Send(*HelloReply) error 107 | Recv() (*HelloRequest, error) 108 | grpc.ServerStream 109 | } 110 | 111 | type streamingGreeterSayHelloStreamingServer struct { 112 | grpc.ServerStream 113 | } 114 | 115 | func (x *streamingGreeterSayHelloStreamingServer) Send(m *HelloReply) error { 116 | return x.ServerStream.SendMsg(m) 117 | } 118 | 119 | func (x *streamingGreeterSayHelloStreamingServer) Recv() (*HelloRequest, error) { 120 | m := new(HelloRequest) 121 | if err := x.ServerStream.RecvMsg(m); err != nil { 122 | return nil, err 123 | } 124 | return m, nil 125 | } 126 | 127 | // StreamingGreeter_ServiceDesc is the grpc.ServiceDesc for StreamingGreeter service. 128 | // It's only intended for direct use with grpc.RegisterService, 129 | // and not to be introspected or modified (even as a copy) 130 | var StreamingGreeter_ServiceDesc = grpc.ServiceDesc{ 131 | ServiceName: "hellostream.StreamingGreeter", 132 | HandlerType: (*StreamingGreeterServer)(nil), 133 | Methods: []grpc.MethodDesc{}, 134 | Streams: []grpc.StreamDesc{ 135 | { 136 | StreamName: "SayHelloStreaming", 137 | Handler: _StreamingGreeter_SayHelloStreaming_Handler, 138 | ServerStreams: true, 139 | ClientStreams: true, 140 | }, 141 | }, 142 | Metadata: "api/hellostream.proto", 143 | } 144 | -------------------------------------------------------------------------------- /gen.go: -------------------------------------------------------------------------------- 1 | package larking_io 2 | 3 | //go:generate sh gen.sh 4 | -------------------------------------------------------------------------------- /gen.sh: -------------------------------------------------------------------------------- 1 | protoc -I ~/src/github.com/googleapis/api-common-protos/ -I.. \ 2 | --go_out=module=larking.io:. \ 3 | --go-grpc_out=module=larking.io:. \ 4 | ../larking/api/*.proto 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module larking.io 2 | 3 | go 1.22.7 4 | 5 | toolchain go1.22.9 6 | 7 | require ( 8 | github.com/gobwas/ws v1.2.0 9 | github.com/google/go-cmp v0.6.0 10 | golang.org/x/net v0.29.0 11 | golang.org/x/sync v0.8.0 12 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 13 | google.golang.org/grpc v1.68.0 14 | google.golang.org/protobuf v1.34.2 15 | ) 16 | 17 | require ( 18 | github.com/gobwas/httphead v0.1.0 // indirect 19 | github.com/gobwas/pool v0.2.1 // indirect 20 | golang.org/x/sys v0.25.0 // indirect 21 | golang.org/x/text v0.18.0 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= 2 | github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= 3 | github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= 4 | github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 5 | github.com/gobwas/ws v1.2.0 h1:u0p9s3xLYpZCA1z5JgCkMeB34CKCMMQbM+G8Ii7YD0I= 6 | github.com/gobwas/ws v1.2.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= 7 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 8 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 9 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 10 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 11 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 12 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 13 | golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 14 | golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 15 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 16 | golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 17 | golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 18 | golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 19 | golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 20 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= 21 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= 22 | google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0= 23 | google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA= 24 | google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= 25 | google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 26 | -------------------------------------------------------------------------------- /health/health.go: -------------------------------------------------------------------------------- 1 | // package health provides gRPC health server HTTP annotations. 2 | package health 3 | 4 | import ( 5 | "google.golang.org/genproto/googleapis/api/annotations" 6 | "google.golang.org/genproto/googleapis/api/serviceconfig" 7 | "google.golang.org/grpc/health" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | // AddHealthz adds a /v1/healthz endpoint to the service binding to the 12 | // grpc.health.v1.Health service: 13 | // - get /v1/healthz -> grpc.health.v1.Health.Check 14 | // - websocket /v1/healthz -> grpc.health.v1.Health.Watch 15 | func AddHealthz(dst *serviceconfig.Service) { 16 | src := &serviceconfig.Service{ 17 | Http: &annotations.Http{Rules: []*annotations.HttpRule{{ 18 | Selector: "grpc.health.v1.Health.Check", 19 | Pattern: &annotations.HttpRule_Get{ 20 | // Get is a HTTP GET. 21 | Get: "/v1/healthz", 22 | }, 23 | }, { 24 | Selector: "grpc.health.v1.Health.Watch", 25 | Pattern: &annotations.HttpRule_Custom{ 26 | Custom: &annotations.CustomHttpPattern{ 27 | Kind: "WEBSOCKET", 28 | Path: "/v1/healthz", 29 | }, 30 | }, 31 | }}}, 32 | } 33 | proto.Merge(dst, src) 34 | } 35 | 36 | // NewServer returns a new grpc.Health server. 37 | func NewServer() *health.Server { return health.NewServer() } 38 | -------------------------------------------------------------------------------- /larking/bench.txt: -------------------------------------------------------------------------------- 1 | goos: darwin 2 | goarch: amd64 3 | pkg: larking.io/larking 4 | cpu: VirtualApple @ 2.50GHz 5 | BenchmarkLexer-8 5162886 232.7 ns/op 0 B/op 0 allocs/op 6 | PASS 7 | ok larking.io/larking 1.953s 8 | -------------------------------------------------------------------------------- /larking/code.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "net/http" 9 | 10 | "github.com/gobwas/ws" 11 | "google.golang.org/grpc/codes" 12 | ) 13 | 14 | var codeToHTTPStatus = [...]int{ 15 | http.StatusOK, // 0 16 | http.StatusRequestTimeout, // 1 17 | http.StatusInternalServerError, // 2 18 | http.StatusBadRequest, // 3 19 | http.StatusGatewayTimeout, // 4 20 | http.StatusNotFound, // 5 21 | http.StatusConflict, // 6 22 | http.StatusForbidden, // 7 23 | http.StatusTooManyRequests, // 8 24 | http.StatusBadRequest, // 9 25 | http.StatusConflict, // 10 26 | http.StatusBadRequest, // 11 27 | http.StatusNotImplemented, // 12 28 | http.StatusInternalServerError, // 13 29 | http.StatusServiceUnavailable, // 14 30 | http.StatusInternalServerError, // 15 31 | http.StatusUnauthorized, // 16 32 | } 33 | 34 | func HTTPStatusCode(c codes.Code) int { 35 | if int(c) > len(codeToHTTPStatus) { 36 | return http.StatusInternalServerError 37 | } 38 | return codeToHTTPStatus[c] 39 | } 40 | 41 | // TODO: validate error codes. 42 | var codeToWSStatus = [...]ws.StatusCode{ 43 | ws.StatusNormalClosure, // 0 44 | ws.StatusGoingAway, // 1 45 | ws.StatusInternalServerError, // 2 46 | ws.StatusUnsupportedData, // 3 47 | ws.StatusGoingAway, // 4 48 | ws.StatusInternalServerError, // 5 49 | ws.StatusGoingAway, // 6 50 | ws.StatusInternalServerError, // 7 51 | ws.StatusInternalServerError, // 8 52 | ws.StatusInternalServerError, // 9 53 | ws.StatusInternalServerError, // 10 54 | ws.StatusInternalServerError, // 11 55 | ws.StatusUnsupportedData, // 12 56 | ws.StatusInternalServerError, // 13 57 | ws.StatusInternalServerError, // 14 58 | ws.StatusInternalServerError, // 15 59 | ws.StatusPolicyViolation, // 16 60 | } 61 | 62 | func WSStatusCode(c codes.Code) ws.StatusCode { 63 | if int(c) > len(codeToHTTPStatus) { 64 | return ws.StatusInternalServerError 65 | } 66 | return codeToWSStatus[c] 67 | } 68 | -------------------------------------------------------------------------------- /larking/codec.go: -------------------------------------------------------------------------------- 1 | // Copyright 2023 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "encoding/binary" 9 | "fmt" 10 | "io" 11 | "sync" 12 | 13 | "google.golang.org/grpc/encoding" 14 | "google.golang.org/protobuf/encoding/protodelim" 15 | "google.golang.org/protobuf/encoding/protojson" 16 | "google.golang.org/protobuf/encoding/protowire" 17 | "google.golang.org/protobuf/proto" 18 | ) 19 | 20 | var bytesPool = sync.Pool{ 21 | New: func() interface{} { 22 | b := make([]byte, 0, 64) 23 | return &b 24 | }, 25 | } 26 | 27 | // growcap scales up the capacity of a slice. 28 | // Taken from the Go 1.14 runtime and proto package. 29 | func growcap(oldcap, wantcap int) (newcap int) { 30 | if wantcap > oldcap*2 { 31 | newcap = wantcap 32 | } else if oldcap < 1024 { 33 | // The Go 1.14 runtime takes this case when len(s) < 1024, 34 | // not when cap(s) < 1024. The difference doesn't seem 35 | // significant here. 36 | newcap = oldcap * 2 37 | } else { 38 | newcap = oldcap 39 | for 0 < newcap && newcap < wantcap { 40 | newcap += newcap / 4 41 | } 42 | if newcap <= 0 { 43 | newcap = wantcap 44 | } 45 | } 46 | return newcap 47 | } 48 | 49 | // Codec defines the interface used to encode and decode messages. 50 | type Codec interface { 51 | encoding.Codec 52 | // MarshalAppend appends the marshaled form of v to b and returns the result. 53 | MarshalAppend([]byte, interface{}) ([]byte, error) 54 | } 55 | 56 | // StreamCodec is used in streaming RPCs where the message boundaries are 57 | // determined by the codec. 58 | type StreamCodec interface { 59 | Codec 60 | 61 | // ReadNext returns the size of the next message appended to buf. 62 | // ReadNext reads from r until either it has read a complete message or 63 | // encountered an error and returns all the data read from r. 64 | // The message is contained in dst[:n]. 65 | // Excess data read from r is stored in dst[n:]. 66 | ReadNext(buf []byte, r io.Reader, limit int) (dst []byte, n int, err error) 67 | // WriteNext writes the message to w with a size aware encoding 68 | // returning the number of bytes written. 69 | WriteNext(w io.Writer, src []byte) (n int, err error) 70 | } 71 | 72 | func errInvalidType(v any) error { 73 | return fmt.Errorf("marshal invalid type %T", v) 74 | } 75 | 76 | // CodecProto is a Codec implementation with protobuf binary format. 77 | type CodecProto struct { 78 | proto.MarshalOptions 79 | } 80 | 81 | func (c CodecProto) Marshal(v interface{}) ([]byte, error) { 82 | m, ok := v.(proto.Message) 83 | if !ok { 84 | return nil, errInvalidType(v) 85 | } 86 | return c.MarshalOptions.Marshal(m) 87 | } 88 | 89 | func (c CodecProto) MarshalAppend(b []byte, v interface{}) ([]byte, error) { 90 | m, ok := v.(proto.Message) 91 | if !ok { 92 | return nil, errInvalidType(v) 93 | } 94 | return c.MarshalOptions.MarshalAppend(b, m) 95 | } 96 | 97 | func (CodecProto) Unmarshal(data []byte, v interface{}) error { 98 | m, ok := v.(proto.Message) 99 | if !ok { 100 | return errInvalidType(v) 101 | } 102 | return proto.Unmarshal(data, m) 103 | } 104 | 105 | // ReadNext reads a varint size-delimited wire-format message from r. 106 | func (c CodecProto) ReadNext(b []byte, r io.Reader, limit int) ([]byte, int, error) { 107 | for i := 0; i < binary.MaxVarintLen64; i++ { 108 | for i >= len(b) { 109 | if len(b) == cap(b) { 110 | // Add more capacity (let append pick how much). 111 | b = append(b, 0)[:len(b)] 112 | } 113 | n, err := r.Read(b[len(b):cap(b)]) 114 | b = b[:len(b)+n] 115 | if err != nil { 116 | return b, 0, err 117 | } 118 | } 119 | if b[i] < 0x80 { 120 | break 121 | } 122 | } 123 | 124 | size, n := protowire.ConsumeVarint(b) 125 | if n < 0 { 126 | return b, 0, protowire.ParseError(n) 127 | } 128 | if limit > 0 && int(size) > limit { 129 | return b, 0, &protodelim.SizeTooLargeError{Size: size, MaxSize: uint64(limit)} 130 | } 131 | b = b[n:] // consume the varint 132 | n = int(size) 133 | 134 | if len(b) < n { 135 | if cap(b) < n { 136 | dst := make([]byte, len(b), growcap(cap(b), n)) 137 | copy(dst, b) 138 | b = dst 139 | } 140 | if _, err := io.ReadFull(r, b[len(b):n]); err != nil { 141 | if err == io.EOF { 142 | return b, 0, io.ErrUnexpectedEOF 143 | } 144 | return b, 0, err 145 | } 146 | b = b[:n] 147 | } 148 | return b, n, nil 149 | } 150 | 151 | // WriteNext writes the length of the message encoded as 4 byte unsigned integer 152 | // and then writes the message to w. 153 | func (c CodecProto) WriteNext(w io.Writer, b []byte) (int, error) { 154 | var sizeArr [binary.MaxVarintLen64]byte 155 | sizeBuf := protowire.AppendVarint(sizeArr[:0], uint64(len(b))) 156 | if _, err := w.Write(sizeBuf); err != nil { 157 | return 0, err 158 | } 159 | return w.Write(b) 160 | } 161 | 162 | // Name == "proto" overwritting internal proto codec 163 | func (CodecProto) Name() string { return "proto" } 164 | 165 | // CodecJSON is a Codec implementation with protobuf json format. 166 | type CodecJSON struct { 167 | protojson.MarshalOptions 168 | protojson.UnmarshalOptions 169 | } 170 | 171 | func (c CodecJSON) Marshal(v interface{}) ([]byte, error) { 172 | m, ok := v.(proto.Message) 173 | if !ok { 174 | return nil, errInvalidType(v) 175 | } 176 | return c.MarshalOptions.Marshal(m) 177 | } 178 | 179 | func (c CodecJSON) MarshalAppend(b []byte, v interface{}) ([]byte, error) { 180 | m, ok := v.(proto.Message) 181 | if !ok { 182 | return nil, errInvalidType(v) 183 | } 184 | return c.MarshalOptions.MarshalAppend(b, m) 185 | } 186 | 187 | func (c CodecJSON) Unmarshal(data []byte, v interface{}) error { 188 | m, ok := v.(proto.Message) 189 | if !ok { 190 | return errInvalidType(v) 191 | } 192 | return c.UnmarshalOptions.Unmarshal(data, m) 193 | } 194 | 195 | // ReadNext reads the length of the message around the json object. 196 | // It reads until it finds a matching number of braces. 197 | // It does not validate the JSON. 198 | func (c CodecJSON) ReadNext(b []byte, r io.Reader, limit int) ([]byte, int, error) { 199 | var ( 200 | braceCount int 201 | isString bool 202 | isEscaped bool 203 | ) 204 | for i := 0; i < int(limit); i++ { 205 | for i >= len(b) { 206 | if len(b) == cap(b) { 207 | // Add more capacity (let append pick how much). 208 | b = append(b, 0)[:len(b)] 209 | } 210 | n, err := r.Read(b[len(b):cap(b)]) 211 | b = b[:len(b)+n] 212 | if err != nil { 213 | return b, 0, err 214 | } 215 | } 216 | 217 | switch { 218 | case isEscaped: 219 | isEscaped = false 220 | case isString: 221 | switch b[i] { 222 | case '\\': 223 | isEscaped = true 224 | case '"': 225 | isString = false 226 | } 227 | default: 228 | switch b[i] { 229 | case '{': 230 | braceCount++ 231 | case '}': 232 | braceCount-- 233 | if braceCount == 0 { 234 | return b, i + 1, nil 235 | } 236 | if braceCount < 0 { 237 | return b, 0, fmt.Errorf("unbalanced braces") 238 | } 239 | case '"': 240 | isString = true 241 | } 242 | } 243 | } 244 | return b, 0, &protodelim.SizeTooLargeError{Size: uint64(len(b)), MaxSize: uint64(limit)} 245 | } 246 | 247 | // WriteNext writes the raw JSON message to w without any size prefix. 248 | func (c CodecJSON) WriteNext(w io.Writer, b []byte) (int, error) { 249 | return w.Write(b) 250 | } 251 | 252 | func (CodecJSON) Name() string { return "json" } 253 | 254 | type codecHTTPBody struct{} 255 | 256 | func (codecHTTPBody) Marshal(v interface{}) ([]byte, error) { 257 | panic("not implemented") 258 | } 259 | 260 | func (codecHTTPBody) MarshalAppend(b []byte, v interface{}) ([]byte, error) { 261 | panic("not implemented") 262 | } 263 | 264 | func (codecHTTPBody) Unmarshal(data []byte, v interface{}) error { 265 | panic("not implemented") 266 | } 267 | 268 | func (codecHTTPBody) Name() string { return "body" } 269 | 270 | func (codecHTTPBody) ReadNext(b []byte, r io.Reader, limit int) ([]byte, int, error) { 271 | var total int 272 | for { 273 | if len(b) == cap(b) { 274 | // Add more capacity (let append pick how much). 275 | b = append(b, 0)[:len(b)] 276 | } 277 | n, err := r.Read(b[len(b):cap(b)]) 278 | b = b[:len(b)+n] 279 | total += int(n) 280 | if total > limit { 281 | total = limit 282 | } 283 | if err != nil || total == limit { 284 | return b, total, err 285 | } 286 | } 287 | } 288 | 289 | func (codecHTTPBody) WriteNext(w io.Writer, b []byte) (int, error) { 290 | return w.Write(b) 291 | } 292 | -------------------------------------------------------------------------------- /larking/codec_test.go: -------------------------------------------------------------------------------- 1 | package larking 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "testing" 7 | 8 | "google.golang.org/protobuf/encoding/protowire" 9 | "google.golang.org/protobuf/proto" 10 | "larking.io/api/testpb" 11 | ) 12 | 13 | func TestStreamCodecs(t *testing.T) { 14 | 15 | protob, err := proto.Marshal(&testpb.Message{ 16 | Text: "hello, protobuf", 17 | }) 18 | if err != nil { 19 | t.Fatal(err) 20 | } 21 | protobig, err := proto.Marshal(&testpb.Message{ 22 | Text: string(bytes.Repeat([]byte{'a'}, 1<<20)), 23 | }) 24 | if err != nil { 25 | t.Fatal(err) 26 | } 27 | jsonb, err := (&CodecJSON{}).Marshal(&testpb.Message{ 28 | Text: "hello, json", 29 | }) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | jsonescape := []byte(`{"text":"hello, json} \" }}"}`) 34 | 35 | tests := []struct { 36 | name string 37 | codec Codec 38 | input []byte 39 | extra []byte 40 | want []byte 41 | wantErr error 42 | }{{ 43 | name: "proto buffered", 44 | codec: CodecProto{}, 45 | input: func() []byte { 46 | b := protowire.AppendVarint(nil, uint64(len(protob))) 47 | t.Log("len(b)=", len(b)) 48 | return append(b, protob...) 49 | }(), 50 | want: protob, 51 | }, { 52 | name: "proto unbuffered", 53 | codec: CodecProto{}, 54 | input: make([]byte, 0, 4+len(protob)), 55 | extra: func() []byte { 56 | b := protowire.AppendVarint(nil, uint64(len(protob))) 57 | return append(b, protob...) 58 | }(), 59 | want: protob, 60 | }, { 61 | name: "proto partial size", 62 | codec: CodecProto{}, 63 | input: func() []byte { 64 | b := protowire.AppendVarint(nil, uint64(len(protob))) 65 | return append(b, protob...)[:1] 66 | }(), 67 | extra: func() []byte { 68 | b := protowire.AppendVarint(nil, uint64(len(protob))) 69 | return append(b, protob...)[1:] 70 | }(), 71 | want: protob, 72 | }, { 73 | name: "proto partial message", 74 | codec: CodecProto{}, 75 | input: func() []byte { 76 | b := protowire.AppendVarint(nil, uint64(len(protob))) 77 | return append(b, protob...)[:6] 78 | }(), 79 | extra: func() []byte { 80 | b := protowire.AppendVarint(nil, uint64(len(protob))) 81 | return append(b, protob...)[6:] 82 | }(), 83 | want: protob, 84 | }, { 85 | name: "proto zero size", 86 | codec: CodecProto{}, 87 | extra: func() []byte { 88 | b := protowire.AppendVarint(nil, 0) 89 | return b 90 | }(), 91 | want: []byte{}, 92 | }, { 93 | name: "proto big size", 94 | codec: CodecProto{}, 95 | extra: func() []byte { 96 | b := protowire.AppendVarint(nil, uint64(len(protobig))) 97 | return append(b, protobig...) 98 | }(), 99 | want: protobig, 100 | }, { 101 | name: "json buffered", 102 | codec: CodecJSON{}, 103 | input: jsonb, 104 | want: jsonb, 105 | }, { 106 | name: "json unbuffered", 107 | codec: CodecJSON{}, 108 | input: make([]byte, 0, 4+len(jsonb)), 109 | extra: jsonb, 110 | want: jsonb, 111 | }, { 112 | name: "json partial object", 113 | codec: CodecJSON{}, 114 | input: jsonb[:2], 115 | extra: jsonb[2:], 116 | want: jsonb, 117 | }, { 118 | name: "json escape", 119 | codec: CodecJSON{}, 120 | input: jsonescape, 121 | want: jsonescape, 122 | }} 123 | 124 | for _, tt := range tests { 125 | t.Run(tt.name, func(t *testing.T) { 126 | codec := tt.codec.(StreamCodec) 127 | 128 | r := bytes.NewReader(tt.extra) 129 | b, n, err := codec.ReadNext(tt.input, r, len(tt.want)) 130 | if err != nil { 131 | if tt.wantErr != nil { 132 | if !errors.Is(err, tt.wantErr) { 133 | t.Fatalf("got %v, want %v", err, tt.wantErr) 134 | } 135 | return 136 | } 137 | t.Fatal(err) 138 | } 139 | if n > len(b) { 140 | t.Fatalf("n %v > %v", n, len(b)) 141 | } 142 | 143 | got := b[:n] 144 | if !bytes.Equal(got, tt.want) { 145 | t.Errorf("got %s, want %s", got, tt.want) 146 | } 147 | 148 | var msg testpb.Message 149 | if err := codec.Unmarshal(got, &msg); err != nil { 150 | t.Error(err) 151 | } 152 | 153 | b, err = codec.MarshalAppend(b[:0], &msg) 154 | if err != nil { 155 | t.Fatal(err) 156 | } 157 | 158 | var buf bytes.Buffer 159 | if _, err := codec.WriteNext(&buf, b); err != nil { 160 | t.Fatal(err) 161 | } 162 | 163 | onwire := append([]byte(nil), tt.input...) 164 | onwire = append(onwire, tt.extra...) 165 | if !bytes.Equal(buf.Bytes(), onwire) { 166 | t.Errorf("onwire got %v, want %v", buf.Bytes(), onwire) 167 | } 168 | }) 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /larking/compress.go: -------------------------------------------------------------------------------- 1 | package larking 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "io" 7 | "sync" 8 | 9 | "google.golang.org/grpc/encoding" 10 | ) 11 | 12 | var bufPool = sync.Pool{ 13 | New: func() interface{} { 14 | return &bytes.Buffer{} 15 | }, 16 | } 17 | 18 | // Compressor is used to compress and decompress messages. 19 | // Based on grpc/encoding. 20 | type Compressor interface { 21 | encoding.Compressor 22 | } 23 | 24 | // CompressorGzip implements the Compressor interface. 25 | // Based on grpc/encoding/gzip. 26 | type CompressorGzip struct { 27 | Level *int 28 | poolCompressor sync.Pool 29 | poolDecompressor sync.Pool 30 | } 31 | 32 | // Name returns gzip. 33 | func (*CompressorGzip) Name() string { return "gzip" } 34 | 35 | type gzipWriter struct { 36 | *gzip.Writer 37 | pool *sync.Pool 38 | } 39 | 40 | // Compress implements the Compressor interface. 41 | func (c *CompressorGzip) Compress(w io.Writer) (io.WriteCloser, error) { 42 | z, ok := c.poolCompressor.Get().(*gzipWriter) 43 | if !ok { 44 | level := gzip.DefaultCompression 45 | if c.Level != nil { 46 | level = *c.Level 47 | } 48 | newZ, err := gzip.NewWriterLevel(w, level) 49 | if err != nil { 50 | return nil, err 51 | } 52 | return &gzipWriter{Writer: newZ, pool: &c.poolCompressor}, nil 53 | } 54 | z.Reset(w) 55 | return z, nil 56 | } 57 | 58 | func (z *gzipWriter) Close() error { 59 | defer z.pool.Put(z) 60 | return z.Writer.Close() 61 | } 62 | 63 | type gzipReader struct { 64 | *gzip.Reader 65 | pool *sync.Pool 66 | } 67 | 68 | // Decompress implements the Compressor interface. 69 | func (c *CompressorGzip) Decompress(r io.Reader) (io.Reader, error) { 70 | z, ok := c.poolDecompressor.Get().(*gzipReader) 71 | if !ok { 72 | newZ, err := gzip.NewReader(r) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return &gzipReader{Reader: newZ, pool: &c.poolDecompressor}, nil 77 | } 78 | if err := z.Reset(r); err != nil { 79 | z.pool.Put(z) 80 | return nil, err 81 | } 82 | return z, nil 83 | } 84 | 85 | func (z *gzipReader) Read(p []byte) (n int, err error) { 86 | n, err = z.Reader.Read(p) 87 | if err == io.EOF { 88 | z.pool.Put(z) 89 | } 90 | return n, err 91 | } 92 | -------------------------------------------------------------------------------- /larking/grpc_test.go: -------------------------------------------------------------------------------- 1 | package larking 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "net/http" 8 | "os" 9 | "testing" 10 | "time" 11 | 12 | "github.com/google/go-cmp/cmp" 13 | "golang.org/x/net/http2" 14 | "golang.org/x/net/http2/h2c" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/credentials/insecure" 17 | "google.golang.org/grpc/encoding" 18 | grpc_testing "google.golang.org/grpc/interop/grpc_testing" 19 | "google.golang.org/grpc/metadata" 20 | "google.golang.org/protobuf/testing/protocmp" 21 | ) 22 | 23 | func TestGRPC(t *testing.T) { 24 | // Create test server. 25 | ts := grpc_testing.UnimplementedTestServiceServer{} 26 | 27 | o := new(overrides) 28 | m, err := NewMux( 29 | UnaryServerInterceptorOption(o.unary()), 30 | StreamServerInterceptorOption(o.stream()), 31 | ) 32 | if err != nil { 33 | t.Fatalf("failed to create mux: %v", err) 34 | } 35 | grpc_testing.RegisterTestServiceServer(m, ts) 36 | 37 | index := http.HandlerFunc(m.serveGRPC) 38 | 39 | h2s := &http2.Server{} 40 | hs := &http.Server{ 41 | ReadTimeout: 10 * time.Second, 42 | WriteTimeout: 10 * time.Second, 43 | MaxHeaderBytes: 1 << 20, // 1 MB 44 | Handler: h2c.NewHandler(index, h2s), 45 | } 46 | if err := http2.ConfigureServer(hs, h2s); err != nil { 47 | t.Fatalf("failed to configure server: %v", err) 48 | } 49 | 50 | lis, err := net.Listen("tcp", "localhost:0") 51 | if err != nil { 52 | t.Fatalf("failed to listen: %v", err) 53 | } 54 | defer lis.Close() 55 | 56 | // Start server. 57 | go func() { 58 | if err := hs.Serve(lis); err != nil && err != http.ErrServerClosed { 59 | fmt.Println(err) 60 | os.Exit(1) 61 | } 62 | }() 63 | defer hs.Close() 64 | 65 | encoding.RegisterCompressor(&CompressorGzip{}) 66 | 67 | conns := []struct { 68 | name string 69 | opts []grpc.DialOption 70 | }{{ 71 | name: "insecure", 72 | opts: []grpc.DialOption{ 73 | grpc.WithTransportCredentials(insecure.NewCredentials()), 74 | grpc.WithBlock(), 75 | }, 76 | }, { 77 | name: "compressed", 78 | opts: []grpc.DialOption{ 79 | grpc.WithTransportCredentials(insecure.NewCredentials()), 80 | grpc.WithBlock(), 81 | grpc.WithDefaultCallOptions(grpc.UseCompressor("gzip")), 82 | }, 83 | }} 84 | 85 | // https://github.com/grpc/grpc/blob/master/src/proto/grpc/testing/test.proto 86 | tests := []struct { 87 | name string 88 | method string 89 | desc grpc.StreamDesc 90 | inouts []any 91 | }{{ 92 | name: "unary", 93 | method: "/grpc.testing.TestService/UnaryCall", 94 | desc: grpc.StreamDesc{}, 95 | inouts: []any{ 96 | in{ 97 | msg: &grpc_testing.SimpleRequest{ 98 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 99 | }, 100 | }, 101 | out{ 102 | msg: &grpc_testing.SimpleResponse{ 103 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 104 | }, 105 | }, 106 | }, 107 | }, { 108 | name: "client_streaming", 109 | method: "/grpc.testing.TestService/StreamingInputCall", 110 | desc: grpc.StreamDesc{ 111 | ClientStreams: true, 112 | }, 113 | inouts: []any{ 114 | in{ 115 | msg: &grpc_testing.StreamingInputCallRequest{ 116 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 117 | }, 118 | }, 119 | in{ 120 | msg: &grpc_testing.StreamingInputCallRequest{ 121 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 122 | }, 123 | }, 124 | out{ 125 | msg: &grpc_testing.StreamingInputCallResponse{ 126 | AggregatedPayloadSize: 2, 127 | }, 128 | }, 129 | }, 130 | }, { 131 | name: "server_streaming", 132 | method: "/grpc.testing.TestService/StreamingOutputCall", 133 | desc: grpc.StreamDesc{ 134 | ServerStreams: true, 135 | }, 136 | inouts: []any{ 137 | in{ 138 | msg: &grpc_testing.StreamingOutputCallRequest{ 139 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 140 | }, 141 | }, 142 | out{ 143 | msg: &grpc_testing.StreamingOutputCallResponse{ 144 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 145 | }, 146 | }, 147 | out{ 148 | msg: &grpc_testing.StreamingOutputCallResponse{ 149 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 150 | }, 151 | }, 152 | }, 153 | }, { 154 | name: "full_streaming", 155 | method: "/grpc.testing.TestService/FullDuplexCall", 156 | desc: grpc.StreamDesc{ 157 | ClientStreams: true, 158 | ServerStreams: true, 159 | }, 160 | inouts: []any{ 161 | in{ 162 | msg: &grpc_testing.StreamingOutputCallRequest{ 163 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 164 | }, 165 | }, 166 | out{ 167 | msg: &grpc_testing.StreamingOutputCallResponse{ 168 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 169 | }, 170 | }, 171 | in{ 172 | msg: &grpc_testing.StreamingOutputCallRequest{ 173 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 174 | }, 175 | }, 176 | out{ 177 | msg: &grpc_testing.StreamingOutputCallResponse{ 178 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 179 | }, 180 | }, 181 | }, 182 | }, { 183 | name: "half_streaming", 184 | method: "/grpc.testing.TestService/HalfDuplexCall", 185 | desc: grpc.StreamDesc{ 186 | ClientStreams: true, 187 | ServerStreams: true, 188 | }, 189 | inouts: []any{ 190 | in{ 191 | msg: &grpc_testing.StreamingOutputCallRequest{ 192 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 193 | }, 194 | }, 195 | in{ 196 | msg: &grpc_testing.StreamingOutputCallRequest{ 197 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 198 | }, 199 | }, 200 | out{ 201 | msg: &grpc_testing.StreamingOutputCallResponse{ 202 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 203 | }, 204 | }, 205 | out{ 206 | msg: &grpc_testing.StreamingOutputCallResponse{ 207 | Payload: &grpc_testing.Payload{Body: []byte{0}}, 208 | }, 209 | }, 210 | }, 211 | }, { 212 | name: "large_client_streaming", 213 | method: "/grpc.testing.TestService/StreamingInputCall", 214 | desc: grpc.StreamDesc{ 215 | ClientStreams: true, 216 | }, 217 | inouts: []any{ 218 | in{ 219 | msg: &grpc_testing.StreamingInputCallRequest{ 220 | Payload: &grpc_testing.Payload{Body: make([]byte, 1024)}, 221 | }, 222 | }, 223 | in{ 224 | msg: &grpc_testing.StreamingInputCallRequest{ 225 | Payload: &grpc_testing.Payload{Body: make([]byte, 1024)}, 226 | }, 227 | }, 228 | out{ 229 | msg: &grpc_testing.StreamingInputCallResponse{ 230 | AggregatedPayloadSize: 2, 231 | }, 232 | }, 233 | }, 234 | }} 235 | 236 | opts := cmp.Options{protocmp.Transform()} 237 | for _, tc := range conns { 238 | t.Run(tc.name, func(t *testing.T) { 239 | conn, err := grpc.Dial(lis.Addr().String(), tc.opts...) 240 | if err != nil { 241 | t.Fatalf("failed to dial: %v", err) 242 | } 243 | defer conn.Close() 244 | 245 | for _, tt := range tests { 246 | t.Run(tt.name, func(t *testing.T) { 247 | o.reset(t, "test", tt.inouts) 248 | 249 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) 250 | defer cancel() 251 | 252 | ctx = metadata.AppendToOutgoingContext(ctx, "test", tt.method) 253 | 254 | stream, err := conn.NewStream(ctx, &tt.desc, tt.method) 255 | if err != nil { 256 | t.Fatalf("failed to create stream: %v", err) 257 | } 258 | 259 | for i, inout := range tt.inouts { 260 | 261 | switch v := inout.(type) { 262 | case in: 263 | t.Logf("stream.SendMsg: %d", i) 264 | if err := stream.SendMsg(v.msg); err != nil { 265 | t.Fatalf("failed to send msg: %v", err) 266 | } 267 | case out: 268 | t.Logf("stream.RecvMsg: %d", i) 269 | want := v.msg 270 | got := v.msg.ProtoReflect().New().Interface() 271 | if err := stream.RecvMsg(got); err != nil { 272 | t.Fatalf("failed to recv msg: %v", err) 273 | } 274 | diff := cmp.Diff(got, want, opts...) 275 | if diff != "" { 276 | t.Error(diff) 277 | } 278 | } 279 | } 280 | }) 281 | } 282 | }) 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /larking/handler.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "fmt" 9 | "log" 10 | "reflect" 11 | 12 | "google.golang.org/grpc" 13 | "google.golang.org/grpc/metadata" 14 | "google.golang.org/protobuf/reflect/protoreflect" 15 | ) 16 | 17 | type handlerFunc func(*muxOptions, grpc.ServerStream) error 18 | 19 | type handler struct { 20 | desc protoreflect.MethodDescriptor 21 | handler handlerFunc 22 | method string // /Service/Method 23 | } 24 | 25 | // TODO: use grpclog? 26 | //var logger = grpclog.Component("core") 27 | 28 | // RegisterService satisfies grpc.ServiceRegistrar for generated service code hooks. 29 | func (m *Mux) RegisterService(sd *grpc.ServiceDesc, ss interface{}) { 30 | if ss != nil { 31 | ht := reflect.TypeOf(sd.HandlerType).Elem() 32 | st := reflect.TypeOf(ss) 33 | if !st.Implements(ht) { 34 | log.Fatalf("larking: RegisterService found the handler of type %v that does not satisfy %v", st, ht) 35 | } 36 | } 37 | if err := m.registerService(sd, ss); err != nil { 38 | log.Fatalf("larking: RegisterService error: %v", err) 39 | } 40 | } 41 | 42 | func (m *Mux) registerService(gsd *grpc.ServiceDesc, ss interface{}) error { 43 | 44 | // Load the state for writing. 45 | m.mu.Lock() 46 | defer m.mu.Unlock() 47 | s := m.loadState().clone() 48 | 49 | d, err := m.opts.files.FindDescriptorByName(protoreflect.FullName(gsd.ServiceName)) 50 | if err != nil { 51 | return err 52 | } 53 | sd, ok := d.(protoreflect.ServiceDescriptor) 54 | if !ok { 55 | return fmt.Errorf("invalid method descriptor %T", d) 56 | } 57 | mds := sd.Methods() 58 | 59 | findMethod := func(methodName string) (protoreflect.MethodDescriptor, error) { 60 | md := mds.ByName(protoreflect.Name(methodName)) 61 | if md == nil { 62 | return nil, fmt.Errorf("missing method descriptor for %v", methodName) 63 | } 64 | return md, nil 65 | } 66 | 67 | for i := range gsd.Methods { 68 | d := &gsd.Methods[i] 69 | method := "/" + gsd.ServiceName + "/" + d.MethodName 70 | 71 | md, err := findMethod(d.MethodName) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | h := &handler{ 77 | method: method, 78 | desc: md, 79 | handler: func(opts *muxOptions, stream grpc.ServerStream) error { 80 | ctx := stream.Context() 81 | 82 | // TODO: opts? 83 | reply, err := d.Handler(ss, ctx, stream.RecvMsg, opts.unaryInterceptor) 84 | if err != nil { 85 | return err 86 | } 87 | return stream.SendMsg(reply) 88 | }, 89 | } 90 | 91 | if err := s.appendHandler(m.opts, md, h); err != nil { 92 | return err 93 | } 94 | } 95 | for i := range gsd.Streams { 96 | d := &gsd.Streams[i] 97 | method := "/" + gsd.ServiceName + "/" + d.StreamName 98 | md, err := findMethod(d.StreamName) 99 | if err != nil { 100 | return err 101 | } 102 | 103 | h := &handler{ 104 | method: method, 105 | desc: md, 106 | handler: func(opts *muxOptions, stream grpc.ServerStream) error { 107 | info := &grpc.StreamServerInfo{ 108 | FullMethod: method, 109 | IsClientStream: d.ClientStreams, 110 | IsServerStream: d.ServerStreams, 111 | } 112 | 113 | return opts.stream(ss, stream, info, d.Handler) 114 | }, 115 | } 116 | if err := s.appendHandler(m.opts, md, h); err != nil { 117 | return err 118 | } 119 | } 120 | 121 | m.storeState(s) 122 | return nil 123 | } 124 | 125 | var _ grpc.ServerTransportStream = (*serverTransportStream)(nil) 126 | 127 | // serverTransportStream wraps gprc.SeverStream to support header/trailers. 128 | type serverTransportStream struct { 129 | grpc.ServerStream 130 | method string 131 | } 132 | 133 | func (s *serverTransportStream) Method() string { return s.method } 134 | func (s *serverTransportStream) SetHeader(md metadata.MD) error { 135 | return s.ServerStream.SetHeader(md) 136 | } 137 | func (s *serverTransportStream) SendHeader(md metadata.MD) error { 138 | return s.ServerStream.SendHeader(md) 139 | } 140 | func (s *serverTransportStream) SetTrailer(md metadata.MD) error { 141 | s.ServerStream.SetTrailer(md) 142 | return nil 143 | } 144 | -------------------------------------------------------------------------------- /larking/handler_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "io" 12 | "net/http" 13 | "net/http/httptest" 14 | "strings" 15 | "testing" 16 | 17 | "github.com/google/go-cmp/cmp" 18 | "google.golang.org/grpc" 19 | "google.golang.org/grpc/metadata" 20 | "google.golang.org/protobuf/proto" 21 | "google.golang.org/protobuf/testing/protocmp" 22 | "larking.io/api/testpb" 23 | ) 24 | 25 | func TestHandler(t *testing.T) { 26 | ms := &testpb.UnimplementedMessagingServer{} 27 | 28 | req := httptest.NewRequest(http.MethodPatch, "/v1/messages/msg_123", strings.NewReader( 29 | `{ "text": "Hi!" }`, 30 | )) 31 | req.Header.Set("Content-Type", "application/json") 32 | 33 | interceptor := func( 34 | ctx context.Context, 35 | req interface{}, 36 | info *grpc.UnaryServerInfo, 37 | _ grpc.UnaryHandler, 38 | ) (interface{}, error) { 39 | _, ok := metadata.FromIncomingContext(ctx) 40 | if !ok { 41 | return nil, fmt.Errorf("missing context metadata") 42 | } 43 | 44 | if info.FullMethod != "/larking.testpb.Messaging/UpdateMessage" { 45 | return nil, fmt.Errorf("invalid method %s", info.FullMethod) 46 | } 47 | 48 | in := req.(proto.Message) 49 | want := &testpb.UpdateMessageRequestOne{ 50 | MessageId: "msg_123", 51 | Message: &testpb.Message{ 52 | Text: "Hi!", 53 | }, 54 | } 55 | 56 | if !proto.Equal(want, in) { 57 | diff := cmp.Diff(in, want, protocmp.Transform()) 58 | t.Fatal(diff) 59 | return nil, fmt.Errorf("unexpected message") 60 | } 61 | return &testpb.Message{Text: "hello, patch!"}, nil 62 | } 63 | 64 | m, err := NewMux(UnaryServerInterceptorOption(interceptor)) 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | testpb.RegisterMessagingServer(m, ms) 69 | 70 | w := httptest.NewRecorder() 71 | 72 | m.ServeHTTP(w, req) 73 | t.Log(w) 74 | r := w.Result() 75 | if r.StatusCode != 200 { 76 | t.Fatal(r.Status) 77 | } 78 | 79 | data, err := io.ReadAll(r.Body) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | t.Log(string(data)) 84 | 85 | rsp := &testpb.Message{} 86 | if err := json.Unmarshal(data, rsp); err != nil { 87 | t.Fatal(err) 88 | } 89 | want := &testpb.Message{Text: "hello, patch!"} 90 | 91 | if !proto.Equal(want, rsp) { 92 | t.Fatal(cmp.Diff(rsp, want, protocmp.Transform())) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /larking/http_test.go: -------------------------------------------------------------------------------- 1 | package larking 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "math/rand" 8 | "net/http/httptest" 9 | "testing" 10 | 11 | "google.golang.org/genproto/googleapis/api/httpbody" 12 | "google.golang.org/grpc/codes" 13 | "google.golang.org/grpc/status" 14 | "larking.io/api/testpb" 15 | ) 16 | 17 | type asHTTPBodyServer struct { 18 | testpb.UnimplementedFilesServer 19 | } 20 | 21 | func (s *asHTTPBodyServer) UploadDownload(ctx context.Context, req *testpb.UploadFileRequest) (*httpbody.HttpBody, error) { 22 | return &httpbody.HttpBody{ 23 | ContentType: req.File.GetContentType(), 24 | Data: req.File.GetData(), 25 | }, nil 26 | } 27 | 28 | // LargeUploadDownload implements testpb.FilesServer 29 | // Echoes the request body as the response body. 30 | func (s *asHTTPBodyServer) LargeUploadDownload(stream testpb.Files_LargeUploadDownloadServer) error { 31 | var req testpb.UploadFileRequest 32 | r, err := AsHTTPBodyReader(stream, &req) 33 | if err != nil { 34 | return err 35 | } 36 | if req.File.Data != nil { 37 | return status.Error(codes.Internal, "unexpected data") 38 | } 39 | if req.File.ContentType != "image/jpeg" { 40 | return status.Error(codes.Internal, "unexpected content type") 41 | } 42 | if req.Filename != "cat.jpg" { 43 | return status.Error(codes.Internal, "unexpected filename") 44 | } 45 | 46 | rsp := &httpbody.HttpBody{ 47 | ContentType: req.File.GetContentType(), 48 | } 49 | w, err := AsHTTPBodyWriter(stream, rsp) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | n, err := io.Copy(w, r) 55 | if err != nil { 56 | return status.Errorf(codes.Internal, "copy error: %d %v", n, err) 57 | } 58 | if n == 0 { 59 | return status.Error(codes.Internal, "zero bytes read") 60 | } 61 | return err 62 | } 63 | 64 | func TestAsHTTPBody(t *testing.T) { 65 | // Create test server. 66 | ts := &asHTTPBodyServer{} 67 | 68 | m, err := NewMux() 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | testpb.RegisterFilesServer(m, ts) 73 | 74 | b := make([]byte, 1024*1024) 75 | if _, err := rand.Read(b); err != nil { 76 | t.Fatal(err) 77 | } 78 | body := bytes.NewReader(b) 79 | w := httptest.NewRecorder() 80 | r := httptest.NewRequest("POST", "/files/large/cat.jpg", body) 81 | r.Header.Set("Content-Type", "image/jpeg") 82 | m.ServeHTTP(w, r) 83 | 84 | if w.Code != 200 { 85 | t.Errorf("unexpected status: %d", w.Code) 86 | t.Log(w.Body.String()) 87 | return 88 | } 89 | if w.Header().Get("Content-Type") != "image/jpeg" { 90 | t.Errorf("unexpected content type: %s", w.Header().Get("Content-Type")) 91 | } 92 | if !bytes.Equal(b, w.Body.Bytes()) { 93 | t.Errorf("bytes not equal: %d != %d", len(b), len(w.Body.Bytes())) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /larking/lexer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "strings" 9 | "unicode" 10 | "unicode/utf8" 11 | 12 | "google.golang.org/grpc/codes" 13 | "google.golang.org/grpc/status" 14 | ) 15 | 16 | // ### Path template syntax 17 | // 18 | // Template = "/" Segments [ Verb ] ; 19 | // Segments = Segment { "/" Segment } ; 20 | // Segment = "*" | "**" | LITERAL | Variable ; 21 | // Variable = "{" FieldPath [ "=" Segments ] "}" ; 22 | // FieldPath = IDENT { "." IDENT } ; 23 | // Verb = ":" LITERAL ; 24 | 25 | type tokenType uint16 26 | 27 | const ( 28 | tokenError tokenType = 0 29 | tokenSlash tokenType = 1 << iota // / 30 | tokenStar // * 31 | tokenStarStar // ** 32 | tokenVariableStart // { 33 | tokenVariableEnd // } 34 | tokenEqual // = 35 | tokenIdent // a-z A-Z 0-9 - _ 36 | tokenLiteral // a-z A-Z 0-9 - _ . 37 | tokenDot // . 38 | tokenVerb // : 39 | tokenPath // a-z A-Z 0-9 - _ . ~ ! $ & ' ( ) * + , ; = @ 40 | tokenEOF 41 | ) 42 | 43 | type token struct { 44 | val string 45 | typ tokenType 46 | } 47 | 48 | type tokens []token 49 | 50 | func (toks tokens) String() string { 51 | var b strings.Builder 52 | for _, tok := range toks { 53 | b.WriteString(tok.val) 54 | } 55 | return b.String() 56 | } 57 | 58 | func (toks tokens) index(typ tokenType) int { 59 | for i, tok := range toks { 60 | if tok.typ == typ { 61 | return i 62 | } 63 | } 64 | return -1 65 | } 66 | 67 | func (toks tokens) indexAny(s tokenType) int { 68 | for i, tok := range toks { 69 | if s&tok.typ != 0 { 70 | return i 71 | } 72 | } 73 | return -1 74 | } 75 | 76 | type lexer struct { 77 | input string 78 | toks [64]token 79 | len int 80 | start int 81 | pos int 82 | width int 83 | } 84 | 85 | func (l *lexer) tokens() tokens { return l.toks[:l.len] } 86 | 87 | const eof = -1 88 | 89 | func (l *lexer) next() (r rune) { 90 | if l.pos >= len(l.input) { 91 | l.width = 0 92 | return eof 93 | } 94 | if c := l.input[l.pos]; c < utf8.RuneSelf { 95 | l.width = 1 96 | r = rune(c) 97 | } else { 98 | r, l.width = utf8.DecodeRuneInString(l.input[l.pos:]) 99 | } 100 | l.pos += l.width 101 | return r 102 | } 103 | 104 | func (l *lexer) current() (r rune) { 105 | if l.width == 0 { 106 | return 0 107 | } else if l.pos > l.width { 108 | r, _ = utf8.DecodeRuneInString(l.input[l.pos-l.width:]) 109 | } else { 110 | r, _ = utf8.DecodeRuneInString(l.input) 111 | } 112 | return r 113 | } 114 | 115 | func (l *lexer) backup() { 116 | l.pos -= l.width 117 | } 118 | 119 | func (l *lexer) acceptRun(isValid func(r rune) bool) int { 120 | var i int 121 | for isValid(l.next()) { 122 | i++ 123 | } 124 | l.backup() 125 | return i 126 | } 127 | 128 | var errTokenLimit = status.Errorf(codes.InvalidArgument, "path: too many tokens") 129 | 130 | func (l *lexer) emit(typ tokenType) error { 131 | if l.len >= len(l.toks) { 132 | return errTokenLimit 133 | } 134 | tok := token{typ: typ, val: l.input[l.start:l.pos]} 135 | 136 | l.toks[l.len] = tok 137 | l.len++ 138 | l.start = l.pos 139 | return nil 140 | } 141 | 142 | func (l *lexer) errUnexpected() error { 143 | if err := l.emit(tokenError); err != nil { 144 | return err 145 | } 146 | r := l.current() 147 | return status.Errorf(codes.InvalidArgument, "path: %v:%v unexpected rune %q", l.pos-l.width, l.pos, r) 148 | } 149 | func (l *lexer) errShort() error { 150 | if err := l.emit(tokenError); err != nil { 151 | return err 152 | } 153 | r := l.current() 154 | return status.Errorf(codes.InvalidArgument, "path: %v:%v short read %q", l.pos-l.width, l.pos, r) 155 | } 156 | 157 | func isIdent(r rune) bool { 158 | return unicode.IsLetter(r) || unicode.IsNumber(r) || r == '_' || r == '-' 159 | } 160 | 161 | func isLiteral(r rune) bool { 162 | return isIdent(r) || r == '.' 163 | } 164 | 165 | func isPath(r rune) bool { 166 | return isLiteral(r) || r == '~' || r == '!' || r == '$' || r == '&' || 167 | r == '\'' || r == '(' || r == ')' || r == '*' || r == '+' || 168 | r == ',' || r == ';' || r == '=' || r == '@' 169 | } 170 | 171 | func lexIdent(l *lexer) error { 172 | if i := l.acceptRun(isIdent); i == 0 { 173 | return l.errShort() 174 | } 175 | return l.emit(tokenIdent) 176 | } 177 | 178 | func lexLiteral(l *lexer) error { 179 | if i := l.acceptRun(isLiteral); i == 0 { 180 | return l.errShort() 181 | } 182 | return l.emit(tokenLiteral) 183 | } 184 | 185 | func lexFieldPath(l *lexer) error { 186 | if err := lexIdent(l); err != nil { 187 | return err 188 | } 189 | for { 190 | if r := l.next(); r != '.' { 191 | l.backup() // unknown 192 | return nil 193 | } 194 | if err := l.emit(tokenDot); err != nil { 195 | return err 196 | } 197 | if err := lexIdent(l); err != nil { 198 | return err 199 | } 200 | } 201 | } 202 | 203 | func lexVerb(l *lexer) error { 204 | if err := lexLiteral(l); err != nil { 205 | return err 206 | } 207 | if r := l.next(); r == eof { 208 | return l.emit(tokenEOF) 209 | } 210 | return l.errUnexpected() 211 | } 212 | 213 | func lexVariable(l *lexer) error { 214 | r := l.next() 215 | if r != '{' { 216 | return l.errUnexpected() 217 | } 218 | if err := l.emit(tokenVariableStart); err != nil { 219 | return err 220 | } 221 | if err := lexFieldPath(l); err != nil { 222 | return err 223 | } 224 | 225 | r = l.next() 226 | if r == '=' { 227 | if err := l.emit(tokenEqual); err != nil { 228 | return err 229 | } 230 | 231 | if err := lexSegments(l); err != nil { 232 | return err 233 | } 234 | r = l.next() 235 | } 236 | 237 | if r != '}' { 238 | return l.errUnexpected() 239 | } 240 | return l.emit(tokenVariableEnd) 241 | } 242 | 243 | func lexSegment(l *lexer) error { 244 | r := l.next() 245 | switch { 246 | case unicode.IsLetter(r): 247 | return lexLiteral(l) 248 | case r == '*': 249 | rn := l.next() 250 | if rn == '*' { 251 | return l.emit(tokenStarStar) 252 | } 253 | l.backup() 254 | return l.emit(tokenStar) 255 | case r == '{': 256 | l.backup() 257 | return lexVariable(l) 258 | default: 259 | return l.errUnexpected() 260 | } 261 | } 262 | 263 | func lexSegments(l *lexer) error { 264 | for { 265 | if err := lexSegment(l); err != nil { 266 | return err 267 | } 268 | if r := l.next(); r != '/' { 269 | l.backup() // unknown 270 | return nil 271 | } 272 | if err := l.emit(tokenSlash); err != nil { 273 | return err 274 | } 275 | } 276 | } 277 | 278 | func lexTemplate(l *lexer) error { 279 | if r := l.next(); r != '/' { 280 | return l.errUnexpected() 281 | } 282 | if err := l.emit(tokenSlash); err != nil { 283 | return err 284 | } 285 | if err := lexSegments(l); err != nil { 286 | return err 287 | } 288 | 289 | switch r := l.next(); r { 290 | case ':': 291 | if err := l.emit(tokenVerb); err != nil { 292 | return err 293 | } 294 | return lexVerb(l) 295 | case eof: 296 | if err := l.emit(tokenEOF); err != nil { 297 | return err 298 | } 299 | return nil 300 | default: 301 | return l.errUnexpected() 302 | } 303 | } 304 | 305 | func lexPathSegment(l *lexer) error { 306 | if i := l.acceptRun(isPath); i == 0 { 307 | return l.errShort() 308 | } 309 | return l.emit(tokenPath) 310 | } 311 | 312 | // lexPath emits all tokenSlash, tokenVerb and the rest as tokenPath 313 | func lexPath(l *lexer) error { 314 | for { 315 | switch r := l.next(); r { 316 | case '/': 317 | if err := l.emit(tokenSlash); err != nil { 318 | return err 319 | } 320 | if err := lexPathSegment(l); err != nil { 321 | return err 322 | } 323 | case ':': 324 | if err := l.emit(tokenVerb); err != nil { 325 | return err 326 | } 327 | if err := lexPathSegment(l); err != nil { 328 | return err 329 | } 330 | case eof: 331 | return l.emit(tokenEOF) 332 | default: 333 | return l.errUnexpected() 334 | } 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /larking/lexer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "testing" 9 | "unsafe" 10 | ) 11 | 12 | func TestLexer(t *testing.T) { 13 | tests := []struct { 14 | name string 15 | tmpl string 16 | want tokens 17 | wantErr bool 18 | }{{ 19 | name: "one", 20 | tmpl: "/v1/messages/{name=name/*}", 21 | want: tokens{ 22 | {typ: tokenSlash, val: "/"}, 23 | {typ: tokenLiteral, val: "v1"}, 24 | {typ: tokenSlash, val: "/"}, 25 | {typ: tokenLiteral, val: "messages"}, 26 | {typ: tokenSlash, val: "/"}, 27 | {typ: tokenVariableStart, val: "{"}, 28 | {typ: tokenIdent, val: "name"}, 29 | {typ: tokenEqual, val: "="}, 30 | {typ: tokenLiteral, val: "name"}, 31 | {typ: tokenSlash, val: "/"}, 32 | {typ: tokenStar, val: "*"}, 33 | {typ: tokenVariableEnd, val: "}"}, 34 | {typ: tokenEOF, val: ""}, 35 | }, 36 | }} 37 | 38 | for _, tt := range tests { 39 | t.Run(tt.name, func(t *testing.T) { 40 | 41 | l := &lexer{ 42 | input: tt.tmpl, 43 | } 44 | err := lexTemplate(l) 45 | if tt.wantErr { 46 | if err == nil { 47 | t.Error("wanted failure but succeeded") 48 | } 49 | return 50 | } 51 | if err != nil { 52 | t.Error(err) 53 | } 54 | if n, m := len(tt.want), len(l.tokens()); n != m { 55 | t.Errorf("mismatch length %v != %v:\n\t%v\n\t%v", n, m, tt.want, l.tokens()) 56 | return 57 | } 58 | for i, want := range tt.want { 59 | tok := l.toks[i] 60 | if want.typ != tok.typ || want.val != tok.val { 61 | t.Errorf("%d: %v != %v", i, tok, want) 62 | } 63 | } 64 | }) 65 | } 66 | } 67 | 68 | func BenchmarkLexer(b *testing.B) { 69 | var l lexer 70 | input := "/v1/books/1/shevles/1:read" 71 | 72 | b.ReportAllocs() 73 | b.ResetTimer() 74 | for i := 0; i < b.N; i++ { 75 | l = lexer{input: input} 76 | if err := lexPath(&l); err != nil { 77 | b.Fatal(err) 78 | } 79 | } 80 | b.StopTimer() 81 | if n := l.len; n != 13 { 82 | b.Errorf("expected %d tokens: %d", 7, n) 83 | } 84 | b.Logf("%v", unsafe.Sizeof(l)) 85 | } 86 | -------------------------------------------------------------------------------- /larking/log.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "context" 9 | 10 | "google.golang.org/grpc" 11 | ) 12 | 13 | type logStream struct { 14 | grpc.ServerStream 15 | info *grpc.StreamServerInfo 16 | ctxFn NewContextFunc 17 | } 18 | 19 | func (s logStream) Context() context.Context { 20 | return s.ctxFn(s.ServerStream.Context(), s.info.FullMethod, s.info.IsClientStream, s.info.IsServerStream) 21 | } 22 | 23 | // NewContextFunc is a function that creates a new context for a request. 24 | // The returned context is used for the duration of the request. 25 | type NewContextFunc func(ctx context.Context, fullMethod string, isClientStream, isServerStream bool) context.Context 26 | 27 | // NewUnaryContext returns a UnaryServerInterceptor that calls ctxFn to 28 | // create a new context for each request. 29 | func NewUnaryContext(ctxFn NewContextFunc) grpc.UnaryServerInterceptor { 30 | return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { 31 | ctx = ctxFn(ctx, info.FullMethod, false, false) 32 | return handler(ctx, req) 33 | } 34 | } 35 | 36 | // NewStreamContext returns a StreamServerInterceptor that calls ctxFn to 37 | // create a new context for each request. 38 | func NewStreamContext(ctxFn NewContextFunc) grpc.StreamServerInterceptor { 39 | return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { 40 | return handler(srv, logStream{ 41 | ServerStream: ss, 42 | info: info, 43 | ctxFn: ctxFn, 44 | }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /larking/mux_test.go: -------------------------------------------------------------------------------- 1 | package larking 2 | 3 | import ( 4 | "testing" 5 | 6 | "google.golang.org/genproto/googleapis/api/annotations" 7 | ) 8 | 9 | func TestRuleSelector(t *testing.T) { 10 | rule := &annotations.HttpRule{ 11 | Selector: "larking.LarkingService.Get", 12 | Pattern: &annotations.HttpRule_Get{ 13 | Get: "/v1/{name=projects/*/instances/*}", 14 | }, 15 | } 16 | healthzRule := &annotations.HttpRule{ 17 | Selector: "grpc.health.v1.Health.Check", 18 | Pattern: &annotations.HttpRule_Get{ 19 | Get: "/healthz", 20 | }, 21 | } 22 | wildcardRule := &annotations.HttpRule{ 23 | Selector: "wildcard.Service.*", 24 | Pattern: &annotations.HttpRule_Get{ 25 | Get: "/wildcard", 26 | }, 27 | } 28 | 29 | var hr ruleSelector 30 | hr.setRules([]*annotations.HttpRule{rule, healthzRule, wildcardRule}) 31 | 32 | t.Log(&hr) 33 | 34 | rules := hr.getRules("larking.LarkingService.Get") 35 | if rules == nil { 36 | t.Fatal("got nil") 37 | } 38 | got := rules[0] 39 | if got != rule { 40 | t.Fatalf("got %v, want %v", got, rule) 41 | } 42 | 43 | rules = hr.getRules("grpc.health.v1.Health.Check") 44 | if rules == nil { 45 | t.Fatal("got nil") 46 | } 47 | got = rules[0] 48 | if got != healthzRule { 49 | t.Fatalf("got %v, want %v", got, healthzRule) 50 | } 51 | 52 | rules = hr.getRules("wildcard.Service.Get.DeepMethod") 53 | if rules == nil { 54 | t.Fatal("got nil") 55 | } 56 | got = rules[0] 57 | if got != wildcardRule { 58 | t.Fatalf("got %v, want %v", got, wildcardRule) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /larking/negotiate.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // 3 | // Use of this source code is governed by a BSD-style 4 | // license that can be found in the LICENSE file or at 5 | // https://developers.google.com/open-source/licenses/bsd. 6 | 7 | package larking 8 | 9 | // Source: github.com/golang/gddo/httputil 10 | 11 | import ( 12 | "net/http" 13 | "strings" 14 | ) 15 | 16 | // Octet types from RFC 2616. 17 | var octetTypes [256]octetType 18 | 19 | type octetType byte 20 | 21 | const ( 22 | isToken octetType = 1 << iota 23 | isSpace 24 | ) 25 | 26 | func init() { 27 | // OCTET = 28 | // CHAR = 29 | // CTL = 30 | // CR = 31 | // LF = 32 | // SP = 33 | // HT = 34 | // <"> = 35 | // CRLF = CR LF 36 | // LWS = [CRLF] 1*( SP | HT ) 37 | // TEXT = 38 | // separators = "(" | ")" | "<" | ">" | "@" | "," | ";" | ":" | "\" | <"> 39 | // | "/" | "[" | "]" | "?" | "=" | "{" | "}" | SP | HT 40 | // token = 1* 41 | // qdtext = > 42 | 43 | for c := 0; c < 256; c++ { 44 | var t octetType 45 | isCtl := c <= 31 || c == 127 46 | isChar := 0 <= c && c <= 127 47 | isSeparator := strings.ContainsRune(" \t\"(),/:;<=>?@[]\\{}", rune(c)) 48 | if strings.ContainsRune(" \t\r\n", rune(c)) { 49 | t |= isSpace 50 | } 51 | if isChar && !isCtl && !isSeparator { 52 | t |= isToken 53 | } 54 | octetTypes[c] = t 55 | } 56 | } 57 | 58 | func expectQuality(s string) (q float64, rest string) { 59 | switch { 60 | case len(s) == 0: 61 | return -1, "" 62 | case s[0] == '0': 63 | q = 0 64 | case s[0] == '1': 65 | q = 1 66 | default: 67 | return -1, "" 68 | } 69 | s = s[1:] 70 | if !strings.HasPrefix(s, ".") { 71 | return q, s 72 | } 73 | s = s[1:] 74 | i := 0 75 | n := 0 76 | d := 1 77 | for ; i < len(s); i++ { 78 | b := s[i] 79 | if b < '0' || b > '9' { 80 | break 81 | } 82 | n = n*10 + int(b) - '0' 83 | d *= 10 84 | } 85 | return q + float64(n)/float64(d), s[i:] 86 | } 87 | 88 | func skipSpace(s string) (rest string) { 89 | i := 0 90 | for ; i < len(s); i++ { 91 | if octetTypes[s[i]]&isSpace == 0 { 92 | break 93 | } 94 | } 95 | return s[i:] 96 | } 97 | 98 | func expectTokenSlash(s string) (token, rest string) { 99 | i := 0 100 | for ; i < len(s); i++ { 101 | b := s[i] 102 | if (octetTypes[b]&isToken == 0) && b != '/' { 103 | break 104 | } 105 | } 106 | return s[:i], s[i:] 107 | } 108 | 109 | // acceptSpec describes an Accept* header. 110 | type acceptSpec struct { 111 | Value string 112 | Q float64 113 | } 114 | 115 | // parseAccept parses Accept* headers. 116 | func parseAccept(values []string) (specs []acceptSpec) { 117 | loop: 118 | for _, s := range values { 119 | for { 120 | var spec acceptSpec 121 | spec.Value, s = expectTokenSlash(s) 122 | if spec.Value == "" { 123 | continue loop 124 | } 125 | spec.Q = 1.0 126 | s = skipSpace(s) 127 | if strings.HasPrefix(s, ";") { 128 | s = skipSpace(s[1:]) 129 | if !strings.HasPrefix(s, "q=") { 130 | continue loop 131 | } 132 | spec.Q, s = expectQuality(s[2:]) 133 | if spec.Q < 0.0 { 134 | continue loop 135 | } 136 | } 137 | specs = append(specs, spec) 138 | s = skipSpace(s) 139 | if !strings.HasPrefix(s, ",") { 140 | continue loop 141 | } 142 | s = skipSpace(s[1:]) 143 | } 144 | } 145 | return 146 | } 147 | 148 | // negotiateContentEncoding returns the best offered content encoding for the 149 | // request's Accept-Encoding header. If two offers match with equal weight and 150 | // then the offer earlier in the list is preferred. If no offers are 151 | // acceptable, then "" is returned. 152 | func negotiateContentEncoding(header http.Header, offers []string) string { 153 | bestOffer := "identity" 154 | bestQ := -1.0 155 | specs := parseAccept(header["Accept-Encoding"]) 156 | for _, offer := range offers { 157 | for _, spec := range specs { 158 | if spec.Q > bestQ && 159 | (spec.Value == "*" || spec.Value == offer) { 160 | bestQ = spec.Q 161 | bestOffer = offer 162 | } 163 | } 164 | } 165 | if bestQ == 0 { 166 | bestOffer = "" 167 | } 168 | return bestOffer 169 | } 170 | 171 | // negotiateContentType returns the best offered content type for the request's 172 | // Accept header. If two offers match with equal weight, then the more specific 173 | // offer is preferred. For example, text/* trumps */*. If two offers match 174 | // with equal weight and specificity, then the offer earlier in the list is 175 | // preferred. If no offers match, then defaultOffer is returned. 176 | func negotiateContentType(header http.Header, offers []string, defaultOffer string) string { 177 | bestOffer := defaultOffer 178 | bestQ := -1.0 179 | bestWild := 3 180 | specs := parseAccept(header["Accept"]) 181 | for _, offer := range offers { 182 | for _, spec := range specs { 183 | switch { 184 | case spec.Q == 0.0: 185 | // ignore 186 | case spec.Q < bestQ: 187 | // better match found 188 | case spec.Value == "*/*": 189 | if spec.Q > bestQ || bestWild > 2 { 190 | bestQ = spec.Q 191 | bestWild = 2 192 | bestOffer = offer 193 | } 194 | case strings.HasSuffix(spec.Value, "/*"): 195 | if strings.HasPrefix(offer, spec.Value[:len(spec.Value)-1]) && 196 | (spec.Q > bestQ || bestWild > 1) { 197 | bestQ = spec.Q 198 | bestWild = 1 199 | bestOffer = offer 200 | } 201 | default: 202 | if spec.Value == offer && 203 | (spec.Q > bestQ || bestWild > 0) { 204 | bestQ = spec.Q 205 | bestWild = 0 206 | bestOffer = offer 207 | } 208 | } 209 | } 210 | } 211 | return bestOffer 212 | } 213 | -------------------------------------------------------------------------------- /larking/negotiate_test.go: -------------------------------------------------------------------------------- 1 | package larking 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | ) 7 | 8 | var negotiateContentEncodingTests = []struct { 9 | s string 10 | offers []string 11 | expect string 12 | }{ 13 | {"", []string{"identity", "gzip"}, "identity"}, 14 | {"*;q=0", []string{"identity", "gzip"}, ""}, 15 | {"gzip", []string{"identity", "gzip"}, "gzip"}, 16 | } 17 | 18 | func TestNegotiateContentEnoding(t *testing.T) { 19 | for _, tt := range negotiateContentEncodingTests { 20 | h := http.Header{"Accept-Encoding": {tt.s}} 21 | actual := negotiateContentEncoding(h, tt.offers) 22 | if actual != tt.expect { 23 | t.Errorf("NegotiateContentEncoding(%q, %#v)=%q, want %q", tt.s, tt.offers, actual, tt.expect) 24 | } 25 | } 26 | } 27 | 28 | var negotiateContentTypeTests = []struct { 29 | s string 30 | offers []string 31 | defaultOffer string 32 | expect string 33 | }{ 34 | {"application/proto", []string{"application/json", "application/proto"}, "application/json", "application/proto"}, 35 | {"text/html, */*;q=0", []string{"x/y"}, "", ""}, 36 | {"text/html, */*", []string{"x/y"}, "", "x/y"}, 37 | {"text/html, image/png", []string{"text/html", "image/png"}, "", "text/html"}, 38 | {"text/html, image/png", []string{"image/png", "text/html"}, "", "image/png"}, 39 | {"text/html, image/png; q=0.5", []string{"image/png"}, "", "image/png"}, 40 | {"text/html, image/png; q=0.5", []string{"text/html"}, "", "text/html"}, 41 | {"text/html, image/png; q=0.5", []string{"foo/bar"}, "", ""}, 42 | {"text/html, image/png; q=0.5", []string{"image/png", "text/html"}, "", "text/html"}, 43 | {"text/html, image/png; q=0.5", []string{"text/html", "image/png"}, "", "text/html"}, 44 | {"text/html;q=0.5, image/png", []string{"image/png"}, "", "image/png"}, 45 | {"text/html;q=0.5, image/png", []string{"text/html"}, "", "text/html"}, 46 | {"text/html;q=0.5, image/png", []string{"image/png", "text/html"}, "", "image/png"}, 47 | {"text/html;q=0.5, image/png", []string{"text/html", "image/png"}, "", "image/png"}, 48 | {"image/png, image/*;q=0.5", []string{"image/jpg", "image/png"}, "", "image/png"}, 49 | {"image/png, image/*;q=0.5", []string{"image/jpg"}, "", "image/jpg"}, 50 | {"image/png, image/*;q=0.5", []string{"image/jpg", "image/gif"}, "", "image/jpg"}, 51 | {"image/png, image/*", []string{"image/jpg", "image/gif"}, "", "image/jpg"}, 52 | {"image/png, image/*", []string{"image/gif", "image/jpg"}, "", "image/gif"}, 53 | {"image/png, image/*", []string{"image/gif", "image/png"}, "", "image/png"}, 54 | {"image/png, image/*", []string{"image/png", "image/gif"}, "", "image/png"}, 55 | } 56 | 57 | func TestNegotiateContentType(t *testing.T) { 58 | for _, tt := range negotiateContentTypeTests { 59 | h := http.Header{"Accept": {tt.s}} 60 | actual := negotiateContentType(h, tt.offers, tt.defaultOffer) 61 | if actual != tt.expect { 62 | t.Errorf("NegotiateContentType(%q, %#v, %q)=%q, want %q", tt.s, tt.offers, tt.defaultOffer, actual, tt.expect) 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /larking/proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "io" 9 | 10 | "google.golang.org/grpc" 11 | rpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha" 12 | "google.golang.org/protobuf/proto" 13 | "google.golang.org/protobuf/reflect/protodesc" 14 | "google.golang.org/protobuf/reflect/protoreflect" 15 | "google.golang.org/protobuf/reflect/protoregistry" 16 | ) 17 | 18 | // TODO: fetch type on a per stream basis 19 | type serverReflectionServer struct { 20 | rpb.UnimplementedServerReflectionServer 21 | m *Mux 22 | s *grpc.Server 23 | } 24 | 25 | // RegisterReflectionServer registers the server reflection service for multiple 26 | // proxied gRPC servers. Each individual reflection stream is merged to provide 27 | // a consistent view at the point of stream creation. 28 | func (m *Mux) RegisterReflectionServer(s *grpc.Server) { 29 | rpb.RegisterServerReflectionServer(s, &serverReflectionServer{ 30 | m: m, 31 | s: s, 32 | }) 33 | } 34 | 35 | // does marshalling on it and returns the marshalled result. 36 | // 37 | //nolint:unused // fileDescEncodingByFilename finds the file descriptor for given filename, 38 | func (s *serverReflectionServer) fileDescEncodingByFilename(name string) ([]byte, error) { 39 | fd, err := protoregistry.GlobalFiles.FindFileByPath(name) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return proto.Marshal(protodesc.ToFileDescriptorProto(fd)) 44 | } 45 | 46 | // does marshalling on it and returns the marshalled result. 47 | // The given symbol can be a type, a service or a method. 48 | // 49 | //nolint:unused // fileDescEncodingContainingSymbol finds the file descriptor containing the given symbol, 50 | func (s *serverReflectionServer) fileDescEncodingContainingSymbol(name string) ([]byte, error) { 51 | fullname := protoreflect.FullName(name) 52 | d, err := protoregistry.GlobalFiles.FindDescriptorByName(fullname) 53 | if err != nil { 54 | return nil, err 55 | } 56 | fd := d.ParentFile() 57 | return proto.Marshal(protodesc.ToFileDescriptorProto(fd)) 58 | } 59 | 60 | // does marshalling on it and returns the marshalled result. 61 | // 62 | //nolint:unused // fileDescEncodingContainingExtension finds the file descriptor containing given extension, 63 | func (s *serverReflectionServer) fileDescEncodingContainingExtension(typeName string, extNum int32) ([]byte, error) { 64 | fullname := protoreflect.FullName(typeName) 65 | fieldnumber := protoreflect.FieldNumber(extNum) 66 | ext, err := protoregistry.GlobalTypes.FindExtensionByNumber(fullname, fieldnumber) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | extd := ext.TypeDescriptor() 72 | d, err := protoregistry.GlobalFiles.FindDescriptorByName(extd.FullName()) 73 | if err != nil { 74 | return nil, err 75 | } 76 | fd := d.ParentFile() 77 | 78 | return proto.Marshal(protodesc.ToFileDescriptorProto(fd)) 79 | } 80 | 81 | //nolint:unused // allExtensionNumbersForTypeName returns all extension numbers for the given type. 82 | func (s *serverReflectionServer) allExtensionNumbersForTypeName(name string) ([]int32, error) { 83 | fullname := protoreflect.FullName(name) 84 | _, err := protoregistry.GlobalFiles.FindDescriptorByName(fullname) 85 | if err != nil { 86 | return nil, err 87 | } 88 | 89 | n := protoregistry.GlobalTypes.NumExtensionsByMessage(fullname) 90 | if n == 0 { 91 | return nil, nil 92 | } 93 | 94 | extNums := make([]int32, 0, n) 95 | protoregistry.GlobalTypes.RangeExtensionsByMessage( 96 | fullname, 97 | func(et protoreflect.ExtensionType) bool { 98 | ed := et.TypeDescriptor().Descriptor() 99 | extNums = append(extNums, int32(ed.Number())) 100 | return true 101 | }, 102 | ) 103 | return extNums, nil 104 | } 105 | 106 | // ServerReflectionInfo is the reflection service handler. 107 | func (s *serverReflectionServer) ServerReflectionInfo(stream rpb.ServerReflection_ServerReflectionInfoServer) error { 108 | 109 | //ss := s.m.loadState() 110 | 111 | for { 112 | in, err := stream.Recv() 113 | if err == io.EOF { 114 | return nil 115 | } 116 | if err != nil { 117 | return err 118 | } 119 | 120 | out := &rpb.ServerReflectionResponse{ 121 | ValidHost: in.Host, 122 | OriginalRequest: in, 123 | } 124 | /*switch req := in.MessageRequest.(type) { 125 | case *rpb.ServerReflectionRequest_FileByFilename: 126 | b, err := s.fileDescEncodingByFilename(req.FileByFilename) 127 | if err != nil { 128 | out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{ 129 | ErrorResponse: &rpb.ErrorResponse{ 130 | ErrorCode: int32(codes.NotFound), 131 | ErrorMessage: err.Error(), 132 | }, 133 | } 134 | } else { 135 | out.MessageResponse = &rpb.ServerReflectionResponse_FileDescriptorResponse{ 136 | FileDescriptorResponse: &rpb.FileDescriptorResponse{FileDescriptorProto: [][]byte{b}}, 137 | } 138 | } 139 | case *rpb.ServerReflectionRequest_FileContainingSymbol: 140 | b, err := s.fileDescEncodingContainingSymbol(req.FileContainingSymbol) 141 | if err != nil { 142 | out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{ 143 | ErrorResponse: &rpb.ErrorResponse{ 144 | ErrorCode: int32(codes.NotFound), 145 | ErrorMessage: err.Error(), 146 | }, 147 | } 148 | } else { 149 | out.MessageResponse = &rpb.ServerReflectionResponse_FileDescriptorResponse{ 150 | FileDescriptorResponse: &rpb.FileDescriptorResponse{FileDescriptorProto: [][]byte{b}}, 151 | } 152 | } 153 | case *rpb.ServerReflectionRequest_FileContainingExtension: 154 | typeName := req.FileContainingExtension.ContainingType 155 | extNum := req.FileContainingExtension.ExtensionNumber 156 | b, err := s.fileDescEncodingContainingExtension(typeName, extNum) 157 | if err != nil { 158 | out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{ 159 | ErrorResponse: &rpb.ErrorResponse{ 160 | ErrorCode: int32(codes.NotFound), 161 | ErrorMessage: err.Error(), 162 | }, 163 | } 164 | } else { 165 | out.MessageResponse = &rpb.ServerReflectionResponse_FileDescriptorResponse{ 166 | FileDescriptorResponse: &rpb.FileDescriptorResponse{FileDescriptorProto: [][]byte{b}}, 167 | } 168 | } 169 | case *rpb.ServerReflectionRequest_AllExtensionNumbersOfType: 170 | extNums, err := s.allExtensionNumbersForTypeName(req.AllExtensionNumbersOfType) 171 | if err != nil { 172 | out.MessageResponse = &rpb.ServerReflectionResponse_ErrorResponse{ 173 | ErrorResponse: &rpb.ErrorResponse{ 174 | ErrorCode: int32(codes.NotFound), 175 | ErrorMessage: err.Error(), 176 | }, 177 | } 178 | } else { 179 | out.MessageResponse = &rpb.ServerReflectionResponse_AllExtensionNumbersResponse{ 180 | AllExtensionNumbersResponse: &rpb.ExtensionNumberResponse{ 181 | BaseTypeName: req.AllExtensionNumbersOfType, 182 | ExtensionNumber: extNums, 183 | }, 184 | } 185 | } 186 | case *rpb.ServerReflectionRequest_ListServices: 187 | svcInfo := s.s.GetServiceInfo() 188 | serviceResponses := make([]*rpb.ServiceResponse, 0, len(svcInfo)) 189 | for svcName := range svcInfo { 190 | serviceResponses = append(serviceResponses, &rpb.ServiceResponse{ 191 | Name: svcName, 192 | }) 193 | } 194 | sort.Slice(serviceResponses, func(i, j int) bool { 195 | return serviceResponses[i].Name < serviceResponses[j].Name 196 | }) 197 | out.MessageResponse = &rpb.ServerReflectionResponse_ListServicesResponse{ 198 | ListServicesResponse: &rpb.ListServiceResponse{ 199 | Service: serviceResponses, 200 | }, 201 | } 202 | default: 203 | return status.Errorf(codes.InvalidArgument, "invalid MessageRequest: %v", in.MessageRequest) 204 | }*/ 205 | 206 | if err := stream.Send(out); err != nil { 207 | return err 208 | } 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /larking/proxy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "context" 9 | "net" 10 | "net/http" 11 | "testing" 12 | 13 | "github.com/google/go-cmp/cmp" 14 | "google.golang.org/genproto/googleapis/api/httpbody" 15 | "google.golang.org/grpc" 16 | "google.golang.org/grpc/credentials/insecure" 17 | "google.golang.org/grpc/metadata" 18 | "google.golang.org/grpc/reflection" 19 | "google.golang.org/protobuf/proto" 20 | "google.golang.org/protobuf/testing/protocmp" 21 | 22 | "golang.org/x/sync/errgroup" 23 | "larking.io/api/testpb" 24 | ) 25 | 26 | func TestGRPCProxy(t *testing.T) { 27 | // Create test server. 28 | ms := &testpb.UnimplementedMessagingServer{} 29 | fs := &testpb.UnimplementedFilesServer{} 30 | 31 | o := &overrides{} 32 | gs := grpc.NewServer(o.streamOption(), o.unaryOption()) 33 | testpb.RegisterMessagingServer(gs, ms) 34 | testpb.RegisterFilesServer(gs, fs) 35 | reflection.Register(gs) 36 | 37 | lis, err := net.Listen("tcp", "localhost:0") 38 | if err != nil { 39 | t.Fatalf("failed to listen: %v", err) 40 | } 41 | defer lis.Close() 42 | 43 | var g errgroup.Group 44 | defer func() { 45 | if err := g.Wait(); err != nil { 46 | if err != http.ErrServerClosed { 47 | t.Fatal(err) 48 | } 49 | } 50 | }() 51 | 52 | g.Go(func() error { 53 | return gs.Serve(lis) 54 | }) 55 | defer gs.Stop() 56 | 57 | // Create the client. 58 | conn, err := grpc.Dial( 59 | lis.Addr().String(), 60 | grpc.WithTransportCredentials(insecure.NewCredentials()), 61 | ) 62 | if err != nil { 63 | t.Fatalf("cannot connect to server: %v", err) 64 | } 65 | defer conn.Close() 66 | 67 | h, err := NewMux() 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | if err := h.RegisterConn(context.Background(), conn); err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | lisProxy, err := net.Listen("tcp", "localhost:0") 76 | if err != nil { 77 | t.Fatalf("failed to listen: %v", err) 78 | } 79 | defer lisProxy.Close() 80 | 81 | ts, err := NewServer(h) 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | g.Go(func() error { 87 | return ts.Serve(lisProxy) 88 | }) 89 | defer ts.Close() 90 | 91 | cc, err := grpc.Dial( 92 | lisProxy.Addr().String(), 93 | grpc.WithTransportCredentials(insecure.NewCredentials()), 94 | ) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | cmpOpts := cmp.Options{protocmp.Transform()} 100 | 101 | var unaryStreamDesc = &grpc.StreamDesc{ 102 | ClientStreams: false, 103 | ServerStreams: false, 104 | } 105 | 106 | tests := []struct { 107 | name string 108 | desc *grpc.StreamDesc 109 | method string 110 | inouts []interface{} 111 | }{{ 112 | name: "unary_message", 113 | desc: unaryStreamDesc, 114 | method: "/larking.testpb.Messaging/GetMessageOne", 115 | inouts: []interface{}{in{ 116 | msg: &testpb.GetMessageRequestOne{Name: "proxy"}, 117 | }, out{ 118 | msg: &testpb.Message{Text: "success"}, 119 | }}, 120 | }, { 121 | name: "stream_file", 122 | desc: &grpc.StreamDesc{ 123 | ClientStreams: true, 124 | ServerStreams: true, 125 | }, 126 | method: "/larking.testpb.Files/LargeUploadDownload", 127 | inouts: []interface{}{in{ 128 | method: "/larking.testpb.Files/LargeUploadDownload", 129 | msg: &testpb.UploadFileRequest{ 130 | Filename: "cat.jpg", 131 | File: &httpbody.HttpBody{ 132 | ContentType: "jpg", 133 | Data: []byte("cat"), 134 | }, 135 | }, 136 | }, in{ 137 | msg: &testpb.UploadFileRequest{ 138 | File: &httpbody.HttpBody{ 139 | Data: []byte("dog"), 140 | }, 141 | }, 142 | }, out{ 143 | msg: &httpbody.HttpBody{ 144 | Data: []byte("cat"), 145 | }, 146 | }, out{ 147 | msg: &httpbody.HttpBody{ 148 | Data: []byte("dog"), 149 | }, 150 | }}, 151 | }} 152 | 153 | for _, tt := range tests { 154 | t.Run(tt.name, func(t *testing.T) { 155 | o.reset(t, "test", tt.inouts) 156 | 157 | ctx := context.Background() 158 | ctx = metadata.AppendToOutgoingContext(ctx, "test", tt.method) 159 | 160 | s, err := cc.NewStream(ctx, tt.desc, tt.method) 161 | if err != nil { 162 | t.Fatal(err) 163 | } 164 | 165 | for i := 0; i < len(tt.inouts); i++ { 166 | switch typ := tt.inouts[i].(type) { 167 | case in: 168 | if err := s.SendMsg(typ.msg); err != nil { 169 | t.Fatal(err) 170 | } 171 | case out: 172 | out := proto.Clone(typ.msg) 173 | if err := s.RecvMsg(out); err != nil { 174 | t.Fatal(err) 175 | } 176 | diff := cmp.Diff(out, typ.msg, cmpOpts...) 177 | if diff != "" { 178 | t.Fatal(diff) 179 | } 180 | } 181 | } 182 | }) 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /larking/server.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "context" 9 | "crypto/tls" 10 | "fmt" 11 | "net/http" 12 | "os" 13 | "os/signal" 14 | "strings" 15 | "time" 16 | 17 | "golang.org/x/net/http2" 18 | "golang.org/x/net/http2/h2c" 19 | ) 20 | 21 | // NewOSSignalContext tries to gracefully handle OS closure. 22 | func NewOSSignalContext(ctx context.Context) (context.Context, func()) { 23 | // trap Ctrl+C and call cancel on the context 24 | ctx, cancel := context.WithCancel(ctx) 25 | c := make(chan os.Signal, 1) 26 | signal.Notify(c, os.Interrupt) 27 | go func() { 28 | select { 29 | case <-c: 30 | cancel() 31 | case <-ctx.Done(): 32 | } 33 | }() 34 | 35 | return ctx, func() { 36 | signal.Stop(c) 37 | cancel() 38 | } 39 | } 40 | 41 | // NewServer creates a new http.Server with http2 support. 42 | // The server is configured with the given options. 43 | // It is a convenience function for creating a new http.Server. 44 | func NewServer(mux *Mux, opts ...ServerOption) (*http.Server, error) { 45 | if mux == nil { 46 | return nil, fmt.Errorf("invalid mux must not be nil") 47 | } 48 | 49 | var svrOpts serverOptions 50 | for _, opt := range opts { 51 | if err := opt(&svrOpts); err != nil { 52 | return nil, err 53 | } 54 | } 55 | 56 | h := svrOpts.serveMux 57 | if h == nil { 58 | h = http.NewServeMux() 59 | } 60 | if len(svrOpts.muxPatterns) == 0 { 61 | svrOpts.muxPatterns = []string{"/"} 62 | } 63 | for _, pattern := range svrOpts.muxPatterns { 64 | prefix := strings.TrimSuffix(pattern, "/") 65 | if len(prefix) > 0 { 66 | h.Handle(prefix+"/", http.StripPrefix(prefix, mux)) 67 | } else { 68 | h.Handle("/", mux) 69 | } 70 | } 71 | 72 | h2s := &http2.Server{} 73 | hs := &http.Server{ 74 | ReadHeaderTimeout: 10 * time.Second, 75 | MaxHeaderBytes: 1 << 20, // 1 MB 76 | Handler: h2c.NewHandler(h, h2s), 77 | TLSConfig: svrOpts.tlsConfig, 78 | } 79 | if err := http2.ConfigureServer(hs, h2s); err != nil { 80 | return nil, err 81 | } 82 | return hs, nil 83 | } 84 | 85 | type serverOptions struct { 86 | tlsConfig *tls.Config 87 | serveMux *http.ServeMux 88 | muxPatterns []string 89 | } 90 | 91 | // ServerOption is similar to grpc.ServerOption. 92 | type ServerOption func(*serverOptions) error 93 | 94 | func TLSCredsOption(c *tls.Config) ServerOption { 95 | return func(opts *serverOptions) error { 96 | opts.tlsConfig = c 97 | return nil 98 | } 99 | } 100 | 101 | func MuxHandleOption(patterns ...string) ServerOption { 102 | return func(opts *serverOptions) error { 103 | if opts.muxPatterns != nil { 104 | return fmt.Errorf("duplicate mux patterns registered") 105 | } 106 | opts.muxPatterns = patterns 107 | return nil 108 | } 109 | } 110 | 111 | func HTTPHandlerOption(pattern string, handler http.Handler) ServerOption { 112 | return func(opts *serverOptions) error { 113 | if opts.serveMux == nil { 114 | opts.serveMux = http.NewServeMux() 115 | } 116 | opts.serveMux.Handle(pattern, handler) 117 | return nil 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /larking/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "time" 9 | 10 | "google.golang.org/grpc/stats" 11 | ) 12 | 13 | const ( 14 | payloadLen = 1 15 | sizeLen = 4 16 | headerLen = payloadLen + sizeLen 17 | ) 18 | 19 | func outPayload(client bool, msg interface{}, payload []byte, t time.Time) *stats.OutPayload { 20 | return &stats.OutPayload{ 21 | Client: client, 22 | Payload: msg, 23 | Length: len(payload), 24 | WireLength: len(payload) + headerLen, 25 | SentTime: t, 26 | } 27 | } 28 | 29 | func inPayload(client bool, msg interface{}, payload []byte, t time.Time) *stats.InPayload { 30 | return &stats.InPayload{ 31 | Client: true, 32 | RecvTime: t, 33 | Payload: msg, 34 | Length: len(payload), 35 | WireLength: len(payload) + headerLen, 36 | } 37 | } 38 | 39 | // strAddr is a net.Addr backed by either a TCP "ip:port" string, or 40 | // the empty string if unknown. 41 | type strAddr string 42 | 43 | func (a strAddr) Network() string { 44 | if a != "" { 45 | // Per the documentation on net/http.Request.RemoteAddr, if this is 46 | // set, it's set to the IP:port of the peer (hence, TCP): 47 | // https://golang.org/pkg/net/http/#Request 48 | // 49 | // If we want to support Unix sockets later, we can 50 | // add our own grpc-specific convention within the 51 | // grpc codebase to set RemoteAddr to a different 52 | // format, or probably better: we can attach it to the 53 | // context and use that from serverHandlerTransport.RemoteAddr. 54 | return "tcp" 55 | } 56 | return "" 57 | } 58 | func (a strAddr) String() string { return string(a) } 59 | -------------------------------------------------------------------------------- /larking/web.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | // Support for gRPC-web 8 | // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md 9 | // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md 10 | 11 | import ( 12 | "bytes" 13 | "encoding/base64" 14 | "encoding/binary" 15 | "fmt" 16 | "io" 17 | "net/http" 18 | "strings" 19 | ) 20 | 21 | const ( 22 | grpcBase = "application/grpc" 23 | grpcWeb = "application/grpc-web" 24 | grpcWebText = "application/grpc-web-text" 25 | ) 26 | 27 | // isWebRequest checks for gRPC Web headers. 28 | func isWebRequest(r *http.Request) (typ string, enc string, ok bool) { 29 | ct := r.Header.Get("Content-Type") 30 | if !strings.HasPrefix(ct, "application/grpc-web") || r.Method != http.MethodPost { 31 | return typ, enc, false 32 | } 33 | typ, enc, ok = strings.Cut(ct, "+") 34 | if !ok { 35 | enc = "proto" 36 | } 37 | ok = typ == grpcWeb || typ == grpcWebText 38 | return typ, enc, ok 39 | } 40 | 41 | type webWriter struct { 42 | w http.ResponseWriter 43 | resp io.Writer 44 | seenHeaders map[string]bool 45 | typ string // grpcWeb or grpcWebText 46 | enc string // proto or json 47 | wroteHeader bool 48 | wroteResp bool 49 | } 50 | 51 | func newWebWriter(w http.ResponseWriter, typ, enc string) *webWriter { 52 | var resp io.Writer = w 53 | if typ == grpcWebText { 54 | resp = base64.NewEncoder(base64.StdEncoding, resp) 55 | 56 | } 57 | 58 | return &webWriter{ 59 | w: w, 60 | typ: typ, 61 | enc: enc, 62 | resp: resp, 63 | } 64 | } 65 | 66 | func (w *webWriter) seeHeaders() { 67 | hdr := w.Header() 68 | hdr.Set("Content-Type", w.typ+"+"+w.enc) // override content-type 69 | 70 | keys := make(map[string]bool, len(hdr)) 71 | for k := range hdr { 72 | if strings.HasPrefix(k, http.TrailerPrefix) { 73 | continue 74 | } 75 | keys[k] = true 76 | } 77 | w.seenHeaders = keys 78 | w.wroteHeader = true 79 | } 80 | 81 | func (w *webWriter) Write(b []byte) (int, error) { 82 | if !w.wroteHeader { 83 | w.seeHeaders() 84 | } 85 | return w.resp.Write(b) 86 | } 87 | 88 | func (w *webWriter) Header() http.Header { return w.w.Header() } 89 | 90 | func (w *webWriter) WriteHeader(code int) { 91 | w.seeHeaders() 92 | w.w.WriteHeader(code) 93 | } 94 | 95 | func (w *webWriter) Flush() { 96 | if w.wroteHeader || w.wroteResp { 97 | if f, ok := w.w.(http.Flusher); ok { 98 | f.Flush() 99 | } 100 | } 101 | } 102 | 103 | func (w *webWriter) writeTrailer() error { 104 | hdr := w.Header() 105 | 106 | tr := make(http.Header, len(hdr)-len(w.seenHeaders)+1) 107 | for key, val := range hdr { 108 | if w.seenHeaders[key] { 109 | continue 110 | } 111 | key = strings.TrimPrefix(key, http.TrailerPrefix) 112 | // https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md#protocol-differences-vs-grpc-over-http2 113 | tr[strings.ToLower(key)] = val 114 | } 115 | 116 | var buf bytes.Buffer 117 | if err := tr.Write(&buf); err != nil { 118 | return err 119 | } 120 | 121 | head := []byte{1 << 7, 0, 0, 0, 0} // MSB=1 indicates this is a trailer data frame. 122 | binary.BigEndian.PutUint32(head[1:5], uint32(buf.Len())) 123 | if _, err := w.Write(head); err != nil { 124 | return err 125 | } 126 | if _, err := w.Write(buf.Bytes()); err != nil { 127 | return err 128 | } 129 | return nil 130 | } 131 | 132 | func (w *webWriter) flushWithTrailer() { 133 | // Write trailers only if message has been sent. 134 | if w.wroteHeader || w.wroteResp { 135 | if err := w.writeTrailer(); err != nil { 136 | return // nothing 137 | } 138 | } 139 | w.Flush() 140 | } 141 | 142 | type readCloser struct { 143 | io.Reader 144 | io.Closer 145 | } 146 | 147 | func (m *Mux) serveGRPCWeb(w http.ResponseWriter, r *http.Request) { 148 | typ, enc, ok := isWebRequest(r) 149 | if !ok { 150 | msg := fmt.Sprintf("invalid gRPC-Web content type: %v", r.Header.Get("Content-Type")) 151 | http.Error(w, msg, http.StatusBadRequest) 152 | return 153 | } 154 | // TODO: Check for websocket request and upgrade. 155 | if strings.EqualFold(r.Header.Get("Upgrade"), "websocket") { 156 | http.Error(w, "unimplemented websocket support", http.StatusInternalServerError) 157 | return 158 | } 159 | 160 | r.ProtoMajor = 2 161 | r.ProtoMinor = 0 162 | 163 | hdr := r.Header 164 | hdr.Del("Content-Length") 165 | hdr.Set("Content-Type", grpcBase+"+"+enc) 166 | 167 | if typ == grpcWebText { 168 | body := base64.NewDecoder(base64.StdEncoding, r.Body) 169 | r.Body = readCloser{body, r.Body} 170 | } 171 | 172 | ww := newWebWriter(w, typ, enc) 173 | m.serveGRPC(ww, r) 174 | ww.flushWithTrailer() 175 | } 176 | -------------------------------------------------------------------------------- /larking/web_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 Edward McFarlane. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package larking 6 | 7 | import ( 8 | "bytes" 9 | "encoding/binary" 10 | "io" 11 | "net/http" 12 | "net/http/httptest" 13 | "testing" 14 | 15 | "github.com/google/go-cmp/cmp" 16 | "google.golang.org/genproto/googleapis/rpc/status" 17 | "google.golang.org/protobuf/encoding/protojson" 18 | "google.golang.org/protobuf/proto" 19 | "google.golang.org/protobuf/testing/protocmp" 20 | "larking.io/api/testpb" 21 | ) 22 | 23 | func TestWeb(t *testing.T) { 24 | 25 | // Create test server. 26 | ms := &testpb.UnimplementedMessagingServer{} 27 | 28 | o := new(overrides) 29 | m, err := NewMux( 30 | UnaryServerInterceptorOption(o.unary()), 31 | StreamServerInterceptorOption(o.stream()), 32 | ) 33 | if err != nil { 34 | t.Fatalf("failed to create mux: %v", err) 35 | } 36 | 37 | testpb.RegisterMessagingServer(m, ms) 38 | 39 | type want struct { 40 | msg proto.Message 41 | statusCode int 42 | } 43 | 44 | frame := func(b []byte, msb uint8) []byte { 45 | head := append([]byte{0 | msb, 0, 0, 0, 0}, b...) 46 | binary.BigEndian.PutUint32(head[1:5], uint32(len(b))) 47 | return head 48 | } 49 | deframe := func(b []byte) ([]byte, []byte) { 50 | if len(b) < 5 { 51 | t.Errorf("invalid deframe") 52 | return nil, nil 53 | } 54 | x := int(binary.BigEndian.Uint32(b[1:5])) 55 | b = b[5:] 56 | return b[:x], b[x:] 57 | } 58 | 59 | // TODO: compare http.Response output 60 | tests := []struct { 61 | in in 62 | out out 63 | want want 64 | req *http.Request 65 | name string 66 | }{{ 67 | name: "unary proto request", 68 | req: func() *http.Request { 69 | msg := &testpb.GetMessageRequestOne{Name: "name/hello"} 70 | b, err := proto.Marshal(msg) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | 75 | body := bytes.NewReader(frame(b, 0)) 76 | req := httptest.NewRequest(http.MethodPost, "/larking.testpb.Messaging/GetMessageOne", body) 77 | req.Header.Set("Content-Type", grpcWeb+"+proto") 78 | return req 79 | }(), 80 | in: in{ 81 | method: "/larking.testpb.Messaging/GetMessageOne", 82 | msg: &testpb.GetMessageRequestOne{Name: "name/hello"}, 83 | }, 84 | out: out{ 85 | msg: &testpb.Message{Text: "hello, world!"}, 86 | }, 87 | want: want{ 88 | statusCode: 200, 89 | msg: &testpb.Message{Text: "hello, world!"}, 90 | }, 91 | }} 92 | 93 | opts := cmp.Options{protocmp.Transform()} 94 | for _, tt := range tests { 95 | t.Run(tt.name, func(t *testing.T) { 96 | o.reset(t, "test", []interface{}{tt.in, tt.out}) 97 | 98 | req := tt.req 99 | req.Header["test"] = []string{tt.in.method} 100 | 101 | w := httptest.NewRecorder() 102 | m.ServeHTTP(w, req) 103 | resp := w.Result() 104 | 105 | t.Log("resp", resp) 106 | 107 | b, err := io.ReadAll(resp.Body) 108 | if err != nil { 109 | t.Fatal(err) 110 | } 111 | t.Logf("resp length: %d", len(b)) 112 | 113 | if sc := tt.want.statusCode; sc != resp.StatusCode { 114 | t.Errorf("expected %d got %d", tt.want.statusCode, resp.StatusCode) 115 | var msg status.Status 116 | if err := protojson.Unmarshal(b, &msg); err != nil { 117 | t.Error(err, string(b)) 118 | return 119 | } 120 | t.Error("status.code", msg.Code) 121 | t.Error("status.message", msg.Message) 122 | return 123 | } 124 | 125 | //if tt.want.body != nil { 126 | // if !bytes.Equal(b, tt.want.body) { 127 | // t.Errorf("length %d != %d", len(tt.want.body), len(b)) 128 | // t.Errorf("body %s != %s", tt.want.body, b) 129 | // } 130 | //} 131 | if tt.want.msg != nil { 132 | b, _ := deframe(b) 133 | msg := proto.Clone(tt.want.msg) 134 | if err := proto.Unmarshal(b, msg); err != nil { 135 | t.Errorf("%v: %X", err, b) 136 | return 137 | } 138 | diff := cmp.Diff(msg, tt.want.msg, opts...) 139 | if diff != "" { 140 | t.Error(diff) 141 | } 142 | } 143 | }) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /larking/websocket.go: -------------------------------------------------------------------------------- 1 | package larking 2 | 3 | import ( 4 | "context" 5 | "net" 6 | 7 | "github.com/gobwas/ws" 8 | "github.com/gobwas/ws/wsutil" 9 | "google.golang.org/grpc" 10 | "google.golang.org/grpc/metadata" 11 | "google.golang.org/protobuf/encoding/protojson" 12 | "google.golang.org/protobuf/proto" 13 | ) 14 | 15 | const kindWebsocket = "WEBSOCKET" 16 | 17 | type streamWS struct { 18 | ctx context.Context 19 | conn net.Conn 20 | method *method 21 | header metadata.MD 22 | trailer metadata.MD 23 | params params 24 | recvN int 25 | sendN int 26 | sentHeader bool 27 | } 28 | 29 | func (s *streamWS) SetHeader(md metadata.MD) error { 30 | if !s.sentHeader { 31 | s.header = metadata.Join(s.header, md) 32 | } 33 | return nil 34 | 35 | } 36 | func (s *streamWS) SendHeader(md metadata.MD) error { 37 | if s.sentHeader { 38 | return nil // already sent? 39 | } 40 | // TODO: headers? 41 | s.sentHeader = true 42 | return nil 43 | } 44 | 45 | func (s *streamWS) SetTrailer(md metadata.MD) { 46 | s.sentHeader = true 47 | s.trailer = metadata.Join(s.trailer, md) 48 | } 49 | 50 | func (s *streamWS) Context() context.Context { 51 | sts := &serverTransportStream{s, s.method.name} 52 | return grpc.NewContextWithServerTransportStream(s.ctx, sts) 53 | } 54 | 55 | func (s *streamWS) SendMsg(v interface{}) error { 56 | s.sendN += 1 57 | reply := v.(proto.Message) 58 | //ctx := s.ctx 59 | 60 | cur := reply.ProtoReflect() 61 | for _, fd := range s.method.resp { 62 | cur = cur.Mutable(fd).Message() 63 | } 64 | msg := cur.Interface() 65 | 66 | // TODO: contentType check? 67 | b, err := protojson.Marshal(msg) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | if err := wsutil.WriteServerMessage(s.conn, ws.OpText, b); err != nil { 73 | return err 74 | } 75 | return nil 76 | } 77 | 78 | func (s *streamWS) RecvMsg(m interface{}) error { 79 | s.recvN += 1 80 | args := m.(proto.Message) 81 | 82 | if s.method.hasBody { 83 | cur := args.ProtoReflect() 84 | for _, fd := range s.method.body { 85 | cur = cur.Mutable(fd).Message() 86 | } 87 | 88 | msg := cur.Interface() 89 | 90 | b, _, err := wsutil.ReadClientData(s.conn) 91 | if err != nil { 92 | return err 93 | } 94 | 95 | // TODO: contentType check? 96 | // What marshalling options should we support? 97 | if err := protojson.Unmarshal(b, msg); err != nil { 98 | return err 99 | } 100 | } 101 | 102 | if s.recvN == 1 { 103 | if err := s.params.set(args); err != nil { 104 | return err 105 | } 106 | } 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /larking/websocket_test.go: -------------------------------------------------------------------------------- 1 | package larking 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "net" 8 | "net/http" 9 | "testing" 10 | "time" 11 | 12 | "github.com/gobwas/ws" 13 | "github.com/gobwas/ws/wsutil" 14 | "github.com/google/go-cmp/cmp" 15 | "golang.org/x/sync/errgroup" 16 | "google.golang.org/grpc" 17 | "google.golang.org/protobuf/encoding/protojson" 18 | "google.golang.org/protobuf/proto" 19 | "google.golang.org/protobuf/testing/protocmp" 20 | "larking.io/api/testpb" 21 | ) 22 | 23 | type wsHeader http.Header 24 | 25 | func (h wsHeader) WriteTo(w io.Writer) (int64, error) { 26 | var buf bytes.Buffer 27 | if err := http.Header(h).Write(&buf); err != nil { 28 | return 0, err 29 | } 30 | return io.Copy(w, &buf) 31 | } 32 | 33 | func TestWebsocket(t *testing.T) { 34 | // Create test server. 35 | fs := &testpb.UnimplementedChatRoomServer{} 36 | o := &overrides{} 37 | 38 | var g errgroup.Group 39 | defer func() { 40 | if err := g.Wait(); err != nil { 41 | t.Fatal(err) 42 | } 43 | }() 44 | mux, err := NewMux( 45 | UnaryServerInterceptorOption(o.unary()), 46 | StreamServerInterceptorOption(o.stream()), 47 | ) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | mux.RegisterService(&testpb.ChatRoom_ServiceDesc, fs) 52 | 53 | s, err := NewServer(mux) 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | lis, err := net.Listen("tcp", "localhost:0") 59 | if err != nil { 60 | t.Fatalf("failed to listen: %v", err) 61 | } 62 | defer lis.Close() 63 | 64 | g.Go(func() (err error) { 65 | if err := s.Serve(lis); err != nil && err != http.ErrServerClosed { 66 | return err 67 | } 68 | return nil 69 | }) 70 | defer func() { 71 | if err := s.Shutdown(context.Background()); err != nil { 72 | t.Fatal(err) 73 | } 74 | }() 75 | 76 | cmpOpts := cmp.Options{protocmp.Transform()} 77 | var unaryStreamDesc = &grpc.StreamDesc{ 78 | ClientStreams: false, 79 | ServerStreams: false, 80 | } 81 | 82 | tests := []struct { 83 | name string 84 | desc *grpc.StreamDesc 85 | path string 86 | method string 87 | client []interface{} 88 | server []interface{} 89 | }{{ 90 | name: "unary", 91 | desc: unaryStreamDesc, 92 | path: "/v1/rooms/chat", 93 | method: "/larking.testpb.ChatRoom/Chat", 94 | client: []interface{}{ 95 | in{ 96 | msg: &testpb.ChatMessage{ 97 | Text: "hello", 98 | }, 99 | }, 100 | out{ 101 | msg: &testpb.ChatMessage{ 102 | Text: "world", 103 | }, 104 | }, 105 | }, 106 | server: []interface{}{ 107 | in{ 108 | msg: &testpb.ChatMessage{ 109 | Name: "rooms/chat", // name added from URL path 110 | Text: "hello", 111 | }, 112 | }, 113 | out{ 114 | msg: &testpb.ChatMessage{ 115 | Text: "world", 116 | }, 117 | }, 118 | }, 119 | }} 120 | 121 | for _, tt := range tests { 122 | t.Run(tt.name, func(t *testing.T) { 123 | o.reset(t, "test", tt.server) 124 | 125 | ctx, cancel := context.WithTimeout(testContext(t), time.Minute) 126 | defer cancel() 127 | 128 | urlStr := "ws://" + lis.Addr().String() + tt.path 129 | 130 | // TODO: protocols and headers. 131 | conn, _, _, err := ws.Dialer{ 132 | Header: wsHeader( 133 | map[string][]string{ 134 | "test": {tt.method}, 135 | }, 136 | ), 137 | }.Dial(ctx, urlStr) 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | defer conn.Close() 142 | 143 | for i := 0; i < len(tt.client); i++ { 144 | switch typ := tt.client[i].(type) { 145 | case in: 146 | b, err := protojson.Marshal(typ.msg) 147 | if err != nil { 148 | t.Fatal(err) 149 | } 150 | if err := wsutil.WriteClientMessage(conn, ws.OpText, b); err != nil { 151 | t.Fatal(err) 152 | } 153 | t.Log("write", string(b)) 154 | 155 | case out: 156 | b, op, err := wsutil.ReadServerData(conn) 157 | if err != nil { 158 | t.Fatal(op, err) 159 | } 160 | t.Log("read", string(b)) 161 | 162 | out := proto.Clone(typ.msg) 163 | if err := protojson.Unmarshal(b, out); err != nil { 164 | t.Fatal(err) 165 | } 166 | diff := cmp.Diff(out, typ.msg, cmpOpts...) 167 | if diff != "" { 168 | t.Fatal(diff) 169 | } 170 | } 171 | } 172 | }) 173 | } 174 | } 175 | --------------------------------------------------------------------------------