├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature-request.yml ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── codeql-analysis.yml │ ├── linters.yml │ ├── linux.yml │ ├── macos.yml │ └── windows.yml ├── .gitignore ├── .golangci.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── benchmarks └── main.go ├── codecov.yml ├── go.mod ├── go.sum ├── internal ├── bpool.go └── receive.go └── pkg ├── frame ├── frame.go ├── frame.md ├── frame_flags.go └── frame_test.go ├── pipe ├── pipe.go └── pipe_test.go ├── relay └── interface.go ├── rpc ├── client.go ├── client_server_test.go └── codec.go └── socket ├── socket.go └── socket_test.go /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: roadrunner-server 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: 🐛 File a bug report 3 | title: "[🐛 BUG]: " 4 | labels: ["B-bug", "F-need-verification"] 5 | assignees: 6 | - rustatian 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to fill out this bug report! 12 | 13 | - type: checkboxes 14 | id: search-done 15 | attributes: 16 | label: No duplicates 🥲. 17 | options: 18 | - label: I have searched for a similar issue in our bug tracker and didn't find any solutions. 19 | required: true 20 | - type: textarea 21 | id: what-happened 22 | attributes: 23 | label: What happened? 24 | description: Also tell us, what did you expect to happen? 25 | placeholder: Tell us what you see! 26 | value: "A bug happened!" 27 | validations: 28 | required: true 29 | - type: textarea 30 | id: version 31 | attributes: 32 | label: Version 33 | description: What version of our software are you running? 34 | placeholder: 2.6.0 35 | validations: 36 | required: true 37 | - type: textarea 38 | id: logs 39 | attributes: 40 | label: Relevant log output 41 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. 42 | render: shell -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: ❓ Start a discussion or ask a question. 5 | url: https://github.com/spiral/roadrunner-plugins/discussions 6 | about: Please ask and answer questions here. 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.yml: -------------------------------------------------------------------------------- 1 | name: Feauture request 2 | description: 💡 Suggest an idea for this project 3 | title: "[💡 FEATURE REQUEST]: " 4 | labels: ["C-feature-request"] 5 | assignees: 6 | - rustatian 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thanks for taking the time to share your idea! 12 | 13 | - type: textarea 14 | id: idea 15 | attributes: 16 | label: I have an idea! 17 | description: Clear and concise description of your idea. 18 | placeholder: Tell us what you see! 19 | value: "I have an idea, listen to me!!" 20 | validations: 21 | required: true 22 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | version: 2 6 | 7 | updates: 8 | - package-ecosystem: gomod # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: daily 12 | reviewers: 13 | - "rustatian" 14 | assignees: 15 | - "rustatian" 16 | 17 | - package-ecosystem: "github-actions" 18 | directory: "/" 19 | schedule: 20 | interval: daily 21 | reviewers: 22 | - "rustatian" 23 | assignees: 24 | - "rustatian" 25 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Reason for This PR 2 | 3 | `[Author TODO: add issue # or explain reasoning.]` 4 | 5 | ## Description of Changes 6 | 7 | `[Author TODO: add description of changes.]` 8 | 9 | ## License Acceptance 10 | 11 | By submitting this pull request, I confirm that my contribution is made under 12 | the terms of the MIT license. 13 | 14 | ## PR Checklist 15 | 16 | `[Author TODO: Meet these criteria.]` 17 | `[Reviewer TODO: Verify that these criteria are met. Request changes if not]` 18 | 19 | - [ ] All commits in this PR are signed (`git commit -s`) or (`git commit -S`). 20 | - [ ] The reason for this PR is clearly provided (issue no. or explanation). 21 | - [ ] The description of changes is clear and encompassing. 22 | - [ ] Any required documentation changes (code and docs) are included in this PR. 23 | - [ ] Any user-facing changes are mentioned in `CHANGELOG.md`. 24 | - [ ] All added/changed functionality is tested. 25 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [ master ] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [ master ] 14 | schedule: 15 | - cron: '0 15 * * 6' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: [ 'go' ] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # Initializes the Golang environment for the CodeQL tools. 40 | # https://github.com/github/codeql-action/issues/1842#issuecomment-1704398087 41 | - name: Install Go 42 | uses: actions/setup-go@v4 43 | with: 44 | go-version-file: go.mod 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v2 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 https://git.io/JvXDl 63 | 64 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 65 | # and modify them (or add more) to build your code if your project 66 | # uses a compiled language 67 | 68 | #- run: | 69 | # make bootstrap 70 | # make release 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v2 74 | -------------------------------------------------------------------------------- /.github/workflows/linters.yml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | linters: 7 | name: Golang-CI (lint) 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out code 11 | uses: actions/checkout@v4 12 | 13 | - name: Set up Go 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: 'stable' 17 | 18 | - name: Run linter 19 | uses: golangci/golangci-lint-action@v7 # Action page: 20 | with: 21 | only-new-issues: false # show only new issues if it's a pull request 22 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Linux 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | testing: 7 | name: Tests [Go ${{ matrix.go }} OS ${{ matrix.os }}] 8 | runs-on: ${{ matrix.os }} 9 | timeout-minutes: 20 10 | strategy: 11 | fail-fast: true 12 | matrix: 13 | go: [ stable ] 14 | os: [ ubuntu-latest ] 15 | steps: 16 | - name: Set up Go ${{ matrix.go }} 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: ${{ matrix.go }} 20 | 21 | - name: Check out code 22 | uses: actions/checkout@v3 23 | 24 | - name: Init Go modules Cache # Docs: 25 | uses: actions/cache@v3 26 | with: 27 | path: ~/go/pkg/mod 28 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 29 | restore-keys: ${{ runner.os }}-go- 30 | 31 | - name: Install Go dependencies 32 | run: go mod download 33 | 34 | - name: Run golang tests on Linux with codecov 35 | run: | 36 | mkdir ./coverage-ci 37 | go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/frame.txt -covermode=atomic ./pkg/frame 38 | go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/pipe.txt -covermode=atomic ./pkg/pipe 39 | go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/rpc.txt -covermode=atomic ./pkg/rpc 40 | go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/socket.txt -covermode=atomic ./pkg/socket 41 | cat ./coverage-ci/*.txt > ./coverage-ci/summary.txt 42 | 43 | - uses: codecov/codecov-action@v3 # Docs: 44 | with: 45 | token: ${{ secrets.CODECOV_TOKEN }} 46 | file: ./coverage-ci/summary.txt 47 | fail_ci_if_error: false 48 | -------------------------------------------------------------------------------- /.github/workflows/macos.yml: -------------------------------------------------------------------------------- 1 | name: MacOS 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Tests [Go ${{ matrix.go }} OS ${{ matrix.os }}] 8 | runs-on: ${{ matrix.os }} 9 | timeout-minutes: 20 10 | strategy: 11 | fail-fast: true 12 | matrix: 13 | go: [ stable ] 14 | os: [ macos-latest ] 15 | steps: 16 | - name: Set up Go ${{ matrix.go }} 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: ${{ matrix.go }} 20 | 21 | - name: Check out code 22 | uses: actions/checkout@v3 23 | 24 | - name: Init Go modules Cache # Docs: 25 | uses: actions/cache@v3 26 | with: 27 | path: ~/go/pkg/mod 28 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 29 | restore-keys: ${{ runner.os }}-go- 30 | 31 | - name: Install Go dependencies 32 | run: go mod download 33 | 34 | - name: Run golang tests on MacOS 35 | run: | 36 | mkdir ./coverage-ci 37 | go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/frame.txt -covermode=atomic ./pkg/frame 38 | go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/pipe.txt -covermode=atomic ./pkg/pipe 39 | go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/rpc.txt -covermode=atomic ./pkg/rpc 40 | go test -v -race -cover -tags=debug -coverpkg=./... -coverprofile=./coverage-ci/socket.txt -covermode=atomic ./pkg/socket 41 | cat ./coverage-ci/*.txt > ./coverage-ci/summary.txt 42 | -------------------------------------------------------------------------------- /.github/workflows/windows.yml: -------------------------------------------------------------------------------- 1 | name: Windows 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Tests [Go ${{ matrix.go }} OS ${{ matrix.os }}] 8 | runs-on: ${{ matrix.os }} 9 | timeout-minutes: 20 10 | strategy: 11 | fail-fast: true 12 | matrix: 13 | go: [ stable ] 14 | os: [ windows-latest ] 15 | steps: 16 | - name: Set up Go ${{ matrix.go }} 17 | uses: actions/setup-go@v4 18 | with: 19 | go-version: ${{ matrix.go }} 20 | 21 | - name: Check out code 22 | uses: actions/checkout@v3 23 | 24 | - name: Init Go modules Cache # Docs: 25 | uses: actions/cache@v3 26 | with: 27 | path: ~/go/pkg/mod 28 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 29 | restore-keys: ${{ runner.os }}-go- 30 | 31 | - name: Install Go dependencies 32 | run: go mod download 33 | 34 | 35 | - name: Run golang tests on Windows 36 | run: | 37 | go test -v -race -tags=debug ./pkg/frame 38 | go test -v -race -tags=debug ./pkg/pipe 39 | go test -v -race -tags=debug ./pkg/rpc 40 | go test -v -race -tags=debug ./pkg/socket 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Dependency directories (remove the comment below to include it) 17 | # vendor/ 18 | .idea 19 | bench 20 | **vendor 21 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | run: 3 | modules-download-mode: readonly 4 | allow-parallel-runners: true 5 | output: 6 | formats: 7 | text: 8 | path: stdout 9 | linters: 10 | default: none 11 | enable: 12 | - asciicheck 13 | - bodyclose 14 | - copyloopvar 15 | - dogsled 16 | - dupl 17 | - errcheck 18 | - errorlint 19 | - exhaustive 20 | - gochecknoinits 21 | - goconst 22 | - gocritic 23 | - goprintffuncname 24 | - gosec 25 | - govet 26 | - ineffassign 27 | - misspell 28 | - nakedret 29 | - nestif 30 | - noctx 31 | - nolintlint 32 | - prealloc 33 | - staticcheck 34 | - tparallel 35 | - unconvert 36 | - unparam 37 | - unused 38 | - whitespace 39 | settings: 40 | dupl: 41 | threshold: 100 42 | goconst: 43 | min-len: 2 44 | min-occurrences: 3 45 | godot: 46 | scope: declarations 47 | capital: true 48 | lll: 49 | line-length: 120 50 | misspell: 51 | locale: US 52 | nolintlint: 53 | require-specific: true 54 | prealloc: 55 | simple: true 56 | range-loops: true 57 | for-loops: true 58 | exclusions: 59 | generated: lax 60 | presets: 61 | - comments 62 | - common-false-positives 63 | - legacy 64 | - std-error-handling 65 | rules: 66 | - linters: 67 | - scopelint 68 | path: pkg/frame/frame_test.go 69 | paths: 70 | - pkg/frame/frame_test.go 71 | - pkg/pipe/pipe_test.go 72 | - pkg/rpc/client_server.test.go 73 | - .github 74 | - .git 75 | - benchmarks 76 | - third_party$ 77 | - builtin$ 78 | - examples$ 79 | formatters: 80 | enable: 81 | - gofmt 82 | - goimports 83 | exclusions: 84 | generated: lax 85 | paths: 86 | - pkg/frame/frame_test.go 87 | - pkg/pipe/pipe_test.go 88 | - pkg/rpc/client_server.test.go 89 | - .github 90 | - .git 91 | - benchmarks 92 | - third_party$ 93 | - builtin$ 94 | - examples$ 95 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | v3.4.0 (17.03.2022) 5 | ------------------- 6 | 7 | ## 🚀 New: 8 | 9 | - ✏️ Add `Stream` and `Stop` bits to the protocol. 10 | - ✏️ Minimum golang version required is `v1.18` 11 | 12 | --- 13 | 14 | v3.2.5 (12.01.2022) 15 | ------------------- 16 | 17 | ## 📦 Packages: 18 | 19 | - ✏️ Replace `jsoniter` with `goccy/json` 20 | 21 | --- 22 | 23 | v3.2.4 (25.11.2021) 24 | ------------------- 25 | 26 | ## 🚀 New: 27 | 28 | - ✏️ Include bad header data in the returned error. 29 | 30 | --- 31 | 32 | v3.2.3 (18.10.2021) 33 | ------------------- 34 | 35 | ## 🚀 New: 36 | 37 | - ✏️ New reusable pool with size classes. More than 50% boost in allocation time. 38 | - ✏️ Better handling `file not found` error. 39 | 40 | --- 41 | 42 | v3.2.2 (14.10.2021) 43 | ------------------- 44 | 45 | ## 🚀 New: 46 | 47 | - 🚀 Internal performance improvements (~15% for the RPC). 48 | 49 | --- 50 | 51 | v3.2.1 (19.08.2021) 52 | ------------------- 53 | 54 | ## 📦 Packages: 55 | 56 | - 📦 Update Go to `v1.17` 57 | - 📦 Update `msgpack` to `v5` 58 | 59 | --- 60 | 61 | v3.2.0 (09.08.2021) 62 | ------------------- 63 | 64 | ## 🚀 New: 65 | 66 | - ✏️ 50% reduce bound checks in the frame's operations. [PR](https://github.com/roadrunner-server/goridge/pull/143) 67 | - 68 | 69 | ## 🩹 Fixes: 70 | 71 | - 🐛 Fix: bug with incorrectly interpreted header len AND operation. 72 | 73 | ## 🔨 BC: 74 | 75 | - 💔 Frame now for the internal (but public) operations receive additional `[]byte` slice to skip bound checks. 76 | 77 | --- 78 | 79 | v3.1.4 (14.06.2021) 80 | ------------------- 81 | 82 | ## 🩹 Fixes: 83 | 84 | - 🐛 Fix: Duplicated RPC error message: [PR](https://github.com/roadrunner-server/goridge/pull/129) 85 | 86 | --- 87 | 88 | v3.1.3 (11.06.2021) 89 | ------------------- 90 | 91 | ## 🩹 Fixes: 92 | 93 | - 🐛 Fix: Performance optimization fixes [PR](https://github.com/roadrunner-server/goridge/pull/127) 94 | 95 | --- 96 | 97 | v3.1.2 (10.06.2021) 98 | ------------------- 99 | 100 | ## 🩹 Fixes: 101 | 102 | - 🐛 Fix: Bug with incorrect proto interface casting which leads to a panic. 103 | 104 | --- 105 | 106 | v3.1.1 (09.06.2021) 107 | ------------------- 108 | 109 | ## 🚀 New: 110 | 111 | - ✏️ Standardise error operations in the whole project. 112 | - ✏️ Remove `frame` to `byte` type aliasing in hot paths. 113 | 114 | --- 115 | 116 | v3.1.0 (09.06.2021) 117 | ------------------- 118 | 119 | ## 👀 New: 120 | 121 | - ✏️ Add support for the `PROTOBUF` in the RPC. 122 | 123 | --- 124 | 125 | ### v3.0.1 (06.02.2021) 126 | 127 | - Move relay interface to the pkg folder. 128 | - Separate CI into Linux, MacOS, Windows and Linters. 129 | - Correct README.md (Installation and sample) 130 | - Add PR template. 131 | - Add dependabot. 132 | 133 | ### v3.0.0 (18.01.2021) 134 | 135 | - New protocol (pkg/frame/frame.md) 136 | - Various payloads support: msgpack, raw bytes, JSON, gob. 137 | - New repo architecture. 138 | - Shared memory [alfa]. 139 | 140 | ### v2.4.6 (21.10.2020) 141 | 142 | - Remove RawMessage check for the payload (@wolfy-j) 143 | - golang version in go.mod bumped to 1.15 144 | 145 | ## v2.4.2 (19.05.2020) 146 | 147 | - Add phpstan analyzer 148 | - Fix code warnings from phpstan 149 | - Improve Relay factory and SocketRelay 150 | - Improve test coverage 151 | - Performance improvements 152 | - See the full milestone here: [link](https://github.com/roadrunner-server/goridge/milestone/5?closed=1) 153 | 154 | ## v2.4.0 (05.05.2020) 155 | 156 | - More tests for PHP (@vvval) 157 | - Upgrade PHP version to the 7.2 (currently minimum supported) 158 | - Add new RelayInterface [link](https://github.com/roadrunner-server/goridge/pull/56/files#diff-85a3f483116946b4093f21ad855af4a8) ( 159 | @vvval) 160 | - See the full milestone here: [link](https://github.com/roadrunner-server/goridge/issues?q=is%3Aclosed+milestone%3A2.4.0) 161 | 162 | ## v2.3.1 (21.04.2020) 163 | 164 | - Syscall usage optimized. Now the data is packing and sending via 1 (or 2 in some cases) send_socket calls, instead of 165 | 2-3 (by @vvval) 166 | - Unix sockets supported on windows (AF_UNIX) starting from OS Build 17056. 167 | - Add the ability to define own relay with a codec (by @Reasno) 168 | 169 | ## v2.3.0 (23.03.2020) 170 | 171 | - Replace std encoding/json package with the https://github.com/json-iterator/go 172 | - Add BORS and GHA support 173 | - golang modules updated to v2 174 | 175 | ## v2.2.1 (30.11.2019) 176 | 177 | - Fixed too strict StreamRelay check by @tarampampam 178 | 179 | ## v2.2.0 (29.11.2019) 180 | 181 | - Update travis to support go 1.13 182 | - Update tests (errors handling, simplify) 183 | - Add go modules support 184 | - Optimize pack function 185 | 186 | by @ValeryPiashchynski 187 | 188 | ## v2.1.4 (01.04.2019) 189 | 190 | - minor performance improvements by @zloyuser 191 | - removed nighly from travis 192 | 193 | ## v2.1.3 (30.09.2018) 194 | 195 | - improved performance (reduced number of syscalls required for Send command) 196 | 197 | ## v2.1.2 (07.06.2018) 198 | 199 | - added 8 more bytes to the payload 200 | - added error detection mechanism over binary masks 201 | - added panic handler for pipe relay 202 | 203 | ## v2.1.0 (03.06.2018) 204 | 205 | - added golang ClientCodec implementation 206 | - additional error detections 207 | - added sequence support 208 | - more tests 209 | 210 | ## v2.0.5 (03.04.2018) 211 | 212 | - handled possible panic on reading from broken connection in socket relay 213 | 214 | ## v2.0.4 (23.03.2018) 215 | 216 | - minor performance improvement in memory allocation for buffer by @243083df 217 | 218 | ## v2.0.3 (20.02.2018) 219 | 220 | - fixed unix socket support on MacOS by @bgetsug 221 | 222 | ## v2.0.2 (29.01.2018) 223 | 224 | - typo in SOCK_TPC constant 225 | 226 | ## v2.0.1 (23.01.2018) 227 | 228 | - support sending empty string payloads without PAYLOAD_NONE flag 229 | 230 | ## v2.0.0 (17.11.2017) 231 | 232 | - ext-sockets is not required anymore 233 | - Connection renamed to Relay 234 | - JsonRPC renamed to RPC 235 | - Performance optimizations (+20% speed improvement) 236 | - Added relay over standard streams and pipes 237 | - Added ability to invoke PHP from Go 238 | - Added control headers to manage execution flow 239 | - CLOSE_CONNECTION and KEEP_CONNECTION constants are removed 240 | - \*\_BODY constants renamed to PAYLOAD\_\* to comply with .go code 241 | - Protocol is extracted from Codec as independent abstraction 242 | - Simplified RPC Codec implementation 243 | - Code is formatted to comply to Golint 244 | - More tests 245 | - More documentation 246 | - More error checks in PHP code 247 | - License holder moved from Spiral Framework to SpiralScout 248 | 249 | ## v1.0.4 250 | 251 | - library dependency downgraded to support PHP7.0 by @thePanz 252 | 253 | ## v1.0.1 (14.08.2017) 254 | 255 | - service level exception for invalid json payloads 256 | 257 | ## v1.0.0 (14.08.2017) 258 | 259 | - public Goridge release 260 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SpiralScout 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 | .PHONY: test 2 | test: 3 | go test -v -race -cover -tags=debug ./pkg/frame 4 | go test -v -race -cover -tags=debug ./pkg/pipe 5 | go test -v -race -cover -tags=debug ./pkg/rpc 6 | go test -v -race -cover -tags=debug ./pkg/socket 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | High-performance PHP-to-Golang IPC bridge 2 | ================================================= 3 | [![GoDoc](https://godoc.org/github.com/roadrunner-server/goridge/v3?status.svg)](https://godoc.org/github.com/roadrunner-server/goridge/v3) 4 | ![Linux](https://github.com/roadrunner-server/goridge/workflows/Linux/badge.svg) 5 | ![macOS](https://github.com/roadrunner-server/goridge/workflows/MacOS/badge.svg) 6 | ![Windows](https://github.com/roadrunner-server/goridge/workflows/Windows/badge.svg) 7 | [![Go Report Card](https://goreportcard.com/badge/github.com/spiral/goridge)](https://goreportcard.com/report/github.com/spiral/goridge) 8 | [![Codecov](https://codecov.io/gh/roadrunner-server/goridge/branch/master/graph/badge.svg)](https://codecov.io/gh/roadrunner-server/goridge/) 9 | 10 | 11 | PHPClasses Innovation Award 12 | 13 | Goridge is high performance PHP-to-Golang codec library which works over native PHP sockets and Golang net/rpc package. 14 | The library allows you to call Go service methods from PHP with a minimal footprint, structures and `[]byte` support. 15 | PHP source code can be found in this repository: [goridge-php](https://github.com/roadrunner-php/goridge) 16 | 17 |
18 | See https://github.com/roadrunner-server/roadrunner - High-performance PHP application server, load-balancer and process manager written in Golang 19 |
20 | 21 | Features 22 | -------- 23 | 24 | - no external dependencies or services, drop-in (64bit PHP version required) 25 | - low message footprint (12 bytes over any binary payload), binary error detection 26 | - CRC32 header verification 27 | - sockets over TCP or Unix (ext-sockets is required), standard pipes 28 | - very fast (300k calls per second on Ryzen 1700X over 20 threads) 29 | - native `net/rpc` integration, ability to connect to existed application(s) 30 | - standalone protocol usage 31 | - structured data transfer using json 32 | - `[]byte` transfer, including big payloads 33 | - service, message and transport level error handling 34 | - hackable 35 | - works on Windows 36 | - unix sockets powered (also on Windows) 37 | 38 | Installation 39 | ------------ 40 | 41 | ```go 42 | GO111MODULE=on go get github.com/roadrunner-server/goridge/v3 43 | ``` 44 | 45 | ### Sample of usage 46 | ```go 47 | package main 48 | 49 | import ( 50 | "fmt" 51 | "net" 52 | "net/rpc" 53 | 54 | goridgeRpc "github.com/roadrunner-server/goridge/v3/pkg/rpc" 55 | ) 56 | 57 | type App struct{} 58 | 59 | func (s *App) Hi(name string, r *string) error { 60 | *r = fmt.Sprintf("Hello, %s!", name) 61 | return nil 62 | } 63 | 64 | func main() { 65 | ln, err := net.Listen("tcp", ":6001") 66 | if err != nil { 67 | panic(err) 68 | } 69 | 70 | _ = rpc.Register(new(App)) 71 | 72 | for { 73 | conn, err := ln.Accept() 74 | if err != nil { 75 | continue 76 | } 77 | _ = conn 78 | go rpc.ServeCodec(goridgeRpc.NewCodec(conn)) 79 | } 80 | } 81 | ``` 82 | 83 | License 84 | ------- 85 | 86 | The MIT License (MIT). Please see [`LICENSE`](./LICENSE) for more information. 87 | -------------------------------------------------------------------------------- /benchmarks/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "net/http/pprof" 8 | "net/rpc" 9 | "time" 10 | 11 | goridgeRpc "github.com/roadrunner-server/goridge/v3/pkg/rpc" 12 | "github.com/roadrunner-server/goridge/v3/tests" 13 | ) 14 | 15 | func main() { 16 | s := NewServer() 17 | go func() { 18 | _ = s.Start("localhost:6061") 19 | }() 20 | defer func() { 21 | _ = s.Stop(context.Background()) 22 | }() 23 | 24 | time.Sleep(time.Second * 5) 25 | // create an pprof server 26 | server() 27 | time.Sleep(time.Second * 1) 28 | 29 | client() 30 | } 31 | 32 | // testService sample 33 | type testService struct{} 34 | 35 | func (s *testService) ProtoMessage(payload *tests.Payload, item *tests.Item) error { 36 | (*item).Key = payload.Items[0].Key 37 | return nil 38 | } 39 | 40 | func client() { 41 | err := rpc.RegisterName("testbench", new(testService)) 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | conn, err := net.Dial("tcp", "127.0.0.1:18321") 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | client := rpc.NewClientWithCodec(goridgeRpc.NewClientCodec(conn)) 52 | defer func() { 53 | err := client.Close() 54 | if err != nil { 55 | panic(err) 56 | } 57 | }() 58 | 59 | tt := time.Now().String() 60 | 61 | keysP := &tests.Payload{ 62 | Storage: "memory-rr", 63 | Items: []*tests.Item{ 64 | { 65 | Key: "a", 66 | Value: "hhhhhhhhhhhhhhhhheeeeeeeeeeeeeeeeeeeeeeeeeeeelllllllllllllllllllllllllllllllllloooooooooooooooooooooooooooooo", 67 | Timeout: tt, 68 | }, 69 | { 70 | Key: "b", 71 | Value: "hhhhhhhhhhhhhhhhheeeeeeeeeeeeeeeeeeeeeeeeeeeelllllllllllllllllllllllllllllllllloooooooooooooooooooooooooooooo", 72 | Timeout: tt, 73 | }, 74 | { 75 | Key: "c", 76 | Value: "hhhhhhhhhhhhhhhhheeeeeeeeeeeeeeeeeeeeeeeeeeeelllllllllllllllllllllllllllllllllloooooooooooooooooooooooooooooo", 77 | Timeout: tt, 78 | }, 79 | }, 80 | } 81 | 82 | item := &tests.Item{} 83 | for range 1000000 { 84 | err = client.Call("testbench.ProtoMessage", keysP, item) 85 | if err != nil { 86 | panic(err) 87 | } 88 | } 89 | } 90 | 91 | func server() { 92 | ln, err := net.Listen("tcp", "127.0.0.1:18321") 93 | if err != nil { 94 | panic(err) 95 | } 96 | 97 | go func() { 98 | for { 99 | conn, err2 := ln.Accept() 100 | if err2 != nil { 101 | panic(err2) 102 | } 103 | rpc.ServeCodec(goridgeRpc.NewCodec(conn)) 104 | } 105 | }() 106 | } 107 | 108 | // Server is a HTTP server for debugging. 109 | type Server struct { 110 | srv *http.Server 111 | } 112 | 113 | // NewServer creates new HTTP server for debugging. 114 | func NewServer() Server { 115 | mux := http.NewServeMux() 116 | 117 | mux.HandleFunc("/debug/pprof/", pprof.Index) 118 | mux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) 119 | mux.HandleFunc("/debug/pprof/profile", pprof.Profile) 120 | mux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) 121 | mux.HandleFunc("/debug/pprof/trace", pprof.Trace) 122 | 123 | return Server{srv: &http.Server{Handler: mux}} 124 | } 125 | 126 | // Start debug server. 127 | func (s *Server) Start(addr string) error { 128 | s.srv.Addr = addr 129 | 130 | return s.srv.ListenAndServe() 131 | } 132 | 133 | // Stop debug server. 134 | func (s *Server) Stop(ctx context.Context) error { 135 | return s.srv.Shutdown(ctx) 136 | } 137 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | target: auto 6 | threshold: 0% 7 | informational: true 8 | patch: 9 | default: 10 | target: auto 11 | threshold: 0% 12 | informational: true 13 | # do not include tests folders 14 | ignore: 15 | - "interfaces" -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/roadrunner-server/goridge/v3 2 | 3 | go 1.24 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | github.com/goccy/go-json v0.10.5 9 | github.com/roadrunner-server/errors v1.4.1 10 | github.com/stretchr/testify v1.10.0 11 | github.com/vmihailenco/msgpack/v5 v5.4.1 12 | google.golang.org/protobuf v1.36.6 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/pmezard/go-difflib v1.0.0 // indirect 18 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 19 | gopkg.in/yaml.v3 v3.0.1 // indirect 20 | ) 21 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 4 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 5 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 6 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/roadrunner-server/errors v1.4.1 h1:LKNeaCGiwd3t8IaL840ZNF3UA9yDQlpvHnKddnh0YRQ= 10 | github.com/roadrunner-server/errors v1.4.1/go.mod h1:qeffnIKG0e4j1dzGpa+OGY5VKSfMphizvqWIw8s2lAo= 11 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 12 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 13 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 14 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 15 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 16 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 17 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 18 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 19 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 20 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | -------------------------------------------------------------------------------- /internal/bpool.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var frameChunkedPool = &sync.Map{} 8 | var preallocate = &sync.Once{} 9 | 10 | const ( 11 | OneMB uint32 = 1024 * 1024 * 1 12 | FiveMB uint32 = 1024 * 1024 * 5 13 | TenMB uint32 = 1024 * 1024 * 10 14 | ) 15 | 16 | func Preallocate() { 17 | preallocate.Do(internalAllocate) 18 | } 19 | 20 | func internalAllocate() { 21 | pool1 := &sync.Pool{ 22 | New: func() any { 23 | data := make([]byte, OneMB) 24 | return &data 25 | }, 26 | } 27 | pool5 := &sync.Pool{ 28 | New: func() any { 29 | data := make([]byte, FiveMB) 30 | return &data 31 | }, 32 | } 33 | pool10 := &sync.Pool{ 34 | New: func() any { 35 | data := make([]byte, TenMB) 36 | return &data 37 | }, 38 | } 39 | 40 | frameChunkedPool.Store(OneMB, pool1) 41 | frameChunkedPool.Store(FiveMB, pool5) 42 | frameChunkedPool.Store(TenMB, pool10) 43 | } 44 | 45 | func get(size uint32) *[]byte { 46 | switch { 47 | case size <= OneMB: 48 | val, _ := frameChunkedPool.Load(OneMB) 49 | return val.(*sync.Pool).Get().(*[]byte) 50 | case size <= FiveMB: 51 | val, _ := frameChunkedPool.Load(FiveMB) 52 | return val.(*sync.Pool).Get().(*[]byte) 53 | case size <= TenMB: 54 | val, _ := frameChunkedPool.Load(TenMB) 55 | return val.(*sync.Pool).Get().(*[]byte) 56 | default: 57 | data := make([]byte, size) 58 | return &data 59 | } 60 | } 61 | 62 | func put(size uint32, data *[]byte) { 63 | switch { 64 | case size <= OneMB: 65 | pool, _ := frameChunkedPool.Load(OneMB) 66 | pool.(*sync.Pool).Put(data) 67 | return 68 | case size <= FiveMB: 69 | pool, _ := frameChunkedPool.Load(FiveMB) 70 | pool.(*sync.Pool).Put(data) 71 | return 72 | default: 73 | pool, _ := frameChunkedPool.Load(TenMB) 74 | pool.(*sync.Pool).Put(data) 75 | return 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/receive.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | stderr "errors" 6 | "io" 7 | "time" 8 | 9 | "github.com/roadrunner-server/errors" 10 | "github.com/roadrunner-server/goridge/v3/pkg/frame" 11 | ) 12 | 13 | // shortland for the Could not open input file: ../roadrunner/tests/psr-wfsdorker.php 14 | var res = []byte("Could not op") //nolint:gochecknoglobals 15 | 16 | const validationError = "validation failed on the message sent to STDOUT, see: https://docs.roadrunner.dev/error-codes/stdout-crc, invalid message: %s" 17 | 18 | func ReceiveFrame(relay io.Reader, fr *frame.Frame) error { 19 | const op = errors.Op("goridge_frame_receive") 20 | 21 | _, err := io.ReadFull(relay, fr.Header()) 22 | if err != nil { 23 | return err 24 | } 25 | 26 | // todo: rustatian: think about smarter solution 27 | if bytes.Equal(fr.Header(), res) { 28 | data, errRa := io.ReadAll(relay) 29 | if errRa == nil && len(data) > 0 { 30 | return errors.E(op, errors.FileNotFound, errors.Str(string(fr.Header())+string(data))) 31 | } 32 | 33 | return errors.E(op, errors.FileNotFound, errors.Str("file not found")) 34 | } 35 | 36 | // we have options 37 | if fr.ReadHL(fr.Header()) > 3 { 38 | // we should read the options 39 | optsLen := (fr.ReadHL(fr.Header()) - 3) * frame.WORD 40 | opts := make([]byte, optsLen) 41 | 42 | // read the next part of the frame - options 43 | _, err = io.ReadFull(relay, opts) 44 | if err != nil { 45 | if stderr.Is(err, io.EOF) { 46 | return err 47 | } 48 | return errors.E(op, err) 49 | } 50 | 51 | // we should append frame's 52 | fr.AppendOptions(fr.HeaderPtr(), opts) 53 | } 54 | 55 | // verify header CRC 56 | if !fr.VerifyCRC(fr.Header()) { 57 | type deadliner interface { 58 | SetReadDeadline(time.Time) error 59 | } 60 | 61 | if d, ok := relay.(deadliner); ok { 62 | err = d.SetReadDeadline(time.Now().Add(time.Second * 2)) 63 | if err != nil { 64 | return errors.E(op, errors.Errorf(validationError, fr.Header())) 65 | } 66 | 67 | // we don't care about error here 68 | resp, _ := io.ReadAll(relay) 69 | 70 | return errors.E(op, errors.Errorf(validationError, string(fr.Header())+string(resp))) 71 | } 72 | 73 | // no deadline, so, only 14 bytes 74 | return errors.E(op, errors.Errorf(validationError, fr.Header())) 75 | } 76 | 77 | // read the read payload 78 | pl := fr.ReadPayloadLen(fr.Header()) 79 | // no payload 80 | if pl == 0 { 81 | return nil 82 | } 83 | 84 | pb := get(pl) 85 | _, err2 := io.ReadFull(relay, (*pb)[:pl]) 86 | if err2 != nil { 87 | if stderr.Is(err2, io.EOF) { 88 | put(pl, pb) 89 | return err 90 | } 91 | put(pl, pb) 92 | return errors.E(op, err2) 93 | } 94 | 95 | fr.WritePayload((*pb)[:pl]) 96 | put(pl, pb) 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /pkg/frame/frame.go: -------------------------------------------------------------------------------- 1 | package frame 2 | 3 | import ( 4 | "hash/crc32" 5 | ) 6 | 7 | // OptionsMaxSize represents header's options maximum size 8 | const OptionsMaxSize = 40 9 | 10 | // WORD represents 32bit word 11 | const WORD = 4 12 | 13 | // Frame defines new user level package format. 14 | type Frame struct { 15 | // Payload, max length 4.2GB. 16 | payload []byte 17 | // Header 18 | header []byte 19 | } 20 | 21 | // ReadHeader reads only header, without payload 22 | func ReadHeader(data []byte) *Frame { // inlined, cost 14 23 | _ = data[11] 24 | return &Frame{ 25 | header: data[:12], 26 | payload: nil, 27 | } 28 | } 29 | 30 | // ReadFrame produces Frame from the RAW bytes 31 | // first 12 bytes will be a header 32 | // the rest - payload 33 | func ReadFrame(data []byte) *Frame { // inlined, cost 60 34 | _ = data[11] 35 | opt := data[0] & 0x0F 36 | // if more than 3, that we have options 37 | if opt > 3 { 38 | return &Frame{ 39 | header: data[:opt*WORD], 40 | payload: data[opt*WORD:], 41 | } 42 | } 43 | f := &Frame{ 44 | header: data[:12], 45 | payload: data[12:], 46 | } 47 | 48 | f.header[10], f.header[11] = 0, 0 49 | 50 | return f 51 | } 52 | 53 | // NewFrame initializes new frame with 12-byte header and 100-byte reserved space for the payload 54 | func NewFrame() *Frame { 55 | f := &Frame{ 56 | header: make([]byte, 12), 57 | payload: make([]byte, 0, 100), 58 | } 59 | // set default header len (2) 60 | f.defaultHL(f.header) 61 | return f 62 | } 63 | 64 | // From will represent header and payload as a Frame 65 | func From(header []byte, payload []byte) *Frame { 66 | return &Frame{ 67 | payload: payload, 68 | header: header, 69 | } 70 | } 71 | 72 | // ReadVersion ... To read version, we should return our 4 upper bits to their original place 73 | // 1111_0000 -> 0000_1111 (15) 74 | func (*Frame) ReadVersion(header []byte) byte { 75 | _ = header[0] 76 | return header[0] >> 4 77 | } 78 | 79 | // WriteVersion 80 | // To write version, we should do the following: 81 | // 1. For example, we have version 15 it's 0000_1111 (1 byte) 82 | // 2. We should shift 4 lower bits to upper and write that to the 0th byte 83 | // 3. The 0th byte should become 1111_0000, but it's not 240, it's only 15, because version only 4 bits len 84 | func (*Frame) WriteVersion(header []byte, version byte) { 85 | _ = header[0] 86 | if version > 15 { 87 | panic("version is only 4 bits") 88 | } 89 | header[0] = version<<4 | header[0] 90 | } 91 | 92 | // ReadHL 93 | // The lower 4 bits of the 0th octet occupies our header len data. 94 | // We should erase upper 4 bits, which contain information about Version 95 | // To erase, we are applying bitwise AND to the upper 4 bits and returning result 96 | func (*Frame) ReadHL(header []byte) byte { 97 | // 0101_1111 0000_1111 98 | // 0x0F - 15 99 | return header[0] & 0x0F 100 | } 101 | 102 | func (f *Frame) incrementHL(header []byte) { 103 | hl := f.ReadHL(header) 104 | if hl == 15 { 105 | panic("header len should be less than 15 to increment") 106 | } 107 | header[0] = header[0] | hl + 1 108 | } 109 | 110 | // ReadFlags 111 | // Flags is full 1st byte 112 | func (f *Frame) ReadFlags() byte { 113 | return f.header[1] 114 | } 115 | 116 | func (*Frame) WriteFlags(header []byte, flags ...byte) { 117 | _ = header[1] 118 | for i := range flags { 119 | header[1] |= flags[i] 120 | } 121 | } 122 | 123 | func (*Frame) SetStreamFlag(header []byte) { 124 | _ = header[11] 125 | header[10] |= STREAM 126 | } 127 | 128 | func (*Frame) IsStream(header []byte) bool { 129 | _ = header[11] 130 | return header[10]&STREAM != 0 131 | } 132 | 133 | func (*Frame) IsPing(header []byte) bool { 134 | _ = header[11] 135 | return header[10]&PING != 0 136 | } 137 | 138 | func (*Frame) SetPingBit(header []byte) { 139 | _ = header[11] 140 | header[10] |= PING 141 | } 142 | 143 | func (*Frame) IsPong(header []byte) bool { 144 | _ = header[11] 145 | return header[10]&PONG != 0 146 | } 147 | 148 | func (*Frame) SetPongBit(header []byte) { 149 | _ = header[11] 150 | header[10] |= PONG 151 | } 152 | 153 | func (*Frame) SetStopBit(header []byte) { 154 | _ = header[11] 155 | header[10] |= STOP 156 | } 157 | 158 | func (*Frame) IsStop(header []byte) bool { 159 | return header[10]&STOP != 0 160 | } 161 | 162 | // WriteOptions 163 | // Options slice len should not be more than 10 (40 bytes) 164 | // we need a pointer to the header because we are reallocating the slice 165 | func (f *Frame) WriteOptions(header *[]byte, options ...uint32) { 166 | if options == nil { 167 | return 168 | } 169 | 170 | if header == nil { 171 | panic("header should not be nil") 172 | } 173 | 174 | if len(options) > 10 { 175 | panic("header options limited by 40 bytes") 176 | } 177 | 178 | hl := f.ReadHL(*header) 179 | // check before writing. we can't handle more than 15*4 bytes of HL (3 for header and 12 for options) 180 | if hl == 15 { 181 | panic("header len could not be equal to 15 to write options") 182 | } 183 | 184 | // make a new slice with the exact len (not doubled) 185 | newSl := make([]byte, (len(options)*WORD)+len(*header)) 186 | // copy old data 187 | copy(newSl, *header) 188 | 189 | for i, j := 0, 12; i < len(options); i, j = i+1, j+WORD { 190 | newSl[j] |= byte(options[i]) 191 | newSl[j+1] |= byte(options[i] >> 8) 192 | newSl[j+2] |= byte(options[i] >> 16) 193 | newSl[j+3] |= byte(options[i] >> 24) 194 | 195 | f.incrementHL(newSl) // increment header len by 32 bit 196 | } 197 | 198 | // replace value 199 | *header = newSl 200 | } 201 | 202 | // ReadOptions 203 | // f.readHL() - 2 needed to know actual options size 204 | // we know, that 2 WORDS is minimal header len 205 | // extra WORDS will add extra 32bits to the options (4 bytes) 206 | // cannot inline, cost 117 vs 80 207 | func (f *Frame) ReadOptions(header []byte) []uint32 { //nolint:funlen 208 | ol := f.ReadHL(header) 209 | // we can read options, if there are no options 210 | if ol <= 3 { 211 | return nil 212 | } 213 | 214 | // last byte after main header and first options byte 215 | const lb = 12 216 | 217 | // Get the options len minus the standard options 218 | optionLen := ol - 3 // 3 is the default 219 | // check the options len 220 | if optionLen*WORD > OptionsMaxSize { 221 | panic("options size is limited by 40 bytes (10 4-bytes words)") 222 | } 223 | 224 | // slice in place 225 | options := make([]uint32, optionLen) 226 | 227 | // SAMPLE 228 | // Options starting from 12-th byte till 52-th byte (40 bytes max) 229 | // we should scan with 4 byte window (32bit, WORD) 230 | // 8 12 16 231 | // 9 13 17 232 | // 10 14 18 233 | // 11 15 19 234 | 235 | // loop unwind 236 | i := byte(0) 237 | j := 0 238 | 239 | _ = header[lb+i+3] 240 | _ = options[j] 241 | 242 | // 1 243 | options[j] |= uint32(header[lb+i]) 244 | options[j] |= uint32(header[lb+i+1]) << 8 245 | options[j] |= uint32(header[lb+i+2]) << 16 246 | options[j] |= uint32(header[lb+i+3]) << 24 247 | 248 | i += WORD 249 | j++ 250 | 251 | if i == optionLen*WORD { 252 | goto done 253 | } 254 | 255 | _ = header[lb+i+3] 256 | _ = options[j] 257 | // 2 258 | options[j] |= uint32(header[lb+i]) 259 | options[j] |= uint32(header[lb+i+1]) << 8 260 | options[j] |= uint32(header[lb+i+2]) << 16 261 | options[j] |= uint32(header[lb+i+3]) << 24 262 | 263 | i += WORD 264 | j++ 265 | 266 | if i == optionLen*WORD { 267 | goto done 268 | } 269 | 270 | _ = header[lb+i+3] 271 | _ = options[j] 272 | // 3 273 | options[j] |= uint32(header[lb+i]) 274 | options[j] |= uint32(header[lb+i+1]) << 8 275 | options[j] |= uint32(header[lb+i+2]) << 16 276 | options[j] |= uint32(header[lb+i+3]) << 24 277 | 278 | i += WORD 279 | j++ 280 | 281 | if i == optionLen*WORD { 282 | goto done 283 | } 284 | 285 | _ = header[lb+i+3] 286 | _ = options[j] 287 | // 4 288 | options[j] |= uint32(header[lb+i]) 289 | options[j] |= uint32(header[lb+i+1]) << 8 290 | options[j] |= uint32(header[lb+i+2]) << 16 291 | options[j] |= uint32(header[lb+i+3]) << 24 292 | 293 | i += WORD 294 | j++ 295 | 296 | if i == optionLen*WORD { 297 | goto done 298 | } 299 | 300 | _ = header[lb+i+3] 301 | _ = options[j] 302 | // 5 303 | options[j] |= uint32(header[lb+i]) 304 | options[j] |= uint32(header[lb+i+1]) << 8 305 | options[j] |= uint32(header[lb+i+2]) << 16 306 | options[j] |= uint32(header[lb+i+3]) << 24 307 | 308 | i += WORD 309 | j++ 310 | 311 | if i == optionLen*WORD { 312 | goto done 313 | } 314 | 315 | _ = header[lb+i+3] 316 | _ = options[j] 317 | // 6 318 | options[j] |= uint32(header[lb+i]) 319 | options[j] |= uint32(header[lb+i+1]) << 8 320 | options[j] |= uint32(header[lb+i+2]) << 16 321 | options[j] |= uint32(header[lb+i+3]) << 24 322 | 323 | i += WORD 324 | j++ 325 | 326 | if i == optionLen*WORD { 327 | goto done 328 | } 329 | 330 | _ = header[lb+i+3] 331 | _ = options[j] 332 | // 7 333 | options[j] |= uint32(header[lb+i]) 334 | options[j] |= uint32(header[lb+i+1]) << 8 335 | options[j] |= uint32(header[lb+i+2]) << 16 336 | options[j] |= uint32(header[lb+i+3]) << 24 337 | 338 | i += WORD 339 | j++ 340 | 341 | if i == optionLen*WORD { 342 | goto done 343 | } 344 | 345 | _ = header[lb+i+3] 346 | _ = options[j] 347 | // 8 348 | options[j] |= uint32(header[lb+i]) 349 | options[j] |= uint32(header[lb+i+1]) << 8 350 | options[j] |= uint32(header[lb+i+2]) << 16 351 | options[j] |= uint32(header[lb+i+3]) << 24 352 | 353 | i += WORD 354 | j++ 355 | 356 | if i == optionLen*WORD { 357 | goto done 358 | } 359 | 360 | _ = header[lb+i+3] 361 | _ = options[j] 362 | // 9 363 | options[j] |= uint32(header[lb+i]) 364 | options[j] |= uint32(header[lb+i+1]) << 8 365 | options[j] |= uint32(header[lb+i+2]) << 16 366 | options[j] |= uint32(header[lb+i+3]) << 24 367 | 368 | i += WORD 369 | j++ 370 | 371 | if i == optionLen*WORD { 372 | goto done 373 | } 374 | 375 | _ = header[lb+i+3] 376 | _ = options[j] 377 | // 10 - last possible 378 | options[j] |= uint32(header[lb+i]) 379 | options[j] |= uint32(header[lb+i+1]) << 8 380 | options[j] |= uint32(header[lb+i+2]) << 16 381 | options[j] |= uint32(header[lb+i+3]) << 24 382 | 383 | return options 384 | 385 | done: 386 | return options 387 | } 388 | 389 | // ReadPayloadLen 390 | // LE format used to write Payload 391 | // Using 4 bytes (2,3,4,5 bytes in the header) 392 | func (*Frame) ReadPayloadLen(header []byte) uint32 { 393 | // 2,3,4,5 394 | _ = header[5] 395 | return uint32(header[2]) | uint32(header[3])<<8 | uint32(header[4])<<16 | uint32(header[5])<<24 396 | } 397 | 398 | // WritePayloadLen 399 | // LE format used to write Payload 400 | // Using 4 bytes (2,3,4,5 bytes in the header) 401 | func (*Frame) WritePayloadLen(header []byte, payloadLen uint32) { 402 | _ = header[5] 403 | header[2] = byte(payloadLen) 404 | header[3] = byte(payloadLen >> 8) 405 | header[4] = byte(payloadLen >> 16) 406 | header[5] = byte(payloadLen >> 24) 407 | } 408 | 409 | // WriteCRC will calculate and write CRC32 4-bytes it to the 6th byte (7th reserved) 410 | func (*Frame) WriteCRC(header []byte) { 411 | // 6 7 8 9 10 11 bytes 412 | _ = header[11] 413 | // calculate crc 414 | crc := crc32.ChecksumIEEE(header[:6]) 415 | header[6] = byte(crc) 416 | header[7] = byte(crc >> 8) 417 | header[8] = byte(crc >> 16) 418 | header[9] = byte(crc >> 24) 419 | } 420 | 421 | // AppendOptions appends options to the header 422 | func (*Frame) AppendOptions(header *[]byte, options []byte) { 423 | // make a new slice with the exact len (not doubled) 424 | newSl := make([]byte, len(options)+len(*header)) 425 | // copy old data 426 | copy(newSl, *header) 427 | // j = 12 - first options byte 428 | for i, j := 0, 12; i < len(options); i, j = i+1, j+1 { 429 | newSl[j] = options[i] 430 | } 431 | 432 | // replace value 433 | *header = newSl 434 | } 435 | 436 | // VerifyCRC ... 437 | // Reading info from 6th byte and verifying it with calculated in-place. Should be equal. 438 | // If not - drop the frame as incorrect. 439 | func (*Frame) VerifyCRC(header []byte) bool { 440 | _ = header[9] 441 | return crc32.ChecksumIEEE(header[:6]) == uint32(header[6])|uint32(header[7])<<8|uint32(header[8])<<16|uint32(header[9])<<24 442 | } 443 | 444 | // Bytes returns header with payload 445 | func (f *Frame) Bytes() []byte { 446 | buf := make([]byte, 0, len(f.header)+len(f.payload)) 447 | buf = append(buf, f.header...) 448 | buf = append(buf, f.payload...) 449 | return buf 450 | } 451 | 452 | // Header returns frame header 453 | func (f *Frame) Header() []byte { 454 | return f.header 455 | } 456 | 457 | // HeaderPtr returns frame header pointer 458 | func (f *Frame) HeaderPtr() *[]byte { 459 | return &f.header 460 | } 461 | 462 | // Payload returns frame payload without header 463 | func (f *Frame) Payload() []byte { 464 | // start from the 1st (staring from 0) byte 465 | return f.payload 466 | } 467 | 468 | // WritePayload writes payload 469 | func (f *Frame) WritePayload(data []byte) { 470 | f.payload = make([]byte, len(data)) 471 | copy(f.payload, data) 472 | } 473 | 474 | // Reset a frame 475 | func (f *Frame) Reset() { 476 | f.header = make([]byte, 12) 477 | f.payload = make([]byte, 0, 100) 478 | 479 | f.defaultHL(f.header) 480 | } 481 | 482 | // -------- PRIVATE 483 | func (f *Frame) defaultHL(header []byte) { 484 | f.writeHl(header, 3) 485 | } 486 | 487 | // Writing HL is very simple. Since we are using lower 4 bits 488 | // we can easily apply bitwise OR and set lower 4 bits to needed hl value 489 | func (*Frame) writeHl(header []byte, hl byte) { 490 | header[0] |= hl 491 | } 492 | -------------------------------------------------------------------------------- /pkg/frame/frame.md: -------------------------------------------------------------------------------- 1 | ```log 2 | 0 1 2 3 OCTET 3 | 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 4 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 5 | |Version| HL | Flags | Payload Length | 0 6 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 7 | | Payload Length | Header CRC | 4 8 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 9 | | Header CRC | Stream | 8 10 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 11 | | Options | Payload | 12 12 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 13 | ``` 14 | 15 | 1. `0-th` byte contains version and header length (HL). HL calculated in 32bit words. For example, HL is 3, that means, that size of the header is 3*32bit = 96bits = 12 bytes. 16 | 2. `1-st` byte contains flags. The flags described in frame_flags.go file. It consists of overlapping and non-overlapping flags. 17 | Overlapping flags are just bit flags. They might be combined with bitwise OR and checked with bitwise AND. Non-overlapping flags 18 | can't be used with other flags. In means, that if you have non-overlapping flag in 1-st byte, you can't use other flags. 19 | 20 | 3. `(2, 3, 4, 5)` bytes contain payload length and represented by unsigned long 32bit integer (up to 4Gb in payload). 21 | 4. `(6, 7, 8, 9)` bytes contain header `CRC32` checksum. CRC32 calculated only for `0-5` (including) bytes. 22 | 5. `(10, 11)` bytes contain stream information. `0-th` bit of `10-th` byte used to indicate a stream send, `1st` bit indicates a stop command. 23 | 6. `(12..52)` bytes contain options. Options are optional. As an example of usage, in `goridge` in case of pipes or sockets 24 | we write two unsigned 32bit integers of RPC_SEQ_ID and method length offset. This field can be up to 40 bytes. 25 | 26 | 7. `From (12..52)` lays payload. Maximum payload, that can be transmitted via 1 frame is `4Gb`. -------------------------------------------------------------------------------- /pkg/frame/frame_flags.go: -------------------------------------------------------------------------------- 1 | package frame 2 | 3 | // BYTE flags, it means, that we can set multiply flags from this group using bitwise OR 4 | // For example CONTEXT_SEPARATOR | CodecRaw 5 | const ( 6 | CONTROL byte = 0x01 7 | CodecRaw byte = 0x04 8 | CodecJSON byte = 0x08 9 | CodecMsgpack byte = 0x10 10 | CodecGob byte = 0x20 11 | ERROR byte = 0x40 12 | CodecProto byte = 0x80 13 | 14 | // Version1 byte 15 | Version1 byte = 0x01 16 | 17 | /* 18 | 10th byte, stream 19 | */ 20 | 21 | // STREAM bit 22 | STREAM byte = 0x01 23 | // STOP command 24 | STOP byte = 0x02 25 | // PING command 26 | PING byte = 0x04 27 | // PONG command 28 | PONG byte = 0x08 29 | ) 30 | -------------------------------------------------------------------------------- /pkg/frame/frame_test.go: -------------------------------------------------------------------------------- 1 | package frame 2 | 3 | import ( 4 | "hash/crc32" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | const TestPayload = `alsdjf;lskjdgljasg;lkjsalfkjaskldjflkasjdf;lkasjfdalksdjflkajsdf;lfasdgnslsnblna;sldjjfawlkejr;lwjenlksndlfjawl;ejr;lwjelkrjaldfjl;sdjf` 11 | 12 | func TestNewFrame(t *testing.T) { 13 | nf := NewFrame() 14 | nf.WriteVersion(nf.Header(), Version1) 15 | nf.WriteFlags(nf.Header(), CONTROL) 16 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 17 | nf.WriteCRC(nf.header) 18 | 19 | nf.WritePayload([]byte(TestPayload)) 20 | 21 | data := nf.Bytes() 22 | 23 | rf := ReadFrame(data) 24 | 25 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 26 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 27 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 28 | assert.Equal(t, true, rf.VerifyCRC(rf.Header())) 29 | assert.Equal(t, []uint32(nil), rf.ReadOptions(rf.Header())) 30 | } 31 | 32 | func TestAppendOptions(t *testing.T) { 33 | nf := NewFrame() 34 | nf.WriteVersion(nf.Header(), Version1) 35 | nf.WriteFlags(nf.Header(), CONTROL) 36 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 37 | nf.WriteCRC(nf.header) 38 | 39 | nf.AppendOptions(nf.HeaderPtr(), []byte{byte(112), byte(123), byte(0), byte(0)}) 40 | 41 | nf.WritePayload([]byte(TestPayload)) 42 | 43 | data := nf.Bytes() 44 | 45 | rf := ReadFrame(data) 46 | 47 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 48 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 49 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 50 | assert.Equal(t, true, rf.VerifyCRC(rf.Header())) 51 | assert.Equal(t, []uint32(nil), rf.ReadOptions(rf.Header())) 52 | } 53 | 54 | func TestFrame_VerifyCRC_Fail(t *testing.T) { 55 | nf := NewFrame() 56 | // this is the wrong position 57 | nf.WriteCRC(nf.Header()) 58 | nf.WriteVersion(nf.Header(), Version1) 59 | nf.WriteFlags(nf.Header(), CONTROL) 60 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 61 | 62 | nf.WritePayload([]byte(TestPayload)) 63 | 64 | data := nf.Bytes() 65 | 66 | rf := ReadFrame(data) 67 | 68 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 69 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 70 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 71 | assert.Equal(t, false, rf.VerifyCRC(rf.Header())) 72 | } 73 | 74 | func TestFrame_OptionsWithNoOptions(t *testing.T) { 75 | nf := NewFrame() 76 | nf.WriteVersion(nf.Header(), 1) 77 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 78 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 79 | nf.WriteOptions(nf.HeaderPtr()) 80 | 81 | // test options 82 | options := nf.ReadOptions(nf.Header()) 83 | assert.Equal(t, []uint32(nil), options) 84 | // write payload 85 | nf.WritePayload([]byte(TestPayload)) 86 | nf.WriteCRC(nf.Header()) 87 | data := nf.Bytes() 88 | 89 | rf := ReadFrame(data) 90 | 91 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 92 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 93 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 94 | assert.Equal(t, rf.VerifyCRC(rf.Header()), true) 95 | } 96 | 97 | func TestFrame_Panic(t *testing.T) { 98 | defer func() { 99 | assert.NotNil(t, recover()) 100 | }() 101 | nf := NewFrame() 102 | nf.WriteVersion(nf.Header(), 1) 103 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 104 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 105 | nf.WriteOptions(nf.HeaderPtr(), 323423432, 1213231, 1, 2, 3, 4, 5, 2, 1, 2, 12) 106 | nf.WriteOptions(nf.HeaderPtr(), 323423432) 107 | } 108 | 109 | func TestFrame_IncrementHLPanic(t *testing.T) { 110 | defer func() { 111 | assert.NotNil(t, recover()) 112 | }() 113 | nf := NewFrame() 114 | nf.WriteVersion(nf.Header(), 1) 115 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 116 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 117 | nf.WriteOptions(nf.HeaderPtr(), 323423432, 1213231, 1, 2, 3, 4, 5, 2, 1, 2) 118 | nf.incrementHL(nf.header) 119 | nf.incrementHL(nf.header) 120 | nf.incrementHL(nf.header) 121 | } 122 | 123 | func TestFrame_ReadOptionsPanic(t *testing.T) { 124 | defer func() { 125 | assert.NotNil(t, recover()) 126 | }() 127 | nf := NewFrame() 128 | nf.WriteVersion(nf.Header(), 1) 129 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 130 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 131 | nf.WriteOptions(nf.HeaderPtr(), 323423432, 1213231, 1, 2, 3, 4, 5, 2, 1, 2, 12) 132 | nf.header[53] = 123 133 | } 134 | 135 | func TestFrame_Options(t *testing.T) { 136 | nf := NewFrame() 137 | nf.WriteVersion(nf.Header(), 1) 138 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 139 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 140 | nf.WriteOptions(nf.HeaderPtr(), 323423432, 1213231) 141 | 142 | // test options 143 | options := nf.ReadOptions(nf.Header()) 144 | assert.Equal(t, []uint32{323423432, 1213231}, options) 145 | // write payload 146 | nf.WritePayload([]byte(TestPayload)) 147 | nf.WriteCRC(nf.Header()) 148 | data := nf.Bytes() 149 | 150 | rf := ReadFrame(data) 151 | 152 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 153 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 154 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 155 | assert.Equal(t, rf.VerifyCRC(rf.Header()), true) 156 | } 157 | 158 | func TestFrame_Stream(t *testing.T) { 159 | nf := NewFrame() 160 | nf.WriteVersion(nf.Header(), 1) 161 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 162 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 163 | nf.WriteOptions(nf.HeaderPtr(), 323423432, 1213231) 164 | nf.SetStreamFlag(nf.Header()) 165 | 166 | // test options 167 | options := nf.ReadOptions(nf.Header()) 168 | assert.Equal(t, []uint32{323423432, 1213231}, options) 169 | // write payload 170 | nf.WritePayload([]byte(TestPayload)) 171 | nf.WriteCRC(nf.Header()) 172 | data := nf.Bytes() 173 | 174 | rf := ReadFrame(data) 175 | 176 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 177 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 178 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 179 | assert.Equal(t, true, rf.VerifyCRC(rf.Header())) 180 | assert.Equal(t, true, rf.IsStream(rf.Header())) 181 | } 182 | 183 | func TestFrame_Stop(t *testing.T) { 184 | nf := NewFrame() 185 | nf.WriteVersion(nf.Header(), 1) 186 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 187 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 188 | nf.WriteOptions(nf.HeaderPtr(), 323423432, 1213231) 189 | nf.SetStreamFlag(nf.Header()) 190 | nf.SetStopBit(nf.Header()) 191 | 192 | // test options 193 | options := nf.ReadOptions(nf.Header()) 194 | assert.Equal(t, []uint32{323423432, 1213231}, options) 195 | // write payload 196 | nf.WritePayload([]byte(TestPayload)) 197 | nf.WriteCRC(nf.Header()) 198 | data := nf.Bytes() 199 | 200 | rf := ReadFrame(data) 201 | 202 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 203 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 204 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 205 | assert.Equal(t, true, rf.VerifyCRC(rf.Header())) 206 | assert.Equal(t, true, rf.IsStream(rf.Header())) 207 | assert.Equal(t, true, rf.IsStop(rf.Header())) 208 | } 209 | 210 | func TestFrame_Stop2(t *testing.T) { 211 | nf := NewFrame() 212 | nf.WriteVersion(nf.Header(), 1) 213 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 214 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 215 | nf.WriteOptions(nf.HeaderPtr(), 323423432, 1213231) 216 | nf.SetStreamFlag(nf.Header()) 217 | 218 | // test options 219 | options := nf.ReadOptions(nf.Header()) 220 | assert.Equal(t, []uint32{323423432, 1213231}, options) 221 | // write payload 222 | nf.WritePayload([]byte(TestPayload)) 223 | nf.WriteCRC(nf.Header()) 224 | data := nf.Bytes() 225 | 226 | rf := ReadFrame(data) 227 | 228 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 229 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 230 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 231 | assert.Equal(t, true, rf.VerifyCRC(rf.Header())) 232 | assert.Equal(t, true, rf.IsStream(rf.Header())) 233 | assert.Equal(t, false, rf.IsStop(rf.Header())) 234 | } 235 | 236 | func TestFrame_Stream2(t *testing.T) { 237 | nf := NewFrame() 238 | nf.WriteVersion(nf.Header(), 1) 239 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 240 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 241 | nf.WriteOptions(nf.HeaderPtr(), 323423432, 1213231) 242 | 243 | // test options 244 | options := nf.ReadOptions(nf.Header()) 245 | assert.Equal(t, []uint32{323423432, 1213231}, options) 246 | // write payload 247 | nf.WritePayload([]byte(TestPayload)) 248 | nf.WriteCRC(nf.Header()) 249 | data := nf.Bytes() 250 | 251 | rf := ReadFrame(data) 252 | 253 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 254 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 255 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 256 | assert.Equal(t, true, rf.VerifyCRC(rf.Header())) 257 | assert.Equal(t, false, rf.IsStream(rf.Header())) 258 | } 259 | 260 | func BenchmarkLoops(b *testing.B) { 261 | nf := NewFrame() 262 | nf.WriteVersion(nf.Header(), 1) 263 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 264 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 265 | nf.WriteOptions(nf.HeaderPtr(), 323423432, 1213231, 123123123, 398797979, 323423432, 1213231, 123123123, 398797979, 123, 123) 266 | 267 | b.ReportAllocs() 268 | 269 | for b.Loop() { 270 | options := nf.ReadOptions(nf.Header()) 271 | _ = options 272 | } 273 | } 274 | 275 | func TestFrame_Bytes(t *testing.T) { 276 | nf := NewFrame() 277 | nf.WriteVersion(nf.Header(), 1) 278 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 279 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 280 | 281 | nf.WriteOptions(nf.HeaderPtr(), 323423432) 282 | assert.Equal(t, []uint32{323423432}, nf.ReadOptions(nf.Header())) 283 | nf.WritePayload([]byte(TestPayload)) 284 | 285 | nf.WriteCRC(nf.Header()) 286 | assert.Equal(t, true, nf.VerifyCRC(nf.Header())) 287 | data := nf.Bytes() 288 | 289 | rf := ReadFrame(data) 290 | 291 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 292 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 293 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 294 | assert.Equal(t, true, rf.VerifyCRC(rf.Header())) 295 | assert.Equal(t, []uint32{323423432}, rf.ReadOptions(rf.Header())) 296 | } 297 | 298 | func TestFrame_NotPingPong(t *testing.T) { 299 | nf := NewFrame() 300 | nf.WriteVersion(nf.Header(), 1) 301 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 302 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 303 | 304 | nf.WriteOptions(nf.HeaderPtr(), 323423432) 305 | assert.Equal(t, []uint32{323423432}, nf.ReadOptions(nf.Header())) 306 | nf.WritePayload([]byte(TestPayload)) 307 | 308 | nf.WriteCRC(nf.Header()) 309 | assert.Equal(t, true, nf.VerifyCRC(nf.Header())) 310 | data := nf.Bytes() 311 | 312 | rf := ReadFrame(data) 313 | 314 | assert.False(t, rf.IsPing(rf.Header())) 315 | assert.False(t, rf.IsPong(rf.Header())) 316 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 317 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 318 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 319 | assert.Equal(t, true, rf.VerifyCRC(rf.Header())) 320 | assert.Equal(t, []uint32{323423432}, rf.ReadOptions(rf.Header())) 321 | } 322 | 323 | func TestFrame_Ping(t *testing.T) { 324 | nf := NewFrame() 325 | nf.WriteVersion(nf.Header(), 1) 326 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 327 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 328 | 329 | nf.WriteOptions(nf.HeaderPtr(), 323423432) 330 | assert.Equal(t, []uint32{323423432}, nf.ReadOptions(nf.Header())) 331 | nf.WritePayload([]byte(TestPayload)) 332 | nf.SetPingBit(nf.Header()) 333 | 334 | nf.WriteCRC(nf.Header()) 335 | assert.Equal(t, true, nf.VerifyCRC(nf.Header())) 336 | data := nf.Bytes() 337 | 338 | rf := ReadFrame(data) 339 | 340 | assert.True(t, rf.IsPing(rf.Header())) 341 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 342 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 343 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 344 | assert.Equal(t, true, rf.VerifyCRC(rf.Header())) 345 | assert.Equal(t, []uint32{323423432}, rf.ReadOptions(rf.Header())) 346 | } 347 | 348 | func TestFrame_Pong(t *testing.T) { 349 | nf := NewFrame() 350 | nf.WriteVersion(nf.Header(), 1) 351 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 352 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 353 | 354 | nf.WriteOptions(nf.HeaderPtr(), 323423432) 355 | assert.Equal(t, []uint32{323423432}, nf.ReadOptions(nf.Header())) 356 | nf.WritePayload([]byte(TestPayload)) 357 | nf.SetPongBit(nf.Header()) 358 | 359 | nf.WriteCRC(nf.Header()) 360 | assert.Equal(t, true, nf.VerifyCRC(nf.Header())) 361 | data := nf.Bytes() 362 | 363 | rf := ReadFrame(data) 364 | 365 | assert.True(t, rf.IsPong(rf.Header())) 366 | assert.Equal(t, rf.ReadVersion(rf.Header()), nf.ReadVersion(nf.Header())) 367 | assert.Equal(t, rf.ReadFlags(), nf.ReadFlags()) 368 | assert.Equal(t, rf.ReadPayloadLen(rf.Header()), nf.ReadPayloadLen(nf.Header())) 369 | assert.Equal(t, true, rf.VerifyCRC(rf.Header())) 370 | assert.Equal(t, []uint32{323423432}, rf.ReadOptions(rf.Header())) 371 | } 372 | 373 | func BenchmarkCRC32(b *testing.B) { 374 | b.ReportAllocs() 375 | 376 | for b.Loop() { 377 | res := crc32.ChecksumIEEE([]byte{'t', 't', 'b', 'u', '6', '1', 'g', 'h', 'r', 't'}) 378 | _ = res 379 | } 380 | } 381 | 382 | func BenchmarkFrame_CRC(b *testing.B) { 383 | nf := NewFrame() 384 | nf.WriteVersion(nf.Header(), Version1) 385 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 386 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 387 | nf.WriteOptions(nf.HeaderPtr(), 1000, 1000, 1000, 1000, 1000, 1000) 388 | 389 | b.ReportAllocs() 390 | 391 | for b.Loop() { 392 | nf.WriteCRC(nf.Header()) 393 | if !nf.VerifyCRC(nf.Header()) { 394 | panic("CRC") 395 | } 396 | } 397 | } 398 | 399 | func BenchmarkFrame(b *testing.B) { 400 | 401 | b.ReportAllocs() 402 | 403 | for b.Loop() { 404 | nf := NewFrame() 405 | nf.WriteVersion(nf.Header(), Version1) 406 | nf.WriteFlags(nf.Header(), CONTROL, CodecGob) 407 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 408 | nf.WriteOptions(nf.HeaderPtr(), 1000, 1000, 1000, 1000, 1000, 1000) 409 | nf.WriteCRC(nf.Header()) 410 | 411 | if !nf.VerifyCRC(nf.Header()) { 412 | panic("CRC") 413 | } 414 | } 415 | } 416 | -------------------------------------------------------------------------------- /pkg/pipe/pipe.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/roadrunner-server/errors" 7 | "github.com/roadrunner-server/goridge/v3/internal" 8 | "github.com/roadrunner-server/goridge/v3/pkg/frame" 9 | ) 10 | 11 | // Relay ... PipeRelay communicate with underlying process using standard streams (STDIN, STDOUT). Attention, use TCP alternative for 12 | // Windows as more reliable option. This relay closes automatically with the process. 13 | type Relay struct { 14 | in io.ReadCloser 15 | out io.WriteCloser 16 | } 17 | 18 | // NewPipeRelay creates new pipe based data relay. 19 | func NewPipeRelay(in io.ReadCloser, out io.WriteCloser) *Relay { 20 | internal.Preallocate() 21 | return &Relay{in: in, out: out} 22 | } 23 | 24 | // Send signed (prefixed) data to underlying process. 25 | func (rl *Relay) Send(frame *frame.Frame) error { 26 | const op = errors.Op("pipes frame send") 27 | _, err := rl.out.Write(frame.Bytes()) 28 | if err != nil { 29 | return errors.E(op, err) 30 | } 31 | return nil 32 | } 33 | 34 | func (rl *Relay) Receive(frame *frame.Frame) error { 35 | if frame == nil { 36 | return errors.Str("nil frame") 37 | } 38 | return internal.ReceiveFrame(rl.in, frame) 39 | } 40 | 41 | // Close the connection 42 | func (rl *Relay) Close() error { 43 | _ = rl.out.Close() 44 | _ = rl.in.Close() 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /pkg/pipe/pipe_test.go: -------------------------------------------------------------------------------- 1 | package pipe 2 | 3 | import ( 4 | "io" 5 | "testing" 6 | 7 | "github.com/roadrunner-server/goridge/v3/pkg/frame" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | const TestPayload = `alsdjf;lskjdgljasg;lkjsalfkjaskldjflkasjdf;lkasjfdalksdjflkajsdf;lfasdgnslsnblna;sldjjfawlkejr;lwjenlksndlfjawl;ejr;lwjelkrjaldfjl;sdjf` 12 | 13 | func TestPipeReceive(t *testing.T) { 14 | pr, pw := io.Pipe() 15 | 16 | relay := NewPipeRelay(pr, pw) 17 | 18 | nf := frame.NewFrame() 19 | nf.WriteVersion(nf.Header(), frame.Version1) 20 | nf.WriteFlags(nf.Header(), frame.CONTROL, frame.CodecGob, frame.CodecJSON) 21 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 22 | nf.WritePayload([]byte(TestPayload)) 23 | nf.WriteCRC(nf.Header()) 24 | assert.Equal(t, true, nf.VerifyCRC(nf.Header())) 25 | 26 | go func(frame *frame.Frame) { 27 | defer func() { 28 | _ = pw.Close() 29 | }() 30 | err := relay.Send(nf) 31 | assert.NoError(t, err) 32 | _ = pw.Close() 33 | }(nf) 34 | 35 | fr := frame.NewFrame() 36 | err := relay.Receive(fr) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | 41 | assert.Equal(t, fr.ReadVersion(fr.Header()), nf.ReadVersion(nf.Header())) 42 | assert.Equal(t, fr.ReadFlags(), nf.ReadFlags()) 43 | assert.Equal(t, fr.ReadPayloadLen(fr.Header()), nf.ReadPayloadLen(nf.Header())) 44 | assert.Equal(t, true, fr.VerifyCRC(nf.Header())) 45 | assert.Equal(t, []byte(TestPayload), fr.Payload()) 46 | } 47 | 48 | func TestPipeReceiveWithOptions(t *testing.T) { 49 | pr, pw := io.Pipe() 50 | 51 | relay := NewPipeRelay(pr, pw) 52 | 53 | nf := frame.NewFrame() 54 | nf.WriteVersion(nf.Header(), frame.Version1) 55 | nf.WriteFlags(nf.Header(), frame.CONTROL, frame.CodecGob, frame.CodecJSON) 56 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 57 | nf.WritePayload([]byte(TestPayload)) 58 | nf.WriteOptions(nf.HeaderPtr(), 100, 10000, 100000) 59 | nf.WriteCRC(nf.Header()) 60 | assert.Equal(t, true, nf.VerifyCRC(nf.Header())) 61 | 62 | go func(frame *frame.Frame) { 63 | defer func() { 64 | _ = pw.Close() 65 | }() 66 | err := relay.Send(nf) 67 | assert.NoError(t, err) 68 | _ = pw.Close() 69 | }(nf) 70 | 71 | fr := frame.NewFrame() 72 | err := relay.Receive(fr) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | assert.Equal(t, fr.ReadVersion(fr.Header()), nf.ReadVersion(nf.Header())) 78 | assert.Equal(t, fr.ReadFlags(), nf.ReadFlags()) 79 | assert.Equal(t, fr.ReadPayloadLen(fr.Header()), nf.ReadPayloadLen(nf.Header())) 80 | assert.Equal(t, true, fr.VerifyCRC(fr.Header())) 81 | assert.Equal(t, []byte(TestPayload), fr.Payload()) 82 | assert.Equal(t, []uint32{100, 10000, 100000}, fr.ReadOptions(fr.Header())) 83 | } 84 | 85 | func TestPipeCRC_Failed(t *testing.T) { 86 | pr, pw := io.Pipe() 87 | 88 | relay := NewPipeRelay(pr, pw) 89 | 90 | nf := frame.NewFrame() 91 | nf.WriteVersion(nf.Header(), frame.Version1) 92 | nf.WriteFlags(nf.Header(), frame.CONTROL) 93 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) 94 | 95 | assert.Equal(t, false, nf.VerifyCRC(nf.Header())) 96 | 97 | nf.WritePayload([]byte(TestPayload)) 98 | 99 | go func(frame *frame.Frame) { 100 | defer func() { 101 | _ = pw.Close() 102 | }() 103 | err := relay.Send(nf) 104 | assert.NoError(t, err) 105 | _ = pw.Close() 106 | }(nf) 107 | 108 | fr := frame.NewFrame() 109 | err := relay.Receive(fr) 110 | assert.Error(t, err) 111 | assert.False(t, fr.VerifyCRC(fr.Header())) 112 | 113 | assert.Empty(t, fr.Payload()) 114 | } 115 | -------------------------------------------------------------------------------- /pkg/relay/interface.go: -------------------------------------------------------------------------------- 1 | package relay 2 | 3 | import "github.com/roadrunner-server/goridge/v3/pkg/frame" 4 | 5 | // Relay provide IPC over signed payloads. 6 | type Relay interface { 7 | // Send signed (prefixed) data to PHP process. 8 | Send(frame *frame.Frame) error 9 | 10 | // Receive data from the underlying process and returns associated prefix or error. 11 | Receive(frame *frame.Frame) error 12 | 13 | // Close the connection. 14 | Close() error 15 | } 16 | -------------------------------------------------------------------------------- /pkg/rpc/client.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | "io" 7 | "net/rpc" 8 | "sync" 9 | 10 | "github.com/goccy/go-json" 11 | "github.com/roadrunner-server/errors" 12 | "github.com/roadrunner-server/goridge/v3/pkg/frame" 13 | "github.com/roadrunner-server/goridge/v3/pkg/relay" 14 | "github.com/roadrunner-server/goridge/v3/pkg/socket" 15 | "github.com/vmihailenco/msgpack/v5" 16 | "google.golang.org/protobuf/proto" 17 | ) 18 | 19 | // ClientCodec is codec for goridge connection. 20 | type ClientCodec struct { 21 | // bytes sync.Pool 22 | bPool sync.Pool 23 | fPool sync.Pool 24 | 25 | relay relay.Relay 26 | closed bool 27 | frame *frame.Frame 28 | } 29 | 30 | // NewClientCodec initiates new server rpc codec over socket connection. 31 | func NewClientCodec(rwc io.ReadWriteCloser) *ClientCodec { 32 | return &ClientCodec{ 33 | bPool: sync.Pool{New: func() any { 34 | return new(bytes.Buffer) 35 | }}, 36 | 37 | fPool: sync.Pool{New: func() any { 38 | return frame.NewFrame() 39 | }}, 40 | 41 | relay: socket.NewSocketRelay(rwc), 42 | } 43 | } 44 | 45 | func (c *ClientCodec) get() *bytes.Buffer { 46 | return c.bPool.Get().(*bytes.Buffer) 47 | } 48 | 49 | func (c *ClientCodec) put(b *bytes.Buffer) { 50 | b.Reset() 51 | c.bPool.Put(b) 52 | } 53 | 54 | func (c *ClientCodec) getFrame() *frame.Frame { 55 | return c.fPool.Get().(*frame.Frame) 56 | } 57 | 58 | func (c *ClientCodec) putFrame(f *frame.Frame) { 59 | f.Reset() 60 | c.fPool.Put(f) 61 | } 62 | 63 | // WriteRequest writes request to the connection. Sequential. 64 | func (c *ClientCodec) WriteRequest(r *rpc.Request, body any) error { 65 | const op = errors.Op("goridge_write_request") 66 | 67 | // get a frame from the pool 68 | fr := c.getFrame() 69 | defer c.putFrame(fr) 70 | 71 | // get a buffer from the pool 72 | buf := c.get() 73 | defer c.put(buf) 74 | 75 | // writeServiceMethod to the buffer 76 | buf.WriteString(r.ServiceMethod) 77 | // use fallback as gob 78 | fr.WriteFlags(fr.Header(), frame.CodecGob) 79 | 80 | if body != nil { 81 | // if body is proto message, use proto codec 82 | switch m := body.(type) { 83 | // check if message is PROTO 84 | case proto.Message: 85 | fr.WriteFlags(fr.Header(), frame.CodecProto) 86 | b, err := proto.Marshal(m) 87 | if err != nil { 88 | return errors.E(op, err) 89 | } 90 | buf.Write(b) 91 | default: 92 | enc := gob.NewEncoder(buf) 93 | // write data to the gob 94 | err := enc.Encode(body) 95 | if err != nil { 96 | return errors.E(op, err) 97 | } 98 | } 99 | } 100 | 101 | // SEQ_ID + METHOD_NAME_LEN 102 | fr.WriteOptions(fr.HeaderPtr(), uint32(r.Seq), uint32(len(r.ServiceMethod))) //nolint:gosec 103 | fr.WriteVersion(fr.Header(), frame.Version1) 104 | 105 | fr.WritePayloadLen(fr.Header(), uint32(buf.Len())) //nolint:gosec 106 | fr.WritePayload(buf.Bytes()) 107 | fr.WriteCRC(fr.Header()) 108 | 109 | err := c.relay.Send(fr) 110 | if err != nil { 111 | return errors.E(op, err) 112 | } 113 | return nil 114 | } 115 | 116 | // ReadResponseHeader reads response from the connection. 117 | func (c *ClientCodec) ReadResponseHeader(r *rpc.Response) error { 118 | const op = errors.Op("client_read_response_header") 119 | 120 | // get a frame from sync.Pool 121 | fr := c.getFrame() 122 | 123 | err := c.relay.Receive(fr) 124 | if err != nil { 125 | return errors.E(op, err) 126 | } 127 | if !fr.VerifyCRC(fr.Header()) { 128 | return errors.E(op, errors.Str("CRC verification failed")) 129 | } 130 | 131 | // save the frame after CRC verification 132 | c.frame = fr 133 | 134 | opts := fr.ReadOptions(fr.Header()) 135 | if len(opts) != 2 { 136 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 137 | } 138 | 139 | // check for error 140 | if fr.ReadFlags()&frame.ERROR != 0 { 141 | r.Error = string(fr.Payload()[opts[1]:]) 142 | } 143 | 144 | r.Seq = uint64(opts[0]) 145 | r.ServiceMethod = string(fr.Payload()[:opts[1]]) 146 | 147 | return nil 148 | } 149 | 150 | // ReadResponseBody response from the connection. 151 | func (c *ClientCodec) ReadResponseBody(out any) error { 152 | const op = errors.Op("client_read_response_body") 153 | 154 | // put frame after response was sent 155 | defer c.putFrame(c.frame) 156 | // if there is no out interface to unmarshall the body, skip 157 | if out == nil { 158 | return nil 159 | } 160 | 161 | flags := c.frame.ReadFlags() 162 | 163 | switch { //nolint:dupl 164 | case flags&frame.CodecProto != 0: 165 | opts := c.frame.ReadOptions(c.frame.Header()) 166 | if len(opts) != 2 { 167 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 168 | } 169 | payload := c.frame.Payload()[opts[1]:] 170 | if len(payload) == 0 { 171 | return nil 172 | } 173 | 174 | // check if the out message is a correct proto.Message 175 | // instead send an error 176 | if pOut, ok := out.(proto.Message); ok { 177 | err := proto.Unmarshal(payload, pOut) 178 | if err != nil { 179 | return errors.E(op, err) 180 | } 181 | return nil 182 | } 183 | 184 | return errors.E(op, errors.Str("message type is not a proto")) 185 | case flags&frame.CodecJSON != 0: 186 | opts := c.frame.ReadOptions(c.frame.Header()) 187 | if len(opts) != 2 { 188 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 189 | } 190 | payload := c.frame.Payload()[opts[1]:] 191 | if len(payload) == 0 { 192 | return nil 193 | } 194 | return json.Unmarshal(payload, out) 195 | case flags&frame.CodecGob != 0: 196 | opts := c.frame.ReadOptions(c.frame.Header()) 197 | if len(opts) != 2 { 198 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 199 | } 200 | payload := c.frame.Payload()[opts[1]:] 201 | if len(payload) == 0 { 202 | return nil 203 | } 204 | 205 | buf := c.get() 206 | defer c.put(buf) 207 | 208 | dec := gob.NewDecoder(buf) 209 | buf.Write(payload) 210 | 211 | err := dec.Decode(out) 212 | if err != nil { 213 | return errors.E(op, err) 214 | } 215 | 216 | return nil 217 | case flags&frame.CodecRaw != 0: 218 | opts := c.frame.ReadOptions(c.frame.Header()) 219 | if len(opts) != 2 { 220 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 221 | } 222 | payload := c.frame.Payload()[opts[1]:] 223 | if len(payload) == 0 { 224 | return nil 225 | } 226 | 227 | if raw, ok := out.(*[]byte); ok { 228 | *raw = append(*raw, payload...) 229 | } 230 | 231 | return nil 232 | case flags&frame.CodecMsgpack != 0: 233 | opts := c.frame.ReadOptions(c.frame.Header()) 234 | if len(opts) != 2 { 235 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 236 | } 237 | payload := c.frame.Payload()[opts[1]:] 238 | if len(payload) == 0 { 239 | return nil 240 | } 241 | 242 | return msgpack.Unmarshal(payload, out) 243 | default: 244 | return errors.E(op, errors.Str("unknown decoder used in frame")) 245 | } 246 | } 247 | 248 | // Close closes the client connection. 249 | func (c *ClientCodec) Close() error { 250 | if c.closed { 251 | return nil 252 | } 253 | 254 | c.closed = true 255 | return c.relay.Close() 256 | } 257 | -------------------------------------------------------------------------------- /pkg/rpc/client_server_test.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "crypto/rand" 5 | "net" 6 | "net/rpc" 7 | "strings" 8 | "sync" 9 | "testing" 10 | 11 | "github.com/roadrunner-server/errors" 12 | "github.com/roadrunner-server/goridge/v3/tests" 13 | "github.com/stretchr/testify/assert" 14 | "github.com/stretchr/testify/require" 15 | "google.golang.org/protobuf/proto" 16 | ) 17 | 18 | // testService sample 19 | type testService struct{} 20 | 21 | // Payload sample 22 | type Payload struct { 23 | Name string `json:"name"` 24 | Value int `json:"value"` 25 | Keys map[string]string `json:"keys,omitempty"` 26 | } 27 | 28 | // Echo returns incoming message 29 | func (s *testService) Echo(msg string, r *string) error { 30 | *r = msg 31 | return nil 32 | } 33 | 34 | // Echo returns error 35 | func (s *testService) EchoR(_ string, r *string) error { 36 | *r = "error" 37 | return errors.Str("echoR error") 38 | } 39 | 40 | // Process performs payload conversion 41 | func (s *testService) Process(msg Payload, r *Payload) error { 42 | r.Name = strings.ToUpper(msg.Name) 43 | r.Value = -msg.Value 44 | 45 | if len(msg.Keys) != 0 { 46 | r.Keys = make(map[string]string) 47 | for n, v := range msg.Keys { 48 | r.Keys[v] = n 49 | } 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // EchoBinary work over binary data 56 | func (s *testService) EchoBinary(msg []byte, out *[]byte) error { 57 | *out = append(*out, msg...) 58 | return nil 59 | } 60 | 61 | // Test Proto 62 | func (s *testService) ProtoMessage(payload *tests.Payload, item *tests.Item) error { 63 | (*item).Key = payload.Items[0].Key 64 | return nil 65 | } 66 | 67 | func TestClientServerProto(t *testing.T) { 68 | ln, err := net.Listen("tcp", "127.0.0.1:18935") 69 | assert.NoError(t, err) 70 | 71 | go func() { 72 | for { 73 | conn, err2 := ln.Accept() 74 | assert.NoError(t, err2) 75 | rpc.ServeCodec(NewCodec(conn)) 76 | } 77 | }() 78 | 79 | err = rpc.RegisterName("test123", new(testService)) 80 | assert.NoError(t, err) 81 | 82 | conn, err := net.Dial("tcp", "127.0.0.1:18935") 83 | assert.NoError(t, err) 84 | 85 | client := rpc.NewClientWithCodec(NewClientCodec(conn)) 86 | keysP := &tests.Payload{ 87 | Storage: "memory-rr", 88 | Items: []*tests.Item{ 89 | { 90 | Key: "a", 91 | }, 92 | { 93 | Key: "b", 94 | }, 95 | { 96 | Key: "c", 97 | }, 98 | }, 99 | } 100 | 101 | item := &tests.Item{} 102 | assert.NoError(t, client.Call("test123.ProtoMessage", keysP, item)) 103 | assert.Equal(t, "a", item.Key) 104 | 105 | t.Cleanup(func() { 106 | err2 := client.Close() 107 | if err2 != nil { 108 | t.Fatal(err2) 109 | } 110 | }) 111 | } 112 | 113 | func TestClientServerProtoError(t *testing.T) { 114 | ln, err := net.Listen("tcp", "127.0.0.1:18321") 115 | assert.NoError(t, err) 116 | 117 | go func() { 118 | for { 119 | conn, err2 := ln.Accept() 120 | assert.NoError(t, err2) 121 | rpc.ServeCodec(NewCodec(conn)) 122 | } 123 | }() 124 | 125 | err = rpc.RegisterName("testError", new(testService)) 126 | assert.NoError(t, err) 127 | 128 | conn, err := net.Dial("tcp", "127.0.0.1:18321") 129 | assert.NoError(t, err) 130 | 131 | client := rpc.NewClientWithCodec(NewClientCodec(conn)) 132 | keysP := &tests.Payload{ 133 | Storage: "memory-rr", 134 | Items: []*tests.Item{ 135 | { 136 | Key: "a", 137 | }, 138 | { 139 | Key: "b", 140 | }, 141 | { 142 | Key: "c", 143 | }, 144 | }, 145 | } 146 | 147 | keys, err := proto.Marshal(keysP) 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | 152 | item := &tests.Item{} 153 | assert.Error(t, client.Call("testError.ProtoMessage", keys, item)) 154 | 155 | t.Cleanup(func() { 156 | err2 := client.Close() 157 | if err2 != nil { 158 | t.Fatal(err2) 159 | } 160 | }) 161 | } 162 | 163 | func TestClientServerJSON(t *testing.T) { 164 | ln, err := net.Listen("tcp", "127.0.0.1:18936") 165 | assert.NoError(t, err) 166 | 167 | go func() { 168 | for { 169 | conn, err2 := ln.Accept() 170 | assert.NoError(t, err2) 171 | rpc.ServeCodec(NewCodec(conn)) 172 | } 173 | }() 174 | 175 | err = rpc.RegisterName("test2", new(testService)) 176 | assert.NoError(t, err) 177 | 178 | conn, err := net.Dial("tcp", "127.0.0.1:18936") 179 | assert.NoError(t, err) 180 | 181 | client := rpc.NewClientWithCodec(NewClientCodec(conn)) 182 | 183 | var rp = Payload{} 184 | assert.NoError(t, client.Call("test2.Process", Payload{ 185 | Name: "name", 186 | Value: 1000, 187 | Keys: map[string]string{"key": "value"}, 188 | }, &rp)) 189 | 190 | assert.Equal(t, "NAME", rp.Name) 191 | assert.Equal(t, -1000, rp.Value) 192 | assert.Equal(t, "key", rp.Keys["value"]) 193 | 194 | t.Cleanup(func() { 195 | err2 := client.Close() 196 | if err2 != nil { 197 | t.Fatal(err2) 198 | } 199 | }) 200 | } 201 | 202 | func TestClientServerRaw(t *testing.T) { 203 | ln, err := net.Listen("tcp", "127.0.0.1:18937") 204 | assert.NoError(t, err) 205 | 206 | go func() { 207 | for { 208 | conn, err2 := ln.Accept() 209 | assert.NoError(t, err2) 210 | rpc.ServeCodec(NewCodec(conn)) 211 | } 212 | }() 213 | 214 | err = rpc.RegisterName("testBinary", new(testService)) 215 | assert.NoError(t, err) 216 | 217 | conn, err := net.Dial("tcp", "127.0.0.1:18937") 218 | assert.NoError(t, err) 219 | 220 | client := rpc.NewClientWithCodec(NewClientCodec(conn)) 221 | 222 | data := make([]byte, 100000) 223 | _, _ = rand.Read(data) 224 | 225 | resp := make([]byte, 0, 10000) 226 | assert.NoError(t, client.Call("testBinary.EchoBinary", data, &resp)) 227 | require.Equal(t, data, resp) 228 | 229 | t.Cleanup(func() { 230 | err2 := client.Close() 231 | if err2 != nil { 232 | t.Fatal(err2) 233 | } 234 | }) 235 | } 236 | 237 | func TestClientServerError(t *testing.T) { 238 | ln, err := net.Listen("tcp", "127.0.0.1:12336") 239 | assert.NoError(t, err) 240 | 241 | go func() { 242 | for { 243 | conn, err2 := ln.Accept() 244 | assert.NoError(t, err2) 245 | rpc.ServeCodec(NewCodec(conn)) 246 | } 247 | }() 248 | 249 | err = rpc.RegisterName("testError2", new(testService)) 250 | assert.NoError(t, err) 251 | 252 | conn, err := net.Dial("tcp", "127.0.0.1:12336") 253 | assert.NoError(t, err) 254 | 255 | client := rpc.NewClientWithCodec(NewClientCodec(conn)) 256 | 257 | err = client.Call("unknown", nil, nil) 258 | assert.Error(t, err) 259 | assert.Equal(t, "rpc: service/method request ill-formed: unknown", err.Error()) 260 | 261 | t.Cleanup(func() { 262 | err2 := client.Close() 263 | if err2 != nil { 264 | t.Fatal(err2) 265 | } 266 | }) 267 | } 268 | 269 | func TestClientServerConcurrent(t *testing.T) { 270 | ln, err := net.Listen("tcp", "127.0.0.1:22385") 271 | if err != nil { 272 | panic(err) 273 | } 274 | 275 | go func() { 276 | for { 277 | conn, err2 := ln.Accept() 278 | assert.NoError(t, err2) 279 | rpc.ServeCodec(NewCodec(conn)) 280 | } 281 | }() 282 | 283 | err = rpc.RegisterName("test", new(testService)) 284 | assert.NoError(t, err) 285 | 286 | conn, err := net.Dial("tcp", "127.0.0.1:22385") 287 | assert.NoError(t, err) 288 | 289 | client := rpc.NewClientWithCodec(NewClientCodec(conn)) 290 | 291 | wg := &sync.WaitGroup{} 292 | wg.Add(300) 293 | 294 | // this test uses random inputs 295 | for range 100 { 296 | go func() { 297 | defer wg.Done() 298 | var rp = Payload{} 299 | b := make([]byte, 15) 300 | _, err := rand.Read(b) 301 | assert.NoError(t, err) 302 | 303 | <-client.Go("test.Process", Payload{ 304 | Name: string(b), 305 | Value: 1000, 306 | Keys: map[string]string{"key": string(b)}, 307 | }, &rp, nil).Done 308 | 309 | assert.Equal(t, strings.ToUpper(string(b)), rp.Name) 310 | assert.Equal(t, -1000, rp.Value) 311 | assert.Equal(t, "key", rp.Keys[string(b)]) 312 | }() 313 | 314 | go func() { 315 | var rs = "" 316 | b := make([]byte, 15) 317 | _, err := rand.Read(b) 318 | assert.NoError(t, err) 319 | <-client.Go("test.Echo", string(b), &rs, nil).Done 320 | assert.Equal(t, string(b), rs) 321 | wg.Done() 322 | }() 323 | 324 | go func() { 325 | rs := "" 326 | rb := make([]byte, 0) 327 | 328 | r := make([]byte, 15) 329 | _, err := rand.Read(r) 330 | assert.NoError(t, err) 331 | a := client.Go("test.Echo", string(r), &rs, nil) 332 | b := client.Go("test.EchoBinary", []byte("hello world"), &rb, nil) 333 | c := client.Go("test.EchoR", "hi", &rs, nil) 334 | 335 | <-a.Done 336 | assert.Equal(t, string(r), rs) 337 | <-b.Done 338 | assert.Equal(t, []byte("hello world"), rb) 339 | resC := <-c.Done 340 | assert.Error(t, resC.Error) 341 | wg.Done() 342 | }() 343 | } 344 | 345 | wg.Wait() 346 | 347 | wg2 := &sync.WaitGroup{} 348 | wg2.Add(300) 349 | 350 | for range 100 { 351 | go func() { 352 | defer wg2.Done() 353 | var rp = Payload{} 354 | b := make([]byte, 15) 355 | _, err := rand.Read(b) 356 | assert.NoError(t, err) 357 | 358 | assert.NoError(t, client.Call("test.Process", Payload{ 359 | Name: string(b), 360 | Value: 1000, 361 | Keys: map[string]string{"key": string(b)}, 362 | }, &rp)) 363 | 364 | assert.Equal(t, strings.ToUpper(string(b)), rp.Name) 365 | assert.Equal(t, -1000, rp.Value) 366 | assert.Equal(t, "key", rp.Keys[string(b)]) 367 | }() 368 | 369 | go func() { 370 | defer wg2.Done() 371 | var rs = "" 372 | r := make([]byte, 15) 373 | _, err := rand.Read(r) 374 | assert.NoError(t, err) 375 | 376 | assert.NoError(t, client.Call("test.Echo", string(r), &rs)) 377 | assert.Equal(t, string(r), rs) 378 | }() 379 | 380 | go func() { 381 | defer wg2.Done() 382 | rs := "" 383 | rb := make([]byte, 0, len("hello world")) 384 | 385 | r := make([]byte, 15) 386 | _, err := rand.Read(r) 387 | assert.NoError(t, err) 388 | 389 | assert.NoError(t, client.Call("test.Echo", string(r), &rs)) 390 | assert.Equal(t, string(r), rs) 391 | 392 | assert.NoError(t, client.Call("test.EchoBinary", r, &rb)) 393 | assert.Equal(t, r, rb) 394 | 395 | assert.Error(t, client.Call("test.EchoR", "hi", &rs)) 396 | }() 397 | } 398 | 399 | wg2.Wait() 400 | 401 | t.Cleanup(func() { 402 | err2 := client.Close() 403 | if err2 != nil { 404 | t.Fatal(err2) 405 | } 406 | }) 407 | } 408 | -------------------------------------------------------------------------------- /pkg/rpc/codec.go: -------------------------------------------------------------------------------- 1 | package rpc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/gob" 6 | stderr "errors" 7 | "io" 8 | "net/rpc" 9 | "sync" 10 | 11 | "github.com/goccy/go-json" 12 | "github.com/roadrunner-server/errors" 13 | "github.com/roadrunner-server/goridge/v3/pkg/frame" 14 | "github.com/roadrunner-server/goridge/v3/pkg/relay" 15 | "github.com/roadrunner-server/goridge/v3/pkg/socket" 16 | "github.com/vmihailenco/msgpack/v5" 17 | "google.golang.org/protobuf/proto" 18 | ) 19 | 20 | // Codec represent net/rpc bridge over Goridge socket relay. 21 | type Codec struct { 22 | relay relay.Relay 23 | closed bool 24 | frame *frame.Frame 25 | codec sync.Map 26 | 27 | bPool sync.Pool 28 | fPool sync.Pool 29 | } 30 | 31 | // NewCodec initiates new server rpc codec over socket connection. 32 | func NewCodec(rwc io.ReadWriteCloser) *Codec { 33 | return &Codec{ 34 | relay: socket.NewSocketRelay(rwc), 35 | codec: sync.Map{}, 36 | 37 | bPool: sync.Pool{New: func() any { 38 | return new(bytes.Buffer) 39 | }}, 40 | 41 | fPool: sync.Pool{New: func() any { 42 | return frame.NewFrame() 43 | }}, 44 | } 45 | } 46 | 47 | // NewCodecWithRelay initiates new server rpc codec with a relay of choice. 48 | func NewCodecWithRelay(relay relay.Relay) *Codec { 49 | return &Codec{relay: relay} 50 | } 51 | 52 | func (c *Codec) get() *bytes.Buffer { 53 | return c.bPool.Get().(*bytes.Buffer) 54 | } 55 | 56 | func (c *Codec) put(b *bytes.Buffer) { 57 | b.Reset() 58 | c.bPool.Put(b) 59 | } 60 | 61 | func (c *Codec) getFrame() *frame.Frame { 62 | return c.fPool.Get().(*frame.Frame) 63 | } 64 | 65 | func (c *Codec) putFrame(f *frame.Frame) { 66 | f.Reset() 67 | c.fPool.Put(f) 68 | } 69 | 70 | // WriteResponse marshals response, byte slice or error to remote. 71 | func (c *Codec) WriteResponse(r *rpc.Response, body any) error { //nolint:funlen 72 | const op = errors.Op("goridge_write_response") 73 | fr := c.getFrame() 74 | defer c.putFrame(fr) 75 | 76 | // SEQ_ID + METHOD_NAME_LEN 77 | fr.WriteOptions(fr.HeaderPtr(), uint32(r.Seq), uint32(len(r.ServiceMethod))) //nolint:gosec 78 | // Write protocol version 79 | fr.WriteVersion(fr.Header(), frame.Version1) 80 | 81 | // load and delete associated codec to not waste memory 82 | // because we write it to the fr and don't need more information about it 83 | codec, ok := c.codec.LoadAndDelete(r.Seq) 84 | if !ok { 85 | // fallback codec 86 | fr.WriteFlags(fr.Header(), frame.CodecGob) 87 | } else { 88 | fr.WriteFlags(fr.Header(), codec.(byte)) 89 | } 90 | 91 | // if error returned, we sending it via relay and return error from WriteResponse 92 | if r.Error != "" { 93 | // Append error flag 94 | return c.handleError(r, fr, r.Error) 95 | } 96 | 97 | switch { 98 | case codec.(byte)&frame.CodecProto != 0: 99 | d, err := proto.Marshal(body.(proto.Message)) 100 | if err != nil { 101 | return c.handleError(r, fr, err.Error()) 102 | } 103 | 104 | // initialize buffer 105 | buf := c.get() 106 | defer c.put(buf) 107 | 108 | buf.Grow(len(d) + len(r.ServiceMethod)) 109 | // writeServiceMethod to the buffer 110 | buf.WriteString(r.ServiceMethod) 111 | buf.Write(d) 112 | 113 | fr.WritePayloadLen(fr.Header(), uint32(buf.Len())) //nolint:gosec 114 | // copy inside 115 | fr.WritePayload(buf.Bytes()) 116 | fr.WriteCRC(fr.Header()) 117 | // send buffer 118 | return c.relay.Send(fr) 119 | case codec.(byte)&frame.CodecRaw != 0: 120 | // initialize buffer 121 | buf := c.get() 122 | defer c.put(buf) 123 | 124 | switch data := body.(type) { 125 | case []byte: 126 | buf.Grow(len(data) + len(r.ServiceMethod)) 127 | // writeServiceMethod to the buffer 128 | buf.WriteString(r.ServiceMethod) 129 | buf.Write(data) 130 | 131 | fr.WritePayloadLen(fr.Header(), uint32(buf.Len())) //nolint:gosec 132 | fr.WritePayload(buf.Bytes()) 133 | case *[]byte: 134 | buf.Grow(len(*data) + len(r.ServiceMethod)) 135 | // writeServiceMethod to the buffer 136 | buf.WriteString(r.ServiceMethod) 137 | buf.Write(*data) 138 | 139 | fr.WritePayloadLen(fr.Header(), uint32(buf.Len())) //nolint:gosec 140 | fr.WritePayload(buf.Bytes()) 141 | default: 142 | return c.handleError(r, fr, "unknown Raw payload type") 143 | } 144 | 145 | // send buffer 146 | fr.WriteCRC(fr.Header()) 147 | return c.relay.Send(fr) 148 | 149 | case codec.(byte)&frame.CodecJSON != 0: 150 | data, err := json.Marshal(body) 151 | if err != nil { 152 | return c.handleError(r, fr, err.Error()) 153 | } 154 | 155 | // initialize buffer 156 | buf := c.get() 157 | defer c.put(buf) 158 | 159 | buf.Grow(len(data) + len(r.ServiceMethod)) 160 | // writeServiceMethod to the buffer 161 | buf.WriteString(r.ServiceMethod) 162 | buf.Write(data) 163 | 164 | fr.WritePayloadLen(fr.Header(), uint32(buf.Len())) //nolint:gosec 165 | // copy inside 166 | fr.WritePayload(buf.Bytes()) 167 | fr.WriteCRC(fr.Header()) 168 | // send buffer 169 | return c.relay.Send(fr) 170 | 171 | case codec.(byte)&frame.CodecMsgpack != 0: 172 | b, err := msgpack.Marshal(body) 173 | if err != nil { 174 | return errors.E(op, err) 175 | } 176 | // initialize buffer 177 | buf := c.get() 178 | defer c.put(buf) 179 | 180 | buf.Grow(len(b) + len(r.ServiceMethod)) 181 | // writeServiceMethod to the buffer 182 | buf.WriteString(r.ServiceMethod) 183 | buf.Write(b) 184 | 185 | fr.WritePayloadLen(fr.Header(), uint32(buf.Len())) //nolint:gosec 186 | // copy inside 187 | fr.WritePayload(buf.Bytes()) 188 | fr.WriteCRC(fr.Header()) 189 | // send buffer 190 | return c.relay.Send(fr) 191 | 192 | case codec.(byte)&frame.CodecGob != 0: 193 | // initialize buffer 194 | buf := c.get() 195 | defer c.put(buf) 196 | 197 | buf.WriteString(r.ServiceMethod) 198 | 199 | dec := gob.NewEncoder(buf) 200 | err := dec.Encode(body) 201 | if err != nil { 202 | return errors.E(op, err) 203 | } 204 | 205 | fr.WritePayloadLen(fr.Header(), uint32(buf.Len())) //nolint:gosec 206 | // copy inside 207 | fr.WritePayload(buf.Bytes()) 208 | fr.WriteCRC(fr.Header()) 209 | // send buffer 210 | return c.relay.Send(fr) 211 | default: 212 | return c.handleError(r, fr, errors.E(op, errors.Str("unknown codec")).Error()) 213 | } 214 | } 215 | 216 | func (c *Codec) handleError(r *rpc.Response, fr *frame.Frame, err string) error { 217 | buf := c.get() 218 | defer c.put(buf) 219 | 220 | // write all possible errors 221 | buf.WriteString(r.ServiceMethod) 222 | 223 | const op = errors.Op("handle codec error") 224 | fr.WriteFlags(fr.Header(), frame.ERROR) 225 | // error should be here 226 | if err != "" { 227 | buf.WriteString(err) 228 | } 229 | fr.WritePayloadLen(fr.Header(), uint32(buf.Len())) //nolint:gosec 230 | fr.WritePayload(buf.Bytes()) 231 | 232 | fr.WriteCRC(fr.Header()) 233 | _ = c.relay.Send(fr) 234 | return errors.E(op, errors.Str(r.Error)) 235 | } 236 | 237 | // ReadRequestHeader receives frame with options 238 | // options should have 2 values 239 | // [0] - integer, sequence ID 240 | // [1] - integer, offset for method name 241 | // For example: 242 | // 15Test.Payload 243 | // SEQ_ID: 15 244 | // METHOD_LEN: 12 and we take 12 bytes from the payload as method name 245 | func (c *Codec) ReadRequestHeader(r *rpc.Request) error { 246 | const op = errors.Op("goridge_read_request_header") 247 | f := c.getFrame() 248 | 249 | err := c.relay.Receive(f) 250 | if err != nil { 251 | if stderr.Is(err, io.EOF) { 252 | c.putFrame(f) 253 | return err 254 | } 255 | 256 | c.putFrame(f) 257 | return err 258 | } 259 | 260 | // opts[0] sequence ID 261 | // opts[1] service method name offset from payload in bytes 262 | opts := f.ReadOptions(f.Header()) 263 | if len(opts) != 2 { 264 | c.putFrame(f) 265 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 266 | } 267 | 268 | r.Seq = uint64(opts[0]) 269 | r.ServiceMethod = string(f.Payload()[:opts[1]]) 270 | c.frame = f 271 | return c.storeCodec(r, f.ReadFlags()) 272 | } 273 | 274 | func (c *Codec) storeCodec(r *rpc.Request, flag byte) error { 275 | switch { 276 | case flag&frame.CodecProto != 0: 277 | c.codec.Store(r.Seq, frame.CodecProto) 278 | case flag&frame.CodecJSON != 0: 279 | c.codec.Store(r.Seq, frame.CodecJSON) 280 | case flag&frame.CodecRaw != 0: 281 | c.codec.Store(r.Seq, frame.CodecRaw) 282 | case flag&frame.CodecMsgpack != 0: 283 | c.codec.Store(r.Seq, frame.CodecMsgpack) 284 | case flag&frame.CodecGob != 0: 285 | c.codec.Store(r.Seq, frame.CodecGob) 286 | default: 287 | c.codec.Store(r.Seq, frame.CodecGob) 288 | } 289 | 290 | return nil 291 | } 292 | 293 | // ReadRequestBody fetches prefixed body data and automatically unmarshal it as json. RawBody flag will populate 294 | // []byte lice argument for rpc method. 295 | func (c *Codec) ReadRequestBody(out any) error { 296 | const op = errors.Op("goridge_read_request_body") 297 | if out == nil { 298 | return nil 299 | } 300 | 301 | defer c.putFrame(c.frame) 302 | 303 | flags := c.frame.ReadFlags() 304 | 305 | switch { //nolint:dupl 306 | case flags&frame.CodecProto != 0: 307 | opts := c.frame.ReadOptions(c.frame.Header()) 308 | if len(opts) != 2 { 309 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 310 | } 311 | payload := c.frame.Payload()[opts[1]:] 312 | if len(payload) == 0 { 313 | return nil 314 | } 315 | 316 | // check if the out message is a correct proto.Message 317 | // instead send an error 318 | if pOut, ok := out.(proto.Message); ok { 319 | err := proto.Unmarshal(payload, pOut) 320 | if err != nil { 321 | return errors.E(op, err) 322 | } 323 | return nil 324 | } 325 | 326 | return errors.E(op, errors.Str("message type is not a proto")) 327 | case flags&frame.CodecJSON != 0: 328 | opts := c.frame.ReadOptions(c.frame.Header()) 329 | if len(opts) != 2 { 330 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 331 | } 332 | payload := c.frame.Payload()[opts[1]:] 333 | if len(payload) == 0 { 334 | return nil 335 | } 336 | return json.Unmarshal(payload, out) 337 | case flags&frame.CodecGob != 0: 338 | opts := c.frame.ReadOptions(c.frame.Header()) 339 | if len(opts) != 2 { 340 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 341 | } 342 | payload := c.frame.Payload()[opts[1]:] 343 | if len(payload) == 0 { 344 | return nil 345 | } 346 | 347 | buf := c.get() 348 | defer c.put(buf) 349 | 350 | dec := gob.NewDecoder(buf) 351 | buf.Write(payload) 352 | 353 | err := dec.Decode(out) 354 | if err != nil { 355 | return errors.E(op, err) 356 | } 357 | 358 | return nil 359 | case flags&frame.CodecRaw != 0: 360 | opts := c.frame.ReadOptions(c.frame.Header()) 361 | if len(opts) != 2 { 362 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 363 | } 364 | payload := c.frame.Payload()[opts[1]:] 365 | if len(payload) == 0 { 366 | return nil 367 | } 368 | 369 | if raw, ok := out.(*[]byte); ok { 370 | *raw = append(*raw, payload...) 371 | } 372 | 373 | return nil 374 | case flags&frame.CodecMsgpack != 0: 375 | opts := c.frame.ReadOptions(c.frame.Header()) 376 | if len(opts) != 2 { 377 | return errors.E(op, errors.Str("should be 2 options. SEQ_ID and METHOD_LEN")) 378 | } 379 | payload := c.frame.Payload()[opts[1]:] 380 | if len(payload) == 0 { 381 | return nil 382 | } 383 | 384 | return msgpack.Unmarshal(payload, out) 385 | default: 386 | return errors.E(op, errors.Str("unknown decoder used in frame")) 387 | } 388 | } 389 | 390 | // Close underlying socket. 391 | func (c *Codec) Close() error { 392 | if c.closed { 393 | return nil 394 | } 395 | 396 | c.closed = true 397 | return c.relay.Close() 398 | } 399 | -------------------------------------------------------------------------------- /pkg/socket/socket.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/roadrunner-server/errors" 7 | "github.com/roadrunner-server/goridge/v3/internal" 8 | "github.com/roadrunner-server/goridge/v3/pkg/frame" 9 | ) 10 | 11 | // Relay communicates with underlying process using sockets (TPC or Unix). 12 | type Relay struct { 13 | rwc io.ReadWriteCloser 14 | } 15 | 16 | // NewSocketRelay creates new socket based data relay. 17 | func NewSocketRelay(rwc io.ReadWriteCloser) *Relay { 18 | internal.Preallocate() 19 | return &Relay{rwc: rwc} 20 | } 21 | 22 | // Send signed (prefixed) data to PHP process. 23 | func (rl *Relay) Send(frame *frame.Frame) error { 24 | const op = errors.Op("pipes frame send") 25 | _, err := rl.rwc.Write(frame.Bytes()) 26 | if err != nil { 27 | return errors.E(op, err) 28 | } 29 | return nil 30 | } 31 | 32 | // Receive data from the underlying process and returns associated prefix or error. 33 | func (rl *Relay) Receive(frame *frame.Frame) error { 34 | if frame == nil { 35 | return errors.Str("nil frame") 36 | } 37 | return internal.ReceiveFrame(rl.rwc, frame) 38 | } 39 | 40 | // Close the connection. 41 | func (rl *Relay) Close() error { 42 | return rl.rwc.Close() 43 | } 44 | -------------------------------------------------------------------------------- /pkg/socket/socket_test.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "net" 5 | "testing" 6 | 7 | "github.com/roadrunner-server/goridge/v3/pkg/frame" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | const TestPayload = `alsdjf;lskjdgljasg;lkjsalfkjaskldjflkasjdf;lkasjfdalksdjflkajsdf;lfasdgnslsnblna;sldjjfawlkejr;lwjenlksndlfjawl;ejr;lwjelkrjaldfjl;sdjf` 12 | 13 | func TestSocketRelay(t *testing.T) { 14 | // configure and create tcp4 listener 15 | ls, err := net.Listen("tcp", "localhost:10002") 16 | assert.NoError(t, err) 17 | 18 | // TEST FRAME TO SEND 19 | nf := frame.NewFrame() 20 | nf.WriteVersion(nf.Header(), frame.Version1) 21 | nf.WriteFlags(nf.Header(), frame.CONTROL, frame.CodecGob, frame.CodecJSON) 22 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) //nolint:gosec 23 | nf.WritePayload([]byte(TestPayload)) 24 | nf.WriteCRC(nf.Header()) 25 | assert.Equal(t, true, nf.VerifyCRC(nf.Header())) 26 | 27 | conn, err := net.Dial("tcp", "localhost:10002") 28 | assert.NoError(t, err) 29 | rsend := NewSocketRelay(conn) 30 | err = rsend.Send(nf) 31 | assert.NoError(t, err) 32 | 33 | accept, err := ls.Accept() 34 | assert.NoError(t, err) 35 | assert.NotNil(t, accept) 36 | 37 | r := NewSocketRelay(accept) 38 | 39 | fr := frame.NewFrame() 40 | err = r.Receive(fr) 41 | if err != nil { 42 | t.Fatal(err) 43 | } 44 | 45 | assert.Equal(t, fr.ReadVersion(fr.Header()), nf.ReadVersion(nf.Header())) 46 | assert.Equal(t, fr.ReadFlags(), nf.ReadFlags()) 47 | assert.Equal(t, fr.ReadPayloadLen(fr.Header()), nf.ReadPayloadLen(nf.Header())) 48 | assert.Equal(t, true, fr.VerifyCRC(fr.Header())) 49 | assert.Equal(t, []byte(TestPayload), fr.Payload()) 50 | } 51 | 52 | func TestSocketRelayOptions(t *testing.T) { 53 | // configure and create tcp4 listener 54 | ls, err := net.Listen("tcp", "localhost:10001") 55 | assert.NoError(t, err) 56 | 57 | // TEST FRAME TO SEND 58 | nf := frame.NewFrame() 59 | nf.WriteVersion(nf.Header(), frame.Version1) 60 | nf.WriteFlags(nf.Header(), frame.CONTROL, frame.CodecGob, frame.CodecJSON) 61 | nf.WritePayloadLen(nf.Header(), uint32(len([]byte(TestPayload)))) //nolint:gosec 62 | nf.WritePayload([]byte(TestPayload)) 63 | nf.WriteOptions(nf.HeaderPtr(), 100, 10000, 100000) 64 | nf.WriteCRC(nf.Header()) 65 | assert.Equal(t, true, nf.VerifyCRC(nf.Header())) 66 | 67 | conn, err := net.Dial("tcp", "localhost:10001") 68 | assert.NoError(t, err) 69 | rsend := NewSocketRelay(conn) 70 | err = rsend.Send(nf) 71 | assert.NoError(t, err) 72 | 73 | accept, err := ls.Accept() 74 | assert.NoError(t, err) 75 | assert.NotNil(t, accept) 76 | 77 | r := NewSocketRelay(accept) 78 | 79 | fr := frame.NewFrame() 80 | err = r.Receive(fr) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | 85 | assert.Equal(t, fr.ReadVersion(fr.Header()), nf.ReadVersion(nf.Header())) 86 | assert.Equal(t, fr.ReadFlags(), nf.ReadFlags()) 87 | assert.Equal(t, fr.ReadPayloadLen(fr.Header()), nf.ReadPayloadLen(nf.Header())) 88 | assert.Equal(t, true, fr.VerifyCRC(fr.Header())) 89 | assert.Equal(t, []byte(TestPayload), fr.Payload()) 90 | assert.Equal(t, []uint32{100, 10000, 100000}, fr.ReadOptions(fr.Header())) 91 | } 92 | 93 | func TestSocketRelayNoPayload(t *testing.T) { 94 | // configure and create tcp4 listener 95 | ls, err := net.Listen("tcp", "localhost:12221") 96 | assert.NoError(t, err) 97 | 98 | // TEST FRAME TO SEND 99 | nf := frame.NewFrame() 100 | nf.WriteVersion(nf.Header(), frame.Version1) 101 | nf.WriteFlags(nf.Header(), frame.CONTROL, frame.CodecGob, frame.CodecJSON) 102 | nf.WriteOptions(nf.HeaderPtr(), 100, 10000, 100000) 103 | nf.WriteCRC(nf.Header()) 104 | assert.Equal(t, true, nf.VerifyCRC(nf.Header())) 105 | 106 | conn, err := net.Dial("tcp", "localhost:12221") 107 | assert.NoError(t, err) 108 | rsend := NewSocketRelay(conn) 109 | err = rsend.Send(nf) 110 | assert.NoError(t, err) 111 | 112 | accept, err := ls.Accept() 113 | assert.NoError(t, err) 114 | assert.NotNil(t, accept) 115 | 116 | r := NewSocketRelay(accept) 117 | 118 | fr := frame.NewFrame() 119 | err = r.Receive(fr) 120 | if err != nil { 121 | t.Fatal(err) 122 | } 123 | 124 | assert.Equal(t, fr.ReadVersion(fr.Header()), nf.ReadVersion(nf.Header())) 125 | assert.Equal(t, fr.ReadFlags(), nf.ReadFlags()) 126 | assert.Equal(t, fr.ReadPayloadLen(fr.Header()), nf.ReadPayloadLen(nf.Header())) 127 | assert.Equal(t, true, fr.VerifyCRC(fr.Header())) 128 | assert.Equal(t, []byte{}, fr.Payload()) // empty 129 | assert.Equal(t, []uint32{100, 10000, 100000}, fr.ReadOptions(fr.Header())) 130 | } 131 | 132 | func TestSocketRelayWrongCRC(t *testing.T) { 133 | // configure and create tcp4 listener 134 | ls, err := net.Listen("tcp", "localhost:13445") 135 | assert.NoError(t, err) 136 | 137 | // TEST FRAME TO SEND 138 | nf := frame.NewFrame() 139 | nf.WriteVersion(nf.Header(), frame.Version1) 140 | nf.WriteFlags(nf.Header(), frame.CONTROL, frame.CodecGob, frame.CodecJSON) 141 | nf.WriteOptions(nf.HeaderPtr(), 100, 10000, 100000) 142 | nf.WriteCRC(nf.Header()) 143 | nf.Header()[6] = 22 // just random wrong CRC directly 144 | 145 | conn, err := net.Dial("tcp", "localhost:13445") 146 | assert.NoError(t, err) 147 | _, err = conn.Write(nf.Bytes()) 148 | assert.NoError(t, err) 149 | 150 | accept, err := ls.Accept() 151 | assert.NoError(t, err) 152 | assert.NotNil(t, accept) 153 | 154 | r := NewSocketRelay(accept) 155 | 156 | fr := frame.NewFrame() 157 | err = r.Receive(fr) 158 | assert.Error(t, err) 159 | assert.False(t, fr.VerifyCRC(fr.Header())) 160 | 161 | assert.Empty(t, fr.Payload()) 162 | } 163 | --------------------------------------------------------------------------------