├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── release.yml │ └── test.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── benchmarks_test.go ├── codec.go ├── codec_json.go ├── codec_json_jsoniter.go ├── codec_msgpack.go ├── codec_pb.go ├── codec_test.go ├── codecov.yml ├── go.mod ├── go.sum ├── internal ├── examples │ ├── fixture │ │ ├── basic.go │ │ └── middleware.go │ └── tcp │ │ ├── broadcast │ │ ├── client │ │ │ └── main.go │ │ ├── common │ │ │ └── message.go │ │ └── server │ │ │ └── main.go │ │ ├── custom_packet │ │ ├── client │ │ │ └── main.go │ │ ├── common │ │ │ ├── message.go │ │ │ ├── packer.go │ │ │ └── packer_test.go │ │ └── server │ │ │ └── main.go │ │ ├── proto_packet │ │ ├── client │ │ │ └── main.go │ │ ├── common │ │ │ ├── make.sh │ │ │ ├── message.pb.go │ │ │ ├── message.proto │ │ │ ├── packer.go │ │ │ └── packer_test.go │ │ └── server │ │ │ └── main.go │ │ ├── simple │ │ ├── client │ │ │ └── main.go │ │ ├── common │ │ │ └── message.go │ │ └── server │ │ │ └── main.go │ │ └── simple_tls │ │ ├── client │ │ └── main.go │ │ ├── common │ │ └── message.go │ │ └── server │ │ └── main.go ├── mock │ ├── codec_mock.go │ └── server_mock.go └── test_data │ ├── certificates │ ├── cert.key │ └── cert.pem │ ├── msgpack │ └── sample.go │ └── pb │ ├── make.sh │ ├── test.pb.go │ └── test.proto ├── logger.go ├── logger_test.go ├── message.go ├── message_test.go ├── packer.go ├── packer_mock.go ├── packer_test.go ├── router.go ├── router_context.go ├── router_context_test.go ├── router_test.go ├── server.go ├── server_test.go ├── session.go └── session_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | *.{cmd,[cC][mM][dD]} text eol=crlf 3 | *.{bat,[bB][aA][tT]} text eol=crlf 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [DarthPestilane] 4 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | environment: 12 | # release manually 13 | name: release 14 | steps: 15 | - name: Checkout Code 16 | uses: actions/checkout@v4 17 | 18 | - name: Run GoReleaser 19 | uses: goreleaser/goreleaser-action@v5 20 | if: success() && startsWith(github.ref, 'refs/tags/') 21 | with: 22 | version: latest 23 | args: release --rm-dist --skip-announce --timeout=2m 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GORELEASER_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | lint: 13 | env: 14 | CGO_ENABLED: 0 15 | GO111MODULE: on 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout Code 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Go 22 | uses: actions/setup-go@v4 23 | with: 24 | go-version: 1.17.x 25 | 26 | - name: Cache 27 | uses: actions/cache@v4 28 | with: 29 | path: | 30 | ~/.cache/go-build 31 | ~/go/pkg/mod 32 | key: cache-go-${{ runner.os }}-1.17.x-${{ github.run_number }} 33 | restore-keys: | 34 | cache-go-${{ runner.os}}-1.17.x- 35 | 36 | - name: Build 37 | run: make build-all 38 | 39 | - name: Lint 40 | uses: golangci/golangci-lint-action@v3 41 | with: 42 | version: v1.54 43 | skip-pkg-cache: true 44 | skip-build-cache: true 45 | 46 | - name: Refresh GoReport 47 | run: curl -XPOST --data 'repo=github.com/darthPestilane/easytcp' 'https://goreportcard.com/checks' 48 | 49 | test: 50 | needs: lint 51 | env: 52 | GO111MODULE: on 53 | strategy: 54 | matrix: 55 | os: [ubuntu-latest, macos-12, windows-latest] 56 | go-version: [1.17.x] 57 | runs-on: ${{ matrix.os }} 58 | steps: 59 | - name: Checkout Code 60 | uses: actions/checkout@v4 61 | 62 | - name: Set up Go ${{ matrix.go-version }} 63 | uses: actions/setup-go@v4 64 | with: 65 | go-version: ${{ matrix.go-version }} 66 | 67 | - name: Cache 68 | uses: actions/cache@v4 69 | with: 70 | path: | 71 | ~/.cache/go-build 72 | ~/Library/Caches/go-build 73 | %LocalAppData%\go-build 74 | ~/go/pkg/mod 75 | key: cache-go-${{ runner.os }}-${{ matrix.go-version }}-${{ github.run_number }} 76 | restore-keys: | 77 | cache-go-${{ runner.os }}-${{ matrix.go-version }}- 78 | 79 | - name: Test 80 | run: make test-v 81 | 82 | - name: Upload coverage 83 | uses: codecov/codecov-action@v3 84 | with: 85 | token: ${{ secrets.CODECOV_TOKEN }} 86 | files: .testCoverage.txt 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vscode/ 3 | vender/ 4 | dist/ 5 | 6 | *.exe 7 | *.exe~ 8 | *.test 9 | *.out 10 | *.dll 11 | *.so 12 | *.dylib 13 | 14 | .testCoverage.txt 15 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # This file is the golangci-lint configuration 2 | 3 | # This file contains all available configuration options 4 | # with their default values. 5 | 6 | # options for analysis running 7 | run: 8 | # timeout for analysis, e.g. 30s, 5m, default is 1m 9 | timeout: 10m 10 | 11 | # exit code when at least one issue was found, default is 1 12 | issues-exit-code: 1 13 | 14 | # include test files or not, default is true 15 | tests: true 16 | 17 | # which dirs to skip: issues from them won't be reported; 18 | # can use regexp here: generated.*, regexp is applied on full path; 19 | # default value is empty list, but default dirs are skipped independently 20 | # from this option's value (see skip-dirs-use-default). 21 | skip-dirs: ['examples', 'internal/mock'] 22 | 23 | # default is true. Enables skipping of directories: 24 | # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ 25 | skip-dirs-use-default: true 26 | 27 | # output configuration options 28 | output: 29 | # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" 30 | format: colored-line-number 31 | 32 | # print lines of code with issue, default is true 33 | print-issued-lines: true 34 | 35 | # print linter name in the end of issue text, default is true 36 | print-linter-name: true 37 | 38 | linters: 39 | disable-all: true 40 | enable: 41 | - govet 42 | - errcheck 43 | - staticcheck 44 | - unused 45 | - gosimple 46 | - ineffassign 47 | - typecheck 48 | - goconst 49 | - gofmt 50 | - nakedret 51 | - unconvert 52 | - unparam 53 | - exportloopref 54 | - godot 55 | fast: true 56 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | builds: 4 | - skip: true 5 | 6 | release: 7 | draft: true 8 | 9 | changelog: 10 | sort: asc 11 | filters: 12 | exclude: 13 | - '^docs:' 14 | - '^test:' 15 | - '^chore:' 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rui Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | export CGO_ENABLED=0 2 | 3 | default: build 4 | ldflags=-ldflags="-s" 5 | build: 6 | go build ${ldflags} -v 7 | 8 | build-all: 9 | go build ${ldflags} -v ./... 10 | 11 | lint: 12 | golangci-lint run --concurrency=2 13 | 14 | lint-fix: 15 | golangci-lint run --concurrency=2 --fix 16 | 17 | test: 18 | CGO_ENABLED=1 go test -count=1 -race -covermode=atomic -coverprofile=.testCoverage.txt -timeout=2m . 19 | 20 | test-v: 21 | CGO_ENABLED=1 go test -count=1 -race -covermode=atomic -coverprofile=.testCoverage.txt -timeout=2m -v . 22 | 23 | cover-view: 24 | go tool cover -func .testCoverage.txt 25 | go tool cover -html .testCoverage.txt 26 | 27 | check: test lint 28 | go tool cover -func .testCoverage.txt 29 | 30 | bench: 31 | go test -bench=. -run=none -benchmem -benchtime=250000x 32 | 33 | tidy: 34 | go mod tidy -v 35 | 36 | os=`uname` 37 | gen: 38 | ifeq (${os}, $(filter ${os}, Windows Windows_NT)) # If on windows, there might be something unexpected. 39 | rm -rf ./**/gomock_reflect_* 40 | go generate 2>/dev/null 41 | rm -rf ./**/gomock_reflect_* 42 | else 43 | go generate -v 44 | endif 45 | 46 | release-local: 47 | goreleaser release --rm-dist --skip-announce --skip-publish --snapshot 48 | 49 | clean: 50 | go clean -r -x -cache -i 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EasyTCP 2 | 3 | [![gh-action](https://github.com/DarthPestilane/easytcp/actions/workflows/test.yml/badge.svg)](https://github.com/DarthPestilane/easytcp/actions/workflows/test.yml) 4 | [![Go Report](https://goreportcard.com/badge/github.com/darthPestilane/easytcp)](https://goreportcard.com/report/github.com/darthPestilane/easytcp) 5 | [![codecov](https://codecov.io/gh/DarthPestilane/easytcp/branch/master/graph/badge.svg?token=002KJ5IV4Z)](https://codecov.io/gh/DarthPestilane/easytcp) 6 | [![Go Reference](https://pkg.go.dev/badge/github.com/DarthPestilane/easytcp.svg)](https://pkg.go.dev/github.com/DarthPestilane/easytcp) 7 | [![Mentioned in Awesome Go](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#networking) 8 | 9 | ``` 10 | $ ./start 11 | 12 | [EASYTCP] Message-Route Table: 13 | +------------+-----------------------+--------------------------------- 14 | | Message ID | Route Handler | Middlewares | 15 | +------------+-----------------------+--------------------------------- 16 | | 1000 | path/to/handler.Func1 | /path/to/middleware.Func1(g) | 17 | | | | /path/to/middleware.Func2 | 18 | +------------+-----------------------+--------------------------------- 19 | | 1002 | path/to/handler.Func2 | /path/to/middleware.Func1(g) | 20 | | | | /path/to/middleware.Func2 | 21 | +------------+-----------------------+--------------------------------- 22 | [EASYTCP] Serving at: tcp://[::]:10001 23 | ``` 24 | 25 | ## Introduction 26 | 27 | `EasyTCP` is a light-weight and less painful TCP server framework written in Go (Golang) based on the standard `net` package. 28 | 29 | ✨ Features: 30 | 31 | - Non-invasive design 32 | - Pipelined middlewares for route handler 33 | - Customizable message packer and codec, and logger 34 | - Handy functions to handle request data and send response 35 | - Common hooks 36 | 37 | `EasyTCP` helps you build a TCP server easily and fast. 38 | 39 | This package has been tested on the latest Linux, Macos and Windows. 40 | 41 | ## Install 42 | 43 | Use the below Go command to install EasyTCP. 44 | 45 | ```sh 46 | $ go get -u github.com/DarthPestilane/easytcp 47 | ``` 48 | 49 | Note: EasyTCP uses **Go Modules** to manage dependencies. 50 | 51 | ## Quick start 52 | 53 | ```go 54 | package main 55 | 56 | import ( 57 | "fmt" 58 | "github.com/DarthPestilane/easytcp" 59 | ) 60 | 61 | func main() { 62 | // Create a new server with options. 63 | s := easytcp.NewServer(&easytcp.ServerOption{ 64 | Packer: easytcp.NewDefaultPacker(), // use default packer 65 | Codec: nil, // don't use codec 66 | }) 67 | 68 | // Register a route with message's ID. 69 | // The `DefaultPacker` treats id as int, 70 | // so when we add routes or return response, we should use int. 71 | s.AddRoute(1001, func(c easytcp.Context) { 72 | // acquire request 73 | req := c.Request() 74 | 75 | // do things... 76 | fmt.Printf("[server] request received | id: %d; size: %d; data: %s\n", req.ID(), len(req.Data()), req.Data()) 77 | 78 | // set response 79 | c.SetResponseMessage(easytcp.NewMessage(1002, []byte("copy that"))) 80 | }) 81 | 82 | // Set custom logger (optional). 83 | easytcp.SetLogger(lg) 84 | 85 | // Add global middlewares (optional). 86 | s.Use(recoverMiddleware) 87 | 88 | // Set hooks (optional). 89 | s.OnSessionCreate = func(session easytcp.Session) {...} 90 | s.OnSessionClose = func(session easytcp.Session) {...} 91 | 92 | // Set not-found route handler (optional). 93 | s.NotFoundHandler(handler) 94 | 95 | // Listen and serve. 96 | if err := s.Run(":5896"); err != nil && err != server.ErrServerStopped { 97 | fmt.Println("serve error: ", err.Error()) 98 | } 99 | } 100 | ``` 101 | 102 | ### If we setup with the codec 103 | 104 | ```go 105 | // Create a new server with options. 106 | s := easytcp.NewServer(&easytcp.ServerOption{ 107 | Packer: easytcp.NewDefaultPacker(), // use default packer 108 | Codec: &easytcp.JsonCodec{}, // use JsonCodec 109 | }) 110 | 111 | // Register a route with message's ID. 112 | // The `DefaultPacker` treats id as int, 113 | // so when we add routes or return response, we should use int. 114 | s.AddRoute(1001, func(c easytcp.Context) { 115 | // decode request data and bind to `reqData` 116 | var reqData map[string]interface{} 117 | if err := c.Bind(&reqData); err != nil { 118 | // handle err 119 | } 120 | 121 | // do things... 122 | respId := 1002 123 | respData := map[string]interface{}{ 124 | "success": true, 125 | "feeling": "Great!", 126 | } 127 | 128 | // encode response data and set to `c` 129 | if err := c.SetResponse(respId, respData); err != nil { 130 | // handle err 131 | } 132 | }) 133 | ``` 134 | 135 | Above is the server side example. There are client and more detailed examples including: 136 | 137 | - [broadcasting](./internal/examples/tcp/broadcast) 138 | - [custom packet](./internal/examples/tcp/custom_packet) 139 | - [communicating with protobuf](./internal/examples/tcp/proto_packet) 140 | 141 | in [examples/tcp](./internal/examples/tcp). 142 | 143 | ## Benchmark 144 | 145 | ```sh 146 | go test -bench=. -run=none -benchmem -benchtime=250000x 147 | goos: darwin 148 | goarch: amd64 149 | pkg: github.com/DarthPestilane/easytcp 150 | cpu: Intel(R) Core(TM) i5-8279U CPU @ 2.40GHz 151 | Benchmark_NoHandler-8 250000 4277 ns/op 83 B/op 2 allocs/op 152 | Benchmark_OneHandler-8 250000 4033 ns/op 81 B/op 2 allocs/op 153 | Benchmark_DefaultPacker_Pack-8 250000 38.00 ns/op 16 B/op 1 allocs/op 154 | Benchmark_DefaultPacker_Unpack-8 250000 105.8 ns/op 96 B/op 3 allocs/op 155 | ``` 156 | 157 | *since easytcp is built on the top of golang `net` library, the benchmark of networks does not make much sense.* 158 | 159 | ## Architecture 160 | 161 | ``` 162 | accepting connection: 163 | 164 | +------------+ +-------------------+ +----------------+ 165 | | | | | | | 166 | | | | | | | 167 | | tcp server |--->| accept connection |--->| create session | 168 | | | | | | | 169 | | | | | | | 170 | +------------+ +-------------------+ +----------------+ 171 | 172 | in session: 173 | 174 | +------------------+ +-----------------------+ +----------------------------------+ 175 | | read connection |--->| unpack packet payload |--->| | 176 | +------------------+ +-----------------------+ | | 177 | | router (middlewares and handler) | 178 | +------------------+ +-----------------------+ | | 179 | | write connection |<---| pack packet payload |<---| | 180 | +------------------+ +-----------------------+ +----------------------------------+ 181 | 182 | in route handler: 183 | 184 | +----------------------------+ +------------+ 185 | | codec decode request data |--->| | 186 | +----------------------------+ | | 187 | | user logic | 188 | +----------------------------+ | | 189 | | codec encode response data |<---| | 190 | +----------------------------+ +------------+ 191 | ``` 192 | 193 | ## Conception 194 | 195 | ### Routing 196 | 197 | EasyTCP considers every message has a `ID` segment to distinguish one another. 198 | A message will be routed according to its id, to the handler through middlewares. 199 | 200 | ``` 201 | request flow: 202 | 203 | +----------+ +--------------+ +--------------+ +---------+ 204 | | request |--->| |--->| |--->| | 205 | +----------+ | | | | | | 206 | | middleware 1 | | middleware 2 | | handler | 207 | +----------+ | | | | | | 208 | | response |<---| |<---| |<---| | 209 | +----------+ +--------------+ +--------------+ +---------+ 210 | ``` 211 | 212 | #### Register a route 213 | 214 | ```go 215 | s.AddRoute(reqID, func(c easytcp.Context) { 216 | // acquire request 217 | req := c.Request() 218 | 219 | // do things... 220 | fmt.Printf("[server] request received | id: %d; size: %d; data: %s\n", req.ID(), len(req.Data()), req.Data()) 221 | 222 | // set response 223 | c.SetResponseMessage(easytcp.NewMessage(respID, []byte("copy that"))) 224 | }) 225 | ``` 226 | 227 | #### Using middleware 228 | 229 | ```go 230 | // register global middlewares. 231 | // global middlewares are prior than per-route middlewares, they will be invoked first 232 | s.Use(recoverMiddleware, logMiddleware, ...) 233 | 234 | // register middlewares for one route 235 | s.AddRoute(reqID, handler, middleware1, middleware2) 236 | 237 | // a middleware looks like: 238 | var exampleMiddleware easytcp.MiddlewareFunc = func(next easytcp.HandlerFunc) easytcp.HandlerFunc { 239 | return func(c easytcp.Context) { 240 | // do things before... 241 | next(c) 242 | // do things after... 243 | } 244 | } 245 | ``` 246 | 247 | ### Packer 248 | 249 | A packer is to pack and unpack packets' payload. We can set the Packer when creating the server. 250 | 251 | ```go 252 | s := easytcp.NewServer(&easytcp.ServerOption{ 253 | Packer: new(MyPacker), // this is optional, the default one is DefaultPacker 254 | }) 255 | ``` 256 | 257 | We can set our own Packer or EasyTCP uses [`DefaultPacker`](./packer.go). 258 | 259 | The `DefaultPacker` considers packet's payload as a `Size(4)|ID(4)|Data(n)` format. **`Size` only represents the length of `Data` instead of the whole payload length** 260 | 261 | This may not covery some particular cases, but fortunately, we can create our own Packer. 262 | 263 | ```go 264 | // CustomPacker is a custom packer, implements Packer interafce. 265 | // Treats Packet format as `size(2)id(2)data(n)` 266 | type CustomPacker struct{} 267 | 268 | func (p *CustomPacker) bytesOrder() binary.ByteOrder { 269 | return binary.BigEndian 270 | } 271 | 272 | func (p *CustomPacker) Pack(msg *easytcp.Message) ([]byte, error) { 273 | size := len(msg.Data()) // only the size of data. 274 | buffer := make([]byte, 2+2+size) 275 | p.bytesOrder().PutUint16(buffer[:2], uint16(size)) 276 | p.bytesOrder().PutUint16(buffer[2:4], msg.ID().(uint16)) 277 | copy(buffer[4:], msg.Data()) 278 | return buffer, nil 279 | } 280 | 281 | func (p *CustomPacker) Unpack(reader io.Reader) (*easytcp.Message, error) { 282 | headerBuffer := make([]byte, 2+2) 283 | if _, err := io.ReadFull(reader, headerBuffer); err != nil { 284 | return nil, fmt.Errorf("read size and id err: %s", err) 285 | } 286 | size := p.bytesOrder().Uint16(headerBuffer[:2]) 287 | id := p.bytesOrder().Uint16(headerBuffer[2:]) 288 | 289 | data := make([]byte, size) 290 | if _, err := io.ReadFull(reader, data); err != nil { 291 | return nil, fmt.Errorf("read data err: %s", err) 292 | } 293 | 294 | // since msg.ID is type of uint16, we need to use uint16 as well when adding routes. 295 | // eg: server.AddRoute(uint16(123), ...) 296 | msg := easytcp.NewMessage(id, data) 297 | msg.Set("theWholeLength", 2+2+size) // we can set our custom kv data here. 298 | // c.Request().Get("theWholeLength") // and get them in route handler. 299 | return msg, nil 300 | } 301 | ``` 302 | 303 | And see more custom packers: 304 | - [custom_packet](./examples/tcp/custom_packet/common/packer.go) 305 | - [proto_packet](./examples/tcp/proto_packet/common/packer.go) 306 | 307 | ### Codec 308 | 309 | A Codec is to encode and decode message data. The Codec is optional, EasyTCP won't encode or decode message data if the Codec is not set. 310 | 311 | We can set Codec when creating the server. 312 | 313 | ```go 314 | s := easytcp.NewServer(&easytcp.ServerOption{ 315 | Codec: &easytcp.JsonCodec{}, // this is optional. The JsonCodec is a built-in codec 316 | }) 317 | ``` 318 | 319 | Since we set the codec, we may want to decode the request data in route handler. 320 | 321 | ```go 322 | s.AddRoute(reqID, func(c easytcp.Context) { 323 | var reqData map[string]interface{} 324 | if err := c.Bind(&reqData); err != nil { // here we decode message data and bind to reqData 325 | // handle error... 326 | } 327 | req := c.Request() 328 | fmt.Printf("[server] request received | id: %d; size: %d; data-decoded: %+v\n", req.ID(), len(req.Data()), reqData()) 329 | respData := map[string]string{"key": "value"} 330 | if err := c.SetResponse(respID, respData); err != nil { 331 | // handle error... 332 | } 333 | }) 334 | ``` 335 | 336 | Codec's encoding will be invoked before message packed, 337 | and decoding should be invoked in the route handler which is after message unpacked. 338 | 339 | #### JSON Codec 340 | 341 | `JsonCodec` is an EasyTCP's built-in codec, which uses `encoding/json` as the default implementation. 342 | Can be changed by build from other tags. 343 | 344 | [jsoniter](https://github.com/json-iterator/go) : 345 | 346 | ```sh 347 | go build -tags=jsoniter . 348 | ``` 349 | 350 | #### Protobuf Codec 351 | 352 | `ProtobufCodec` is an EasyTCP's built-in codec, which uses `google.golang.org/protobuf` as the implementation. 353 | 354 | #### Msgpack Codec 355 | 356 | `MsgpackCodec` is an EasyTCP's built-in codec, which uses `github.com/vmihailenco/msgpack` as the implementation. 357 | 358 | ## Contribute 359 | 360 | Check out a new branch for the job, and make sure github action passed. 361 | 362 | Use issues for everything 363 | 364 | - For a small change, just send a PR. 365 | - For bigger changes open an issue for discussion before sending a PR. 366 | - PR should have: 367 | - Test case 368 | - Documentation 369 | - Example (If it makes sense) 370 | - You can also contribute by: 371 | - Reporting issues 372 | - Suggesting new features or enhancements 373 | - Improve/fix documentation 374 | 375 | ## Stargazers over time 376 | 377 | [![Stargazers over time](https://starchart.cc/DarthPestilane/easytcp.svg)](https://starchart.cc/DarthPestilane/easytcp) 378 | -------------------------------------------------------------------------------- /benchmarks_test.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "bytes" 5 | "github.com/stretchr/testify/assert" 6 | "io" 7 | "net" 8 | "testing" 9 | ) 10 | 11 | // go test -bench="^Benchmark_\w+$" -run=none -benchmem -benchtime=250000x 12 | 13 | type mutedLogger struct{} 14 | 15 | func (m *mutedLogger) Errorf(_ string, _ ...interface{}) {} 16 | func (m *mutedLogger) Tracef(_ string, _ ...interface{}) {} 17 | 18 | func Benchmark_NoHandler(b *testing.B) { 19 | s := NewServer(&ServerOption{ 20 | DoNotPrintRoutes: true, 21 | }) 22 | go s.Run("127.0.0.1:0") // nolint 23 | defer s.Stop() // nolint 24 | 25 | <-s.acceptingC 26 | 27 | // client 28 | client, err := net.Dial("tcp", s.Listener.Addr().String()) 29 | if err != nil { 30 | panic(err) 31 | } 32 | defer client.Close() // nolint 33 | 34 | packedBytes, _ := s.Packer.Pack(NewMessage(1, []byte("ping"))) 35 | beforeBench(b) 36 | for i := 0; i < b.N; i++ { 37 | _, _ = client.Write(packedBytes) 38 | } 39 | } 40 | 41 | func Benchmark_OneHandler(b *testing.B) { 42 | s := NewServer(&ServerOption{ 43 | DoNotPrintRoutes: true, 44 | }) 45 | s.AddRoute(1, func(ctx Context) {}) 46 | go s.Run("127.0.0.1:0") // nolint 47 | defer s.Stop() // nolint 48 | 49 | <-s.acceptingC 50 | 51 | // client 52 | client, err := net.Dial("tcp", s.Listener.Addr().String()) 53 | if err != nil { 54 | panic(err) 55 | } 56 | defer client.Close() // nolint 57 | 58 | packedMsg, _ := s.Packer.Pack(NewMessage(1, []byte("ping"))) 59 | beforeBench(b) 60 | for i := 0; i < b.N; i++ { 61 | _, _ = client.Write(packedMsg) 62 | } 63 | } 64 | 65 | func Benchmark_DefaultPacker_Pack(b *testing.B) { 66 | packer := NewDefaultPacker() 67 | msg := NewMessage(1, []byte("test")) 68 | beforeBench(b) 69 | for i := 0; i < b.N; i++ { 70 | _, _ = packer.Pack(msg) 71 | } 72 | } 73 | 74 | func Benchmark_DefaultPacker_Unpack(b *testing.B) { 75 | packer := NewDefaultPacker() 76 | msg := NewMessage(1, []byte("test")) 77 | dataBytes, err := packer.Pack(msg) 78 | assert.NoError(b, err) 79 | 80 | r := bytes.NewReader(dataBytes) 81 | beforeBench(b) 82 | for i := 0; i < b.N; i++ { 83 | _, _ = packer.Unpack(r) 84 | _, _ = r.Seek(0, io.SeekStart) 85 | } 86 | } 87 | 88 | func beforeBench(b *testing.B) { 89 | _log = &mutedLogger{} 90 | b.ReportAllocs() 91 | b.ResetTimer() 92 | } 93 | -------------------------------------------------------------------------------- /codec.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | //go:generate mockgen -destination internal/mock/codec_mock.go -package mock . Codec 4 | 5 | // Codec is a generic codec for encoding and decoding data. 6 | type Codec interface { 7 | // Encode encodes data into []byte. 8 | // Returns error when error occurred. 9 | Encode(v interface{}) ([]byte, error) 10 | 11 | // Decode decodes data into v. 12 | // Returns error when error occurred. 13 | Decode(data []byte, v interface{}) error 14 | } 15 | -------------------------------------------------------------------------------- /codec_json.go: -------------------------------------------------------------------------------- 1 | //go:build !jsoniter 2 | // +build !jsoniter 3 | 4 | package easytcp 5 | 6 | import ( 7 | "encoding/json" 8 | ) 9 | 10 | var _ Codec = &JsonCodec{} 11 | 12 | // JsonCodec implements the Codec interface. 13 | // JsonCodec encodes and decodes data in json way. 14 | type JsonCodec struct{} 15 | 16 | // Encode implements the Codec Encode method. 17 | func (c *JsonCodec) Encode(v interface{}) ([]byte, error) { 18 | return json.Marshal(v) 19 | } 20 | 21 | // Decode implements the Codec Decode method. 22 | func (c *JsonCodec) Decode(data []byte, v interface{}) error { 23 | return json.Unmarshal(data, v) 24 | } 25 | -------------------------------------------------------------------------------- /codec_json_jsoniter.go: -------------------------------------------------------------------------------- 1 | //go:build jsoniter 2 | // +build jsoniter 3 | 4 | package easytcp 5 | 6 | import ( 7 | jsoniter "github.com/json-iterator/go" 8 | ) 9 | 10 | var _ Codec = &JsonCodec{} 11 | 12 | // JsonCodec implements the Codec interface. 13 | // JsonCodec encodes and decodes data in json way. 14 | type JsonCodec struct{} 15 | 16 | // Encode implements the Codec Encode method. 17 | func (c *JsonCodec) Encode(v interface{}) ([]byte, error) { 18 | return jsoniter.Marshal(v) 19 | } 20 | 21 | // Decode implements the Codec Decode method. 22 | func (c *JsonCodec) Decode(data []byte, v interface{}) error { 23 | return jsoniter.Unmarshal(data, v) 24 | } 25 | -------------------------------------------------------------------------------- /codec_msgpack.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "github.com/vmihailenco/msgpack/v5" 5 | ) 6 | 7 | // MsgpackCodec implements the Codec interface. 8 | type MsgpackCodec struct{} 9 | 10 | // Encode implements the Codec Encode method. 11 | func (m *MsgpackCodec) Encode(v interface{}) ([]byte, error) { 12 | return msgpack.Marshal(v) 13 | } 14 | 15 | // Decode implements the Codec Decode method. 16 | func (m *MsgpackCodec) Decode(data []byte, v interface{}) error { 17 | return msgpack.Unmarshal(data, v) 18 | } 19 | -------------------------------------------------------------------------------- /codec_pb.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "fmt" 5 | "google.golang.org/protobuf/proto" 6 | ) 7 | 8 | // ProtobufCodec implements the Codec interface. 9 | type ProtobufCodec struct{} 10 | 11 | // Encode implements the Codec Encode method. 12 | func (p *ProtobufCodec) Encode(v interface{}) ([]byte, error) { 13 | m, ok := v.(proto.Message) 14 | if !ok { 15 | return nil, fmt.Errorf("v should be proto.Message but %T", v) 16 | } 17 | return proto.Marshal(m) 18 | } 19 | 20 | // Decode implements the Codec Decode method. 21 | func (p *ProtobufCodec) Decode(data []byte, v interface{}) error { 22 | m, ok := v.(proto.Message) 23 | if !ok { 24 | return fmt.Errorf("v should be proto.Message but %T", v) 25 | } 26 | return proto.Unmarshal(data, m) 27 | } 28 | -------------------------------------------------------------------------------- /codec_test.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "github.com/DarthPestilane/easytcp/internal/test_data/msgpack" 5 | "github.com/DarthPestilane/easytcp/internal/test_data/pb" 6 | "github.com/stretchr/testify/assert" 7 | "google.golang.org/protobuf/proto" 8 | "testing" 9 | ) 10 | 11 | func TestJsonCodec_Decode(t *testing.T) { 12 | c := &JsonCodec{} 13 | data := []byte(`{"id": 1}`) 14 | var v struct { 15 | Id int `json:"id"` 16 | } 17 | assert.NoError(t, c.Decode(data, &v)) 18 | assert.EqualValues(t, v.Id, 1) 19 | } 20 | 21 | func TestJsonCodec_Encode(t *testing.T) { 22 | c := &JsonCodec{} 23 | v := struct { 24 | Id int `json:"id"` 25 | }{Id: 1} 26 | b, err := c.Encode(v) 27 | assert.NoError(t, err) 28 | assert.JSONEq(t, string(b), `{"id": 1}`) 29 | } 30 | 31 | func TestProtobufCodec(t *testing.T) { 32 | c := &ProtobufCodec{} 33 | t.Run("when encode/decode with invalid params", func(t *testing.T) { 34 | // encoding 35 | b, err := c.Encode(123) 36 | assert.Error(t, err) 37 | assert.Nil(t, b) 38 | 39 | // decoding 40 | var v int 41 | assert.Error(t, c.Decode([]byte("test"), &v)) 42 | }) 43 | t.Run("when succeed", func(t *testing.T) { 44 | // encoding 45 | v := &pb.Sample{Foo: "bar", Bar: 33} 46 | b, err := c.Encode(v) 47 | assert.NoError(t, err) 48 | assert.NotNil(t, b) 49 | 50 | // decoding 51 | sample := &pb.Sample{} 52 | assert.NoError(t, c.Decode(b, sample)) 53 | 54 | // comparing 55 | assert.True(t, proto.Equal(v, sample)) 56 | }) 57 | } 58 | 59 | func TestMsgpackCodec(t *testing.T) { 60 | item := msgpack.Sample{ 61 | Foo: "test", 62 | Bar: 1, 63 | Baz: map[int]string{2: "2"}, 64 | } 65 | c := &MsgpackCodec{} 66 | b, err := c.Encode(item) 67 | assert.NoError(t, err) 68 | assert.NotNil(t, b) 69 | 70 | var itemDec msgpack.Sample 71 | assert.NoError(t, c.Decode(b, &itemDec)) 72 | assert.Equal(t, item, itemDec) 73 | } 74 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "examples" 3 | - "internal/mock" 4 | - "internal/test_data" 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/DarthPestilane/easytcp 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/golang/mock v1.5.0 7 | github.com/google/uuid v1.2.0 8 | github.com/json-iterator/go v1.1.11 9 | github.com/olekukonko/tablewriter v0.0.5 10 | github.com/sirupsen/logrus v1.8.1 11 | github.com/spf13/cast v1.4.1 12 | github.com/stretchr/testify v1.7.0 13 | github.com/vmihailenco/msgpack/v5 v5.3.4 14 | google.golang.org/protobuf v1.33.0 15 | ) 16 | 17 | require ( 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/mattn/go-runewidth v0.0.13 // indirect 20 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 21 | github.com/modern-go/reflect2 v1.0.1 // indirect 22 | github.com/pmezard/go-difflib v1.0.0 // indirect 23 | github.com/rivo/uniseg v0.2.0 // indirect 24 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 25 | golang.org/x/sys v0.1.0 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/golang/mock v1.5.0 h1:jlYHihg//f7RRwuPfptm04yp4s7O6Kw8EZiVYIGcH0g= 5 | github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= 6 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 7 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 8 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 9 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 10 | github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= 11 | github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 12 | github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= 13 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 14 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 15 | github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= 16 | github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 17 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 18 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 19 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 20 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 21 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 22 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 23 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 24 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 28 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 29 | github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 30 | github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= 31 | github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= 32 | github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 33 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 34 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 35 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 36 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 37 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 38 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 39 | github.com/vmihailenco/msgpack/v5 v5.3.4 h1:qMKAwOV+meBw2Y8k9cVwAy7qErtYCwBzZ2ellBfvnqc= 40 | github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= 41 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 42 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 43 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 44 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 45 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 46 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 47 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 48 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 49 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 50 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 51 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 52 | golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= 53 | golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 54 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 55 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 56 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 57 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 58 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 59 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 60 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 61 | google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= 62 | google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 63 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 64 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 66 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 67 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 68 | -------------------------------------------------------------------------------- /internal/examples/fixture/basic.go: -------------------------------------------------------------------------------- 1 | package fixture 2 | 3 | const ServerAddr = "0.0.0.0:8888" 4 | -------------------------------------------------------------------------------- /internal/examples/fixture/middleware.go: -------------------------------------------------------------------------------- 1 | package fixture 2 | 3 | import ( 4 | "github.com/DarthPestilane/easytcp" 5 | "github.com/sirupsen/logrus" 6 | "runtime/debug" 7 | ) 8 | 9 | func RecoverMiddleware(log *logrus.Logger) easytcp.MiddlewareFunc { 10 | return func(next easytcp.HandlerFunc) easytcp.HandlerFunc { 11 | return func(c easytcp.Context) { 12 | defer func() { 13 | if r := recover(); r != nil { 14 | log.WithField("sid", c.Session().ID()).Errorf("PANIC | %+v | %s", r, debug.Stack()) 15 | } 16 | }() 17 | next(c) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/examples/tcp/broadcast/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DarthPestilane/easytcp" 6 | "github.com/DarthPestilane/easytcp/internal/examples/fixture" 7 | "github.com/DarthPestilane/easytcp/internal/examples/tcp/broadcast/common" 8 | "github.com/sirupsen/logrus" 9 | "net" 10 | "time" 11 | ) 12 | 13 | var log *logrus.Logger 14 | var packer easytcp.Packer 15 | 16 | func init() { 17 | log = logrus.New() 18 | log.SetLevel(logrus.DebugLevel) 19 | packer = easytcp.NewDefaultPacker() 20 | } 21 | 22 | func main() { 23 | senderClient() 24 | for i := 0; i < 10; i++ { 25 | readerClient(i) 26 | } 27 | 28 | select {} 29 | } 30 | 31 | func establish() (net.Conn, error) { 32 | return net.Dial("tcp", fixture.ServerAddr) 33 | } 34 | 35 | func senderClient() { 36 | conn, err := establish() 37 | if err != nil { 38 | log.Error(err) 39 | return 40 | } 41 | // send 42 | go func() { 43 | for { 44 | time.Sleep(time.Second) 45 | data := []byte(fmt.Sprintf("hello everyone @%d", time.Now().Unix())) 46 | packedMsg, _ := packer.Pack(easytcp.NewMessage(common.MsgIdBroadCastReq, data)) 47 | if _, err := conn.Write(packedMsg); err != nil { 48 | log.Error(err) 49 | return 50 | } 51 | } 52 | }() 53 | 54 | // read 55 | go func() { 56 | for { 57 | msg, err := packer.Unpack(conn) 58 | if err != nil { 59 | log.Error(err) 60 | return 61 | } 62 | log.Infof("sender | recv ack | %s", msg.Data()) 63 | } 64 | }() 65 | } 66 | 67 | func readerClient(id int) { 68 | conn, err := establish() 69 | if err != nil { 70 | log.Error(err) 71 | return 72 | } 73 | 74 | go func() { 75 | for { 76 | msg, err := packer.Unpack(conn) 77 | if err != nil { 78 | log.Error(err) 79 | return 80 | } 81 | log.Debugf("reader %03d | recv broadcast | %s", id, msg.Data()) 82 | } 83 | }() 84 | } 85 | -------------------------------------------------------------------------------- /internal/examples/tcp/broadcast/common/message.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | // broadcast messages 4 | const ( 5 | _ int = iota + 200 6 | MsgIdBroadCastReq 7 | MsgIdBroadCastAck 8 | ) 9 | -------------------------------------------------------------------------------- /internal/examples/tcp/broadcast/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DarthPestilane/easytcp" 6 | "github.com/DarthPestilane/easytcp/internal/examples/fixture" 7 | "github.com/DarthPestilane/easytcp/internal/examples/tcp/broadcast/common" 8 | "github.com/sirupsen/logrus" 9 | "os" 10 | "os/signal" 11 | "sync" 12 | "syscall" 13 | "time" 14 | ) 15 | 16 | var log *logrus.Logger 17 | var sessions *SessionManager 18 | 19 | func init() { 20 | log = logrus.New() 21 | sessions = &SessionManager{nextId: 1, storage: map[int64]easytcp.Session{}} 22 | } 23 | 24 | type SessionManager struct { 25 | nextId int64 26 | lock sync.Mutex 27 | storage map[int64]easytcp.Session 28 | } 29 | 30 | func main() { 31 | s := easytcp.NewServer(&easytcp.ServerOption{ 32 | Packer: easytcp.NewDefaultPacker(), 33 | }) 34 | 35 | s.OnSessionCreate = func(sess easytcp.Session) { 36 | // store session 37 | sessions.lock.Lock() 38 | defer sessions.lock.Unlock() 39 | sess.SetID(sessions.nextId) 40 | sessions.nextId++ 41 | sessions.storage[sess.ID().(int64)] = sess 42 | } 43 | 44 | s.OnSessionClose = func(sess easytcp.Session) { 45 | // remove session 46 | delete(sessions.storage, sess.ID().(int64)) 47 | } 48 | 49 | s.Use(fixture.RecoverMiddleware(log), logMiddleware) 50 | 51 | s.AddRoute(common.MsgIdBroadCastReq, func(ctx easytcp.Context) { 52 | reqData := ctx.Request().Data() 53 | 54 | // broadcasting to other sessions 55 | currentSession := ctx.Session() 56 | for _, sess := range sessions.storage { 57 | targetSession := sess 58 | if currentSession.ID() == targetSession.ID() { 59 | continue 60 | } 61 | respData := fmt.Sprintf("%s (broadcast from %d to %d)", reqData, currentSession.ID(), targetSession.ID()) 62 | respMsg := easytcp.NewMessage(common.MsgIdBroadCastAck, []byte(respData)) 63 | go func() { 64 | targetSession.AllocateContext().SetResponseMessage(respMsg).Send() 65 | // can also write like this. 66 | // ctx.Copy().SetResponseMessage(respMsg).SendTo(targetSession) 67 | // or this. 68 | // ctx.Copy().SetSession(targetSession).SetResponseMessage(respMsg).Send() 69 | }() 70 | } 71 | 72 | ctx.SetResponseMessage(easytcp.NewMessage(common.MsgIdBroadCastAck, []byte("broadcast done"))) 73 | }) 74 | 75 | go func() { 76 | if err := s.Run(fixture.ServerAddr); err != nil { 77 | log.Error(err) 78 | } 79 | }() 80 | 81 | sigCh := make(chan os.Signal, 1) 82 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 83 | <-sigCh 84 | if err := s.Stop(); err != nil { 85 | log.Errorf("server stopped err: %s", err) 86 | } 87 | time.Sleep(time.Second) 88 | } 89 | 90 | func logMiddleware(next easytcp.HandlerFunc) easytcp.HandlerFunc { 91 | return func(ctx easytcp.Context) { 92 | log.Infof("recv request | %s", ctx.Request().Data()) 93 | defer func() { 94 | var respMsg = ctx.Response() 95 | log.Infof("send response |sessId: %d; id: %d; size: %d; data: %s", ctx.Session().ID(), respMsg.ID(), len(respMsg.Data()), respMsg.Data()) 96 | }() 97 | next(ctx) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /internal/examples/tcp/custom_packet/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/DarthPestilane/easytcp" 5 | "github.com/DarthPestilane/easytcp/internal/examples/fixture" 6 | "github.com/DarthPestilane/easytcp/internal/examples/tcp/custom_packet/common" 7 | "github.com/sirupsen/logrus" 8 | "net" 9 | "time" 10 | ) 11 | 12 | func main() { 13 | conn, err := net.Dial("tcp", fixture.ServerAddr) 14 | if err != nil { 15 | panic(err) 16 | } 17 | log := logrus.New() 18 | codec := &easytcp.JsonCodec{} 19 | packer := &common.CustomPacker{} 20 | go func() { 21 | // write loop 22 | for { 23 | time.Sleep(time.Second) 24 | req := &common.Json01Req{ 25 | Key1: "hello", 26 | Key2: 10, 27 | Key3: true, 28 | } 29 | data, err := codec.Encode(req) 30 | if err != nil { 31 | panic(err) 32 | } 33 | packedMsg, err := packer.Pack(easytcp.NewMessage("json01-req", data)) 34 | if err != nil { 35 | panic(err) 36 | } 37 | if _, err := conn.Write(packedMsg); err != nil { 38 | panic(err) 39 | } 40 | } 41 | }() 42 | go func() { 43 | // read loop 44 | for { 45 | msg, err := packer.Unpack(conn) 46 | if err != nil { 47 | panic(err) 48 | } 49 | fullSize := msg.MustGet("fullSize") 50 | log.Infof("ack received | fullSize:(%d) id:(%v) dataSize:(%d) data: %s", fullSize, msg.ID(), len(msg.Data()), msg.Data()) 51 | } 52 | }() 53 | select {} 54 | } 55 | -------------------------------------------------------------------------------- /internal/examples/tcp/custom_packet/common/message.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type Json01Req struct { 4 | Key1 string `json:"key_1"` 5 | Key2 int `json:"key_2"` 6 | Key3 bool `json:"key_3"` 7 | } 8 | 9 | type Json01Resp struct { 10 | Success bool `json:"success"` 11 | Data interface{} `json:"data"` 12 | } 13 | -------------------------------------------------------------------------------- /internal/examples/tcp/custom_packet/common/packer.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "github.com/DarthPestilane/easytcp" 7 | "github.com/spf13/cast" 8 | "io" 9 | ) 10 | 11 | // CustomPacker treats packet as: 12 | // 13 | // totalSize(4)|idSize(2)|id(n)|data(n) 14 | // 15 | // | segment | type | size | remark | 16 | // | ----------- | ------ | ------- | --------------------- | 17 | // | `totalSize` | uint32 | 4 | the whole packet size | 18 | // | `idSize` | uint16 | 2 | length of id | 19 | // | `id` | string | dynamic | | 20 | // | `data` | []byte | dynamic | | 21 | type CustomPacker struct{} 22 | 23 | func (p *CustomPacker) bytesOrder() binary.ByteOrder { 24 | return binary.LittleEndian 25 | } 26 | 27 | func (p *CustomPacker) Pack(msg *easytcp.Message) ([]byte, error) { 28 | // format: totalSize(4)|idSize(2)|id(n)|data(n) 29 | 30 | id, err := cast.ToStringE(msg.ID()) 31 | if err != nil { 32 | return nil, err 33 | } 34 | buffer := make([]byte, 4+2+len(id)+len(msg.Data())) 35 | p.bytesOrder().PutUint32(buffer[:4], uint32(len(buffer))) // write totalSize 36 | p.bytesOrder().PutUint16(buffer[4:6], uint16(len(id))) // write idSize 37 | copy(buffer[6:6+len(id)], id) // write id 38 | copy(buffer[6+len(id):], msg.Data()) // write data 39 | 40 | return buffer, nil 41 | } 42 | 43 | func (p *CustomPacker) Unpack(reader io.Reader) (*easytcp.Message, error) { 44 | // format: totalSize(4)|idSize(2)|id(n)|data(n) 45 | 46 | headerBuff := make([]byte, 4+2) 47 | if _, err := io.ReadFull(reader, headerBuff); err != nil { 48 | if err == io.EOF { 49 | return nil, err 50 | } 51 | return nil, fmt.Errorf("read header err: %s", err) 52 | } 53 | totalSize := int(p.bytesOrder().Uint32(headerBuff[:4])) // read totalSize 54 | idSize := int(p.bytesOrder().Uint16(headerBuff[4:])) // read idSize 55 | dataSize := totalSize - 4 - 2 - idSize 56 | 57 | bodyBuff := make([]byte, idSize+dataSize) 58 | if _, err := io.ReadFull(reader, bodyBuff); err != nil { 59 | return nil, fmt.Errorf("read body err: %s", err) 60 | } 61 | id := string(bodyBuff[:idSize]) // read id 62 | data := bodyBuff[idSize:] // read body 63 | 64 | // ID is a string, so we should use a string-type id to register routes. 65 | // eg: server.AddRoute("string-id", handler) 66 | msg := easytcp.NewMessage(id, data) 67 | msg.Set("fullSize", totalSize) 68 | return msg, nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/examples/tcp/custom_packet/common/packer_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "github.com/DarthPestilane/easytcp" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestCustomPacker(t *testing.T) { 11 | packer := &CustomPacker{} 12 | msg := easytcp.NewMessage("test", []byte("data")) 13 | packedBytes, err := packer.Pack(msg) 14 | assert.NoError(t, err) 15 | assert.NotNil(t, packedBytes) 16 | 17 | r := bytes.NewBuffer(packedBytes) 18 | newMsg, err := packer.Unpack(r) 19 | assert.NoError(t, err) 20 | assert.NotNil(t, newMsg) 21 | assert.Equal(t, newMsg.ID(), msg.ID()) 22 | assert.Equal(t, newMsg.Data(), msg.Data()) 23 | } 24 | -------------------------------------------------------------------------------- /internal/examples/tcp/custom_packet/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DarthPestilane/easytcp" 6 | "github.com/DarthPestilane/easytcp/internal/examples/fixture" 7 | "github.com/DarthPestilane/easytcp/internal/examples/tcp/custom_packet/common" 8 | "github.com/sirupsen/logrus" 9 | "os" 10 | "os/signal" 11 | "syscall" 12 | ) 13 | 14 | var log *logrus.Logger 15 | 16 | func init() { 17 | log = logrus.New() 18 | log.SetLevel(logrus.DebugLevel) 19 | } 20 | 21 | func main() { 22 | easytcp.SetLogger(log) 23 | 24 | s := easytcp.NewServer(&easytcp.ServerOption{ 25 | // specify codec and packer 26 | Codec: &easytcp.JsonCodec{}, 27 | Packer: &common.CustomPacker{}, 28 | }) 29 | 30 | s.AddRoute("json01-req", handler, fixture.RecoverMiddleware(log), logMiddleware) 31 | 32 | go func() { 33 | if err := s.Run(fixture.ServerAddr); err != nil { 34 | log.Errorf("serve err: %s", err) 35 | } 36 | }() 37 | 38 | sigCh := make(chan os.Signal, 1) 39 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 40 | <-sigCh 41 | if err := s.Stop(); err != nil { 42 | log.Errorf("server stopped err: %s", err) 43 | } 44 | } 45 | 46 | func handler(ctx easytcp.Context) { 47 | var data common.Json01Req 48 | _ = ctx.Bind(&data) 49 | 50 | err := ctx.SetResponse("json01-resp", &common.Json01Resp{ 51 | Success: true, 52 | Data: fmt.Sprintf("%s:%d:%t", data.Key1, data.Key2, data.Key3), 53 | }) 54 | if err != nil { 55 | log.Errorf("set response failed: %s", err) 56 | } 57 | } 58 | 59 | func logMiddleware(next easytcp.HandlerFunc) easytcp.HandlerFunc { 60 | return func(ctx easytcp.Context) { 61 | fullSize := ctx.Request().MustGet("fullSize") 62 | req := ctx.Request() 63 | log.Infof("recv request | fullSize:(%d) id:(%v) dataSize(%d) data: %s", fullSize, req.ID(), len(req.Data()), req.Data()) 64 | 65 | defer func() { 66 | resp := ctx.Response() 67 | if resp != nil { 68 | log.Infof("send response | dataSize:(%d) id:(%v) data: %s", len(resp.Data()), resp.ID(), resp.Data()) 69 | } else { 70 | log.Infof("don't send response since nil") 71 | } 72 | }() 73 | next(ctx) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/examples/tcp/proto_packet/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/DarthPestilane/easytcp" 5 | "github.com/DarthPestilane/easytcp/internal/examples/fixture" 6 | "github.com/DarthPestilane/easytcp/internal/examples/tcp/proto_packet/common" 7 | "github.com/sirupsen/logrus" 8 | "net" 9 | "time" 10 | ) 11 | 12 | var log *logrus.Logger 13 | 14 | func init() { 15 | log = logrus.New() 16 | log.SetLevel(logrus.DebugLevel) 17 | } 18 | 19 | func main() { 20 | conn, err := net.Dial("tcp", fixture.ServerAddr) 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | packer := &common.CustomPacker{} 26 | codec := &easytcp.ProtobufCodec{} 27 | 28 | go func() { 29 | for { 30 | var id = common.ID_FooReqID 31 | req := &common.FooReq{ 32 | Bar: "bar", 33 | Buz: 22, 34 | } 35 | data, err := codec.Encode(req) 36 | if err != nil { 37 | panic(err) 38 | } 39 | packedMsg, err := packer.Pack(easytcp.NewMessage(id, data)) 40 | if err != nil { 41 | panic(err) 42 | } 43 | if _, err := conn.Write(packedMsg); err != nil { 44 | panic(err) 45 | } 46 | log.Debugf("send | id: %d; size: %d; data: %s", id, len(data), req.String()) 47 | time.Sleep(time.Second) 48 | } 49 | }() 50 | 51 | for { 52 | msg, err := packer.Unpack(conn) 53 | if err != nil { 54 | panic(err) 55 | } 56 | var respData common.FooResp 57 | if err := codec.Decode(msg.Data(), &respData); err != nil { 58 | panic(err) 59 | } 60 | log.Infof("recv | id: %d; size: %d; data: %s", msg.ID(), len(msg.Data()), respData.String()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /internal/examples/tcp/proto_packet/common/make.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | protoc --go_out=. --go_opt=paths=source_relative *.proto; 4 | -------------------------------------------------------------------------------- /internal/examples/tcp/proto_packet/common/message.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.27.1 4 | // protoc v3.17.3 5 | // source: message.proto 6 | 7 | package common 8 | 9 | import ( 10 | "google.golang.org/protobuf/reflect/protoreflect" 11 | "google.golang.org/protobuf/runtime/protoimpl" 12 | "reflect" 13 | "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type ID int32 24 | 25 | const ( 26 | ID__ ID = 0 27 | ID_FooReqID ID = 1 28 | ID_FooRespID ID = 2 29 | ) 30 | 31 | // Enum value maps for ID. 32 | var ( 33 | ID_name = map[int32]string{ 34 | 0: "_", 35 | 1: "FooReqID", 36 | 2: "FooRespID", 37 | } 38 | ID_value = map[string]int32{ 39 | "_": 0, 40 | "FooReqID": 1, 41 | "FooRespID": 2, 42 | } 43 | ) 44 | 45 | func (x ID) Enum() *ID { 46 | p := new(ID) 47 | *p = x 48 | return p 49 | } 50 | 51 | func (x ID) String() string { 52 | return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) 53 | } 54 | 55 | func (ID) Descriptor() protoreflect.EnumDescriptor { 56 | return file_message_proto_enumTypes[0].Descriptor() 57 | } 58 | 59 | func (ID) Type() protoreflect.EnumType { 60 | return &file_message_proto_enumTypes[0] 61 | } 62 | 63 | func (x ID) Number() protoreflect.EnumNumber { 64 | return protoreflect.EnumNumber(x) 65 | } 66 | 67 | // Deprecated: Use ID.Descriptor instead. 68 | func (ID) EnumDescriptor() ([]byte, []int) { 69 | return file_message_proto_rawDescGZIP(), []int{0} 70 | } 71 | 72 | type FooReq struct { 73 | state protoimpl.MessageState 74 | sizeCache protoimpl.SizeCache 75 | unknownFields protoimpl.UnknownFields 76 | 77 | Bar string `protobuf:"bytes,1,opt,name=Bar,proto3" json:"Bar,omitempty"` 78 | Buz int32 `protobuf:"varint,2,opt,name=Buz,proto3" json:"Buz,omitempty"` 79 | } 80 | 81 | func (x *FooReq) Reset() { 82 | *x = FooReq{} 83 | if protoimpl.UnsafeEnabled { 84 | mi := &file_message_proto_msgTypes[0] 85 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 86 | ms.StoreMessageInfo(mi) 87 | } 88 | } 89 | 90 | func (x *FooReq) String() string { 91 | return protoimpl.X.MessageStringOf(x) 92 | } 93 | 94 | func (*FooReq) ProtoMessage() {} 95 | 96 | func (x *FooReq) ProtoReflect() protoreflect.Message { 97 | mi := &file_message_proto_msgTypes[0] 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 FooReq.ProtoReflect.Descriptor instead. 109 | func (*FooReq) Descriptor() ([]byte, []int) { 110 | return file_message_proto_rawDescGZIP(), []int{0} 111 | } 112 | 113 | func (x *FooReq) GetBar() string { 114 | if x != nil { 115 | return x.Bar 116 | } 117 | return "" 118 | } 119 | 120 | func (x *FooReq) GetBuz() int32 { 121 | if x != nil { 122 | return x.Buz 123 | } 124 | return 0 125 | } 126 | 127 | type FooResp struct { 128 | state protoimpl.MessageState 129 | sizeCache protoimpl.SizeCache 130 | unknownFields protoimpl.UnknownFields 131 | 132 | Code int32 `protobuf:"varint,1,opt,name=Code,proto3" json:"Code,omitempty"` 133 | Message string `protobuf:"bytes,2,opt,name=Message,proto3" json:"Message,omitempty"` 134 | } 135 | 136 | func (x *FooResp) Reset() { 137 | *x = FooResp{} 138 | if protoimpl.UnsafeEnabled { 139 | mi := &file_message_proto_msgTypes[1] 140 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 141 | ms.StoreMessageInfo(mi) 142 | } 143 | } 144 | 145 | func (x *FooResp) String() string { 146 | return protoimpl.X.MessageStringOf(x) 147 | } 148 | 149 | func (*FooResp) ProtoMessage() {} 150 | 151 | func (x *FooResp) ProtoReflect() protoreflect.Message { 152 | mi := &file_message_proto_msgTypes[1] 153 | if protoimpl.UnsafeEnabled && x != nil { 154 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 155 | if ms.LoadMessageInfo() == nil { 156 | ms.StoreMessageInfo(mi) 157 | } 158 | return ms 159 | } 160 | return mi.MessageOf(x) 161 | } 162 | 163 | // Deprecated: Use FooResp.ProtoReflect.Descriptor instead. 164 | func (*FooResp) Descriptor() ([]byte, []int) { 165 | return file_message_proto_rawDescGZIP(), []int{1} 166 | } 167 | 168 | func (x *FooResp) GetCode() int32 { 169 | if x != nil { 170 | return x.Code 171 | } 172 | return 0 173 | } 174 | 175 | func (x *FooResp) GetMessage() string { 176 | if x != nil { 177 | return x.Message 178 | } 179 | return "" 180 | } 181 | 182 | var File_message_proto protoreflect.FileDescriptor 183 | 184 | var file_message_proto_rawDesc = []byte{ 185 | 0x0a, 0x0d, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 186 | 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x22, 0x2c, 0x0a, 0x06, 0x46, 0x6f, 0x6f, 0x52, 0x65, 187 | 0x71, 0x12, 0x10, 0x0a, 0x03, 0x42, 0x61, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 188 | 0x42, 0x61, 0x72, 0x12, 0x10, 0x0a, 0x03, 0x42, 0x75, 0x7a, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 189 | 0x52, 0x03, 0x42, 0x75, 0x7a, 0x22, 0x37, 0x0a, 0x07, 0x46, 0x6f, 0x6f, 0x52, 0x65, 0x73, 0x70, 190 | 0x12, 0x12, 0x0a, 0x04, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 191 | 0x43, 0x6f, 0x64, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 192 | 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x2a, 0x28, 193 | 0x0a, 0x02, 0x49, 0x44, 0x12, 0x05, 0x0a, 0x01, 0x5f, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x46, 194 | 0x6f, 0x6f, 0x52, 0x65, 0x71, 0x49, 0x44, 0x10, 0x01, 0x12, 0x0d, 0x0a, 0x09, 0x46, 0x6f, 0x6f, 195 | 0x52, 0x65, 0x73, 0x70, 0x49, 0x44, 0x10, 0x02, 0x42, 0x09, 0x5a, 0x07, 0x2f, 0x63, 0x6f, 0x6d, 196 | 0x6d, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, 197 | } 198 | 199 | var ( 200 | file_message_proto_rawDescOnce sync.Once 201 | file_message_proto_rawDescData = file_message_proto_rawDesc 202 | ) 203 | 204 | func file_message_proto_rawDescGZIP() []byte { 205 | file_message_proto_rawDescOnce.Do(func() { 206 | file_message_proto_rawDescData = protoimpl.X.CompressGZIP(file_message_proto_rawDescData) 207 | }) 208 | return file_message_proto_rawDescData 209 | } 210 | 211 | var file_message_proto_enumTypes = make([]protoimpl.EnumInfo, 1) 212 | var file_message_proto_msgTypes = make([]protoimpl.MessageInfo, 2) 213 | var file_message_proto_goTypes = []interface{}{ 214 | (ID)(0), // 0: common.ID 215 | (*FooReq)(nil), // 1: common.FooReq 216 | (*FooResp)(nil), // 2: common.FooResp 217 | } 218 | var file_message_proto_depIdxs = []int32{ 219 | 0, // [0:0] is the sub-list for method output_type 220 | 0, // [0:0] is the sub-list for method input_type 221 | 0, // [0:0] is the sub-list for extension type_name 222 | 0, // [0:0] is the sub-list for extension extendee 223 | 0, // [0:0] is the sub-list for field type_name 224 | } 225 | 226 | func init() { file_message_proto_init() } 227 | func file_message_proto_init() { 228 | if File_message_proto != nil { 229 | return 230 | } 231 | if !protoimpl.UnsafeEnabled { 232 | file_message_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 233 | switch v := v.(*FooReq); i { 234 | case 0: 235 | return &v.state 236 | case 1: 237 | return &v.sizeCache 238 | case 2: 239 | return &v.unknownFields 240 | default: 241 | return nil 242 | } 243 | } 244 | file_message_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { 245 | switch v := v.(*FooResp); i { 246 | case 0: 247 | return &v.state 248 | case 1: 249 | return &v.sizeCache 250 | case 2: 251 | return &v.unknownFields 252 | default: 253 | return nil 254 | } 255 | } 256 | } 257 | type x struct{} 258 | out := protoimpl.TypeBuilder{ 259 | File: protoimpl.DescBuilder{ 260 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 261 | RawDescriptor: file_message_proto_rawDesc, 262 | NumEnums: 1, 263 | NumMessages: 2, 264 | NumExtensions: 0, 265 | NumServices: 0, 266 | }, 267 | GoTypes: file_message_proto_goTypes, 268 | DependencyIndexes: file_message_proto_depIdxs, 269 | EnumInfos: file_message_proto_enumTypes, 270 | MessageInfos: file_message_proto_msgTypes, 271 | }.Build() 272 | File_message_proto = out.File 273 | file_message_proto_rawDesc = nil 274 | file_message_proto_goTypes = nil 275 | file_message_proto_depIdxs = nil 276 | } 277 | -------------------------------------------------------------------------------- /internal/examples/tcp/proto_packet/common/message.proto: -------------------------------------------------------------------------------- 1 | syntax = 'proto3'; 2 | 3 | package common; 4 | 5 | option go_package = '/common'; 6 | 7 | enum ID { 8 | _ = 0; 9 | FooReqID = 1; 10 | FooRespID = 2; 11 | } 12 | 13 | message FooReq { 14 | string Bar = 1; 15 | int32 Buz = 2; 16 | } 17 | message FooResp { 18 | int32 Code = 1; 19 | string Message = 2; 20 | } 21 | -------------------------------------------------------------------------------- /internal/examples/tcp/proto_packet/common/packer.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "github.com/DarthPestilane/easytcp" 7 | "io" 8 | ) 9 | 10 | // CustomPacker treats packet as: 11 | // 12 | // totalSize(4)|id(4)|data(n) 13 | // 14 | // | segment | type | size | remark | 15 | // | ----------- | ------ | ------- | --------------------- | 16 | // | `totalSize` | uint32 | 4 | the whole packet size | 17 | // | `id` | uint32 | 4 | | 18 | // | `data` | []byte | dynamic | | 19 | type CustomPacker struct{} 20 | 21 | func (p *CustomPacker) Pack(msg *easytcp.Message) ([]byte, error) { 22 | buffer := make([]byte, 4+4+len(msg.Data())) 23 | p.byteOrder().PutUint32(buffer[0:4], uint32(len(buffer))) // write totalSize 24 | p.byteOrder().PutUint32(buffer[4:8], uint32(msg.ID().(ID))) // write id 25 | copy(buffer[8:], msg.Data()) // write data 26 | return buffer, nil 27 | } 28 | 29 | func (p *CustomPacker) Unpack(reader io.Reader) (*easytcp.Message, error) { 30 | headerBuffer := make([]byte, 4+4) 31 | if _, err := io.ReadFull(reader, headerBuffer); err != nil { 32 | if err == io.EOF { 33 | return nil, err 34 | } 35 | return nil, fmt.Errorf("read header from reader err: %s", err) 36 | } 37 | totalSize := p.byteOrder().Uint32(headerBuffer[:4]) // read totalSize 38 | id := ID(p.byteOrder().Uint32(headerBuffer[4:])) // read id 39 | 40 | // read data 41 | dataSize := totalSize - 4 - 4 42 | data := make([]byte, dataSize) 43 | if _, err := io.ReadFull(reader, data); err != nil { 44 | return nil, fmt.Errorf("read data from reader err: %s", err) 45 | } 46 | return easytcp.NewMessage(id, data), nil 47 | } 48 | 49 | func (*CustomPacker) byteOrder() binary.ByteOrder { 50 | return binary.BigEndian 51 | } 52 | -------------------------------------------------------------------------------- /internal/examples/tcp/proto_packet/common/packer_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "bytes" 5 | "github.com/DarthPestilane/easytcp" 6 | "github.com/stretchr/testify/assert" 7 | "testing" 8 | ) 9 | 10 | func TestCustomPacker(t *testing.T) { 11 | packer := &CustomPacker{} 12 | 13 | msg := easytcp.NewMessage(ID(123), []byte("data")) 14 | packedBytes, err := packer.Pack(msg) 15 | assert.NoError(t, err) 16 | assert.NotNil(t, packedBytes) 17 | 18 | r := bytes.NewBuffer(packedBytes) 19 | newMsg, err := packer.Unpack(r) 20 | assert.NoError(t, err) 21 | assert.NotNil(t, newMsg) 22 | assert.Equal(t, newMsg.ID(), msg.ID()) 23 | assert.Equal(t, newMsg.Data(), msg.Data()) 24 | } 25 | -------------------------------------------------------------------------------- /internal/examples/tcp/proto_packet/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/DarthPestilane/easytcp" 5 | "github.com/DarthPestilane/easytcp/internal/examples/fixture" 6 | "github.com/DarthPestilane/easytcp/internal/examples/tcp/proto_packet/common" 7 | "github.com/sirupsen/logrus" 8 | "google.golang.org/protobuf/proto" 9 | ) 10 | 11 | var log *logrus.Logger 12 | 13 | func init() { 14 | log = logrus.New() 15 | log.SetLevel(logrus.DebugLevel) 16 | } 17 | 18 | func main() { 19 | srv := easytcp.NewServer(&easytcp.ServerOption{ 20 | Packer: &common.CustomPacker{}, 21 | Codec: &easytcp.ProtobufCodec{}, 22 | }) 23 | 24 | srv.AddRoute(common.ID_FooReqID, handle, logTransmission(&common.FooReq{}, &common.FooResp{})) 25 | 26 | if err := srv.Run(fixture.ServerAddr); err != nil { 27 | log.Errorf("serve err: %s", err) 28 | } 29 | } 30 | 31 | func handle(c easytcp.Context) { 32 | var reqData common.FooReq 33 | _ = c.Bind(&reqData) 34 | err := c.SetResponse(common.ID_FooRespID, &common.FooResp{ 35 | Code: 2, 36 | Message: "success", 37 | }) 38 | if err != nil { 39 | log.Errorf("set response failed: %s", err) 40 | } 41 | } 42 | 43 | func logTransmission(req, resp proto.Message) easytcp.MiddlewareFunc { 44 | return func(next easytcp.HandlerFunc) easytcp.HandlerFunc { 45 | return func(c easytcp.Context) { 46 | if err := c.Bind(req); err == nil { 47 | log.Debugf("recv | id: %d; size: %d; data: %s", c.Request().ID(), len(c.Request().Data()), req) 48 | } 49 | defer func() { 50 | respMsg := c.Response() 51 | if respMsg != nil { 52 | _ = c.Session().Codec().Decode(respMsg.Data(), resp) 53 | log.Infof("send | id: %d; size: %d; data: %s", respMsg.ID(), len(respMsg.Data()), resp) 54 | } 55 | }() 56 | next(c) 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /internal/examples/tcp/simple/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/DarthPestilane/easytcp" 5 | "github.com/DarthPestilane/easytcp/internal/examples/fixture" 6 | "github.com/DarthPestilane/easytcp/internal/examples/tcp/simple/common" 7 | "github.com/sirupsen/logrus" 8 | "net" 9 | "time" 10 | ) 11 | 12 | func main() { 13 | conn, err := net.Dial("tcp", fixture.ServerAddr) 14 | if err != nil { 15 | panic(err) 16 | } 17 | log := logrus.New() 18 | packer := easytcp.NewDefaultPacker() 19 | go func() { 20 | // write loop 21 | for { 22 | time.Sleep(time.Second) 23 | msg := easytcp.NewMessage(common.MsgIdPingReq, []byte("ping, ping, ping")) 24 | packedBytes, err := packer.Pack(msg) 25 | if err != nil { 26 | panic(err) 27 | } 28 | if _, err := conn.Write(packedBytes); err != nil { 29 | panic(err) 30 | } 31 | log.Infof("snd >>> | id:(%d) size:(%d) data: %s", msg.ID(), len(msg.Data()), msg.Data()) 32 | } 33 | }() 34 | go func() { 35 | // read loop 36 | for { 37 | msg, err := packer.Unpack(conn) 38 | if err != nil { 39 | panic(err) 40 | } 41 | log.Infof("rec <<< | id:(%d) size:(%d) data: %s", msg.ID(), len(msg.Data()), msg.Data()) 42 | } 43 | }() 44 | select {} 45 | } 46 | -------------------------------------------------------------------------------- /internal/examples/tcp/simple/common/message.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | _ int = iota 5 | MsgIdPingReq 6 | MsgIdPingAck 7 | ) 8 | -------------------------------------------------------------------------------- /internal/examples/tcp/simple/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/DarthPestilane/easytcp" 6 | "github.com/DarthPestilane/easytcp/internal/examples/fixture" 7 | "github.com/DarthPestilane/easytcp/internal/examples/tcp/simple/common" 8 | "github.com/sirupsen/logrus" 9 | "os" 10 | "os/signal" 11 | "runtime" 12 | "syscall" 13 | "time" 14 | ) 15 | 16 | var log *logrus.Logger 17 | 18 | func init() { 19 | log = logrus.New() 20 | log.SetLevel(logrus.TraceLevel) 21 | } 22 | 23 | func main() { 24 | // go printGoroutineNum() 25 | 26 | easytcp.SetLogger(log) 27 | s := easytcp.NewServer(&easytcp.ServerOption{ 28 | SocketReadBufferSize: 1024 * 1024, 29 | SocketWriteBufferSize: 1024 * 1024, 30 | ReadTimeout: time.Second * 3, 31 | WriteTimeout: time.Second * 3, 32 | RespQueueSize: 0, 33 | Packer: easytcp.NewDefaultPacker(), 34 | Codec: nil, 35 | }) 36 | s.OnSessionCreate = func(sess easytcp.Session) { 37 | log.Infof("session created: %v", sess.ID()) 38 | } 39 | s.OnSessionClose = func(sess easytcp.Session) { 40 | log.Warnf("session closed: %v", sess.ID()) 41 | } 42 | 43 | // register global middlewares 44 | s.Use(fixture.RecoverMiddleware(log), logMiddleware) 45 | 46 | // register a route 47 | s.AddRoute(common.MsgIdPingReq, func(c easytcp.Context) { 48 | c.SetResponseMessage(easytcp.NewMessage(common.MsgIdPingAck, []byte("pong"))) 49 | }) 50 | 51 | go func() { 52 | if err := s.Run(fixture.ServerAddr); err != nil { 53 | log.Errorf("serve err: %s", err) 54 | } 55 | }() 56 | 57 | sigCh := make(chan os.Signal, 1) 58 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 59 | <-sigCh 60 | if err := s.Stop(); err != nil { 61 | log.Errorf("server stopped err: %s", err) 62 | } 63 | time.Sleep(time.Second * 3) 64 | } 65 | 66 | func logMiddleware(next easytcp.HandlerFunc) easytcp.HandlerFunc { 67 | return func(c easytcp.Context) { 68 | req := c.Request() 69 | log.Infof("rec <<< id:(%d) size:(%d) data: %s", req.ID(), len(req.Data()), req.Data()) 70 | defer func() { 71 | resp := c.Response() 72 | log.Infof("snd >>> id:(%d) size:(%d) data: %s", resp.ID(), len(resp.Data()), resp.Data()) 73 | }() 74 | next(c) 75 | } 76 | } 77 | 78 | // nolint: deadcode, unused 79 | func printGoroutineNum() { 80 | for { 81 | fmt.Println("goroutine num: ", runtime.NumGoroutine()) 82 | time.Sleep(time.Second) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/examples/tcp/simple_tls/client/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "crypto/x509" 6 | "github.com/DarthPestilane/easytcp" 7 | "github.com/DarthPestilane/easytcp/internal/examples/fixture" 8 | "github.com/DarthPestilane/easytcp/internal/examples/tcp/simple_tls/common" 9 | "github.com/sirupsen/logrus" 10 | "io/ioutil" 11 | "time" 12 | ) 13 | 14 | func main() { 15 | pem, err := ioutil.ReadFile("internal/test_data/certificates/cert.pem") 16 | if err != nil { 17 | panic(err) 18 | } 19 | rootCerts := x509.NewCertPool() 20 | rootCerts.AppendCertsFromPEM(pem) 21 | tlsCfg := &tls.Config{ 22 | RootCAs: rootCerts, 23 | InsecureSkipVerify: true, 24 | } 25 | conn, err := tls.Dial("tcp", fixture.ServerAddr, tlsCfg) 26 | if err != nil { 27 | panic(err) 28 | } 29 | 30 | log := logrus.New() 31 | packer := easytcp.NewDefaultPacker() 32 | go func() { 33 | // write loop 34 | for { 35 | time.Sleep(time.Second) 36 | msg := easytcp.NewMessage(common.MsgIdPingReq, []byte("ping, ping, ping")) 37 | packedBytes, err := packer.Pack(msg) 38 | if err != nil { 39 | panic(err) 40 | } 41 | if _, err := conn.Write(packedBytes); err != nil { 42 | panic(err) 43 | } 44 | log.Infof("snd >>> | id:(%d) size:(%d) data: %s", msg.ID(), len(msg.Data()), msg.Data()) 45 | } 46 | }() 47 | go func() { 48 | // read loop 49 | for { 50 | msg, err := packer.Unpack(conn) 51 | if err != nil { 52 | panic(err) 53 | } 54 | log.Infof("rec <<< | id:(%d) size:(%d) data: %s", msg.ID(), len(msg.Data()), msg.Data()) 55 | } 56 | }() 57 | select {} 58 | } 59 | -------------------------------------------------------------------------------- /internal/examples/tcp/simple_tls/common/message.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | _ int = iota 5 | MsgIdPingReq 6 | MsgIdPingAck 7 | ) 8 | -------------------------------------------------------------------------------- /internal/examples/tcp/simple_tls/server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "github.com/DarthPestilane/easytcp" 7 | "github.com/DarthPestilane/easytcp/internal/examples/fixture" 8 | "github.com/DarthPestilane/easytcp/internal/examples/tcp/simple_tls/common" 9 | "github.com/sirupsen/logrus" 10 | "os" 11 | "os/signal" 12 | "runtime" 13 | "syscall" 14 | "time" 15 | ) 16 | 17 | var log *logrus.Logger 18 | 19 | func init() { 20 | log = logrus.New() 21 | log.SetLevel(logrus.TraceLevel) 22 | } 23 | 24 | func main() { 25 | // go printGoroutineNum() 26 | 27 | easytcp.SetLogger(log) 28 | s := easytcp.NewServer(&easytcp.ServerOption{ 29 | ReadTimeout: time.Second * 3, 30 | WriteTimeout: time.Second * 3, 31 | RespQueueSize: -1, 32 | Packer: easytcp.NewDefaultPacker(), 33 | Codec: nil, 34 | }) 35 | s.OnSessionCreate = func(sess easytcp.Session) { 36 | log.Infof("session created: %v", sess.ID()) 37 | } 38 | s.OnSessionClose = func(sess easytcp.Session) { 39 | log.Warnf("session closed: %v", sess.ID()) 40 | } 41 | 42 | // register global middlewares 43 | s.Use(fixture.RecoverMiddleware(log), logMiddleware) 44 | 45 | // register a route 46 | s.AddRoute(common.MsgIdPingReq, func(c easytcp.Context) { 47 | c.SetResponseMessage(easytcp.NewMessage(common.MsgIdPingAck, []byte("pong, pong, pong"))) 48 | }) 49 | 50 | cert, err := tls.LoadX509KeyPair("internal/test_data/certificates/cert.pem", "internal/test_data/certificates/cert.key") 51 | if err != nil { 52 | panic(err) 53 | } 54 | go func() { 55 | if err := s.RunTLS(fixture.ServerAddr, &tls.Config{Certificates: []tls.Certificate{cert}}); err != nil { 56 | log.Errorf("serve err: %s", err) 57 | } 58 | }() 59 | 60 | sigCh := make(chan os.Signal, 1) 61 | signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) 62 | <-sigCh 63 | if err := s.Stop(); err != nil { 64 | log.Errorf("server stopped err: %s", err) 65 | } 66 | time.Sleep(time.Second * 3) 67 | } 68 | 69 | func logMiddleware(next easytcp.HandlerFunc) easytcp.HandlerFunc { 70 | return func(c easytcp.Context) { 71 | req := c.Request() 72 | log.Infof("rec <<< id:(%d) size:(%d) data: %s", req.ID, len(req.Data()), req.Data()) 73 | defer func() { 74 | resp := c.Response() 75 | log.Infof("snd >>> id:(%d) size:(%d) data: %s", resp.ID, len(resp.Data()), resp.Data()) 76 | }() 77 | next(c) 78 | } 79 | } 80 | 81 | // nolint: deadcode, unused 82 | func printGoroutineNum() { 83 | for { 84 | fmt.Println("goroutine num: ", runtime.NumGoroutine()) 85 | time.Sleep(time.Second) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /internal/mock/codec_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/DarthPestilane/easytcp (interfaces: Codec) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | reflect "reflect" 9 | 10 | gomock "github.com/golang/mock/gomock" 11 | ) 12 | 13 | // MockCodec is a mock of Codec interface. 14 | type MockCodec struct { 15 | ctrl *gomock.Controller 16 | recorder *MockCodecMockRecorder 17 | } 18 | 19 | // MockCodecMockRecorder is the mock recorder for MockCodec. 20 | type MockCodecMockRecorder struct { 21 | mock *MockCodec 22 | } 23 | 24 | // NewMockCodec creates a new mock instance. 25 | func NewMockCodec(ctrl *gomock.Controller) *MockCodec { 26 | mock := &MockCodec{ctrl: ctrl} 27 | mock.recorder = &MockCodecMockRecorder{mock} 28 | return mock 29 | } 30 | 31 | // EXPECT returns an object that allows the caller to indicate expected use. 32 | func (m *MockCodec) EXPECT() *MockCodecMockRecorder { 33 | return m.recorder 34 | } 35 | 36 | // Decode mocks base method. 37 | func (m *MockCodec) Decode(arg0 []byte, arg1 interface{}) error { 38 | m.ctrl.T.Helper() 39 | ret := m.ctrl.Call(m, "Decode", arg0, arg1) 40 | ret0, _ := ret[0].(error) 41 | return ret0 42 | } 43 | 44 | // Decode indicates an expected call of Decode. 45 | func (mr *MockCodecMockRecorder) Decode(arg0, arg1 interface{}) *gomock.Call { 46 | mr.mock.ctrl.T.Helper() 47 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Decode", reflect.TypeOf((*MockCodec)(nil).Decode), arg0, arg1) 48 | } 49 | 50 | // Encode mocks base method. 51 | func (m *MockCodec) Encode(arg0 interface{}) ([]byte, error) { 52 | m.ctrl.T.Helper() 53 | ret := m.ctrl.Call(m, "Encode", arg0) 54 | ret0, _ := ret[0].([]byte) 55 | ret1, _ := ret[1].(error) 56 | return ret0, ret1 57 | } 58 | 59 | // Encode indicates an expected call of Encode. 60 | func (mr *MockCodecMockRecorder) Encode(arg0 interface{}) *gomock.Call { 61 | mr.mock.ctrl.T.Helper() 62 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Encode", reflect.TypeOf((*MockCodec)(nil).Encode), arg0) 63 | } 64 | -------------------------------------------------------------------------------- /internal/mock/server_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: net (interfaces: Listener,Error,Conn) 3 | 4 | // Package mock is a generated GoMock package. 5 | package mock 6 | 7 | import ( 8 | net "net" 9 | reflect "reflect" 10 | time "time" 11 | 12 | gomock "github.com/golang/mock/gomock" 13 | ) 14 | 15 | // MockListener is a mock of Listener interface. 16 | type MockListener struct { 17 | ctrl *gomock.Controller 18 | recorder *MockListenerMockRecorder 19 | } 20 | 21 | // MockListenerMockRecorder is the mock recorder for MockListener. 22 | type MockListenerMockRecorder struct { 23 | mock *MockListener 24 | } 25 | 26 | // NewMockListener creates a new mock instance. 27 | func NewMockListener(ctrl *gomock.Controller) *MockListener { 28 | mock := &MockListener{ctrl: ctrl} 29 | mock.recorder = &MockListenerMockRecorder{mock} 30 | return mock 31 | } 32 | 33 | // EXPECT returns an object that allows the caller to indicate expected use. 34 | func (m *MockListener) EXPECT() *MockListenerMockRecorder { 35 | return m.recorder 36 | } 37 | 38 | // Accept mocks base method. 39 | func (m *MockListener) Accept() (net.Conn, error) { 40 | m.ctrl.T.Helper() 41 | ret := m.ctrl.Call(m, "Accept") 42 | ret0, _ := ret[0].(net.Conn) 43 | ret1, _ := ret[1].(error) 44 | return ret0, ret1 45 | } 46 | 47 | // Accept indicates an expected call of Accept. 48 | func (mr *MockListenerMockRecorder) Accept() *gomock.Call { 49 | mr.mock.ctrl.T.Helper() 50 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Accept", reflect.TypeOf((*MockListener)(nil).Accept)) 51 | } 52 | 53 | // Addr mocks base method. 54 | func (m *MockListener) Addr() net.Addr { 55 | m.ctrl.T.Helper() 56 | ret := m.ctrl.Call(m, "Addr") 57 | ret0, _ := ret[0].(net.Addr) 58 | return ret0 59 | } 60 | 61 | // Addr indicates an expected call of Addr. 62 | func (mr *MockListenerMockRecorder) Addr() *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Addr", reflect.TypeOf((*MockListener)(nil).Addr)) 65 | } 66 | 67 | // Close mocks base method. 68 | func (m *MockListener) Close() error { 69 | m.ctrl.T.Helper() 70 | ret := m.ctrl.Call(m, "Close") 71 | ret0, _ := ret[0].(error) 72 | return ret0 73 | } 74 | 75 | // Close indicates an expected call of Close. 76 | func (mr *MockListenerMockRecorder) Close() *gomock.Call { 77 | mr.mock.ctrl.T.Helper() 78 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockListener)(nil).Close)) 79 | } 80 | 81 | // MockError is a mock of Error interface. 82 | type MockError struct { 83 | ctrl *gomock.Controller 84 | recorder *MockErrorMockRecorder 85 | } 86 | 87 | // MockErrorMockRecorder is the mock recorder for MockError. 88 | type MockErrorMockRecorder struct { 89 | mock *MockError 90 | } 91 | 92 | // NewMockError creates a new mock instance. 93 | func NewMockError(ctrl *gomock.Controller) *MockError { 94 | mock := &MockError{ctrl: ctrl} 95 | mock.recorder = &MockErrorMockRecorder{mock} 96 | return mock 97 | } 98 | 99 | // EXPECT returns an object that allows the caller to indicate expected use. 100 | func (m *MockError) EXPECT() *MockErrorMockRecorder { 101 | return m.recorder 102 | } 103 | 104 | // Error mocks base method. 105 | func (m *MockError) Error() string { 106 | m.ctrl.T.Helper() 107 | ret := m.ctrl.Call(m, "Error") 108 | ret0, _ := ret[0].(string) 109 | return ret0 110 | } 111 | 112 | // Error indicates an expected call of Error. 113 | func (mr *MockErrorMockRecorder) Error() *gomock.Call { 114 | mr.mock.ctrl.T.Helper() 115 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Error", reflect.TypeOf((*MockError)(nil).Error)) 116 | } 117 | 118 | // Temporary mocks base method. 119 | func (m *MockError) Temporary() bool { 120 | m.ctrl.T.Helper() 121 | ret := m.ctrl.Call(m, "Temporary") 122 | ret0, _ := ret[0].(bool) 123 | return ret0 124 | } 125 | 126 | // Temporary indicates an expected call of Temporary. 127 | func (mr *MockErrorMockRecorder) Temporary() *gomock.Call { 128 | mr.mock.ctrl.T.Helper() 129 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Temporary", reflect.TypeOf((*MockError)(nil).Temporary)) 130 | } 131 | 132 | // Timeout mocks base method. 133 | func (m *MockError) Timeout() bool { 134 | m.ctrl.T.Helper() 135 | ret := m.ctrl.Call(m, "Timeout") 136 | ret0, _ := ret[0].(bool) 137 | return ret0 138 | } 139 | 140 | // Timeout indicates an expected call of Timeout. 141 | func (mr *MockErrorMockRecorder) Timeout() *gomock.Call { 142 | mr.mock.ctrl.T.Helper() 143 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Timeout", reflect.TypeOf((*MockError)(nil).Timeout)) 144 | } 145 | 146 | // MockConn is a mock of Conn interface. 147 | type MockConn struct { 148 | ctrl *gomock.Controller 149 | recorder *MockConnMockRecorder 150 | } 151 | 152 | // MockConnMockRecorder is the mock recorder for MockConn. 153 | type MockConnMockRecorder struct { 154 | mock *MockConn 155 | } 156 | 157 | // NewMockConn creates a new mock instance. 158 | func NewMockConn(ctrl *gomock.Controller) *MockConn { 159 | mock := &MockConn{ctrl: ctrl} 160 | mock.recorder = &MockConnMockRecorder{mock} 161 | return mock 162 | } 163 | 164 | // EXPECT returns an object that allows the caller to indicate expected use. 165 | func (m *MockConn) EXPECT() *MockConnMockRecorder { 166 | return m.recorder 167 | } 168 | 169 | // Close mocks base method. 170 | func (m *MockConn) Close() error { 171 | m.ctrl.T.Helper() 172 | ret := m.ctrl.Call(m, "Close") 173 | ret0, _ := ret[0].(error) 174 | return ret0 175 | } 176 | 177 | // Close indicates an expected call of Close. 178 | func (mr *MockConnMockRecorder) Close() *gomock.Call { 179 | mr.mock.ctrl.T.Helper() 180 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockConn)(nil).Close)) 181 | } 182 | 183 | // LocalAddr mocks base method. 184 | func (m *MockConn) LocalAddr() net.Addr { 185 | m.ctrl.T.Helper() 186 | ret := m.ctrl.Call(m, "LocalAddr") 187 | ret0, _ := ret[0].(net.Addr) 188 | return ret0 189 | } 190 | 191 | // LocalAddr indicates an expected call of LocalAddr. 192 | func (mr *MockConnMockRecorder) LocalAddr() *gomock.Call { 193 | mr.mock.ctrl.T.Helper() 194 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LocalAddr", reflect.TypeOf((*MockConn)(nil).LocalAddr)) 195 | } 196 | 197 | // Read mocks base method. 198 | func (m *MockConn) Read(arg0 []byte) (int, error) { 199 | m.ctrl.T.Helper() 200 | ret := m.ctrl.Call(m, "Read", arg0) 201 | ret0, _ := ret[0].(int) 202 | ret1, _ := ret[1].(error) 203 | return ret0, ret1 204 | } 205 | 206 | // Read indicates an expected call of Read. 207 | func (mr *MockConnMockRecorder) Read(arg0 interface{}) *gomock.Call { 208 | mr.mock.ctrl.T.Helper() 209 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockConn)(nil).Read), arg0) 210 | } 211 | 212 | // RemoteAddr mocks base method. 213 | func (m *MockConn) RemoteAddr() net.Addr { 214 | m.ctrl.T.Helper() 215 | ret := m.ctrl.Call(m, "RemoteAddr") 216 | ret0, _ := ret[0].(net.Addr) 217 | return ret0 218 | } 219 | 220 | // RemoteAddr indicates an expected call of RemoteAddr. 221 | func (mr *MockConnMockRecorder) RemoteAddr() *gomock.Call { 222 | mr.mock.ctrl.T.Helper() 223 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoteAddr", reflect.TypeOf((*MockConn)(nil).RemoteAddr)) 224 | } 225 | 226 | // SetDeadline mocks base method. 227 | func (m *MockConn) SetDeadline(arg0 time.Time) error { 228 | m.ctrl.T.Helper() 229 | ret := m.ctrl.Call(m, "SetDeadline", arg0) 230 | ret0, _ := ret[0].(error) 231 | return ret0 232 | } 233 | 234 | // SetDeadline indicates an expected call of SetDeadline. 235 | func (mr *MockConnMockRecorder) SetDeadline(arg0 interface{}) *gomock.Call { 236 | mr.mock.ctrl.T.Helper() 237 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetDeadline", reflect.TypeOf((*MockConn)(nil).SetDeadline), arg0) 238 | } 239 | 240 | // SetReadDeadline mocks base method. 241 | func (m *MockConn) SetReadDeadline(arg0 time.Time) error { 242 | m.ctrl.T.Helper() 243 | ret := m.ctrl.Call(m, "SetReadDeadline", arg0) 244 | ret0, _ := ret[0].(error) 245 | return ret0 246 | } 247 | 248 | // SetReadDeadline indicates an expected call of SetReadDeadline. 249 | func (mr *MockConnMockRecorder) SetReadDeadline(arg0 interface{}) *gomock.Call { 250 | mr.mock.ctrl.T.Helper() 251 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReadDeadline", reflect.TypeOf((*MockConn)(nil).SetReadDeadline), arg0) 252 | } 253 | 254 | // SetWriteDeadline mocks base method. 255 | func (m *MockConn) SetWriteDeadline(arg0 time.Time) error { 256 | m.ctrl.T.Helper() 257 | ret := m.ctrl.Call(m, "SetWriteDeadline", arg0) 258 | ret0, _ := ret[0].(error) 259 | return ret0 260 | } 261 | 262 | // SetWriteDeadline indicates an expected call of SetWriteDeadline. 263 | func (mr *MockConnMockRecorder) SetWriteDeadline(arg0 interface{}) *gomock.Call { 264 | mr.mock.ctrl.T.Helper() 265 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetWriteDeadline", reflect.TypeOf((*MockConn)(nil).SetWriteDeadline), arg0) 266 | } 267 | 268 | // Write mocks base method. 269 | func (m *MockConn) Write(arg0 []byte) (int, error) { 270 | m.ctrl.T.Helper() 271 | ret := m.ctrl.Call(m, "Write", arg0) 272 | ret0, _ := ret[0].(int) 273 | ret1, _ := ret[1].(error) 274 | return ret0, ret1 275 | } 276 | 277 | // Write indicates an expected call of Write. 278 | func (mr *MockConnMockRecorder) Write(arg0 interface{}) *gomock.Call { 279 | mr.mock.ctrl.T.Helper() 280 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Write", reflect.TypeOf((*MockConn)(nil).Write), arg0) 281 | } 282 | -------------------------------------------------------------------------------- /internal/test_data/certificates/cert.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDXR3nB7F/pyCP4 3 | uZJH9ZmLasplwfyU1jjg/OqhhyJgt4lCbsvshKMATOINVkZECA/XnjAb+naAL+4K 4 | Kuh5CTvywPBqeaBJyZboOJYz4EP/yJCKu6HiFyABnfbtrE5UwBJWzPLOhvg9aaQ9 5 | RBLqpH9MtWQoSFBmfpVOMRnnirt4V9VNDq2u+fUbVmWwjWypNqnNnfqp9yZfpPhz 6 | a9CaSMt1HtKRr1N9wriS3u3/gk7bQizfzM5LWUnSsQoCXlW/uQSae+Jp4wZC62wQ 7 | tPrc+4shU6WjkJp2hLGD96Hqg4hn/gMZey7r6x54r6qJq2eK7R0lihZqrk0u3jOA 8 | 5BM4w22/AgMBAAECggEAP/dD7WQfx6YpUhFJrsoCGpPbmcq/EzZn5iogSWiOxXAj 9 | f7LOMvPiPweZL19QN4yxsF6XaujL5yDWuPyw4K3muyWrCegjwWj9xvhxYO6lJc5h 10 | bGd+HWDDqNdX9Jz7FWGQ0WvKRaWDAzRtkaq1eDTygkdvgCykfx/jmz0ptkvZklLz 11 | TWM/nr1fxswHkQVfCWxRuRf+XYPXba86H1iM7RYCIFiApO7GMR20QjiJzmavjVFJ 12 | 7akv28Ca9PIFJkmGoDB0tlBhfKkWo5hcHA0t0Uz6k5Ks+a1qKOlp/avRKFE3xBy9 13 | SNuolhYRzeEU2N2GbBu0FfC7nG3FnI0mxnF3H0+60QKBgQDu0i9PTARuhSgHYUGg 14 | DzwBKcpsprNOpOQ4tmA8JHghuAB79hIqqaWVpWYU9YzBNlA9/AQMYFm++pX6JYkq 15 | rma3rkIROzs+W/9dtoGmDgDjmmWmBE3H9wgpxRjmCZ65kyVNRK0MymemVdgrN300 16 | Jymsq6GZFD5pMTFIEFgzFzLmtwKBgQDmw8Z4I/mHUhJ3iXugBlPAAkLBERREPwYB 17 | vTGzcBxD5upzFyUrq21TGNhAPS8ss+GnmPQOEBCi8ose5ehxJ6f6FxuziVuowyn0 18 | Bx/jtCMHVLGmOM5SKJFodL1vsXufV57O638k1EvLsgH2vmI3oK6m/8EOZ/gnD0S6 19 | 6I6Ja5tpOQKBgGCI73yzMptmEa8h/f/wCIZD2UIgBBzHBEV0WuQUrcabdP6mkeNS 20 | 3c7mo6PXOcUj6j2T8CL8k2piKluJ7q8k/fpDYwtKEQF8+HVt/2wa/vsBfxMjbDln 21 | PpJ7zDu4KcPDmfFo0DZ6Xnla+91EOcTqC6tzWQfiqfOlYdFKYgyM1RNzAoGAYQD5 22 | A/WzZePlKWScmBcwy2zn3LquN0X6425BXzmIWC7QbRLUqDfGnAC8nrxZgUQYXlhY 23 | dzTfmW+1dYaVoENYRDPEjEL4ScfIcfEwwYouk11R1Bra+ARfo3Y3T6Ve3wt5EWhD 24 | KRsoxXaNhshfBx0/bani4Ihp8xli/eLWUAPw71kCgYBkxxW2u5hhrjEU14oeaH0a 25 | Z0Os/gJlLZiWgNFi3Fm6iNbzwigELHX5q+HT7ybOYhCWOVYAyrZpvUogNNtqFQh5 26 | NvlaHy0OpzdwUVXRuadgoUXMo7UE6DyYKjl8ddDSUcIAiyhOczs0qOk+d5O0cBC8 27 | 6PsLBGgTUrEFCTzCUKPz9g== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /internal/test_data/certificates/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDIjCCAgoCCQCTBFILb0p2XTANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJD 3 | TjELMAkGA1UECAwCR0QxCzAJBgNVBAcMAlNaMRMwEQYDVQQKDApBY21lLCBJbmMu 4 | MRUwEwYDVQQDDAxBY21lIFJvb3QgQ0EwHhcNMjIwMzE4MDc0NDEzWhcNMjMwMzE4 5 | MDc0NDEzWjBTMQswCQYDVQQGEwJDTjELMAkGA1UECAwCR0QxCzAJBgNVBAcMAlNa 6 | MRMwEQYDVQQKDApBY21lLCBJbmMuMRUwEwYDVQQDDAxBY21lIFJvb3QgQ0EwggEi 7 | MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDXR3nB7F/pyCP4uZJH9ZmLaspl 8 | wfyU1jjg/OqhhyJgt4lCbsvshKMATOINVkZECA/XnjAb+naAL+4KKuh5CTvywPBq 9 | eaBJyZboOJYz4EP/yJCKu6HiFyABnfbtrE5UwBJWzPLOhvg9aaQ9RBLqpH9MtWQo 10 | SFBmfpVOMRnnirt4V9VNDq2u+fUbVmWwjWypNqnNnfqp9yZfpPhza9CaSMt1HtKR 11 | r1N9wriS3u3/gk7bQizfzM5LWUnSsQoCXlW/uQSae+Jp4wZC62wQtPrc+4shU6Wj 12 | kJp2hLGD96Hqg4hn/gMZey7r6x54r6qJq2eK7R0lihZqrk0u3jOA5BM4w22/AgMB 13 | AAEwDQYJKoZIhvcNAQELBQADggEBAH+qa+pxRVGcItLiVV+S6LjUPVK7syDF2DSC 14 | rx/iyX6wui37ht5x8IHmn9y8jbVso7lGZgEssaDN4ezrGIgrbKafMyeyZYjIKkfa 15 | xDS+r11K7JoeJ8ZwbOQrZFrtDDotXRylaOVmlhBfTBOaJFAwaceb7SWizll3m3cb 16 | 5+s6DqQWAwS2FLyBCnHgJ6H200mPedPa8jd7fzF1aVk6WxlS9q3H0hRR4G1KZbYH 17 | vkQe6hw+OzY6jPMXTzCDlBF7t951imtoQ3wPR141XMFVaAKB/vQhUwKmtQayT0uU 18 | vW3FSjiadLfQ6QDQuNZFd30o3SCmZ/y0+Ak0psm2RMiurmX3Sd0= 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /internal/test_data/msgpack/sample.go: -------------------------------------------------------------------------------- 1 | package msgpack 2 | 3 | // Sample is a sample struct for test only. 4 | type Sample struct { 5 | Foo string `msgpack:"foo"` 6 | Bar int64 `msgpack:"bar"` 7 | Baz map[int]string `msgpack:"baz"` 8 | } 9 | -------------------------------------------------------------------------------- /internal/test_data/pb/make.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | protoc --go_out=. --go_opt=paths=source_relative *.proto; 4 | -------------------------------------------------------------------------------- /internal/test_data/pb/test.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // versions: 3 | // protoc-gen-go v1.27.1 4 | // protoc v3.17.3 5 | // source: test.proto 6 | 7 | package pb 8 | 9 | import ( 10 | "google.golang.org/protobuf/reflect/protoreflect" 11 | "google.golang.org/protobuf/runtime/protoimpl" 12 | "reflect" 13 | "sync" 14 | ) 15 | 16 | const ( 17 | // Verify that this generated code is sufficiently up-to-date. 18 | _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) 19 | // Verify that runtime/protoimpl is sufficiently up-to-date. 20 | _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) 21 | ) 22 | 23 | type Sample struct { 24 | state protoimpl.MessageState 25 | sizeCache protoimpl.SizeCache 26 | unknownFields protoimpl.UnknownFields 27 | 28 | Foo string `protobuf:"bytes,1,opt,name=Foo,proto3" json:"Foo,omitempty"` 29 | Bar int64 `protobuf:"varint,2,opt,name=Bar,proto3" json:"Bar,omitempty"` 30 | } 31 | 32 | func (x *Sample) Reset() { 33 | *x = Sample{} 34 | if protoimpl.UnsafeEnabled { 35 | mi := &file_test_proto_msgTypes[0] 36 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 37 | ms.StoreMessageInfo(mi) 38 | } 39 | } 40 | 41 | func (x *Sample) String() string { 42 | return protoimpl.X.MessageStringOf(x) 43 | } 44 | 45 | func (*Sample) ProtoMessage() {} 46 | 47 | func (x *Sample) ProtoReflect() protoreflect.Message { 48 | mi := &file_test_proto_msgTypes[0] 49 | if protoimpl.UnsafeEnabled && x != nil { 50 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) 51 | if ms.LoadMessageInfo() == nil { 52 | ms.StoreMessageInfo(mi) 53 | } 54 | return ms 55 | } 56 | return mi.MessageOf(x) 57 | } 58 | 59 | // Deprecated: Use Sample.ProtoReflect.Descriptor instead. 60 | func (*Sample) Descriptor() ([]byte, []int) { 61 | return file_test_proto_rawDescGZIP(), []int{0} 62 | } 63 | 64 | func (x *Sample) GetFoo() string { 65 | if x != nil { 66 | return x.Foo 67 | } 68 | return "" 69 | } 70 | 71 | func (x *Sample) GetBar() int64 { 72 | if x != nil { 73 | return x.Bar 74 | } 75 | return 0 76 | } 77 | 78 | var File_test_proto protoreflect.FileDescriptor 79 | 80 | var file_test_proto_rawDesc = []byte{ 81 | 0x0a, 0x0a, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x07, 0x74, 0x65, 82 | 0x73, 0x74, 0x5f, 0x70, 0x62, 0x22, 0x2c, 0x0a, 0x06, 0x53, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x12, 83 | 0x10, 0x0a, 0x03, 0x46, 0x6f, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x46, 0x6f, 84 | 0x6f, 0x12, 0x10, 0x0a, 0x03, 0x42, 0x61, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x03, 85 | 0x42, 0x61, 0x72, 0x42, 0x05, 0x5a, 0x03, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 86 | 0x6f, 0x33, 87 | } 88 | 89 | var ( 90 | file_test_proto_rawDescOnce sync.Once 91 | file_test_proto_rawDescData = file_test_proto_rawDesc 92 | ) 93 | 94 | func file_test_proto_rawDescGZIP() []byte { 95 | file_test_proto_rawDescOnce.Do(func() { 96 | file_test_proto_rawDescData = protoimpl.X.CompressGZIP(file_test_proto_rawDescData) 97 | }) 98 | return file_test_proto_rawDescData 99 | } 100 | 101 | var file_test_proto_msgTypes = make([]protoimpl.MessageInfo, 1) 102 | var file_test_proto_goTypes = []interface{}{ 103 | (*Sample)(nil), // 0: test_pb.Sample 104 | } 105 | var file_test_proto_depIdxs = []int32{ 106 | 0, // [0:0] is the sub-list for method output_type 107 | 0, // [0:0] is the sub-list for method input_type 108 | 0, // [0:0] is the sub-list for extension type_name 109 | 0, // [0:0] is the sub-list for extension extendee 110 | 0, // [0:0] is the sub-list for field type_name 111 | } 112 | 113 | func init() { file_test_proto_init() } 114 | func file_test_proto_init() { 115 | if File_test_proto != nil { 116 | return 117 | } 118 | if !protoimpl.UnsafeEnabled { 119 | file_test_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { 120 | switch v := v.(*Sample); i { 121 | case 0: 122 | return &v.state 123 | case 1: 124 | return &v.sizeCache 125 | case 2: 126 | return &v.unknownFields 127 | default: 128 | return nil 129 | } 130 | } 131 | } 132 | type x struct{} 133 | out := protoimpl.TypeBuilder{ 134 | File: protoimpl.DescBuilder{ 135 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), 136 | RawDescriptor: file_test_proto_rawDesc, 137 | NumEnums: 0, 138 | NumMessages: 1, 139 | NumExtensions: 0, 140 | NumServices: 0, 141 | }, 142 | GoTypes: file_test_proto_goTypes, 143 | DependencyIndexes: file_test_proto_depIdxs, 144 | MessageInfos: file_test_proto_msgTypes, 145 | }.Build() 146 | File_test_proto = out.File 147 | file_test_proto_rawDesc = nil 148 | file_test_proto_goTypes = nil 149 | file_test_proto_depIdxs = nil 150 | } 151 | -------------------------------------------------------------------------------- /internal/test_data/pb/test.proto: -------------------------------------------------------------------------------- 1 | syntax = 'proto3'; 2 | 3 | package test_pb; 4 | 5 | option go_package = '/pb'; 6 | 7 | message Sample { 8 | string Foo = 1; 9 | int64 Bar = 2; 10 | } 11 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | ) 8 | 9 | var _ Logger = &DefaultLogger{} 10 | 11 | // _log is the instance of Logger interface. 12 | var _log Logger = newDiscardLogger() 13 | 14 | // Logger is the generic interface for log recording. 15 | type Logger interface { 16 | Errorf(format string, args ...interface{}) 17 | Tracef(format string, args ...interface{}) 18 | } 19 | 20 | func newDiscardLogger() *DefaultLogger { 21 | return &DefaultLogger{ 22 | rawLogger: log.New(io.Discard, "easytcp", log.LstdFlags), 23 | } 24 | } 25 | 26 | // DefaultLogger is the default logger instance for this package. 27 | // DefaultLogger uses the built-in log.Logger. 28 | type DefaultLogger struct { 29 | rawLogger *log.Logger 30 | } 31 | 32 | // Errorf implements Logger Errorf method. 33 | func (d *DefaultLogger) Errorf(format string, args ...interface{}) { 34 | d.rawLogger.Printf("[ERROR] %s", fmt.Sprintf(format, args...)) 35 | } 36 | 37 | // Tracef implements Logger Tracef method. 38 | func (d *DefaultLogger) Tracef(format string, args ...interface{}) { 39 | d.rawLogger.Printf("[TRACE] %s", fmt.Sprintf(format, args...)) 40 | } 41 | 42 | // Log returns the package logger. 43 | func Log() Logger { 44 | return _log 45 | } 46 | 47 | // SetLogger sets the package logger. 48 | func SetLogger(lg Logger) { 49 | _log = lg 50 | } 51 | -------------------------------------------------------------------------------- /logger_test.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "log" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | func newLogger() *DefaultLogger { 11 | return &DefaultLogger{ 12 | rawLogger: log.New(os.Stdout, "easytcp ", log.Ldate|log.Ltime|log.Lmicroseconds|log.Lmsgprefix), 13 | } 14 | } 15 | 16 | func TestDefaultLogger_Errorf(t *testing.T) { 17 | lg := newLogger() 18 | lg.Errorf("err: %s", "some error") 19 | } 20 | 21 | func TestDefaultLogger_Tracef(t *testing.T) { 22 | lg := newLogger() 23 | lg.Tracef("some trace info: %s", "here") 24 | } 25 | 26 | func TestSetLogger(t *testing.T) { 27 | lg := &mutedLogger{} 28 | SetLogger(lg) 29 | assert.Equal(t, Log(), lg) 30 | } 31 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | ) 7 | 8 | // NewMessage creates a Message pointer. 9 | func NewMessage(id interface{}, data []byte) *Message { 10 | return &Message{ 11 | id: id, 12 | data: data, 13 | } 14 | } 15 | 16 | // Message is the abstract of inbound and outbound message. 17 | type Message struct { 18 | id interface{} 19 | data []byte 20 | storage map[string]interface{} 21 | mu sync.RWMutex 22 | } 23 | 24 | // ID returns the id of current message. 25 | func (m *Message) ID() interface{} { 26 | return m.id 27 | } 28 | 29 | // Data returns the data part of current message. 30 | func (m *Message) Data() []byte { 31 | return m.data 32 | } 33 | 34 | // Set stores kv pair. 35 | func (m *Message) Set(key string, value interface{}) { 36 | m.mu.Lock() 37 | defer m.mu.Unlock() 38 | if m.storage == nil { 39 | m.storage = make(map[string]interface{}) 40 | } 41 | m.storage[key] = value 42 | } 43 | 44 | // Get retrieves the value according to the key. 45 | func (m *Message) Get(key string) (value interface{}, exists bool) { 46 | m.mu.RLock() 47 | defer m.mu.RUnlock() 48 | value, exists = m.storage[key] 49 | return 50 | } 51 | 52 | // MustGet retrieves the value according to the key. 53 | // Panics if key does not exist. 54 | func (m *Message) MustGet(key string) interface{} { 55 | if v, ok := m.Get(key); ok { 56 | return v 57 | } 58 | panic(fmt.Errorf("key `%s` does not exist", key)) 59 | } 60 | 61 | // Remove deletes the key from storage. 62 | func (m *Message) Remove(key string) { 63 | m.mu.Lock() 64 | defer m.mu.Unlock() 65 | delete(m.storage, key) 66 | } 67 | -------------------------------------------------------------------------------- /message_test.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestMessage_GetSetAndRemove(t *testing.T) { 9 | msg := &Message{} 10 | msg.Set("key", "test") 11 | 12 | v, ok := msg.Get("key") 13 | assert.True(t, ok) 14 | assert.Equal(t, v, "test") 15 | 16 | v, ok = msg.Get("not-found") 17 | assert.False(t, ok) 18 | assert.Nil(t, v) 19 | 20 | msg.Remove("key") 21 | v, ok = msg.Get("key") 22 | assert.False(t, ok) 23 | assert.Nil(t, v) 24 | } 25 | 26 | func TestMessage_MustGet(t *testing.T) { 27 | msg := &Message{} 28 | msg.Set("key", "test") 29 | 30 | v := msg.MustGet("key") 31 | assert.Equal(t, v, "test") 32 | 33 | assert.Panics(t, func() { msg.MustGet("not-found") }) 34 | } 35 | -------------------------------------------------------------------------------- /packer.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | "github.com/spf13/cast" 7 | "io" 8 | ) 9 | 10 | //go:generate mockgen -destination ./packer_mock.go -package easytcp . Packer 11 | 12 | // Packer is a generic interface to pack and unpack message packet. 13 | type Packer interface { 14 | // Pack packs Message into the packet to be written. 15 | Pack(msg *Message) ([]byte, error) 16 | 17 | // Unpack unpacks the message packet from reader, 18 | // returns the message, and error if error occurred. 19 | Unpack(reader io.Reader) (*Message, error) 20 | } 21 | 22 | var _ Packer = &DefaultPacker{} 23 | 24 | // NewDefaultPacker create a *DefaultPacker with initial field value. 25 | func NewDefaultPacker() *DefaultPacker { 26 | return &DefaultPacker{ 27 | MaxDataSize: 1 << 10 << 10, // 1MB 28 | } 29 | } 30 | 31 | // DefaultPacker is the default Packer used in session. 32 | // Treats the packet with the format: 33 | // 34 | // dataSize(4)|id(4)|data(n) 35 | // 36 | // | segment | type | size | remark | 37 | // | ---------- | ------ | ------- | ----------------------- | 38 | // | `dataSize` | uint32 | 4 | the size of `data` only | 39 | // | `id` | uint32 | 4 | | 40 | // | `data` | []byte | dynamic | | 41 | // . 42 | type DefaultPacker struct { 43 | // MaxDataSize represents the max size of `data` 44 | MaxDataSize int 45 | } 46 | 47 | func (d *DefaultPacker) bytesOrder() binary.ByteOrder { 48 | return binary.LittleEndian 49 | } 50 | 51 | // Pack implements the Packer Pack method. 52 | func (d *DefaultPacker) Pack(msg *Message) ([]byte, error) { 53 | dataSize := len(msg.Data()) 54 | if d.MaxDataSize > 0 && dataSize > d.MaxDataSize { 55 | return nil, fmt.Errorf("the dataSize %d is beyond the max: %d", dataSize, d.MaxDataSize) 56 | } 57 | buffer := make([]byte, 4+4+dataSize) 58 | d.bytesOrder().PutUint32(buffer[:4], uint32(dataSize)) // write dataSize 59 | id, err := cast.ToUint32E(msg.ID()) 60 | if err != nil { 61 | return nil, fmt.Errorf("invalid type of msg.ID: %s", err) 62 | } 63 | d.bytesOrder().PutUint32(buffer[4:8], id) // write id 64 | copy(buffer[8:], msg.Data()) // write data 65 | return buffer, nil 66 | } 67 | 68 | // Unpack implements the Packer Unpack method. 69 | // Unpack returns the message whose ID is type of int. 70 | // So we need use int id to register routes. 71 | func (d *DefaultPacker) Unpack(reader io.Reader) (*Message, error) { 72 | headerBuffer := make([]byte, 4+4) 73 | if _, err := io.ReadFull(reader, headerBuffer); err != nil { 74 | if err == io.EOF { 75 | return nil, err 76 | } 77 | return nil, fmt.Errorf("read size and id err: %s", err) 78 | } 79 | dataSize := d.bytesOrder().Uint32(headerBuffer[:4]) 80 | if d.MaxDataSize > 0 && int(dataSize) > d.MaxDataSize { 81 | return nil, fmt.Errorf("the dataSize %d is beyond the max: %d", dataSize, d.MaxDataSize) 82 | } 83 | id := d.bytesOrder().Uint32(headerBuffer[4:8]) 84 | data := make([]byte, dataSize) 85 | if _, err := io.ReadFull(reader, data); err != nil { 86 | if err == io.EOF { 87 | return nil, err 88 | } 89 | return nil, fmt.Errorf("read data err: %s", err) 90 | } 91 | return NewMessage(int(id), data), nil 92 | } 93 | -------------------------------------------------------------------------------- /packer_mock.go: -------------------------------------------------------------------------------- 1 | // Code generated by MockGen. DO NOT EDIT. 2 | // Source: github.com/DarthPestilane/easytcp (interfaces: Packer) 3 | 4 | // Package easytcp is a generated GoMock package. 5 | package easytcp 6 | 7 | import ( 8 | io "io" 9 | reflect "reflect" 10 | 11 | gomock "github.com/golang/mock/gomock" 12 | ) 13 | 14 | // MockPacker is a mock of Packer interface. 15 | type MockPacker struct { 16 | ctrl *gomock.Controller 17 | recorder *MockPackerMockRecorder 18 | } 19 | 20 | // MockPackerMockRecorder is the mock recorder for MockPacker. 21 | type MockPackerMockRecorder struct { 22 | mock *MockPacker 23 | } 24 | 25 | // NewMockPacker creates a new mock instance. 26 | func NewMockPacker(ctrl *gomock.Controller) *MockPacker { 27 | mock := &MockPacker{ctrl: ctrl} 28 | mock.recorder = &MockPackerMockRecorder{mock} 29 | return mock 30 | } 31 | 32 | // EXPECT returns an object that allows the caller to indicate expected use. 33 | func (m *MockPacker) EXPECT() *MockPackerMockRecorder { 34 | return m.recorder 35 | } 36 | 37 | // Pack mocks base method. 38 | func (m *MockPacker) Pack(arg0 *Message) ([]byte, error) { 39 | m.ctrl.T.Helper() 40 | ret := m.ctrl.Call(m, "Pack", arg0) 41 | ret0, _ := ret[0].([]byte) 42 | ret1, _ := ret[1].(error) 43 | return ret0, ret1 44 | } 45 | 46 | // Pack indicates an expected call of Pack. 47 | func (mr *MockPackerMockRecorder) Pack(arg0 interface{}) *gomock.Call { 48 | mr.mock.ctrl.T.Helper() 49 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Pack", reflect.TypeOf((*MockPacker)(nil).Pack), arg0) 50 | } 51 | 52 | // Unpack mocks base method. 53 | func (m *MockPacker) Unpack(arg0 io.Reader) (*Message, error) { 54 | m.ctrl.T.Helper() 55 | ret := m.ctrl.Call(m, "Unpack", arg0) 56 | ret0, _ := ret[0].(*Message) 57 | ret1, _ := ret[1].(error) 58 | return ret0, ret1 59 | } 60 | 61 | // Unpack indicates an expected call of Unpack. 62 | func (mr *MockPackerMockRecorder) Unpack(arg0 interface{}) *gomock.Call { 63 | mr.mock.ctrl.T.Helper() 64 | return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Unpack", reflect.TypeOf((*MockPacker)(nil).Unpack), arg0) 65 | } 66 | -------------------------------------------------------------------------------- /packer_test.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "github.com/stretchr/testify/assert" 7 | "reflect" 8 | "testing" 9 | ) 10 | 11 | func TestDefaultPacker_PackAndUnpack(t *testing.T) { 12 | packer := NewDefaultPacker() 13 | 14 | t.Run("when handle different types of id", func(t *testing.T) { 15 | var testIdInt = 1 16 | var testIdInt32 int32 = 1 17 | var testIdInt64 int64 = 1 18 | 19 | var testIdUint uint = 1 20 | var testIdUint32 uint32 = 1 21 | var testIdUint64 uint64 = 1 22 | 23 | ids := []interface{}{ 24 | testIdInt, &testIdInt, 25 | testIdInt32, &testIdInt32, 26 | testIdInt64, &testIdInt64, 27 | 28 | testIdUint, &testIdUint, 29 | testIdUint32, &testIdUint32, 30 | testIdUint64, &testIdUint64, 31 | } 32 | for _, id := range ids { 33 | msg := NewMessage(id, []byte("test")) 34 | packedBytes, err := packer.Pack(msg) 35 | assert.NoError(t, err) 36 | assert.NotNil(t, packedBytes) 37 | assert.Equal(t, packedBytes[8:], []byte("test")) 38 | 39 | r := bytes.NewBuffer(packedBytes) 40 | newMsg, err := packer.Unpack(r) 41 | assert.NoError(t, err) 42 | assert.NotNil(t, newMsg) 43 | assert.EqualValues(t, reflect.Indirect(reflect.ValueOf(msg.ID())).Interface(), newMsg.ID()) 44 | assert.Equal(t, newMsg.Data(), msg.Data()) 45 | } 46 | }) 47 | 48 | t.Run("when handle invalid type of id", func(t *testing.T) { 49 | msg := NewMessage("cannot cast to uint32", []byte("test")) 50 | packedBytes, err := packer.Pack(msg) 51 | assert.Error(t, err) 52 | assert.Nil(t, packedBytes) 53 | }) 54 | 55 | t.Run("when size is too big", func(t *testing.T) { 56 | r := bytes.NewBuffer(nil) 57 | assert.NoError(t, binary.Write(r, binary.BigEndian, uint32(packer.MaxDataSize+1))) 58 | assert.NoError(t, binary.Write(r, binary.BigEndian, uint32(1))) 59 | assert.NoError(t, binary.Write(r, binary.BigEndian, []byte("test"))) 60 | msg, err := packer.Unpack(r) 61 | assert.Error(t, err) 62 | assert.Nil(t, msg) 63 | }) 64 | } 65 | -------------------------------------------------------------------------------- /router.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "fmt" 5 | "github.com/olekukonko/tablewriter" 6 | "github.com/spf13/cast" 7 | "io" 8 | "os" 9 | "reflect" 10 | "runtime" 11 | "sort" 12 | "strings" 13 | ) 14 | 15 | func newRouter() *Router { 16 | return &Router{ 17 | handlerMapper: make(map[interface{}]HandlerFunc), 18 | middlewaresMapper: make(map[interface{}][]MiddlewareFunc), 19 | } 20 | } 21 | 22 | // Router is a router for incoming message. 23 | // Router routes the message to its handler and middlewares. 24 | type Router struct { 25 | // handlerMapper maps message's ID to handler. 26 | // Handler will be called around middlewares. 27 | handlerMapper map[interface{}]HandlerFunc 28 | 29 | // middlewaresMapper maps message's ID to a list of middlewares. 30 | // These middlewares will be called before the handler in handlerMapper. 31 | middlewaresMapper map[interface{}][]MiddlewareFunc 32 | 33 | // globalMiddlewares is a list of MiddlewareFunc. 34 | // globalMiddlewares will be called before the ones in middlewaresMapper. 35 | globalMiddlewares []MiddlewareFunc 36 | 37 | notFoundHandler HandlerFunc 38 | } 39 | 40 | // HandlerFunc is the function type for handlers. 41 | type HandlerFunc func(ctx Context) 42 | 43 | // MiddlewareFunc is the function type for middlewares. 44 | // A common pattern is like: 45 | // 46 | // var mf MiddlewareFunc = func(next HandlerFunc) HandlerFunc { 47 | // return func(ctx Context) { 48 | // next(ctx) 49 | // } 50 | // } 51 | type MiddlewareFunc func(next HandlerFunc) HandlerFunc 52 | 53 | var nilHandler HandlerFunc = func(ctx Context) {} 54 | 55 | // handleRequest walks ctx through middlewares and handler, 56 | // and returns response message. 57 | func (r *Router) handleRequest(ctx Context) { 58 | reqMsg := ctx.Request() 59 | if reqMsg == nil { 60 | return 61 | } 62 | var handler HandlerFunc 63 | if v, has := r.handlerMapper[reqMsg.ID()]; has { 64 | handler = v 65 | } 66 | 67 | var mws = r.globalMiddlewares 68 | if v, has := r.middlewaresMapper[reqMsg.ID()]; has { 69 | mws = append(mws, v...) // append to global ones 70 | } 71 | 72 | // create the handlers stack 73 | wrapped := r.wrapHandlers(handler, mws) 74 | 75 | // and call the handlers stack 76 | wrapped(ctx) 77 | } 78 | 79 | // wrapHandlers wraps handler and middlewares into a right order call stack. 80 | // Makes something like: 81 | // 82 | // var wrapped HandlerFunc = m1(m2(m3(handle))) 83 | func (r *Router) wrapHandlers(handler HandlerFunc, middles []MiddlewareFunc) (wrapped HandlerFunc) { 84 | if handler == nil { 85 | handler = r.notFoundHandler 86 | } 87 | if handler == nil { 88 | handler = nilHandler 89 | } 90 | wrapped = handler 91 | for i := len(middles) - 1; i >= 0; i-- { 92 | m := middles[i] 93 | wrapped = m(wrapped) 94 | } 95 | return wrapped 96 | } 97 | 98 | // register stores handler and middlewares for id. 99 | func (r *Router) register(id interface{}, h HandlerFunc, m ...MiddlewareFunc) { 100 | if h != nil { 101 | r.handlerMapper[id] = h 102 | } 103 | ms := make([]MiddlewareFunc, 0, len(m)) 104 | for _, mm := range m { 105 | if mm != nil { 106 | ms = append(ms, mm) 107 | } 108 | } 109 | if len(ms) != 0 { 110 | r.middlewaresMapper[id] = ms 111 | } 112 | } 113 | 114 | // registerMiddleware stores the global middlewares. 115 | func (r *Router) registerMiddleware(m ...MiddlewareFunc) { 116 | for _, mm := range m { 117 | if mm != nil { 118 | r.globalMiddlewares = append(r.globalMiddlewares, mm) 119 | } 120 | } 121 | } 122 | 123 | // printHandlers prints registered route handlers to console. 124 | func (r *Router) printHandlers(addr string) { 125 | var w io.Writer = os.Stdout 126 | 127 | _, _ = fmt.Fprintf(w, "\n[EASYTCP] Message-Route Table:\n") 128 | table := tablewriter.NewWriter(w) 129 | table.SetHeader([]string{"Message ID", "Route Handler", "Middleware"}) 130 | table.SetAutoFormatHeaders(false) // don't uppercase the header 131 | table.SetAutoWrapText(false) // respect the "\n" of cell content 132 | table.SetRowLine(true) 133 | table.SetColumnAlignment([]int{tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT}) 134 | 135 | // sort ids 136 | ids := make([]interface{}, 0, len(r.handlerMapper)) 137 | for id := range r.handlerMapper { 138 | ids = append(ids, id) 139 | } 140 | sort.Slice(ids, func(i, j int) bool { 141 | a, b := cast.ToString(ids[i]), cast.ToString(ids[j]) 142 | return a < b 143 | }) 144 | 145 | // add table row 146 | for _, id := range ids { 147 | // route handler 148 | h := r.handlerMapper[id] 149 | handlerName := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name() 150 | 151 | middlewareNames := make([]string, 0, len(r.globalMiddlewares)+len(r.middlewaresMapper[id])) 152 | // global middleware 153 | for _, m := range r.globalMiddlewares { 154 | middlewareName := fmt.Sprintf("%s(g)", runtime.FuncForPC(reflect.ValueOf(m).Pointer()).Name()) 155 | middlewareNames = append(middlewareNames, middlewareName) 156 | } 157 | 158 | // route middleware 159 | for _, m := range r.middlewaresMapper[id] { 160 | middlewareName := runtime.FuncForPC(reflect.ValueOf(m).Pointer()).Name() 161 | middlewareNames = append(middlewareNames, middlewareName) 162 | } 163 | 164 | table.Append([]string{fmt.Sprintf("%v", id), handlerName, strings.Join(middlewareNames, "\n")}) 165 | } 166 | 167 | table.Render() 168 | _, _ = fmt.Fprintf(w, "[EASYTCP] Serving at: %s\n\n", addr) 169 | } 170 | 171 | func (r *Router) setNotFoundHandler(handler HandlerFunc) { 172 | r.notFoundHandler = handler 173 | } 174 | -------------------------------------------------------------------------------- /router_context.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | // Context is a generic context in a message routing. 11 | // It allows us to pass variables between handler and middlewares. 12 | type Context interface { 13 | context.Context 14 | 15 | // WithContext sets the underline context. 16 | // It's very useful to control the workflow when send to response channel. 17 | WithContext(ctx context.Context) Context 18 | 19 | // Session returns the current session. 20 | Session() Session 21 | 22 | // SetSession sets session. 23 | SetSession(sess Session) Context 24 | 25 | // Request returns request message. 26 | Request() *Message 27 | 28 | // SetRequest encodes data with session's codec and sets request message. 29 | SetRequest(id, data interface{}) error 30 | 31 | // MustSetRequest encodes data with session's codec and sets request message. 32 | // panics on error. 33 | MustSetRequest(id, data interface{}) Context 34 | 35 | // SetRequestMessage sets request message directly. 36 | SetRequestMessage(msg *Message) Context 37 | 38 | // Bind decodes request message to v. 39 | Bind(v interface{}) error 40 | 41 | // Response returns the response message. 42 | Response() *Message 43 | 44 | // SetResponse encodes data with session's codec and sets response message. 45 | SetResponse(id, data interface{}) error 46 | 47 | // MustSetResponse encodes data with session's codec and sets response message. 48 | // panics on error. 49 | MustSetResponse(id, data interface{}) Context 50 | 51 | // SetResponseMessage sets response message directly. 52 | SetResponseMessage(msg *Message) Context 53 | 54 | // Send sends itself to current session. 55 | Send() bool 56 | 57 | // SendTo sends itself to session. 58 | SendTo(session Session) bool 59 | 60 | // Get returns key value from storage. 61 | Get(key string) (value interface{}, exists bool) 62 | 63 | // Set store key value into storage. 64 | Set(key string, value interface{}) 65 | 66 | // Remove deletes the key from storage. 67 | Remove(key string) 68 | 69 | // Copy returns a copy of Context. 70 | Copy() Context 71 | } 72 | 73 | var _ Context = &routeContext{} // implementation check 74 | 75 | // newContext creates a routeContext pointer. 76 | func newContext() *routeContext { 77 | return &routeContext{ 78 | rawCtx: context.Background(), 79 | } 80 | } 81 | 82 | // routeContext implements the Context interface. 83 | type routeContext struct { 84 | rawCtx context.Context 85 | mu sync.RWMutex 86 | storage map[string]interface{} 87 | session Session 88 | reqMsg *Message 89 | respMsg *Message 90 | } 91 | 92 | // Deadline implements the context.Context Deadline method. 93 | func (c *routeContext) Deadline() (time.Time, bool) { 94 | return c.rawCtx.Deadline() 95 | } 96 | 97 | // Done implements the context.Context Done method. 98 | func (c *routeContext) Done() <-chan struct{} { 99 | return c.rawCtx.Done() 100 | } 101 | 102 | // Err implements the context.Context Err method. 103 | func (c *routeContext) Err() error { 104 | return c.rawCtx.Err() 105 | } 106 | 107 | // Value implements the context.Context Value method. 108 | func (c *routeContext) Value(key interface{}) interface{} { 109 | if keyAsString, ok := key.(string); ok { 110 | val, _ := c.Get(keyAsString) 111 | return val 112 | } 113 | return nil 114 | } 115 | 116 | // WithContext sets the underline context. 117 | func (c *routeContext) WithContext(ctx context.Context) Context { 118 | c.rawCtx = ctx 119 | return c 120 | } 121 | 122 | // Session implements Context.Session method. 123 | func (c *routeContext) Session() Session { 124 | return c.session 125 | } 126 | 127 | // SetSession sets session. 128 | func (c *routeContext) SetSession(sess Session) Context { 129 | c.session = sess 130 | return c 131 | } 132 | 133 | // Request implements Context.Request method. 134 | func (c *routeContext) Request() *Message { 135 | return c.reqMsg 136 | } 137 | 138 | // SetRequest sets request by id and data. 139 | func (c *routeContext) SetRequest(id, data interface{}) error { 140 | codec := c.session.Codec() 141 | if codec == nil { 142 | return fmt.Errorf("codec is nil") 143 | } 144 | dataBytes, err := codec.Encode(data) 145 | if err != nil { 146 | return err 147 | } 148 | c.reqMsg = NewMessage(id, dataBytes) 149 | return nil 150 | } 151 | 152 | // MustSetRequest implements Context.MustSetRequest method. 153 | func (c *routeContext) MustSetRequest(id, data interface{}) Context { 154 | if err := c.SetRequest(id, data); err != nil { 155 | panic(err) 156 | } 157 | return c 158 | } 159 | 160 | // SetRequestMessage sets request message. 161 | func (c *routeContext) SetRequestMessage(msg *Message) Context { 162 | c.reqMsg = msg 163 | return c 164 | } 165 | 166 | // Bind implements Context.Bind method. 167 | func (c *routeContext) Bind(v interface{}) error { 168 | if c.session.Codec() == nil { 169 | return fmt.Errorf("message codec is nil") 170 | } 171 | return c.session.Codec().Decode(c.reqMsg.Data(), v) 172 | } 173 | 174 | // Response implements Context.Response method. 175 | func (c *routeContext) Response() *Message { 176 | return c.respMsg 177 | } 178 | 179 | // SetResponse implements Context.SetResponse method. 180 | func (c *routeContext) SetResponse(id, data interface{}) error { 181 | codec := c.session.Codec() 182 | if codec == nil { 183 | return fmt.Errorf("codec is nil") 184 | } 185 | dataBytes, err := codec.Encode(data) 186 | if err != nil { 187 | return err 188 | } 189 | c.respMsg = NewMessage(id, dataBytes) 190 | return nil 191 | } 192 | 193 | // MustSetResponse implements Context.MustSetResponse method. 194 | func (c *routeContext) MustSetResponse(id, data interface{}) Context { 195 | if err := c.SetResponse(id, data); err != nil { 196 | panic(err) 197 | } 198 | return c 199 | } 200 | 201 | // SetResponseMessage implements Context.SetResponseMessage method. 202 | func (c *routeContext) SetResponseMessage(msg *Message) Context { 203 | c.respMsg = msg 204 | return c 205 | } 206 | 207 | // Send implements Context.Send method. 208 | func (c *routeContext) Send() bool { 209 | return c.session.Send(c) 210 | } 211 | 212 | // SendTo implements Context.SendTo method. 213 | func (c *routeContext) SendTo(sess Session) bool { 214 | return sess.Send(c) 215 | } 216 | 217 | // Get implements Context.Get method. 218 | func (c *routeContext) Get(key string) (value interface{}, exists bool) { 219 | c.mu.RLock() 220 | value, exists = c.storage[key] 221 | c.mu.RUnlock() 222 | return 223 | } 224 | 225 | // Set implements Context.Set method. 226 | func (c *routeContext) Set(key string, value interface{}) { 227 | c.mu.Lock() 228 | if c.storage == nil { 229 | c.storage = make(map[string]interface{}) 230 | } 231 | c.storage[key] = value 232 | c.mu.Unlock() 233 | } 234 | 235 | // Remove implements Context.Remove method. 236 | func (c *routeContext) Remove(key string) { 237 | c.mu.Lock() 238 | delete(c.storage, key) 239 | c.mu.Unlock() 240 | } 241 | 242 | // Copy implements Context.Copy method. 243 | func (c *routeContext) Copy() Context { 244 | return &routeContext{ 245 | rawCtx: c.rawCtx, 246 | storage: c.storage, 247 | session: c.session, 248 | reqMsg: c.reqMsg, 249 | respMsg: c.respMsg, 250 | } 251 | } 252 | 253 | func (c *routeContext) reset() { 254 | c.rawCtx = context.Background() 255 | c.session = nil 256 | c.reqMsg = nil 257 | c.respMsg = nil 258 | c.storage = nil 259 | } 260 | -------------------------------------------------------------------------------- /router_context_test.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/DarthPestilane/easytcp/internal/mock" 7 | "github.com/golang/mock/gomock" 8 | "github.com/stretchr/testify/assert" 9 | "testing" 10 | ) 11 | 12 | func newTestContext(sess *session, reqMsg *Message) *routeContext { 13 | ctx := newContext() 14 | ctx.session = sess 15 | ctx.reqMsg = reqMsg 16 | return ctx 17 | } 18 | 19 | func Test_routeContext_Deadline(t *testing.T) { 20 | c := newTestContext(nil, nil) 21 | dl, ok := c.Deadline() 22 | assert.False(t, ok) 23 | assert.Zero(t, dl) 24 | } 25 | 26 | func Test_routeContext_Done(t *testing.T) { 27 | c := newTestContext(nil, nil) 28 | done := c.Done() 29 | assert.Nil(t, done) 30 | } 31 | 32 | func Test_routeContext_Err(t *testing.T) { 33 | c := newTestContext(nil, nil) 34 | assert.Nil(t, c.Err()) 35 | } 36 | 37 | func Test_routeContext_Value(t *testing.T) { 38 | c := newTestContext(nil, nil) 39 | assert.Nil(t, c.Value("not found")) 40 | c.Set("found", true) 41 | assert.True(t, c.Value("found").(bool)) 42 | 43 | assert.Nil(t, c.Value(123)) 44 | } 45 | 46 | func Test_routeContext_Get(t *testing.T) { 47 | c := newTestContext(nil, nil) 48 | v, ok := c.Get("not found") 49 | assert.False(t, ok) 50 | assert.Nil(t, v) 51 | 52 | c.Set("found", true) 53 | v, ok = c.Get("found") 54 | assert.True(t, ok) 55 | assert.True(t, v.(bool)) 56 | } 57 | 58 | func Test_routeContext_Set(t *testing.T) { 59 | c := newTestContext(nil, nil) 60 | c.Set("found", true) 61 | v, ok := c.storage["found"] 62 | assert.True(t, ok) 63 | assert.True(t, v.(bool)) 64 | } 65 | 66 | func Test_routeContext_Remove(t *testing.T) { 67 | c := newTestContext(nil, nil) 68 | c.Set("found", true) 69 | c.Remove("found") 70 | v, ok := c.Get("found") 71 | assert.False(t, ok) 72 | assert.Nil(t, v) 73 | } 74 | 75 | func Test_routeContext_Bind(t *testing.T) { 76 | t.Run("when session has codec", func(t *testing.T) { 77 | reqMsg := NewMessage(1, []byte(`{"data":"test"}`)) 78 | sess := newSession(nil, &sessionOption{Codec: &JsonCodec{}}) 79 | 80 | c := newTestContext(sess, reqMsg) 81 | data := make(map[string]string) 82 | assert.NoError(t, c.Bind(&data)) 83 | assert.EqualValues(t, data["data"], "test") 84 | 85 | // when dst is invalid 86 | var dst string 87 | assert.Error(t, c.Bind(&dst)) 88 | }) 89 | t.Run("when session hasn't codec", func(t *testing.T) { 90 | reqMsg := NewMessage(1, []byte("test")) 91 | sess := newSession(nil, &sessionOption{}) 92 | 93 | c := newTestContext(sess, reqMsg) 94 | var data string 95 | assert.Error(t, c.Bind(&data)) 96 | assert.Empty(t, data) 97 | }) 98 | } 99 | 100 | func Test_routeContext_Session(t *testing.T) { 101 | sess := newSession(nil, &sessionOption{}) 102 | 103 | c := newTestContext(sess, nil) 104 | assert.Equal(t, c.Session(), sess) 105 | } 106 | 107 | func Test_routeContext_SetResponse(t *testing.T) { 108 | t.Run("when session hasn't codec", func(t *testing.T) { 109 | reqMsg := NewMessage(1, []byte("test")) 110 | sess := newSession(nil, &sessionOption{}) 111 | 112 | c := newTestContext(sess, reqMsg) 113 | err := c.SetResponse(1, []string{"invalid", "data"}) 114 | assert.Error(t, err) 115 | assert.Nil(t, c.respMsg) 116 | }) 117 | t.Run("when encode failed", func(t *testing.T) { 118 | ctrl := gomock.NewController(t) 119 | defer ctrl.Finish() 120 | 121 | reqMsg := &Message{} 122 | codec := mock.NewMockCodec(ctrl) 123 | codec.EXPECT().Encode(gomock.Any()).Return(nil, fmt.Errorf("some err")) 124 | sess := newSession(nil, &sessionOption{Codec: codec}) 125 | 126 | c := newTestContext(sess, reqMsg) 127 | err := c.SetResponse(1, "test") 128 | assert.Error(t, err) 129 | assert.Nil(t, c.respMsg) 130 | }) 131 | t.Run("when succeed", func(t *testing.T) { 132 | ctrl := gomock.NewController(t) 133 | defer ctrl.Finish() 134 | reqMsg := NewMessage(1, []byte("test")) 135 | codec := mock.NewMockCodec(ctrl) 136 | codec.EXPECT().Encode(gomock.Any()).Return([]byte("test"), nil) 137 | sess := newSession(nil, &sessionOption{Codec: codec}) 138 | 139 | c := newTestContext(sess, reqMsg) 140 | err := c.SetResponse(1, "test") 141 | assert.NoError(t, err) 142 | assert.Equal(t, c.respMsg, reqMsg) 143 | }) 144 | } 145 | 146 | func Test_routeContext_Send(t *testing.T) { 147 | t.Run("when success", func(t *testing.T) { 148 | sess := newSession(nil, &sessionOption{}) 149 | ctx := newTestContext(sess, nil) 150 | ctx.SetResponseMessage(NewMessage(1, []byte("test"))) 151 | go ctx.Send() 152 | ctx2 := <-sess.respStream 153 | assert.Equal(t, ctx, ctx2) 154 | }) 155 | } 156 | 157 | func Test_routeContext_SendTo(t *testing.T) { 158 | t.Run("when success", func(t *testing.T) { 159 | sess1 := newSession(nil, &sessionOption{}) 160 | sess2 := newSession(nil, &sessionOption{}) 161 | ctx := newTestContext(sess1, nil) 162 | ctx.SetResponseMessage(NewMessage(1, []byte("test"))) 163 | go ctx.SendTo(sess2) 164 | ctx2 := <-sess2.respStream 165 | assert.Equal(t, ctx, ctx2) 166 | }) 167 | } 168 | 169 | func Test_routeContext_reset(t *testing.T) { 170 | sess := newSession(nil, &sessionOption{}) 171 | reqMsg := NewMessage(1, []byte("test")) 172 | ctx := newTestContext(sess, reqMsg) 173 | ctx.reset() 174 | assert.Equal(t, ctx.rawCtx, context.Background()) 175 | assert.Nil(t, ctx.session) 176 | assert.Nil(t, ctx.reqMsg) 177 | assert.Nil(t, ctx.respMsg) 178 | assert.Empty(t, ctx.storage) 179 | } 180 | 181 | func Test_routeContext_Copy(t *testing.T) { 182 | ctx := newTestContext(nil, nil) 183 | ctx.SetResponseMessage(NewMessage(1, []byte("resp origin"))) 184 | 185 | ctx2 := ctx.Copy() 186 | ctx2.SetResponseMessage(NewMessage(2, []byte("resp copy"))) 187 | 188 | assert.EqualValues(t, ctx.respMsg.ID(), 1) 189 | assert.Equal(t, ctx.respMsg.Data(), []byte("resp origin")) 190 | assert.EqualValues(t, ctx2.Response().ID(), 2) 191 | assert.Equal(t, ctx2.Response().Data(), []byte("resp copy")) 192 | } 193 | 194 | func Test_routeContext_MustSetResponse(t *testing.T) { 195 | t.Run("when session hasn't codec", func(t *testing.T) { 196 | reqMsg := NewMessage(1, []byte("test")) 197 | sess := newSession(nil, &sessionOption{}) 198 | 199 | c := newTestContext(sess, reqMsg) 200 | assert.Panics(t, func() { 201 | c.MustSetResponse(1, []string{"invalid", "data"}) 202 | }) 203 | }) 204 | t.Run("when encode failed", func(t *testing.T) { 205 | ctrl := gomock.NewController(t) 206 | defer ctrl.Finish() 207 | 208 | reqMsg := &Message{} 209 | codec := mock.NewMockCodec(ctrl) 210 | codec.EXPECT().Encode(gomock.Any()).Return(nil, fmt.Errorf("some err")) 211 | sess := newSession(nil, &sessionOption{Codec: codec}) 212 | 213 | c := newTestContext(sess, reqMsg) 214 | assert.Panics(t, func() { 215 | c.MustSetResponse(1, "test") 216 | }) 217 | }) 218 | t.Run("when succeed", func(t *testing.T) { 219 | ctrl := gomock.NewController(t) 220 | defer ctrl.Finish() 221 | reqMsg := NewMessage(1, []byte("test")) 222 | codec := mock.NewMockCodec(ctrl) 223 | codec.EXPECT().Encode(gomock.Any()).Return([]byte("test"), nil) 224 | sess := newSession(nil, &sessionOption{Codec: codec}) 225 | 226 | c := newTestContext(sess, reqMsg) 227 | assert.NotPanics(t, func() { 228 | assert.Equal(t, c.MustSetResponse(1, "test"), c) 229 | }) 230 | }) 231 | } 232 | 233 | func Test_routeContext_SetSession(t *testing.T) { 234 | sess := newSession(nil, &sessionOption{}) 235 | c := newTestContext(nil, nil) 236 | assert.Equal(t, c.SetSession(sess), c) 237 | assert.Equal(t, c.Session(), sess) 238 | } 239 | 240 | func Test_routeContext_SetRequest(t *testing.T) { 241 | t.Run("when session hasn't codec", func(t *testing.T) { 242 | sess := newSession(nil, &sessionOption{}) 243 | c := newTestContext(sess, nil) 244 | err := c.SetRequest(1, []string{"invalid", "data"}) 245 | assert.Error(t, err) 246 | assert.Nil(t, c.reqMsg) 247 | }) 248 | t.Run("when encode failed", func(t *testing.T) { 249 | ctrl := gomock.NewController(t) 250 | defer ctrl.Finish() 251 | 252 | codec := mock.NewMockCodec(ctrl) 253 | codec.EXPECT().Encode(gomock.Any()).Return(nil, fmt.Errorf("some err")) 254 | sess := newSession(nil, &sessionOption{Codec: codec}) 255 | 256 | c := newTestContext(sess, nil) 257 | err := c.SetRequest(1, "test") 258 | assert.Error(t, err) 259 | assert.Nil(t, c.reqMsg) 260 | }) 261 | t.Run("when succeed", func(t *testing.T) { 262 | ctrl := gomock.NewController(t) 263 | defer ctrl.Finish() 264 | reqMsg := NewMessage(1, []byte("test")) 265 | codec := mock.NewMockCodec(ctrl) 266 | codec.EXPECT().Encode(gomock.Any()).Return([]byte("test"), nil) 267 | sess := newSession(nil, &sessionOption{Codec: codec}) 268 | 269 | c := newTestContext(sess, nil) 270 | err := c.SetRequest(1, "test") 271 | assert.NoError(t, err) 272 | assert.Equal(t, c.reqMsg, reqMsg) 273 | }) 274 | } 275 | 276 | func Test_routeContext_MustSetRequest(t *testing.T) { 277 | t.Run("when session hasn't codec", func(t *testing.T) { 278 | sess := newSession(nil, &sessionOption{}) 279 | 280 | c := newTestContext(sess, nil) 281 | assert.Panics(t, func() { 282 | c.MustSetRequest(1, []string{"invalid", "data"}) 283 | }) 284 | }) 285 | t.Run("when encode failed", func(t *testing.T) { 286 | ctrl := gomock.NewController(t) 287 | defer ctrl.Finish() 288 | 289 | codec := mock.NewMockCodec(ctrl) 290 | codec.EXPECT().Encode(gomock.Any()).Return(nil, fmt.Errorf("some err")) 291 | sess := newSession(nil, &sessionOption{Codec: codec}) 292 | 293 | c := newTestContext(sess, nil) 294 | assert.Panics(t, func() { 295 | c.MustSetRequest(1, "test") 296 | }) 297 | }) 298 | t.Run("when succeed", func(t *testing.T) { 299 | ctrl := gomock.NewController(t) 300 | defer ctrl.Finish() 301 | 302 | codec := mock.NewMockCodec(ctrl) 303 | codec.EXPECT().Encode(gomock.Any()).Return([]byte("test"), nil) 304 | sess := newSession(nil, &sessionOption{Codec: codec}) 305 | 306 | c := newTestContext(sess, nil) 307 | assert.NotPanics(t, func() { 308 | assert.Equal(t, c.MustSetRequest(1, "test"), c) 309 | }) 310 | }) 311 | } 312 | 313 | func Test_routeContext_SetRequestMessage(t *testing.T) { 314 | reqMsg := NewMessage(1, []byte("test")) 315 | c := newContext() 316 | c.SetRequestMessage(reqMsg) 317 | assert.Equal(t, c.reqMsg, reqMsg) 318 | } 319 | -------------------------------------------------------------------------------- /router_test.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "reflect" 6 | "runtime" 7 | "testing" 8 | ) 9 | 10 | func TestRouter_register(t *testing.T) { 11 | rt := newRouter() 12 | 13 | var id = 1 14 | 15 | rt.register(id, nil) 16 | _, ok := rt.handlerMapper[id] 17 | assert.False(t, ok) 18 | _, ok = rt.middlewaresMapper[id] 19 | assert.False(t, ok) 20 | 21 | h := nilHandler 22 | m1 := func(next HandlerFunc) HandlerFunc { 23 | return func(ctx Context) { 24 | next(ctx) 25 | } 26 | } 27 | m2 := func(next HandlerFunc) HandlerFunc { 28 | return func(ctx Context) { 29 | next(ctx) 30 | } 31 | } 32 | rt.register(id, h, m1, nil, m2) 33 | v, ok := rt.handlerMapper[id] 34 | assert.True(t, ok) 35 | expect := runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name() 36 | actual := runtime.FuncForPC(reflect.ValueOf(v).Pointer()).Name() 37 | assert.Equal(t, expect, actual) 38 | mhs, ok := rt.middlewaresMapper[id] 39 | assert.True(t, ok) 40 | expects := []MiddlewareFunc{m1, m2} 41 | for i, mh := range mhs { 42 | expect := runtime.FuncForPC(reflect.ValueOf(expects[i]).Pointer()).Name() 43 | actual := runtime.FuncForPC(reflect.ValueOf(mh).Pointer()).Name() 44 | assert.Equal(t, expect, actual) 45 | } 46 | } 47 | 48 | func TestRouter_registerMiddleware(t *testing.T) { 49 | rt := newRouter() 50 | 51 | rt.registerMiddleware() 52 | assert.Len(t, rt.globalMiddlewares, 0) 53 | 54 | rt.registerMiddleware(nil, nil) 55 | assert.Len(t, rt.globalMiddlewares, 0) 56 | 57 | m1 := func(next HandlerFunc) HandlerFunc { 58 | return func(ctx Context) { 59 | next(ctx) 60 | } 61 | } 62 | m2 := func(next HandlerFunc) HandlerFunc { 63 | return func(ctx Context) { 64 | next(ctx) 65 | } 66 | } 67 | m3 := func(next HandlerFunc) HandlerFunc { 68 | return func(ctx Context) { 69 | next(ctx) 70 | } 71 | } 72 | rt.registerMiddleware(m1, m2) 73 | assert.Len(t, rt.globalMiddlewares, 2) 74 | 75 | rt.registerMiddleware(m3) 76 | assert.Len(t, rt.globalMiddlewares, 3) 77 | 78 | expects := []MiddlewareFunc{m1, m2, m3} 79 | for i, m := range rt.globalMiddlewares { 80 | expect := runtime.FuncForPC(reflect.ValueOf(expects[i]).Pointer()).Name() 81 | actual := runtime.FuncForPC(reflect.ValueOf(m).Pointer()).Name() 82 | assert.Equal(t, expect, actual) 83 | } 84 | } 85 | 86 | func TestRouter_handleReq(t *testing.T) { 87 | t.Run("when request message is nil", func(t *testing.T) { 88 | rt := newRouter() 89 | ctx := &routeContext{} 90 | rt.handleRequest(ctx) 91 | }) 92 | t.Run("when handler and middlewares not found", func(t *testing.T) { 93 | rt := newRouter() 94 | reqMsg := NewMessage(1, []byte("test")) 95 | ctx := &routeContext{reqMsg: reqMsg} 96 | rt.handleRequest(ctx) 97 | assert.Nil(t, ctx.respMsg) 98 | }) 99 | t.Run("when handler and middlewares found", func(t *testing.T) { 100 | rt := newRouter() 101 | var id = 1 102 | rt.register(id, nilHandler, func(next HandlerFunc) HandlerFunc { 103 | return func(ctx Context) { next(ctx) } 104 | }) 105 | 106 | reqMsg := NewMessage(1, []byte("test")) 107 | ctx := &routeContext{reqMsg: reqMsg} 108 | rt.handleRequest(ctx) 109 | assert.Nil(t, ctx.respMsg) 110 | }) 111 | } 112 | 113 | func TestRouter_wrapHandlers(t *testing.T) { 114 | rt := newRouter() 115 | t.Run("it works when there's no handler nor middleware", func(t *testing.T) { 116 | wrap := rt.wrapHandlers(nil, nil) 117 | ctx := &routeContext{} 118 | wrap(ctx) 119 | assert.Nil(t, ctx.respMsg) 120 | }) 121 | t.Run("it should invoke handlers in the right order", func(t *testing.T) { 122 | result := make([]string, 0) 123 | 124 | middles := []MiddlewareFunc{ 125 | func(next HandlerFunc) HandlerFunc { 126 | return func(ctx Context) { 127 | result = append(result, "m1-before") 128 | next(ctx) 129 | } 130 | }, 131 | func(next HandlerFunc) HandlerFunc { 132 | return func(ctx Context) { 133 | result = append(result, "m2-before") 134 | next(ctx) 135 | result = append(result, "m2-after") 136 | } 137 | }, 138 | func(next HandlerFunc) HandlerFunc { 139 | return func(ctx Context) { 140 | next(ctx) 141 | result = append(result, "m3-after") 142 | } 143 | }, 144 | } 145 | var handler HandlerFunc = func(ctx Context) { 146 | result = append(result, "done") 147 | ctx.SetResponseMessage(NewMessage(2, []byte("done"))) 148 | } 149 | 150 | wrap := rt.wrapHandlers(handler, middles) 151 | ctx := &routeContext{} 152 | wrap(ctx) 153 | assert.EqualValues(t, ctx.respMsg.Data(), "done") 154 | assert.Equal(t, result, []string{"m1-before", "m2-before", "done", "m3-after", "m2-after"}) 155 | }) 156 | } 157 | 158 | func TestRouter_printHandlers(t *testing.T) { 159 | t.Run("when there's no route registered", func(t *testing.T) { 160 | rt := newRouter() 161 | rt.printHandlers("localhost") 162 | }) 163 | t.Run("when there are routes registered", func(t *testing.T) { 164 | rt := newRouter() 165 | 166 | m1 := func(next HandlerFunc) HandlerFunc { 167 | return func(ctx Context) { 168 | next(ctx) 169 | } 170 | } 171 | 172 | m2 := func(next HandlerFunc) HandlerFunc { 173 | return func(ctx Context) { 174 | next(ctx) 175 | } 176 | } 177 | m3 := func(next HandlerFunc) HandlerFunc { 178 | return func(ctx Context) { 179 | next(ctx) 180 | } 181 | } 182 | m4 := func(next HandlerFunc) HandlerFunc { 183 | return func(ctx Context) { 184 | next(ctx) 185 | } 186 | } 187 | rt.registerMiddleware(m1) 188 | rt.registerMiddleware(m2) 189 | 190 | rt.register(1234, nilHandler, m3, m4) 191 | rt.register(12345678, nilHandler) 192 | rt.register(12345, nilHandler) 193 | rt.register(123456, nilHandler) 194 | rt.printHandlers("localhost") 195 | }) 196 | } 197 | 198 | func TestRouter_setNotFoundHandler(t *testing.T) { 199 | rt := newRouter() 200 | assert.Nil(t, rt.notFoundHandler) 201 | rt.setNotFoundHandler(func(ctx Context) {}) 202 | assert.NotNil(t, rt.notFoundHandler) 203 | } 204 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "net" 7 | "time" 8 | ) 9 | 10 | //go:generate mockgen -destination internal/mock/server_mock.go -package mock net Listener,Error,Conn 11 | 12 | // Server is a server for TCP connections. 13 | type Server struct { 14 | Listener net.Listener 15 | 16 | // Packer is the message packer, will be passed to session. 17 | Packer Packer 18 | 19 | // Codec is the message codec, will be passed to session. 20 | Codec Codec 21 | 22 | // OnSessionCreate is an event hook, will be invoked when session's created. 23 | OnSessionCreate func(sess Session) 24 | 25 | // OnSessionClose is an event hook, will be invoked when session's closed. 26 | OnSessionClose func(sess Session) 27 | 28 | socketReadBufferSize int 29 | socketWriteBufferSize int 30 | socketSendDelay bool 31 | readTimeout time.Duration 32 | writeTimeout time.Duration 33 | respQueueSize int 34 | router *Router 35 | printRoutes bool 36 | acceptingC chan struct{} 37 | stoppedC chan struct{} 38 | asyncRouter bool 39 | } 40 | 41 | // ServerOption is the option for Server. 42 | type ServerOption struct { 43 | SocketReadBufferSize int // sets the socket read buffer size. 44 | SocketWriteBufferSize int // sets the socket write buffer size. 45 | SocketSendDelay bool // sets the socket delay or not. 46 | ReadTimeout time.Duration // sets the timeout for connection read. 47 | WriteTimeout time.Duration // sets the timeout for connection write. 48 | Packer Packer // packs and unpacks packet payload, default packer is the DefaultPacker. 49 | Codec Codec // encodes and decodes the message data, can be nil. 50 | RespQueueSize int // sets the response channel size of session, DefaultRespQueueSize will be used if < 0. 51 | DoNotPrintRoutes bool // whether to print registered route handlers to the console. 52 | 53 | // AsyncRouter represents whether to execute a route HandlerFunc of each session in a goroutine. 54 | // true means execute in a goroutine. 55 | AsyncRouter bool 56 | } 57 | 58 | // ErrServerStopped is returned when server stopped. 59 | var ErrServerStopped = fmt.Errorf("server stopped") 60 | 61 | const DefaultRespQueueSize = 1024 62 | 63 | // NewServer creates a Server according to opt. 64 | func NewServer(opt *ServerOption) *Server { 65 | if opt.Packer == nil { 66 | opt.Packer = NewDefaultPacker() 67 | } 68 | if opt.RespQueueSize < 0 { 69 | opt.RespQueueSize = DefaultRespQueueSize 70 | } 71 | return &Server{ 72 | socketReadBufferSize: opt.SocketReadBufferSize, 73 | socketWriteBufferSize: opt.SocketWriteBufferSize, 74 | socketSendDelay: opt.SocketSendDelay, 75 | respQueueSize: opt.RespQueueSize, 76 | readTimeout: opt.ReadTimeout, 77 | writeTimeout: opt.WriteTimeout, 78 | Packer: opt.Packer, 79 | Codec: opt.Codec, 80 | printRoutes: !opt.DoNotPrintRoutes, 81 | router: newRouter(), 82 | acceptingC: make(chan struct{}), 83 | stoppedC: make(chan struct{}), 84 | asyncRouter: opt.AsyncRouter, 85 | } 86 | } 87 | 88 | // Serve starts to serve the lis. 89 | func (s *Server) Serve(lis net.Listener) error { 90 | s.Listener = lis 91 | if s.printRoutes { 92 | s.router.printHandlers(fmt.Sprintf("tcp://%s", s.Listener.Addr())) 93 | } 94 | return s.acceptLoop() 95 | } 96 | 97 | // Run starts to listen TCP and keeps accepting TCP connection in a loop. 98 | // The loop breaks when error occurred, and the error will be returned. 99 | func (s *Server) Run(addr string) error { 100 | lis, err := net.Listen("tcp", addr) 101 | if err != nil { 102 | return err 103 | } 104 | return s.Serve(lis) 105 | } 106 | 107 | // RunTLS starts serve TCP with TLS. 108 | func (s *Server) RunTLS(addr string, config *tls.Config) error { 109 | lis, err := tls.Listen("tcp", addr, config) 110 | if err != nil { 111 | return err 112 | } 113 | return s.Serve(lis) 114 | } 115 | 116 | // acceptLoop accepts TCP connections in a loop, and handle connections in goroutines. 117 | // Returns error when error occurred. 118 | func (s *Server) acceptLoop() error { 119 | close(s.acceptingC) 120 | for { 121 | if s.isStopped() { 122 | _log.Tracef("server accept loop stopped") 123 | return ErrServerStopped 124 | } 125 | 126 | conn, err := s.Listener.Accept() 127 | if err != nil { 128 | if s.isStopped() { 129 | _log.Tracef("server accept loop stopped") 130 | return ErrServerStopped 131 | } 132 | return fmt.Errorf("accept err: %s", err) 133 | } 134 | if s.socketReadBufferSize > 0 { 135 | if c, ok := conn.(*net.TCPConn); ok { 136 | if err := c.SetReadBuffer(s.socketReadBufferSize); err != nil { 137 | return fmt.Errorf("conn set read buffer err: %s", err) 138 | } 139 | } 140 | } 141 | if s.socketWriteBufferSize > 0 { 142 | if c, ok := conn.(*net.TCPConn); ok { 143 | if err := c.SetWriteBuffer(s.socketWriteBufferSize); err != nil { 144 | return fmt.Errorf("conn set write buffer err: %s", err) 145 | } 146 | } 147 | } 148 | if s.socketSendDelay { 149 | if c, ok := conn.(*net.TCPConn); ok { 150 | if err := c.SetNoDelay(false); err != nil { 151 | return fmt.Errorf("conn set no delay err: %s", err) 152 | } 153 | } 154 | } 155 | go s.handleConn(conn) 156 | } 157 | } 158 | 159 | // handleConn creates a new session with `conn`, 160 | // handles the message through the session in different goroutines, 161 | // and waits until the session's closed, then close the `conn`. 162 | func (s *Server) handleConn(conn net.Conn) { 163 | defer conn.Close() // nolint 164 | 165 | sess := newSession(conn, &sessionOption{ 166 | Packer: s.Packer, 167 | Codec: s.Codec, 168 | respQueueSize: s.respQueueSize, 169 | asyncRouter: s.asyncRouter, 170 | }) 171 | if s.OnSessionCreate != nil { 172 | s.OnSessionCreate(sess) 173 | } 174 | close(sess.afterCreateHookC) 175 | 176 | go sess.readInbound(s.router, s.readTimeout) // start reading message packet from connection. 177 | go sess.writeOutbound(s.writeTimeout) // start writing message packet to connection. 178 | 179 | select { 180 | case <-sess.closedC: // wait for session finished. 181 | case <-s.stoppedC: // or the server is stopped. 182 | } 183 | 184 | if s.OnSessionClose != nil { 185 | s.OnSessionClose(sess) 186 | } 187 | close(sess.afterCloseHookC) 188 | } 189 | 190 | // Stop stops server. Closing Listener and all connections. 191 | func (s *Server) Stop() error { 192 | close(s.stoppedC) 193 | return s.Listener.Close() 194 | } 195 | 196 | // AddRoute registers message handler and middlewares to the router. 197 | func (s *Server) AddRoute(msgID interface{}, handler HandlerFunc, middlewares ...MiddlewareFunc) { 198 | s.router.register(msgID, handler, middlewares...) 199 | } 200 | 201 | // Use registers global middlewares to the router. 202 | func (s *Server) Use(middlewares ...MiddlewareFunc) { 203 | s.router.registerMiddleware(middlewares...) 204 | } 205 | 206 | // NotFoundHandler sets the not-found handler for router. 207 | func (s *Server) NotFoundHandler(handler HandlerFunc) { 208 | s.router.setNotFoundHandler(handler) 209 | } 210 | 211 | func (s *Server) isStopped() bool { 212 | select { 213 | case <-s.stoppedC: 214 | return true 215 | default: 216 | return false 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /server_test.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | "net" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestNewServer(t *testing.T) { 14 | s := NewServer(&ServerOption{ 15 | ReadTimeout: 0, 16 | WriteTimeout: 0, 17 | Codec: &JsonCodec{}, 18 | RespQueueSize: -1, 19 | }) 20 | assert.NotNil(t, s.acceptingC) 21 | assert.IsType(t, s.Packer, NewDefaultPacker()) 22 | assert.Equal(t, s.Codec, &JsonCodec{}) 23 | assert.Equal(t, s.respQueueSize, DefaultRespQueueSize) 24 | assert.NotNil(t, s.acceptingC) 25 | assert.NotNil(t, s.stoppedC) 26 | } 27 | 28 | func TestServer_Serve(t *testing.T) { 29 | server := NewServer(&ServerOption{}) 30 | lis, err := net.Listen("tcp", ":0") 31 | require.NoError(t, err) 32 | done := make(chan struct{}) 33 | go func() { 34 | assert.ErrorIs(t, server.Serve(lis), ErrServerStopped) 35 | close(done) 36 | }() 37 | <-server.acceptingC 38 | time.Sleep(time.Millisecond * 5) 39 | err = server.Stop() 40 | assert.NoError(t, err) 41 | <-done 42 | } 43 | 44 | func TestServer_Run(t *testing.T) { 45 | server := NewServer(&ServerOption{}) 46 | done := make(chan struct{}) 47 | go func() { 48 | assert.ErrorIs(t, server.Run("localhost:0"), ErrServerStopped) 49 | close(done) 50 | }() 51 | <-server.acceptingC 52 | time.Sleep(time.Millisecond * 5) 53 | err := server.Stop() 54 | assert.NoError(t, err) 55 | <-done 56 | } 57 | 58 | func TestServer_RunTLS(t *testing.T) { 59 | server := NewServer(&ServerOption{ 60 | SocketReadBufferSize: 123, // won't work 61 | }) 62 | cert, err := tls.LoadX509KeyPair("internal/test_data/certificates/cert.pem", "internal/test_data/certificates/cert.key") 63 | assert.NoError(t, err) 64 | done := make(chan struct{}) 65 | go func() { 66 | cfg := &tls.Config{ 67 | Certificates: []tls.Certificate{cert}, 68 | } 69 | assert.ErrorIs(t, server.RunTLS("localhost:0", cfg), ErrServerStopped) 70 | close(done) 71 | }() 72 | <-server.acceptingC 73 | time.Sleep(time.Millisecond * 5) 74 | err = server.Stop() 75 | assert.NoError(t, err) 76 | <-done 77 | } 78 | 79 | func TestServer_acceptLoop(t *testing.T) { 80 | t.Run("when everything's fine", func(t *testing.T) { 81 | server := NewServer(&ServerOption{ 82 | SocketReadBufferSize: 1024, 83 | SocketWriteBufferSize: 1024, 84 | }) 85 | address, err := net.ResolveTCPAddr("tcp", "localhost:0") 86 | assert.NoError(t, err) 87 | lis, err := net.ListenTCP("tcp", address) 88 | assert.NoError(t, err) 89 | server.Listener = lis 90 | go func() { 91 | err := server.acceptLoop() 92 | assert.Error(t, err) 93 | }() 94 | 95 | <-server.acceptingC 96 | 97 | // client 98 | cli, err := net.Dial("tcp", lis.Addr().String()) 99 | assert.NoError(t, err) 100 | 101 | time.Sleep(time.Millisecond * 5) 102 | 103 | assert.NoError(t, cli.Close()) 104 | assert.NoError(t, server.Stop()) 105 | }) 106 | 107 | t.Run("when server is stopped", func(t *testing.T) { 108 | server := NewServer(&ServerOption{ 109 | SocketReadBufferSize: 1024, 110 | SocketWriteBufferSize: 1024, 111 | }) 112 | address, err := net.ResolveTCPAddr("tcp", "localhost:0") 113 | assert.NoError(t, err) 114 | lis, err := net.ListenTCP("tcp", address) 115 | assert.NoError(t, err) 116 | server.Listener = lis 117 | assert.NoError(t, server.Stop()) 118 | assert.ErrorIs(t, server.acceptLoop(), ErrServerStopped) 119 | }) 120 | } 121 | 122 | func TestServer_Stop(t *testing.T) { 123 | server := NewServer(&ServerOption{}) 124 | go func() { 125 | err := server.Run("localhost:0") 126 | assert.Error(t, err) 127 | assert.Equal(t, err, ErrServerStopped) 128 | }() 129 | 130 | <-server.acceptingC 131 | 132 | // client 133 | cli, err := net.Dial("tcp", server.Listener.Addr().String()) 134 | assert.NoError(t, err) 135 | 136 | time.Sleep(time.Millisecond * 5) 137 | 138 | assert.NoError(t, server.Stop()) // stop server first 139 | assert.NoError(t, cli.Close()) 140 | } 141 | 142 | func TestServer_handleConn(t *testing.T) { 143 | type TestReq struct { 144 | Param string 145 | } 146 | type TestResp struct { 147 | Success bool 148 | } 149 | 150 | // options 151 | codec := &JsonCodec{} 152 | packer := NewDefaultPacker() 153 | 154 | // server 155 | server := NewServer(&ServerOption{ 156 | SocketReadBufferSize: 1, 157 | SocketWriteBufferSize: 1, 158 | SocketSendDelay: true, 159 | Codec: codec, 160 | Packer: packer, 161 | RespQueueSize: -1, 162 | AsyncRouter: true, 163 | }) 164 | 165 | // hooks 166 | server.OnSessionCreate = func(sess Session) { 167 | fmt.Printf("session created | id: %s\n", sess.ID()) 168 | } 169 | server.OnSessionClose = func(sess Session) { 170 | fmt.Printf("session closed | id: %s\n", sess.ID()) 171 | } 172 | 173 | // register route 174 | server.AddRoute(1, func(ctx Context) { 175 | var reqData TestReq 176 | assert.NoError(t, ctx.Bind(&reqData)) 177 | assert.EqualValues(t, 1, ctx.Request().ID()) 178 | assert.Equal(t, reqData.Param, "hello test") 179 | ctx.MustSetResponse(2, &TestResp{Success: true}) 180 | }) 181 | // use middleware 182 | server.Use(func(next HandlerFunc) HandlerFunc { 183 | return func(ctx Context) { 184 | defer func() { 185 | if r := recover(); r != nil { 186 | assert.Fail(t, "caught panic") 187 | } 188 | }() 189 | next(ctx) 190 | } 191 | }) 192 | 193 | go func() { 194 | err := server.Run("localhost:0") 195 | assert.Error(t, err) 196 | assert.Equal(t, err, ErrServerStopped) 197 | }() 198 | defer func() { assert.NoError(t, server.Stop()) }() 199 | 200 | <-server.acceptingC 201 | 202 | // client 203 | cli, err := net.Dial("tcp", server.Listener.Addr().String()) 204 | assert.NoError(t, err) 205 | defer func() { assert.NoError(t, cli.Close()) }() 206 | 207 | // client send msg 208 | reqData := &TestReq{Param: "hello test"} 209 | reqDataByte, err := codec.Encode(reqData) 210 | assert.NoError(t, err) 211 | reqMsg, err := packer.Pack(NewMessage(1, reqDataByte)) 212 | assert.NoError(t, err) 213 | _, err = cli.Write(reqMsg) 214 | assert.NoError(t, err) 215 | 216 | // client read msg 217 | respMsg, err := packer.Unpack(cli) 218 | assert.NoError(t, err) 219 | var respData TestResp 220 | assert.NoError(t, codec.Decode(respMsg.Data(), &respData)) 221 | assert.EqualValues(t, 2, respMsg.ID()) 222 | assert.True(t, respData.Success) 223 | } 224 | 225 | func TestServer_NotFoundHandler(t *testing.T) { 226 | // server 227 | server := NewServer(&ServerOption{ 228 | Packer: NewDefaultPacker(), 229 | }) 230 | server.NotFoundHandler(func(ctx Context) { 231 | ctx.SetResponseMessage(NewMessage(101, []byte("handler not found"))) 232 | }) 233 | go func() { 234 | err := server.Run(":0") 235 | assert.Equal(t, err, ErrServerStopped) 236 | }() 237 | 238 | <-server.acceptingC 239 | 240 | // client 241 | cli, err := net.Dial("tcp", server.Listener.Addr().String()) 242 | assert.NoError(t, err) 243 | defer func() { assert.NoError(t, cli.Close()) }() 244 | 245 | // send msg 246 | reqBytes, err := server.Packer.Pack(NewMessage(1, []byte("test"))) 247 | assert.NoError(t, err) 248 | _, err = cli.Write(reqBytes) 249 | assert.NoError(t, err) 250 | 251 | // read msg 252 | reqMsg, err := server.Packer.Unpack(cli) 253 | assert.NoError(t, err) 254 | assert.EqualValues(t, reqMsg.ID(), 101) 255 | assert.Equal(t, reqMsg.Data(), []byte("handler not found")) 256 | } 257 | 258 | func TestServer_SessionHooks(t *testing.T) { 259 | // server 260 | server := NewServer(&ServerOption{}) 261 | 262 | sessCh := make(chan Session, 1) 263 | server.OnSessionCreate = func(sess Session) { 264 | fmt.Printf("session created | id: %s\n", sess.ID()) 265 | sessCh <- sess 266 | close(sessCh) 267 | } 268 | server.OnSessionClose = func(sess Session) { 269 | fmt.Printf("session closed | id: %s\n", sess.ID()) 270 | } 271 | 272 | go func() { 273 | err := server.Run("localhost:0") 274 | assert.Error(t, err) 275 | assert.Equal(t, err, ErrServerStopped) 276 | }() 277 | defer func() { assert.NoError(t, server.Stop()) }() 278 | 279 | <-server.acceptingC 280 | 281 | // client 282 | cli, err := net.Dial("tcp", server.Listener.Addr().String()) 283 | assert.NoError(t, err) 284 | 285 | theSess := <-sessCh 286 | 287 | <-theSess.AfterCreateHook() 288 | 289 | assert.NoError(t, cli.Close()) 290 | <-theSess.AfterCloseHook() 291 | } 292 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "fmt" 5 | "github.com/google/uuid" 6 | "io" 7 | "net" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | // Session represents a TCP session. 13 | type Session interface { 14 | // ID returns current session's id. 15 | ID() interface{} 16 | 17 | // SetID sets current session's id. 18 | SetID(id interface{}) 19 | 20 | // Send sends the ctx to the respStream. 21 | Send(ctx Context) bool 22 | 23 | // Codec returns the codec, can be nil. 24 | Codec() Codec 25 | 26 | // Close closes current session. 27 | Close() 28 | 29 | // AllocateContext gets a Context ships with current session. 30 | AllocateContext() Context 31 | 32 | // Conn returns the underlined connection. 33 | Conn() net.Conn 34 | 35 | // AfterCreateHook blocks until session's on-create hook triggered. 36 | AfterCreateHook() <-chan struct{} 37 | 38 | // AfterCloseHook blocks until session's on-close hook triggered. 39 | AfterCloseHook() <-chan struct{} 40 | } 41 | 42 | type session struct { 43 | id interface{} // session's ID. 44 | conn net.Conn // tcp connection 45 | closedC chan struct{} // to close when read/write loop stopped 46 | closeOnce sync.Once // ensure one session only close once 47 | afterCreateHookC chan struct{} // to close after session's on-create hook triggered 48 | afterCloseHookC chan struct{} // to close after session's on-close hook triggered 49 | respStream chan Context // response queue channel, pushed in Send() and popped in writeOutbound() 50 | packer Packer // to pack and unpack message 51 | codec Codec // encode/decode message data 52 | ctxPool sync.Pool // router context pool 53 | asyncRouter bool // calls router HandlerFunc in a goroutine if false 54 | } 55 | 56 | // sessionOption is the extra options for session. 57 | type sessionOption struct { 58 | Packer Packer 59 | Codec Codec 60 | respQueueSize int 61 | asyncRouter bool 62 | } 63 | 64 | // newSession creates a new session. 65 | // Parameter conn is the TCP connection, 66 | // opt includes packer, codec, and channel size. 67 | // Returns a session pointer. 68 | func newSession(conn net.Conn, opt *sessionOption) *session { 69 | return &session{ 70 | id: uuid.NewString(), // use uuid as default 71 | conn: conn, 72 | closedC: make(chan struct{}), 73 | afterCreateHookC: make(chan struct{}), 74 | afterCloseHookC: make(chan struct{}), 75 | respStream: make(chan Context, opt.respQueueSize), 76 | packer: opt.Packer, 77 | codec: opt.Codec, 78 | ctxPool: sync.Pool{New: func() interface{} { return newContext() }}, 79 | asyncRouter: opt.asyncRouter, 80 | } 81 | } 82 | 83 | // ID returns the session's id. 84 | func (s *session) ID() interface{} { 85 | return s.id 86 | } 87 | 88 | // SetID sets session id. 89 | // Can be called in server.OnSessionCreate() callback. 90 | func (s *session) SetID(id interface{}) { 91 | s.id = id 92 | } 93 | 94 | // Send pushes response message to respStream. 95 | // Returns false if session is closed or ctx is done. 96 | func (s *session) Send(ctx Context) (ok bool) { 97 | select { 98 | case <-ctx.Done(): 99 | return false 100 | case <-s.closedC: 101 | return false 102 | case s.respStream <- ctx: 103 | return true 104 | } 105 | } 106 | 107 | // Codec implements Session Codec. 108 | func (s *session) Codec() Codec { 109 | return s.codec 110 | } 111 | 112 | // Close closes the session, but doesn't close the connection. 113 | // The connection will be closed in the server once the session's closed. 114 | func (s *session) Close() { 115 | s.closeOnce.Do(func() { close(s.closedC) }) 116 | } 117 | 118 | // AfterCreateHook blocks until session's on-create hook triggered. 119 | func (s *session) AfterCreateHook() <-chan struct{} { 120 | return s.afterCreateHookC 121 | } 122 | 123 | // AfterCloseHook blocks until session's on-close hook triggered. 124 | func (s *session) AfterCloseHook() <-chan struct{} { 125 | return s.afterCloseHookC 126 | } 127 | 128 | // AllocateContext gets a Context from pool and reset all but session. 129 | func (s *session) AllocateContext() Context { 130 | c := s.ctxPool.Get().(*routeContext) 131 | c.reset() 132 | c.SetSession(s) 133 | return c 134 | } 135 | 136 | // Conn returns the underlined connection instance. 137 | func (s *session) Conn() net.Conn { 138 | return s.conn 139 | } 140 | 141 | // readInbound reads message packet from connection in a loop. 142 | // And send unpacked message to reqQueue, which will be consumed in router. 143 | // The loop breaks if errors occurred or the session is closed. 144 | func (s *session) readInbound(router *Router, timeout time.Duration) { 145 | for { 146 | select { 147 | case <-s.closedC: 148 | return 149 | default: 150 | } 151 | if timeout > 0 { 152 | if err := s.conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { 153 | _log.Errorf("session %s set read deadline err: %s", s.id, err) 154 | break 155 | } 156 | } 157 | reqMsg, err := s.packer.Unpack(s.conn) 158 | if err != nil { 159 | logMsg := fmt.Sprintf("session %s unpack inbound packet err: %s", s.id, err) 160 | if err == io.EOF { 161 | _log.Tracef(logMsg) 162 | } else { 163 | _log.Errorf(logMsg) 164 | } 165 | break 166 | } 167 | if reqMsg == nil { 168 | continue 169 | } 170 | 171 | if s.asyncRouter { 172 | go s.handleReq(router, reqMsg) 173 | } else { 174 | s.handleReq(router, reqMsg) 175 | } 176 | } 177 | _log.Tracef("session %s readInbound exit because of error", s.id) 178 | s.Close() 179 | } 180 | 181 | func (s *session) handleReq(router *Router, reqMsg *Message) { 182 | ctx := s.AllocateContext().SetRequestMessage(reqMsg) 183 | router.handleRequest(ctx) 184 | s.Send(ctx) 185 | } 186 | 187 | // writeOutbound fetches message from respStream channel and writes to TCP connection in a loop. 188 | // Parameter writeTimeout specified the connection writing timeout. 189 | // The loop breaks if errors occurred, or the session is closed. 190 | func (s *session) writeOutbound(writeTimeout time.Duration) { 191 | for { 192 | var ctx Context 193 | select { 194 | case <-s.closedC: 195 | return 196 | case ctx = <-s.respStream: 197 | } 198 | 199 | outboundBytes, err := s.packResponse(ctx) 200 | if err != nil { 201 | _log.Errorf("session %s pack outbound message err: %s", s.id, err) 202 | continue 203 | } 204 | if outboundBytes == nil { 205 | continue 206 | } 207 | 208 | if writeTimeout > 0 { 209 | if err := s.conn.SetWriteDeadline(time.Now().Add(writeTimeout)); err != nil { 210 | _log.Errorf("session %s set write deadline err: %s", s.id, err) 211 | break 212 | } 213 | } 214 | 215 | if _, err := s.conn.Write(outboundBytes); err != nil { 216 | _log.Errorf("session %s conn write err: %s", s.id, err) 217 | break 218 | } 219 | } 220 | s.Close() 221 | _log.Tracef("session %s writeOutbound exit because of error", s.id) 222 | } 223 | 224 | func (s *session) packResponse(ctx Context) ([]byte, error) { 225 | defer s.ctxPool.Put(ctx) 226 | if ctx.Response() == nil { 227 | return nil, nil 228 | } 229 | return s.packer.Pack(ctx.Response()) 230 | } 231 | -------------------------------------------------------------------------------- /session_test.go: -------------------------------------------------------------------------------- 1 | package easytcp 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/DarthPestilane/easytcp/internal/mock" 7 | "github.com/golang/mock/gomock" 8 | "github.com/stretchr/testify/assert" 9 | "io" 10 | "net" 11 | "sync" 12 | "testing" 13 | "time" 14 | ) 15 | 16 | func TestNewTCPSession(t *testing.T) { 17 | var s *session 18 | assert.NotPanics(t, func() { 19 | s = newSession(nil, &sessionOption{}) 20 | }) 21 | assert.NotNil(t, s) 22 | assert.NotNil(t, s.closedC) 23 | assert.NotNil(t, s.respStream) 24 | } 25 | 26 | func TestTCPSession_close(t *testing.T) { 27 | sess := newSession(nil, &sessionOption{}) 28 | wg := sync.WaitGroup{} 29 | for i := 0; i < 100; i++ { 30 | wg.Add(1) 31 | go func() { 32 | defer wg.Done() 33 | sess.Close() // goroutine safe 34 | }() 35 | } 36 | wg.Wait() 37 | _, ok := <-sess.closedC 38 | assert.False(t, ok) 39 | } 40 | 41 | func TestTCPSession_ID(t *testing.T) { 42 | sess := newSession(nil, &sessionOption{}) 43 | assert.NotEmpty(t, sess.id) 44 | assert.Equal(t, sess.ID(), sess.id) 45 | } 46 | 47 | func TestTCPSession_readInbound(t *testing.T) { 48 | t.Run("when connection set read timeout failed", func(t *testing.T) { 49 | p1, _ := net.Pipe() 50 | _ = p1.Close() 51 | sess := newSession(p1, &sessionOption{}) 52 | go sess.readInbound(nil, time.Millisecond) 53 | <-sess.closedC 54 | }) 55 | t.Run("when connection read timeout", func(t *testing.T) { 56 | p1, _ := net.Pipe() 57 | packer := &DefaultPacker{} 58 | sess := newSession(p1, &sessionOption{Packer: packer}) 59 | done := make(chan struct{}) 60 | go func() { 61 | sess.readInbound(nil, time.Millisecond*10) 62 | close(done) 63 | }() 64 | <-done 65 | }) 66 | t.Run("when unpack message failed with error", func(t *testing.T) { 67 | ctrl := gomock.NewController(t) 68 | defer ctrl.Finish() 69 | 70 | packer := NewMockPacker(ctrl) 71 | packer.EXPECT().Unpack(gomock.Any()).Return(nil, fmt.Errorf("some error")) 72 | 73 | sess := newSession(nil, &sessionOption{Packer: packer, Codec: mock.NewMockCodec(ctrl)}) 74 | done := make(chan struct{}) 75 | go func() { 76 | sess.readInbound(nil, 0) 77 | close(done) 78 | }() 79 | <-done 80 | <-sess.closedC 81 | }) 82 | t.Run("when unpack message returns nil message", func(t *testing.T) { 83 | ctrl := gomock.NewController(t) 84 | defer ctrl.Finish() 85 | 86 | first := true 87 | packer := NewMockPacker(ctrl) 88 | packer.EXPECT().Unpack(gomock.Any()).Times(2).DoAndReturn(func(_ io.Reader) (*Message, error) { 89 | if first { 90 | first = false 91 | return nil, nil 92 | } else { 93 | return nil, fmt.Errorf("unpack error") 94 | } 95 | }) 96 | 97 | sess := newSession(nil, &sessionOption{Packer: packer, Codec: mock.NewMockCodec(ctrl)}) 98 | done := make(chan struct{}) 99 | go func() { 100 | sess.readInbound(nil, 0) 101 | close(done) 102 | }() 103 | <-done 104 | }) 105 | t.Run("when send response failed", func(t *testing.T) { 106 | ctrl := gomock.NewController(t) 107 | defer ctrl.Finish() 108 | 109 | packer := NewMockPacker(ctrl) 110 | packer.EXPECT().Unpack(gomock.Any()).AnyTimes().Return(NewMessage(1, []byte("test")), nil) 111 | 112 | r := newRouter() 113 | r.register(1, func(ctx Context) { 114 | ctx.Session().Close() 115 | }) 116 | 117 | sess := newSession(nil, &sessionOption{Packer: packer, respQueueSize: 10}) 118 | loopDone := make(chan struct{}) 119 | go func() { 120 | sess.readInbound(r, 0) 121 | close(loopDone) 122 | }() 123 | <-loopDone 124 | }) 125 | t.Run("when unpack message works fine", func(t *testing.T) { 126 | ctrl := gomock.NewController(t) 127 | defer ctrl.Finish() 128 | 129 | first := true 130 | packer := NewMockPacker(ctrl) 131 | packer.EXPECT().Unpack(gomock.Any()).Times(2).DoAndReturn(func(_ io.Reader) (*Message, error) { 132 | if first { 133 | first = false 134 | return NewMessage(1, []byte("unpack ok")), nil 135 | } else { 136 | return nil, fmt.Errorf("unpack error") 137 | } 138 | }) 139 | 140 | r := newRouter() 141 | r.register(1, func(ctx Context) { 142 | ctx.SetResponseMessage(NewMessage(2, []byte("ok"))) 143 | }) 144 | 145 | sess := newSession(nil, &sessionOption{Packer: packer, Codec: nil, respQueueSize: 10}) 146 | readDone := make(chan struct{}) 147 | go func() { 148 | sess.readInbound(r, 0) 149 | close(readDone) 150 | }() 151 | <-readDone 152 | }) 153 | } 154 | 155 | func TestTCPSession_Send(t *testing.T) { 156 | t.Run("when session is closed", func(t *testing.T) { 157 | reqMsg := NewMessage(1, []byte("test")) 158 | sess := newSession(nil, &sessionOption{}) 159 | sess.Close() // close session 160 | assert.False(t, sess.AllocateContext().SetRequestMessage(reqMsg).Send()) 161 | }) 162 | t.Run("when ctx is done", func(t *testing.T) { 163 | sess := newSession(nil, &sessionOption{}) 164 | ctx, cancel := context.WithCancel(context.Background()) 165 | 166 | c := sess.AllocateContext().WithContext(ctx) 167 | done := make(chan struct{}) 168 | go func() { 169 | assert.False(t, c.Send()) 170 | close(done) 171 | }() 172 | 173 | cancel() 174 | <-done 175 | }) 176 | t.Run("when send succeed", func(t *testing.T) { 177 | sess := newSession(nil, &sessionOption{}) 178 | sess.respStream = make(chan Context) // no buffer 179 | go func() { <-sess.respStream }() 180 | assert.True(t, sess.AllocateContext().SetRequestMessage(NewMessage(1, []byte("test"))).Send()) 181 | sess.Close() 182 | }) 183 | } 184 | 185 | func TestTCPSession_writeOutbound(t *testing.T) { 186 | t.Run("when session is closed", func(t *testing.T) { 187 | ctrl := gomock.NewController(t) 188 | defer ctrl.Finish() 189 | 190 | packer := NewMockPacker(ctrl) 191 | packer.EXPECT().Pack(gomock.Any()).AnyTimes().Return(nil, nil) 192 | 193 | sess := newSession(nil, &sessionOption{Packer: packer, respQueueSize: 10}) 194 | doneLoop := make(chan struct{}) 195 | sess.Close() 196 | go func() { 197 | sess.writeOutbound(0) // should stop looping and return 198 | close(doneLoop) 199 | }() 200 | time.Sleep(time.Millisecond * 5) 201 | <-doneLoop 202 | }) 203 | t.Run("when response message is nil", func(t *testing.T) { 204 | ctrl := gomock.NewController(t) 205 | defer ctrl.Finish() 206 | 207 | packer := NewMockPacker(ctrl) 208 | packer.EXPECT().Pack(gomock.Any()).AnyTimes().Return(nil, nil) 209 | 210 | sess := newSession(nil, &sessionOption{Packer: packer, respQueueSize: 1024}) 211 | sess.respStream <- sess.AllocateContext() 212 | doneLoop := make(chan struct{}) 213 | go func() { 214 | sess.writeOutbound(0) // should stop looping and return 215 | close(doneLoop) 216 | }() 217 | time.Sleep(time.Millisecond * 5) 218 | sess.Close() 219 | <-doneLoop 220 | }) 221 | t.Run("when pack response message failed", func(t *testing.T) { 222 | ctrl := gomock.NewController(t) 223 | defer ctrl.Finish() 224 | 225 | packer := NewMockPacker(ctrl) 226 | packer.EXPECT().Pack(gomock.Any()).Return(nil, fmt.Errorf("some err")) 227 | 228 | sess := newSession(nil, &sessionOption{Packer: packer}) 229 | done := make(chan struct{}) 230 | go func() { 231 | sess.respStream <- sess.AllocateContext().SetResponseMessage(NewMessage(1, []byte("test"))) 232 | close(done) 233 | }() 234 | time.Sleep(time.Microsecond * 15) 235 | go sess.writeOutbound(0) 236 | time.Sleep(time.Millisecond * 15) 237 | <-done 238 | sess.Close() // should break the write loop 239 | }) 240 | t.Run("when pack returns nil data", func(t *testing.T) { 241 | ctrl := gomock.NewController(t) 242 | defer ctrl.Finish() 243 | 244 | packer := NewMockPacker(ctrl) 245 | packer.EXPECT().Pack(gomock.Any()).Return(nil, nil) 246 | 247 | sess := newSession(nil, &sessionOption{Packer: packer, respQueueSize: 100}) 248 | sess.respStream <- sess.AllocateContext().SetResponseMessage(NewMessage(1, []byte("test"))) // push to queue 249 | doneLoop := make(chan struct{}) 250 | go func() { 251 | sess.writeOutbound(0) 252 | close(doneLoop) 253 | }() 254 | time.Sleep(time.Millisecond * 5) 255 | sess.Close() // should break the write loop 256 | }) 257 | t.Run("when set write deadline failed", func(t *testing.T) { 258 | ctrl := gomock.NewController(t) 259 | defer ctrl.Finish() 260 | 261 | packer := NewMockPacker(ctrl) 262 | packer.EXPECT().Pack(gomock.Any()).Return([]byte("pack succeed"), nil) 263 | 264 | p1, _ := net.Pipe() 265 | _ = p1.Close() 266 | sess := newSession(p1, &sessionOption{Packer: packer}) 267 | go func() { sess.respStream <- sess.AllocateContext().SetResponseMessage(NewMessage(1, []byte("test"))) }() 268 | go sess.writeOutbound(time.Millisecond * 10) 269 | _, ok := <-sess.closedC 270 | assert.False(t, ok) 271 | }) 272 | t.Run("when conn write timeout", func(t *testing.T) { 273 | ctrl := gomock.NewController(t) 274 | defer ctrl.Finish() 275 | 276 | packer := NewMockPacker(ctrl) 277 | packer.EXPECT().Pack(gomock.Any()).Return([]byte("pack succeed"), nil) 278 | 279 | p1, _ := net.Pipe() 280 | sess := newSession(p1, &sessionOption{Packer: packer}) 281 | go func() { sess.respStream <- sess.AllocateContext().SetResponseMessage(NewMessage(1, []byte("test"))) }() 282 | go sess.writeOutbound(time.Millisecond * 10) 283 | _, ok := <-sess.closedC 284 | assert.False(t, ok) 285 | _ = p1.Close() 286 | }) 287 | t.Run("when conn write returns fatal error", func(t *testing.T) { 288 | ctrl := gomock.NewController(t) 289 | defer ctrl.Finish() 290 | 291 | packer := NewMockPacker(ctrl) 292 | packer.EXPECT().Pack(gomock.Any()).Return([]byte("pack succeed"), nil) 293 | 294 | p1, _ := net.Pipe() 295 | assert.NoError(t, p1.Close()) 296 | sess := newSession(p1, &sessionOption{Packer: packer}) 297 | go func() { sess.respStream <- sess.AllocateContext().SetResponseMessage(NewMessage(1, []byte("test"))) }() 298 | sess.writeOutbound(0) // should stop looping and return 299 | _, ok := <-sess.closedC 300 | assert.False(t, ok) 301 | }) 302 | t.Run("when write succeed", func(t *testing.T) { 303 | ctrl := gomock.NewController(t) 304 | defer ctrl.Finish() 305 | 306 | packer := NewMockPacker(ctrl) 307 | packer.EXPECT().Pack(gomock.Any()).Return([]byte("pack succeed"), nil) 308 | 309 | p1, p2 := net.Pipe() 310 | sess := newSession(p1, &sessionOption{Packer: packer}) 311 | go func() { sess.AllocateContext().SetResponseMessage(NewMessage(1, []byte("test"))).Send() }() 312 | done := make(chan struct{}) 313 | go func() { 314 | sess.writeOutbound(0) 315 | close(done) 316 | }() 317 | time.Sleep(time.Millisecond * 5) 318 | _, _ = p2.Read(make([]byte, 100)) 319 | sess.Close() 320 | <-done 321 | }) 322 | } 323 | 324 | func Test_session_SetID(t *testing.T) { 325 | sess := newSession(nil, &sessionOption{}) 326 | _, ok := sess.ID().(string) 327 | assert.True(t, ok) 328 | sess.SetID(123) 329 | assert.Equal(t, sess.ID(), 123) 330 | } 331 | 332 | func Test_session_Conn(t *testing.T) { 333 | ctrl := gomock.NewController(t) 334 | defer ctrl.Finish() 335 | 336 | conn := mock.NewMockConn(ctrl) 337 | s := newSession(conn, &sessionOption{}) 338 | assert.Equal(t, s.Conn(), conn) 339 | } 340 | --------------------------------------------------------------------------------