Repository: panjf2000/gnet Branch: dev Commit: 91cf015e41af Files: 126 Total size: 558.5 KB Directory structure: gitextract_0awrewzb/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug-report.yaml │ │ ├── feature-request.yaml │ │ └── question.yaml │ ├── PULL_REQUEST_TEMPLATE.md │ ├── release-drafter.yml │ └── workflows/ │ ├── codeql.yml │ ├── cross-compile-bsd.yml │ ├── gh-translator.yml │ ├── pull-request.yml │ ├── release-drafter.yml │ ├── stale-bot.yml │ ├── test.yml │ ├── test_gc_opt.yml │ ├── test_poll_opt.yml │ └── test_poll_opt_gc_opt.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README_ZH.md ├── acceptor_unix.go ├── acceptor_windows.go ├── client_test.go ├── client_unix.go ├── client_windows.go ├── conn_map.go ├── conn_matrix.go ├── conn_matrix_test.go ├── connection_bsd.go ├── connection_linux.go ├── connection_unix.go ├── connection_windows.go ├── context.go ├── engine_unix.go ├── engine_windows.go ├── eventloop_unix.go ├── eventloop_unix_test.go ├── eventloop_windows.go ├── gnet.go ├── gnet_test.go ├── go.mod ├── go.sum ├── internal/ │ └── gfd/ │ └── gfd.go ├── listener_unix.go ├── listener_windows.go ├── load_balancer.go ├── options.go ├── os_unix_test.go ├── os_windows_test.go ├── pkg/ │ ├── bs/ │ │ └── bs.go │ ├── buffer/ │ │ ├── elastic/ │ │ │ ├── elastic_buffer_test.go │ │ │ ├── elastic_ring_buffer.go │ │ │ └── elastic_ring_list_buffer.go │ │ ├── linkedlist/ │ │ │ ├── linked_list_buffer.go │ │ │ └── llbuffer_test.go │ │ └── ring/ │ │ ├── ring_buffer.go │ │ └── ring_buffer_test.go │ ├── errors/ │ │ └── errors.go │ ├── io/ │ │ ├── io.go │ │ ├── io_bsd.go │ │ └── io_linux.go │ ├── logging/ │ │ └── logger.go │ ├── math/ │ │ ├── math.go │ │ └── math_test.go │ ├── netpoll/ │ │ ├── defs_bsd_32bit.go │ │ ├── defs_bsd_64bit.go │ │ ├── defs_linux.go │ │ ├── defs_linux_386.go │ │ ├── defs_linux_amd64.go │ │ ├── defs_linux_arm.go │ │ ├── defs_linux_arm64.go │ │ ├── defs_linux_mips64x.go │ │ ├── defs_linux_mipsx.go │ │ ├── defs_linux_ppc64.go │ │ ├── defs_linux_ppc64le.go │ │ ├── defs_linux_riscv64.go │ │ ├── defs_linux_s390x.go │ │ ├── defs_poller.go │ │ ├── defs_poller_bsd.go │ │ ├── defs_poller_epoll.go │ │ ├── defs_poller_kqueue.go │ │ ├── defs_poller_netbsd.go │ │ ├── example_test.go │ │ ├── netpoll.go │ │ ├── poller_epoll_default.go │ │ ├── poller_epoll_ultimate.go │ │ ├── poller_kqueue_default.go │ │ ├── poller_kqueue_ultimate.go │ │ ├── poller_kqueue_wakeup.go │ │ ├── poller_kqueue_wakeup1.go │ │ ├── poller_unix_ultimate.go │ │ ├── syscall_epoll_generic_linux.go │ │ ├── syscall_epoll_linux.go │ │ ├── syscall_epoll_riscv64_arm64_linux.go │ │ └── syscall_errors_linux.go │ ├── pool/ │ │ ├── bytebuffer/ │ │ │ └── bytebuffer.go │ │ ├── byteslice/ │ │ │ ├── byteslice.go │ │ │ └── byteslice_test.go │ │ ├── goroutine/ │ │ │ └── goroutine.go │ │ └── ringbuffer/ │ │ └── ringbuffer.go │ ├── queue/ │ │ ├── lock_free_queue.go │ │ ├── queue.go │ │ └── queue_test.go │ └── socket/ │ ├── fd_unix.go │ ├── sock_bsd.go │ ├── sock_cloexec.go │ ├── sock_linux.go │ ├── sock_posix.go │ ├── sockaddr.go │ ├── socket.go │ ├── sockopts_bsd.go │ ├── sockopts_darwin.go │ ├── sockopts_freebsd.go │ ├── sockopts_linux.go │ ├── sockopts_openbsd.go │ ├── sockopts_posix.go │ ├── sockopts_unix.go │ ├── sockopts_unix1.go │ ├── sys_cloexec.go │ ├── tcp_socket.go │ ├── udp_socket.go │ └── unix_socket.go ├── reactor_default.go └── reactor_ultimate.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [panjf2000] patreon: panjf2000 open_collective: panjf2000 ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug-report.yaml ================================================ name: Bug Report description: Oops!..., it's a bug. title: "[Bug]: " labels: ["bug"] assignees: - panjf2000 body: - type: markdown id: tips attributes: value: | ## Before you go any further - Please read [*How To Ask Questions The Smart Way*](http://www.catb.org/~esr/faqs/smart-questions.html) ( Chinese translation: [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)) before you file an issue formally. - Keep in mind that there is always welcome to ask questions on [Discord](https://discord.gg/UyKD7NZcfH). - First of all, visit our [website](https://gnet.host) and [Github Org](https://github.com/gnet-io) for docs and examples. - type: checkboxes id: checklist attributes: label: Actions I've taken before I'm here description: Make sure you have tried the following ways but still the problem has not been solved. options: - label: I've thoroughly read the documentations on this issue but still have no clue. required: true - label: I've searched the Github Issues but didn't find any duplicate issues that have been resolved. required: true - label: I've searched the internet for this issue but didn't find anything helpful. required: true validations: required: true - type: textarea id: bug-report attributes: label: What happened? description: Describe (and illustrate) the bug that you encountered precisely. placeholder: Please describe what happened and how it happened, the more details you provide, the faster the bug gets fixed. validations: required: true - type: dropdown id: major-version attributes: label: Major version of gnet description: What major version of gnet are you running? options: - v2 - v1 validations: required: true - type: input id: specific-version attributes: label: Specific version of gnet description: What's the specific version of gnet? placeholder: "For example: v2.1.2" validations: required: true - type: dropdown id: os attributes: label: Operating system multiple: true options: - Linux - macOS - Windows - BSD validations: required: true - type: input id: os-version attributes: label: OS version description: What's the specific version of OS? placeholder: "Run `uname -srm` command to get the info, for example: Darwin 21.5.0 arm64, Linux 5.4.0-137-generic x86_64" validations: required: true - type: input id: go-version attributes: label: Go version description: What's the specific version of Go? placeholder: "Run `go version` command to get the info, for example: go1.20.5 linux/amd64" validations: required: true - type: textarea id: logs attributes: label: Relevant log output description: Please copy and paste any relevant log output. render: shell validations: required: true - type: textarea id: code attributes: label: Reproducer description: Please provide the minimal code to reproduce the bug. render: go - type: textarea id: how-to-reproduce attributes: label: How to Reproduce description: Steps to reproduce the result. placeholder: Tell us step by step how we can replicate this bug and what we should see in the end. value: | Steps to reproduce the behavior: 1. Go to '....' 2. Click on '....' 3. Do '....' 4. See '....' validations: required: true - type: dropdown id: bug-in-latest-code attributes: label: Does this issue reproduce with the latest release? description: Is this bug still present in the latest version? options: - It can reproduce with the latest release - It gets fixed in the latest release - I haven't verified it with the latest release validations: required: true ================================================ FILE: .github/ISSUE_TEMPLATE/feature-request.yaml ================================================ name: Feature Request description: Propose an idea to make gnet even better. title: "[Feature]: " labels: ["proposal", "enhancement"] assignees: - panjf2000 body: - type: markdown id: tips attributes: value: | ## Before you go any further - Please read [*How To Ask Questions The Smart Way*](http://www.catb.org/~esr/faqs/smart-questions.html) ( Chinese translation: [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)) before you file an issue formally. - Keep in mind that there is always welcome to ask questions on [Discord](https://discord.gg/UyKD7NZcfH). - First of all, visit our [website](https://gnet.host) and [Github Org](https://github.com/gnet-io) for docs and examples. - type: textarea id: feature-request attributes: label: Description of new feature description: Make a concise but clear description about this new feature. placeholder: Describe this new feature with critical details. validations: required: true - type: textarea id: feature-scenario attributes: label: Scenarios for new feature description: Explain why you need this feature and what scenarios can benefit from it. placeholder: Please try to convince us that this new feature makes sense, also it will improve gnet. validations: required: true - type: dropdown id: breaking-changes attributes: label: Breaking changes or not? description: Is this new feature going to break the backward compatibility of the existing APIs? options: - "Yes" - "No" validations: required: true - type: textarea id: code attributes: label: Code snippets (optional) description: Illustrate this new feature with source code, what it looks like in code. render: go - type: textarea id: feature-alternative attributes: label: Alternatives for new feature description: Alternatives in your mind in case that the feature can't be done for some reasons. placeholder: A concise but clear description of any alternative solutions or features you had in mind. value: None. - type: textarea id: additional-context attributes: label: Additional context (optional) description: Any additional context about this new feature we should know. placeholder: What else should we know before we start discussing this new feature? value: None. ================================================ FILE: .github/ISSUE_TEMPLATE/question.yaml ================================================ name: Question description: Ask questions about gnet. title: "[Question]: " labels: ["question", "help wanted"] body: - type: markdown id: tips attributes: value: | ## Before you go any further - Please read [*How To Ask Questions The Smart Way*](http://www.catb.org/~esr/faqs/smart-questions.html) ( Chinese translation: [《提问的智慧》](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)) before you file an issue formally. - Keep in mind that there is always welcome to ask questions on [Discord](https://discord.gg/UyKD7NZcfH). - First of all, visit our [website](https://gnet.host) and [Github Org](https://github.com/gnet-io) for docs and examples. - Make sure what you're looking for here is an issue rather than a [discussion](https://github.com/panjf2000/gnet/discussions/new/choose). - type: checkboxes id: checklist attributes: label: Actions I've taken before I'm here description: Make sure you have tried the following ways to get the answer of your problem. options: - label: I've thoroughly read the documentations about this problem but still have no answer. required: true - label: I've searched the Github Issues/Discussions but didn't find any similar problems that have been solved. required: true - label: I've searched the internet for this problem but didn't find anything helpful. required: true validations: required: true - type: textarea id: question attributes: label: Questions with details description: What do you want to know? placeholder: Describe your question with critical details here. validations: required: true - type: textarea id: code attributes: label: Code snippets (optional) description: Illustrate your question with source code if needed. render: go ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ## 1. Are you opening this pull request for bug-fix, optimization or new feature? ## 2. Please describe how these code changes achieve your intention. ## 3. Please link to the relevant issues (if any). ## 4. What documentation changes (if any) need to be made/updated because of this PR? ## 4. Checklist - [ ] I have squashed all insignificant commits. - [ ] I have commented my code for explaining package types, values, functions, and non-obvious lines. - [ ] I have written unit tests and verified that all tests passes (if needed). - [ ] I have documented feature info on the README (only when this PR is adding a new feature). - [ ] (optional) I am willing to help maintain this change if there are issues with it later. ================================================ FILE: .github/release-drafter.yml ================================================ name-template: Gnet v$RESOLVED_VERSION tag-template: v$RESOLVED_VERSION categories: - title: 🧨 Breaking changes labels: - breaking changes - title: 🚀 Features labels: - proposal - new feature - title: 🛩 Enhancements labels: - enhancements - optimization - title: 🐛 Bugfixes labels: - bug - title: 📚 Documentation labels: - docs - title: 🗃 Misc labels: - chores change-template: '- $TITLE (#$NUMBER)' change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks. version-resolver: major: labels: - major minor: labels: - minor - new feature - proposal patch: labels: - patch - bug - dependencies default: patch autolabeler: - label: bug title: - /fix/i - /bug/i - /resolve/i - label: docs files: - '*.md' title: - /doc/i - /README/i - label: enhancement title: - /opt:/i - /refactor/i - /optimize/i - /improve/i - /update/i - /remove/i - /delete/i - label: optimization title: - /opt:/i - /refactor/i - /optimize/i - /improve/i - /update/i - /remove/i - /delete/i - label: new feature title: - /feat:/i - /feature/i - /implement/i - /add/i - /minor/i - label: dependencies title: - /dep:/i - /dependencies/i - /upgrade/i - /bump up/i - label: chores title: - /chore/i - /misc/i - /cleanup/i - /clean up/i - label: major title: - /major:/i - label: minor title: - /minor:/i - label: patch title: - /patch:/i template: | ## Changelogs $CHANGES **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION Thanks to all these contributors: $CONTRIBUTORS for making this release possible. ================================================ FILE: .github/workflows/codeql.yml ================================================ name: "Code Scanning" on: push: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/codeql.yml' pull_request: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/codeql.yml' schedule: # ┌───────────── minute (0 - 59) # │ ┌───────────── hour (0 - 23) # │ │ ┌───────────── day of the month (1 - 31) # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) # │ │ │ │ │ # │ │ │ │ │ # │ │ │ │ │ # * * * * * - cron: '30 4 * * 0' jobs: CodeQL-Build: # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest runs-on: ubuntu-latest permissions: # required for all workflows security-events: write steps: - name: Checkout repository uses: actions/checkout@v4 with: fetch-depth: 2 # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: go # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). # If this step fails, then you should remove it and run the build manually (see below). - name: Autobuild uses: github/codeql-action/autobuild@v3 # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun # ✏️ If the Autobuild fails above, remove it and uncomment the following # three lines and modify them (or add more) to build your code if your # project uses a compiled language #- run: | # make bootstrap # make release - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 ================================================ FILE: .github/workflows/cross-compile-bsd.yml ================================================ name: Cross-compile for *BSD on: push: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/cross-compile-bsd.yml' pull_request: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/cross-compile-bsd.yml' env: GO111MODULE: on GOPROXY: "https://proxy.golang.org" jobs: build: strategy: fail-fast: false matrix: go: ['1.20', '1.25'] os: - ubuntu-latest name: Go ${{ matrix.go }} @ ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - name: Print Go environment id: go-env run: | printf "Using go at: $(which go)\n" printf "Go version: $(go version)\n" printf "\n\nGo environment:\n\n" go env printf "\n\nSystem environment:\n\n" env # Calculate the short SHA1 hash of the git commit echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "GO_CACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT - name: Cross-compiling for DragonFlyBSD run: GOOS=dragonfly GOARCH=amd64 go build - name: Cross-compiling for DragonFlyBSD -tags=poll_opt,gc_opt run: GOOS=dragonfly GOARCH=amd64 go build -tags=poll_opt,gc_opt - name: Cross-compiling for FreeBSD run: GOOS=freebsd GOARCH=amd64 go build - name: Cross-compiling for FreeBSD -tags=poll_opt,gc_opt run: GOOS=freebsd GOARCH=amd64 go build -tags=poll_opt,gc_opt - name: Cross-compiling for NetBSD run: GOOS=netbsd GOARCH=amd64 go build - name: Cross-compiling for NetBSD -tags=poll_opt,gc_opt run: GOOS=netbsd GOARCH=amd64 go build -tags=poll_opt,gc_opt - name: Cross-compiling for OpenBSD run: GOOS=openbsd GOARCH=amd64 go build - name: Cross-compiling for OpenBSD -tags=poll_opt,gc_opt run: GOOS=openbsd GOARCH=amd64 go build -tags=poll_opt,gc_opt ================================================ FILE: .github/workflows/gh-translator.yml ================================================ name: 'gh-translator' on: issues: types: [opened] pull_request: types: [opened] issue_comment: types: [created, edited] discussion: types: [created, edited, answered] discussion_comment: types: [created, edited] jobs: build: runs-on: ubuntu-latest steps: - uses: usthe/issues-translate-action@v2.7 with: BOT_GITHUB_TOKEN: ${{ secrets.GH_TRANSLATOR_TOKEN }} IS_MODIFY_TITLE: true CUSTOM_BOT_NOTE: 🤖 Non-English text detected, translating ... ================================================ FILE: .github/workflows/pull-request.yml ================================================ name: Check pull request target on: pull_request: types: - opened - reopened - synchronize branches: - master jobs: check-branches: runs-on: ubuntu-latest steps: - name: Check target branch run: | if [ ${{ github.head_ref }} != "dev" ]; then echo "Only pull requests from dev branch are only allowed to be merged into master branch." exit 1 fi ================================================ FILE: .github/workflows/release-drafter.yml ================================================ name: Release Drafter on: push: # branches to consider in the event; optional, defaults to all branches: - master # pull_request event is required only for autolabeler pull_request: # Only following types are handled by the action, but one can default to all as well types: [opened, reopened, synchronize] # pull_request_target event is required for autolabeler to support PRs from forks # pull_request_target: # types: [opened, reopened, synchronize] permissions: contents: read jobs: update_release_draft: permissions: # write permission is required to create a github release contents: write # write permission is required for autolabeler # otherwise, read permission is required at least pull-requests: write runs-on: ubuntu-latest steps: # (Optional) GitHub Enterprise requires GHE_HOST variable set #- name: Set GHE_HOST # run: | # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV # Drafts your next Release notes as Pull Requests are merged into "master" - uses: release-drafter/release-drafter@v6 # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml # with: # config-name: my-config.yml # disable-autolabeler: true env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/stale-bot.yml ================================================ name: Monitor inactive issues and PRs on: schedule: - cron: '0 0 * * *' workflow_dispatch: jobs: stale-issues: runs-on: ubuntu-latest permissions: actions: write issues: write pull-requests: write steps: - uses: actions/stale@v9 with: operations-per-run: 100 days-before-issue-stale: 30 days-before-issue-close: 7 stale-issue-label: 'stale' stale-issue-message: | This issue is marked as stale because it has been open for 30 days with no activity. You should take one of the following actions: - Manually close this issue if it is no longer relevant - Comment if you have more information to share This issue will be automatically closed in 7 days if no further activity occurs. close-issue-message: | This issue was closed because it has been inactive for 7 days since being marked as stale. If you believe this is a false alarm, please leave a comment for it or open a new issue, you can also reopen this issue directly if you have permission. days-before-pr-stale: 21 days-before-pr-close: 7 stale-pr-label: 'stale' stale-pr-message: | This PR is marked as stale because it has been open for 21 days with no activity. You should take one of the following actions: - Manually close this PR if it is no longer relevant - Push new commits or comment if you have more information to share This PR will be automatically closed in 7 days if no further activity occurs. close-pr-message: | This PR was closed because it has been inactive for 7 days since being marked as stale. If you believe this is a false alarm, feel free to reopen this PR or create a new one. repo-token: ${{ secrets.GITHUB_TOKEN }} ================================================ FILE: .github/workflows/test.yml ================================================ name: Run tests on: push: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/test.yml' pull_request: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/test.yml' env: GO111MODULE: on GOPROXY: "https://proxy.golang.org" jobs: lint: strategy: matrix: os: - ubuntu-latest - macos-latest #- windows-latest name: Run golangci-lint runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: '^1.20' cache: false - name: Setup and run golangci-lint uses: golangci/golangci-lint-action@v7 with: version: v2.1.6 args: -v -E gocritic -E misspell -E revive -E godot --timeout 5m test: needs: lint strategy: fail-fast: false matrix: go: ['1.20', '1.26'] os: - ubuntu-latest - macos-14 # check out this issue: https://github.com/actions/runner-images/issues/10924 - windows-latest name: Go ${{ matrix.go }} @ ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - name: Print Go environment id: go-env run: | printf "Using go at: $(which go)\n" printf "Go version: $(go version)\n" printf "\n\nGo environment:\n\n" go env printf "\n\nSystem environment:\n\n" env # Calculate the short SHA1 hash of the git commit echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "GO_CACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT - name: Run unit tests for packages run: go test $(go list ./... | tail -n +2) # NOTE: -race is conditionally disabled for Windows with Go 1.26 due to # known instability of the Go race detector on this platform/version # combination (intermittent crashes and timeouts observed in CI). # This is likely a regression in Go 1.26 on Windows. Check out #752 and #754 for details. # Once the underlying issue is resolved and CI is stable, this # condition should be revisited so that race testing can be re-enabled. - name: Run integration tests run: go test -v ${{ !(runner.os == 'Windows' && startsWith(matrix.go, '1.26')) && '-race' || '' }} -coverprofile="codecov.report" -covermode=atomic -timeout 15m -failfast - name: Upload the code coverage report to codecov.io uses: codecov/codecov-action@v5 with: files: ./codecov.report flags: unittests name: codecov-gnet fail_ci_if_error: true verbose: true env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .github/workflows/test_gc_opt.yml ================================================ name: Run tests with -tags=gc_opt on: push: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/test_gc_opt.yml' pull_request: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/test_gc_opt.yml' env: GO111MODULE: on GOPROXY: "https://proxy.golang.org" jobs: lint: strategy: matrix: os: - ubuntu-latest - macos-latest #- windows-latest name: Run golangci-lint runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: '^1.20' cache: false - name: Setup and run golangci-lint uses: golangci/golangci-lint-action@v7 with: version: v2.1.6 args: -v -E gocritic -E misspell -E revive -E godot --timeout 5m test: needs: lint strategy: fail-fast: false matrix: go: ['1.20', '1.26'] os: - ubuntu-latest - macos-14 # check out this issue: https://github.com/actions/runner-images/issues/10924 name: Go ${{ matrix.go }} @ ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - name: Print Go environment id: go-env run: | printf "Using go at: $(which go)\n" printf "Go version: $(go version)\n" printf "\n\nGo environment:\n\n" go env printf "\n\nSystem environment:\n\n" env # Calculate the short SHA1 hash of the git commit echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "GO_CACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT - name: Run unit tests for packages run: go test $(go list ./... | tail -n +2) - name: Run integration tests run: go test -v -race -tags=gc_opt -coverprofile="codecov.report" -covermode=atomic -timeout 15m -failfast - name: Upload the code coverage report to codecov.io uses: codecov/codecov-action@v5 with: files: ./codecov.report flags: unittests name: codecov-gnet fail_ci_if_error: true verbose: true env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .github/workflows/test_poll_opt.yml ================================================ name: Run tests with -tags=poll_opt on: push: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/test_poll_opt.yml' pull_request: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/test_poll_opt.yml' env: GO111MODULE: on GOPROXY: "https://proxy.golang.org" jobs: lint: strategy: matrix: os: - ubuntu-latest - macos-latest name: Run golangci-lint runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: '^1.20' cache: false - name: Setup and run golangci-lint uses: golangci/golangci-lint-action@v7 with: version: v2.1.6 args: -v -E gocritic -E misspell -E revive -E godot test: needs: lint strategy: fail-fast: false matrix: go: ['1.20', '1.26'] os: - ubuntu-latest - macos-14 # check out this issue: https://github.com/actions/runner-images/issues/10924 name: Go ${{ matrix.go }} @ ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - name: Print Go environment id: go-env run: | printf "Using go at: $(which go)\n" printf "Go version: $(go version)\n" printf "\n\nGo environment:\n\n" go env printf "\n\nSystem environment:\n\n" env # Calculate the short SHA1 hash of the git commit echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "GO_CACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT - name: Run unit tests for packages run: go test $(go list ./... | tail -n +2) - name: Run integration tests with -tags=poll_opt run: go test -v -tags=poll_opt -coverprofile="codecov.report" -covermode=atomic -timeout 10m -failfast - name: Upload the code coverage report to codecov.io uses: codecov/codecov-action@v5 with: files: ./codecov.report flags: unittests name: codecov-gnet-poll_opt fail_ci_if_error: true verbose: true env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .github/workflows/test_poll_opt_gc_opt.yml ================================================ name: Run tests with -tags=poll_opt,gc_opt on: push: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/test_poll_opt_gc_opt.yml' pull_request: branches: - master - dev - 1.x paths-ignore: - '**.md' - '**.yml' - '**.yaml' - '!.github/workflows/test_poll_opt_gc_opt.yml' env: GO111MODULE: on GOPROXY: "https://proxy.golang.org" jobs: lint: strategy: matrix: os: - ubuntu-latest - macos-latest name: Run golangci-lint runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Go uses: actions/setup-go@v5 with: go-version: '^1.20' cache: false - name: Setup and run golangci-lint uses: golangci/golangci-lint-action@v7 with: version: v2.1.6 args: -v -E gocritic -E misspell -E revive -E godot test: needs: lint strategy: fail-fast: false matrix: go: ['1.20', '1.26'] os: - ubuntu-latest - macos-14 # check out this issue: https://github.com/actions/runner-images/issues/10924 name: Go ${{ matrix.go }} @ ${{ matrix.os }} runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ matrix.go }} - name: Print Go environment id: go-env run: | printf "Using go at: $(which go)\n" printf "Go version: $(go version)\n" printf "\n\nGo environment:\n\n" go env printf "\n\nSystem environment:\n\n" env # Calculate the short SHA1 hash of the git commit echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT echo "GO_CACHE=$(go env GOCACHE)" >> $GITHUB_OUTPUT - name: Run unit tests for packages run: go test $(go list ./... | tail -n +2) - name: Run integration tests with -tags=poll_opt run: go test -v -tags=poll_opt,gc_opt -coverprofile="codecov.report" -covermode=atomic -timeout 10m -failfast - name: Upload the code coverage report to codecov.io uses: codecov/codecov-action@v5 with: files: ./codecov.report flags: unittests name: codecov-gnet-poll_opt fail_ci_if_error: true verbose: true env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} ================================================ FILE: .gitignore ================================================ # IDEs .idea/ .vscode/ # dependencies /node_modules # production /build # generated files .docusaurus .docusaurus/ .cache-loader # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at panjf2000@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing ## With issues: - Use the search tool before opening a new issue. - Please provide source code and commit sha if you found a bug. - Review existing issues and provide feedback or react to them. ## With pull requests: - Open your pull request against `dev`. - Open one pull request for only one feature/proposal, if you have several those, please put them into different PRs, whereas you are allowed to open one pull request with several bug-fixes. - Your pull request should have no more than two commits, if not, you should squash them. - It should pass all tests in the available continuous integrations systems such as TravisCI. - You should add/modify tests to cover your proposed code changes. - If your pull request contains a new feature, please document it on the README. ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright (c) 2019-present Andy Pan Copyright (c) 2017 Joshua J Baker Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================

gnet

panjf2000%2Fgnet | Trendshift

English | [中文](README_ZH.md) ### 🎉🎉🎉 Feel free to join [the channels about `gnet` on the Discord Server](https://discord.gg/UyKD7NZcfH). # 📖 Introduction `gnet` is an event-driven networking framework that is ultra-fast and lightweight. It is built from scratch by exploiting [epoll](https://man7.org/linux/man-pages/man7/epoll.7.html) and [kqueue](https://en.wikipedia.org/wiki/Kqueue) and it can achieve much higher performance with lower memory consumption than Go [net](https://golang.org/pkg/net/) in many specific scenarios. `gnet` and [net](https://golang.org/pkg/net/) don't share the same philosophy in network programming. Thus, building network applications with `gnet` can be significantly different from building them with [net](https://golang.org/pkg/net/), and the philosophies can't be reconciled. There are other similar products written in other programming languages in the community, such as [libuv](https://github.com/libuv/libuv), [netty](https://github.com/netty/netty), [twisted](https://github.com/twisted/twisted), [tornado](https://github.com/tornadoweb/tornado), etc. which work in a similar pattern as `gnet` under the hood. `gnet` is not designed to displace the Go [net](https://golang.org/pkg/net/), but to create an alternative in the Go ecosystem for building performance-critical network services. As a result of which, `gnet` is not as comprehensive as Go [net](https://golang.org/pkg/net/), it provides only the core functionality (via a concise set of APIs) required by a network application and it doesn't plan on becoming a coverall networking framework, as I think Go [net](https://golang.org/pkg/net/) has done a good enough job in that area. `gnet` sells itself as a high-performance, lightweight, non-blocking, event-driven networking framework written in pure Go which works on the transport layer with TCP/UDP protocols and Unix Domain Socket. It enables developers to implement their own protocols(HTTP, RPC, WebSocket, Redis, etc.) of application layer upon `gnet` for building diversified network services. For instance, you get an HTTP Server if you implement HTTP protocol upon `gnet` while you have a Redis Server done with the implementation of Redis protocol upon `gnet` and so on. **`gnet` derives from the project: `evio` with much higher performance and more features.** # 🚀 Features ## 🦖 Milestones - [x] [High-performance](#-performance) event-driven looping based on a networking model of multiple threads/goroutines - [x] Built-in goroutine pool powered by the library [ants](https://github.com/panjf2000/ants) - [x] Lock-free during the entire runtime - [x] Concise and easy-to-use APIs - [x] Efficient, reusable, and elastic memory buffer: (Elastic-)Ring-Buffer, Linked-List-Buffer and Elastic-Mixed-Buffer - [x] Multiple protocols/IPC mechanisms: `TCP`, `UDP`, and `Unix Domain Socket` - [x] Multiple load-balancing algorithms: `Round-Robin`, `Source-Addr-Hash`, and `Least-Connections` - [x] Flexible ticker event - [x] `gnet` client - [x] Running on `Linux`, `macOS`, `Windows`, and *BSD: `Darwin`/`DragonFlyBSD`/`FreeBSD`/`NetBSD`/`OpenBSD` - [x] **Edge-triggered** I/O support - [x] Multiple network addresses binding - [x] Support registering new connections to event-loops ## 🕊 Roadmap - [ ] **TLS** support - [ ] [io_uring](https://github.com/axboe/liburing/wiki/io_uring-and-networking-in-2023) support - [ ] **KCP** support ***Windows version of `gnet` should only be used in development for developing and testing, it shouldn't be used in production.*** # 🎬 Getting started `gnet` is available as a Go module and we highly recommend that you use `gnet` via [Go Modules](https://go.dev/blog/using-go-modules), with Go 1.11 Modules enabled (Go 1.11+), you can just simply add `import "github.com/panjf2000/gnet/v2"` to the codebase and run `go mod download/go mod tidy` or `go [build|run|test]` to download the necessary dependencies automatically. ## With v2 ```bash go get -u github.com/panjf2000/gnet/v2 ``` ## With v1 ```bash go get -u github.com/panjf2000/gnet ``` # 🎡 Use cases The following corporations/organizations use `gnet` as the underlying network service in production.
If you're also using `gnet` in production, please help us enrich this list by opening a pull request. # 📊 Performance ## Benchmarks on TechEmpower ```bash # Hardware Environment * 28 HT Cores Intel(R) Xeon(R) Gold 5120 CPU @ 3.20GHz * 32GB RAM * Dedicated Cisco 10-gigabit Ethernet switch * Debian 12 "bookworm" * Go1.19.x linux/amd64 ``` ![](https://raw.githubusercontent.com/panjf2000/illustrations/master/benchmark/techempower-plaintext-top50-light.jpg) This is a leaderboard of the top ***50*** out of ***486*** frameworks that encompass various programming languages worldwide, in which `gnet` is ranked ***first***. ![](https://raw.githubusercontent.com/panjf2000/illustrations/master/benchmark/techempower-plaintext-topN-go-light.png) This is the full framework ranking of Go and `gnet` tops all the other frameworks, which makes `gnet` the ***fastest*** networking framework in Go. To see the full ranking list, visit [TechEmpower Benchmark **Round 22**](https://www.techempower.com/benchmarks/#hw=ph&test=plaintext§ion=data-r22). ***Note that the HTTP implementation of gnet on TechEmpower is half-baked and fine-tuned for benchmark purposes only and far from production-ready.*** ## Contrasts to the similar networking libraries ### On Linux (epoll) #### Environment ```bash # Machine information OS : Ubuntu 20.04/x86_64 CPU : 8 CPU cores, AMD EPYC 7K62 48-Core Processor Memory : 16.0 GiB # Go version and settings Go Version : go1.17.2 linux/amd64 GOMAXPROCS : 8 # Benchmark parameters TCP connections : 1000/2000/5000/10000 Packet size : 512/1024/2048/4096/8192/16384/32768/65536 bytes Test duration : 15s ``` [![](https://github.com/panjf2000/gnet_benchmarks/raw/master/results/echo_conn_linux.png)](https://github.com/gnet-io/gnet-benchmarks) [![](https://github.com/panjf2000/gnet_benchmarks/raw/master/results/echo_packet_linux.png)]((https://github.com/gnet-io/gnet-benchmarks)) ### On MacOS (kqueue) #### Environment ```bash # Machine information OS : MacOS Big Sur/x86_64 CPU : 6 CPU cores, Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz Memory : 16.0 GiB # Go version and settings Go Version : go1.16.5 darwin/amd64 GOMAXPROCS : 12 # Benchmark parameters TCP connections : 300/400/500/600/700 Packet size : 512/1024/2048/4096/8192 bytes Test duration : 15s ``` [![](https://github.com/panjf2000/gnet_benchmarks/raw/master/results/echo_conn_macos.png)]((https://github.com/gnet-io/gnet-benchmarks)) [![](https://github.com/panjf2000/gnet_benchmarks/raw/master/results/echo_packet_macos.png)]((https://github.com/gnet-io/gnet-benchmarks)) ### Combat with Rust [![](https://res.strikefreedom.top/static_res/blog/figures/Gjfx2GoXAAA5haW.jpeg)](https://www.youtube.com/watch?v=31R8Ef9A0iw) # ⚠️ License The source code of `gnet` should be distributed under the Apache-2.0 license. # 👏 Contributors Please read the [Contributing Guidelines](CONTRIBUTING.md) before opening a PR and thank you to all the developers who already made contributions to `gnet`! # ⚓ Relevant Articles - [A Million WebSockets and Go](https://www.freecodecamp.org/news/million-websockets-and-go-cc58418460bb/) - [Going Infinite, handling 1M websockets connections in Go](https://speakerdeck.com/eranyanay/going-infinite-handling-1m-websockets-connections-in-go) - [Go netpoller 原生网络模型之源码全面揭秘](https://strikefreedom.top/go-netpoll-io-multiplexing-reactor) - [gnet: 一个轻量级且高性能的 Golang 网络库](https://strikefreedom.top/go-event-loop-networking-library-gnet) - [最快的 Go 网络框架 gnet 来啦!](https://strikefreedom.top/releasing-gnet-v1-with-techempower) # ☕️ Buy me a coffee > Please be sure to leave your name, GitHub account, or other social media accounts when you donate by the following means so that I can add it to the list of donors as a token of my appreciation.
By me coffee Patreon OpenCollective
# 🔑 JetBrains OS licenses `gnet` has been being developed with `GoLand` IDE under the ***free JetBrains Open Source license(s)*** granted by JetBrains s.r.o., hence I would like to express my thanks here. JetBrains logo. # 🔋 Sponsorship

This project is supported by:

================================================ FILE: README_ZH.md ================================================

gnet

panjf2000%2Fgnet | Trendshift

[英文](README.md) | 中文 ### 🎉🎉🎉 欢迎加入 `gnet` 在 [Discord 服务器上的频道](https://discord.gg/UyKD7NZcfH). # 📖 简介 `gnet` 是一个基于事件驱动的高性能和轻量级网络框架。这个框架是基于 [epoll](https://en.wikipedia.org/wiki/Epoll) 和 [kqueue](https://en.wikipedia.org/wiki/Kqueue) 从零开发的,而且相比 Go [net](https://golang.org/pkg/net/),它能以更低的内存占用实现更高的性能。 `gnet` 和 [net](https://golang.org/pkg/net/) 有着不一样的网络编程范式。因此,用 `gnet` 开发网络应用和用 [net](https://golang.org/pkg/net/) 开发区别很大,而且两者之间不可调和。社区里有其他同类的产品像是 [libuv](https://github.com/libuv/libuv), [netty](https://github.com/netty/netty), [twisted](https://github.com/twisted/twisted), [tornado](https://github.com/tornadoweb/tornado),`gnet` 的底层工作原理和这些框架非常类似。 `gnet` 不是为了取代 [net](https://golang.org/pkg/net/) 而生的,而是在 Go 生态中为开发者提供一个开发性能敏感的网络服务的替代品。也正因如此,`gnet` 在功能全面性上比不了 Go [net](https://golang.org/pkg/net/),它只会提供网络应用所需的最核心的功能和最精简的 APIs,而且 `gnet` 也并没有打算变成一个无所不包的网络框架,因为我觉得 Go [net](https://golang.org/pkg/net/) 在这方面已经做得足够好了。 `gnet` 的卖点在于它是一个高性能、轻量级、非阻塞的纯 Go 语言实现的传输层(TCP/UDP/Unix Domain Socket)网络框架。开发者可以使用 `gnet` 来实现自己的应用层网络协议(HTTP、RPC、Redis、WebSocket 等等),从而构建出自己的应用层网络服务。比如在 `gnet` 上实现 HTTP 协议就可以创建出一个 HTTP 服务器 或者 Web 开发框架,实现 Redis 协议就可以创建出自己的 Redis 服务器等等。 **`gnet` 衍生自另一个项目:`evio`,但拥有更丰富的功能特性,且性能远胜之。** # 🚀 功能 ## 🦖 里程碑 - [x] 基于多线程/协程网络模型的[高性能](#-性能测试)事件驱动循环 - [x] 内置 goroutine 池,由开源库 [ants](https://github.com/panjf2000/ants) 提供支持 - [x] 整个生命周期是无锁的 - [x] 简单易用的 APIs - [x] 高效、可重用而且自动伸缩的内存 buffer:(Elastic-)Ring-Buffer, Linked-List-Buffer and Elastic-Mixed-Buffer - [x] 多种网络协议/IPC 机制:`TCP`、`UDP` 和 `Unix Domain Socket` - [x] 多种负载均衡算法:`Round-Robin(轮询)`、`Source-Addr-Hash(源地址哈希)` 和 `Least-Connections(最少连接数)` - [x] 灵活的事件定时器 - [x] `gnet` 客户端支持 - [x] 支持 `Linux`, `macOS`, `Windows` 和 *BSD 操作系统: `Darwin`/`DragonFlyBSD`/`FreeBSD`/`NetBSD`/`OpenBSD` - [x] **Edge-triggered** I/O 支持 - [x] 多网络地址绑定 - [x] 支持注册新的连接到事件循环 ## 🕊 蓝图 - [ ] 支持 **TLS** - [ ] 支持 [io_uring](https://github.com/axboe/liburing/wiki/io_uring-and-networking-in-2023) - [ ] 支持 **KCP** ***`gnet` 的 Windows 版本应该仅用于开发阶段的开发和测试,切勿用于生产环境***。 # 🎬 开始 `gnet` 是一个 Go module,而且我们也强烈推荐通过 [Go Modules](https://go.dev/blog/using-go-modules) 来使用 `gnet`,在开启 Go Modules 支持(Go 1.11+)之后可以通过简单地在代码中写 `import "github.com/panjf2000/gnet/v2"` 来引入 `gnet`,然后执行 `go mod download/go mod tidy` 或者 `go [build|run|test]` 这些命令来自动下载所依赖的包。 ## 使用 v2 ```bash go get -u github.com/panjf2000/gnet/v2 ``` ## 使用 v1 ```bash go get -u github.com/panjf2000/gnet ``` # 🎡 用户案例 以下公司/组织在生产环境上使用了 `gnet` 作为底层网络服务。
如果你也正在生产环境上使用 `gnet`,欢迎提 Pull Request 来丰富这份列表。 # 📊 性能测试 ## TechEmpower 性能测试 ```bash # 硬件环境 * 28 HT Cores Intel(R) Xeon(R) Gold 5120 CPU @ 3.20GHz * 32GB RAM * Dedicated Cisco 10-gigabit Ethernet switch * Debian 12 "bookworm" * Go1.19.x linux/amd64 ``` ![](https://raw.githubusercontent.com/panjf2000/illustrations/master/benchmark/techempower-plaintext-top50-light.jpg) 这是包含全部编程语言框架的性能排名***前 50*** 的结果,总榜单包含了全世界共计 ***486*** 个框架,其中 `gnet` 排名***第一***。 ![](https://raw.githubusercontent.com/panjf2000/illustrations/master/benchmark/techempower-plaintext-topN-go-light.png) 这是 Go 语言分类下的全部排名,`gnet` 超越了其他所有框架,位列第一,是***最快***的 Go 网络框架。 完整的排行可以通过 [TechEmpower Benchmark **Round 22**](https://www.techempower.com/benchmarks/#hw=ph&test=plaintext§ion=data-r22) 查看。 ***请注意,TechEmpower 上的 gnet 的 HTTP 实现是不完备且针对性调优的,仅仅是用于压测目的,不是生产可用的***。 ## 同类型的网络库性能对比 ### On Linux (epoll) #### Environment ```bash # Machine information OS : Ubuntu 20.04/x86_64 CPU : 8 CPU cores, AMD EPYC 7K62 48-Core Processor Memory : 16.0 GiB # Go version and settings Go Version : go1.17.2 linux/amd64 GOMAXPROCS : 8 # Benchmark parameters TCP connections : 1000/2000/5000/10000 Packet size : 512/1024/2048/4096/8192/16384/32768/65536 bytes Test duration : 15s ``` [![](https://github.com/panjf2000/gnet_benchmarks/raw/master/results/echo_conn_linux.png)](https://github.com/gnet-io/gnet-benchmarks) [![](https://github.com/panjf2000/gnet_benchmarks/raw/master/results/echo_packet_linux.png)]((https://github.com/gnet-io/gnet-benchmarks)) ### On MacOS (kqueue) #### Environment ```bash # Machine information OS : MacOS Big Sur/x86_64 CPU : 6 CPU cores, Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz Memory : 16.0 GiB # Go version and settings Go Version : go1.16.5 darwin/amd64 GOMAXPROCS : 12 # Benchmark parameters TCP connections : 300/400/500/600/700 Packet size : 512/1024/2048/4096/8192 bytes Test duration : 15s ``` [![](https://github.com/panjf2000/gnet_benchmarks/raw/master/results/echo_conn_macos.png)]((https://github.com/gnet-io/gnet-benchmarks)) [![](https://github.com/panjf2000/gnet_benchmarks/raw/master/results/echo_packet_macos.png)]((https://github.com/gnet-io/gnet-benchmarks)) ### "硬刚" Rust [![](https://res.strikefreedom.top/static_res/blog/figures/Gjfx2GoXAAA5haW.jpeg)](https://www.youtube.com/watch?v=31R8Ef9A0iw) # ⚠️ 证书 `gnet` 的源码需在遵循 Apache-2.0 开源证书的前提下使用。 # 👏 贡献者 请在提 PR 之前仔细阅读 [Contributing Guidelines](CONTRIBUTING.md),感谢那些为 `gnet` 贡献过代码的开发者! # ⚓ 相关文章 - [A Million WebSockets and Go](https://www.freecodecamp.org/news/million-websockets-and-go-cc58418460bb/) - [Going Infinite, handling 1M websockets connections in Go](https://speakerdeck.com/eranyanay/going-infinite-handling-1m-websockets-connections-in-go) - [Go netpoller 原生网络模型之源码全面揭秘](https://strikefreedom.top/go-netpoll-io-multiplexing-reactor) - [gnet: 一个轻量级且高性能的 Golang 网络库](https://strikefreedom.top/go-event-loop-networking-library-gnet) - [最快的 Go 网络框架 gnet 来啦!](https://strikefreedom.top/releasing-gnet-v1-with-techempower) # ☕️ 打赏 > 当您通过以下方式进行捐赠时,请务必留下姓名、GitHub 账号或其他社交媒体账号,以便我将其添加到捐赠者名单中,以表谢意。
By me coffee Patreon OpenCollective
# 🔑 JetBrains 开源证书支持 `gnet` 项目一直以来都是在 JetBrains 公司旗下的 GoLand 集成开发环境中进行开发,基于 ***free JetBrains Open Source license(s)*** 正版免费授权,在此表达我的谢意。 JetBrains logo. # 🔋 赞助商

本项目由以下机构赞助:

================================================ FILE: acceptor_unix.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package gnet import ( "runtime" "golang.org/x/sys/unix" "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/netpoll" "github.com/panjf2000/gnet/v2/pkg/queue" "github.com/panjf2000/gnet/v2/pkg/socket" ) func (el *eventloop) accept0(fd int, _ netpoll.IOEvent, _ netpoll.IOFlags) error { for { nfd, sa, err := socket.Accept(fd) switch err { case nil: case unix.EAGAIN: // the Accept queue has been drained out, we can return now return nil case unix.EINTR, unix.ECONNRESET, unix.ECONNABORTED: // ECONNRESET or ECONNABORTED could indicate that a socket // in the Accept queue was closed before we Accept()ed it. // It's a silly error, let's retry it. continue default: el.getLogger().Errorf("Accept() failed due to error: %v", err) return errors.ErrAcceptSocket } remoteAddr := socket.SockaddrToTCPOrUnixAddr(sa) network := el.listeners[fd].network if opts := el.engine.opts; opts.TCPKeepAlive > 0 && network == "tcp" && (runtime.GOOS != "linux" && runtime.GOOS != "freebsd" && runtime.GOOS != "dragonfly") { // TCP keepalive options are not inherited from the listening socket // on platforms other than Linux, FreeBSD, or DragonFlyBSD. // We therefore need to set them on the accepted socket explicitly. // // Check out https://github.com/nginx/nginx/pull/337 for details. if err = setKeepAlive( nfd, true, opts.TCPKeepAlive, opts.TCPKeepInterval, opts.TCPKeepCount); err != nil { el.getLogger().Errorf("failed to set TCP keepalive on fd=%d: %v", fd, err) } } el := el.engine.eventLoops.next(remoteAddr) c := newStreamConn(network, nfd, el, sa, el.listeners[fd].addr, remoteAddr) err = el.poller.Trigger(queue.HighPriority, el.register, c) if err != nil { el.getLogger().Errorf("failed to enqueue the accepted socket fd=%d to poller: %v", c.fd, err) _ = unix.Close(nfd) c.release() } } } func (el *eventloop) accept(fd int, ev netpoll.IOEvent, flags netpoll.IOFlags) error { network := el.listeners[fd].network if network == "udp" { return el.readUDP(fd, ev, flags) } nfd, sa, err := socket.Accept(fd) switch err { case nil: case unix.EINTR, unix.EAGAIN, unix.ECONNRESET, unix.ECONNABORTED: // ECONNRESET or ECONNABORTED could indicate that a socket // in the Accept queue was closed before we Accept()ed it. // It's a silly error, let's retry it. return nil default: el.getLogger().Errorf("Accept() failed due to error: %v", err) return errors.ErrAcceptSocket } remoteAddr := socket.SockaddrToTCPOrUnixAddr(sa) if opts := el.engine.opts; opts.TCPKeepAlive > 0 && el.listeners[fd].network == "tcp" && (runtime.GOOS != "linux" && runtime.GOOS != "freebsd" && runtime.GOOS != "dragonfly") { // TCP keepalive options are not inherited from the listening socket // on platforms other than Linux, FreeBSD, or DragonFlyBSD. // We therefore need to set them on the accepted socket explicitly. // // Check out https://github.com/nginx/nginx/pull/337 for details. if err = setKeepAlive( nfd, true, opts.TCPKeepAlive, opts.TCPKeepInterval, opts.TCPKeepCount); err != nil { el.getLogger().Errorf("failed to set TCP keepalive on fd=%d: %v", fd, err) } } c := newStreamConn(network, nfd, el, sa, el.listeners[fd].addr, remoteAddr) return el.register0(c) } ================================================ FILE: acceptor_windows.go ================================================ // Copyright (c) 2023 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gnet import ( "errors" "net" "runtime" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/pool/goroutine" ) func (eng *engine) listenStream(ln net.Listener) (err error) { if eng.opts.LockOSThread { runtime.LockOSThread() defer runtime.UnlockOSThread() } defer func() { eng.shutdown(err) }() for { // Accept TCP socket. tc, e := ln.Accept() if e != nil { err = e if !eng.beingShutdown.Load() { eng.opts.Logger.Errorf("Accept() fails due to error: %v", err) } else if errors.Is(err, net.ErrClosed) { err = errors.Join(err, errorx.ErrEngineShutdown) } return } el := eng.eventLoops.next(tc.RemoteAddr()) c := newStreamConn(el, tc, nil) el.ch <- &openConn{c: c} goroutine.DefaultWorkerPool.Submit(func() { var buffer [0x10000]byte for { n, err := tc.Read(buffer[:]) if err != nil { el.ch <- &netErr{c, err} return } el.ch <- packTCPConn(c, buffer[:n]) } }) } } func (eng *engine) ListenUDP(pc net.PacketConn) (err error) { if eng.opts.LockOSThread { runtime.LockOSThread() defer runtime.UnlockOSThread() } defer func() { eng.shutdown(err) }() var buffer [0x10000]byte for { // Read data from UDP socket. n, addr, e := pc.ReadFrom(buffer[:]) if e != nil { err = e if !eng.beingShutdown.Load() { eng.opts.Logger.Errorf("failed to receive data from UDP fd due to error:%v", err) } else if errors.Is(err, net.ErrClosed) { err = errors.Join(err, errorx.ErrEngineShutdown) } return } el := eng.eventLoops.next(addr) c := newUDPConn(el, pc, nil, pc.LocalAddr(), addr, nil) el.ch <- packUDPConn(c, buffer[:n]) } } ================================================ FILE: client_test.go ================================================ //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd || windows package gnet import ( "bytes" crand "crypto/rand" "io" "math/rand" "net" "path/filepath" "sync" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "go.uber.org/zap" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" bbPool "github.com/panjf2000/gnet/v2/pkg/pool/bytebuffer" goPool "github.com/panjf2000/gnet/v2/pkg/pool/goroutine" ) type connHandler struct { network string rspCh chan []byte data []byte } type clientEvents struct { *BuiltinEventEngine tester *testing.T svr *testClient } func (ev *clientEvents) OnBoot(e Engine) Action { fd, err := e.Dup() assert.ErrorIsf(ev.tester, err, errorx.ErrEmptyEngine, "expected error: %v, but got: %v", errorx.ErrUnsupportedOp, err) assert.EqualValuesf(ev.tester, fd, -1, "expected -1, but got: %d", fd) fd, err = e.DupListener("tcp", "abc") assert.ErrorIsf(ev.tester, err, errorx.ErrEmptyEngine, "expected error: %v, but got: %v", errorx.ErrUnsupportedOp, err) assert.EqualValuesf(ev.tester, fd, -1, "expected -1, but got: %d", fd) return None } var pingMsg = []byte("PING\r\n") func (ev *clientEvents) OnOpen(Conn) (out []byte, action Action) { out = pingMsg return } func (ev *clientEvents) OnClose(Conn, error) Action { if ev.svr != nil { if atomic.AddInt32(&ev.svr.clientActive, -1) == 0 { return Shutdown } } return None } func (ev *clientEvents) OnTraffic(c Conn) (action Action) { handler := c.Context().(*connHandler) packetLen := streamLen if handler.network == "udp" { packetLen = datagramLen } buf, err := c.Next(-1) assert.NoError(ev.tester, err) handler.data = append(handler.data, buf...) if len(handler.data) < packetLen { return } handler.rspCh <- handler.data handler.data = nil return } func (ev *clientEvents) OnTick() (delay time.Duration, action Action) { delay = 200 * time.Millisecond return } func (ev *clientEvents) OnShutdown(e Engine) { fd, err := e.Dup() assert.ErrorIsf(ev.tester, err, errorx.ErrEmptyEngine, "expected error: %v, but got: %v", errorx.ErrUnsupportedOp, err) assert.EqualValuesf(ev.tester, fd, -1, "expected -1, but got: %d", fd) fd, err = e.DupListener("tcp", "abc") assert.ErrorIsf(ev.tester, err, errorx.ErrEmptyEngine, "expected error: %v, but got: %v", errorx.ErrUnsupportedOp, err) assert.EqualValuesf(ev.tester, fd, -1, "expected -1, but got: %d", fd) } func TestClient(t *testing.T) { // start an engine // connect 10 clients // each client will pipe random data for 1-3 seconds. // the writes to the engine will be random sizes. 0KB - 1MB. // the engine will echo back the data. // waits for graceful connection closing. t.Run("poll-LT", func(t *testing.T) { t.Run("tcp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "tcp", ":9991", &testConf{false, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "tcp", ":9992", &testConf{false, 0, false, true, false, false, 10, LeastConnections}) }) }) t.Run("tcp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "tcp", ":9991", &testConf{false, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "tcp", ":9992", &testConf{false, 0, false, true, true, false, 10, LeastConnections}) }) }) t.Run("udp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "udp", ":9991", &testConf{false, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "udp", ":9992", &testConf{false, 0, false, true, false, false, 10, LeastConnections}) }) }) t.Run("udp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "udp", ":9991", &testConf{false, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "udp", ":9992", &testConf{false, 0, false, true, true, false, 10, LeastConnections}) }) }) t.Run("unix", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{false, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{false, 0, false, true, false, false, 10, SourceAddrHash}) }) }) t.Run("unix-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{false, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{false, 0, false, true, true, false, 10, SourceAddrHash}) }) }) }) t.Run("poll-ET", func(t *testing.T) { t.Run("tcp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "tcp", ":9991", &testConf{true, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "tcp", ":9992", &testConf{true, 0, false, true, false, false, 10, LeastConnections}) }) }) t.Run("tcp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "tcp", ":9991", &testConf{true, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "tcp", ":9992", &testConf{true, 0, false, true, true, false, 10, LeastConnections}) }) }) t.Run("udp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "udp", ":9991", &testConf{true, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "udp", ":9992", &testConf{true, 0, false, true, false, false, 10, LeastConnections}) }) }) t.Run("udp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "udp", ":9991", &testConf{true, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "udp", ":9992", &testConf{true, 0, false, true, true, false, 10, LeastConnections}) }) }) t.Run("unix", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 0, false, true, false, false, 10, SourceAddrHash}) }) }) t.Run("unix-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 0, false, true, true, false, 10, SourceAddrHash}) }) }) }) t.Run("poll-ET-chunk", func(t *testing.T) { t.Run("tcp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "tcp", ":9991", &testConf{true, 1 << 18, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "tcp", ":9992", &testConf{true, 1 << 19, false, true, false, false, 10, LeastConnections}) }) }) t.Run("tcp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "tcp", ":9991", &testConf{true, 1 << 18, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "tcp", ":9992", &testConf{true, 1 << 19, false, true, true, false, 10, LeastConnections}) }) }) t.Run("udp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "udp", ":9991", &testConf{true, 1 << 18, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "udp", ":9992", &testConf{true, 1 << 19, false, true, false, false, 10, LeastConnections}) }) }) t.Run("udp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "udp", ":9991", &testConf{true, 1 << 18, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "udp", ":9992", &testConf{true, 1 << 19, false, true, true, false, 10, LeastConnections}) }) }) t.Run("unix", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 1 << 18, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 1 << 19, false, true, false, false, 10, SourceAddrHash}) }) }) t.Run("unix-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 1 << 18, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 1 << 19, false, true, true, false, 10, SourceAddrHash}) }) }) }) t.Run("poll-reuseport-LT", func(t *testing.T) { t.Run("tcp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "tcp", ":9991", &testConf{false, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "tcp", ":9992", &testConf{false, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("tcp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "tcp", ":9991", &testConf{false, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "tcp", ":9992", &testConf{false, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("udp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "udp", ":9991", &testConf{false, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "udp", ":9992", &testConf{false, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("udp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "udp", ":9991", &testConf{false, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "udp", ":9992", &testConf{false, 0, true, true, true, false, 10, LeastConnections}) }) }) t.Run("unix", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{false, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{false, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("unix-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{false, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{false, 0, true, true, true, false, 10, LeastConnections}) }) }) }) t.Run("poll-reuseport-ET", func(t *testing.T) { t.Run("tcp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "tcp", ":9991", &testConf{true, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "tcp", ":9992", &testConf{true, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("tcp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "tcp", ":9991", &testConf{true, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "tcp", ":9992", &testConf{true, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("udp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "udp", ":9991", &testConf{true, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "udp", ":9992", &testConf{true, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("udp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "udp", ":9991", &testConf{true, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "udp", ":9992", &testConf{true, 0, true, true, true, false, 10, LeastConnections}) }) }) t.Run("unix", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("unix-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runClient(t, "unix", testUnixAddr(t), &testConf{true, 0, true, true, true, false, 10, LeastConnections}) }) }) }) } type testClient struct { *BuiltinEventEngine client *Client tester *testing.T eng Engine network string addr string multicore bool async bool nclients int started int32 connected int32 clientActive int32 disconnected int32 udpReadHeader int32 } func (s *testClient) OnBoot(eng Engine) (action Action) { s.eng = eng return } func (s *testClient) OnOpen(c Conn) (out []byte, action Action) { c.SetContext(&sync.Once{}) atomic.AddInt32(&s.connected, 1) assert.NotNil(s.tester, c.LocalAddr(), "nil local addr") assert.NotNil(s.tester, c.RemoteAddr(), "nil remote addr") return } func (s *testClient) OnClose(c Conn, err error) (action Action) { if err != nil { logging.Debugf("error occurred on closed, %v\n", err) } if s.network != "udp" { assert.IsType(s.tester, c.Context(), new(sync.Once), "invalid context") } atomic.AddInt32(&s.disconnected, 1) if atomic.LoadInt32(&s.connected) == atomic.LoadInt32(&s.disconnected) && atomic.LoadInt32(&s.disconnected) == int32(s.nclients) { action = Shutdown } return } func (s *testClient) OnShutdown(Engine) { if s.network == "udp" { assert.EqualValues(s.tester, int32(s.nclients), atomic.LoadInt32(&s.udpReadHeader)) } } func (s *testClient) OnTraffic(c Conn) (action Action) { readHeader := func() { ping := make([]byte, len(pingMsg)) n, err := io.ReadFull(c, ping) assert.NoError(s.tester, err) assert.EqualValues(s.tester, len(pingMsg), n) assert.Equal(s.tester, string(pingMsg), string(ping), "bad header") } v := c.Context() if v != nil { v.(*sync.Once).Do(readHeader) } if s.async { buf := bbPool.Get() _, err := c.WriteTo(buf) assert.NoError(s.tester, err, "WriteTo error") if s.network == "tcp" || s.network == "unix" { // just for test _ = c.InboundBuffered() _ = c.OutboundBuffered() _, _ = c.Discard(1) } if v == nil && bytes.Equal(buf.Bytes(), pingMsg) { atomic.AddInt32(&s.udpReadHeader, 1) buf.Reset() } err = goPool.DefaultWorkerPool.Submit( func() { if buf.Len() > 0 { err := c.AsyncWrite(buf.Bytes(), nil) assert.NoError(s.tester, err) } }) assert.NoError(s.tester, err) return } buf, err := c.Next(-1) assert.NoError(s.tester, err, "Reading data error") if v == nil && bytes.Equal(buf, pingMsg) { atomic.AddInt32(&s.udpReadHeader, 1) buf = nil } if len(buf) > 0 { n, err := c.Write(buf) assert.NoError(s.tester, err) assert.EqualValues(s.tester, len(buf), n) } return } func (s *testClient) OnTick() (delay time.Duration, action Action) { delay = 100 * time.Millisecond if atomic.CompareAndSwapInt32(&s.started, 0, 1) { for i := 0; i < s.nclients; i++ { atomic.AddInt32(&s.clientActive, 1) var netConn bool if i%2 == 0 { netConn = true } err := goPool.DefaultWorkerPool.Submit( func() { startGnetClient(s.tester, s.client, s.network, s.addr, s.multicore, s.async, netConn) }) assert.NoError(s.tester, err) } } if s.network == "udp" && atomic.LoadInt32(&s.clientActive) == 0 { action = Shutdown return } return } func runClient(t *testing.T, network, addr string, conf *testConf) { ts := &testClient{ tester: t, network: network, addr: addr, multicore: conf.multicore, async: conf.async, nclients: conf.clients, } var err error clientEV := &clientEvents{tester: t, svr: ts} ts.client, err = NewClient( clientEV, WithMulticore(conf.multicore), WithEdgeTriggeredIO(conf.et), WithEdgeTriggeredIOChunk(conf.etChunk), WithTCPNoDelay(TCPNoDelay), WithLockOSThread(true), WithTicker(true), ) assert.NoError(t, err) err = ts.client.Start() assert.NoError(t, err) defer ts.client.Stop() //nolint:errcheck err = Run(ts, network+"://"+addr, WithEdgeTriggeredIO(conf.et), WithEdgeTriggeredIOChunk(conf.etChunk), WithLockOSThread(conf.async), WithMulticore(conf.multicore), WithReusePort(conf.reuseport), WithTicker(true), WithTCPKeepAlive(time.Minute), WithTCPKeepInterval(time.Second*10), WithTCPKeepCount(10), WithLoadBalancing(conf.lb)) assert.NoError(t, err) } func startGnetClient(t *testing.T, cli *Client, network, addr string, multicore, async, netDial bool) { var ( c Conn err error ) handler := &connHandler{ network: network, rspCh: make(chan []byte, 1), } if netDial { var netConn net.Conn netConn, err = net.Dial(network, addr) assert.NoError(t, err) c, err = cli.EnrollContext(netConn, handler) } else { c, err = cli.DialContext(network, addr, handler) } assert.NoError(t, err) defer c.Close() //nolint:errcheck err = c.Wake(nil) assert.NoError(t, err) rspCh := handler.rspCh duration := time.Duration((rand.Float64()*2+1)*float64(time.Second)) / 2 logging.Debugf("test duration: %v", duration) start := time.Now() for time.Since(start) < duration { reqData := make([]byte, streamLen) if network == "udp" { reqData = reqData[:datagramLen] } _, err = crand.Read(reqData) assert.NoError(t, err) err = c.AsyncWrite(reqData, nil) assert.NoError(t, err) respData := <-rspCh assert.NoError(t, err) if !async { // assert.Equalf(t, reqData, respData, "response mismatch with protocol:%s, multi-core:%t, content of bytes: %d vs %d", network, multicore, string(reqData), string(respData)) assert.Equalf( t, reqData, respData, "response mismatch with protocol:%s, multi-core:%t, length of bytes: %d vs %d", network, multicore, len(reqData), len(respData), ) } } } type clientEventsForWake struct { BuiltinEventEngine tester *testing.T ch chan struct{} } func (ev *clientEventsForWake) OnBoot(_ Engine) Action { ev.ch = make(chan struct{}) return None } func (ev *clientEventsForWake) OnTraffic(c Conn) (action Action) { n, err := c.Read(nil) assert.Zerof(ev.tester, n, "expected: %v, but got: %v", 0, n) assert.NoErrorf(ev.tester, err, "expected: %v, but got: %v", nil, err) buf := make([]byte, 10) n, err = c.Read(buf) assert.Zerof(ev.tester, n, "expected: %v, but got: %v", 0, n) assert.ErrorIsf(ev.tester, err, io.ErrShortBuffer, "expected error: %v, but got: %v", io.ErrShortBuffer, err) buf, err = c.Next(10) assert.Nilf(ev.tester, buf, "expected: %v, but got: %v", nil, buf) assert.ErrorIsf(ev.tester, err, io.ErrShortBuffer, "expected error: %v, but got: %v", io.ErrShortBuffer, err) buf, err = c.Next(-1) assert.Emptyf(ev.tester, buf, "expected an empty slice, but got: %v", buf) assert.NoErrorf(ev.tester, err, "expected: %v, but got: %v", nil, err) buf, err = c.Peek(10) assert.Nilf(ev.tester, buf, "expected: %v, but got: %v", nil, buf) assert.ErrorIsf(ev.tester, err, io.ErrShortBuffer, "expected error: %v, but got: %v", io.ErrShortBuffer, err) buf, err = c.Peek(-1) assert.Emptyf(ev.tester, buf, "expected an empty slice, but got: %v", buf) assert.NoErrorf(ev.tester, err, "expected: %v, but got: %v", nil, err) n, err = c.Discard(10) assert.Zerof(ev.tester, n, "expected: %v, but got: %v", 0, n) assert.NoErrorf(ev.tester, err, "expected: %v, but got: %v", nil, err) n, err = c.Discard(-1) assert.Zerof(ev.tester, n, "expected: %v, but got: %v", 0, n) assert.NoErrorf(ev.tester, err, "expected: %v, but got: %v", nil, err) m, err := c.WriteTo(io.Discard) assert.Zerof(ev.tester, n, "expected: %v, but got: %v", 0, m) assert.NoErrorf(ev.tester, err, "expected: %v, but got: %v", nil, err) n = c.InboundBuffered() assert.Zerof(ev.tester, n, "expected: %v, but got: %v", 0, m) <-ev.ch return None } type serverEventsForWake struct { BuiltinEventEngine network, addr string client *Client clientEV *clientEventsForWake tester *testing.T clients int32 started int32 } func (ev *serverEventsForWake) OnOpen(_ Conn) ([]byte, Action) { atomic.AddInt32(&ev.clients, 1) return nil, None } func (ev *serverEventsForWake) OnClose(_ Conn, _ error) Action { if atomic.AddInt32(&ev.clients, -1) == 0 { return Shutdown } return None } func (ev *serverEventsForWake) OnTick() (time.Duration, Action) { if atomic.CompareAndSwapInt32(&ev.started, 0, 1) { err := goPool.DefaultWorkerPool.Submit(func() { testConnWakeImmediately(ev.tester, ev.client, ev.clientEV, ev.network, ev.addr) }) assert.NoError(ev.tester, err) } return 100 * time.Millisecond, None } func testConnWakeImmediately(t *testing.T, client *Client, clientEV *clientEventsForWake, network, addr string) { c, err := client.Dial(network, addr) assert.NoErrorf(t, err, "failed to dial: %v", err) err = c.Wake(nil) assert.NoError(t, err) err = c.Close() assert.NoError(t, err) clientEV.ch <- struct{}{} } func TestWakeConnImmediately(t *testing.T) { currentLogger, currentFlusher := logging.GetDefaultLogger(), logging.GetDefaultFlusher() t.Cleanup(func() { logging.SetDefaultLoggerAndFlusher(currentLogger, currentFlusher) // restore }) clientEV := &clientEventsForWake{tester: t} logPath := filepath.Join(t.TempDir(), "gnet-test-wake-conn-immediately.log") client, err := NewClient(clientEV, WithSocketRecvBuffer(4*1024), WithSocketSendBuffer(4*1024), WithLogPath(logPath), WithLogLevel(logging.WarnLevel), WithReadBufferCap(512), WithWriteBufferCap(512)) assert.NoError(t, err) logging.Cleanup() err = client.Start() assert.NoError(t, err) defer client.Stop() //nolint:errcheck serverEV := &serverEventsForWake{tester: t, network: "tcp", addr: ":18888", client: client, clientEV: clientEV} err = Run(serverEV, serverEV.network+"://"+serverEV.addr, WithTicker(true)) assert.NoError(t, err) } func TestClientReadOnEOF(t *testing.T) { currentLogger, currentFlusher := logging.GetDefaultLogger(), logging.GetDefaultFlusher() t.Cleanup(func() { logging.SetDefaultLoggerAndFlusher(currentLogger, currentFlusher) // restore }) ln, err := net.Listen("tcp", "127.0.0.1:9999") assert.NoError(t, err) defer ln.Close() //nolint:errcheck err = goPool.DefaultWorkerPool.Submit(func() { for { conn, err := ln.Accept() if err != nil { break } err = goPool.DefaultWorkerPool.Submit(func() { process(conn) }) assert.NoError(t, err) } }) assert.NoError(t, err) ev := &clientReadOnEOF{ result: make(chan struct { data []byte err error }, 1), data: []byte("test"), } cli, err := NewClient(ev, WithSocketRecvBuffer(4*1024), WithSocketSendBuffer(4*1024), WithTCPKeepAlive(time.Minute), WithLogger(zap.NewExample().Sugar()), WithReadBufferCap(32*1024), WithWriteBufferCap(32*1024)) assert.NoError(t, err) defer cli.Stop() //nolint:errcheck err = cli.Start() assert.NoError(t, err) _, err = cli.Dial("tcp", "127.0.0.1:9999") assert.NoError(t, err) select { case res := <-ev.result: assert.NoError(t, res.err) assert.EqualValuesf(t, ev.data, res.data, "expected: %v, but got: %v", ev.data, res.data) case <-time.After(5 * time.Second): t.Errorf("timeout waiting for the result") } } func process(conn net.Conn) { defer conn.Close() //nolint:errcheck buf := make([]byte, 8) n, err := conn.Read(buf) if err != nil { return } _, _ = conn.Write(buf[:n]) _ = conn.Close() } type clientReadOnEOF struct { BuiltinEventEngine data []byte result chan struct { data []byte err error } } func (clientReadOnEOF) OnBoot(Engine) (action Action) { return None } func (cli clientReadOnEOF) OnOpen(Conn) (out []byte, action Action) { return cli.data, None } func (clientReadOnEOF) OnClose(Conn, error) (action Action) { return Close } func (cli clientReadOnEOF) OnTraffic(c Conn) (action Action) { data, err := c.Next(-1) cli.result <- struct { data []byte err error }{data: data, err: err} return None } ================================================ FILE: client_unix.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package gnet import ( "context" "errors" "net" "strconv" "syscall" "golang.org/x/sync/errgroup" "golang.org/x/sys/unix" "github.com/panjf2000/gnet/v2/pkg/buffer/ring" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" "github.com/panjf2000/gnet/v2/pkg/math" "github.com/panjf2000/gnet/v2/pkg/netpoll" "github.com/panjf2000/gnet/v2/pkg/queue" "github.com/panjf2000/gnet/v2/pkg/socket" ) // Client of gnet. type Client struct { opts *Options eng *engine } // NewClient creates an instance of Client. func NewClient(eh EventHandler, opts ...Option) (cli *Client, err error) { options := loadOptions(opts...) cli = new(Client) cli.opts = options logger, logFlusher := logging.GetDefaultLogger(), logging.GetDefaultFlusher() if options.Logger == nil { if options.LogPath != "" { logger, logFlusher, _ = logging.CreateLoggerAsLocalFile(options.LogPath, options.LogLevel) } options.Logger = logger } else { logger = options.Logger logFlusher = nil } logging.SetDefaultLoggerAndFlusher(logger, logFlusher) rootCtx, shutdown := context.WithCancel(context.Background()) eg, ctx := errgroup.WithContext(rootCtx) eng := engine{ listeners: make(map[int]*listener), opts: options, turnOff: shutdown, eventHandler: eh, eventLoops: new(leastConnectionsLoadBalancer), concurrency: struct { *errgroup.Group ctx context.Context }{eg, ctx}, } if options.EdgeTriggeredIOChunk > 0 { options.EdgeTriggeredIO = true options.EdgeTriggeredIOChunk = math.CeilToPowerOfTwo(options.EdgeTriggeredIOChunk) } else if options.EdgeTriggeredIO { options.EdgeTriggeredIOChunk = 1 << 20 // 1MB } rbc := options.ReadBufferCap switch { case rbc <= 0: options.ReadBufferCap = MaxStreamBufferCap case rbc <= ring.DefaultBufferSize: options.ReadBufferCap = ring.DefaultBufferSize default: options.ReadBufferCap = math.CeilToPowerOfTwo(rbc) } wbc := options.WriteBufferCap switch { case wbc <= 0: options.WriteBufferCap = MaxStreamBufferCap case wbc <= ring.DefaultBufferSize: options.WriteBufferCap = ring.DefaultBufferSize default: options.WriteBufferCap = math.CeilToPowerOfTwo(wbc) } cli.eng = &eng return } // Start starts the client event-loop, handing IO events. func (cli *Client) Start() error { numEventLoop := determineEventLoops(cli.opts) logging.Infof("Starting gnet client with %d event loops", numEventLoop) cli.eng.eventHandler.OnBoot(Engine{cli.eng}) var el0 *eventloop for i := 0; i < numEventLoop; i++ { p, err := netpoll.OpenPoller() if err != nil { cli.eng.closeEventLoops() return err } el := eventloop{ listeners: cli.eng.listeners, engine: cli.eng, poller: p, buffer: make([]byte, cli.opts.ReadBufferCap), eventHandler: cli.eng.eventHandler, } el.connections.init() cli.eng.eventLoops.register(&el) if cli.opts.Ticker && el.idx == 0 { el0 = &el } } cli.eng.eventLoops.iterate(func(_ int, el *eventloop) bool { cli.eng.concurrency.Go(el.run) return true }) // Start the ticker. if el0 != nil { ctx := cli.eng.concurrency.ctx cli.eng.concurrency.Go(func() error { el0.ticker(ctx) return nil }) } logging.Debugf("default logging level is %s", logging.LogLevel()) return nil } // Stop stops the client event-loop. func (cli *Client) Stop() error { cli.eng.shutdown(nil) cli.eng.eventHandler.OnShutdown(Engine{cli.eng}) // Notify all event-loops to exit. cli.eng.eventLoops.iterate(func(_ int, el *eventloop) bool { logging.Error(el.poller.Trigger(queue.HighPriority, func(_ any) error { return errorx.ErrEngineShutdown }, nil)) return true }) // Wait for all event-loops to exit. err := cli.eng.concurrency.Wait() cli.eng.closeEventLoops() // Put the engine into the shutdown state. cli.eng.inShutdown.Store(true) // Flush the logger. logging.Cleanup() return err } // Dial is like net.Dial(). func (cli *Client) Dial(network, address string) (Conn, error) { return cli.DialContext(network, address, nil) } // DialContext is like Dial but also accepts an empty interface ctx that can be obtained later via Conn.Context. func (cli *Client) DialContext(network, address string, ctx any) (Conn, error) { c, err := net.Dial(network, address) if err != nil { return nil, err } return cli.EnrollContext(c, ctx) } // Enroll converts a net.Conn to gnet.Conn and then adds it into the Client. func (cli *Client) Enroll(c net.Conn) (Conn, error) { return cli.EnrollContext(c, nil) } // EnrollContext is like Enroll but also accepts an empty interface ctx that can be obtained later via Conn.Context. func (cli *Client) EnrollContext(c net.Conn, ctx any) (Conn, error) { defer c.Close() //nolint:errcheck sc, ok := c.(syscall.Conn) if !ok { return nil, errors.New("failed to convert net.Conn to syscall.Conn") } rc, err := sc.SyscallConn() if err != nil { return nil, errors.New("failed to get syscall.RawConn from net.Conn") } var dupFD int e := rc.Control(func(fd uintptr) { dupFD, err = socket.Dup(int(fd)) }) if err != nil { return nil, err } if e != nil { return nil, e } if cli.opts.SocketSendBuffer > 0 { if err = socket.SetSendBuffer(dupFD, cli.opts.SocketSendBuffer); err != nil { return nil, err } } if cli.opts.SocketRecvBuffer > 0 { if err = socket.SetRecvBuffer(dupFD, cli.opts.SocketRecvBuffer); err != nil { return nil, err } } el := cli.eng.eventLoops.next(nil) var ( sockAddr unix.Sockaddr gc *conn ) switch c.(type) { case *net.UnixConn: sockAddr, _, _, err = socket.GetUnixSockAddr(c.RemoteAddr().Network(), c.RemoteAddr().String()) if err != nil { return nil, err } ua := c.LocalAddr().(*net.UnixAddr) ua.Name = c.RemoteAddr().String() + "." + strconv.Itoa(dupFD) gc = newStreamConn("unix", dupFD, el, sockAddr, c.LocalAddr(), c.RemoteAddr()) case *net.TCPConn: if cli.opts.TCPNoDelay == TCPNoDelay { if err = socket.SetNoDelay(dupFD, 1); err != nil { return nil, err } } if cli.opts.TCPKeepAlive > 0 { if err = setKeepAlive( dupFD, true, cli.opts.TCPKeepAlive, cli.opts.TCPKeepInterval, cli.opts.TCPKeepCount); err != nil { return nil, err } } sockAddr, _, _, _, err = socket.GetTCPSockAddr(c.RemoteAddr().Network(), c.RemoteAddr().String()) if err != nil { return nil, err } gc = newStreamConn("tcp", dupFD, el, sockAddr, c.LocalAddr(), c.RemoteAddr()) case *net.UDPConn: sockAddr, _, _, _, err = socket.GetUDPSockAddr(c.RemoteAddr().Network(), c.RemoteAddr().String()) if err != nil { return nil, err } gc = newUDPConn(dupFD, el, c.LocalAddr(), sockAddr, true) default: return nil, errorx.ErrUnsupportedProtocol } gc.ctx = ctx connOpened := make(chan struct{}) ccb := &connWithCallback{c: gc, cb: func() { close(connOpened) }} err = el.poller.Trigger(queue.HighPriority, el.register, ccb) if err != nil { gc.Close() //nolint:errcheck return nil, err } <-connOpened return gc, nil } ================================================ FILE: client_windows.go ================================================ // Copyright (c) 2023 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gnet import ( "context" "net" "golang.org/x/sync/errgroup" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" "github.com/panjf2000/gnet/v2/pkg/pool/goroutine" ) type Client struct { opts *Options eng *engine } func NewClient(eh EventHandler, opts ...Option) (cli *Client, err error) { options := loadOptions(opts...) cli = &Client{opts: options} logger, logFlusher := logging.GetDefaultLogger(), logging.GetDefaultFlusher() if options.Logger == nil { if options.LogPath != "" { logger, logFlusher, _ = logging.CreateLoggerAsLocalFile(options.LogPath, options.LogLevel) } options.Logger = logger } else { logger = options.Logger logFlusher = nil } logging.SetDefaultLoggerAndFlusher(logger, logFlusher) rootCtx, shutdown := context.WithCancel(context.Background()) eg, ctx := errgroup.WithContext(rootCtx) eng := engine{ listeners: []*listener{}, opts: options, turnOff: shutdown, eventHandler: eh, eventLoops: new(leastConnectionsLoadBalancer), concurrency: struct { *errgroup.Group ctx context.Context }{eg, ctx}, } cli.eng = &eng return } func (cli *Client) Start() error { numEventLoop := determineEventLoops(cli.opts) logging.Infof("Starting gnet client with %d event loops", numEventLoop) cli.eng.eventHandler.OnBoot(Engine{cli.eng}) var el0 *eventloop for i := 0; i < numEventLoop; i++ { el := eventloop{ ch: make(chan any, 1024), eng: cli.eng, connections: make(map[*conn]struct{}), eventHandler: cli.eng.eventHandler, } cli.eng.eventLoops.register(&el) cli.eng.concurrency.Go(el.run) if cli.opts.Ticker && el.idx == 0 { el0 = &el } } if el0 != nil { ctx := cli.eng.concurrency.ctx cli.eng.concurrency.Go(func() error { el0.ticker(ctx) return nil }) } logging.Debugf("default logging level is %s", logging.LogLevel()) return nil } func (cli *Client) Stop() error { cli.eng.shutdown(nil) cli.eng.eventHandler.OnShutdown(Engine{cli.eng}) // Notify all event-loops to exit. cli.eng.closeEventLoops() // Wait for all event-loops to exit. err := cli.eng.concurrency.Wait() // Put the engine into the shutdown state. cli.eng.inShutdown.Store(true) // Flush the logger. logging.Cleanup() return err } func (cli *Client) Dial(network, addr string) (Conn, error) { return cli.DialContext(network, addr, nil) } func (cli *Client) DialContext(network, addr string, ctx any) (Conn, error) { var ( c net.Conn err error ) c, err = net.Dial(network, addr) if err != nil { return nil, err } return cli.EnrollContext(c, ctx) } func (cli *Client) Enroll(nc net.Conn) (gc Conn, err error) { return cli.EnrollContext(nc, nil) } func (cli *Client) EnrollContext(nc net.Conn, ctx any) (gc Conn, err error) { el := cli.eng.eventLoops.next(nil) connOpened := make(chan struct{}) switch v := nc.(type) { case *net.TCPConn: if cli.opts.TCPNoDelay == TCPNoDelay { if err = v.SetNoDelay(true); err != nil { return } } c := newStreamConn(el, nc, ctx) if opts := cli.opts; opts.TCPKeepAlive > 0 { idle := opts.TCPKeepAlive intvl := opts.TCPKeepInterval if intvl == 0 { intvl = opts.TCPKeepAlive / 5 } cnt := opts.TCPKeepCount if opts.TCPKeepCount == 0 { cnt = 5 } if err = c.SetKeepAlive(true, idle, intvl, cnt); err != nil { return } } el.ch <- &openConn{c: c, cb: func() { close(connOpened) }} goroutine.DefaultWorkerPool.Submit(func() { var buffer [0x10000]byte for { n, err := nc.Read(buffer[:]) if err != nil { el.ch <- &netErr{c, err} return } el.ch <- packTCPConn(c, buffer[:n]) } }) gc = c case *net.UnixConn: c := newStreamConn(el, nc, ctx) el.ch <- &openConn{c: c, cb: func() { close(connOpened) }} goroutine.DefaultWorkerPool.Submit(func() { var buffer [0x10000]byte for { n, err := nc.Read(buffer[:]) if err != nil { el.ch <- &netErr{c, err} return } el.ch <- packTCPConn(c, buffer[:n]) } }) gc = c case *net.UDPConn: c := newUDPConn(el, nil, nc, nc.LocalAddr(), nc.RemoteAddr(), ctx) el.ch <- &openConn{c: c, cb: func() { close(connOpened) }} goroutine.DefaultWorkerPool.Submit(func() { var buffer [0x10000]byte for { n, err := nc.Read(buffer[:]) if err != nil { el.ch <- &netErr{c, err} return } c := newUDPConn(el, nil, nc, nc.LocalAddr(), nc.RemoteAddr(), ctx) el.ch <- packUDPConn(c, buffer[:n]) } }) gc = c default: return nil, errorx.ErrUnsupportedProtocol } <-connOpened return } ================================================ FILE: conn_map.go ================================================ // Copyright (c) 2023 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build (darwin || dragonfly || freebsd || linux || netbsd || openbsd) && !gc_opt package gnet import ( "sync/atomic" "github.com/panjf2000/gnet/v2/internal/gfd" ) type connMatrix struct { connCount int32 connMap map[int]*conn } func (cm *connMatrix) init() { cm.connMap = make(map[int]*conn) } func (cm *connMatrix) iterate(f func(*conn) bool) { for _, c := range cm.connMap { if c != nil { if !f(c) { return } } } } func (cm *connMatrix) incCount(_ int, delta int32) { atomic.AddInt32(&cm.connCount, delta) } func (cm *connMatrix) loadCount() (n int32) { return atomic.LoadInt32(&cm.connCount) } func (cm *connMatrix) addConn(c *conn, index int) { c.gfd = gfd.NewGFD(c.fd, index, 0, 0) cm.connMap[c.fd] = c cm.incCount(0, 1) } func (cm *connMatrix) delConn(c *conn) { delete(cm.connMap, c.fd) cm.incCount(0, -1) } func (cm *connMatrix) getConn(fd int) *conn { return cm.connMap[fd] } /* func (cm *connMatrix) getConnByGFD(fd gfd.GFD) *conn { return cm.connMap[fd.Fd()] } */ ================================================ FILE: conn_matrix.go ================================================ // Copyright (c) 2023 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build (darwin || dragonfly || freebsd || linux || netbsd || openbsd) && gc_opt package gnet import ( "sync/atomic" "github.com/panjf2000/gnet/v2/internal/gfd" ) type connMatrix struct { disableCompact bool // disable compaction when it is true connCounts [gfd.ConnMatrixRowMax]int32 // number of active connections in event-loop row int // next available row index column int // next available column index table [gfd.ConnMatrixRowMax][]*conn // connection matrix of *conn, multiple slices fd2gfd map[int]gfd.GFD // fd -> gfd.GFD } func (cm *connMatrix) init() { cm.fd2gfd = make(map[int]gfd.GFD) } func (cm *connMatrix) iterate(f func(*conn) bool) { cm.disableCompact = true defer func() { cm.disableCompact = false }() for _, conns := range cm.table { for _, c := range conns { if c != nil { if !f(c) { return } } } } } func (cm *connMatrix) incCount(row int, delta int32) { atomic.AddInt32(&cm.connCounts[row], delta) } func (cm *connMatrix) loadCount() (n int32) { for i := 0; i < len(cm.connCounts); i++ { n += atomic.LoadInt32(&cm.connCounts[i]) } return } func (cm *connMatrix) addConn(c *conn, index int) { if cm.row >= gfd.ConnMatrixRowMax { return } if cm.table[cm.row] == nil { cm.table[cm.row] = make([]*conn, gfd.ConnMatrixColumnMax) } c.gfd = gfd.NewGFD(c.fd, index, cm.row, cm.column) cm.fd2gfd[c.fd] = c.gfd cm.table[cm.row][cm.column] = c cm.incCount(cm.row, 1) if cm.column++; cm.column == gfd.ConnMatrixColumnMax { cm.row++ cm.column = 0 } } func (cm *connMatrix) delConn(c *conn) { cfd, cgfd := c.fd, c.gfd delete(cm.fd2gfd, cfd) cm.incCount(cgfd.ConnMatrixRow(), -1) if cm.connCounts[cgfd.ConnMatrixRow()] == 0 { cm.table[cgfd.ConnMatrixRow()] = nil } else { cm.table[cgfd.ConnMatrixRow()][cgfd.ConnMatrixColumn()] = nil } if cm.row > cgfd.ConnMatrixRow() || cm.column > cgfd.ConnMatrixColumn() { cm.row, cm.column = cgfd.ConnMatrixRow(), cgfd.ConnMatrixColumn() } // Locate the last *conn in table and move it to the deleted location. if cm.disableCompact || cm.table[cgfd.ConnMatrixRow()] == nil { // the deleted *conn is the last one, do nothing here. return } // Traverse backward to find the first non-empty point in the matrix until we reach the deleted position. for row := gfd.ConnMatrixRowMax - 1; row >= cgfd.ConnMatrixRow(); row-- { if cm.connCounts[row] == 0 { continue } columnMin := -1 if row == cgfd.ConnMatrixRow() { columnMin = cgfd.ConnMatrixColumn() } for column := gfd.ConnMatrixColumnMax - 1; column > columnMin; column-- { if cm.table[row][column] == nil { continue } gFd := cm.table[row][column].gfd gFd.UpdateIndexes(cgfd.ConnMatrixRow(), cgfd.ConnMatrixColumn()) cm.table[row][column].gfd = gFd cm.fd2gfd[gFd.Fd()] = gFd cm.table[cgfd.ConnMatrixRow()][cgfd.ConnMatrixColumn()] = cm.table[row][column] cm.incCount(row, -1) cm.incCount(cgfd.ConnMatrixRow(), 1) if cm.connCounts[row] == 0 { cm.table[row] = nil } else { cm.table[row][column] = nil } cm.row, cm.column = row, column return } } } func (cm *connMatrix) getConn(fd int) *conn { gFD, ok := cm.fd2gfd[fd] if !ok { return nil } if cm.table[gFD.ConnMatrixRow()] == nil { return nil } return cm.table[gFD.ConnMatrixRow()][gFD.ConnMatrixColumn()] } /* func (cm *connMatrix) getConnByGFD(fd gfd.GFD) *conn { if cm.table[fd.ConnMatrixRow()] == nil { return nil } return cm.table[fd.ConnMatrixRow()][fd.ConnMatrixColumn()] } */ ================================================ FILE: conn_matrix_test.go ================================================ //go:build (darwin || dragonfly || freebsd || linux || netbsd || openbsd) && gc_opt package gnet import ( "net" "testing" "github.com/stretchr/testify/require" "golang.org/x/sys/unix" goPool "github.com/panjf2000/gnet/v2/pkg/pool/goroutine" ) var testVastConns = false func TestConnMatrix(t *testing.T) { t.Run("1k-connections", func(t *testing.T) { testConnMatrix(t, 1000) }) t.Run("10k-connections", func(t *testing.T) { testConnMatrix(t, 10000) }) t.Run("100k-connections", func(t *testing.T) { if !testVastConns { t.Skip("skipped because testVastConns is set to false") } testConnMatrix(t, 100000) }) t.Run("1m-connections", func(t *testing.T) { if !testVastConns { t.Skip("skipped because testVastConns is set to false") } testConnMatrix(t, 1000000) }) } const ( actionAdd = iota + 1 actionDel ) type handleConn struct { c *conn action int } func testConnMatrix(t *testing.T, n int) { handleConns := make(chan *handleConn, 1024) connections := connMatrix{} connections.init() el := eventloop{engine: &engine{opts: &Options{}}} done := make(chan struct{}) err := goPool.DefaultWorkerPool.Submit(func() { for i := 0; i < n+n/2; i++ { v := <-handleConns switch v.action { case actionAdd: connections.addConn(v.c, 0) case actionDel: connections.delConn(v.c) } } close(done) }) require.NoError(t, err) for i := 0; i < n; i++ { c := newStreamConn("tcp", i, &el, &unix.SockaddrInet4{}, &net.TCPAddr{}, &net.TCPAddr{}) handleConns <- &handleConn{c, actionAdd} if i%2 == 0 { _ = goPool.DefaultWorkerPool.Submit(func() { handleConns <- &handleConn{c, actionDel} }) } } m := n / 2 <-done if count := connections.loadCount(); count != int32(n)/2 { t.Fatalf("unexpected conn count %d, expected %d", count, int32(n)/2) } for i := 0; i < len(connections.table); i++ { if connections.connCounts[i] == 0 { continue } for j := 0; j < len(connections.table[i]) && m > 0; j++ { m-- c := connections.table[i][j] if c == nil { t.Fatalf("unexpected nil connection at row %d, column %d", i, j) } if c.fd != c.gfd.Fd() { t.Fatalf("unexpected fd %d, expected fd %d", c.gfd.Fd(), c.fd) } if i != c.gfd.ConnMatrixRow() || j != c.gfd.ConnMatrixColumn() { t.Fatalf("unexpected row %d, column %d, expected row %d, column %d", c.gfd.ConnMatrixRow(), c.gfd.ConnMatrixColumn(), i, j) } gfd, ok := connections.fd2gfd[c.fd] if !ok { t.Fatalf("missing gfd for fd %d", c.fd) } if gfd != c.gfd { t.Fatalf("expected gfd: %v, but got gfd: %v", c.gfd, gfd) } } } t.Log("connMatrix remains compact after many additions and deletions, test done!") } ================================================ FILE: connection_bsd.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || netbsd || openbsd package gnet import ( "io" "golang.org/x/sys/unix" "github.com/panjf2000/gnet/v2/pkg/netpoll" ) func (c *conn) processIO(_ int, filter netpoll.IOEvent, flags netpoll.IOFlags) (err error) { el := c.loop switch filter { case unix.EVFILT_READ: err = el.read(c) case unix.EVFILT_WRITE: err = el.write(c) } // EV_EOF indicates that the remote has closed the connection. // We check for EV_EOF after processing the read/write event // to ensure that nothing is left out on this event filter. if flags&unix.EV_EOF != 0 && c.opened && err == nil { switch filter { case unix.EVFILT_READ: // Received the event of EVFILT_READ|EV_EOF, but the previous eventloop.read // failed to drain the socket buffer, so we make sure we get it done this time. c.isEOF = true err = el.read(c) case unix.EVFILT_WRITE: // On macOS, the kqueue in either LT or ET mode will notify with one event for the // EOF of the TCP remote: EVFILT_READ|EV_ADD|EV_CLEAR|EV_EOF. But for some reason, // two events will be issued in ET mode for the EOF of the Unix remote in this order: // 1) EVFILT_WRITE|EV_ADD|EV_CLEAR|EV_EOF, 2) EVFILT_READ|EV_ADD|EV_CLEAR|EV_EOF. err = el.write(c) default: c.outboundBuffer.Release() // don't bother to write to a connection that is already broken err = el.close(c, io.EOF) } } return } ================================================ FILE: connection_linux.go ================================================ /* * Copyright (c) 2021 The Gnet Authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package gnet import ( "io" "golang.org/x/sys/unix" "github.com/panjf2000/gnet/v2/pkg/netpoll" ) func (c *conn) processIO(_ int, ev netpoll.IOEvent, _ netpoll.IOFlags) error { el := c.loop // First check for any unexpected non-IO events. // For these events we just close the connection directly. if ev&(netpoll.ErrEvents|unix.EPOLLRDHUP) != 0 && ev&netpoll.ReadWriteEvents == 0 { c.outboundBuffer.Release() // don't bother to write to a connection that is already broken return el.close(c, io.EOF) } // Secondly, check for EPOLLOUT before EPOLLIN, the former has a higher priority // than the latter regardless of the aliveness of the current connection: // // 1. When the connection is alive and the system is overloaded, we want to // offload the incoming traffic by writing all pending data back to the remotes // before continuing to read and handle requests. // 2. When the connection is dead, we need to try writing any pending data back // to the remote first and then close the connection. // // We perform eventloop.write for EPOLLOUT because it can take good care of either case. if ev&(netpoll.WriteEvents|netpoll.ErrEvents) != 0 { if err := el.write(c); err != nil { return err } } // Check for EPOLLIN before EPOLLRDHUP in case that there are pending data in // the socket buffer. if ev&(netpoll.ReadEvents|netpoll.ErrEvents) != 0 { if err := el.read(c); err != nil { return err } } // Ultimately, check for EPOLLRDHUP, this event indicates that the remote has // either closed connection or shut down the writing half of the connection. if ev&unix.EPOLLRDHUP != 0 && c.opened { if ev&unix.EPOLLIN == 0 { // unreadable EPOLLRDHUP, close the connection directly return el.close(c, io.EOF) } // Received the event of EPOLLIN|EPOLLRDHUP, but the previous eventloop.read // failed to drain the socket buffer, so we ensure to get it done this time. c.isEOF = true return el.read(c) } return nil } ================================================ FILE: connection_unix.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package gnet import ( "io" "net" "os" "time" "golang.org/x/sys/unix" "github.com/panjf2000/gnet/v2/internal/gfd" "github.com/panjf2000/gnet/v2/pkg/bs" "github.com/panjf2000/gnet/v2/pkg/buffer/elastic" errorx "github.com/panjf2000/gnet/v2/pkg/errors" gio "github.com/panjf2000/gnet/v2/pkg/io" "github.com/panjf2000/gnet/v2/pkg/netpoll" bsPool "github.com/panjf2000/gnet/v2/pkg/pool/byteslice" "github.com/panjf2000/gnet/v2/pkg/queue" "github.com/panjf2000/gnet/v2/pkg/socket" ) type conn struct { fd int // file descriptor gfd gfd.GFD // gnet file descriptor ctx any // user-defined context remote unix.Sockaddr // remote socket address proto string // protocol name: "tcp", "udp", or "unix". localAddr net.Addr // local addr remoteAddr net.Addr // remote addr loop *eventloop // connected event-loop outboundBuffer elastic.Buffer // buffer for data that is eligible to be sent to the remote pollAttachment netpoll.PollAttachment // connection attachment for poller inboundBuffer elastic.RingBuffer // buffer for leftover data from the remote buffer []byte // buffer for the latest bytes cache []byte // temporary cache for the inbound data isDatagram bool // UDP protocol opened bool // connection opened event fired isEOF bool // whether the connection has reached EOF } func newStreamConn(proto string, fd int, el *eventloop, sa unix.Sockaddr, localAddr, remoteAddr net.Addr) (c *conn) { c = &conn{ fd: fd, proto: proto, remote: sa, loop: el, localAddr: localAddr, remoteAddr: remoteAddr, pollAttachment: netpoll.PollAttachment{FD: fd}, } c.pollAttachment.Callback = c.processIO c.outboundBuffer.Reset(el.engine.opts.WriteBufferCap) return } func newUDPConn(fd int, el *eventloop, localAddr net.Addr, sa unix.Sockaddr, connected bool) (c *conn) { c = &conn{ fd: fd, proto: "udp", gfd: gfd.NewGFD(fd, el.idx, 0, 0), remote: sa, loop: el, localAddr: localAddr, remoteAddr: socket.SockaddrToUDPAddr(sa), isDatagram: true, pollAttachment: netpoll.PollAttachment{FD: fd, Callback: el.readUDP}, } if connected { c.remote = nil } return } func (c *conn) release() { c.opened = false c.isEOF = false c.ctx = nil c.buffer = nil if addr, ok := c.localAddr.(*net.TCPAddr); ok && len(c.loop.listeners) == 0 && len(addr.Zone) > 0 { bsPool.Put(bs.StringToBytes(addr.Zone)) } if addr, ok := c.remoteAddr.(*net.TCPAddr); ok && len(addr.Zone) > 0 { bsPool.Put(bs.StringToBytes(addr.Zone)) } if addr, ok := c.localAddr.(*net.UDPAddr); ok && len(c.loop.listeners) == 0 && len(addr.Zone) > 0 { bsPool.Put(bs.StringToBytes(addr.Zone)) } if addr, ok := c.remoteAddr.(*net.UDPAddr); ok && len(addr.Zone) > 0 { bsPool.Put(bs.StringToBytes(addr.Zone)) } c.localAddr = nil c.remoteAddr = nil if !c.isDatagram { c.remote = nil c.inboundBuffer.Done() c.outboundBuffer.Release() } } func (c *conn) open(buf []byte) error { if c.isDatagram && c.remote == nil { return unix.Send(c.fd, buf, 0) } for { n, err := unix.Write(c.fd, buf) if err != nil { if err == unix.EAGAIN { _, _ = c.outboundBuffer.Write(buf) break } return err } buf = buf[n:] if len(buf) == 0 { break } } return nil } func (c *conn) write(data []byte) (n int, err error) { isET := c.loop.engine.opts.EdgeTriggeredIO n = len(data) // If there is pending data in outbound buffer, // the current data ought to be appended to the // outbound buffer for maintaining the sequence // of network packets. if !c.outboundBuffer.IsEmpty() { _, _ = c.outboundBuffer.Write(data) return } defer func() { if err != nil { _ = c.loop.close(c, os.NewSyscallError("write", err)) } }() var sent int loop: if sent, err = unix.Write(c.fd, data); err != nil { // A temporary error occurs, append the data to outbound buffer, // writing it back to the remote in the next round for LT mode. if err == unix.EAGAIN { _, err = c.outboundBuffer.Write(data) if !isET { err = c.loop.poller.ModReadWrite(&c.pollAttachment, isET) } return } return 0, err } data = data[sent:] if isET && len(data) > 0 { goto loop } // Failed to send all data back to the remote, buffer the leftover data for the next round. if len(data) > 0 { _, _ = c.outboundBuffer.Write(data) err = c.loop.poller.ModReadWrite(&c.pollAttachment, isET) } return } func (c *conn) writev(bs [][]byte) (n int, err error) { isET := c.loop.engine.opts.EdgeTriggeredIO for _, b := range bs { n += len(b) } // If there is pending data in outbound buffer, // the current data ought to be appended to the // outbound buffer for maintaining the sequence // of network packets. if !c.outboundBuffer.IsEmpty() { _, _ = c.outboundBuffer.Writev(bs) return } defer func() { if err != nil { _ = c.loop.close(c, os.NewSyscallError("writev", err)) } }() remaining := n var sent int loop: if sent, err = gio.Writev(c.fd, bs); err != nil { // A temporary error occurs, append the data to outbound buffer, // writing it back to the remote in the next round for LT mode. if err == unix.EAGAIN { _, err = c.outboundBuffer.Writev(bs) if !isET { err = c.loop.poller.ModReadWrite(&c.pollAttachment, isET) } return } return 0, err } pos := len(bs) if remaining -= sent; remaining > 0 { for i := range bs { bn := len(bs[i]) if sent < bn { bs[i] = bs[i][sent:] pos = i break } sent -= bn } } bs = bs[pos:] if isET && remaining > 0 { goto loop } // Failed to send all data back to the remote, buffer the leftover data for the next round. if remaining > 0 { _, _ = c.outboundBuffer.Writev(bs) err = c.loop.poller.ModReadWrite(&c.pollAttachment, isET) } return } type asyncWriteHook struct { callback AsyncCallback data []byte } func (c *conn) asyncWrite(a any) (err error) { hook := a.(*asyncWriteHook) defer func() { if hook.callback != nil { _ = hook.callback(c, err) } }() if !c.opened { return net.ErrClosed } _, err = c.write(hook.data) return } type asyncWritevHook struct { callback AsyncCallback data [][]byte } func (c *conn) asyncWritev(a any) (err error) { hook := a.(*asyncWritevHook) defer func() { if hook.callback != nil { _ = hook.callback(c, err) } }() if !c.opened { return net.ErrClosed } _, err = c.writev(hook.data) return } func (c *conn) sendTo(buf []byte, addr unix.Sockaddr) (n int, err error) { defer func() { if err != nil { n = 0 } }() if addr != nil { return len(buf), unix.Sendto(c.fd, buf, 0, addr) } if c.remote == nil { // connected UDP socket of client return len(buf), unix.Send(c.fd, buf, 0) } return len(buf), unix.Sendto(c.fd, buf, 0, c.remote) // unconnected UDP socket of server } func (c *conn) resetBuffer() { c.buffer = c.buffer[:0] c.inboundBuffer.Reset() c.inboundBuffer.Done() } func (c *conn) Read(p []byte) (n int, err error) { if c.inboundBuffer.IsEmpty() { n = copy(p, c.buffer) c.buffer = c.buffer[n:] if n == 0 && len(p) > 0 { err = io.ErrShortBuffer } return } n, _ = c.inboundBuffer.Read(p) if n == len(p) { return } m := copy(p[n:], c.buffer) n += m c.buffer = c.buffer[m:] return } func (c *conn) Next(n int) (buf []byte, err error) { inBufferLen := c.inboundBuffer.Buffered() if totalLen := inBufferLen + len(c.buffer); n > totalLen { return nil, io.ErrShortBuffer } else if n <= 0 { n = totalLen } if c.inboundBuffer.IsEmpty() { buf = c.buffer[:n] c.buffer = c.buffer[n:] return } buf = bsPool.Get(n) _, err = c.Read(buf) return } func (c *conn) Peek(n int) (buf []byte, err error) { inBufferLen := c.inboundBuffer.Buffered() if totalLen := inBufferLen + len(c.buffer); n > totalLen { return nil, io.ErrShortBuffer } else if n <= 0 { n = totalLen } if c.inboundBuffer.IsEmpty() { return c.buffer[:n], err } head, tail := c.inboundBuffer.Peek(n) if len(head) == n { return head, err } buf = bsPool.Get(n)[:0] buf = append(buf, head...) buf = append(buf, tail...) if inBufferLen >= n { return } remaining := n - inBufferLen buf = append(buf, c.buffer[:remaining]...) c.cache = buf return } func (c *conn) Discard(n int) (int, error) { if len(c.cache) > 0 { bsPool.Put(c.cache) c.cache = nil } inBufferLen := c.inboundBuffer.Buffered() if totalLen := inBufferLen + len(c.buffer); n >= totalLen || n <= 0 { c.resetBuffer() return totalLen, nil } if c.inboundBuffer.IsEmpty() { c.buffer = c.buffer[n:] return n, nil } discarded, _ := c.inboundBuffer.Discard(n) if discarded < inBufferLen { return discarded, nil } remaining := n - inBufferLen c.buffer = c.buffer[remaining:] return n, nil } func (c *conn) Write(p []byte) (int, error) { if c.isDatagram { return c.sendTo(p, nil) } return c.write(p) } func (c *conn) SendTo(p []byte, addr net.Addr) (int, error) { if !c.isDatagram { return 0, errorx.ErrUnsupportedOp } sa := socket.NetAddrToSockaddr(addr) if sa == nil { return 0, errorx.ErrInvalidNetworkAddress } return c.sendTo(p, sa) } func (c *conn) Writev(bs [][]byte) (int, error) { if c.isDatagram { return 0, errorx.ErrUnsupportedOp } return c.writev(bs) } func (c *conn) ReadFrom(r io.Reader) (int64, error) { return c.outboundBuffer.ReadFrom(r) } func (c *conn) WriteTo(w io.Writer) (n int64, err error) { if !c.inboundBuffer.IsEmpty() { if n, err = c.inboundBuffer.WriteTo(w); err != nil { return } } var m int m, err = w.Write(c.buffer) n += int64(m) c.buffer = c.buffer[m:] return } func (c *conn) Flush() error { return c.loop.write(c) } func (c *conn) InboundBuffered() int { return c.inboundBuffer.Buffered() + len(c.buffer) } func (c *conn) OutboundBuffered() int { return c.outboundBuffer.Buffered() } func (c *conn) Context() any { return c.ctx } func (c *conn) SetContext(ctx any) { c.ctx = ctx } func (c *conn) LocalAddr() net.Addr { return c.localAddr } func (c *conn) RemoteAddr() net.Addr { return c.remoteAddr } // Implementation of Socket interface // func (c *conn) Gfd() gfd.GFD { return c.gfd } func (c *conn) Fd() int { return c.fd } func (c *conn) Dup() (fd int, err error) { return socket.Dup(c.fd) } func (c *conn) SetReadBuffer(bytes int) error { return socket.SetRecvBuffer(c.fd, bytes) } func (c *conn) SetWriteBuffer(bytes int) error { return socket.SetSendBuffer(c.fd, bytes) } func (c *conn) SetLinger(sec int) error { return socket.SetLinger(c.fd, sec) } func (c *conn) SetNoDelay(noDelay bool) error { return socket.SetNoDelay(c.fd, func(b bool) int { if b { return 1 } return 0 }(noDelay)) } func (c *conn) SetKeepAlivePeriod(d time.Duration) error { if c.proto != "tcp" { return errorx.ErrUnsupportedOp } return socket.SetKeepAlivePeriod(c.fd, int(d.Seconds())) } func (c *conn) SetKeepAlive(enabled bool, idle, intvl time.Duration, cnt int) error { if c.proto != "tcp" { return errorx.ErrUnsupportedOp } return socket.SetKeepAlive(c.fd, enabled, int(idle.Seconds()), int(intvl.Seconds()), cnt) } func (c *conn) AsyncWrite(buf []byte, callback AsyncCallback) error { if c.isDatagram { _, err := c.sendTo(buf, nil) // TODO: it will not go asynchronously with UDP, so calling a callback is needless, // we may remove this branch in the future, please don't rely on the callback // to do something important under UDP, if you're working with UDP, just call Conn.Write // to send back your data. if callback != nil { _ = callback(nil, nil) } return err } return c.loop.poller.Trigger(queue.HighPriority, c.asyncWrite, &asyncWriteHook{callback, buf}) } func (c *conn) AsyncWritev(bs [][]byte, callback AsyncCallback) error { if c.isDatagram { return errorx.ErrUnsupportedOp } return c.loop.poller.Trigger(queue.HighPriority, c.asyncWritev, &asyncWritevHook{callback, bs}) } func (c *conn) Wake(callback AsyncCallback) error { return c.loop.poller.Trigger(queue.LowPriority, func(_ any) (err error) { err = c.loop.wake(c) if callback != nil { _ = callback(c, err) } return }, nil) } func (c *conn) CloseWithCallback(callback AsyncCallback) error { return c.loop.poller.Trigger(queue.LowPriority, func(_ any) (err error) { err = c.loop.close(c, nil) if callback != nil { _ = callback(c, err) } return }, nil) } func (c *conn) Close() error { return c.loop.poller.Trigger(queue.LowPriority, func(_ any) (err error) { err = c.loop.close(c, nil) return }, nil) } func (c *conn) EventLoop() EventLoop { return c.loop } func (*conn) SetDeadline(_ time.Time) error { return errorx.ErrUnsupportedOp } func (*conn) SetReadDeadline(_ time.Time) error { return errorx.ErrUnsupportedOp } func (*conn) SetWriteDeadline(_ time.Time) error { return errorx.ErrUnsupportedOp } ================================================ FILE: connection_windows.go ================================================ // Copyright (c) 2023 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gnet import ( "errors" "io" "net" "os" "syscall" "time" "golang.org/x/sys/windows" "github.com/panjf2000/gnet/v2/pkg/buffer/elastic" errorx "github.com/panjf2000/gnet/v2/pkg/errors" bbPool "github.com/panjf2000/gnet/v2/pkg/pool/bytebuffer" bsPool "github.com/panjf2000/gnet/v2/pkg/pool/byteslice" "github.com/panjf2000/gnet/v2/pkg/pool/goroutine" ) type netErr struct { c *conn err error } type tcpConn struct { c *conn b *bbPool.ByteBuffer } type udpConn struct { c *conn } type openConn struct { c *conn cb func() } type conn struct { pc net.PacketConn ctx any // user-defined context loop *eventloop // owner event-loop buffer *bbPool.ByteBuffer // reuse memory of inbound data as a temporary buffer cache []byte // temporary cache for the inbound data rawConn net.Conn // original connection localAddr net.Addr // local server addr remoteAddr net.Addr // remote addr inboundBuffer elastic.RingBuffer // buffer for data from the remote } func packTCPConn(c *conn, buf []byte) *tcpConn { b := bbPool.Get() _, _ = b.Write(buf) return &tcpConn{c: c, b: b} } func unpackTCPConn(tc *tcpConn) *conn { if tc.c.buffer == nil { // the connection has been closed return nil } _, _ = tc.c.buffer.Write(tc.b.B) bbPool.Put(tc.b) tc.b = nil return tc.c } func packUDPConn(c *conn, buf []byte) *udpConn { _, _ = c.buffer.Write(buf) return &udpConn{c} } func newStreamConn(el *eventloop, nc net.Conn, ctx any) (c *conn) { return &conn{ ctx: ctx, loop: el, buffer: bbPool.Get(), rawConn: nc, localAddr: nc.LocalAddr(), remoteAddr: nc.RemoteAddr(), } } func (c *conn) release() { c.ctx = nil c.localAddr = nil if c.rawConn != nil { c.rawConn = nil c.remoteAddr = nil } c.inboundBuffer.Done() bbPool.Put(c.buffer) c.buffer = nil } func newUDPConn(el *eventloop, pc net.PacketConn, rc net.Conn, localAddr, remoteAddr net.Addr, ctx any) *conn { return &conn{ ctx: ctx, pc: pc, rawConn: rc, loop: el, buffer: bbPool.Get(), localAddr: localAddr, remoteAddr: remoteAddr, } } func (c *conn) resetBuffer() { c.buffer.Reset() c.inboundBuffer.Reset() c.inboundBuffer.Done() } func (c *conn) Read(p []byte) (n int, err error) { if c.inboundBuffer.IsEmpty() { n = copy(p, c.buffer.B) c.buffer.B = c.buffer.B[n:] if n == 0 && len(p) > 0 { err = io.ErrShortBuffer } return } n, _ = c.inboundBuffer.Read(p) if n == len(p) { return } m := copy(p[n:], c.buffer.B) n += m c.buffer.B = c.buffer.B[m:] return } func (c *conn) Next(n int) (buf []byte, err error) { inBufferLen := c.inboundBuffer.Buffered() if totalLen := inBufferLen + c.buffer.Len(); n > totalLen { return nil, io.ErrShortBuffer } else if n <= 0 { n = totalLen } if c.inboundBuffer.IsEmpty() { buf = c.buffer.B[:n] c.buffer.B = c.buffer.B[n:] return } buf = bsPool.Get(n) _, err = c.Read(buf) return } func (c *conn) Peek(n int) (buf []byte, err error) { inBufferLen := c.inboundBuffer.Buffered() if totalLen := inBufferLen + c.buffer.Len(); n > totalLen { return nil, io.ErrShortBuffer } else if n <= 0 { n = totalLen } if c.inboundBuffer.IsEmpty() { return c.buffer.B[:n], err } head, tail := c.inboundBuffer.Peek(n) if len(head) == n { return head, err } buf = bsPool.Get(n)[:0] buf = append(buf, head...) buf = append(buf, tail...) if inBufferLen >= n { return } remaining := n - inBufferLen buf = append(buf, c.buffer.B[:remaining]...) c.cache = buf return } func (c *conn) Discard(n int) (int, error) { if len(c.cache) > 0 { bsPool.Put(c.cache) c.cache = nil } inBufferLen := c.inboundBuffer.Buffered() if totalLen := inBufferLen + c.buffer.Len(); n >= totalLen || n <= 0 { c.resetBuffer() return totalLen, nil } if c.inboundBuffer.IsEmpty() { c.buffer.B = c.buffer.B[n:] return n, nil } discarded, _ := c.inboundBuffer.Discard(n) if discarded < inBufferLen { return discarded, nil } remaining := n - inBufferLen c.buffer.B = c.buffer.B[remaining:] return n, nil } func (c *conn) Write(p []byte) (int, error) { if c.rawConn == nil && c.pc == nil { return 0, net.ErrClosed } if c.rawConn != nil { return c.rawConn.Write(p) } return c.pc.WriteTo(p, c.remoteAddr) } func (c *conn) SendTo(p []byte, addr net.Addr) (int, error) { if c.pc == nil { return 0, errorx.ErrUnsupportedOp } if addr == nil { return 0, errorx.ErrInvalidNetworkAddress } return c.pc.WriteTo(p, addr) } func (c *conn) Writev(bs [][]byte) (int, error) { if c.pc != nil { // not available for UDP return 0, errorx.ErrUnsupportedOp } if c.rawConn != nil { bb := bbPool.Get() defer bbPool.Put(bb) for i := range bs { _, _ = bb.Write(bs[i]) } return c.rawConn.Write(bb.Bytes()) } return 0, net.ErrClosed } func (c *conn) ReadFrom(r io.Reader) (int64, error) { if c.rawConn != nil { return io.Copy(c.rawConn, r) } return 0, net.ErrClosed } func (c *conn) WriteTo(w io.Writer) (n int64, err error) { if !c.inboundBuffer.IsEmpty() { if n, err = c.inboundBuffer.WriteTo(w); err != nil { return } } if c.buffer == nil { return 0, nil } defer c.buffer.Reset() return c.buffer.WriteTo(w) } func (c *conn) Flush() error { return nil } func (c *conn) InboundBuffered() int { if c.buffer == nil { return 0 } return c.inboundBuffer.Buffered() + c.buffer.Len() } func (c *conn) OutboundBuffered() int { return 0 } func (c *conn) Context() any { return c.ctx } func (c *conn) SetContext(ctx any) { c.ctx = ctx } func (c *conn) LocalAddr() net.Addr { return c.localAddr } func (c *conn) RemoteAddr() net.Addr { return c.remoteAddr } func (c *conn) Fd() (fd int) { if c.rawConn == nil { return -1 } rc, err := c.rawConn.(syscall.Conn).SyscallConn() if err != nil { return -1 } if err := rc.Control(func(i uintptr) { fd = int(i) }); err != nil { return -1 } return } func (c *conn) Dup() (fd int, err error) { if c.rawConn == nil && c.pc == nil { return -1, net.ErrClosed } var ( sc syscall.Conn ok bool ) if c.rawConn != nil { sc, ok = c.rawConn.(syscall.Conn) } else { sc, ok = c.pc.(syscall.Conn) } if !ok { return -1, errors.New("failed to convert net.Conn to syscall.Conn") } rc, err := sc.SyscallConn() if err != nil { return -1, errors.New("failed to get syscall.RawConn from net.Conn") } var dupHandle windows.Handle e := rc.Control(func(fd uintptr) { process := windows.CurrentProcess() err = windows.DuplicateHandle( process, windows.Handle(fd), process, &dupHandle, 0, true, windows.DUPLICATE_SAME_ACCESS, ) }) if err != nil { return -1, err } if e != nil { return -1, e } return int(dupHandle), nil } func (c *conn) SetReadBuffer(bytes int) error { if c.rawConn == nil && c.pc == nil { return net.ErrClosed } if c.rawConn != nil { return c.rawConn.(interface{ SetReadBuffer(int) error }).SetReadBuffer(bytes) } return c.pc.(interface{ SetReadBuffer(int) error }).SetReadBuffer(bytes) } func (c *conn) SetWriteBuffer(bytes int) error { if c.rawConn == nil && c.pc == nil { return net.ErrClosed } if c.rawConn != nil { return c.rawConn.(interface{ SetWriteBuffer(int) error }).SetWriteBuffer(bytes) } return c.pc.(interface{ SetWriteBuffer(int) error }).SetWriteBuffer(bytes) } func (c *conn) SetLinger(sec int) error { if c.rawConn == nil { return net.ErrClosed } tc, ok := c.rawConn.(*net.TCPConn) if !ok { return errorx.ErrUnsupportedOp } return tc.SetLinger(sec) } func (c *conn) SetNoDelay(noDelay bool) error { if c.rawConn == nil { return net.ErrClosed } tc, ok := c.rawConn.(*net.TCPConn) if !ok { return errorx.ErrUnsupportedOp } return tc.SetNoDelay(noDelay) } func (c *conn) SetKeepAlivePeriod(d time.Duration) error { return c.SetKeepAlive(d > 0, d, d/5, 5) } func (c *conn) SetKeepAlive(enabled bool, idle, intvl time.Duration, cnt int) error { if c.rawConn == nil && c.pc == nil { return net.ErrClosed } if c.pc != nil { return errorx.ErrUnsupportedOp } tc, ok := c.rawConn.(*net.TCPConn) if !ok { return errorx.ErrUnsupportedOp } if enabled && (idle <= 0 || intvl <= 0 || cnt <= 0) { return errors.New("invalid time duration") } if err := tc.SetKeepAlive(enabled); err != nil { return err } if !enabled { return nil } if err := tc.SetKeepAlivePeriod(idle); err != nil { return err } if err := windows.SetsockoptInt( windows.Handle(c.Fd()), windows.IPPROTO_TCP, windows.TCP_KEEPINTVL, int(intvl.Seconds())); err != nil { return os.NewSyscallError("setsockopt", err) } if err := windows.SetsockoptInt( windows.Handle(c.Fd()), windows.IPPROTO_TCP, windows.TCP_KEEPCNT, cnt); err != nil { return os.NewSyscallError("setsockopt", err) } return nil } // Gfd return an uninitialized GFD which is not valid, // this method is only implemented for compatibility, don't use it on Windows. // func (c *conn) Gfd() gfd.GFD { return gfd.GFD{} } func (c *conn) AsyncWrite(buf []byte, cb AsyncCallback) error { fn := func() error { _, err := c.Write(buf) if cb != nil { _ = cb(c, err) } return err } var err error select { case c.loop.ch <- fn: default: // If the event-loop channel is full, asynchronize this operation to avoid blocking the eventloop. err = goroutine.DefaultWorkerPool.Submit(func() { c.loop.ch <- fn }) } return err } func (c *conn) AsyncWritev(bs [][]byte, cb AsyncCallback) error { if c.pc != nil { return errorx.ErrUnsupportedOp } buf := bbPool.Get() for _, b := range bs { _, _ = buf.Write(b) } return c.AsyncWrite(buf.Bytes(), func(c Conn, err error) error { defer bbPool.Put(buf) if cb == nil { return err } return cb(c, err) }) } func (c *conn) Wake(cb AsyncCallback) (err error) { wakeFn := func() (err error) { err = c.loop.wake(c) if cb != nil { _ = cb(c, err) } return } select { case c.loop.ch <- wakeFn: default: // If the event-loop channel is full, asynchronize this operation to avoid blocking the eventloop. err = goroutine.DefaultWorkerPool.Submit(func() { c.loop.ch <- wakeFn }) } return } func (c *conn) Close() (err error) { closeFn := func() error { return c.loop.close(c, nil) } select { case c.loop.ch <- closeFn: default: // If the event-loop channel is full, asynchronize this operation to avoid blocking the eventloop. err = goroutine.DefaultWorkerPool.Submit(func() { c.loop.ch <- closeFn }) } return } func (c *conn) CloseWithCallback(cb AsyncCallback) (err error) { closeFn := func() (err error) { err = c.loop.close(c, nil) if cb != nil { _ = cb(c, err) } return } select { case c.loop.ch <- closeFn: default: // If the event-loop channel is full, asynchronize this operation to avoid blocking the eventloop. err = goroutine.DefaultWorkerPool.Submit(func() { c.loop.ch <- closeFn }) } return } func (c *conn) EventLoop() EventLoop { return c.loop } func (*conn) SetDeadline(_ time.Time) error { return errorx.ErrUnsupportedOp } func (*conn) SetReadDeadline(_ time.Time) error { return errorx.ErrUnsupportedOp } func (*conn) SetWriteDeadline(_ time.Time) error { return errorx.ErrUnsupportedOp } ================================================ FILE: context.go ================================================ /* * Copyright (c) 2025 The Gnet Authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package gnet import ( "context" "net" ) // contextKey is a key for Conn values in context.Context. type contextKey struct{} // NewContext returns a new context.Context that carries the value // that will be attached to the Conn. func NewContext(ctx context.Context, v any) context.Context { return context.WithValue(ctx, contextKey{}, v) } // FromContext retrieves context value of the Conn stored in ctx, if any. func FromContext(ctx context.Context) any { return ctx.Value(contextKey{}) } // connContextKey is a key for net.Conn values in context.Context. type connContextKey struct{} // NewNetConnContext returns a new context.Context that carries the net.Conn value. func NewNetConnContext(ctx context.Context, c net.Conn) context.Context { return context.WithValue(ctx, connContextKey{}, c) } // FromNetConnContext retrieves the net.Conn value from ctx, if any. func FromNetConnContext(ctx context.Context) (net.Conn, bool) { c, ok := ctx.Value(connContextKey{}).(net.Conn) return c, ok } // netAddrContextKey is a key for net.Addr values in context.Context. type netAddrContextKey struct{} // NewNetAddrContext returns a new context.Context that carries the net.Addr value. func NewNetAddrContext(ctx context.Context, a net.Addr) context.Context { return context.WithValue(ctx, netAddrContextKey{}, a) } // FromNetAddrContext retrieves the net.Addr value from ctx, if any. func FromNetAddrContext(ctx context.Context) (net.Addr, bool) { a, ok := ctx.Value(netAddrContextKey{}).(net.Addr) return a, ok } ================================================ FILE: engine_unix.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package gnet import ( "context" "errors" "strings" "sync/atomic" "time" "golang.org/x/sync/errgroup" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" "github.com/panjf2000/gnet/v2/pkg/netpoll" "github.com/panjf2000/gnet/v2/pkg/queue" "github.com/panjf2000/gnet/v2/pkg/socket" ) type engine struct { listeners map[int]*listener // listeners for accepting incoming connections opts *Options // options with engine ingress *eventloop // main event-loop that monitors all listeners eventLoops loadBalancer // event-loops for handling events inShutdown atomic.Bool // whether the engine is in shutdown turnOff context.CancelFunc eventHandler EventHandler // user eventHandler concurrency struct { *errgroup.Group ctx context.Context } } func (eng *engine) isShutdown() bool { return eng.inShutdown.Load() } // shutdown signals the engine to shut down. func (eng *engine) shutdown(err error) { if err != nil && !errors.Is(err, errorx.ErrEngineShutdown) { eng.opts.Logger.Errorf("engine is being shutdown with error: %v", err) } // Cancel the context to stop the engine. eng.turnOff() } func (eng *engine) closeEventLoops() { eng.eventLoops.iterate(func(_ int, el *eventloop) bool { for _, ln := range el.listeners { ln.close() } _ = el.poller.Close() return true }) if eng.ingress != nil { for _, ln := range eng.listeners { ln.close() } err := eng.ingress.poller.Close() if err != nil { eng.opts.Logger.Errorf("failed to close poller when stopping engine: %v", err) } } } func (eng *engine) runEventLoops(ctx context.Context, numEventLoop int) error { var el0 *eventloop lns := eng.listeners // Create loops locally and bind the listeners. for i := 0; i < numEventLoop; i++ { if i > 0 { lns = make(map[int]*listener, len(eng.listeners)) for _, l := range eng.listeners { ln, err := initListener(l.network, l.address, eng.opts) if err != nil { return err } lns[ln.fd] = ln } } p, err := netpoll.OpenPoller() if err != nil { return err } el := new(eventloop) el.listeners = lns el.engine = eng el.poller = p el.buffer = make([]byte, eng.opts.ReadBufferCap) el.connections.init() el.eventHandler = eng.eventHandler for _, ln := range lns { if err = el.poller.AddRead(ln.packPollAttachment(el.accept), false); err != nil { return err } } eng.eventLoops.register(el) // Start the ticker. if eng.opts.Ticker && el.idx == 0 { el0 = el } } // Start event-loops in the background. eng.eventLoops.iterate(func(_ int, el *eventloop) bool { eng.concurrency.Go(el.run) return true }) if el0 != nil { eng.concurrency.Go(func() error { el0.ticker(ctx) return nil }) } return nil } func (eng *engine) activateReactors(ctx context.Context, numEventLoop int) error { for i := 0; i < numEventLoop; i++ { p, err := netpoll.OpenPoller() if err != nil { return err } el := new(eventloop) el.listeners = eng.listeners el.engine = eng el.poller = p el.buffer = make([]byte, eng.opts.ReadBufferCap) el.connections.init() el.eventHandler = eng.eventHandler eng.eventLoops.register(el) } // Start sub reactors in the background. eng.eventLoops.iterate(func(_ int, el *eventloop) bool { eng.concurrency.Go(el.orbit) return true }) p, err := netpoll.OpenPoller() if err != nil { return err } el := new(eventloop) el.listeners = eng.listeners el.idx = -1 el.engine = eng el.poller = p el.eventHandler = eng.eventHandler for _, ln := range eng.listeners { if err = el.poller.AddRead(ln.packPollAttachment(el.accept0), true); err != nil { return err } } eng.ingress = el // Start the main reactor in the background. eng.concurrency.Go(el.rotate) // Start the ticker. if eng.opts.Ticker { eng.concurrency.Go(func() error { eng.ingress.ticker(ctx) return nil }) } return nil } func (eng *engine) start(ctx context.Context, numEventLoop int) error { if eng.opts.ReusePort { return eng.runEventLoops(ctx, numEventLoop) } return eng.activateReactors(ctx, numEventLoop) } func (eng *engine) stop(ctx context.Context, s Engine) { // Wait on a signal for shutdown <-ctx.Done() eng.eventHandler.OnShutdown(s) // Notify all event-loops to exit. eng.eventLoops.iterate(func(i int, el *eventloop) bool { err := el.poller.Trigger(queue.HighPriority, func(_ any) error { return errorx.ErrEngineShutdown }, nil) if err != nil { eng.opts.Logger.Errorf("failed to enqueue shutdown signal of high-priority for event-loop(%d): %v", i, err) } return true }) if eng.ingress != nil { err := eng.ingress.poller.Trigger(queue.HighPriority, func(_ any) error { return errorx.ErrEngineShutdown }, nil) if err != nil { eng.opts.Logger.Errorf("failed to enqueue shutdown signal of high-priority for main event-loop: %v", err) } } if err := eng.concurrency.Wait(); err != nil { eng.opts.Logger.Errorf("engine shutdown error: %v", err) } // Close all listeners and pollers of event-loops. eng.closeEventLoops() // Put the engine into the shutdown state. eng.inShutdown.Store(true) } func run(eventHandler EventHandler, listeners []*listener, options *Options, addrs []string) error { numEventLoop := determineEventLoops(options) logging.Infof("Launching gnet with %d event-loops, listening on: %s", numEventLoop, strings.Join(addrs, " | ")) lns := make(map[int]*listener, len(listeners)) for _, ln := range listeners { lns[ln.fd] = ln } rootCtx, shutdown := context.WithCancel(context.Background()) eg, ctx := errgroup.WithContext(rootCtx) eng := engine{ listeners: lns, opts: options, turnOff: shutdown, eventHandler: eventHandler, concurrency: struct { *errgroup.Group ctx context.Context }{eg, ctx}, } switch options.LB { case RoundRobin: eng.eventLoops = new(roundRobinLoadBalancer) case LeastConnections: eng.eventLoops = new(leastConnectionsLoadBalancer) case SourceAddrHash: eng.eventLoops = new(sourceAddrHashLoadBalancer) } e := Engine{&eng} switch eng.eventHandler.OnBoot(e) { case None, Close: case Shutdown: return nil } if err := eng.start(ctx, numEventLoop); err != nil { eng.closeEventLoops() eng.opts.Logger.Errorf("gnet engine is stopping with error: %v", err) return err } defer eng.stop(rootCtx, e) for _, addr := range addrs { allEngines.Store(addr, &eng) } return nil } func setKeepAlive(fd int, enabled bool, idle, intvl time.Duration, cnt int) error { if intvl == 0 { intvl = idle / 5 } if cnt == 0 { cnt = 5 } return socket.SetKeepAlive(fd, enabled, int(idle.Seconds()), int(intvl.Seconds()), cnt) } /* func (eng *engine) sendCmd(cmd *asyncCmd, urgent bool) error { if !gfd.Validate(cmd.fd) { return errors.ErrInvalidConn } el := eng.eventLoops.index(cmd.fd.EventLoopIndex()) if el == nil { return errors.ErrInvalidConn } if urgent { return el.poller.Trigger(queue.LowPriority, el.execCmd, cmd) } return el.poller.Trigger(el.execCmd, cmd) } */ ================================================ FILE: engine_windows.go ================================================ // Copyright (c) 2023 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gnet import ( "context" "errors" "strings" "sync/atomic" "golang.org/x/sync/errgroup" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" ) type engine struct { listeners []*listener opts *Options // options with engine eventLoops loadBalancer // event-loops for handling events inShutdown atomic.Bool // whether the engine is in shutdown beingShutdown atomic.Bool // whether the engine is being shutdown turnOff context.CancelFunc eventHandler EventHandler // user eventHandler concurrency struct { *errgroup.Group ctx context.Context } } func (eng *engine) isShutdown() bool { return eng.inShutdown.Load() } // shutdown signals the engine to shut down. func (eng *engine) shutdown(err error) { if err != nil && !errors.Is(err, errorx.ErrEngineShutdown) { eng.opts.Logger.Errorf("engine is being shutdown with error: %v", err) } eng.turnOff() eng.beingShutdown.Store(true) } func (eng *engine) closeEventLoops() { eng.eventLoops.iterate(func(i int, el *eventloop) bool { el.ch <- errorx.ErrEngineShutdown return true }) for _, ln := range eng.listeners { ln.close() } } func (eng *engine) start(ctx context.Context, numEventLoop int) error { var el0 *eventloop for i := 0; i < numEventLoop; i++ { el := eventloop{ ch: make(chan any, 1024), eng: eng, connections: make(map[*conn]struct{}), eventHandler: eng.eventHandler, } eng.eventLoops.register(&el) eng.concurrency.Go(el.run) if i == 0 && eng.opts.Ticker { el0 = &el } } if el0 != nil { eng.concurrency.Go(func() error { el0.ticker(ctx) return nil }) } for _, ln := range eng.listeners { l := ln if l.pc != nil { eng.concurrency.Go(func() error { return eng.ListenUDP(l.pc) }) } else { eng.concurrency.Go(func() error { return eng.listenStream(l.ln) }) } } return nil } func (eng *engine) stop(ctx context.Context, engine Engine) { <-ctx.Done() eng.eventHandler.OnShutdown(engine) eng.closeEventLoops() if err := eng.concurrency.Wait(); err != nil && !errors.Is(err, errorx.ErrEngineShutdown) { eng.opts.Logger.Errorf("engine shutdown error: %v", err) } eng.inShutdown.Store(true) } func run(eventHandler EventHandler, listeners []*listener, options *Options, addrs []string) error { numEventLoop := determineEventLoops(options) logging.Infof("Launching gnet with %d event-loops, listening on: %s", numEventLoop, strings.Join(addrs, " | ")) rootCtx, shutdown := context.WithCancel(context.Background()) eg, ctx := errgroup.WithContext(rootCtx) eng := engine{ opts: options, listeners: listeners, turnOff: shutdown, eventHandler: eventHandler, concurrency: struct { *errgroup.Group ctx context.Context }{eg, ctx}, } switch options.LB { case RoundRobin: eng.eventLoops = new(roundRobinLoadBalancer) // If there are more than one listener, we can't use roundRobinLoadBalancer because // it's not concurrency-safe, replace it with leastConnectionsLoadBalancer. if len(listeners) > 1 { eng.eventLoops = new(leastConnectionsLoadBalancer) } case LeastConnections: eng.eventLoops = new(leastConnectionsLoadBalancer) case SourceAddrHash: eng.eventLoops = new(sourceAddrHashLoadBalancer) } engine := Engine{eng: &eng} switch eventHandler.OnBoot(engine) { case None, Close: case Shutdown: return nil } if err := eng.start(ctx, numEventLoop); err != nil { eng.opts.Logger.Errorf("gnet engine is stopping with error: %v", err) return err } defer eng.stop(rootCtx, engine) for _, addr := range addrs { allEngines.Store(addr, &eng) } return nil } /* func (eng *engine) sendCmd(_ *asyncCmd, _ bool) error { return errorx.ErrUnsupportedOp } */ ================================================ FILE: eventloop_unix.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package gnet import ( "context" "errors" "fmt" "io" "net" "os" "strconv" "strings" "syscall" "time" "golang.org/x/sys/unix" errorx "github.com/panjf2000/gnet/v2/pkg/errors" gio "github.com/panjf2000/gnet/v2/pkg/io" "github.com/panjf2000/gnet/v2/pkg/logging" "github.com/panjf2000/gnet/v2/pkg/netpoll" "github.com/panjf2000/gnet/v2/pkg/pool/goroutine" "github.com/panjf2000/gnet/v2/pkg/queue" "github.com/panjf2000/gnet/v2/pkg/socket" ) type eventloop struct { listeners map[int]*listener // listeners idx int // loop index in the engine loops list engine *engine // engine in loop poller *netpoll.Poller // epoll or kqueue buffer []byte // read packet buffer whose capacity is set by user, default value is 64KB connections connMatrix // loop connections storage eventHandler EventHandler // user eventHandler } func (el *eventloop) Register(ctx context.Context, addr net.Addr) (<-chan RegisteredResult, error) { if el.engine.isShutdown() { return nil, errorx.ErrEngineInShutdown } if addr == nil { return nil, errorx.ErrInvalidNetworkAddress } return el.enroll(nil, addr, FromContext(ctx)) } func (el *eventloop) Enroll(ctx context.Context, c net.Conn) (<-chan RegisteredResult, error) { if el.engine.isShutdown() { return nil, errorx.ErrEngineInShutdown } if c == nil { return nil, errorx.ErrInvalidNetConn } return el.enroll(c, c.RemoteAddr(), FromContext(ctx)) } func (el *eventloop) Execute(ctx context.Context, runnable Runnable) error { if el.engine.isShutdown() { return errorx.ErrEngineInShutdown } if runnable == nil { return errorx.ErrNilRunnable } return el.poller.Trigger(queue.LowPriority, func(any) error { return runnable.Run(ctx) }, nil) } func (el *eventloop) Schedule(context.Context, Runnable, time.Duration) error { return errorx.ErrUnsupportedOp } func (el *eventloop) Close(c Conn) error { return el.close(c.(*conn), nil) } func (el *eventloop) getLogger() logging.Logger { return el.engine.opts.Logger } func (el *eventloop) countConn() int32 { return el.connections.loadCount() } func (el *eventloop) closeConns() { // Close loops and all outstanding connections el.connections.iterate(func(c *conn) bool { _ = el.close(c, nil) return true }) } type connWithCallback struct { c *conn cb func() } func (el *eventloop) enroll(c net.Conn, addr net.Addr, ctx any) (resCh chan RegisteredResult, err error) { resCh = make(chan RegisteredResult, 1) err = goroutine.DefaultWorkerPool.Submit(func() { defer close(resCh) var err error if c == nil { if c, err = net.Dial(addr.Network(), addr.String()); err != nil { resCh <- RegisteredResult{Err: err} return } } defer c.Close() //nolint:errcheck sc, ok := c.(syscall.Conn) if !ok { resCh <- RegisteredResult{ Err: fmt.Errorf("failed to assert syscall.Conn from net.Conn: %s", addr.String()), } return } rc, err := sc.SyscallConn() if err != nil { resCh <- RegisteredResult{Err: err} return } var dupFD int err1 := rc.Control(func(fd uintptr) { dupFD, err = socket.Dup(int(fd)) }) if err != nil { resCh <- RegisteredResult{Err: err} return } if err1 != nil { resCh <- RegisteredResult{Err: err1} return } var ( sockAddr unix.Sockaddr gc *conn ) switch c.(type) { case *net.UnixConn: sockAddr, _, _, err = socket.GetUnixSockAddr(c.RemoteAddr().Network(), c.RemoteAddr().String()) if err != nil { resCh <- RegisteredResult{Err: err} return } ua := c.LocalAddr().(*net.UnixAddr) ua.Name = c.RemoteAddr().String() + "." + strconv.Itoa(dupFD) gc = newStreamConn("unix", dupFD, el, sockAddr, c.LocalAddr(), c.RemoteAddr()) case *net.TCPConn: sockAddr, _, _, _, err = socket.GetTCPSockAddr(c.RemoteAddr().Network(), c.RemoteAddr().String()) if err != nil { resCh <- RegisteredResult{Err: err} return } gc = newStreamConn("tcp", dupFD, el, sockAddr, c.LocalAddr(), c.RemoteAddr()) case *net.UDPConn: sockAddr, _, _, _, err = socket.GetUDPSockAddr(c.RemoteAddr().Network(), c.RemoteAddr().String()) if err != nil { resCh <- RegisteredResult{Err: err} return } gc = newUDPConn(dupFD, el, c.LocalAddr(), sockAddr, true) default: resCh <- RegisteredResult{Err: fmt.Errorf("unknown type of conn: %T", c)} return } gc.ctx = ctx connOpened := make(chan struct{}) ccb := &connWithCallback{c: gc, cb: func() { close(connOpened) }} if err := el.poller.Trigger(queue.LowPriority, el.register, ccb); err != nil { gc.Close() //nolint:errcheck resCh <- RegisteredResult{Err: err} return } <-connOpened resCh <- RegisteredResult{Conn: gc} }) return } func (el *eventloop) register(a any) error { c, ok := a.(*conn) if !ok { ccb := a.(*connWithCallback) c = ccb.c defer ccb.cb() } return el.register0(c) } func (el *eventloop) register0(c *conn) error { addEvents := el.poller.AddRead if el.engine.opts.EdgeTriggeredIO { addEvents = el.poller.AddReadWrite } if err := addEvents(&c.pollAttachment, el.engine.opts.EdgeTriggeredIO); err != nil { _ = unix.Close(c.fd) c.release() return err } el.connections.addConn(c, el.idx) if c.isDatagram && c.remote != nil { return nil } return el.open(c) } func (el *eventloop) open(c *conn) error { c.opened = true out, action := el.eventHandler.OnOpen(c) if out != nil { if err := c.open(out); err != nil { return err } } if !c.outboundBuffer.IsEmpty() && !el.engine.opts.EdgeTriggeredIO { if err := el.poller.ModReadWrite(&c.pollAttachment, false); err != nil { return err } } return el.handleAction(c, action) } func (el *eventloop) read0(a any) error { return el.read(a.(*conn)) } func (el *eventloop) read(c *conn) error { if !c.opened { return nil } var recv int isET := el.engine.opts.EdgeTriggeredIO chunk := el.engine.opts.EdgeTriggeredIOChunk loop: n, err := unix.Read(c.fd, el.buffer) if err != nil || n == 0 { if err == unix.EAGAIN { return nil } if n == 0 { err = io.EOF } return el.close(c, os.NewSyscallError("read", err)) } recv += n c.buffer = el.buffer[:n] action := el.eventHandler.OnTraffic(c) switch action { case None: case Close: return el.close(c, nil) case Shutdown: return errorx.ErrEngineShutdown } _, _ = c.inboundBuffer.Write(c.buffer) c.buffer = c.buffer[:0] if c.isEOF || (isET && recv < chunk) { goto loop } // To prevent infinite reading in ET mode and starving other events, // we need to set up threshold for the maximum read bytes per connection // on each event-loop. If the threshold is reached and there are still // unread data in the socket buffer, we must issue another read event manually. if isET && n == len(el.buffer) { return el.poller.Trigger(queue.LowPriority, el.read0, c) } return nil } func (el *eventloop) write0(a any) error { return el.write(a.(*conn)) } // The default value of UIO_MAXIOV/IOV_MAX is 1024 on Linux and most BSD-like OSs. const iovMax = 1024 func (el *eventloop) write(c *conn) error { if c.outboundBuffer.IsEmpty() { return nil } isET := el.engine.opts.EdgeTriggeredIO chunk := el.engine.opts.EdgeTriggeredIOChunk var ( n int sent int err error ) loop: iov, _ := c.outboundBuffer.Peek(-1) if len(iov) > 1 { if len(iov) > iovMax { iov = iov[:iovMax] } n, err = gio.Writev(c.fd, iov) } else { n, err = unix.Write(c.fd, iov[0]) } _, _ = c.outboundBuffer.Discard(n) switch err { case nil: case unix.EAGAIN: return nil default: return el.close(c, os.NewSyscallError("write", err)) } sent += n if isET && !c.outboundBuffer.IsEmpty() && sent < chunk { goto loop } // All data have been sent, it's no need to monitor the writable events for LT mode, // remove the writable event from poller to help the future event-loops if necessary. if !isET && c.outboundBuffer.IsEmpty() { return el.poller.ModRead(&c.pollAttachment, false) } // To prevent infinite writing in ET mode and starving other events, // we need to set up threshold for the maximum write bytes per connection // on each event-loop. If the threshold is reached and there are still // pending data to write, we must issue another write event manually. if isET && !c.outboundBuffer.IsEmpty() { return el.poller.Trigger(queue.HighPriority, el.write0, c) } return nil } func (el *eventloop) close(c *conn, err error) error { if !c.opened || el.connections.getConn(c.fd) == nil { return nil // ignore stale connections } el.connections.delConn(c) action := el.eventHandler.OnClose(c, err) // Send residual data in buffer back to the remote before actually closing the connection. for !c.outboundBuffer.IsEmpty() { iov, _ := c.outboundBuffer.Peek(0) if len(iov) > iovMax { iov = iov[:iovMax] } n, err := gio.Writev(c.fd, iov) if err != nil { break } _, _ = c.outboundBuffer.Discard(n) } c.release() var errStr strings.Builder err0, err1 := el.poller.Delete(c.fd), unix.Close(c.fd) if err0 != nil { err0 = fmt.Errorf("failed to delete fd=%d from poller in event-loop(%d): %v", c.fd, el.idx, os.NewSyscallError("delete", err0)) errStr.WriteString(err0.Error()) errStr.WriteString(" | ") } if err1 != nil { err1 = fmt.Errorf("failed to close fd=%d in event-loop(%d): %v", c.fd, el.idx, os.NewSyscallError("close", err1)) errStr.WriteString(err1.Error()) } if errStr.Len() > 0 { return errors.New(strings.TrimSuffix(errStr.String(), " | ")) } return el.handleAction(c, action) } func (el *eventloop) wake(c *conn) error { if !c.opened || el.connections.getConn(c.fd) == nil { return nil // ignore stale connections } action := el.eventHandler.OnTraffic(c) return el.handleAction(c, action) } func (el *eventloop) ticker(ctx context.Context) { var ( action Action delay time.Duration timer *time.Timer ) defer func() { if timer != nil { timer.Stop() } }() for { delay, action = el.eventHandler.OnTick() switch action { case None, Close: case Shutdown: // It seems reasonable to mark this as low-priority, waiting for some tasks like asynchronous writes // to finish up before shutting down the service. err := el.poller.Trigger(queue.LowPriority, func(_ any) error { return errorx.ErrEngineShutdown }, nil) el.getLogger().Debugf("failed to enqueue shutdown signal of high-priority for event-loop(%d): %v", el.idx, err) } if timer == nil { timer = time.NewTimer(delay) } else { timer.Reset(delay) } select { case <-ctx.Done(): el.getLogger().Debugf("stopping ticker in event-loop(%d) from Engine, error:%v", el.idx, ctx.Err()) return case <-timer.C: } } } func (el *eventloop) readUDP(fd int, _ netpoll.IOEvent, _ netpoll.IOFlags) error { n, sa, err := unix.Recvfrom(fd, el.buffer, 0) if err != nil { if err == unix.EAGAIN { return nil } return fmt.Errorf("failed to read UDP packet from fd=%d in event-loop(%d), %v", fd, el.idx, os.NewSyscallError("recvfrom", err)) } var c *conn if ln, ok := el.listeners[fd]; ok { c = newUDPConn(fd, el, ln.addr, sa, false) } else { c = el.connections.getConn(fd) } c.buffer = el.buffer[:n] action := el.eventHandler.OnTraffic(c) if c.remote != nil { c.release() } if action == Shutdown { return errorx.ErrEngineShutdown } return nil } func (el *eventloop) handleAction(c *conn, action Action) error { switch action { case None: return nil case Close: return el.close(c, nil) case Shutdown: return errorx.ErrEngineShutdown default: return nil } } /* func (el *eventloop) execCmd(a any) (err error) { cmd := a.(*asyncCmd) c := el.connections.getConnByGFD(cmd.fd) if c == nil || c.gfd != cmd.fd { return errorx.ErrInvalidConn } defer func() { if cmd.cb != nil { _ = cmd.cb(c, err) } }() switch cmd.typ { case asyncCmdClose: return el.close(c, nil) case asyncCmdWake: return el.wake(c) case asyncCmdWrite: _, err = c.Write(cmd.param.([]byte)) case asyncCmdWritev: _, err = c.Writev(cmd.param.([][]byte)) default: return errorx.ErrUnsupportedOp } return } */ ================================================ FILE: eventloop_unix_test.go ================================================ //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package gnet import ( "context" "net" "runtime" "runtime/debug" "sync/atomic" "testing" "time" "golang.org/x/sys/unix" "github.com/stretchr/testify/assert" goPool "github.com/panjf2000/gnet/v2/pkg/pool/goroutine" ) func (lb *roundRobinLoadBalancer) register(el *eventloop) { lb.baseLoadBalancer.register(el) registerInitConn(el) } func (lb *leastConnectionsLoadBalancer) register(el *eventloop) { lb.baseLoadBalancer.register(el) registerInitConn(el) } func (lb *sourceAddrHashLoadBalancer) register(el *eventloop) { lb.baseLoadBalancer.register(el) registerInitConn(el) } func registerInitConn(el *eventloop) { for i := 0; i < int(atomic.LoadInt32(&nowEventLoopInitConn)); i++ { c := newStreamConn("tcp", i, el, &unix.SockaddrInet4{}, &net.TCPAddr{}, &net.TCPAddr{}) el.connections.addConn(c, el.idx) } } // nowEventLoopInitConn initializes the number of conn fake data, must be set to 0 after use. var ( nowEventLoopInitConn int32 testBigGC = false ) func BenchmarkGC4El100k(b *testing.B) { oldGc := debug.SetGCPercent(-1) ts1 := benchServeGC(b, "tcp", ":0", true, 4, 100000) b.Run("Run-4-eventloop-100000", func(b *testing.B) { for i := 0; i < b.N; i++ { runtime.GC() } }) _ = ts1.eng.Stop(context.Background()) debug.SetGCPercent(oldGc) } func BenchmarkGC4El200k(b *testing.B) { oldGc := debug.SetGCPercent(-1) ts1 := benchServeGC(b, "tcp", ":0", true, 4, 200000) b.Run("Run-4-eventloop-200000", func(b *testing.B) { for i := 0; i < b.N; i++ { runtime.GC() } }) _ = ts1.eng.Stop(context.Background()) debug.SetGCPercent(oldGc) } func BenchmarkGC4El500k(b *testing.B) { oldGc := debug.SetGCPercent(-1) ts1 := benchServeGC(b, "tcp", ":0", true, 4, 500000) b.Run("Run-4-eventloop-500000", func(b *testing.B) { for i := 0; i < b.N; i++ { runtime.GC() } }) _ = ts1.eng.Stop(context.Background()) debug.SetGCPercent(oldGc) } func benchServeGC(b *testing.B, network, addr string, async bool, elNum int, initConnCount int32) *benchmarkServerGC { ts := &benchmarkServerGC{ tester: b, network: network, addr: addr, async: async, elNum: elNum, initOk: make(chan struct{}), initConnCount: initConnCount, } nowEventLoopInitConn = initConnCount _ = goPool.DefaultWorkerPool.Submit(func() { err := Run(ts, network+"://"+addr, WithLockOSThread(async), WithNumEventLoop(elNum), WithTCPKeepAlive(time.Minute), WithTCPNoDelay(TCPDelay)) assert.NoError(b, err) nowEventLoopInitConn = 0 }) <-ts.initOk return ts } type benchmarkServerGC struct { *BuiltinEventEngine tester *testing.B eng Engine network string addr string async bool elNum int initConnCount int32 initOk chan struct{} } func (s *benchmarkServerGC) OnBoot(eng Engine) (action Action) { s.eng = eng _ = goPool.DefaultWorkerPool.Submit(func() { for s.eng.eng.eventLoops.len() != s.elNum || s.eng.CountConnections() != s.elNum*int(s.initConnCount) { time.Sleep(time.Millisecond) } close(s.initOk) }) return } // TestServeGC generate fake data asynchronously, if you need to test, manually open the comment. func TestServeGC(t *testing.T) { t.Run("gc-loop", func(t *testing.T) { t.Run("1-loop-10000", func(t *testing.T) { if testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 1, 10000) }) t.Run("1-loop-100000", func(t *testing.T) { if !testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 1, 100000) }) t.Run("1-loop-1000000", func(t *testing.T) { if !testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 1, 1000000) }) t.Run("2-loop-10000", func(t *testing.T) { if testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 2, 10000) }) t.Run("2-loop-100000", func(t *testing.T) { if !testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 2, 100000) }) t.Run("2-loop-1000000", func(t *testing.T) { if !testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 2, 1000000) }) t.Run("4-loop-10000", func(t *testing.T) { if testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 4, 10000) }) t.Run("4-loop-100000", func(t *testing.T) { if !testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 4, 100000) }) t.Run("4-loop-1000000", func(t *testing.T) { if !testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 4, 1000000) }) t.Run("16-loop-10000", func(t *testing.T) { if testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 16, 10000) }) t.Run("16-loop-100000", func(t *testing.T) { if !testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 16, 100000) }) t.Run("16-loop-1000000", func(t *testing.T) { if !testBigGC { t.Skipf("Skip when testBigGC=%t", testBigGC) } testServeGC(t, "tcp", ":0", true, true, 16, 1000000) }) }) } func testServeGC(t *testing.T, network, addr string, multicore, async bool, elNum int, initConnCount int32) { ts := &testServerGC{ tester: t, network: network, addr: addr, multicore: multicore, async: async, elNum: elNum, } nowEventLoopInitConn = initConnCount err := Run(ts, network+"://"+addr, WithLockOSThread(async), WithMulticore(multicore), WithNumEventLoop(elNum), WithTCPKeepAlive(time.Minute), WithTCPNoDelay(TCPDelay)) assert.NoError(t, err) nowEventLoopInitConn = 0 } type testServerGC struct { *BuiltinEventEngine tester *testing.T eng Engine network string addr string multicore bool async bool elNum int } func (s *testServerGC) OnBoot(eng Engine) (action Action) { s.eng = eng gcSecs := 5 if testBigGC { gcSecs = 10 } err := goPool.DefaultWorkerPool.Submit(func() { s.GC(gcSecs) }) assert.NoError(s.tester, err) return } func (s *testServerGC) GC(secs int) { defer func() { _ = s.eng.Stop(context.Background()) runtime.GC() }() var gcAllTime, gcAllCount time.Duration gcStart := time.Now() for range time.Tick(time.Second) { gcAllCount++ now := time.Now() runtime.GC() gcTime := time.Since(now) gcAllTime += gcTime s.tester.Log(s.tester.Name(), s.network, "server gc:", gcTime, "average gc time:", gcAllTime/gcAllCount) if time.Since(gcStart) >= time.Second*time.Duration(secs) { break } } } ================================================ FILE: eventloop_windows.go ================================================ // Copyright (c) 2023 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gnet import ( "context" "errors" "fmt" "net" "runtime" "sync/atomic" "time" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" "github.com/panjf2000/gnet/v2/pkg/pool/goroutine" ) type eventloop struct { ch chan any // channel for event-loop idx int // index of event-loop in event-loops eng *engine // engine in loop connCount int32 // number of active connections in event-loop connections map[*conn]struct{} // TCP connection map: fd -> conn eventHandler EventHandler // user eventHandler } func (el *eventloop) Register(ctx context.Context, addr net.Addr) (<-chan RegisteredResult, error) { if el.eng.isShutdown() { return nil, errorx.ErrEngineInShutdown } if addr == nil { return nil, errorx.ErrInvalidNetworkAddress } return el.enroll(nil, addr, FromContext(ctx)) } func (el *eventloop) Enroll(ctx context.Context, c net.Conn) (<-chan RegisteredResult, error) { if el.eng.isShutdown() { return nil, errorx.ErrEngineInShutdown } if c == nil { return nil, errorx.ErrInvalidNetConn } return el.enroll(c, c.RemoteAddr(), FromContext(ctx)) } func (el *eventloop) Execute(ctx context.Context, runnable Runnable) error { if el.eng.isShutdown() { return errorx.ErrEngineInShutdown } if runnable == nil { return errorx.ErrNilRunnable } return goroutine.DefaultWorkerPool.Submit(func() { el.ch <- func() error { return runnable.Run(ctx) } }) } func (el *eventloop) Schedule(context.Context, Runnable, time.Duration) error { return errorx.ErrUnsupportedOp } func (el *eventloop) Close(c Conn) error { return el.close(c.(*conn), nil) } func (el *eventloop) getLogger() logging.Logger { return el.eng.opts.Logger } func (el *eventloop) enroll(c net.Conn, addr net.Addr, ctx any) (resCh chan RegisteredResult, err error) { resCh = make(chan RegisteredResult, 1) err = goroutine.DefaultWorkerPool.Submit(func() { defer close(resCh) var err error if c == nil { if c, err = net.Dial(addr.Network(), addr.String()); err != nil { resCh <- RegisteredResult{Err: err} return } } connOpened := make(chan struct{}) var gc *conn switch addr.Network() { case "tcp", "tcp4", "tcp6", "unix": gc = newStreamConn(el, c, ctx) el.ch <- &openConn{c: gc, cb: func() { close(connOpened) }} goroutine.DefaultWorkerPool.Submit(func() { var buffer [0x10000]byte for { n, err := c.Read(buffer[:]) if err != nil { el.ch <- &netErr{gc, err} return } el.ch <- packTCPConn(gc, buffer[:n]) } }) case "udp", "udp4", "udp6": gc = newUDPConn(el, nil, c, c.LocalAddr(), c.RemoteAddr(), ctx) el.ch <- &openConn{c: gc, cb: func() { close(connOpened) }} goroutine.DefaultWorkerPool.Submit(func() { var buffer [0x10000]byte for { n, err := c.Read(buffer[:]) if err != nil { el.ch <- &netErr{gc, err} return } gc := newUDPConn(el, nil, c, c.LocalAddr(), c.RemoteAddr(), ctx) el.ch <- packUDPConn(gc, buffer[:n]) } }) } <-connOpened resCh <- RegisteredResult{Conn: gc} }) return } func (el *eventloop) incConn(delta int32) { atomic.AddInt32(&el.connCount, delta) } func (el *eventloop) countConn() int32 { return atomic.LoadInt32(&el.connCount) } func (el *eventloop) run() (err error) { defer func() { el.eng.shutdown(err) for c := range el.connections { _ = el.close(c, nil) } }() if el.eng.opts.LockOSThread { runtime.LockOSThread() defer runtime.UnlockOSThread() } for i := range el.ch { switch v := i.(type) { case error: err = v case *netErr: err = el.close(v.c, v.err) case *openConn: err = el.open(v) case *tcpConn: err = el.read(unpackTCPConn(v)) case *udpConn: err = el.readUDP(v.c) case func() error: err = v() } if errors.Is(err, errorx.ErrEngineShutdown) { el.getLogger().Debugf("event-loop(%d) is exiting in terms of the demand from user, %v", el.idx, err) break } else if err != nil { el.getLogger().Debugf("event-loop(%d) got a nonlethal error: %v", el.idx, err) } } return nil } func (el *eventloop) open(oc *openConn) error { if oc.cb != nil { defer oc.cb() } c := oc.c el.connections[c] = struct{}{} el.incConn(1) out, action := el.eventHandler.OnOpen(c) if out != nil { if _, err := c.rawConn.Write(out); err != nil { return err } } return el.handleAction(c, action) } func (el *eventloop) read(c *conn) error { if _, ok := el.connections[c]; !ok { return nil // ignore stale wakes. } action := el.eventHandler.OnTraffic(c) switch action { case None: case Close: return el.close(c, nil) case Shutdown: return errorx.ErrEngineShutdown } _, _ = c.inboundBuffer.Write(c.buffer.B) c.buffer.Reset() return nil } func (el *eventloop) readUDP(c *conn) error { action := el.eventHandler.OnTraffic(c) if action == Shutdown { return errorx.ErrEngineShutdown } c.release() return nil } func (el *eventloop) ticker(ctx context.Context) { if el == nil { return } var ( action Action delay time.Duration timer *time.Timer ) defer func() { if timer != nil { timer.Stop() } }() var shutdown bool for { delay, action = el.eventHandler.OnTick() switch action { case None, Close: case Shutdown: if !shutdown { shutdown = true el.ch <- errorx.ErrEngineShutdown el.getLogger().Debugf("stopping ticker in event-loop(%d) from Tick()", el.idx) } } if timer == nil { timer = time.NewTimer(delay) } else { timer.Reset(delay) } select { case <-ctx.Done(): el.getLogger().Debugf("stopping ticker in event-loop(%d) from Server, error:%v", el.idx, ctx.Err()) return case <-timer.C: } } } func (el *eventloop) wake(c *conn) error { if _, ok := el.connections[c]; !ok { return nil // ignore stale wakes. } action := el.eventHandler.OnTraffic(c) return el.handleAction(c, action) } func (el *eventloop) close(c *conn, err error) error { if _, ok := el.connections[c]; c.rawConn == nil || !ok { return nil // ignore stale wakes. } delete(el.connections, c) el.incConn(-1) action := el.eventHandler.OnClose(c, err) err = c.rawConn.Close() c.release() if err != nil { return fmt.Errorf("failed to close connection=%s in event-loop(%d): %v", c.remoteAddr, el.idx, err) } return el.handleAction(c, action) } func (el *eventloop) handleAction(c *conn, action Action) error { switch action { case None: return nil case Close: return el.close(c, nil) case Shutdown: return errorx.ErrEngineShutdown default: return nil } } ================================================ FILE: gnet.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package gnet implements a high-performance, lightweight, non-blocking, // event-driven networking framework written in pure Go. // // Visit https://gnet.host/ for more details about gnet. package gnet import ( "context" "io" "net" "net/url" "path" "runtime" "strings" "sync" "time" "github.com/panjf2000/gnet/v2/internal/gfd" "github.com/panjf2000/gnet/v2/pkg/buffer/ring" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" "github.com/panjf2000/gnet/v2/pkg/math" ) // Action is an action that occurs after the completion of an event. type Action int const ( // None indicates that no action should occur following an event. None Action = iota // Close closes the connection. Close // Shutdown shutdowns the engine. Shutdown ) // Engine represents an engine context which provides some functions. type Engine struct { // eng is the internal engine struct. eng *engine } // Validate checks whether the engine is available. func (e Engine) Validate() error { if e.eng == nil || len(e.eng.listeners) == 0 { return errorx.ErrEmptyEngine } if e.eng.isShutdown() { return errorx.ErrEngineInShutdown } return nil } // CountConnections counts the number of currently active connections and returns it. func (e Engine) CountConnections() (count int) { if e.Validate() != nil { return -1 } e.eng.eventLoops.iterate(func(_ int, el *eventloop) bool { count += int(el.countConn()) return true }) return } // Register registers the new connection to the event-loop that is chosen // based off of the algorithm set by WithLoadBalancing. // You should call either of the NewNetConnContext or NewNetAddrContext // and pass the returned context to this method. net.Conn will precede // net.Addr if both are present in the context. // // Note that you need to switch to another load-balancing algorithm over // the default RoundRobin when starting the engine, to avoid data race // issue if you plan on calling this method from somewhere later on. func (e Engine) Register(ctx context.Context) (<-chan RegisteredResult, error) { if err := e.Validate(); err != nil { return nil, err } if e.eng.eventLoops.len() == 0 { return nil, errorx.ErrEmptyEngine } c, ok := FromNetConnContext(ctx) if ok { return e.eng.eventLoops.next(c.RemoteAddr()).Enroll(ctx, c) } addr, ok := FromNetAddrContext(ctx) if ok { return e.eng.eventLoops.next(addr).Register(ctx, addr) } return nil, errorx.ErrInvalidNetworkAddress } // Dup returns a copy of the underlying file descriptor of listener. // It is the caller's responsibility to close dupFD when finished. // Closing listener does not affect dupFD, and closing dupFD does not affect listener. // // Note that this method is only available when the engine has only one listener. func (e Engine) Dup() (fd int, err error) { if err := e.Validate(); err != nil { return -1, err } if len(e.eng.listeners) > 1 { return -1, errorx.ErrUnsupportedOp } for _, ln := range e.eng.listeners { fd, err = ln.dup() } return } // DupListener is like Dup, but it duplicates the listener with the given network and address. // This is useful when there are multiple listeners. func (e Engine) DupListener(network, addr string) (int, error) { if err := e.Validate(); err != nil { return -1, err } for _, ln := range e.eng.listeners { if ln.network == network && ln.address == addr { return ln.dup() } } return -1, errorx.ErrInvalidNetworkAddress } // Stop gracefully shuts down this Engine without interrupting any active event-loops, // it waits indefinitely for connections and event-loops to be closed and then shuts down. func (e Engine) Stop(ctx context.Context) error { if err := e.Validate(); err != nil { return err } e.eng.shutdown(nil) ticker := time.NewTicker(shutdownPollInterval) defer ticker.Stop() for { if e.eng.isShutdown() { return nil } select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: } } } /* type asyncCmdType uint8 const ( asyncCmdClose = iota + 1 asyncCmdWake asyncCmdWrite asyncCmdWritev ) type asyncCmd struct { fd gfd.GFD typ asyncCmdType cb AsyncCallback param any } // AsyncWrite writes data to the given connection asynchronously. func (e Engine) AsyncWrite(fd gfd.GFD, p []byte, cb AsyncCallback) error { if err := e.Validate(); err != nil { return err } return e.eng.sendCmd(&asyncCmd{fd: fd, typ: asyncCmdWrite, cb: cb, param: p}, false) } // AsyncWritev is like AsyncWrite, but it accepts a slice of byte slices. func (e Engine) AsyncWritev(fd gfd.GFD, batch [][]byte, cb AsyncCallback) error { if err := e.Validate(); err != nil { return err } return e.eng.sendCmd(&asyncCmd{fd: fd, typ: asyncCmdWritev, cb: cb, param: batch}, false) } // Close closes the given connection. func (e Engine) Close(fd gfd.GFD, cb AsyncCallback) error { if err := e.Validate(); err != nil { return err } return e.eng.sendCmd(&asyncCmd{fd: fd, typ: asyncCmdClose, cb: cb}, false) } // Wake wakes up the given connection. func (e Engine) Wake(fd gfd.GFD, cb AsyncCallback) error { if err := e.Validate(); err != nil { return err } return e.eng.sendCmd(&asyncCmd{fd: fd, typ: asyncCmdWake, cb: cb}, true) } */ // Reader is an interface that consists of a number of methods for reading that Conn must implement. // // Note that the methods in this interface are not concurrency-safe for concurrent use, // you must invoke them within any method in EventHandler. type Reader interface { io.Reader io.WriterTo // Next returns the next n bytes and advances the inbound buffer. // buf must not be used in a new goroutine. Otherwise, use Read instead. // // If the number of the available bytes is less than requested, // a pair of (0, io.ErrShortBuffer) is returned. Next(n int) (buf []byte, err error) // Peek returns the next n bytes without advancing the inbound buffer, // the returned bytes remain valid until a Discard is called. // buf must neither be used in a new goroutine nor anywhere after the call // to Discard, make a copy of buf manually or use Read otherwise. // // If the number of the available bytes is less than requested, // a pair of (0, io.ErrShortBuffer) is returned. Peek(n int) (buf []byte, err error) // Discard advances the inbound buffer with next n bytes, returning the number of bytes discarded. Discard(n int) (discarded int, err error) // InboundBuffered returns the number of bytes that can be read from the current buffer. InboundBuffered() int } // Writer is an interface that consists of a number of methods for writing that Conn must implement. type Writer interface { io.Writer // not concurrency-safe io.ReaderFrom // not concurrency-safe // SendTo transmits a message to the given address, it's not concurrency-safe. // It is available only for UDP sockets, an ErrUnsupportedOp will be returned // when it is called on a non-UDP socket. // This method should be used only when you need to send a message to a specific // address over the UDP socket, otherwise you should use Conn.Write() instead. SendTo(buf []byte, addr net.Addr) (n int, err error) // Writev writes multiple byte slices to remote synchronously, it's not concurrency-safe, // you must invoke it within any method in EventHandler. Writev(bs [][]byte) (n int, err error) // Flush writes any buffered data to the underlying connection, it's not concurrency-safe, // you must invoke it within any method in EventHandler. Flush() error // OutboundBuffered returns the number of bytes that can be read from the current buffer. // it's not concurrency-safe, you must invoke it within any method in EventHandler. OutboundBuffered() int // AsyncWrite writes bytes to remote asynchronously, it's concurrency-safe, // you don't have to invoke it within any method in EventHandler, // usually you would call it in an individual goroutine. // // Note that it will go synchronously with UDP, so it is needless to call // this asynchronous method, we may disable this method for UDP and just // return ErrUnsupportedOp in the future, therefore, please don't rely on // this method to do something important under UDP, if you're working with UDP, // just call Conn.Write to send back your data. AsyncWrite(buf []byte, callback AsyncCallback) (err error) // AsyncWritev writes multiple byte slices to remote asynchronously, // you don't have to invoke it within any method in EventHandler, // usually you would call it in an individual goroutine. AsyncWritev(bs [][]byte, callback AsyncCallback) (err error) } // AsyncCallback is a callback that will be invoked after the asynchronous function finishes. // // Note that the parameter gnet.Conn might have been already released when it's UDP protocol, // thus it shouldn't be accessed. // This callback will be executed in event-loop, thus it must not block, otherwise, // it blocks the event-loop. type AsyncCallback func(c Conn, err error) error // Socket is a set of functions which manipulate the underlying file descriptor of a connection. // // Note that the methods in this interface are concurrency-safe for concurrent use, // you don't have to invoke them within any method in EventHandler. type Socket interface { // Gfd returns the gfd of socket. // Gfd() gfd.GFD // Fd returns the underlying file descriptor. Fd() int // Dup returns a copy of the underlying file descriptor. // It is the caller's responsibility to close fd when finished. // Closing c does not affect fd, and closing fd does not affect c. // // The returned file descriptor is different from the // connection. Attempting to change the properties of the original // using this duplicate may or may not have the desired effect. Dup() (int, error) // SetReadBuffer sets the size of the operating system's // receive buffer associated with the connection. SetReadBuffer(size int) error // SetWriteBuffer sets the size of the operating system's // transmit buffer associated with the connection. SetWriteBuffer(size int) error // SetLinger sets the behavior of Close on a connection which still // has data waiting to be sent or to be acknowledged. // // If secs < 0 (the default), the operating system finishes sending the // data in the background. // // If secs == 0, the operating system discards any unsent or // unacknowledged data. // // If secs > 0, the data is sent in the background as with sec < 0. On // some operating systems after sec seconds have elapsed any remaining // unsent data may be discarded. SetLinger(secs int) error // SetKeepAlivePeriod tells the operating system to send keep-alive // messages on the connection and sets period between TCP keep-alive probes. SetKeepAlivePeriod(d time.Duration) error // SetKeepAlive enables/disables the TCP keepalive with all socket options: // TCP_KEEPIDLE, TCP_KEEPINTVL and TCP_KEEPCNT. idle is the value for TCP_KEEPIDLE, // intvl is the value for TCP_KEEPINTVL, cnt is the value for TCP_KEEPCNT, // ignored when enabled is false. // // With TCP keep-alive enabled, idle is the time (in seconds) the connection // needs to remain idle before TCP starts sending keep-alive probes, // intvl is the time (in seconds) between individual keep-alive probes. // TCP will drop the connection after sending cnt probes without getting // any replies from the peer; then the socket is destroyed, and OnClose // is triggered. // // If one of idle, intvl, or cnt is less than 1, an error is returned. SetKeepAlive(enabled bool, idle, intvl time.Duration, cnt int) error // SetNoDelay controls whether the operating system should delay // packet transmission in hopes of sending fewer packets (Nagle's // algorithm). // The default is true (no delay), meaning that data is sent as soon as possible after a Write. SetNoDelay(noDelay bool) error } // Runnable defines the common protocol of an execution on an event-loop. // This interface should be implemented and passed to an event-loop in some way, // then the event-loop will invoke Run to perform the execution. // !!!Caution: Run must not contain any blocking operations like heavy disk or // network I/O, or else it will block the event-loop. type Runnable interface { // Run is about to be executed by the event-loop. Run(ctx context.Context) error } // RunnableFunc is an adapter to allow the use of ordinary function as a Runnable. type RunnableFunc func(ctx context.Context) error // Run executes the RunnableFunc itself. func (fn RunnableFunc) Run(ctx context.Context) error { return fn(ctx) } // RegisteredResult is the result of a Register call. type RegisteredResult struct { Conn Conn Err error } // EventLoop provides a set of methods for manipulating the event-loop. type EventLoop interface { // Register connects to the given address and registers the connection to the current event-loop, // it's concurrency-safe. Register(ctx context.Context, addr net.Addr) (<-chan RegisteredResult, error) // Enroll is like Register, but it accepts an established net.Conn instead of a net.Addr, // it's concurrency-safe. Enroll(ctx context.Context, c net.Conn) (<-chan RegisteredResult, error) // Execute will execute the given runnable on the event-loop at some time in the future, // it's concurrency-safe. Execute(ctx context.Context, runnable Runnable) error // Schedule is like Execute, but it allows you to specify when the runnable is executed. // In other words, the runnable will be executed when the delay duration is reached, // it's concurrency-safe. // TODO(panjf2000): not supported yet, implement this. Schedule(ctx context.Context, runnable Runnable, delay time.Duration) error // Close closes the given Conn that belongs to the current event-loop. // It must be called on the same event-loop that the connection belongs to. // This method is not concurrency-safe, you must invoke it on the event loop. Close(Conn) error } // Conn is an interface of underlying connection. type Conn interface { Reader // all methods in Reader are not concurrency-safe. Writer // some methods in Writer are concurrency-safe, some are not. Socket // all methods in Socket are concurrency-safe. // Context returns a user-defined context, it's not concurrency-safe, // you must invoke it within any method in EventHandler. Context() (ctx any) // EventLoop returns the event-loop that the connection belongs to. // The returned EventLoop is concurrency-safe. EventLoop() EventLoop // SetContext sets a user-defined context, it's not concurrency-safe, // you must invoke it within any method in EventHandler. SetContext(ctx any) // LocalAddr is the connection's local socket address, it's not concurrency-safe, // you must invoke it within any method in EventHandler. LocalAddr() net.Addr // RemoteAddr is the connection's remote address, it's not concurrency-safe, // you must invoke it within any method in EventHandler. RemoteAddr() net.Addr // Wake triggers an OnTraffic event for the current connection, it's concurrency-safe. Wake(callback AsyncCallback) error // CloseWithCallback closes the current connection, it's concurrency-safe. // Usually you should provide a non-nil callback for this method, // otherwise your better choice is Close(). CloseWithCallback(callback AsyncCallback) error // Close closes the current connection, implements net.Conn, it's concurrency-safe. Close() error // SetDeadline implements net.Conn. SetDeadline(time.Time) error // SetReadDeadline implements net.Conn. SetReadDeadline(time.Time) error // SetWriteDeadline implements net.Conn. SetWriteDeadline(time.Time) error } type ( // EventHandler represents the engine events' callbacks for the Run call. // Each event has an Action return value that is used manage the state // of the connection and engine. EventHandler interface { // OnBoot fires when the engine is ready for accepting connections. // The parameter engine has information and various utilities. OnBoot(eng Engine) (action Action) // OnShutdown fires when the engine is being shut down, it is called right after // all event-loops and connections are closed. OnShutdown(eng Engine) // OnOpen fires when a new connection has been opened. // // The Conn c has information about the connection such as its local and remote addresses. // The parameter out is the return value which is going to be sent back to the remote. // Sending large amounts of data back to the remote in OnOpen is usually not recommended. OnOpen(c Conn) (out []byte, action Action) // OnClose fires when a connection has been closed. // The parameter err is the last known connection error. OnClose(c Conn, err error) (action Action) // OnTraffic fires when a socket receives data from the remote. // // Also check out the comments on Reader and Writer interfaces. OnTraffic(c Conn) (action Action) // OnTick fires immediately after the engine starts and will fire again // following the duration specified by the delay return value. OnTick() (delay time.Duration, action Action) } // BuiltinEventEngine is a built-in implementation of EventHandler which feeds // each method with an empty implementation, you can embed it within your custom // struct when you don't intend to implement the entire EventHandler. BuiltinEventEngine struct{} ) // OnBoot fires when the engine is ready for accepting connections. // The parameter engine has information and various utilities. func (*BuiltinEventEngine) OnBoot(_ Engine) (action Action) { return } // OnShutdown fires when the engine is being shut down, it is called right after // all event-loops and connections are closed. func (*BuiltinEventEngine) OnShutdown(_ Engine) { } // OnOpen fires when a new connection has been opened. // The parameter out is the return value which is going to be sent back to the remote. func (*BuiltinEventEngine) OnOpen(_ Conn) (out []byte, action Action) { return } // OnClose fires when a connection has been closed. // The parameter err is the last known connection error. func (*BuiltinEventEngine) OnClose(_ Conn, _ error) (action Action) { return } // OnTraffic fires when a local socket receives data from the remote. func (*BuiltinEventEngine) OnTraffic(_ Conn) (action Action) { return } // OnTick fires immediately after the engine starts and will fire again // following the duration specified by the delay return value. func (*BuiltinEventEngine) OnTick() (delay time.Duration, action Action) { return } // MaxStreamBufferCap is the default buffer size for each stream-oriented connection(TCP/Unix). var MaxStreamBufferCap = 64 * 1024 // 64KB func createListeners(addrs []string, opts ...Option) ([]*listener, *Options, error) { options := loadOptions(opts...) logger, logFlusher := logging.GetDefaultLogger(), logging.GetDefaultFlusher() if options.Logger == nil { if options.LogPath != "" { logger, logFlusher, _ = logging.CreateLoggerAsLocalFile(options.LogPath, options.LogLevel) } options.Logger = logger } else { logger = options.Logger logFlusher = nil } logging.SetDefaultLoggerAndFlusher(logger, logFlusher) logging.Debugf("default logging level is %s", logging.LogLevel()) // The maximum number of operating system threads that the Go program can use is initially set to 10000, // which should also be the maximum number of I/O event-loops locked to OS threads that users can start up. if options.LockOSThread && options.NumEventLoop > 10000 { logging.Errorf("too many event-loops under LockOSThread mode, should be less than 10,000 "+ "while you are trying to set up %d\n", options.NumEventLoop) return nil, nil, errorx.ErrTooManyEventLoopThreads } if options.EdgeTriggeredIOChunk > 0 { options.EdgeTriggeredIO = true options.EdgeTriggeredIOChunk = math.CeilToPowerOfTwo(options.EdgeTriggeredIOChunk) } else if options.EdgeTriggeredIO { options.EdgeTriggeredIOChunk = 1 << 20 // 1MB } rbc := options.ReadBufferCap switch { case rbc <= 0: options.ReadBufferCap = MaxStreamBufferCap case rbc <= ring.DefaultBufferSize: options.ReadBufferCap = ring.DefaultBufferSize default: options.ReadBufferCap = math.CeilToPowerOfTwo(rbc) } wbc := options.WriteBufferCap switch { case wbc <= 0: options.WriteBufferCap = MaxStreamBufferCap case wbc <= ring.DefaultBufferSize: options.WriteBufferCap = ring.DefaultBufferSize default: options.WriteBufferCap = math.CeilToPowerOfTwo(wbc) } var hasUDP, hasUnix bool for _, addr := range addrs { proto, _, err := parseProtoAddr(addr) if err != nil { return nil, nil, err } hasUDP = hasUDP || strings.HasPrefix(proto, "udp") hasUnix = hasUnix || proto == "unix" } // SO_REUSEPORT enables duplicate address and port bindings across various // Unix-like OSs, whereas there is platform-specific inconsistency: // Linux implemented SO_REUSEPORT with load balancing for incoming connections // while *BSD implemented it for only binding to the same address and port, which // makes it pointless to enable SO_REUSEPORT on *BSD and Darwin for gnet with // multiple event-loops because only the first or last event-loop will be constantly // woken up to accept incoming connections and handle I/O events while the rest of // event-loops remain idle. // Thus, we disable SO_REUSEPORT on *BSD and Darwin by default. // // Note that FreeBSD 12 introduced a new socket option named SO_REUSEPORT_LB // with the capability of load balancing, it's the equivalent of Linux's SO_REUSEPORT. // Also note that DragonFlyBSD 3.6.0 extended SO_REUSEPORT to distribute workload to // available sockets, which makes it the same as Linux's SO_REUSEPORT. goos := runtime.GOOS if options.ReusePort && (options.Multicore || options.NumEventLoop > 1) && (goos != "linux" && goos != "dragonfly" && goos != "freebsd") { options.ReusePort = false } // Despite the fact that SO_REUSEPORT can be set on a Unix domain socket // via setsockopt() without reporting an error, SO_REUSEPORT is actually // not supported for sockets of AF_UNIX. Thus, we avoid setting it on the // Unix domain sockets. // As of this commit https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=5b0af621c3f6, // EOPNOTSUPP will be returned when trying to set SO_REUSEPORT on an AF_UNIX socket on Linux. We therefore // avoid setting it on Unix domain sockets on all UNIX-like platforms to keep this behavior consistent. if options.ReusePort && hasUnix { options.ReusePort = false } // If there is UDP address in the list, we have no choice but to enable SO_REUSEPORT anyway, // also disable edge-triggered I/O for UDP by default. if hasUDP { options.ReusePort = true options.EdgeTriggeredIO = false } listeners := make([]*listener, len(addrs)) for i, a := range addrs { proto, addr, err := parseProtoAddr(a) if err != nil { return nil, nil, err } ln, err := initListener(proto, addr, options) if err != nil { return nil, nil, err } listeners[i] = ln } return listeners, options, nil } // Run starts handling events on the specified address. // // Address should use a scheme prefix and be formatted // like `tcp://192.168.0.10:9851` or `unix://socket`. // Valid network schemes: // // tcp - bind to both IPv4 and IPv6 // tcp4 - IPv4 // tcp6 - IPv6 // udp - bind to both IPv4 and IPv6 // udp4 - IPv4 // udp6 - IPv6 // unix - Unix Domain Socket // // The "tcp" network scheme is assumed when one is not specified. func Run(eventHandler EventHandler, protoAddr string, opts ...Option) error { listeners, options, err := createListeners([]string{protoAddr}, opts...) if err != nil { return err } defer func() { for _, ln := range listeners { ln.close() } logging.Cleanup() }() return run(eventHandler, listeners, options, []string{protoAddr}) } // Rotate is like Run but accepts multiple network addresses. func Rotate(eventHandler EventHandler, addrs []string, opts ...Option) error { listeners, options, err := createListeners(addrs, opts...) if err != nil { return err } defer func() { for _, ln := range listeners { ln.close() } logging.Cleanup() }() return run(eventHandler, listeners, options, addrs) } var ( allEngines sync.Map // shutdownPollInterval is how often we poll to check whether engine has been shut down during gnet.Stop(). shutdownPollInterval = 500 * time.Millisecond ) // Stop gracefully shuts down the engine without interrupting any active event-loops, // it waits indefinitely for connections and event-loops to be closed and then shuts down. // // Deprecated: The global Stop only shuts down the last registered Engine with the same // protocol and IP:Port as the previous Engine's, which can lead to leaks of Engine if // you invoke gnet.Run multiple times using the same protocol and IP:Port under the // condition that WithReuseAddr(true) and WithReusePort(true) are enabled. // Use Engine.Stop instead. func Stop(ctx context.Context, protoAddr string) error { var eng *engine if s, ok := allEngines.Load(protoAddr); ok { eng = s.(*engine) eng.shutdown(nil) defer allEngines.Delete(protoAddr) } else { return errorx.ErrEngineInShutdown } if eng.isShutdown() { return errorx.ErrEngineInShutdown } ticker := time.NewTicker(shutdownPollInterval) defer ticker.Stop() for { if eng.isShutdown() { return nil } select { case <-ctx.Done(): return ctx.Err() case <-ticker.C: } } } func parseProtoAddr(protoAddr string) (string, string, error) { // Percent-encode "%" in the address to avoid url.Parse error. // This is for cases like this: udp://[ff02::3%lo0]:9991 protoAddr = strings.ReplaceAll(protoAddr, "%", "%25") if runtime.GOOS == "windows" { if strings.HasPrefix(protoAddr, "unix://") { parts := strings.SplitN(protoAddr, "://", 2) if parts[1] == "" { return "", "", errorx.ErrInvalidNetworkAddress } return parts[0], parts[1], nil } } u, err := url.Parse(protoAddr) if err != nil { return "", "", err } switch u.Scheme { case "": return "", "", errorx.ErrInvalidNetworkAddress case "tcp", "tcp4", "tcp6", "udp", "udp4", "udp6": if u.Host == "" || u.Path != "" { return "", "", errorx.ErrInvalidNetworkAddress } return u.Scheme, u.Host, nil case "unix": hostPath := path.Join(u.Host, u.Path) if hostPath == "" { return "", "", errorx.ErrInvalidNetworkAddress } return u.Scheme, hostPath, nil default: return "", "", errorx.ErrUnsupportedProtocol } } func determineEventLoops(opts *Options) int { numEventLoop := 1 if opts.Multicore { numEventLoop = runtime.NumCPU() } if opts.NumEventLoop > 0 { numEventLoop = opts.NumEventLoop } if numEventLoop > gfd.EventLoopIndexMax { numEventLoop = gfd.EventLoopIndexMax } return numEventLoop } ================================================ FILE: gnet_test.go ================================================ package gnet import ( "bufio" "bytes" "context" crand "crypto/rand" "encoding/binary" "errors" "io" "math" "math/rand" "net" "os" "path/filepath" "runtime" "strings" "sync" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap" "golang.org/x/sync/errgroup" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" bbPool "github.com/panjf2000/gnet/v2/pkg/pool/bytebuffer" goPool "github.com/panjf2000/gnet/v2/pkg/pool/goroutine" ) var ( datagramLen = 1024 streamLen = 1024 * 1024 ) type testConf struct { et bool etChunk int reuseport bool multicore bool async bool writev bool clients int lb LoadBalancing } // testUnixAddr uses os.MkdirTemp to get a name that is unique. // See https://github.com/golang/go/issues/62614. func testUnixAddr(t testing.TB) string { // Pass an empty pattern to get a directory name that is as short as possible. // If we end up with a name longer than the sun_path field in the sockaddr_un // struct, we won't be able to make the syscall to open the socket. d, err := os.MkdirTemp("", "") require.NoError(t, err) t.Cleanup(func() { require.NoError(t, os.RemoveAll(d)) }) return filepath.Join(d, "sock") } func TestServer(t *testing.T) { // start an engine // connect 10 clients // each client will pipe random data for 1-3 seconds. // the writes to the engine will be random sizes. 0KB - 1MB. // the engine will echo back the data. // waits for graceful connection closing. t.Run("poll-LT", func(t *testing.T) { t.Run("tcp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{false, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{false, 0, false, true, false, false, 10, LeastConnections}) }) }) t.Run("tcp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{false, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{false, 0, false, true, true, false, 10, LeastConnections}) }) }) t.Run("tcp-async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{false, 0, false, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{false, 0, false, true, true, true, 10, LeastConnections}) }) }) t.Run("udp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"udp://:9991"}, &testConf{false, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"udp://:9992"}, &testConf{false, 0, false, true, false, false, 10, LeastConnections}) }) }) t.Run("udp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"udp://:9991"}, &testConf{false, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"udp://:9992"}, &testConf{false, 0, false, true, true, false, 10, LeastConnections}) }) }) t.Run("unix", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, false, true, false, false, 10, SourceAddrHash}) }) }) t.Run("unix-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, false, true, true, false, 10, SourceAddrHash}) }) }) t.Run("unix-async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, false, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, false, true, true, true, 10, SourceAddrHash}) }) }) }) t.Run("poll-ET", func(t *testing.T) { t.Run("tcp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{true, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{true, 0, false, true, false, false, 10, LeastConnections}) }) }) t.Run("tcp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{true, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{true, 0, false, true, true, false, 10, LeastConnections}) }) }) t.Run("tcp-async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{true, 0, false, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{true, 0, false, true, true, true, 10, LeastConnections}) }) }) t.Run("udp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"udp://:9991"}, &testConf{true, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"udp://:9992"}, &testConf{true, 0, false, true, false, false, 10, LeastConnections}) }) }) t.Run("udp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"udp://:9991"}, &testConf{true, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"udp://:9992"}, &testConf{true, 0, false, true, true, false, 10, LeastConnections}) }) }) t.Run("unix", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, false, true, false, false, 10, SourceAddrHash}) }) }) t.Run("unix-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, false, true, true, false, 10, SourceAddrHash}) }) }) t.Run("unix-async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, false, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, false, true, true, true, 10, SourceAddrHash}) }) }) }) t.Run("poll-ET-chunk", func(t *testing.T) { t.Run("tcp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{true, 1 << 18, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{true, 1 << 19, false, true, false, false, 10, LeastConnections}) }) }) t.Run("tcp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{true, 1 << 18, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{true, 1 << 19, false, true, true, false, 10, LeastConnections}) }) }) t.Run("tcp-async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{true, 1 << 18, false, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{true, 1 << 19, false, true, true, true, 10, LeastConnections}) }) }) t.Run("udp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"udp://:9991"}, &testConf{true, 1 << 18, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"udp://:9992"}, &testConf{true, 1 << 19, false, true, false, false, 10, LeastConnections}) }) }) t.Run("udp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"udp://:9991"}, &testConf{true, 1 << 18, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"udp://:9992"}, &testConf{true, 1 << 19, false, true, true, false, 10, LeastConnections}) }) }) t.Run("unix", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 1 << 18, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 1 << 19, false, true, false, false, 10, SourceAddrHash}) }) }) t.Run("unix-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 1 << 18, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 1 << 19, false, true, true, false, 10, SourceAddrHash}) }) }) t.Run("unix-async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 1 << 18, false, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 1 << 19, false, true, true, true, 10, SourceAddrHash}) }) }) }) t.Run("poll-reuseport-LT", func(t *testing.T) { t.Run("tcp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{false, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{false, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("tcp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{false, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{false, 0, true, true, true, false, 10, LeastConnections}) }) }) t.Run("tcp-async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{false, 0, true, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{false, 0, true, true, true, true, 10, LeastConnections}) }) }) t.Run("udp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"udp://:9991"}, &testConf{false, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"udp://:9992"}, &testConf{false, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("udp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"udp://:9991"}, &testConf{false, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"udp://:9992"}, &testConf{false, 0, true, true, true, false, 10, LeastConnections}) }) }) t.Run("unix", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("unix-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, true, true, true, false, 10, LeastConnections}) }) }) t.Run("unix-async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, true, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{false, 0, true, true, true, true, 10, LeastConnections}) }) }) }) t.Run("poll-reuseport-ET", func(t *testing.T) { t.Run("tcp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{true, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{true, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("tcp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{true, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{true, 0, true, true, true, false, 10, LeastConnections}) }) }) t.Run("tcp-async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991"}, &testConf{true, 0, true, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9992"}, &testConf{true, 0, true, true, true, true, 10, LeastConnections}) }) }) t.Run("udp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"udp://:9991"}, &testConf{true, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"udp://:9992"}, &testConf{true, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("udp-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"udp://:9991"}, &testConf{true, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"udp://:9992"}, &testConf{true, 0, true, true, true, false, 10, LeastConnections}) }) }) t.Run("unix", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("unix-async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, true, true, true, false, 10, LeastConnections}) }) }) t.Run("unix-async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, true, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"unix://" + testUnixAddr(t)}, &testConf{true, 0, true, true, true, true, 10, LeastConnections}) }) }) }) t.Run("poll-multi-addrs-LT", func(t *testing.T) { t.Run("sync", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "udp://:9993", "udp://:9994", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "udp://:9997", "udp://:9998", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, false, true, false, false, 10, LeastConnections}) }) }) t.Run("sync-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, false, false, false, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, false, true, false, true, 10, LeastConnections}) }) }) t.Run("async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "udp://:9993", "udp://:9994", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "udp://:9997", "udp://:9998", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, false, true, true, false, 10, LeastConnections}) }) }) t.Run("async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, false, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, false, true, true, true, 10, LeastConnections}) }) }) }) t.Run("poll-multi-addrs-reuseport-LT", func(t *testing.T) { t.Run("sync", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "udp://:9993", "udp://:9994", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "udp://:9997", "udp://:9998", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("sync-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, true, false, false, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, true, true, false, true, 10, LeastConnections}) }) }) t.Run("async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "udp://:9993", "udp://:9994", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "udp://:9997", "udp://:9998", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, true, true, true, false, 10, LeastConnections}) }) }) t.Run("async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, true, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{false, 0, true, true, true, true, 10, LeastConnections}) }) }) }) t.Run("poll-multi-addrs-ET", func(t *testing.T) { t.Run("sync", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "udp://:9993", "udp://:9994", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, false, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "udp://:9997", "udp://:9998", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, false, true, false, false, 10, LeastConnections}) }) }) t.Run("sync-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, false, false, false, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, false, true, false, true, 10, LeastConnections}) }) }) t.Run("async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "udp://:9993", "udp://:9994", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, false, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "udp://:9997", "udp://:9998", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, false, true, true, false, 10, LeastConnections}) }) }) t.Run("async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, false, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, false, true, true, true, 10, LeastConnections}) }) }) }) t.Run("poll-multi-addrs-reuseport-ET", func(t *testing.T) { t.Run("sync", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "udp://:9993", "udp://:9994", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, true, false, false, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "udp://:9997", "udp://:9998", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, true, true, false, false, 10, LeastConnections}) }) }) t.Run("sync-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, true, false, false, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, true, true, false, true, 10, LeastConnections}) }) }) t.Run("async", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "udp://:9993", "udp://:9994", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, true, false, true, false, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "udp://:9997", "udp://:9998", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, true, true, true, false, 10, LeastConnections}) }) }) t.Run("async-writev", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9991", "tcp://:9992", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, true, false, true, true, 10, RoundRobin}) }) t.Run("N-loop", func(t *testing.T) { runServer(t, []string{"tcp://:9995", "tcp://:9996", "unix://" + testUnixAddr(t), "unix://" + testUnixAddr(t)}, &testConf{true, 0, true, true, true, true, 10, LeastConnections}) }) }) }) } type testServer struct { *BuiltinEventEngine tester *testing.T eng Engine addrs []string multicore bool async bool writev bool nclients int started int32 connected int32 disconnected int32 clientActive int32 } func (s *testServer) OnBoot(eng Engine) (action Action) { s.eng = eng if len(s.addrs) > 1 { fd, err := s.eng.Dup() assert.ErrorIsf(s.tester, err, errorx.ErrUnsupportedOp, "dup error") assert.EqualValuesf(s.tester, -1, fd, "expected fd: -1, but got: %d", fd) addr := s.addrs[rand.Intn(len(s.addrs))] network, address, _ := parseProtoAddr(addr) fd, err = s.eng.DupListener(network, address) assert.NoErrorf(s.tester, err, "DupListener error") assert.Greaterf(s.tester, fd, 2, "expected duplicated fd: > 2, but got: %d", fd) assert.NoErrorf(s.tester, SysClose(fd), "close fd error") // Test invalid input fd, err = s.eng.DupListener("tcp", "abc") assert.ErrorIsf(s.tester, err, errorx.ErrInvalidNetworkAddress, "expected ErrInvalidNetworkAddress") assert.EqualValuesf(s.tester, -1, fd, "expected fd: -1, but got: %d", fd) } else { fd, err := s.eng.Dup() assert.NoErrorf(s.tester, err, "Dup error") assert.Greaterf(s.tester, fd, 2, "expected duplicated fd: > 2, but got: %d", fd) assert.NoErrorf(s.tester, SysClose(fd), "close fd error") } return } func (s *testServer) OnOpen(c Conn) (out []byte, action Action) { c.SetContext(c) atomic.AddInt32(&s.connected, 1) out = []byte("andypan\r\n") assert.NotNil(s.tester, c.LocalAddr(), "nil local addr") assert.NotNil(s.tester, c.RemoteAddr(), "nil remote addr") return } func (s *testServer) OnShutdown(_ Engine) { if len(s.addrs) > 1 { fd, err := s.eng.Dup() assert.ErrorIsf(s.tester, err, errorx.ErrUnsupportedOp, "dup error") assert.EqualValuesf(s.tester, -1, fd, "expected fd: -1, but got: %d", fd) addr := s.addrs[rand.Intn(len(s.addrs))] network, address, _ := parseProtoAddr(addr) fd, err = s.eng.DupListener(network, address) assert.NoErrorf(s.tester, err, "DupListener error") assert.Greaterf(s.tester, fd, 2, "expected duplicated fd: > 2, but got: %d", fd) assert.NoErrorf(s.tester, SysClose(fd), "close fd error") // Test invalid input fd, err = s.eng.DupListener("tcp", "abc") assert.ErrorIsf(s.tester, err, errorx.ErrInvalidNetworkAddress, "expected ErrInvalidNetworkAddress") assert.EqualValuesf(s.tester, -1, fd, "expected fd: -1, but got: %d", fd) } else { fd, err := s.eng.Dup() assert.NoErrorf(s.tester, err, "Dup error") assert.Greaterf(s.tester, fd, 2, "expected duplicated fd: > 2, but got: %d", fd) assert.NoErrorf(s.tester, SysClose(fd), "close fd error") } } func (s *testServer) OnClose(c Conn, err error) (action Action) { if err != nil { logging.Debugf("error occurred on closed, %v\n", err) } assert.Equal(s.tester, c.Context(), c, "invalid context") atomic.AddInt32(&s.disconnected, 1) return } func (s *testServer) OnTraffic(c Conn) (action Action) { if s.async { buf := bbPool.Get() n, err := c.WriteTo(buf) assert.NoError(s.tester, err, "WriteTo error") assert.Greater(s.tester, n, int64(0), "WriteTo error") if c.LocalAddr().Network() == "tcp" || c.LocalAddr().Network() == "unix" { err = goPool.DefaultWorkerPool.Submit( func() { if s.writev { mid := buf.Len() / 2 bs := make([][]byte, 2) bs[0] = buf.B[:mid] bs[1] = buf.B[mid:] err := c.AsyncWritev(bs, func(c Conn, err error) error { if c.RemoteAddr() != nil { logging.Debugf("conn=%s done writev: %v", c.RemoteAddr().String(), err) } bbPool.Put(buf) return nil }) assert.NoError(s.tester, err, "AsyncWritev error") } else { err := c.AsyncWrite(buf.Bytes(), func(c Conn, err error) error { if c.RemoteAddr() != nil { logging.Debugf("conn=%s done write: %v", c.RemoteAddr().String(), err) } bbPool.Put(buf) return nil }) assert.NoError(s.tester, err, "AsyncWritev error") } }) assert.NoError(s.tester, err) return } else if c.LocalAddr().Network() == "udp" { err := goPool.DefaultWorkerPool.Submit( func() { err := c.AsyncWrite(buf.Bytes(), nil) assert.NoError(s.tester, err, "AsyncWritev error") }) assert.NoError(s.tester, err) return } return } buf, err := c.Next(-1) assert.NoError(s.tester, err, "reading data error") var n int if s.writev { mid := len(buf) / 2 n, err = c.Writev([][]byte{buf[:mid], buf[mid:]}) } else { n, err = c.Write(buf) } assert.NoError(s.tester, err, "writev error") assert.EqualValues(s.tester, n, len(buf), "short writev") // Only for code coverage of testing. if !s.multicore { assert.NoError(s.tester, c.Flush(), "flush error") _ = c.Fd() fd, err := c.Dup() assert.NoError(s.tester, err, "dup error") assert.Greaterf(s.tester, fd, 2, "expected fd: > 2, but got: %d", fd) assert.NoError(s.tester, SysClose(fd), "close error") // TODO(panjf2000): somehow these two system calls will fail with Unix Domain Socket, // returning "invalid argument" error on macOS in Github actions intermittently, // try to figure it out. if c.LocalAddr().Network() == "unix" && runtime.GOOS == "darwin" { _ = c.SetReadBuffer(streamLen) _ = c.SetWriteBuffer(streamLen) } else { assert.NoError(s.tester, c.SetReadBuffer(streamLen), "set read buffer error") assert.NoError(s.tester, c.SetWriteBuffer(streamLen), "set write buffer error") } if c.LocalAddr().Network() == "tcp" { assert.NoError(s.tester, c.SetLinger(1), "set linger error") assert.NoError(s.tester, c.SetNoDelay(false), "set no delay error") assert.NoError(s.tester, c.SetKeepAlivePeriod(time.Minute), "set keepalive period error") assert.EqualError(s.tester, c.SetKeepAlive(true, 0, 0, 0), "invalid time duration", "set keepalive error") assert.NoError(s.tester, c.SetKeepAlive(false, 0, 0, 0), "set keepalive error") assert.NoError(s.tester, c.SetKeepAlive(true, time.Minute*10, time.Minute, 10), "set keepalive error") } else { assert.ErrorIs(s.tester, c.SetKeepAlivePeriod(time.Minute), errorx.ErrUnsupportedOp, "non-TCP connection should not support keepalive") assert.ErrorIs(s.tester, c.SetKeepAlive(true, 0, 0, 0), errorx.ErrUnsupportedOp, "non-TCP connection should not support keepalive") } assert.Zero(s.tester, c.InboundBuffered(), "inbound buffer error") assert.GreaterOrEqual(s.tester, c.OutboundBuffered(), 0, "outbound buffer error") n, err := c.Discard(1) assert.NoErrorf(s.tester, err, "discard error") assert.Zerof(s.tester, n, "discard error") assert.ErrorIs(s.tester, c.SetDeadline(time.Now().Add(time.Second)), errorx.ErrUnsupportedOp) assert.ErrorIs(s.tester, c.SetReadDeadline(time.Now().Add(time.Second)), errorx.ErrUnsupportedOp) assert.ErrorIs(s.tester, c.SetWriteDeadline(time.Now().Add(time.Second)), errorx.ErrUnsupportedOp) if c.LocalAddr().Network() == "udp" { n, err := c.Writev([][]byte{}) assert.ErrorIs(s.tester, err, errorx.ErrUnsupportedOp, "udp Writev error") assert.Zero(s.tester, n, "udp Writev error") err = c.AsyncWritev([][]byte{}, nil) assert.ErrorIs(s.tester, err, errorx.ErrUnsupportedOp, "udp Writev error") _, err = c.SendTo(buf, nil) assert.ErrorIsf(s.tester, err, errorx.ErrInvalidNetworkAddress, "got error: %v, expected error: %v", err, errorx.ErrInvalidNetworkAddress) } else { _, err = c.SendTo(buf, c.RemoteAddr()) assert.ErrorIsf(s.tester, err, errorx.ErrUnsupportedOp, "got error: %v, expected error: %v", err, errorx.ErrUnsupportedOp) } } return } func (s *testServer) OnTick() (delay time.Duration, action Action) { delay = 100 * time.Millisecond if atomic.CompareAndSwapInt32(&s.started, 0, 1) { for _, protoAddr := range s.addrs { proto, addr, err := parseProtoAddr(protoAddr) assert.NoError(s.tester, err) for i := 0; i < s.nclients; i++ { atomic.AddInt32(&s.clientActive, 1) err := goPool.DefaultWorkerPool.Submit(func() { startClient(s.tester, proto, addr, s.multicore, s.async, 0, nil) atomic.AddInt32(&s.clientActive, -1) }) assert.NoError(s.tester, err) } } } if atomic.LoadInt32(&s.clientActive) == 0 { var streamAddrs int for _, addr := range s.addrs { if !strings.HasPrefix(addr, "udp") { streamAddrs++ } } streamConns := s.nclients * streamAddrs disconnected := atomic.LoadInt32(&s.disconnected) if int(disconnected) == streamConns && disconnected == atomic.LoadInt32(&s.connected) { action = Shutdown assert.EqualValues(s.tester, 0, s.eng.CountConnections()) } } return } func runServer(t *testing.T, addrs []string, conf *testConf) { ts := &testServer{ tester: t, addrs: addrs, multicore: conf.multicore, async: conf.async, writev: conf.writev, nclients: conf.clients, } var err error if len(addrs) > 1 { err = Rotate(ts, addrs, WithEdgeTriggeredIO(conf.et), WithEdgeTriggeredIOChunk(conf.etChunk), WithLockOSThread(conf.async), WithMulticore(conf.multicore), WithReusePort(conf.reuseport), WithTicker(true), WithTCPKeepAlive(time.Minute), WithTCPKeepInterval(time.Second*10), WithTCPKeepCount(10), WithTCPNoDelay(TCPNoDelay), WithLoadBalancing(conf.lb)) } else { err = Run(ts, addrs[0], WithEdgeTriggeredIO(conf.et), WithEdgeTriggeredIOChunk(conf.etChunk), WithLockOSThread(conf.async), WithMulticore(conf.multicore), WithReusePort(conf.reuseport), WithTicker(true), WithTCPKeepAlive(time.Minute), WithTCPKeepInterval(time.Second*10), WithTCPKeepCount(10), WithTCPNoDelay(TCPDelay), WithLoadBalancing(conf.lb)) } assert.NoError(t, err) } func startClient(t *testing.T, network, addr string, multicore, async bool, packetSize int, stallCh chan struct{}) { c, err := net.Dial(network, addr) assert.NoError(t, err) defer c.Close() //nolint:errcheck rd := bufio.NewReader(c) if network != "udp" { msg, err := rd.ReadBytes('\n') assert.NoError(t, err) assert.Equal(t, string(msg), "andypan\r\n", "bad header") } if stallCh != nil { <-stallCh } duration := time.Duration((rand.Float64()*2+1)*float64(time.Second)) / 2 logging.Debugf("test duration: %v", duration) start := time.Now() if packetSize == 0 { packetSize = streamLen if network == "udp" { packetSize = datagramLen } } for time.Since(start) < duration { reqData := make([]byte, packetSize) _, err = crand.Read(reqData) assert.NoError(t, err) _, err = c.Write(reqData) assert.NoError(t, err) respData := make([]byte, len(reqData)) _, err = io.ReadFull(rd, respData) assert.NoError(t, err) if !async { // assert.Equalf(t, reqData, respData, "response mismatch with protocol:%s, multi-core:%t, content of bytes: %d vs %d", network, multicore, string(reqData), string(respData)) assert.Equalf( t, reqData, respData, "response mismatch with protocol:%s, multi-core:%t, length of bytes: %d vs %d", network, multicore, len(reqData), len(respData), ) } } } func TestDefaultGnetServer(*testing.T) { svr := BuiltinEventEngine{} svr.OnBoot(Engine{}) svr.OnOpen(nil) svr.OnClose(nil, nil) svr.OnTraffic(nil) svr.OnTick() } type testBadAddrServer struct { *BuiltinEventEngine } func (t *testBadAddrServer) OnBoot(_ Engine) (action Action) { return Shutdown } func TestBadAddresses(t *testing.T) { events := new(testBadAddrServer) err := Run(events, "tulip://howdy") require.ErrorIs(t, err, errorx.ErrUnsupportedProtocol) err = Run(events, "howdy") require.ErrorIs(t, err, errorx.ErrInvalidNetworkAddress) err = Run(events, "tcp://") require.ErrorIs(t, err, errorx.ErrInvalidNetworkAddress) err = Run(events, "tcp://localhost:8080/foo") require.ErrorIs(t, err, errorx.ErrInvalidNetworkAddress) err = Run(events, "unix://") require.ErrorIs(t, err, errorx.ErrInvalidNetworkAddress) err = Run(events, ":foo") require.Error(t, err, "missing protocol scheme") err = Run(events, "tcp://127.0.0.1\n") require.Error(t, err, "invalid control character in URL") } func TestTick(t *testing.T) { testTick("tcp", ":9989", t) } type testTickServer struct { *BuiltinEventEngine count int } func (t *testTickServer) OnTick() (delay time.Duration, action Action) { delay = time.Millisecond * 10 if t.count == 25 { action = Shutdown return } t.count++ return } func testTick(network, addr string, t *testing.T) { events := &testTickServer{} start := time.Now() opts := Options{Ticker: true} err := Run(events, network+"://"+addr, WithOptions(opts)) require.NoError(t, err) dur := time.Since(start) if dur < 250&time.Millisecond || dur > time.Second { t.Logf("bad ticker timing: %d", dur) } } func TestWakeConn(t *testing.T) { testWakeConn(t, "tcp", ":9990") } type testWakeConnServer struct { *BuiltinEventEngine tester *testing.T network string addr string conn chan Conn c Conn wake bool } func (t *testWakeConnServer) OnOpen(c Conn) (out []byte, action Action) { t.conn <- c return } func (t *testWakeConnServer) OnClose(Conn, error) (action Action) { action = Shutdown return } func (t *testWakeConnServer) OnTraffic(c Conn) (action Action) { _, _ = c.Write([]byte("Waking up.")) action = -1 return } func (t *testWakeConnServer) OnTick() (delay time.Duration, action Action) { if !t.wake { t.wake = true delay = time.Millisecond * 100 err := goPool.DefaultWorkerPool.Submit(func() { conn, err := net.Dial(t.network, t.addr) assert.NoError(t.tester, err) defer conn.Close() //nolint:errcheck r := make([]byte, 10) _, err = conn.Read(r) assert.NoError(t.tester, err) }) assert.NoError(t.tester, err) return } t.c = <-t.conn _ = t.c.Wake(func(c Conn, err error) error { logging.Debugf("conn=%s done wake: %v", c.RemoteAddr().String(), err) return nil }) delay = time.Millisecond * 100 return } func testWakeConn(t *testing.T, network, addr string) { currentLogger, currentFlusher := logging.GetDefaultLogger(), logging.GetDefaultFlusher() t.Cleanup(func() { logging.SetDefaultLoggerAndFlusher(currentLogger, currentFlusher) // restore }) svr := &testWakeConnServer{tester: t, network: network, addr: addr, conn: make(chan Conn, 1)} logger := zap.NewExample() err := Run(svr, network+"://"+addr, WithTicker(true), WithNumEventLoop(2*runtime.NumCPU()), WithLogger(logger.Sugar()), WithSocketRecvBuffer(4*1024), WithSocketSendBuffer(4*1024), WithReadBufferCap(2000), WithWriteBufferCap(2000)) assert.NoError(t, err) _ = logger.Sync() } func TestShutdown(t *testing.T) { testShutdown(t, "tcp", ":9991") } type testShutdownServer struct { *BuiltinEventEngine tester *testing.T eng Engine network string addr string count int clients int32 N int } func (t *testShutdownServer) OnBoot(eng Engine) (action Action) { t.eng = eng return } func (t *testShutdownServer) OnOpen(Conn) (out []byte, action Action) { assert.EqualValues(t.tester, atomic.AddInt32(&t.clients, 1), t.eng.CountConnections()) return } func (t *testShutdownServer) OnClose(Conn, error) (action Action) { atomic.AddInt32(&t.clients, -1) return } func (t *testShutdownServer) OnTick() (delay time.Duration, action Action) { if t.count == 0 { // start clients for i := 0; i < t.N; i++ { err := goPool.DefaultWorkerPool.Submit(func() { conn, err := net.Dial(t.network, t.addr) assert.NoError(t.tester, err) defer conn.Close() //nolint:errcheck _, err = conn.Read([]byte{0}) assert.Error(t.tester, err) }) assert.NoError(t.tester, err) } } else if int(atomic.LoadInt32(&t.clients)) == t.N { action = Shutdown } t.count++ delay = time.Second / 20 return } func testShutdown(t *testing.T, network, addr string) { currentLogger, currentFlusher := logging.GetDefaultLogger(), logging.GetDefaultFlusher() t.Cleanup(func() { logging.SetDefaultLoggerAndFlusher(currentLogger, currentFlusher) // restore }) events := &testShutdownServer{tester: t, network: network, addr: addr, N: 100} logPath := filepath.Join(t.TempDir(), "gnet-test-shutdown.log") err := Run(events, network+"://"+addr, WithLogPath(logPath), WithLogLevel(logging.WarnLevel), WithTicker(true), WithReadBufferCap(512), WithWriteBufferCap(512)) assert.NoError(t, err) assert.Equal(t, 0, int(events.clients), "did not close all clients") } func TestCloseActionError(t *testing.T) { testCloseActionError(t, "tcp", ":9992") } type testCloseActionErrorServer struct { *BuiltinEventEngine tester *testing.T network, addr string action bool } func (t *testCloseActionErrorServer) OnClose(Conn, error) (action Action) { action = Shutdown return } func (t *testCloseActionErrorServer) OnTraffic(c Conn) (action Action) { n := c.InboundBuffered() buf := make([]byte, n) m, err := c.Read(buf) assert.NoError(t.tester, err) assert.EqualValuesf(t.tester, n, m, "read %d bytes, expected %d", m, n) n, err = c.Write(buf) assert.NoError(t.tester, err) assert.EqualValuesf(t.tester, m, n, "wrote %d bytes, expected %d", n, m) action = Close return } func (t *testCloseActionErrorServer) OnTick() (delay time.Duration, action Action) { if !t.action { t.action = true delay = time.Millisecond * 100 err := goPool.DefaultWorkerPool.Submit(func() { conn, err := net.Dial(t.network, t.addr) assert.NoError(t.tester, err) defer conn.Close() //nolint:errcheck data := []byte("Hello World!") _, _ = conn.Write(data) _, err = conn.Read(data) assert.NoError(t.tester, err) }) assert.NoError(t.tester, err) return } delay = time.Millisecond * 100 return } func testCloseActionError(t *testing.T, network, addr string) { events := &testCloseActionErrorServer{tester: t, network: network, addr: addr} err := Run(events, network+"://"+addr, WithTicker(true)) assert.NoError(t, err) } func TestShutdownActionError(t *testing.T) { testShutdownActionError(t, "tcp", ":9993") } type testShutdownActionErrorServer struct { *BuiltinEventEngine tester *testing.T network, addr string action bool } func (t *testShutdownActionErrorServer) OnTraffic(c Conn) (action Action) { buf, _ := c.Peek(-1) _, _ = c.Write(buf) _, _ = c.Discard(-1) action = Shutdown return } func (t *testShutdownActionErrorServer) OnTick() (delay time.Duration, action Action) { if !t.action { t.action = true delay = time.Millisecond * 100 err := goPool.DefaultWorkerPool.Submit(func() { conn, err := net.Dial(t.network, t.addr) assert.NoError(t.tester, err) defer conn.Close() //nolint:errcheck data := []byte("Hello World!") _, _ = conn.Write(data) _, err = conn.Read(data) assert.NoError(t.tester, err) }) assert.NoError(t.tester, err) return } delay = time.Millisecond * 100 return } func testShutdownActionError(t *testing.T, network, addr string) { events := &testShutdownActionErrorServer{tester: t, network: network, addr: addr} err := Run(events, network+"://"+addr, WithTicker(true)) assert.NoError(t, err) } func TestCloseActionOnOpen(t *testing.T) { testCloseActionOnOpen(t, "tcp", ":9994") } type testCloseActionOnOpenServer struct { *BuiltinEventEngine tester *testing.T network, addr string action bool } func (t *testCloseActionOnOpenServer) OnOpen(Conn) (out []byte, action Action) { action = Close return } func (t *testCloseActionOnOpenServer) OnClose(Conn, error) (action Action) { action = Shutdown return } func (t *testCloseActionOnOpenServer) OnTick() (delay time.Duration, action Action) { if !t.action { t.action = true delay = time.Millisecond * 100 err := goPool.DefaultWorkerPool.Submit(func() { conn, err := net.Dial(t.network, t.addr) assert.NoError(t.tester, err) defer conn.Close() //nolint:errcheck }) assert.NoError(t.tester, err) return } delay = time.Millisecond * 100 return } func testCloseActionOnOpen(t *testing.T, network, addr string) { events := &testCloseActionOnOpenServer{tester: t, network: network, addr: addr} err := Run(events, network+"://"+addr, WithTicker(true)) assert.NoError(t, err) } func TestShutdownActionOnOpen(t *testing.T) { testShutdownActionOnOpen(t, "tcp", ":9995") } type testShutdownActionOnOpenServer struct { *BuiltinEventEngine tester *testing.T network, addr string action bool eng Engine } func (t *testShutdownActionOnOpenServer) OnOpen(Conn) (out []byte, action Action) { action = Shutdown return } func (t *testShutdownActionOnOpenServer) OnShutdown(e Engine) { t.eng = e fd, err := t.eng.Dup() assert.Greaterf(t.tester, fd, 2, "expected fd: > 2, but got: %d", fd) assert.NoErrorf(t.tester, err, "dup error") assert.NoErrorf(t.tester, SysClose(fd), "close error") logging.Debugf("dup fd: %d with error: %v\n", fd, err) } func (t *testShutdownActionOnOpenServer) OnTick() (delay time.Duration, action Action) { if !t.action { t.action = true delay = time.Millisecond * 100 err := goPool.DefaultWorkerPool.Submit(func() { conn, err := net.Dial(t.network, t.addr) assert.NoError(t.tester, err) defer conn.Close() //nolint:errcheck }) assert.NoError(t.tester, err) return } delay = time.Millisecond * 100 return } func testShutdownActionOnOpen(t *testing.T, network, addr string) { events := &testShutdownActionOnOpenServer{tester: t, network: network, addr: addr} err := Run(events, network+"://"+addr, WithTicker(true)) require.NoError(t, err) _, err = events.eng.Dup() require.ErrorIsf(t, err, errorx.ErrEngineInShutdown, "expected error: %v, but got: %v", errorx.ErrEngineInShutdown, err) } func TestUDPShutdown(t *testing.T) { testUDPShutdown(t, "udp4", ":9000") } type testUDPShutdownServer struct { *BuiltinEventEngine tester *testing.T network string addr string tick bool } func (t *testUDPShutdownServer) OnTraffic(c Conn) (action Action) { buf, _ := c.Peek(-1) _, _ = c.Write(buf) _, _ = c.Discard(-1) action = Shutdown return } func (t *testUDPShutdownServer) OnTick() (delay time.Duration, action Action) { if !t.tick { t.tick = true delay = time.Millisecond * 100 err := goPool.DefaultWorkerPool.Submit(func() { conn, err := net.Dial(t.network, t.addr) assert.NoError(t.tester, err) defer conn.Close() //nolint:errcheck data := []byte("Hello World!") _, err = conn.Write(data) assert.NoError(t.tester, err) _, err = conn.Read(data) assert.NoError(t.tester, err) }) assert.NoError(t.tester, err) return } delay = time.Millisecond * 100 return } func testUDPShutdown(t *testing.T, network, addr string) { svr := &testUDPShutdownServer{tester: t, network: network, addr: addr} err := Run(svr, network+"://"+addr, WithTicker(true)) assert.NoError(t, err) } func TestCloseConnection(t *testing.T) { testCloseConnection(t, "tcp", ":9996") } type testCloseConnectionServer struct { *BuiltinEventEngine tester *testing.T network, addr string action bool } func (t *testCloseConnectionServer) OnClose(Conn, error) (action Action) { action = Shutdown return } func (t *testCloseConnectionServer) OnTraffic(c Conn) (action Action) { buf, _ := c.Peek(-1) _, _ = c.Write(buf) _, _ = c.Discard(-1) err := goPool.DefaultWorkerPool.Submit(func() { time.Sleep(time.Second) err := c.CloseWithCallback(func(_ Conn, err error) error { assert.ErrorIsf(t.tester, err, errorx.ErrEngineShutdown, "should be engine shutdown error") return nil }) assert.NoError(t.tester, err) }) assert.NoError(t.tester, err) return } func (t *testCloseConnectionServer) OnTick() (delay time.Duration, action Action) { delay = time.Millisecond * 100 if !t.action { t.action = true err := goPool.DefaultWorkerPool.Submit(func() { conn, err := net.Dial(t.network, t.addr) assert.NoError(t.tester, err) defer conn.Close() //nolint:errcheck data := []byte("Hello World!") _, _ = conn.Write(data) _, err = conn.Read(data) assert.NoError(t.tester, err) // waiting the engine shutdown. _, err = conn.Read(data) assert.Error(t.tester, err) }) assert.NoError(t.tester, err) return } return } func testCloseConnection(t *testing.T, network, addr string) { events := &testCloseConnectionServer{tester: t, network: network, addr: addr} err := Run(events, network+"://"+addr, WithTicker(true)) assert.NoError(t, err) } func TestServerOptionsCheck(t *testing.T) { err := Run(&BuiltinEventEngine{}, "tcp://:3500", WithNumEventLoop(10001), WithLockOSThread(true)) assert.ErrorIs(t, err, errorx.ErrTooManyEventLoopThreads, "error returned with LockOSThread option") } func TestStopServer(t *testing.T) { testStop(t, "tcp", ":9997") } type testStopServer struct { *BuiltinEventEngine tester *testing.T network, addr, protoAddr string eng Engine action bool } func (t *testStopServer) OnBoot(eng Engine) (action Action) { t.eng = eng return } func (t *testStopServer) OnClose(Conn, error) (action Action) { logging.Debugf("closing connection...") return } func (t *testStopServer) OnTraffic(c Conn) (action Action) { buf, _ := c.Peek(-1) _, _ = c.Write(buf) _, _ = c.Discard(-1) return } func (t *testStopServer) OnTick() (delay time.Duration, action Action) { delay = time.Millisecond * 100 if !t.action { t.action = true err := goPool.DefaultWorkerPool.Submit(func() { conn, err := net.Dial(t.network, t.addr) assert.NoError(t.tester, err) defer conn.Close() //nolint:errcheck data := []byte("Hello World!") _, _ = conn.Write(data) _, err = conn.Read(data) assert.NoError(t.tester, err) err = goPool.DefaultWorkerPool.Submit(func() { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() logging.Debugf("stop engine...", t.eng.Stop(ctx)) }) assert.NoError(t.tester, err) // waiting the engine shutdown. _, err = conn.Read(data) assert.Error(t.tester, err) }) assert.NoError(t.tester, err) return } return } func testStop(t *testing.T, network, addr string) { events := &testStopServer{tester: t, network: network, addr: addr, protoAddr: network + "://" + addr} err := Run(events, events.protoAddr, WithTicker(true)) assert.NoError(t, err) } func TestEngineStop(t *testing.T) { testEngineStop(t, "tcp", ":9998") } type testStopEngine struct { *BuiltinEventEngine tester *testing.T network, addr, protoAddr string eng Engine stopIter int64 name string exchngCount int64 } func (t *testStopEngine) OnBoot(eng Engine) (action Action) { t.eng = eng return } func (t *testStopEngine) OnClose(Conn, error) (action Action) { logging.Debugf("closing connection...") return } func (t *testStopEngine) OnTraffic(c Conn) (action Action) { buf, _ := c.Peek(-1) _, _ = c.Write(buf) _, _ = c.Discard(-1) atomic.AddInt64(&t.exchngCount, 1) return } func (t *testStopEngine) OnTick() (delay time.Duration, action Action) { delay = time.Millisecond * 100 err := goPool.DefaultWorkerPool.Submit(func() { conn, err := net.Dial(t.network, t.addr) assert.NoError(t.tester, err) defer conn.Close() //nolint:errcheck data := []byte("Hello World! " + t.name) _, _ = conn.Write(data) _, err = conn.Read(data) assert.NoError(t.tester, err) iter := atomic.LoadInt64(&t.stopIter) if iter <= 0 { ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) defer cancel() logging.Debugf("stop engine...", t.eng.Stop(ctx)) // waiting the engine shutdown. _, err = conn.Read(data) assert.Error(t.tester, err) } atomic.AddInt64(&t.stopIter, -1) }) assert.NoError(t.tester, err) return } func testEngineStop(t *testing.T, network, addr string) { events1 := &testStopEngine{tester: t, network: network, addr: addr, protoAddr: network + "://" + addr, name: "1", stopIter: 2} events2 := &testStopEngine{tester: t, network: network, addr: addr, protoAddr: network + "://" + addr, name: "2", stopIter: 5} result1 := make(chan error, 1) err := goPool.DefaultWorkerPool.Submit(func() { err := Run(events1, events1.protoAddr, WithTicker(true), WithReuseAddr(true), WithReusePort(true)) result1 <- err }) require.NoError(t, err) // ensure the first handler processes before starting the next since the delay per tick is 100ms time.Sleep(150 * time.Millisecond) result2 := make(chan error, 1) err = goPool.DefaultWorkerPool.Submit(func() { err := Run(events2, events2.protoAddr, WithTicker(true), WithReuseAddr(true), WithReusePort(true)) result2 <- err }) require.NoError(t, err) err = <-result1 require.NoError(t, err) err = <-result2 require.NoError(t, err) // make sure that each handler processed at least 1 require.Greater(t, events1.exchngCount, int64(0)) require.Greater(t, events2.exchngCount, int64(0)) require.Equal(t, int64(2+1+5+1), events1.exchngCount+events2.exchngCount) // stop an already stopped engine require.Equal(t, errorx.ErrEngineInShutdown, events1.eng.Stop(context.Background())) } // Test should not panic when we wake up server_closed conn. func TestClosedWakeUp(t *testing.T) { events := &testClosedWakeUpServer{ tester: t, BuiltinEventEngine: &BuiltinEventEngine{}, network: "tcp", addr: ":9999", protoAddr: "tcp://:9999", clientClosed: make(chan struct{}), serverClosed: make(chan struct{}), wakeup: make(chan struct{}), } err := Run(events, events.protoAddr) assert.NoError(t, err) } type testClosedWakeUpServer struct { *BuiltinEventEngine tester *testing.T network, addr, protoAddr string wakeup chan struct{} serverClosed chan struct{} clientClosed chan struct{} } func (s *testClosedWakeUpServer) OnBoot(eng Engine) (action Action) { err := goPool.DefaultWorkerPool.Submit(func() { c, err := net.Dial(s.network, s.addr) assert.NoError(s.tester, err) _, err = c.Write([]byte("hello")) assert.NoError(s.tester, err) <-s.wakeup _, err = c.Write([]byte("hello again")) assert.NoError(s.tester, err) close(s.clientClosed) <-s.serverClosed logging.Debugf("stop engine...", eng.Stop(context.TODO())) }) assert.NoError(s.tester, err) return None } func (s *testClosedWakeUpServer) OnTraffic(c Conn) Action { assert.NotNil(s.tester, c.RemoteAddr()) select { case <-s.wakeup: default: close(s.wakeup) } err := goPool.DefaultWorkerPool.Submit(func() { assert.NoError(s.tester, c.Wake(nil)) }) assert.NoError(s.tester, err) err = goPool.DefaultWorkerPool.Submit(func() { assert.NoError(s.tester, c.Close()) }) assert.NoError(s.tester, err) <-s.clientClosed _, _ = c.Write([]byte("answer")) return None } func (s *testClosedWakeUpServer) OnClose(Conn, error) (action Action) { select { case <-s.serverClosed: default: close(s.serverClosed) } return } type testMultiInstLoggerRaceServer struct { *BuiltinEventEngine } func (t *testMultiInstLoggerRaceServer) OnBoot(_ Engine) (action Action) { return Shutdown } func TestMultiInstLoggerRace(t *testing.T) { currentLogger, currentFlusher := logging.GetDefaultLogger(), logging.GetDefaultFlusher() t.Cleanup(func() { logging.SetDefaultLoggerAndFlusher(currentLogger, currentFlusher) // restore }) logger1, _ := zap.NewDevelopment() events1 := new(testMultiInstLoggerRaceServer) g := errgroup.Group{} g.Go(func() error { err := Run(events1, "tulip://howdy", WithLogger(logger1.Sugar())) return err }) logger2, _ := zap.NewDevelopment() events2 := new(testMultiInstLoggerRaceServer) g.Go(func() error { err := Run(events2, "tulip://howdy", WithLogger(logger2.Sugar())) return err }) assert.ErrorIs(t, g.Wait(), errorx.ErrUnsupportedProtocol) } type testDisconnectedAsyncWriteServer struct { BuiltinEventEngine tester *testing.T addr string writev, clientStarted bool exit atomic.Bool } func (t *testDisconnectedAsyncWriteServer) OnTraffic(c Conn) Action { _, err := c.Next(0) assert.NoErrorf(t.tester, err, "c.Next error: %v", err) err = goPool.DefaultWorkerPool.Submit(func() { for range time.Tick(100 * time.Millisecond) { if t.exit.Load() { break } var err error if t.writev { err = c.AsyncWritev([][]byte{[]byte("hello"), []byte("hello")}, func(_ Conn, err error) error { if err == nil { return nil } assert.ErrorIsf(t.tester, err, net.ErrClosed, "expected error: %v, but got: %v", net.ErrClosed, err) t.exit.Store(true) return nil }) } else { err = c.AsyncWrite([]byte("hello"), func(_ Conn, err error) error { if err == nil { return nil } assert.ErrorIsf(t.tester, err, net.ErrClosed, "expected error: %v, but got: %v", net.ErrClosed, err) t.exit.Store(true) return nil }) } if err != nil { return } } }) assert.NoError(t.tester, err) return None } func (t *testDisconnectedAsyncWriteServer) OnTick() (delay time.Duration, action Action) { delay = 500 * time.Millisecond if t.exit.Load() { action = Shutdown return } if !t.clientStarted { t.clientStarted = true err := goPool.DefaultWorkerPool.Submit(func() { c, err := net.Dial("tcp", t.addr) assert.NoError(t.tester, err) _, err = c.Write([]byte("hello")) assert.NoError(t.tester, err) assert.NoError(t.tester, c.Close()) }) assert.NoError(t.tester, err) } return } func TestDisconnectedAsyncWrite(t *testing.T) { t.Run("async-write", func(t *testing.T) { events := &testDisconnectedAsyncWriteServer{tester: t, addr: ":10000"} err := Run(events, "tcp://:10000", WithTicker(true)) assert.NoError(t, err) }) t.Run("async-writev", func(t *testing.T) { events := &testDisconnectedAsyncWriteServer{tester: t, addr: ":10001", writev: true} err := Run(events, "tcp://:10001", WithTicker(true)) assert.NoError(t, err) }) } var errIncompletePacket = errors.New("incomplete packet") type simServer struct { BuiltinEventEngine tester *testing.T eng Engine network string addr string multicore bool nclients int packetSize int batchWrite int batchRead int started int32 connected int32 disconnected int32 } func (s *simServer) OnBoot(eng Engine) (action Action) { s.eng = eng return } func (s *simServer) OnOpen(c Conn) (out []byte, action Action) { c.SetContext(&testCodec{}) atomic.AddInt32(&s.connected, 1) out = []byte("andypan\r\n") assert.NotNil(s.tester, c.LocalAddr(), "nil local addr") assert.NotNil(s.tester, c.RemoteAddr(), "nil remote addr") return } func (s *simServer) OnClose(_ Conn, err error) (action Action) { if err != nil { logging.Debugf("error occurred on closed, %v\n", err) } atomic.AddInt32(&s.disconnected, 1) if atomic.LoadInt32(&s.connected) == atomic.LoadInt32(&s.disconnected) && atomic.LoadInt32(&s.disconnected) == int32(s.nclients) { action = Shutdown } return } func (s *simServer) OnTraffic(c Conn) (action Action) { codec := c.Context().(*testCodec) var packets [][]byte for i := 0; i < s.batchRead; i++ { data, err := codec.Decode(c) if errors.Is(err, errIncompletePacket) { break } if err != nil { logging.Errorf("invalid packet: %v", err) return Close } packet, _ := codec.Encode(data) packets = append(packets, packet) } if n := len(packets); n > 1 { _, _ = c.Writev(packets) } else if n == 1 { _, _ = c.Write(packets[0]) } if len(packets) == s.batchRead && c.InboundBuffered() > 0 { err := c.Wake(nil) // wake up the connection manually to avoid missing the leftover data assert.NoError(s.tester, err) } return } func (s *simServer) OnTick() (delay time.Duration, action Action) { if atomic.CompareAndSwapInt32(&s.started, 0, 1) { for i := 0; i < s.nclients; i++ { err := goPool.DefaultWorkerPool.Submit(func() { runSimClient(s.tester, s.network, s.addr, s.packetSize, s.batchWrite) }) assert.NoError(s.tester, err) } } delay = 100 * time.Millisecond return } // All current protocols. const ( magicNumber = 1314 magicNumberSize = 2 bodySize = 4 ) var magicNumberBytes []byte func init() { magicNumberBytes = make([]byte, magicNumberSize) binary.BigEndian.PutUint16(magicNumberBytes, uint16(magicNumber)) } // Protocol format: // // * 0 2 6 // * +-----------+-----------------------+ // * | magic | body len | // * +-----------+-----------+-----------+ // * | | // * + + // * | body bytes | // * + + // * | ... ... | // * +-----------------------------------+. type testCodec struct{} func (codec testCodec) Encode(buf []byte) ([]byte, error) { bodyOffset := magicNumberSize + bodySize msgLen := bodyOffset + len(buf) data := make([]byte, msgLen) copy(data, magicNumberBytes) binary.BigEndian.PutUint32(data[magicNumberSize:bodyOffset], uint32(len(buf))) copy(data[bodyOffset:msgLen], buf) return data, nil } func (codec testCodec) Decode(c Conn) ([]byte, error) { bodyOffset := magicNumberSize + bodySize buf, err := c.Peek(bodyOffset) if err != nil { if errors.Is(err, io.ErrShortBuffer) { err = errIncompletePacket } return nil, err } if !bytes.Equal(magicNumberBytes, buf[:magicNumberSize]) { return nil, errors.New("invalid magic number") } bodyLen := binary.BigEndian.Uint32(buf[magicNumberSize:bodyOffset]) msgLen := bodyOffset + int(bodyLen) buf, err = c.Peek(msgLen) if err != nil { if errors.Is(err, io.ErrShortBuffer) { err = errIncompletePacket } return nil, err } body := make([]byte, bodyLen) copy(body, buf[bodyOffset:msgLen]) _, _ = c.Discard(msgLen) return body, nil } func (codec testCodec) Unpack(buf []byte) ([]byte, error) { bodyOffset := magicNumberSize + bodySize if len(buf) < bodyOffset { return nil, errIncompletePacket } if !bytes.Equal(magicNumberBytes, buf[:magicNumberSize]) { return nil, errors.New("invalid magic number") } bodyLen := binary.BigEndian.Uint32(buf[magicNumberSize:bodyOffset]) msgLen := bodyOffset + int(bodyLen) if len(buf) < msgLen { return nil, errIncompletePacket } return buf[bodyOffset:msgLen], nil } func TestSimServer(t *testing.T) { t.Run("packet-size=64,batch=200", func(t *testing.T) { runSimServer(t, ":7200", true, 10, 64, 200, -1) }) t.Run("packet-size=128,batch=100", func(t *testing.T) { runSimServer(t, ":7201", false, 10, 128, 100, 10) }) t.Run("packet-size=256,batch=50", func(t *testing.T) { runSimServer(t, ":7202", true, 10, 256, 50, -1) }) t.Run("packet-size=512,batch=30", func(t *testing.T) { runSimServer(t, ":7203", false, 10, 512, 30, 3) }) t.Run("packet-size=1024,batch=20", func(t *testing.T) { runSimServer(t, ":7204", true, 10, 1024, 20, -1) }) t.Run("packet-size=64*1024,batch=10", func(t *testing.T) { runSimServer(t, ":7205", false, 10, 64*1024, 10, 1) }) t.Run("packet-size=128*1024,batch=5", func(t *testing.T) { runSimServer(t, ":7206", true, 10, 128*1024, 5, -1) }) t.Run("packet-size=512*1024,batch=3", func(t *testing.T) { runSimServer(t, ":7207", false, 10, 512*1024, 3, 1) }) t.Run("packet-size=1024*1024,batch=2", func(t *testing.T) { runSimServer(t, ":7208", true, 10, 1024*1024, 2, -1) }) } func runSimServer(t *testing.T, addr string, et bool, nclients, packetSize, batchWrite, batchRead int) { ts := &simServer{ tester: t, network: "tcp", addr: addr, multicore: true, nclients: nclients, packetSize: packetSize, batchWrite: batchWrite, batchRead: batchRead, } if batchRead < 0 { ts.batchRead = math.MaxInt32 // unlimited read batch } err := Run(ts, ts.network+"://"+ts.addr, WithEdgeTriggeredIO(et), WithMulticore(ts.multicore), WithTicker(true), WithTCPKeepAlive(time.Minute), WithTCPKeepInterval(time.Second*10), WithTCPKeepCount(10), ) assert.NoError(t, err) } func runSimClient(t *testing.T, network, addr string, packetSize, batch int) { c, err := net.Dial(network, addr) assert.NoError(t, err) defer c.Close() //nolint:errcheck rd := bufio.NewReader(c) msg, err := rd.ReadBytes('\n') assert.NoError(t, err) assert.Equal(t, string(msg), "andypan\r\n", "bad header") var duration time.Duration packetBytes := packetSize * batch switch { case packetBytes < 16*1024: duration = 2 * time.Second case packetBytes < 32*1024: duration = 3 * time.Second case packetBytes < 480*1024: duration = 4 * time.Second default: duration = 5 * time.Second } logging.Debugf("test duration: %v", duration) start := time.Now() for time.Since(start) < duration { batchSendAndRecv(t, c, rd, packetSize, batch) } } func batchSendAndRecv(t *testing.T, c net.Conn, rd *bufio.Reader, packetSize, batch int) { codec := testCodec{} var ( requests [][]byte buf []byte packetLen int ) for i := 0; i < batch; i++ { req := make([]byte, packetSize) _, err := crand.Read(req) assert.NoError(t, err) requests = append(requests, req) packet, _ := codec.Encode(req) packetLen = len(packet) buf = append(buf, packet...) } _, err := c.Write(buf) assert.NoError(t, err) respPacket := make([]byte, batch*packetLen) _, err = io.ReadFull(rd, respPacket) assert.NoError(t, err) for i, req := range requests { rsp, err := codec.Unpack(respPacket[i*packetLen:]) assert.NoError(t, err) assert.Equalf(t, req, rsp, "request and response mismatch, packet size: %d, batch: %d, round: %d", packetSize, batch, i) } } type testUDPSendtoServer struct { BuiltinEventEngine addr string tester *testing.T startClientsOnce sync.Once broadcastMsg []byte mu sync.Mutex clientAddrs []net.Addr clientCount int } func (t *testUDPSendtoServer) OnBoot(_ Engine) (action Action) { t.broadcastMsg = []byte("Broadcasting message") t.clientCount = 10 return } func (t *testUDPSendtoServer) OnTraffic(c Conn) Action { msg, err := c.Next(-1) assert.NoErrorf(t.tester, err, "c.Next error: %v", err) assert.NotZero(t.tester, msg, "c.Next should not return empty buffer") t.mu.Lock() t.clientAddrs = append(t.clientAddrs, c.RemoteAddr()) if len(t.clientAddrs) == t.clientCount { for _, addr := range t.clientAddrs { n, err := c.SendTo(t.broadcastMsg, addr) assert.NoError(t.tester, err, "c.SendTo error") assert.EqualValuesf(t.tester, len(t.broadcastMsg), n, "c.SendTo should send %d bytes, but sent %d bytes", len(t.broadcastMsg), n) } } t.mu.Unlock() return None } func (t *testUDPSendtoServer) OnTick() (delay time.Duration, action Action) { t.startClientsOnce.Do(func() { for i := 0; i < t.clientCount; i++ { err := goPool.DefaultWorkerPool.Submit(func() { c, err := net.Dial("udp", t.addr) assert.NoError(t.tester, err) defer c.Close() //nolint:errcheck _, err = c.Write([]byte("Hello World!")) assert.NoError(t.tester, err) msg := make([]byte, len(t.broadcastMsg)) _, err = c.Read(msg) assert.NoError(t.tester, err) assert.EqualValuesf(t.tester, msg, t.broadcastMsg, "broadcast message mismatch, expected: %s, got: %s", t.broadcastMsg, msg) t.mu.Lock() t.clientCount-- t.mu.Unlock() }) assert.NoError(t.tester, err) } }) t.mu.Lock() if t.clientCount == 0 { action = Shutdown } t.mu.Unlock() delay = time.Millisecond * 100 return } func TestUDPSendtoServer(t *testing.T) { // The listening address without an explicit IP works on Linux and Windows // while sendto fails on macOS with EINVAL (invalid argument) that might // also occur on more BSD systems: FreeBSD, OpenBSD, NetBSD, and DragonflyBSD. // To pass this test on all platforms, specify an explicit IP address here. // addr := ":10000" addr := "127.0.0.1:10000" events := &testUDPSendtoServer{tester: t, addr: addr} err := Run(events, "udp://"+addr, WithTicker(true)) assert.NoError(t, err) } func startUDPEchoServer(t *testing.T, c *net.UDPConn) { defer c.Close() //nolint:errcheck buf := make([]byte, datagramLen) for { nr, remote, err := c.ReadFromUDP(buf) if err != nil { break } if nr > 0 { nw, err := c.WriteToUDP(buf[:nr], remote) assert.EqualValuesf(t, nr, nw, "UDP echo server should send %d bytes, but sent %d bytes", nr, nw) assert.NoErrorf(t, err, "error occurred on UDP echo server %v write to %s, %v", remote, c.LocalAddr().String(), err) } } } func startStreamEchoServer(t *testing.T, ln net.Listener) { defer func() { ln.Close() //nolint:errcheck if strings.HasPrefix(ln.Addr().Network(), "unix") { os.Remove(ln.Addr().String()) //nolint:errcheck } }() for { c, err := ln.Accept() if err != nil { return } err = goPool.DefaultWorkerPool.Submit(func() { defer c.Close() //nolint:errcheck buf := make([]byte, streamLen) for { nr, err := c.Read(buf) if err != nil { break } b := buf[:nr] for len(b) > 0 { nw, err := c.Write(b) assert.NoErrorf(t, err, "error occurred on TCP echo server %s write to %s, %v", ln.Addr().String(), c.RemoteAddr().String(), err) b = b[nw:] } } }) assert.NoError(t, err, "error occurred on TCP echo server %s submit task, %v", ln.Addr().String(), err) } } type streamProxyServer struct { BuiltinEventEngine engine Engine eventLoop EventLoop stallCh chan struct{} tester *testing.T ListenerNet string ListenerAddr string connected int32 disconnected int32 backendEstablished int32 backendServerPoolMu sync.Mutex backendServerPool []string startClintOnce sync.Once backendServers []string packetSize int } func (p *streamProxyServer) OnShutdown(eng Engine) { p.engine = eng p.tester.Logf("proxy server %s shutdown!", p.ListenerAddr) } func (p *streamProxyServer) OnOpen(c Conn) (out []byte, action Action) { if c.LocalAddr().String() == p.ListenerAddr { // it's a server connection out = []byte("andypan\r\n") p.backendServerPoolMu.Lock() backendServer := p.backendServerPool[len(p.backendServerPool)-1] p.backendServerPool = p.backendServerPool[:len(p.backendServerPool)-1] p.backendServerPoolMu.Unlock() // Test the error handling of the methods of EventLoop. _, err := c.EventLoop().Register(context.Background(), nil) assert.ErrorIsf(p.tester, err, errorx.ErrInvalidNetworkAddress, "Expected error: %v, but got: %v", errorx.ErrInvalidNetworkAddress, err) _, err = c.EventLoop().Enroll(context.Background(), nil) assert.ErrorIsf(p.tester, err, errorx.ErrInvalidNetConn, "Expected error: %v, but got: %v", errorx.ErrInvalidNetConn, err) err = c.EventLoop().Execute(context.Background(), nil) assert.ErrorIsf(p.tester, err, errorx.ErrNilRunnable, "Expected error: %v, but got: %v", errorx.ErrNilRunnable, err) err = c.EventLoop().Schedule(context.Background(), nil, time.Millisecond) assert.ErrorIsf(p.tester, err, errorx.ErrUnsupportedOp, "Expected error: %v, but got: %v", errorx.ErrUnsupportedOp, err) network, addr, err := parseProtoAddr(backendServer) assert.NoError(p.tester, err, "parseProtoAddr error") var address net.Addr switch { case strings.HasPrefix(network, "tcp"): address, err = net.ResolveTCPAddr(network, addr) case strings.HasPrefix(network, "udp"): address, err = net.ResolveUDPAddr(network, addr) case strings.HasPrefix(network, "unix"): address, err = net.ResolveUnixAddr(network, addr) default: assert.Failf(p.tester, "unsupported protocol", "unsupported protocol: %s", network) } assert.NoError(p.tester, err, "ResolveNetAddr error") ctx := NewContext(context.Background(), c) resCh, err := c.EventLoop().Register(ctx, address) assert.NoError(p.tester, err, "Register connection error") err = goPool.DefaultWorkerPool.Submit(func() { res := <-resCh assert.NoError(p.tester, res.Err, "Register connection error") assert.NotNil(p.tester, res.Conn, "Register connection nil") }) assert.NoError(p.tester, err, "Submit task error") atomic.AddInt32(&p.connected, 1) } else { // it's a client connection // Store the client connection in the context of the server connection. serverConn, ok := c.Context().(Conn) assert.True(p.tester, ok, "context is not Conn") assert.NotNil(p.tester, serverConn, "context is not Conn") serverConn.SetContext(c) err := c.EventLoop().Execute(NewContext(context.Background(), c.LocalAddr()), RunnableFunc(func(ctx context.Context) error { p.tester.Logf("backend connection %v established", FromContext(ctx)) return nil })) assert.NoError(p.tester, err, "Execute task error") if int(atomic.AddInt32(&p.backendEstablished, 1)) == len(p.backendServers) { // Unlock the clients to start sending data. close(p.stallCh) } } return } func (p *streamProxyServer) OnClose(c Conn, err error) (action Action) { connType := "client" if c.LocalAddr().String() == p.ListenerAddr { connType = "server" } logging.Debugf("closing %s connection: %s", connType, c.LocalAddr().String()) if err != nil { logging.Debugf("error occurred on server connnection closing, %v", err) } if c.LocalAddr().String() == p.ListenerAddr { // it's a server connection pc, ok := c.Context().(Conn) assert.True(p.tester, ok, "server context connection is nil") assert.NotNil(p.tester, pc, "server context connection is nil") err := c.EventLoop().Close(pc) // close the corresponding client connection assert.NoError(p.tester, err, "Close client connection error") if disconnected := atomic.AddInt32(&p.disconnected, 1); int(disconnected) == len(p.backendServers) && disconnected == atomic.LoadInt32(&p.connected) { p.eventLoop = c.EventLoop() action = Shutdown } } return } func (p *streamProxyServer) OnTraffic(c Conn) Action { connType := "client" if c.LocalAddr().String() == p.ListenerAddr { connType = "server" } pc, ok := c.Context().(Conn) if !ok { // The backend connection is not established yet, retry later. assert.NoError(p.tester, c.Wake(nil), "Wake connection error") return None } _, err := c.WriteTo(pc) assert.NoErrorf(p.tester, err, "%s: Write error from %s to %s", connType, c.LocalAddr().String(), pc.RemoteAddr().String()) return None } func (p *streamProxyServer) OnTick() (time.Duration, Action) { p.startClintOnce.Do(func() { for i := 0; i < len(p.backendServers); i++ { err := goPool.DefaultWorkerPool.Submit(func() { startClient(p.tester, p.ListenerNet, p.ListenerAddr, true, false, p.packetSize, p.stallCh) }) assert.NoErrorf(p.tester, err, "Submit backend server %s error: %v", p.ListenerAddr, err) } }) return time.Millisecond * 200, None } func TestStreamProxyServer(t *testing.T) { t.Run("tcp-proxy-server", func(t *testing.T) { addr := "tcp://127.0.0.1:10000" backendServers := []string{ "tcp://127.0.0.1:10001", "tcp://127.0.0.1:10002", "tcp://127.0.0.1:10003", "tcp://127.0.0.1:10004", "tcp://127.0.0.1:10005", "tcp://127.0.0.1:10006", "tcp://127.0.0.1:10007", "tcp://127.0.0.1:10008", "tcp://127.0.0.1:10009", "tcp://127.0.0.1:10010", } t.Run("1-loop-LT", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, false, false) }) t.Run("1-loop-ET", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, false, true) }) t.Run("N-loop-LT", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, true, false) }) t.Run("N-loop-ET", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, true, true) }) }) t.Run("unix-proxy-server", func(t *testing.T) { addr := "unix://unix-proxy-server.sock" backendServers := []string{ "unix://unix-proxy-server-1.sock", "unix://unix-proxy-server-2.sock", "unix://unix-proxy-server-3.sock", "unix://unix-proxy-server-4.sock", "unix://unix-proxy-server-5.sock", "unix://unix-proxy-server-6.sock", "unix://unix-proxy-server-7.sock", "unix://unix-proxy-server-8.sock", "unix://unix-proxy-server-9.sock", "unix://unix-proxy-server-10.sock", } t.Run("1-loop-LT", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, false, false) }) t.Run("1-loop-ET", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, false, true) }) t.Run("N-loop-LT", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, true, false) }) t.Run("N-loop-ET", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, true, true) }) }) t.Run("udp-proxy-server", func(t *testing.T) { addr := "tcp://127.0.0.1:11000" backendServers := []string{ "udp://127.0.0.1:11001", "udp://127.0.0.1:11002", "udp://127.0.0.1:11003", "udp://127.0.0.1:11004", "udp://127.0.0.1:11005", "udp://127.0.0.1:11006", "udp://127.0.0.1:11007", "udp://127.0.0.1:11008", "udp://127.0.0.1:11009", "udp://127.0.0.1:11010", } t.Run("1-loop-LT", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, false, false) }) t.Run("1-loop-ET", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, false, true) }) t.Run("N-loop-LT", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, true, false) }) t.Run("N-loop-ET", func(t *testing.T) { testStreamProxyServer(t, addr, backendServers, true, true) }) }) } func testStreamProxyServer(t *testing.T, addr string, backendServers []string, multicore, et bool) { network, address, err := parseProtoAddr(addr) require.NoError(t, err, "parseProtoAddr error") srv := streamProxyServer{ tester: t, stallCh: make(chan struct{}), ListenerNet: network, ListenerAddr: address, backendServers: backendServers, backendServerPool: backendServers, } var ( backends errgroup.Group netServers []io.Closer ) for _, backendServer := range backendServers { network, addr, err := parseProtoAddr(backendServer) require.NoError(t, err, "parseProtoAddr error") if strings.HasPrefix(network, "udp") { udpAddr, err := net.ResolveUDPAddr(network, addr) require.NoError(t, err, "ResolveUDPAddr error") c, err := net.ListenUDP("udp", udpAddr) require.NoError(t, err, "ListenUDP error") backends.Go(func() error { startUDPEchoServer(t, c) return nil }) require.NoErrorf(t, err, "Start backend UDP server %s error: %v", backendServer, err) srv.packetSize = datagramLen netServers = append(netServers, c) } else { ln, err := net.Listen(network, addr) require.NoErrorf(t, err, "Create backend server %s error: %v", network+"://"+addr, err) backends.Go(func() error { startStreamEchoServer(t, ln) return nil }) require.NoErrorf(t, err, "Start backend stream server %s error: %v", backendServer, err) srv.packetSize = streamLen netServers = append(netServers, ln) } } // Give the backend servers some time to start. time.Sleep(time.Second) err = Run(&srv, addr, WithEdgeTriggeredIO(et), WithMulticore(multicore), WithTicker(true)) require.NoErrorf(t, err, "Run error: %v", err) // Test the error handling of the methods of EventLoop after a shutdown. _, err = srv.engine.Register(context.Background()) assert.ErrorIsf(t, err, errorx.ErrEngineInShutdown, "Expected error: %v, but got: %v", errorx.ErrEngineInShutdown, err) _, err = srv.eventLoop.Register(context.Background(), nil) require.ErrorIsf(t, err, errorx.ErrEngineInShutdown, "Expected error: %v, but got: %v", errorx.ErrEngineInShutdown, err) _, err = srv.eventLoop.Enroll(context.Background(), nil) require.ErrorIsf(t, err, errorx.ErrEngineInShutdown, "Expected error: %v, but got: %v", errorx.ErrEngineInShutdown, err) err = srv.eventLoop.Execute(context.Background(), nil) require.ErrorIsf(t, err, errorx.ErrEngineInShutdown, "Expected error: %v, but got: %v", errorx.ErrEngineInShutdown, err) err = srv.eventLoop.Schedule(context.Background(), nil, time.Millisecond) require.ErrorIsf(t, err, errorx.ErrUnsupportedOp, "Expected error: %v, but got: %v", errorx.ErrUnsupportedOp, err) for _, server := range netServers { require.NoError(t, server.Close(), "Close backend server error") } backends.Wait() //nolint:errcheck } type udpProxyServer struct { BuiltinEventEngine packet []byte engine Engine tester *testing.T stallCh chan struct{} ListenerNet string ListenerAddr string activeClients int32 backendEstablished int32 backendBytes int32 initBackendPoolOnce sync.Once backendServerPoolMu sync.Mutex backendServerPool []Conn startClientOnce sync.Once backendServers []string packetSize int } func (p *udpProxyServer) OnBoot(eng Engine) (action Action) { p.engine = eng _, err := eng.Register(context.Background()) assert.ErrorIsf(p.tester, err, errorx.ErrEmptyEngine, "Expected error: %v, but got: %v", errorx.ErrEmptyEngine, err) return } func (p *udpProxyServer) OnShutdown(Engine) { assert.Zero(p.tester, atomic.LoadInt32(&p.backendBytes)%int32(len(p.packet)), "backend received bytes should be a multiple of packet size") } func (p *udpProxyServer) OnOpen(c Conn) (out []byte, action Action) { p.backendServerPoolMu.Lock() p.backendServerPool = append(p.backendServerPool, c) p.backendServerPoolMu.Unlock() if int(atomic.AddInt32(&p.backendEstablished, 1)) == len(p.backendServers) { // Unlock the clients to start sending data. close(p.stallCh) } return nil, None } func (p *udpProxyServer) OnTraffic(c Conn) Action { if c.LocalAddr().String() == p.ListenerAddr { // it's a server connection // Echo back to the client. buf, err := c.Next(-1) assert.NoError(p.tester, err, "Next error") assert.Greaterf(p.tester, len(buf), 0, "Next should not return empty buffer") n, err := c.Write(buf) assert.NoError(p.tester, err, "Write error") assert.EqualValuesf(p.tester, n, len(buf), "Write should send %d bytes, but sent %d bytes", len(buf), n) // Send the packet to a random backend server. p.backendServerPoolMu.Lock() backendServer := p.backendServerPool[rand.Intn(len(p.backendServerPool))] p.backendServerPoolMu.Unlock() err = backendServer.AsyncWrite(p.packet, nil) assert.NoError(p.tester, err, "AsyncWrite error") } else { // it's a backend connection buf, err := c.Next(len(p.packet)) assert.NoError(p.tester, err, "Next error") assert.EqualValuesf(p.tester, buf, p.packet, "Packet mismatch, expected: %s, got: %s", p.packet, buf) atomic.AddInt32(&p.backendBytes, int32(len(buf))) } return None } func (p *udpProxyServer) OnTick() (delay time.Duration, action Action) { p.initBackendPoolOnce.Do(func() { for i, backendServer := range p.backendServers { network, addr, err := parseProtoAddr(backendServer) assert.NoError(p.tester, err, "parseProtoAddr error") var address net.Addr switch { case strings.HasPrefix(network, "tcp"): address, err = net.ResolveTCPAddr(network, addr) case strings.HasPrefix(network, "udp"): address, err = net.ResolveUDPAddr(network, addr) case strings.HasPrefix(network, "unix"): address, err = net.ResolveUnixAddr(network, addr) default: assert.Failf(p.tester, "unsupported protocol", "unsupported protocol: %s", network) } assert.NoError(p.tester, err, "ResolveNetAddr error") // Test the error handling with empty context. _, err = p.engine.Register(context.Background()) assert.ErrorIs(p.tester, err, errorx.ErrInvalidNetworkAddress) var resCh <-chan RegisteredResult if i%2 == 0 { resCh, err = p.engine.Register(NewNetAddrContext(context.Background(), address)) } else { c, e := net.Dial(network, addr) assert.NoError(p.tester, e, "Dial error") resCh, err = p.engine.Register(NewNetConnContext(context.Background(), c)) } assert.NoError(p.tester, err, "Register connection error") err = goPool.DefaultWorkerPool.Submit(func() { res := <-resCh assert.NoError(p.tester, res.Err, "Register connection error") assert.NotNil(p.tester, res.Conn, "Register connection nil") }) assert.NoError(p.tester, err, "Submit connection error") } }) p.startClientOnce.Do(func() { for i := 0; i < len(p.backendServers); i++ { atomic.AddInt32(&p.activeClients, 1) err := goPool.DefaultWorkerPool.Submit(func() { startClient(p.tester, p.ListenerNet, p.ListenerAddr, true, false, p.packetSize, p.stallCh) atomic.AddInt32(&p.activeClients, -1) }) assert.NoErrorf(p.tester, err, "Submit backend server %s error: %v", p.ListenerAddr, err) } }) if atomic.LoadInt32(&p.activeClients) == 0 { return 0, Shutdown } return time.Millisecond * 200, None } func TestUDPProxyServer(t *testing.T) { t.Run("backend-udp-proxy-server", func(t *testing.T) { addr := "udp://127.0.0.1:10000" backendServers := []string{ "udp://127.0.0.1:10001", "udp://127.0.0.1:10002", "udp://127.0.0.1:10003", "udp://127.0.0.1:10004", "udp://127.0.0.1:10005", "udp://127.0.0.1:10006", "udp://127.0.0.1:10007", "udp://127.0.0.1:10008", "udp://127.0.0.1:10009", "udp://127.0.0.1:10010", } t.Run("1-loop-LT", func(t *testing.T) { testUDPProxyServer(t, addr, backendServers, false, false) }) t.Run("1-loop-ET", func(t *testing.T) { testUDPProxyServer(t, addr, backendServers, false, true) }) t.Run("N-loop-LT", func(t *testing.T) { testUDPProxyServer(t, addr, backendServers, true, false) }) t.Run("N-loop-ET", func(t *testing.T) { testUDPProxyServer(t, addr, backendServers, true, true) }) }) t.Run("backend-tcp-proxy-server", func(t *testing.T) { addr := "udp://127.0.0.1:20000" backendServers := []string{ "tcp://127.0.0.1:20001", "tcp://127.0.0.1:20002", "tcp://127.0.0.1:20003", "tcp://127.0.0.1:20004", "tcp://127.0.0.1:20005", "tcp://127.0.0.1:20006", "tcp://127.0.0.1:20007", "tcp://127.0.0.1:20008", "tcp://127.0.0.1:20009", "tcp://127.0.0.1:20010", } t.Run("1-loop-LT", func(t *testing.T) { testUDPProxyServer(t, addr, backendServers, false, false) }) t.Run("1-loop-ET", func(t *testing.T) { testUDPProxyServer(t, addr, backendServers, false, true) }) t.Run("N-loop-LT", func(t *testing.T) { testUDPProxyServer(t, addr, backendServers, true, false) }) t.Run("N-loop-ET", func(t *testing.T) { testUDPProxyServer(t, addr, backendServers, true, true) }) }) } func testUDPProxyServer(t *testing.T, addr string, backendServers []string, multicore, et bool) { network, address, err := parseProtoAddr(addr) require.NoError(t, err, "parseProtoAddr error") srv := udpProxyServer{ tester: t, stallCh: make(chan struct{}), ListenerNet: network, ListenerAddr: address, backendServers: backendServers, packet: []byte("andypan"), packetSize: datagramLen, } var ( backends errgroup.Group netServers []io.Closer ) for _, backendServer := range backendServers { network, addr, err := parseProtoAddr(backendServer) require.NoError(t, err, "parseProtoAddr error") if strings.HasPrefix(network, "udp") { udpAddr, err := net.ResolveUDPAddr(network, addr) require.NoError(t, err, "ResolveUDPAddr error") c, err := net.ListenUDP("udp", udpAddr) require.NoError(t, err, "ListenUDP error") backends.Go(func() error { startUDPEchoServer(t, c) return nil }) require.NoErrorf(t, err, "Start backend UDP server %s error: %v", backendServer, err) netServers = append(netServers, c) } else { ln, err := net.Listen(network, addr) require.NoErrorf(t, err, "Create backend server %s error: %v", network+"://"+addr, err) backends.Go(func() error { startStreamEchoServer(t, ln) return nil }) require.NoErrorf(t, err, "Start backend stream server %s error: %v", backendServer, err) netServers = append(netServers, ln) } } err = Run(&srv, addr, WithLoadBalancing(LeastConnections), WithEdgeTriggeredIO(et), WithMulticore(multicore), WithTicker(true)) require.NoErrorf(t, err, "Run error: %v", err) for _, server := range netServers { require.NoError(t, server.Close(), "Close backend server error") } backends.Wait() //nolint:errcheck } ================================================ FILE: go.mod ================================================ module github.com/panjf2000/gnet/v2 require ( github.com/panjf2000/ants/v2 v2.11.3 github.com/stretchr/testify v1.10.0 github.com/valyala/bytebufferpool v1.0.0 go.uber.org/zap v1.27.0 golang.org/x/sync v0.11.0 golang.org/x/sys v0.30.0 gopkg.in/natefinch/lumberjack.v2 v2.2.1 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/multierr v1.11.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) go 1.20 ================================================ FILE: go.sum ================================================ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg= github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= ================================================ FILE: internal/gfd/gfd.go ================================================ // Copyright (c) 2023 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. /* Package gfd provides a structure GFD to store the fd, eventloop index, connStore indexes and some other information. GFD structure: |eventloop index|conn matrix row index|conn matrix column index|monotone sequence| socket fd | | 1 byte | 1 byte | 2 byte | 4 byte | 8 byte |. */ package gfd import ( "encoding/binary" "math" "sync/atomic" ) // Constants for GFD. const ( ConnMatrixColumnOffset = 2 SequenceOffset = 4 FdOffset = 8 EventLoopIndexMax = math.MaxUint8 + 1 ConnMatrixRowMax = math.MaxUint8 + 1 ConnMatrixColumnMax = math.MaxUint16 + 1 ) type monotoneSeq uint32 func (seq *monotoneSeq) Inc() uint32 { return atomic.AddUint32((*uint32)(seq), 1) } var monoSeq = new(monotoneSeq) // GFD is a structure to store the fd, eventloop index, connStore indexes. type GFD [0x10]byte // Fd returns the underlying fd. func (gfd GFD) Fd() int { return int(binary.BigEndian.Uint64(gfd[FdOffset:])) } // EventLoopIndex returns the eventloop index. func (gfd GFD) EventLoopIndex() int { return int(gfd[0]) } // ConnMatrixRow returns the connMatrix row index. func (gfd GFD) ConnMatrixRow() int { return int(gfd[1]) } // ConnMatrixColumn returns the connMatrix column index. func (gfd GFD) ConnMatrixColumn() int { return int(binary.BigEndian.Uint16(gfd[ConnMatrixColumnOffset:SequenceOffset])) } // Sequence returns the monotonic sequence, only used to prevent fd duplication. func (gfd GFD) Sequence() uint32 { return binary.BigEndian.Uint32(gfd[SequenceOffset:FdOffset]) } // UpdateIndexes updates the connStore indexes. func (gfd *GFD) UpdateIndexes(row, column int) { (*gfd)[1] = byte(row) binary.BigEndian.PutUint16((*gfd)[ConnMatrixColumnOffset:SequenceOffset], uint16(column)) } // Validate checks if the GFD is valid. func (gfd GFD) Validate() bool { return gfd.Fd() > 2 && gfd.Fd() <= math.MaxInt && gfd.EventLoopIndex() >= 0 && gfd.EventLoopIndex() < EventLoopIndexMax && gfd.ConnMatrixRow() >= 0 && gfd.ConnMatrixRow() < ConnMatrixRowMax && gfd.ConnMatrixColumn() >= 0 && gfd.ConnMatrixColumn() < ConnMatrixColumnMax && gfd.Sequence() > 0 } // NewGFD creates a new GFD. func NewGFD(fd, elIndex, row, column int) (gfd GFD) { gfd[0] = byte(elIndex) gfd[1] = byte(row) binary.BigEndian.PutUint16(gfd[ConnMatrixColumnOffset:SequenceOffset], uint16(column)) binary.BigEndian.PutUint32(gfd[SequenceOffset:FdOffset], monoSeq.Inc()) binary.BigEndian.PutUint64(gfd[FdOffset:], uint64(fd)) return } ================================================ FILE: listener_unix.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package gnet import ( "net" "os" "runtime" "strings" "sync" "golang.org/x/sys/unix" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" "github.com/panjf2000/gnet/v2/pkg/netpoll" "github.com/panjf2000/gnet/v2/pkg/socket" ) type listener struct { openOnce, closeOnce sync.Once fd int addr net.Addr address, network string sockOptInts []socket.Option[int] sockOptStrs []socket.Option[string] pollAttachment *netpoll.PollAttachment // listener attachment for poller } func (ln *listener) packPollAttachment(handler netpoll.PollEventHandler) *netpoll.PollAttachment { ln.pollAttachment = &netpoll.PollAttachment{FD: ln.fd, Callback: handler} return ln.pollAttachment } func (ln *listener) dup() (int, error) { return socket.Dup(ln.fd) } func (ln *listener) open() (err error) { ln.openOnce.Do(func() { switch ln.network { case "tcp", "tcp4", "tcp6": ln.fd, ln.addr, err = socket.TCPSocket(ln.network, ln.address, true, ln.sockOptInts, ln.sockOptStrs) ln.network = "tcp" case "udp", "udp4", "udp6": ln.fd, ln.addr, err = socket.UDPSocket(ln.network, ln.address, false, ln.sockOptInts, ln.sockOptStrs) ln.network = "udp" case "unix": _ = os.RemoveAll(ln.address) ln.fd, ln.addr, err = socket.UnixSocket(ln.network, ln.address, true, ln.sockOptInts, ln.sockOptStrs) default: err = errorx.ErrUnsupportedProtocol } }) return } func (ln *listener) close() { ln.closeOnce.Do(func() { if ln.fd > 0 { logging.Error(os.NewSyscallError("close", unix.Close(ln.fd))) } ln.fd = -1 if ln.network == "unix" { logging.Error(os.RemoveAll(ln.address)) } }) } func initListener(network, addr string, options *Options) (ln *listener, err error) { var ( sockOptInts []socket.Option[int] sockOptStrs []socket.Option[string] ) if options.ReusePort && network != "unix" { sockOpt := socket.Option[int]{SetSockOpt: socket.SetReuseport, Opt: 1} sockOptInts = append(sockOptInts, sockOpt) } if options.ReuseAddr { sockOpt := socket.Option[int]{SetSockOpt: socket.SetReuseAddr, Opt: 1} sockOptInts = append(sockOptInts, sockOpt) } if options.TCPNoDelay == TCPNoDelay && strings.HasPrefix(network, "tcp") { sockOpt := socket.Option[int]{SetSockOpt: socket.SetNoDelay, Opt: 1} sockOptInts = append(sockOptInts, sockOpt) } if options.SocketRecvBuffer > 0 { sockOpt := socket.Option[int]{SetSockOpt: socket.SetRecvBuffer, Opt: options.SocketRecvBuffer} sockOptInts = append(sockOptInts, sockOpt) } if options.SocketSendBuffer > 0 { sockOpt := socket.Option[int]{SetSockOpt: socket.SetSendBuffer, Opt: options.SocketSendBuffer} sockOptInts = append(sockOptInts, sockOpt) } if strings.HasPrefix(network, "udp") { udpAddr, err := net.ResolveUDPAddr(network, addr) if err == nil && udpAddr.IP.IsMulticast() { if sockoptFn := socket.SetMulticastMembership(network, udpAddr); sockoptFn != nil { sockOpt := socket.Option[int]{SetSockOpt: sockoptFn, Opt: options.MulticastInterfaceIndex} sockOptInts = append(sockOptInts, sockOpt) } } } if options.BindToDevice != "" { sockOpt := socket.Option[string]{SetSockOpt: socket.SetBindToDevice, Opt: options.BindToDevice} sockOptStrs = append(sockOptStrs, sockOpt) } ln = &listener{network: network, address: addr, sockOptInts: sockOptInts, sockOptStrs: sockOptStrs} err = ln.open() if options.TCPKeepAlive > 0 && ln.network == "tcp" && (runtime.GOOS == "linux" || runtime.GOOS == "freebsd" || runtime.GOOS == "dragonfly") { // TCP keepalive options will be inherited from the listening socket // only when running on Linux, FreeBSD, or DragonFlyBSD. // // Check out https://github.com/nginx/nginx/pull/337 for details. err = setKeepAlive( ln.fd, true, options.TCPKeepAlive, options.TCPKeepInterval, options.TCPKeepCount) } return } ================================================ FILE: listener_windows.go ================================================ // Copyright (c) 2023 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gnet import ( "context" "errors" "net" "os" "sync" "syscall" "golang.org/x/sys/windows" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" ) type listener struct { openOnce, closeOnce sync.Once network string address string lc *net.ListenConfig ln net.Listener pc net.PacketConn addr net.Addr } func (l *listener) dup() (int, error) { if l.ln == nil && l.pc == nil { return -1, errorx.ErrUnsupportedOp } var ( sc syscall.Conn ok bool ) if l.ln != nil { sc, ok = l.ln.(syscall.Conn) } else { sc, ok = l.pc.(syscall.Conn) } if !ok { return -1, errors.New("failed to convert net.Conn to syscall.Conn") } rc, err := sc.SyscallConn() if err != nil { return -1, errors.New("failed to get syscall.RawConn from net.Conn") } var dupHandle windows.Handle e := rc.Control(func(fd uintptr) { process := windows.CurrentProcess() err = windows.DuplicateHandle( process, windows.Handle(fd), process, &dupHandle, 0, true, windows.DUPLICATE_SAME_ACCESS, ) }) if err != nil { return -1, err } if e != nil { return -1, e } return int(dupHandle), nil } func (l *listener) open() (err error) { l.openOnce.Do(func() { switch l.network { case "udp", "udp4", "udp6": if l.pc, err = l.lc.ListenPacket(context.Background(), l.network, l.address); err == nil { l.addr = l.pc.LocalAddr() } case "unix": _ = os.Remove(l.address) fallthrough case "tcp", "tcp4", "tcp6": if l.ln, err = l.lc.Listen(context.Background(), l.network, l.address); err == nil { l.addr = l.ln.Addr() } default: err = errorx.ErrUnsupportedProtocol } }) return } func (l *listener) close() { l.closeOnce.Do(func() { if l.pc != nil { logging.Error(os.NewSyscallError("close", l.pc.Close())) return } l.pc = nil logging.Error(os.NewSyscallError("close", l.ln.Close())) }) } func initListener(network, addr string, options *Options) (*listener, error) { lc := net.ListenConfig{ Control: func(network, address string, c syscall.RawConn) error { return c.Control(func(fd uintptr) { if network != "unix" && (options.ReuseAddr || options.ReusePort) { _ = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1) } if options.TCPNoDelay == TCPNoDelay { _ = windows.SetsockoptInt(windows.Handle(fd), windows.IPPROTO_TCP, windows.TCP_NODELAY, 1) } if options.SocketRecvBuffer > 0 { _ = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_RCVBUF, options.SocketRecvBuffer) } if options.SocketSendBuffer > 0 { _ = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_SNDBUF, options.SocketSendBuffer) } }) }, KeepAlive: options.TCPKeepAlive, } l := listener{network: network, address: addr, lc: &lc} return &l, l.open() } ================================================ FILE: load_balancer.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gnet import ( "hash/crc32" "net" "github.com/panjf2000/gnet/v2/pkg/bs" ) // LoadBalancing represents the type of load-balancing algorithm. type LoadBalancing int const ( // RoundRobin assigns the next accepted connection to the event-loop by polling event-loop list. RoundRobin LoadBalancing = iota // LeastConnections assigns the next accepted connection to the event-loop that is // serving the least number of active connections at the current time. LeastConnections // SourceAddrHash assigns the next accepted connection to the event-loop by hashing the remote address. SourceAddrHash ) type ( // loadBalancer is an interface which manipulates the event-loop set. loadBalancer interface { register(*eventloop) next(net.Addr) *eventloop index(int) *eventloop iterate(func(int, *eventloop) bool) len() int } // baseLoadBalancer with base lb. baseLoadBalancer struct { eventLoops []*eventloop size int } // roundRobinLoadBalancer with Round-Robin algorithm. roundRobinLoadBalancer struct { baseLoadBalancer nextIndex uint64 } // leastConnectionsLoadBalancer with Least-Connections algorithm. leastConnectionsLoadBalancer struct { baseLoadBalancer } // sourceAddrHashLoadBalancer with Hash algorithm. sourceAddrHashLoadBalancer struct { baseLoadBalancer } ) // ==================================== Implementation of base load-balancer ==================================== // register adds a new eventloop into load-balancer. func (lb *baseLoadBalancer) register(el *eventloop) { el.idx = lb.size lb.eventLoops = append(lb.eventLoops, el) lb.size++ } // index returns the eligible eventloop by index. func (lb *baseLoadBalancer) index(i int) *eventloop { if i >= lb.size { return nil } return lb.eventLoops[i] } // iterate iterates all the eventloops. func (lb *baseLoadBalancer) iterate(f func(int, *eventloop) bool) { for i, el := range lb.eventLoops { if !f(i, el) { break } } } // len returns the length of event-loop list. func (lb *baseLoadBalancer) len() int { return lb.size } // ==================================== Implementation of Round-Robin load-balancer ==================================== // next returns the eligible event-loop based on Round-Robin algorithm. func (lb *roundRobinLoadBalancer) next(_ net.Addr) (el *eventloop) { el = lb.eventLoops[lb.nextIndex%uint64(lb.size)] lb.nextIndex++ return } // ================================= Implementation of Least-Connections load-balancer ================================= func (lb *leastConnectionsLoadBalancer) next(_ net.Addr) (el *eventloop) { el = lb.eventLoops[0] minN := el.countConn() for _, v := range lb.eventLoops[1:] { if n := v.countConn(); n < minN { minN = n el = v } } return } // ======================================= Implementation of Hash load-balancer ======================================== // hash converts a string to a unique hash code. func (*sourceAddrHashLoadBalancer) hash(s string) int { v := int(crc32.ChecksumIEEE(bs.StringToBytes(s))) if v >= 0 { return v } return -v } // next returns the eligible event-loop by taking the remainder of a hash code as the index of event-loop list. func (lb *sourceAddrHashLoadBalancer) next(netAddr net.Addr) *eventloop { hashCode := lb.hash(netAddr.String()) return lb.eventLoops[hashCode%lb.size] } ================================================ FILE: options.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gnet import ( "time" "github.com/panjf2000/gnet/v2/pkg/logging" ) // Option is a function that will set up option. type Option func(opts *Options) func loadOptions(options ...Option) *Options { opts := new(Options) for _, option := range options { option(opts) } return opts } // TCPSocketOpt is the type of TCP socket options. type TCPSocketOpt int // Available TCP socket options. const ( TCPNoDelay TCPSocketOpt = iota TCPDelay ) // Options are configurations for the gnet application. type Options struct { // LB represents the load-balancing algorithm used when assigning new connections // to event loops. This option is server-only, and it is not applicable to the client. LB LoadBalancing // ReuseAddr indicates whether to set the SO_REUSEADDR socket option. // This option is server-only. ReuseAddr bool // ReusePort indicates whether to set the SO_REUSEPORT socket option. // This option is server-only. ReusePort bool // MulticastInterfaceIndex is the index of the interface name where the multicast UDP addresses will be bound to. // This option is server-only. MulticastInterfaceIndex int // BindToDevice is the name of the interface to which the listening socket will be bound. // It is only available on Linux at the moment, an error will therefore be returned when // setting this option on non-linux platforms. // This option is server-only. BindToDevice string // Multicore indicates whether the engine will be effectively created with multi-cores, if so, // then you must take care with synchronizing memory between all event callbacks; otherwise, // it will run the engine with single thread. The number of threads in the engine will be // automatically assigned to the number of usable logical CPUs that can be leveraged by the // current process. Multicore bool // NumEventLoop is set up to start the given number of event-loop goroutines. // Note that a non-negative NumEventLoop will override Multicore. NumEventLoop int // ReadBufferCap is the maximum number of bytes that can be read from the remote when the readable event comes. // The default value is 64KB, it can either be reduced to avoid starving the subsequent connections or increased // to read more data from a socket. // // Note that ReadBufferCap will always be converted to the least power of two integer value greater than // or equal to its real amount. ReadBufferCap int // WriteBufferCap is the maximum number of bytes that a static outbound buffer can hold, // if the data exceeds this value, the overflow bytes will be stored in the elastic linked list buffer. // The default value is 64KB. // // Note that WriteBufferCap will always be converted to the least power of two integer value greater than // or equal to its real amount. WriteBufferCap int // LockOSThread is used to determine whether each I/O event-loop should be associated to an OS thread, // it is useful when you need some kind of mechanisms like thread local storage, or invoke certain C // libraries (such as graphics lib: GLib) that require thread-level manipulation via cgo, or want all I/O // event-loops to actually run in parallel for a potential higher performance. LockOSThread bool // Ticker indicates whether the ticker has been set up. Ticker bool // TCPKeepAlive enables the TCP keep-alive mechanism (SO_KEEPALIVE) and set its value // on TCP_KEEPIDLE. // When TCPKeepInterval is not set, 1/5 of TCPKeepAlive will be set on TCP_KEEPINTVL, // and 5 will be set on TCP_KEEPCNT if TCPKeepCount is not assigned to a positive value. TCPKeepAlive time.Duration // TCPKeepInterval is the value for TCP_KEEPINTVL, it's the interval between // TCP keep-alive probes. TCPKeepInterval time.Duration // TCPKeepCount is the number of keep-alive probes that will be sent before // the connection is considered dead and dropped. TCPKeepCount int // TCPNoDelay controls whether the operating system should delay // packet transmission in hopes of sending fewer packets (Nagle's algorithm). // When this option is assigned to TCPNoDelay, TCP_NODELAY socket option will // be turned on, on the contrary, if it is assigned to TCPDelay, the socket // option will be turned off. // // The default is TCPNoDelay, meaning that TCP_NODELAY is turned on and data // will not be buffered but sent as soon as possible after a write operation. TCPNoDelay TCPSocketOpt // SocketRecvBuffer sets the maximum socket receive buffer of kernel in bytes. SocketRecvBuffer int // SocketSendBuffer sets the maximum socket send buffer of kernel in bytes. SocketSendBuffer int // LogPath specifies a local path where logs will be written, this is the easiest // way to set up logging, gnet instantiates a default uber-go/zap logger with this // given log path, you are also allowed to employ your own logger during the lifetime // by implementing the following logging.Logger interface. // // Note that this option can be overridden by a non-nil option Logger. LogPath string // LogLevel specifies the logging level, it should be used along with LogPath. LogLevel logging.Level // Logger is the customized logger for logging info, if it is not set, // then gnet will use the default logger powered by go.uber.org/zap. Logger logging.Logger // EdgeTriggeredIO enables the edge-triggered I/O for the underlying epoll/kqueue event-loop. // Don't enable it unless you are 100% sure what you are doing. // Note that this option is only available for stream-oriented protocol. EdgeTriggeredIO bool // EdgeTriggeredIOChunk specifies the number of bytes that `gnet` can // read/write up to in one event loop of ET. This option implies // EdgeTriggeredIO when it is set to a value greater than 0. // If EdgeTriggeredIO is set to true and EdgeTriggeredIOChunk is not set, // 1MB is used. The value of EdgeTriggeredIOChunk must be a power of 2, // otherwise, it will be rounded up to the nearest power of 2. EdgeTriggeredIOChunk int } // WithOptions sets up all options. func WithOptions(options Options) Option { return func(opts *Options) { *opts = options } } // WithMulticore enables multi-cores mode for gnet engine. func WithMulticore(multicore bool) Option { return func(opts *Options) { opts.Multicore = multicore } } // WithLockOSThread enables LockOSThread mode for I/O event-loops. func WithLockOSThread(lockOSThread bool) Option { return func(opts *Options) { opts.LockOSThread = lockOSThread } } // WithReadBufferCap sets ReadBufferCap for reading bytes. func WithReadBufferCap(readBufferCap int) Option { return func(opts *Options) { opts.ReadBufferCap = readBufferCap } } // WithWriteBufferCap sets WriteBufferCap for pending bytes. func WithWriteBufferCap(writeBufferCap int) Option { return func(opts *Options) { opts.WriteBufferCap = writeBufferCap } } // WithLoadBalancing picks the load-balancing algorithm for gnet engine. func WithLoadBalancing(lb LoadBalancing) Option { return func(opts *Options) { opts.LB = lb } } // WithNumEventLoop sets the number of event loops for gnet engine. func WithNumEventLoop(numEventLoop int) Option { return func(opts *Options) { opts.NumEventLoop = numEventLoop } } // WithReusePort sets SO_REUSEPORT socket option. func WithReusePort(reusePort bool) Option { return func(opts *Options) { opts.ReusePort = reusePort } } // WithReuseAddr sets SO_REUSEADDR socket option. func WithReuseAddr(reuseAddr bool) Option { return func(opts *Options) { opts.ReuseAddr = reuseAddr } } // WithTCPKeepAlive enables the TCP keep-alive mechanism and sets its values. func WithTCPKeepAlive(tcpKeepAlive time.Duration) Option { return func(opts *Options) { opts.TCPKeepAlive = tcpKeepAlive } } // WithTCPKeepInterval sets the interval between TCP keep-alive probes. func WithTCPKeepInterval(tcpKeepInterval time.Duration) Option { return func(opts *Options) { opts.TCPKeepInterval = tcpKeepInterval } } // WithTCPKeepCount sets the number of keep-alive probes that will be sent before // the connection is considered dead and dropped. func WithTCPKeepCount(tcpKeepCount int) Option { return func(opts *Options) { opts.TCPKeepCount = tcpKeepCount } } // WithTCPNoDelay enable/disable the TCP_NODELAY socket option. func WithTCPNoDelay(tcpNoDelay TCPSocketOpt) Option { return func(opts *Options) { opts.TCPNoDelay = tcpNoDelay } } // WithSocketRecvBuffer sets the maximum socket receive buffer of kernel in bytes. func WithSocketRecvBuffer(recvBuf int) Option { return func(opts *Options) { opts.SocketRecvBuffer = recvBuf } } // WithSocketSendBuffer sets the maximum socket send buffer of kernel in bytes. func WithSocketSendBuffer(sendBuf int) Option { return func(opts *Options) { opts.SocketSendBuffer = sendBuf } } // WithTicker indicates whether a ticker is currently set. func WithTicker(ticker bool) Option { return func(opts *Options) { opts.Ticker = ticker } } // WithLogPath specifies a local path for logging file. func WithLogPath(fileName string) Option { return func(opts *Options) { opts.LogPath = fileName } } // WithLogLevel specifies the logging level for the local logging file. func WithLogLevel(lvl logging.Level) Option { return func(opts *Options) { opts.LogLevel = lvl } } // WithLogger specifies a customized logger. func WithLogger(logger logging.Logger) Option { return func(opts *Options) { opts.Logger = logger } } // WithMulticastInterfaceIndex sets the interface name where UDP multicast sockets will be bound to. func WithMulticastInterfaceIndex(idx int) Option { return func(opts *Options) { opts.MulticastInterfaceIndex = idx } } // WithBindToDevice sets the name of the interface to which the listening socket will be bound. // // It is only available on Linux at the moment, an error will therefore be returned when // setting this option on non-linux platforms. func WithBindToDevice(iface string) Option { return func(opts *Options) { opts.BindToDevice = iface } } // WithEdgeTriggeredIO enables the edge-triggered I/O for the underlying epoll/kqueue event-loop. func WithEdgeTriggeredIO(et bool) Option { return func(opts *Options) { opts.EdgeTriggeredIO = et } } // WithEdgeTriggeredIOChunk sets the number of bytes that `gnet` can // read/write up to in one event loop of ET. func WithEdgeTriggeredIOChunk(chunk int) Option { return func(opts *Options) { opts.EdgeTriggeredIOChunk = chunk } } ================================================ FILE: os_unix_test.go ================================================ //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package gnet import ( "context" crand "crypto/rand" "errors" "fmt" "math/rand" "net" "regexp" "runtime" "strings" "sync" "sync/atomic" "testing" "time" "github.com/stretchr/testify/assert" "golang.org/x/sys/unix" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" goPool "github.com/panjf2000/gnet/v2/pkg/pool/goroutine" ) var SysClose = unix.Close // NOTE: TestServeMulticast can fail with "write: no buffer space available" on Wi-Fi interface. func TestServeMulticast(t *testing.T) { t.Run("IPv4", func(t *testing.T) { // 224.0.0.169 is an unassigned address from the Local Network Control Block // https://www.iana.org/assignments/multicast-addresses/multicast-addresses.xhtml#multicast-addresses-1 t.Run("udp-multicast", func(t *testing.T) { testMulticast(t, "224.0.0.169:9991", false, false, -1, 10) }) t.Run("udp-multicast-reuseport", func(t *testing.T) { testMulticast(t, "224.0.0.169:9991", true, false, -1, 10) }) t.Run("udp-multicast-reuseaddr", func(t *testing.T) { testMulticast(t, "224.0.0.169:9991", false, true, -1, 10) }) }) t.Run("IPv6", func(t *testing.T) { iface, err := findLoopbackInterface() assert.NoError(t, err) if iface.Flags&net.FlagMulticast != net.FlagMulticast { t.Skip("multicast is not supported on loopback interface") } // ff02::3 is an unassigned address from Link-Local Scope Multicast Addresses // https://www.iana.org/assignments/ipv6-multicast-addresses/ipv6-multicast-addresses.xhtml#link-local t.Run("udp-multicast", func(t *testing.T) { testMulticast(t, fmt.Sprintf("[ff02::3%%%s]:9991", iface.Name), false, false, iface.Index, 10) }) t.Run("udp-multicast-reuseport", func(t *testing.T) { testMulticast(t, fmt.Sprintf("[ff02::3%%%s]:9991", iface.Name), true, false, iface.Index, 10) }) t.Run("udp-multicast-reuseaddr", func(t *testing.T) { testMulticast(t, fmt.Sprintf("[ff02::3%%%s]:9991", iface.Name), false, true, iface.Index, 10) }) }) } func findLoopbackInterface() (*net.Interface, error) { ifaces, err := net.Interfaces() if err != nil { return nil, err } for _, iface := range ifaces { if iface.Flags&net.FlagLoopback == net.FlagLoopback { return &iface, nil } } return nil, errors.New("no loopback interface") } func testMulticast(t *testing.T, addr string, reuseport, reuseaddr bool, index, nclients int) { ts := &testMcastServer{ t: t, addr: addr, nclients: nclients, } options := []Option{ WithReuseAddr(reuseaddr), WithReusePort(reuseport), WithSocketRecvBuffer(2 * nclients * 1024), // enough space to receive messages from nclients to eliminate dropped packets WithTicker(true), } if index != -1 { options = append(options, WithMulticastInterfaceIndex(index)) } err := Run(ts, "udp://"+addr, options...) assert.NoError(t, err) } type testMcastServer struct { *BuiltinEventEngine t *testing.T mcast sync.Map addr string nclients int started int32 active int32 } func (s *testMcastServer) startMcastClient() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() c, err := net.Dial("udp", s.addr) assert.NoError(s.t, err) defer c.Close() //nolint:errcheck ch := make(chan []byte, 10000) s.mcast.Store(c.LocalAddr().String(), ch) duration := time.Duration((rand.Float64()*2+1)*float64(time.Second)) / 2 logging.Debugf("test duration: %v", duration) start := time.Now() for time.Since(start) < duration { reqData := make([]byte, 1024) _, err = crand.Read(reqData) assert.NoError(s.t, err) _, err = c.Write(reqData) assert.NoError(s.t, err) // Workaround for MacOS "write: no buffer space available" error messages // https://developer.apple.com/forums/thread/42334 time.Sleep(time.Millisecond * 100) select { case respData := <-ch: assert.Equalf(s.t, reqData, respData, "response mismatch, length of bytes: %d vs %d", len(reqData), len(respData)) case <-ctx.Done(): assert.Fail(s.t, "timeout receiving message") return } } } func (s *testMcastServer) OnTraffic(c Conn) (action Action) { buf, _ := c.Next(-1) b := make([]byte, len(buf)) copy(b, buf) ch, ok := s.mcast.Load(c.RemoteAddr().String()) assert.True(s.t, ok) ch.(chan []byte) <- b return } func (s *testMcastServer) OnTick() (delay time.Duration, action Action) { if atomic.CompareAndSwapInt32(&s.started, 0, 1) { for i := 0; i < s.nclients; i++ { atomic.AddInt32(&s.active, 1) err := goPool.DefaultWorkerPool.Submit(func() { defer atomic.AddInt32(&s.active, -1) s.startMcastClient() }) assert.NoError(s.t, err) } } if atomic.LoadInt32(&s.active) == 0 { action = Shutdown return } delay = time.Second / 5 return } type testMulticastBindServer struct { *BuiltinEventEngine } func (t *testMulticastBindServer) OnTick() (delay time.Duration, action Action) { action = Shutdown return } func TestMulticastBindIPv4(t *testing.T) { ts := &testMulticastBindServer{} iface, err := findLoopbackInterface() assert.NoError(t, err) err = Run(ts, "udp://224.0.0.169:9991", WithMulticastInterfaceIndex(iface.Index), WithTicker(true)) assert.NoError(t, err) } func TestMulticastBindIPv6(t *testing.T) { ts := &testMulticastBindServer{} iface, err := findLoopbackInterface() assert.NoError(t, err) err = Run(ts, fmt.Sprintf("udp://[ff02::3%%%s]:9991", iface.Name), WithMulticastInterfaceIndex(iface.Index), WithTicker(true)) assert.NoError(t, err) } func detectLinuxEthernetInterfaceName() (string, error) { ifaces, err := net.Interfaces() if err != nil { return "", err } // Traditionally, network interfaces were named as eth0, eth1, etc., for Ethernet interfaces. // However, with the introduction of predictable network interface names. Meanwhile, modern // convention commonly uses patterns like eno[1-N], ens[1-N], enps, etc., // for Ethernet interfaces. // Check out https://www.thomas-krenn.com/en/wiki/Predictable_Network_Interface_Names and // https://en.wikipedia.org/wiki/Consistent_Network_Device_Naming for more details. regex := regexp.MustCompile(`e(no|ns|np|th)\d+s*\d*$`) for _, iface := range ifaces { if iface.Flags&net.FlagLoopback != 0 || iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagRunning == 0 { continue } if regex.MatchString(iface.Name) { return iface.Name, nil } } return "", errors.New("no Ethernet interface found") } func getInterfaceIP(ifname string, ipv4 bool) (net.IP, error) { iface, err := net.InterfaceByName(ifname) if err != nil { return nil, err } // Get all unicast addresses for this interface addrs, err := iface.Addrs() if err != nil { return nil, err } // Loop through the addresses and find the first IPv4 address for _, addr := range addrs { var ip net.IP switch v := addr.(type) { case *net.IPNet: ip = v.IP case *net.IPAddr: ip = v.IP } // Check if the IP is IPv4. if ip != nil && (ip.To4() != nil) == ipv4 { return ip, nil } } return nil, errors.New("no valid IP address found") } type testBindToDeviceServer[T interface{ *net.TCPAddr | *net.UDPAddr }] struct { BuiltinEventEngine tester *testing.T data []byte packets atomic.Int32 expectedPackets int32 network string loopBackAddr T eth0Addr T broadcastAddr T } func netDial[T *net.TCPAddr | *net.UDPAddr](network string, a T) (net.Conn, error) { addr := any(a) switch v := addr.(type) { case *net.TCPAddr: return net.DialTCP(network, nil, v) case *net.UDPAddr: return net.DialUDP(network, nil, v) default: return nil, errors.New("unsupported address type") } } func (s *testBindToDeviceServer[T]) OnTraffic(c Conn) (action Action) { b, err := c.Next(-1) assert.NoError(s.tester, err) assert.EqualValues(s.tester, s.data, b) _, err = c.Write(b) assert.NoError(s.tester, err) s.packets.Add(1) return } func (s *testBindToDeviceServer[T]) OnShutdown(_ Engine) { assert.EqualValues(s.tester, s.expectedPackets, s.packets.Load()) } func (s *testBindToDeviceServer[T]) OnTick() (delay time.Duration, action Action) { // Send a packet to the loopback interface, it should never make its way to the server // because we've bound the server to eth0. c, err := netDial(s.network, s.loopBackAddr) if strings.HasPrefix(s.network, "tcp") { assert.ErrorContains(s.tester, err, "connection refused") } else { assert.NoError(s.tester, err) defer c.Close() //nolint:errcheck _, err = c.Write(s.data) assert.NoError(s.tester, err) } if s.broadcastAddr != nil { // Send a packet to the broadcast address, it should reach the server. c6, err := netDial(s.network, s.broadcastAddr) assert.NoError(s.tester, err) defer c6.Close() //nolint:errcheck _, err = c6.Write(s.data) assert.NoError(s.tester, err) } // Send a packet to the eth0 interface, it should reach the server. c4, err := netDial(s.network, s.eth0Addr) assert.NoError(s.tester, err) defer c4.Close() //nolint:errcheck _, err = c4.Write(s.data) assert.NoError(s.tester, err) buf := make([]byte, len(s.data)) _, err = c4.Read(buf) assert.NoError(s.tester, err) assert.EqualValues(s.tester, s.data, buf, len(s.data), len(buf)) return time.Second, Shutdown } func TestBindToDevice(t *testing.T) { if runtime.GOOS != "linux" { err := Run(&testBindToDeviceServer[*net.UDPAddr]{}, "tcp://:9999", WithBindToDevice("eth0")) assert.ErrorIs(t, err, errorx.ErrUnsupportedOp) return } lp, err := findLoopbackInterface() assert.NoError(t, err) dev, err := detectLinuxEthernetInterfaceName() assert.NoErrorf(t, err, "no testable Ethernet interface found") t.Logf("detected Ethernet interface: %s", dev) data := []byte("hello") t.Run("IPv4", func(t *testing.T) { ip, err := getInterfaceIP(dev, true) assert.NoError(t, err) t.Run("TCP", func(t *testing.T) { ts := &testBindToDeviceServer[*net.TCPAddr]{ tester: t, data: data, expectedPackets: 1, network: "tcp", loopBackAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 9999, Zone: ""}, eth0Addr: &net.TCPAddr{IP: ip, Port: 9999, Zone: ""}, } assert.NoError(t, err) err = Run(ts, "tcp://0.0.0.0:9999", WithTicker(true), WithBindToDevice(dev)) assert.NoError(t, err) }) t.Run("UDP", func(t *testing.T) { ts := &testBindToDeviceServer[*net.UDPAddr]{ tester: t, data: data, expectedPackets: 2, network: "udp", loopBackAddr: &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 9999, Zone: ""}, eth0Addr: &net.UDPAddr{IP: ip, Port: 9999, Zone: ""}, broadcastAddr: &net.UDPAddr{IP: net.IPv4bcast, Port: 9999, Zone: ""}, } assert.NoError(t, err) err = Run(ts, "udp://0.0.0.0:9999", WithTicker(true), WithBindToDevice(dev)) assert.NoError(t, err) }) }) t.Run("IPv6", func(t *testing.T) { t.Run("TCP", func(t *testing.T) { ip, err := getInterfaceIP(dev, false) assert.NoError(t, err) ts := &testBindToDeviceServer[*net.TCPAddr]{ tester: t, data: data, expectedPackets: 1, network: "tcp6", loopBackAddr: &net.TCPAddr{IP: net.IPv6loopback, Port: 9999, Zone: lp.Name}, eth0Addr: &net.TCPAddr{IP: ip, Port: 9999, Zone: dev}, } assert.NoError(t, err) err = Run(ts, "tcp6://[::]:9999", WithTicker(true), WithBindToDevice(dev)) assert.NoError(t, err) }) t.Run("UDP", func(t *testing.T) { ip, err := getInterfaceIP(dev, false) assert.NoError(t, err) ts := &testBindToDeviceServer[*net.UDPAddr]{ tester: t, data: data, expectedPackets: 2, network: "udp6", loopBackAddr: &net.UDPAddr{IP: net.IPv6loopback, Port: 9999, Zone: lp.Name}, eth0Addr: &net.UDPAddr{IP: ip, Port: 9999, Zone: dev}, broadcastAddr: &net.UDPAddr{IP: net.IPv6linklocalallnodes, Port: 9999, Zone: dev}, } assert.NoError(t, err) err = Run(ts, "udp6://[::]:9999", WithTicker(true), WithBindToDevice(dev)) assert.NoError(t, err) }) }) } /* func TestEngineAsyncWrite(t *testing.T) { t.Run("tcp", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { testEngineAsyncWrite(t, "tcp", ":18888", false, false, 10, LeastConnections) }) t.Run("N-loop", func(t *testing.T) { testEngineAsyncWrite(t, "tcp", ":28888", true, true, 10, RoundRobin) }) }) t.Run("unix", func(t *testing.T) { t.Run("1-loop", func(t *testing.T) { testEngineAsyncWrite(t, "unix", ":18888", false, false, 10, LeastConnections) }) t.Run("N-loop", func(t *testing.T) { testEngineAsyncWrite(t, "unix", ":28888", true, true, 10, RoundRobin) }) }) } type testEngineAsyncWriteServer struct { *BuiltinEventEngine tester *testing.T eng Engine network string addr string multicore bool writev bool nclients int started int32 connected int32 clientActive int32 disconnected int32 workerPool *goPool.Pool } func (s *testEngineAsyncWriteServer) OnBoot(eng Engine) (action Action) { s.eng = eng return } func (s *testEngineAsyncWriteServer) OnOpen(c Conn) (out []byte, action Action) { c.SetContext(c) atomic.AddInt32(&s.connected, 1) out = []byte("sweetness\r\n") assert.NotNil(s.tester, c.LocalAddr(), "nil local addr") assert.NotNil(s.tester, c.RemoteAddr(), "nil remote addr") return } func (s *testEngineAsyncWriteServer) OnClose(c Conn, err error) (action Action) { if err != nil { logging.Debugf("error occurred on closed, %v\n", err) } if s.network != "udp" { assert.Equal(s.tester, c.Context(), c, "invalid context") } atomic.AddInt32(&s.disconnected, 1) if atomic.LoadInt32(&s.connected) == atomic.LoadInt32(&s.disconnected) && atomic.LoadInt32(&s.disconnected) == int32(s.nclients) { action = Shutdown s.workerPool.Release() } return } func (s *testEngineAsyncWriteServer) OnTraffic(c Conn) (action Action) { gFD := c.Gfd() buf := bbPool.Get() _, _ = c.WriteTo(buf) // just for test _ = c.InboundBuffered() _ = c.OutboundBuffered() _, _ = c.Discard(1) _ = s.workerPool.Submit( func() { if s.writev { mid := buf.Len() / 2 bs := make([][]byte, 2) bs[0] = buf.B[:mid] bs[1] = buf.B[mid:] _ = s.eng.AsyncWritev(gFD, bs, func(c Conn, err error) error { if c.RemoteAddr() != nil { logging.Debugf("conn=%s done writev: %v", c.RemoteAddr().String(), err) } bbPool.Put(buf) return nil }) } else { _ = s.eng.AsyncWrite(gFD, buf.Bytes(), func(c Conn, err error) error { if c.RemoteAddr() != nil { logging.Debugf("conn=%s done write: %v", c.RemoteAddr().String(), err) } bbPool.Put(buf) return nil }) } }) return } func (s *testEngineAsyncWriteServer) OnTick() (delay time.Duration, action Action) { delay = time.Second / 5 if atomic.CompareAndSwapInt32(&s.started, 0, 1) { for i := 0; i < s.nclients; i++ { atomic.AddInt32(&s.clientActive, 1) go func() { initClient(s.tester, s.network, s.addr, s.multicore) atomic.AddInt32(&s.clientActive, -1) }() } } if s.network == "udp" && atomic.LoadInt32(&s.clientActive) == 0 { action = Shutdown return } return } func testEngineAsyncWrite(t *testing.T, network, addr string, multicore, writev bool, nclients int, lb LoadBalancing) { ts := &testEngineAsyncWriteServer{ tester: t, network: network, addr: addr, multicore: multicore, writev: writev, nclients: nclients, workerPool: goPool.Default(), } err := Run(ts, network+"://"+addr, WithMulticore(multicore), WithTicker(true), WithLoadBalancing(lb)) assert.NoError(t, err) } func initClient(t *testing.T, network, addr string, multicore bool) { rand.Seed(time.Now().UnixNano()) c, err := net.Dial(network, addr) assert.NoError(t, err) defer c.Close() rd := bufio.NewReader(c) msg, err := rd.ReadBytes('\n') assert.NoError(t, err) assert.Equal(t, string(msg), "sweetness\r\n", "bad header") duration := time.Duration((rand.Float64()*2+1)*float64(time.Second)) / 2 t.Logf("test duration: %dms", duration/time.Millisecond) start := time.Now() for time.Since(start) < duration { reqData := make([]byte, streamLen) _, err = rand.Read(reqData) assert.NoError(t, err) _, err = c.Write(reqData) assert.NoError(t, err) respData := make([]byte, len(reqData)) _, err = io.ReadFull(rd, respData) assert.NoError(t, err) assert.Equalf( t, len(reqData), len(respData), "response mismatch with protocol:%s, multi-core:%t, length of bytes: %d vs %d", network, multicore, len(reqData), len(respData), ) } } func TestEngineWakeConn(t *testing.T) { testEngineWakeConn(t, "tcp", ":9990") } type testEngineWakeConnServer struct { *BuiltinEventEngine tester *testing.T eng Engine network string addr string gFD chan gfd.GFD wake bool } func (t *testEngineWakeConnServer) OnBoot(eng Engine) (action Action) { t.eng = eng return } func (t *testEngineWakeConnServer) OnOpen(c Conn) (out []byte, action Action) { t.gFD <- c.Gfd() return } func (t *testEngineWakeConnServer) OnClose(Conn, error) (action Action) { action = Shutdown return } func (t *testEngineWakeConnServer) OnTraffic(c Conn) (action Action) { _, _ = c.Write([]byte("Waking up.")) action = -1 return } func (t *testEngineWakeConnServer) OnTick() (delay time.Duration, action Action) { if !t.wake { t.wake = true delay = time.Millisecond * 100 go func() { conn, err := net.Dial(t.network, t.addr) assert.NoError(t.tester, err) defer conn.Close() r := make([]byte, 10) _, err = conn.Read(r) assert.NoError(t.tester, err) }() return } gFD := <-t.gFD _ = t.eng.Wake(gFD, func(c Conn, err error) error { logging.Debugf("conn=%s done wake: %v", c.RemoteAddr().String(), err) return nil }) delay = time.Millisecond * 100 return } func testEngineWakeConn(t *testing.T, network, addr string) { svr := &testEngineWakeConnServer{tester: t, network: network, addr: addr, gFD: make(chan gfd.GFD, 1)} logger := zap.NewExample() err := Run(svr, network+"://"+addr, WithTicker(true), WithNumEventLoop(2*runtime.NumCPU()), WithLogger(logger.Sugar()), WithSocketRecvBuffer(4*1024), WithSocketSendBuffer(4*1024), WithReadBufferCap(2000), WithWriteBufferCap(2000)) assert.NoError(t, err) _ = logger.Sync() } // Test should not panic when we wake-up server_closed conn. func TestEngineClosedWakeUp(t *testing.T) { events := &testEngineClosedWakeUpServer{ tester: t, BuiltinEventEngine: &BuiltinEventEngine{}, network: "tcp", addr: ":9999", protoAddr: "tcp://:9999", clientClosed: make(chan struct{}), serverClosed: make(chan struct{}), wakeup: make(chan struct{}), } err := Run(events, events.protoAddr) assert.NoError(t, err) } type testEngineClosedWakeUpServer struct { *BuiltinEventEngine tester *testing.T network, addr, protoAddr string eng Engine wakeup chan struct{} serverClosed chan struct{} clientClosed chan struct{} } func (s *testEngineClosedWakeUpServer) OnBoot(eng Engine) (action Action) { s.eng = eng go func() { c, err := net.Dial(s.network, s.addr) assert.NoError(s.tester, err) _, err = c.Write([]byte("hello")) assert.NoError(s.tester, err) <-s.wakeup _, err = c.Write([]byte("hello again")) assert.NoError(s.tester, err) close(s.clientClosed) <-s.serverClosed logging.Debugf("stop engine...", Stop(context.TODO(), s.protoAddr)) }() return None } func (s *testEngineClosedWakeUpServer) OnTraffic(c Conn) Action { assert.NotNil(s.tester, c.RemoteAddr()) select { case <-s.wakeup: default: close(s.wakeup) } fd := c.Gfd() go func() { assert.NoError(s.tester, c.Wake(nil)) }() go s.eng.Close(fd, nil) <-s.clientClosed _, _ = c.Write([]byte("answer")) return None } func (s *testEngineClosedWakeUpServer) OnClose(Conn, error) (action Action) { select { case <-s.serverClosed: default: close(s.serverClosed) } return } */ ================================================ FILE: os_windows_test.go ================================================ //go:build windows package gnet import ( "syscall" ) func SysClose(fd int) error { return syscall.CloseHandle(syscall.Handle(fd)) } ================================================ FILE: pkg/bs/bs.go ================================================ // Copyright (c) 2020 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package bs provides a few handy bytes/string functions. package bs import ( "unsafe" ) // BytesToString converts byte slice to a string without any memory allocation. func BytesToString(b []byte) string { return unsafe.String(unsafe.SliceData(b), len(b)) } // StringToBytes converts string to a byte slice without any memory allocation. func StringToBytes(s string) []byte { return unsafe.Slice(unsafe.StringData(s), len(s)) } ================================================ FILE: pkg/buffer/elastic/elastic_buffer_test.go ================================================ package elastic import ( "bytes" crand "crypto/rand" "math/rand" "runtime" "testing" "github.com/stretchr/testify/require" ) func TestMixedBuffer_Basic(t *testing.T) { const maxStaticSize = 4 * 1024 mb, _ := New(maxStaticSize) const dataLen = 5 * 1024 data := make([]byte, dataLen) _, err := crand.Read(data) require.NoError(t, err) n, err := mb.Write(data) require.NoError(t, err) require.EqualValues(t, dataLen, n) require.EqualValues(t, dataLen, mb.Buffered()) require.EqualValues(t, dataLen, mb.ringBuffer.Buffered()) rbn := mb.ringBuffer.Len() mb.Reset(-1) newDataLen := rbn + 2*1024 data = make([]byte, newDataLen) _, err = crand.Read(data) require.NoError(t, err) n, err = mb.Write(data) require.NoError(t, err) require.EqualValues(t, newDataLen, n) require.EqualValues(t, newDataLen, mb.Buffered()) require.EqualValues(t, rbn, mb.ringBuffer.Buffered()) bs, err := mb.Peek(-1) require.NoError(t, err) var p []byte for _, b := range bs { p = append(p, b...) } require.EqualValues(t, data, p) bs, err = mb.Peek(rbn) require.NoError(t, err) p = bs[0] require.EqualValues(t, data[:rbn], p) n, err = mb.Discard(rbn) require.NoError(t, err) require.EqualValues(t, rbn, n) require.NotNil(t, mb.ringBuffer) bs, err = mb.Peek(newDataLen - rbn) require.NoError(t, err) p = bs[0] require.EqualValues(t, data[rbn:], p) n, err = mb.Discard(newDataLen - rbn) require.NoError(t, err) require.EqualValues(t, newDataLen-rbn, n) require.True(t, mb.IsEmpty()) runtime.GC() // release ring-buffer from pool. const maxBlocks = 100 var ( headCum int cum int buf bytes.Buffer ) bs = bs[:0] for i := 0; i < maxBlocks; i++ { n := rand.Intn(512) + 128 cum += n data := make([]byte, n) _, err := crand.Read(data) require.NoError(t, err) buf.Write(data) if i < 3 { headCum += n _, _ = mb.Write(data) } else { bs = append(bs, data) } } n, err = mb.Writev(bs) require.GreaterOrEqual(t, mb.ringBuffer.Len(), maxStaticSize) require.NoError(t, err) require.EqualValues(t, cum-headCum, n) require.EqualValues(t, cum, mb.Buffered()) bs, err = mb.Peek(-1) require.NoError(t, err) p = p[:0] for _, b := range bs { p = append(p, b...) } require.EqualValues(t, buf.Bytes(), p) p = make([]byte, cum) n, err = mb.Read(p) require.NoError(t, err) require.EqualValues(t, cum, n) require.EqualValues(t, buf.Bytes(), p) require.NotNil(t, mb.ringBuffer) require.True(t, mb.IsEmpty()) } func TestMixedBuffer_ReadFrom(t *testing.T) { const maxStaticSize = 2 * 1024 mb, _ := New(maxStaticSize) const dataLen = 2 * 1024 data := make([]byte, dataLen) _, err := crand.Read(data) require.NoError(t, err) r := bytes.NewReader(data) n, err := mb.ReadFrom(r) require.NoError(t, err) require.EqualValues(t, dataLen, n) require.EqualValues(t, dataLen, mb.Buffered()) newData := make([]byte, dataLen) _, err = crand.Read(newData) require.NoError(t, err) r.Reset(newData) n, err = mb.ReadFrom(r) require.NoError(t, err) require.EqualValues(t, dataLen, n) require.EqualValues(t, 2*dataLen, mb.Buffered()) require.False(t, mb.listBuffer.IsEmpty()) buf := make([]byte, dataLen) var m int m, err = mb.Read(buf) require.NoError(t, err) require.EqualValues(t, dataLen, m) require.EqualValues(t, data, buf) bs, err := mb.Peek(dataLen) require.NoError(t, err) var p []byte for _, b := range bs { p = append(p, b...) } require.EqualValues(t, newData, p) m, err = mb.Discard(dataLen) require.NoError(t, err) require.EqualValues(t, dataLen, m) require.NotNil(t, mb.ringBuffer) require.True(t, mb.IsEmpty()) } func TestMixedBuffer_WriteTo(t *testing.T) { const maxStaticSize = 4 * 1024 mb, _ := New(maxStaticSize) const maxBlocks = 50 var ( headCum int cum int bs [][]byte buf bytes.Buffer ) for i := 0; i < maxBlocks; i++ { n := rand.Intn(512) + 128 cum += n data := make([]byte, n) _, err := crand.Read(data) require.NoError(t, err) buf.Write(data) if i < 3 { headCum += n _, _ = mb.Write(data) } else { bs = append(bs, data) } } n, err := mb.Writev(bs) require.NoError(t, err) require.EqualValues(t, cum-headCum, n) require.EqualValues(t, cum, mb.Buffered()) newBuf := bytes.NewBuffer(nil) var m int64 m, err = mb.WriteTo(newBuf) require.NoError(t, err) require.EqualValues(t, cum, m) require.EqualValues(t, buf.Bytes(), newBuf.Bytes()) require.NotNil(t, mb.ringBuffer) require.True(t, mb.IsEmpty()) } ================================================ FILE: pkg/buffer/elastic/elastic_ring_buffer.go ================================================ // Copyright (c) 2022 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package elastic implements an elastic ring-buffer. package elastic import ( "io" "github.com/panjf2000/gnet/v2/pkg/buffer/ring" rbPool "github.com/panjf2000/gnet/v2/pkg/pool/ringbuffer" ) // RingBuffer is the elastic wrapper of ring.Buffer. type RingBuffer struct { rb *ring.Buffer } func (b *RingBuffer) instance() *ring.Buffer { if b.rb == nil { b.rb = rbPool.Get() } return b.rb } // Done checks and returns the internal ring-buffer to pool. func (b *RingBuffer) Done() { if b.rb != nil { rbPool.Put(b.rb) b.rb = nil } } func (b *RingBuffer) done() { if b.rb != nil && b.rb.IsEmpty() { rbPool.Put(b.rb) b.rb = nil } } // Peek returns the next n bytes without advancing the read pointer, // it returns all bytes when n <= 0. func (b *RingBuffer) Peek(n int) (head []byte, tail []byte) { if b.rb == nil { return nil, nil } return b.rb.Peek(n) } // Discard skips the next n bytes by advancing the read pointer. func (b *RingBuffer) Discard(n int) (int, error) { if b.rb == nil { return 0, ring.ErrIsEmpty } defer b.done() return b.rb.Discard(n) } // Read reads up to len(p) bytes into p. It returns the number of bytes read (0 <= n <= len(p)) and any error // encountered. // Even if Read returns n < len(p), it may use all of p as scratch space during the call. // If some data is available but not len(p) bytes, Read conventionally returns what is available instead of waiting // for more. // When Read encounters an error or end-of-file condition after successfully reading n > 0 bytes, // it returns the number of bytes read. It may return the (non-nil) error from the same call or return the // error (and n == 0) from a subsequent call. // Callers should always process the n > 0 bytes returned before considering the error err. // Doing so correctly handles I/O errors that happen after reading some bytes and also both of the allowed EOF // behaviors. func (b *RingBuffer) Read(p []byte) (int, error) { if b.rb == nil { return 0, ring.ErrIsEmpty } defer b.done() return b.rb.Read(p) } // ReadByte reads and returns the next byte from the input or ErrIsEmpty. func (b *RingBuffer) ReadByte() (byte, error) { if b.rb == nil { return 0, ring.ErrIsEmpty } defer b.done() return b.rb.ReadByte() } // Write writes len(p) bytes from p to the underlying buf. // It returns the number of bytes written from p (n == len(p) > 0) and any error encountered that caused the write to // stop early. // If the length of p is greater than the writable capacity of this ring-buffer, it will allocate more memory to // this ring-buffer. // Write must not modify the slice data, even temporarily. func (b *RingBuffer) Write(p []byte) (int, error) { if len(p) == 0 { return 0, nil } return b.instance().Write(p) } // WriteByte writes one byte into buffer. func (b *RingBuffer) WriteByte(c byte) error { return b.instance().WriteByte(c) } // Buffered returns the length of available bytes to read. func (b *RingBuffer) Buffered() int { if b.rb == nil { return 0 } return b.rb.Buffered() } // Len returns the length of the underlying buffer. func (b *RingBuffer) Len() int { if b.rb == nil { return 0 } return b.rb.Len() } // Cap returns the size of the underlying buffer. func (b *RingBuffer) Cap() int { if b.rb == nil { return 0 } return b.rb.Cap() } // Available returns the length of available bytes to write. func (b *RingBuffer) Available() int { if b.rb == nil { return 0 } return b.rb.Available() } // WriteString writes the contents of the string s to buffer, which accepts a slice of bytes. func (b *RingBuffer) WriteString(s string) (int, error) { if len(s) == 0 { return 0, nil } return b.instance().WriteString(s) } // Bytes returns all available read bytes. It does not move the read pointer and only copy the available data. func (b *RingBuffer) Bytes() []byte { if b.rb == nil { return nil } return b.rb.Bytes() } // ReadFrom implements io.ReaderFrom. func (b *RingBuffer) ReadFrom(r io.Reader) (int64, error) { return b.instance().ReadFrom(r) } // WriteTo implements io.WriterTo. func (b *RingBuffer) WriteTo(w io.Writer) (int64, error) { if b.rb == nil { return 0, ring.ErrIsEmpty } defer b.done() return b.instance().WriteTo(w) } // IsFull tells if this ring-buffer is full. func (b *RingBuffer) IsFull() bool { if b.rb == nil { return false } return b.rb.IsFull() } // IsEmpty tells if this ring-buffer is empty. func (b *RingBuffer) IsEmpty() bool { if b.rb == nil { return true } return b.rb.IsEmpty() } // Reset the read pointer and write pointer to zero. func (b *RingBuffer) Reset() { if b.rb == nil { return } b.rb.Reset() } ================================================ FILE: pkg/buffer/elastic/elastic_ring_list_buffer.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package elastic import ( "io" "math" "github.com/panjf2000/gnet/v2/pkg/buffer/linkedlist" errorx "github.com/panjf2000/gnet/v2/pkg/errors" ) // Buffer combines ring-buffer and list-buffer. // Ring-buffer is the top-priority buffer to store response data, gnet will only switch to // list-buffer if the data size of ring-buffer reaches the maximum(MaxStackingBytes), list-buffer is more // flexible and scalable, which helps the application reduce memory footprint. type Buffer struct { maxStaticBytes int ringBuffer RingBuffer listBuffer linkedlist.Buffer } // New instantiates an elastic.Buffer and returns it. func New(maxStaticBytes int) (*Buffer, error) { if maxStaticBytes <= 0 { return nil, errorx.ErrNegativeSize } return &Buffer{maxStaticBytes: maxStaticBytes}, nil } // Read reads data from the Buffer. func (mb *Buffer) Read(p []byte) (n int, err error) { n, err = mb.ringBuffer.Read(p) if n == len(p) { return n, err } var m int m, err = mb.listBuffer.Read(p[n:]) n += m return } // Peek returns n bytes as [][]byte, these bytes won't be discarded until Buffer.Discard() is called. func (mb *Buffer) Peek(n int) ([][]byte, error) { if n <= 0 || n == math.MaxInt32 { n = math.MaxInt32 } else if n > mb.Buffered() { return nil, io.ErrShortBuffer } head, tail := mb.ringBuffer.Peek(n) if mb.ringBuffer.Buffered() == n { return [][]byte{head, tail}, nil } return mb.listBuffer.PeekWithBytes(n, head, tail) } // Discard discards n bytes in this buffer. func (mb *Buffer) Discard(n int) (discarded int, err error) { discarded, err = mb.ringBuffer.Discard(n) if n <= discarded { return } n -= discarded var m int m, err = mb.listBuffer.Discard(n) discarded += m return } // Write appends data to this buffer. func (mb *Buffer) Write(p []byte) (n int, err error) { if !mb.listBuffer.IsEmpty() || mb.ringBuffer.Buffered() >= mb.maxStaticBytes { mb.listBuffer.PushBack(p) return len(p), nil } if mb.ringBuffer.Len() >= mb.maxStaticBytes { writable := mb.ringBuffer.Available() if n = len(p); n > writable { _, _ = mb.ringBuffer.Write(p[:writable]) mb.listBuffer.PushBack(p[writable:]) return } } return mb.ringBuffer.Write(p) } // Writev appends multiple byte slices to this buffer. func (mb *Buffer) Writev(bs [][]byte) (int, error) { if !mb.listBuffer.IsEmpty() || mb.ringBuffer.Buffered() >= mb.maxStaticBytes { var n int for _, b := range bs { mb.listBuffer.PushBack(b) n += len(b) } return n, nil } writable := mb.ringBuffer.Available() if mb.ringBuffer.Len() < mb.maxStaticBytes { writable = mb.maxStaticBytes - mb.ringBuffer.Buffered() } var pos, cum int for i, b := range bs { pos = i cum += len(b) if len(b) > writable { _, _ = mb.ringBuffer.Write(b[:writable]) mb.listBuffer.PushBack(b[writable:]) break } n, _ := mb.ringBuffer.Write(b) writable -= n } for pos++; pos < len(bs); pos++ { cum += len(bs[pos]) mb.listBuffer.PushBack(bs[pos]) } return cum, nil } // ReadFrom implements io.ReaderFrom. func (mb *Buffer) ReadFrom(r io.Reader) (int64, error) { if !mb.listBuffer.IsEmpty() || mb.ringBuffer.Buffered() >= mb.maxStaticBytes { return mb.listBuffer.ReadFrom(r) } return mb.ringBuffer.ReadFrom(r) } // WriteTo implements io.WriterTo. func (mb *Buffer) WriteTo(w io.Writer) (n int64, err error) { if n, err = mb.ringBuffer.WriteTo(w); err != nil { return } var m int64 m, err = mb.listBuffer.WriteTo(w) n += m return } // Buffered returns the number of bytes that can be read from the current buffer. func (mb *Buffer) Buffered() int { return mb.ringBuffer.Buffered() + mb.listBuffer.Buffered() } // IsEmpty indicates whether this buffer is empty. func (mb *Buffer) IsEmpty() bool { return mb.ringBuffer.IsEmpty() && mb.listBuffer.IsEmpty() } // Reset resets the buffer. func (mb *Buffer) Reset(maxStaticBytes int) { mb.ringBuffer.Reset() mb.listBuffer.Reset() if maxStaticBytes > 0 { mb.maxStaticBytes = maxStaticBytes } } // Release frees all resource of this buffer. func (mb *Buffer) Release() { mb.ringBuffer.Done() mb.listBuffer.Reset() } ================================================ FILE: pkg/buffer/linkedlist/linked_list_buffer.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package linkedlist implements a memory-reusable linked list of byte slices. package linkedlist import ( "io" "math" bsPool "github.com/panjf2000/gnet/v2/pkg/pool/byteslice" ) type node struct { buf []byte next *node } func (b *node) len() int { return len(b.buf) } // Buffer is a linked list of node. type Buffer struct { head *node tail *node size int bytes int } // Read reads data from the Buffer. func (llb *Buffer) Read(p []byte) (n int, err error) { if len(p) == 0 { return 0, nil } for b := llb.pop(); b != nil; b = llb.pop() { m := copy(p[n:], b.buf) n += m if m < b.len() { b.buf = b.buf[m:] llb.pushFront(b) } else { bsPool.Put(b.buf) } if n == len(p) { return } } if n == 0 { err = io.EOF } return } // AllocNode allocates a []byte with the given length that is expected to // be pushed into the Buffer. func (llb *Buffer) AllocNode(n int) []byte { return bsPool.Get(n) } // FreeNode puts the given []byte back to the pool to free the memory. func (llb *Buffer) FreeNode(p []byte) { bsPool.Put(p) } // Append is like PushBack but appends b without copying it. func (llb *Buffer) Append(p []byte) { n := len(p) if n == 0 { return } llb.pushBack(&node{buf: p}) } // Pop removes and returns the buffer of the head or nil if the list is empty. func (llb *Buffer) Pop() []byte { n := llb.pop() if n == nil { return nil } return n.buf } // PushFront is a wrapper of pushFront, which accepts []byte as its argument. func (llb *Buffer) PushFront(p []byte) { n := len(p) if n == 0 { return } b := bsPool.Get(n) copy(b, p) llb.pushFront(&node{buf: b}) } // PushBack is a wrapper of pushBack, which accepts []byte as its argument. func (llb *Buffer) PushBack(p []byte) { n := len(p) if n == 0 { return } b := bsPool.Get(n) copy(b, p) llb.pushBack(&node{buf: b}) } // Peek assembles the up to maxBytes of [][]byte based on the list of node, // it won't remove these nodes from l until Discard() is called. func (llb *Buffer) Peek(maxBytes int) ([][]byte, error) { if maxBytes <= 0 || maxBytes == math.MaxInt32 { maxBytes = math.MaxInt32 } else if maxBytes > llb.Buffered() { return nil, io.ErrShortBuffer } var bs [][]byte var cum int for iter := llb.head; iter != nil; iter = iter.next { offset := iter.len() if cum+offset > maxBytes { offset = maxBytes - cum } bs = append(bs, iter.buf[:offset]) if cum += offset; cum == maxBytes { break } } return bs, nil } // PeekWithBytes is like Peek but accepts [][]byte and puts them onto head. func (llb *Buffer) PeekWithBytes(maxBytes int, bs ...[]byte) ([][]byte, error) { if maxBytes <= 0 || maxBytes == math.MaxInt32 { maxBytes = math.MaxInt32 } else if maxBytes > llb.Buffered() { return nil, io.ErrShortBuffer } var bss [][]byte var cum int for _, b := range bs { if n := len(b); n > 0 { offset := n if cum+offset > maxBytes { offset = maxBytes - cum } bss = append(bss, b[:offset]) if cum += offset; cum == maxBytes { return bss, nil } } } for iter := llb.head; iter != nil; iter = iter.next { offset := iter.len() if cum+offset > maxBytes { offset = maxBytes - cum } bss = append(bss, iter.buf[:offset]) if cum += offset; cum == maxBytes { break } } return bss, nil } // Discard removes some nodes based on n bytes. func (llb *Buffer) Discard(n int) (discarded int, err error) { if n <= 0 { return } for n != 0 { b := llb.pop() if b == nil { break } if n < b.len() { b.buf = b.buf[n:] discarded += n llb.pushFront(b) break } n -= b.len() discarded += b.len() bsPool.Put(b.buf) } return } const minRead = 512 // ReadFrom implements io.ReaderFrom. func (llb *Buffer) ReadFrom(r io.Reader) (n int64, err error) { var m int for { b := bsPool.Get(minRead) m, err = r.Read(b) if m < 0 { panic("Buffer.ReadFrom: reader returned negative count from Read") } n += int64(m) b = b[:m] if err == io.EOF { bsPool.Put(b) return n, nil } if err != nil { bsPool.Put(b) return } llb.pushBack(&node{buf: b}) } } // WriteTo implements io.WriterTo. func (llb *Buffer) WriteTo(w io.Writer) (n int64, err error) { var m int for b := llb.pop(); b != nil; b = llb.pop() { m, err = w.Write(b.buf) if m > b.len() { panic("Buffer.WriteTo: invalid Write count") } n += int64(m) if err != nil { return } if m < b.len() { b.buf = b.buf[m:] llb.pushFront(b) return n, io.ErrShortWrite } bsPool.Put(b.buf) } return } // Len returns the length of the list. func (llb *Buffer) Len() int { return llb.size } // Buffered returns the number of bytes that can be read from the current buffer. func (llb *Buffer) Buffered() int { return llb.bytes } // IsEmpty reports whether l is empty. func (llb *Buffer) IsEmpty() bool { return llb.head == nil } // Reset removes all elements from this list. func (llb *Buffer) Reset() { for b := llb.pop(); b != nil; b = llb.pop() { bsPool.Put(b.buf) } llb.head = nil llb.tail = nil llb.size = 0 llb.bytes = 0 } // pop returns and removes the head of l. If l is empty, it returns nil. func (llb *Buffer) pop() *node { if llb.head == nil { return nil } b := llb.head llb.head = b.next if llb.head == nil { llb.tail = nil } b.next = nil llb.size-- llb.bytes -= b.len() return b } // pushFront adds the new node to the head of l. func (llb *Buffer) pushFront(b *node) { if b == nil { return } if llb.head == nil { b.next = nil llb.tail = b } else { b.next = llb.head } llb.head = b llb.size++ llb.bytes += b.len() } // pushBack adds a new node to the tail of l. func (llb *Buffer) pushBack(b *node) { if b == nil { return } if llb.tail == nil { llb.head = b } else { llb.tail.next = b } b.next = nil llb.tail = b llb.size++ llb.bytes += b.len() } ================================================ FILE: pkg/buffer/linkedlist/llbuffer_test.go ================================================ package linkedlist import ( "bytes" crand "crypto/rand" "math/rand" "testing" "github.com/stretchr/testify/require" ) func TestLinkedListBuffer_Basic(t *testing.T) { const maxBlocks = 100 var ( llb Buffer cum int buf bytes.Buffer ) for i := 0; i < maxBlocks; i++ { n := rand.Intn(1024) + 128 cum += n data := make([]byte, n) _, err := crand.Read(data) require.NoError(t, err) llb.PushBack(data) buf.Write(data) } require.EqualValues(t, maxBlocks, llb.Len()) require.EqualValues(t, cum, llb.Buffered()) bs, err := llb.Peek(cum / 4) require.NoError(t, err) var p []byte for _, b := range bs { p = append(p, b...) } pn := len(p) require.EqualValues(t, pn, cum/4) require.EqualValues(t, buf.Bytes()[:pn], p) tmpA := make([]byte, cum/16) tmpB := make([]byte, cum/16) _, err = crand.Read(tmpA) require.NoError(t, err) _, err = crand.Read(tmpB) require.NoError(t, err) bs, err = llb.PeekWithBytes(cum/4, tmpA, tmpB) require.NoError(t, err) p = p[:0] for _, b := range bs { p = append(p, b...) } pn = len(p) require.EqualValues(t, pn, cum/4) var tmpBuf bytes.Buffer tmpBuf.Write(tmpA) tmpBuf.Write(tmpB) tmpBuf.Write(buf.Bytes()[:pn-len(tmpA)-len(tmpB)]) require.EqualValues(t, tmpBuf.Bytes(), p) pn, _ = llb.Discard(pn) buf.Next(pn) p = make([]byte, cum-pn) n, err := llb.Read(p) require.NoError(t, err) require.EqualValues(t, cum-pn, n) require.EqualValues(t, buf.Bytes(), p) require.True(t, llb.IsEmpty()) } func TestLinkedListBuffer_ReadFrom(t *testing.T) { var llb Buffer const dataLen = 4 * 1024 data := make([]byte, dataLen) _, err := crand.Read(data) require.NoError(t, err) r := bytes.NewReader(data) n, err := llb.ReadFrom(r) require.NoError(t, err) require.EqualValues(t, dataLen, n) require.EqualValues(t, dataLen, llb.Buffered()) llb.Reset() const headLen = 256 head := make([]byte, headLen) _, err = crand.Read(head) require.NoError(t, err) llb.PushBack(head) _, err = crand.Read(data) require.NoError(t, err) r.Reset(data) n, err = llb.ReadFrom(r) require.NoError(t, err) require.EqualValues(t, dataLen, n) require.EqualValues(t, headLen+dataLen, llb.Buffered()) buf := make([]byte, headLen+dataLen) var m int m, err = llb.Read(buf) require.NoError(t, err) require.EqualValues(t, headLen+dataLen, m) require.EqualValues(t, append(head, data...), buf) require.True(t, llb.IsEmpty()) } func TestLinkedListBuffer_WriteTo(t *testing.T) { const maxBlocks = 20 var ( llb Buffer cum int buf bytes.Buffer ) for i := 0; i < maxBlocks; i++ { n := rand.Intn(1024) + 128 cum += n data := make([]byte, n) _, err := crand.Read(data) require.NoError(t, err) llb.PushBack(data) buf.Write(data) } require.EqualValues(t, maxBlocks, llb.Len()) require.EqualValues(t, cum, llb.Buffered()) newBuf := bytes.NewBuffer(nil) n, err := llb.WriteTo(newBuf) require.NoError(t, err) require.EqualValues(t, cum, n) require.EqualValues(t, buf.Bytes(), newBuf.Bytes()) llb.Reset() buf.Reset() newBuf.Reset() cum = 0 for i := 0; i < maxBlocks; i++ { n := rand.Intn(1024) + 128 cum += n data := make([]byte, n) _, err := crand.Read(data) require.NoError(t, err) llb.PushBack(data) buf.Write(data) } require.EqualValues(t, maxBlocks, llb.Len()) require.EqualValues(t, cum, llb.Buffered()) var discarded int discarded, err = llb.Discard(cum / 2) require.NoError(t, err) buf.Next(discarded) n, err = llb.WriteTo(newBuf) require.NoError(t, err) require.EqualValues(t, cum-discarded, n) require.EqualValues(t, buf.Bytes(), newBuf.Bytes()) llb.Reset() buf.Reset() newBuf.Reset() } ================================================ FILE: pkg/buffer/ring/ring_buffer.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // Copyright (c) 2019 Chao yuepan, Allen Xu // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in all // copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE // Package ring implements a memory-efficient circular buffer. package ring import ( "errors" "io" "github.com/panjf2000/gnet/v2/pkg/bs" "github.com/panjf2000/gnet/v2/pkg/math" bsPool "github.com/panjf2000/gnet/v2/pkg/pool/byteslice" ) const ( // MinRead is the minimum slice size passed to a Read call by // Buffer.ReadFrom. As long as the Buffer has at least MinRead bytes beyond // what is required to hold the contents of r, ReadFrom will not grow the // underlying buffer. MinRead = 512 // DefaultBufferSize is the first-time allocation on a ring-buffer. DefaultBufferSize = 1024 // 1KB bufferGrowThreshold = 4 * 1024 // 4KB ) // ErrIsEmpty will be returned when trying to read an empty ring-buffer. var ErrIsEmpty = errors.New("ring-buffer is empty") // Buffer is a circular buffer that implement io.ReaderWriter interface. type Buffer struct { buf []byte size int r int // next position to read w int // next position to write isEmpty bool } // New returns a new Buffer whose buffer has the given size. func New(size int) *Buffer { if size == 0 { return &Buffer{isEmpty: true} } size = math.CeilToPowerOfTwo(size) return &Buffer{ buf: make([]byte, size), size: size, isEmpty: true, } } // Peek returns the next n bytes without advancing the read pointer, // it returns all bytes when n <= 0. func (rb *Buffer) Peek(n int) (head []byte, tail []byte) { if rb.isEmpty { return } if n <= 0 { return rb.peekAll() } if rb.w > rb.r { m := rb.w - rb.r // length of ring-buffer if m > n { m = n } head = rb.buf[rb.r : rb.r+m] return } m := rb.size - rb.r + rb.w // length of ring-buffer if m > n { m = n } if rb.r+m <= rb.size { head = rb.buf[rb.r : rb.r+m] } else { c1 := rb.size - rb.r head = rb.buf[rb.r:] c2 := m - c1 tail = rb.buf[:c2] } return } // peekAll returns all bytes without advancing the read pointer. func (rb *Buffer) peekAll() (head []byte, tail []byte) { if rb.isEmpty { return } if rb.w > rb.r { head = rb.buf[rb.r:rb.w] return } head = rb.buf[rb.r:] if rb.w != 0 { tail = rb.buf[:rb.w] } return } // Discard skips the next n bytes by advancing the read pointer. func (rb *Buffer) Discard(n int) (discarded int, err error) { if n <= 0 { return 0, nil } discarded = rb.Buffered() if n < discarded { rb.r = (rb.r + n) % rb.size return n, nil } rb.Reset() return } // Read reads up to len(p) bytes into p. It returns the number of bytes read (0 <= n <= len(p)) and any error // encountered. // Even if Read returns n < len(p), it may use all of p as scratch space during the call. // If some data is available but not len(p) bytes, Read conventionally returns what is available instead of waiting // for more. // When Read encounters an error or end-of-file condition after successfully reading n > 0 bytes, // it returns the number of bytes read. It may return the (non-nil) error from the same call or return the // error (and n == 0) from a subsequent call. // Callers should always process the n > 0 bytes returned before considering the error err. // Doing so correctly handles I/O errors that happen after reading some bytes and also both of the allowed EOF // behaviors. func (rb *Buffer) Read(p []byte) (n int, err error) { if len(p) == 0 { return 0, nil } if rb.isEmpty { return 0, ErrIsEmpty } if rb.w > rb.r { n = rb.w - rb.r if n > len(p) { n = len(p) } copy(p, rb.buf[rb.r:rb.r+n]) rb.r += n if rb.r == rb.w { rb.Reset() } return } n = rb.size - rb.r + rb.w if n > len(p) { n = len(p) } if rb.r+n <= rb.size { copy(p, rb.buf[rb.r:rb.r+n]) } else { c1 := rb.size - rb.r copy(p, rb.buf[rb.r:]) c2 := n - c1 copy(p[c1:], rb.buf[:c2]) } rb.r = (rb.r + n) % rb.size if rb.r == rb.w { rb.Reset() } return } // ReadByte reads and returns the next byte from the input or ErrIsEmpty. func (rb *Buffer) ReadByte() (b byte, err error) { if rb.isEmpty { return 0, ErrIsEmpty } b = rb.buf[rb.r] rb.r++ if rb.r == rb.size { rb.r = 0 } if rb.r == rb.w { rb.Reset() } return } // Write writes len(p) bytes from p to the underlying buf. // It returns the number of bytes written from p (n == len(p) > 0) and any error encountered that caused the write to // stop early. // If the length of p is greater than the writable capacity of this ring-buffer, it will allocate more memory to // this ring-buffer. // Write must not modify the slice data, even temporarily. func (rb *Buffer) Write(p []byte) (n int, err error) { n = len(p) if n == 0 { return } free := rb.Available() if n > free { rb.grow(rb.size + n - free) } if rb.w >= rb.r { c1 := rb.size - rb.w if c1 >= n { copy(rb.buf[rb.w:], p) rb.w += n } else { copy(rb.buf[rb.w:], p[:c1]) c2 := n - c1 copy(rb.buf, p[c1:]) rb.w = c2 } } else { copy(rb.buf[rb.w:], p) rb.w += n } if rb.w == rb.size { rb.w = 0 } rb.isEmpty = false return } // WriteByte writes one byte into buffer. func (rb *Buffer) WriteByte(c byte) error { if rb.Available() < 1 { rb.grow(1) } rb.buf[rb.w] = c rb.w++ if rb.w == rb.size { rb.w = 0 } rb.isEmpty = false return nil } // Buffered returns the length of available bytes to read. func (rb *Buffer) Buffered() int { if rb.r == rb.w { if rb.isEmpty { return 0 } return rb.size } if rb.w > rb.r { return rb.w - rb.r } return rb.size - rb.r + rb.w } // Len returns the length of the underlying buffer. func (rb *Buffer) Len() int { return len(rb.buf) } // Cap returns the size of the underlying buffer. func (rb *Buffer) Cap() int { return rb.size } // Available returns the length of available bytes to write. func (rb *Buffer) Available() int { if rb.r == rb.w { if rb.isEmpty { return rb.size } return 0 } if rb.w < rb.r { return rb.r - rb.w } return rb.size - rb.w + rb.r } // WriteString writes the contents of the string s to buffer, which accepts a slice of bytes. func (rb *Buffer) WriteString(s string) (int, error) { return rb.Write(bs.StringToBytes(s)) } // Bytes returns all available read bytes. It does not move the read pointer and only copy the available data. func (rb *Buffer) Bytes() []byte { if rb.isEmpty { return nil } else if rb.w == rb.r { var bb []byte bb = append(bb, rb.buf[rb.r:]...) bb = append(bb, rb.buf[:rb.w]...) return bb } var bb []byte if rb.w > rb.r { bb = append(bb, rb.buf[rb.r:rb.w]...) return bb } bb = append(bb, rb.buf[rb.r:]...) if rb.w != 0 { bb = append(bb, rb.buf[:rb.w]...) } return bb } // ReadFrom implements io.ReaderFrom. func (rb *Buffer) ReadFrom(r io.Reader) (n int64, err error) { var m int for { if rb.Available() < MinRead { rb.grow(rb.Buffered() + MinRead) } if rb.w >= rb.r { m, err = r.Read(rb.buf[rb.w:]) if m < 0 { panic("RingBuffer.ReadFrom: reader returned negative count from Read") } rb.isEmpty = false rb.w = (rb.w + m) % rb.size n += int64(m) if err == io.EOF { return n, nil } if err != nil { return } m, err = r.Read(rb.buf[:rb.r]) if m < 0 { panic("RingBuffer.ReadFrom: reader returned negative count from Read") } rb.w = (rb.w + m) % rb.size n += int64(m) if err == io.EOF { return n, nil } if err != nil { return } } else { m, err = r.Read(rb.buf[rb.w:rb.r]) if m < 0 { panic("RingBuffer.ReadFrom: reader returned negative count from Read") } rb.isEmpty = false rb.w = (rb.w + m) % rb.size n += int64(m) if err == io.EOF { return n, nil } if err != nil { return } } } } // WriteTo implements io.WriterTo. func (rb *Buffer) WriteTo(w io.Writer) (int64, error) { if rb.isEmpty { return 0, ErrIsEmpty } if rb.w > rb.r { n := rb.w - rb.r m, err := w.Write(rb.buf[rb.r : rb.r+n]) if m > n { panic("RingBuffer.WriteTo: invalid Write count") } rb.r += m if rb.r == rb.w { rb.Reset() } if err != nil { return int64(m), err } if !rb.isEmpty { return int64(m), io.ErrShortWrite } return int64(m), nil } n := rb.size - rb.r + rb.w if rb.r+n <= rb.size { m, err := w.Write(rb.buf[rb.r : rb.r+n]) if m > n { panic("RingBuffer.WriteTo: invalid Write count") } rb.r = (rb.r + m) % rb.size if rb.r == rb.w { rb.Reset() } if err != nil { return int64(m), err } if !rb.isEmpty { return int64(m), io.ErrShortWrite } return int64(m), nil } var cum int64 c1 := rb.size - rb.r m, err := w.Write(rb.buf[rb.r:]) if m > c1 { panic("RingBuffer.WriteTo: invalid Write count") } rb.r = (rb.r + m) % rb.size if err != nil { return int64(m), err } if m < c1 { return int64(m), io.ErrShortWrite } cum += int64(m) c2 := n - c1 m, err = w.Write(rb.buf[:c2]) if m > c2 { panic("RingBuffer.WriteTo: invalid Write count") } rb.r = m cum += int64(m) if rb.r == rb.w { rb.Reset() } if err != nil { return cum, err } if !rb.isEmpty { return cum, io.ErrShortWrite } return cum, nil } // IsFull tells if this ring-buffer is full. func (rb *Buffer) IsFull() bool { return rb.r == rb.w && !rb.isEmpty } // IsEmpty tells if this ring-buffer is empty. func (rb *Buffer) IsEmpty() bool { return rb.isEmpty } // Reset the read pointer and write pointer to zero. func (rb *Buffer) Reset() { rb.isEmpty = true rb.r, rb.w = 0, 0 } func (rb *Buffer) grow(newCap int) { if n := rb.size; n == 0 { if newCap <= DefaultBufferSize { newCap = DefaultBufferSize } else { newCap = math.CeilToPowerOfTwo(newCap) } } else { doubleCap := n + n if newCap <= doubleCap { if n < bufferGrowThreshold { newCap = doubleCap } else { // Check 0 < n to detect overflow and prevent an infinite loop. for 0 < n && n < newCap { n += n / 4 } // The n calculation doesn't overflow, set n to newCap. if n > 0 { newCap = n } } } } newBuf := bsPool.Get(newCap) oldLen := rb.Buffered() _, _ = rb.Read(newBuf) bsPool.Put(rb.buf) rb.buf = newBuf rb.r = 0 rb.w = oldLen rb.size = newCap if rb.w > 0 { rb.isEmpty = false } } ================================================ FILE: pkg/buffer/ring/ring_buffer_test.go ================================================ package ring import ( "bytes" crand "crypto/rand" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestRingBuffer_Write(t *testing.T) { rb := New(64) _, err := rb.ReadByte() assert.ErrorIs(t, err, ErrIsEmpty, "expect nil err, but got nil") // check empty or full assert.True(t, rb.IsEmpty(), "expect IsEmpty is true but got false") assert.False(t, rb.IsFull(), "expect IsFull is false but got true") assert.EqualValuesf(t, 0, rb.Buffered(), "expect len 0 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 64, rb.Available(), "expect free 64 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) // write 4 * 4 = 16 bytes data := []byte(strings.Repeat("abcd", 4)) n, _ := rb.Write(data) assert.EqualValuesf(t, 16, n, "expect write 16 bytes but got %d", n) assert.EqualValuesf(t, 16, rb.Buffered(), "expect len 16 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 48, rb.Available(), "expect free 48 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.EqualValuesf(t, data, rb.Bytes(), "expect 4 abcd but got %s. r.w=%d, r.r=%d", rb.Bytes(), rb.w, rb.r) // check empty or full assert.False(t, rb.IsEmpty(), "expect IsEmpty is false but got true") assert.False(t, rb.IsFull(), "expect IsFull is false but got true") // write 48 bytes, should full data = []byte(strings.Repeat("abcd", 12)) n, _ = rb.Write(data) assert.EqualValuesf(t, 48, n, "expect write 48 bytes but got %d", n) assert.EqualValuesf(t, 64, rb.Buffered(), "expect len 64 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 0, rb.Available(), "expect free 0 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.EqualValuesf(t, 0, rb.w, "expect r.w=0 but got %d. r.r=%d", rb.w, rb.r) assert.EqualValuesf(t, []byte(strings.Repeat("abcd", 16)), rb.Bytes(), "expect 16 abcd but got %s. r.w=%d, r.r=%d", rb.Bytes(), rb.w, rb.r) // check empty or full assert.False(t, rb.IsEmpty(), "expect IsEmpty is false but got true") assert.True(t, rb.IsFull(), "expect IsFull is true but got false") assert.True(t, rb.IsFull(), "expect IsFull is true but got false") // write more 4 bytes, should scale from 64 to 128 bytes. n, _ = rb.Write([]byte(strings.Repeat("abcd", 1))) assert.EqualValuesf(t, 4, n, "expect write 4 bytes but got %d", n) size := rb.Cap() assert.EqualValuesf(t, 68, rb.Buffered(), "expect len 68 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, size-68, rb.Available(), "expect free 60 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) // check empty or full assert.False(t, rb.IsEmpty(), "expect IsEmpty is false but got true") assert.False(t, rb.IsFull(), "expect IsFull is false but got true") // reset this ringbuffer and set a long slice rb.Reset() n, _ = rb.Write([]byte(strings.Repeat("abcd", 20))) assert.EqualValuesf(t, 80, n, "expect write 80 bytes but got %d", n) assert.EqualValuesf(t, 80, rb.Buffered(), "expect len 80 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, size-80, rb.Available(), "expect free 48 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.Greaterf(t, rb.w, 0, "expect r.w>=0 but got %d. r.r=%d", rb.w, rb.r) // check empty or full assert.False(t, rb.IsEmpty(), "expect IsEmpty is false but got true") assert.False(t, rb.IsFull(), "expect IsFull is false but got true") assert.EqualValuesf(t, []byte(strings.Repeat("abcd", 20)), rb.Bytes(), "expect 16 abcd but got %s. r.w=%d, r.r=%d", rb.Bytes(), rb.w, rb.r) rb.Reset() size = rb.Cap() // write 4 * 2 = 8 bytes n, _ = rb.Write([]byte(strings.Repeat("abcd", 2))) assert.EqualValuesf(t, 8, n, "expect write 16 bytes but got %d", n) assert.EqualValuesf(t, 8, rb.Buffered(), "expect len 16 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, size-8, rb.Available(), "expect free 48 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) buf := make([]byte, 5) _, _ = rb.Read(buf) assert.EqualValuesf(t, 3, rb.Buffered(), "expect len 3 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) _, _ = rb.Write([]byte(strings.Repeat("abcd", 15))) assert.EqualValuesf(t, []byte("bcd"+strings.Repeat("abcd", 15)), rb.Bytes(), "expect 63 ... but got %s. r.w=%d, r.r=%d", rb.Bytes(), rb.w, rb.r) rb.Reset() n, _ = rb.Write([]byte(strings.Repeat("abcd", 32))) assert.EqualValuesf(t, 128, n, "expect write 128 bytes but got %d", n) assert.EqualValuesf(t, 0, rb.Available(), "expect free 0 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) buf = make([]byte, 16) _, _ = rb.Read(buf) n, _ = rb.Write([]byte(strings.Repeat("1234", 4))) assert.EqualValuesf(t, 16, n, "expect write 16 bytes but got %d", n) assert.EqualValuesf(t, 0, rb.Available(), "expect free 0 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.EqualValuesf(t, []byte(strings.Repeat("abcd", 32)+strings.Repeat("1234", 4)), append(buf, rb.Bytes()...), "expect 16 abcd and 4 1234 but got %s. r.w=%d, r.r=%d", rb.Bytes(), rb.w, rb.r) } func TestZeroRingBuffer(t *testing.T) { rb := New(0) head, tail := rb.Peek(2) assert.Empty(t, head, "head should be empty") assert.Empty(t, tail, "tail should be empty") head, tail = rb.Peek(-1) assert.Empty(t, head, "head should be empty") assert.Empty(t, tail, "tail should be empty") assert.EqualValues(t, 0, rb.Buffered(), "expect length is 0") assert.EqualValues(t, 0, rb.Available(), "expect free is 0") buf := []byte(strings.Repeat("1234", 12)) _, _ = rb.Write(buf) assert.EqualValuesf(t, DefaultBufferSize, rb.Len(), "expect rb.Len()=%d, but got rb.Len()=%d", DefaultBufferSize, rb.Len()) assert.EqualValuesf(t, DefaultBufferSize, rb.Cap(), "expect rb.Cap()=%d, but got rb.Cap()=%d", DefaultBufferSize, rb.Cap()) assert.Truef(t, rb.r == 0 && rb.w == 48 && rb.size == DefaultBufferSize, "expect rb.r=0, rb.w=48, rb.size=64, rb.mask=63, but got rb.r=%d, rb.w=%d, rb.size=%d", rb.r, rb.w, rb.size) assert.EqualValues(t, buf, rb.Bytes(), "expect it is equal") _, _ = rb.Discard(48) assert.Truef(t, rb.IsEmpty() && rb.r == 0 && rb.w == 0, "expect rb is empty and rb.r=rb.w=0, but got rb.r=%d and rb.w=%d", rb.r, rb.w) } func TestRingBufferGrow(t *testing.T) { rb := New(0) head, tail := rb.Peek(2) assert.Empty(t, head, "head should be empty") assert.Empty(t, tail, "tail should be empty") data := make([]byte, DefaultBufferSize+1) n, err := crand.Read(data) assert.NoError(t, err, "failed to generate random data") assert.EqualValuesf(t, DefaultBufferSize+1, n, "expect random data length is %d but got %d", DefaultBufferSize+1, n) n, err = rb.Write(data) assert.NoError(t, err) assert.EqualValues(t, DefaultBufferSize+1, n) assert.EqualValues(t, 2*DefaultBufferSize, rb.Cap()) assert.EqualValues(t, 2*DefaultBufferSize, rb.Len()) assert.EqualValues(t, DefaultBufferSize+1, rb.Buffered()) assert.EqualValues(t, DefaultBufferSize-1, rb.Available()) assert.EqualValues(t, data, rb.Bytes()) rb = New(DefaultBufferSize) newData := make([]byte, 3*512) n, err = crand.Read(newData) assert.NoError(t, err, "failed to generate random data") assert.EqualValuesf(t, 3*512, n, "expect random data length is %d but got %d", 3*512, n) n, err = rb.Write(newData) assert.NoError(t, err) assert.EqualValues(t, 3*512, n) assert.EqualValues(t, 2*DefaultBufferSize, rb.Cap()) assert.EqualValues(t, 2*DefaultBufferSize, rb.Len()) assert.EqualValues(t, 3*512, rb.Buffered()) assert.EqualValues(t, 512, rb.Available()) assert.EqualValues(t, newData, rb.Bytes()) rb.Reset() data = make([]byte, bufferGrowThreshold) n, err = crand.Read(data) assert.NoError(t, err, "failed to generate random data") assert.EqualValuesf(t, bufferGrowThreshold, n, "expect random data length is %d but got %d", bufferGrowThreshold, n) n, err = rb.Write(data) assert.NoError(t, err) assert.EqualValues(t, bufferGrowThreshold, n) assert.EqualValues(t, bufferGrowThreshold, rb.Cap()) assert.EqualValues(t, bufferGrowThreshold, rb.Len()) assert.EqualValues(t, bufferGrowThreshold, rb.Buffered()) assert.EqualValues(t, 0, rb.Available()) assert.EqualValues(t, data, rb.Bytes()) newData = make([]byte, bufferGrowThreshold/2+1) n, err = crand.Read(newData) assert.NoError(t, err, "failed to generate random data") assert.EqualValuesf(t, bufferGrowThreshold/2+1, n, "expect random data length is %d but got %d", bufferGrowThreshold, n) n, err = rb.Write(newData) assert.NoError(t, err) assert.EqualValues(t, bufferGrowThreshold/2+1, n) assert.EqualValues(t, 1.25*(1.25*bufferGrowThreshold), rb.Cap()) assert.EqualValues(t, 1.25*(1.25*bufferGrowThreshold), rb.Len()) assert.EqualValues(t, 1.5*bufferGrowThreshold+1, rb.Buffered()) assert.EqualValues(t, 1.25*(1.25*bufferGrowThreshold)-rb.Buffered(), rb.Available()) assert.EqualValues(t, append(data, newData...), rb.Bytes()) } func TestRingBuffer_Read(t *testing.T) { rb := New(64) // check empty or full assert.True(t, rb.IsEmpty(), "expect IsEmpty is true but got false") assert.False(t, rb.IsFull(), "expect isfull is false but got true") assert.EqualValuesf(t, 0, rb.Buffered(), "expect len 0 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 64, rb.Available(), "expect free 64 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) // read empty buf := make([]byte, 1024) n, err := rb.Read(buf) assert.ErrorIs(t, err, ErrIsEmpty, "expect ErrIsEmpty but got nil") assert.EqualValuesf(t, 0, n, "expect read 0 bytes but got %d", n) assert.EqualValuesf(t, 0, rb.Buffered(), "expect len 0 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 64, rb.Available(), "expect free 64 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.EqualValuesf(t, 0, rb.r, "expect r.r=0 but got %d. r.w=%d", rb.r, rb.w) // write 16 bytes to read _, _ = rb.Write([]byte(strings.Repeat("abcd", 4))) // read all data from buffer, it will be shrunk from 64 to 32. n, err = rb.Read(buf) assert.NoErrorf(t, err, "read failed: %v", err) assert.EqualValuesf(t, 16, n, "expect read 16 bytes but got %d", n) assert.EqualValuesf(t, 0, rb.Buffered(), "expect len 0 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 64, rb.Available(), "expect free 64 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.EqualValuesf(t, 0, rb.r, "expect r.r=0 but got %d. r.w=%d", rb.r, rb.w) // write long slice to read, it will scale from 32 to 128 bytes. _, _ = rb.Write([]byte(strings.Repeat("abcd", 20))) // read all data from buffer, it will be shrunk from 128 to 64. n, err = rb.Read(buf) assert.NoErrorf(t, err, "read failed: %v", err) assert.EqualValuesf(t, 80, n, "expect read 80 bytes but got %d", n) assert.EqualValuesf(t, 0, rb.Buffered(), "expect len 0 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 128, rb.Available(), "expect free 128 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.EqualValuesf(t, 0, rb.r, "expect r.r=0 but got %d. r.w=%d", rb.r, rb.w) rb.Reset() _, _ = rb.Write([]byte(strings.Repeat("1234", 32))) assert.True(t, rb.IsFull(), "ring buffer should be full") assert.EqualValuesf(t, 0, rb.Available(), "expect free 0 bytes but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.EqualValuesf(t, 0, rb.w, "expect r.2=0 but got %d. r.r=%d", rb.w, rb.r) head, tail := rb.Peek(64) assert.Truef(t, len(head) == 64 && tail == nil, "expect len(head)=64 and tail=nil, yet len(head)=%d and tail != nil", len(head)) assert.EqualValuesf(t, 0, rb.r, "expect r.r=0 but got %d", rb.r) assert.EqualValues(t, []byte(strings.Repeat("1234", 16)), head) _, _ = rb.Discard(64) assert.EqualValuesf(t, 64, rb.r, "expect r.r=64 but got %d", rb.r) _, _ = rb.Write([]byte(strings.Repeat("1234", 4))) assert.EqualValuesf(t, 16, rb.w, "expect r.w=16 but got %d", rb.w) head, tail = rb.Peek(128) assert.Truef(t, len(head) == 64 && len(tail) == 16, "expect len(head)=64 and len(tail)=16, yet len(head)=%d and len(tail)=%d", len(head), len(tail)) assert.EqualValues(t, []byte(strings.Repeat("1234", 16)), head) assert.EqualValues(t, []byte(strings.Repeat("1234", 4)), tail) head, tail = rb.Peek(-1) assert.Truef(t, len(head) == 64 && len(tail) == 16, "expect len(head)=64 and len(tail)=16, yet len(head)=%d and len(tail)=%d", len(head), len(tail)) assert.EqualValues(t, []byte(strings.Repeat("1234", 16)), head) assert.EqualValues(t, []byte(strings.Repeat("1234", 4)), tail) _, _ = rb.Discard(64) _, _ = rb.Discard(16) assert.True(t, rb.IsEmpty(), "should be empty") } func TestRingBuffer_ByteInterface(t *testing.T) { rb := New(2) // write one _ = rb.WriteByte('a') assert.EqualValuesf(t, 1, rb.Buffered(), "expect len 1 byte but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 1, rb.Available(), "expect free 1 byte but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.EqualValuesf(t, []byte{'a'}, rb.Bytes(), "expect a but got %s. r.w=%d, r.r=%d", rb.Bytes(), rb.w, rb.r) // check empty or full assert.Falsef(t, rb.IsEmpty(), "expect IsEmpty is false but got true") assert.False(t, rb.IsFull(), "expect IsFull is false but got true") // write two, isFull _ = rb.WriteByte('b') assert.EqualValuesf(t, 2, rb.Buffered(), "expect len 2 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 0, rb.Available(), "expect free 0 byte but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.EqualValuesf(t, []byte{'a', 'b'}, rb.Bytes(), "expect a but got %s. r.w=%d, r.r=%d", rb.Bytes(), rb.w, rb.r) // check empty or full assert.False(t, rb.IsEmpty(), "expect IsEmpty is false but got true") assert.True(t, rb.IsFull(), "expect IsFull is true but got false") // write, it will scale from 2 to 4 bytes. _ = rb.WriteByte('c') assert.EqualValuesf(t, 3, rb.Buffered(), "expect len 3 bytes but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 1, rb.Available(), "expect free 1 byte but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.EqualValuesf(t, []byte{'a', 'b', 'c'}, rb.Bytes(), "expect a but got %s. r.w=%d, r.r=%d", rb.Bytes(), rb.w, rb.r) // check empty or full assert.False(t, rb.IsEmpty(), "expect IsEmpty is false but got true") assert.False(t, rb.IsFull(), "expect IsFull is false but got true") // read one b, err := rb.ReadByte() assert.NoErrorf(t, err, "ReadByte failed: %v", err) assert.EqualValuesf(t, 'a', b, "expect a but got %c. r.w=%d, r.r=%d", b, rb.w, rb.r) assert.EqualValuesf(t, 2, rb.Buffered(), "expect len 2 byte but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 2, rb.Available(), "expect free 2 byte but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) assert.EqualValuesf(t, []byte{'b', 'c'}, rb.Bytes(), "expect a but got %s. r.w=%d, r.r=%d", rb.Bytes(), rb.w, rb.r) // check empty or full assert.False(t, rb.IsEmpty(), "expect IsEmpty is false but got true") assert.False(t, rb.IsFull(), "expect IsFull is false but got true") // read two b, _ = rb.ReadByte() assert.EqualValuesf(t, 'b', b, "expect b but got %c. r.w=%d, r.r=%d", b, rb.w, rb.r) assert.EqualValuesf(t, 1, rb.Buffered(), "expect len 1 byte but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 3, rb.Available(), "expect free 3 byte but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) // check empty or full assert.False(t, rb.IsEmpty(), "expect IsEmpty is false but got true") assert.False(t, rb.IsFull(), "expect IsFull is false but got true") // read three _, _ = rb.ReadByte() assert.EqualValuesf(t, 0, rb.Buffered(), "expect len 0 byte but got %d. r.w=%d, r.r=%d", rb.Buffered(), rb.w, rb.r) assert.EqualValuesf(t, 4, rb.Available(), "expect free 4 byte but got %d. r.w=%d, r.r=%d", rb.Available(), rb.w, rb.r) // check empty or full assert.True(t, rb.IsEmpty(), "expect IsEmpty is true but got false") assert.False(t, rb.IsFull(), "expect IsFull is false but got true") } func TestRingBuffer_ReadFrom(t *testing.T) { rb := New(0) const dataLen = 4 * 1024 data := make([]byte, dataLen) _, err := crand.Read(data) require.NoError(t, err) r := bytes.NewReader(data) n, err := rb.ReadFrom(r) require.NoError(t, err) require.False(t, rb.IsEmpty()) require.EqualValuesf(t, dataLen, n, "ringbuffer should read %d bytes, but got %d", dataLen, n) require.EqualValuesf(t, dataLen, rb.Buffered(), "ringbuffer should have %d bytes, but got %d", dataLen, rb.Buffered()) buf, _ := rb.Peek(-1) require.EqualValues(t, data, buf) buf = make([]byte, dataLen) var m int m, err = rb.Read(buf) require.NoError(t, err) require.EqualValuesf(t, dataLen, m, "ringbuffer should read %d bytes, but got %d", dataLen, m) require.EqualValues(t, data, buf) require.Truef(t, rb.IsEmpty(), "ringbuffer should be empty, but it isn't") require.Zerof(t, rb.Buffered(), "ringbuffer should be empty, but still have %d bytes", rb.Buffered()) rb = New(0) const prefixLen = 2 * 1024 prefix := make([]byte, prefixLen) _, err = crand.Read(prefix) require.NoError(t, err) _, err = crand.Read(data) require.NoError(t, err) r.Reset(data) m, err = rb.Write(prefix) require.NoError(t, err) require.EqualValuesf(t, prefixLen, m, "ringbuffer should read %d bytes, but got %d", prefixLen, m) n, err = rb.ReadFrom(r) require.NoError(t, err) require.EqualValuesf(t, dataLen, n, "ringbuffer should read %d bytes, but got %d", dataLen, n) require.EqualValuesf(t, prefixLen+dataLen, rb.Buffered(), "ringbuffer should have %d bytes, but got %d", prefixLen+dataLen, rb.Buffered()) head, tail := rb.Peek(prefixLen) require.Nil(t, tail) require.EqualValues(t, prefix, head) _, _ = rb.Discard(prefixLen) require.EqualValuesf(t, dataLen, rb.Buffered(), "ringbuffer should have %d bytes, but got %d", dataLen, rb.Buffered()) head, tail = rb.Peek(-1) require.Nil(t, tail) require.EqualValues(t, data, head) _, _ = rb.Discard(dataLen) require.Truef(t, rb.IsEmpty(), "ringbuffer should be empty, but it isn't") require.Zerof(t, rb.Buffered(), "ringbuffer should be empty, but still have %d bytes", rb.Buffered()) const initLen = 5 * 1024 rb = New(initLen) _, err = crand.Read(prefix) require.NoError(t, err) _, err = crand.Read(data) require.NoError(t, err) r.Reset(data) m, err = rb.Write(prefix) require.NoError(t, err) require.EqualValuesf(t, prefixLen, m, "ringbuffer should read %d bytes, but got %d", prefixLen, m) const partLen = 1024 head, tail = rb.Peek(partLen) require.Nil(t, tail) require.EqualValues(t, prefix[:partLen], head) _, _ = rb.Discard(partLen) n, err = rb.ReadFrom(r) require.NoError(t, err) require.EqualValuesf(t, dataLen, n, "ringbuffer should read %d bytes, but got %d", dataLen, n) buf, tail = rb.Peek(-1) buf = append(buf, tail...) require.EqualValues(t, append(prefix[partLen:], data...), buf) _, _ = rb.Discard(prefixLen + dataLen - partLen) require.Truef(t, rb.IsEmpty(), "ringbuffer should be empty, but it isn't") require.Zerof(t, rb.Buffered(), "ringbuffer should be empty, but still have %d bytes", rb.Buffered()) } func TestRingBuffer_WriteTo(t *testing.T) { rb := New(5 * 1024) const dataLen = 4 * 1024 data := make([]byte, dataLen) _, err := crand.Read(data) require.NoError(t, err) n, err := rb.Write(data) require.NoError(t, err) require.EqualValuesf(t, dataLen, n, "ringbuffer should write %d bytes, but got %d", dataLen, n) buf := bytes.NewBuffer(nil) var m int64 m, err = rb.WriteTo(buf) require.NoError(t, err) require.True(t, rb.IsEmpty()) require.EqualValuesf(t, dataLen, m, "ringbuffer should write %d bytes, but got %d", dataLen, m) require.EqualValues(t, data, buf.Bytes()) buf.Reset() _, err = crand.Read(data) require.NoError(t, err) rb = New(dataLen) n, err = rb.Write(data) require.NoError(t, err) require.EqualValuesf(t, dataLen, n, "ringbuffer should write %d bytes, but got %d", dataLen, n) require.Truef(t, rb.IsFull(), "ringbuffer should be full, but it isn't") m, err = rb.WriteTo(buf) require.NoError(t, err) require.True(t, rb.IsEmpty()) require.EqualValuesf(t, dataLen, m, "ringbuffer should write %d bytes, but got %d", dataLen, m) require.EqualValues(t, data, buf.Bytes()) buf.Reset() rb.Reset() _, err = crand.Read(data) require.NoError(t, err) n, err = rb.Write(data) require.NoError(t, err) require.EqualValuesf(t, dataLen, n, "ringbuffer should write %d bytes, but got %d", dataLen, n) require.Truef(t, rb.IsFull(), "ringbuffer should be full, but it isn't") const partLen = 1024 head, tail := rb.Peek(partLen) require.Nil(t, tail) require.EqualValues(t, data[:partLen], head) _, _ = rb.Discard(partLen) partData := make([]byte, partLen/2) _, err = crand.Read(partData) require.NoError(t, err) n, err = rb.Write(partData) require.NoError(t, err) require.EqualValuesf(t, partLen/2, n, "ringbuffer should write %d bytes, but got %d", dataLen, n) require.EqualValues(t, partLen/2, rb.Available()) m, err = rb.WriteTo(buf) require.NoError(t, err) require.EqualValuesf(t, dataLen-partLen/2, m, "ringbuffer should write %d bytes, but got %d", dataLen-partLen/2, m) require.EqualValues(t, append(data[partLen:], partData...), buf.Bytes()) require.True(t, rb.IsEmpty()) } ================================================ FILE: pkg/errors/errors.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package errors defines common errors for gnet. package errors import "errors" var ( // ErrEmptyEngine occurs when trying to do something with an empty engine. ErrEmptyEngine = errors.New("gnet: the internal engine is empty") // ErrEngineShutdown occurs when server is closing. ErrEngineShutdown = errors.New("gnet: server is going to be shutdown") // ErrEngineInShutdown occurs when attempting to shut the server down more than once. ErrEngineInShutdown = errors.New("gnet: server is already in shutdown") // ErrAcceptSocket occurs when acceptor does not accept the new connection properly. ErrAcceptSocket = errors.New("gnet: accept a new connection error") // ErrTooManyEventLoopThreads occurs when attempting to set up more than 10,000 event-loop goroutines under LockOSThread mode. ErrTooManyEventLoopThreads = errors.New("gnet: too many event-loops under LockOSThread mode") // ErrUnsupportedProtocol occurs when trying to use protocol that is not supported. ErrUnsupportedProtocol = errors.New("gnet: only unix, tcp/tcp4/tcp6, udp/udp4/udp6 are supported") // ErrUnsupportedTCPProtocol occurs when trying to use an unsupported TCP protocol. ErrUnsupportedTCPProtocol = errors.New("gnet: only tcp/tcp4/tcp6 are supported") // ErrUnsupportedUDPProtocol occurs when trying to use an unsupported UDP protocol. ErrUnsupportedUDPProtocol = errors.New("gnet: only udp/udp4/udp6 are supported") // ErrUnsupportedUDSProtocol occurs when trying to use an unsupported Unix protocol. ErrUnsupportedUDSProtocol = errors.New("gnet: only unix is supported") // ErrUnsupportedOp occurs when calling some methods that are either not supported or have not been implemented yet. ErrUnsupportedOp = errors.New("gnet: unsupported operation") // ErrNegativeSize occurs when trying to pass a negative size to a buffer. ErrNegativeSize = errors.New("gnet: negative size is not allowed") // ErrNoIPv4AddressOnInterface occurs when an IPv4 multicast address is set on an interface but IPv4 is not configured. ErrNoIPv4AddressOnInterface = errors.New("gnet: no IPv4 address on interface") // ErrInvalidNetworkAddress occurs when the network address is invalid. ErrInvalidNetworkAddress = errors.New("gnet: invalid network address") // ErrInvalidNetConn occurs when trying to do something with an empty net.Conn. ErrInvalidNetConn = errors.New("gnet: the net.Conn is empty") // ErrNilRunnable occurs when trying to execute a nil runnable. ErrNilRunnable = errors.New("gnet: nil runnable is not allowed") ) ================================================ FILE: pkg/io/io.go ================================================ /* * Copyright (c) 2025 The Gnet Authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ // Package io provides some handy network I/O functions. package io ================================================ FILE: pkg/io/io_bsd.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || netbsd || openbsd package io import ( "unsafe" "golang.org/x/sys/unix" ) // Writev invokes the writev system call directly. // // Note that SYS_WRITEV is about to be deprecated on Darwin // and the Go team suggested to use libSystem wrappers instead of direct system-calls, // hence, this way to implement the writev might not be backward-compatible in the future. func Writev(fd int, bs [][]byte) (int, error) { if len(bs) == 0 { return 0, nil } iov := bytes2iovec(bs) n, _, err := unix.RawSyscall(unix.SYS_WRITEV, uintptr(fd), uintptr(unsafe.Pointer(&iov[0])), uintptr(len(iov))) //nolint:staticcheck if err != 0 { return int(n), err } return int(n), nil } // Readv invokes the readv system call directly. // // Note that SYS_READV is about to be deprecated on Darwin // and the Go team suggested to use libSystem wrappers instead of direct system-calls, // hence, this way to implement the readv might not be backward-compatible in the future. func Readv(fd int, bs [][]byte) (int, error) { if len(bs) == 0 { return 0, nil } iov := bytes2iovec(bs) // syscall n, _, err := unix.RawSyscall(unix.SYS_READV, uintptr(fd), uintptr(unsafe.Pointer(&iov[0])), uintptr(len(iov))) //nolint:staticcheck if err != 0 { return int(n), err } return int(n), nil } var _zero uintptr func bytes2iovec(bs [][]byte) []unix.Iovec { iovecs := make([]unix.Iovec, len(bs)) for i, b := range bs { iovecs[i].SetLen(len(b)) if len(b) > 0 { iovecs[i].Base = &b[0] } else { iovecs[i].Base = (*byte)(unsafe.Pointer(&_zero)) } } return iovecs } ================================================ FILE: pkg/io/io_linux.go ================================================ /* * Copyright (c) 2021 The Gnet Authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package io import "golang.org/x/sys/unix" // Writev calls writev() on Linux. func Writev(fd int, iov [][]byte) (int, error) { if len(iov) == 0 { return 0, nil } return unix.Writev(fd, iov) } // Readv calls readv() on Linux. func Readv(fd int, iov [][]byte) (int, error) { if len(iov) == 0 { return 0, nil } return unix.Readv(fd, iov) } ================================================ FILE: pkg/logging/logger.go ================================================ // Copyright (c) 2020 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package logging provides logging functionality for gnet applications, // it sets up a default logger (powered by go.uber.org/zap) // that is about to be used by your gnet application. // You're allowed to replace the default logger with your customized logger by // implementing Logger and assign it to the functional option via gnet.WithLogger, // and then passing it to gnet.Run or gnet.Rotate. // // The environment variable `GNET_LOGGING_LEVEL` determines which zap logger level will be applied for logging. // The environment variable `GNET_LOGGING_FILE` is set to a local file path when you want to print logs into local file. // Alternatives of logging level (the variable of logging level ought to be integer): /* const ( // DebugLevel logs are typically voluminous, and are usually disabled in // production. DebugLevel Level = iota - 1 // InfoLevel is the default logging priority. InfoLevel // WarnLevel logs are more important than Info, but don't need individual // human review. WarnLevel // ErrorLevel logs are high-priority. If an application is running smoothly, // it shouldn't generate any error-level logs. ErrorLevel // DPanicLevel logs are particularly important errors. In development the // logger panics after writing the message. DPanicLevel // PanicLevel logs a message, then panics. PanicLevel // FatalLevel logs a message, then calls os.Exit(1). FatalLevel ) */ package logging import ( "errors" "os" "strconv" "strings" "sync" "go.uber.org/zap" "go.uber.org/zap/buffer" "go.uber.org/zap/zapcore" "gopkg.in/natefinch/lumberjack.v2" ) // Flusher is the callback function which flushes any buffered log entries to the underlying writer. // It is usually called before the gnet process exits. type Flusher = func() error var ( mu sync.RWMutex defaultLogger Logger defaultLoggingLevel Level defaultFlusher Flusher ) // Level is the alias of zapcore.Level. type Level = zapcore.Level const ( // DebugLevel logs are typically voluminous, and are usually disabled in // production. DebugLevel = zapcore.DebugLevel // InfoLevel is the default logging priority. InfoLevel = zapcore.InfoLevel // WarnLevel logs are more important than Info, but don't need individual // human review. WarnLevel = zapcore.WarnLevel // ErrorLevel logs are high-priority. If an application is running smoothly, // it shouldn't generate any error-level logs. ErrorLevel = zapcore.ErrorLevel // DPanicLevel logs are particularly important errors. In development the // logger panics after writing the message. DPanicLevel = zapcore.DPanicLevel // PanicLevel logs a message, then panics. PanicLevel = zapcore.PanicLevel // FatalLevel logs a message, then calls os.Exit(1). FatalLevel = zapcore.FatalLevel ) func init() { lvl := os.Getenv("GNET_LOGGING_LEVEL") if len(lvl) > 0 { loggingLevel, err := strconv.ParseInt(lvl, 10, 8) if err != nil { panic("invalid GNET_LOGGING_LEVEL, " + err.Error()) } defaultLoggingLevel = Level(loggingLevel) } // Initializes the inside default logger of gnet. fileName := os.Getenv("GNET_LOGGING_FILE") if len(fileName) > 0 { var err error defaultLogger, defaultFlusher, err = CreateLoggerAsLocalFile(fileName, defaultLoggingLevel) if err != nil { panic("invalid GNET_LOGGING_FILE, " + err.Error()) } } else { core := zapcore.NewCore(getDevEncoder(), zapcore.Lock(os.Stdout), defaultLoggingLevel) zapLogger := zap.New(core, zap.Development(), zap.AddCaller(), zap.AddStacktrace(ErrorLevel), zap.ErrorOutput(zapcore.Lock(os.Stderr))) defaultLogger = zapLogger.Sugar() } } type prefixEncoder struct { zapcore.Encoder prefix string bufPool buffer.Pool } func (e *prefixEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) { buf := e.bufPool.Get() buf.AppendString(e.prefix) buf.AppendString(" ") logEntry, err := e.Encoder.EncodeEntry(entry, fields) if err != nil { return nil, err } _, err = buf.Write(logEntry.Bytes()) if err != nil { return nil, err } return buf, nil } func getDevEncoder() zapcore.Encoder { encoderConfig := zap.NewDevelopmentEncoderConfig() encoderConfig.EncodeTime = zapcore.RFC3339NanoTimeEncoder encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder return &prefixEncoder{ Encoder: zapcore.NewConsoleEncoder(encoderConfig), prefix: "[gnet]", bufPool: buffer.NewPool(), } } func getProdEncoder() zapcore.Encoder { encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.RFC3339NanoTimeEncoder encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder return &prefixEncoder{ Encoder: zapcore.NewConsoleEncoder(encoderConfig), prefix: "[gnet]", bufPool: buffer.NewPool(), } } // GetDefaultLogger returns the default logger. func GetDefaultLogger() Logger { mu.RLock() defer mu.RUnlock() return defaultLogger } // GetDefaultFlusher returns the default flusher. func GetDefaultFlusher() Flusher { mu.RLock() defer mu.RUnlock() return defaultFlusher } // SetDefaultLoggerAndFlusher sets the default logger and its flusher. func SetDefaultLoggerAndFlusher(logger Logger, flusher Flusher) { mu.Lock() defaultLogger, defaultFlusher = logger, flusher mu.Unlock() } // LogLevel tells what the default logging level is. func LogLevel() string { return strings.ToUpper(defaultLoggingLevel.String()) } // CreateLoggerAsLocalFile setups the logger by local file path. func CreateLoggerAsLocalFile(localFilePath string, logLevel Level) (logger Logger, flush func() error, err error) { if len(localFilePath) == 0 { return nil, nil, errors.New("invalid local logger path") } // lumberjack.Logger is already safe for concurrent use, so we don't need to lock it. lumberJackLogger := &lumberjack.Logger{ Filename: localFilePath, MaxSize: 100, // megabytes MaxBackups: 2, MaxAge: 15, // days } encoder := getProdEncoder() ws := zapcore.AddSync(lumberJackLogger) zapcore.Lock(ws) levelEnabler := zap.LevelEnablerFunc(func(level Level) bool { return level >= logLevel }) core := zapcore.NewCore(encoder, ws, levelEnabler) zapLogger := zap.New(core, zap.AddCaller(), zap.AddStacktrace(ErrorLevel)) logger = zapLogger.Sugar() flush = zapLogger.Sync return } // Cleanup does something windup for logger, like closing, flushing, etc. func Cleanup() { mu.RLock() if defaultFlusher != nil { _ = defaultFlusher() } mu.RUnlock() } // Error prints err if it's not nil. func Error(err error) { if err != nil { mu.RLock() defaultLogger.Errorf("error occurs during runtime, %v", err) mu.RUnlock() } } // Debugf logs messages at DEBUG level. func Debugf(format string, args ...any) { mu.RLock() defaultLogger.Debugf(format, args...) mu.RUnlock() } // Infof logs messages at INFO level. func Infof(format string, args ...any) { mu.RLock() defaultLogger.Infof(format, args...) mu.RUnlock() } // Warnf logs messages at WARN level. func Warnf(format string, args ...any) { mu.RLock() defaultLogger.Warnf(format, args...) mu.RUnlock() } // Errorf logs messages at ERROR level. func Errorf(format string, args ...any) { mu.RLock() defaultLogger.Errorf(format, args...) mu.RUnlock() } // Fatalf logs messages at FATAL level. func Fatalf(format string, args ...any) { mu.RLock() defaultLogger.Fatalf(format, args...) mu.RUnlock() } // Logger is used for logging formatted messages. type Logger interface { // Debugf logs messages at DEBUG level. Debugf(format string, args ...any) // Infof logs messages at INFO level. Infof(format string, args ...any) // Warnf logs messages at WARN level. Warnf(format string, args ...any) // Errorf logs messages at ERROR level. Errorf(format string, args ...any) // Fatalf logs messages at FATAL level. Fatalf(format string, args ...any) } ================================================ FILE: pkg/math/math.go ================================================ // Copyright (c) 2022 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package math provides a few fast math functions. package math import "math/bits" const ( bitSize = 32 << (^uint(0) >> 63) maxintHeadBit = 1 << (bitSize - 2) ) // IsPowerOfTwo reports whether the given n is a power of two. func IsPowerOfTwo(n int) bool { return n > 0 && n&(n-1) == 0 } // CeilToPowerOfTwo returns n if it is a power-of-two, otherwise the next-highest power-of-two. func CeilToPowerOfTwo(n int) int { if n&maxintHeadBit != 0 && n > maxintHeadBit { panic("argument is too large") } if n <= 2 { return 2 } return 1 << bits.Len(uint(n-1)) } // FloorToPowerOfTwo returns n if it is a power-of-two, otherwise the next-highest power-of-two. func FloorToPowerOfTwo(n int) int { if n <= 2 { return n } n |= n >> 1 n |= n >> 2 n |= n >> 4 n |= n >> 8 n |= n >> 16 return n - (n >> 1) } // ClosestPowerOfTwo returns n if it is a power-of-two, otherwise the closest power-of-two. func ClosestPowerOfTwo(n int) int { next := CeilToPowerOfTwo(n) if prev := next / 2; (n - prev) < (next - n) { next = prev } return next } ================================================ FILE: pkg/math/math_test.go ================================================ package math import "testing" func TestCeilToPowerOfTwo(t *testing.T) { type args struct { n int } tests := []struct { name string args args want int }{ // Boundary value tests: 0, 1, 2 {name: "zero", args: args{n: 0}, want: 2}, {name: "one", args: args{n: 1}, want: 2}, {name: "two", args: args{n: 2}, want: 2}, // Small value tests: 3-15 {name: "three", args: args{n: 3}, want: 1 << 2}, {name: "four", args: args{n: 4}, want: 1 << 2}, {name: "five", args: args{n: 5}, want: 1 << 3}, {name: "six", args: args{n: 6}, want: 1 << 3}, {name: "seven", args: args{n: 7}, want: 1 << 3}, {name: "eight", args: args{n: 8}, want: 1 << 3}, {name: "nine", args: args{n: 9}, want: 1 << 4}, {name: "ten", args: args{n: 10}, want: 1 << 4}, {name: "fifteen", args: args{n: 15}, want: 1 << 4}, // Tests for powers of two {name: "power_of_two_16", args: args{n: 1 << 4}, want: 1 << 4}, {name: "power_of_two_32", args: args{n: 1 << 5}, want: 1 << 5}, {name: "power_of_two_64", args: args{n: 1 << 6}, want: 1 << 6}, {name: "power_of_two_128", args: args{n: 1 << 7}, want: 1 << 7}, {name: "power_of_two_256", args: args{n: 1 << 8}, want: 1 << 8}, {name: "power_of_two_512", args: args{n: 1 << 9}, want: 1 << 9}, {name: "power_of_two_1024", args: args{n: 1 << 10}, want: 1 << 10}, // Values near powers of two {name: "near_power_17", args: args{n: (1 << 4) + 1}, want: 1 << 5}, {name: "near_power_31", args: args{n: (1 << 5) - 1}, want: 1 << 5}, {name: "near_power_33", args: args{n: (1 << 5) + 1}, want: 1 << 6}, {name: "near_power_63", args: args{n: (1 << 6) - 1}, want: 1 << 6}, {name: "near_power_65", args: args{n: (1 << 6) + 1}, want: 1 << 7}, {name: "near_power_127", args: args{n: (1 << 7) - 1}, want: 1 << 7}, {name: "near_power_129", args: args{n: (1 << 7) + 1}, want: 1 << 8}, {name: "near_power_255", args: args{n: (1 << 8) - 1}, want: 1 << 8}, {name: "near_power_257", args: args{n: (1 << 8) + 1}, want: 1 << 9}, {name: "near_power_511", args: args{n: (1 << 9) - 1}, want: 1 << 9}, {name: "near_power_513", args: args{n: (1 << 9) + 1}, want: 1 << 10}, {name: "near_power_1023", args: args{n: (1 << 10) - 1}, want: 1 << 10}, // Medium value tests {name: "medium_100", args: args{n: 100}, want: 1 << 7}, {name: "medium_200", args: args{n: 200}, want: 1 << 8}, {name: "medium_500", args: args{n: 500}, want: 1 << 9}, {name: "medium_1000", args: args{n: 1000}, want: 1 << 10}, {name: "medium_2000", args: args{n: 2000}, want: 1 << 11}, {name: "medium_5000", args: args{n: 5000}, want: 1 << 13}, {name: "medium_10000", args: args{n: 10000}, want: 1 << 14}, // Large value tests: around 2^10 {name: "large_1024_minus_1", args: args{n: 1<<10 - 1}, want: 1 << 10}, {name: "large_1024", args: args{n: 1 << 10}, want: 1 << 10}, {name: "large_1024_plus_1", args: args{n: 1<<10 + 1}, want: 1 << 11}, {name: "large_2047", args: args{n: (1 << 11) - 1}, want: 1 << 11}, {name: "large_2048", args: args{n: 1 << 11}, want: 1 << 11}, {name: "large_2049", args: args{n: (1 << 11) + 1}, want: 1 << 12}, // Very large value tests: around 2^20 {name: "very_large_1M_minus_1", args: args{n: 1<<20 - 1}, want: 1 << 20}, {name: "very_large_1M", args: args{n: 1 << 20}, want: 1 << 20}, {name: "very_large_1M_plus_1", args: args{n: 1<<20 + 1}, want: 1 << 21}, // Huge value tests: around 2^30 (32-bit system) {name: "huge_1G_minus_1", args: args{n: 1<<30 - 1}, want: 1 << 30}, {name: "huge_1G", args: args{n: 1 << 30}, want: 1 << 30}, {name: "huge_1G_plus_1", args: args{n: 1<<30 + 1}, want: 1 << 31}, // 64-bit system tests: around 2^32 {name: "extreme_2_32_minus_1", args: args{n: 1<<32 - 1}, want: 1 << 32}, {name: "extreme_2_32", args: args{n: 1 << 32}, want: 1 << 32}, {name: "extreme_2_32_plus_1", args: args{n: 1<<32 + 1}, want: 1 << 33}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := CeilToPowerOfTwo(tt.args.n); got != tt.want { t.Errorf("CeilToPowerOfTwo() = %v, want %v", got, tt.want) } }) } } ================================================ FILE: pkg/netpoll/defs_bsd_32bit.go ================================================ // Copyright (c) 2023 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build (darwin || dragonfly || freebsd || netbsd || openbsd) && (386 || arm || mips || mipsle) package netpoll type keventIdent = uint32 ================================================ FILE: pkg/netpoll/defs_bsd_64bit.go ================================================ // Copyright (c) 2023 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build (darwin || dragonfly || freebsd || netbsd || openbsd) && (amd64 || arm64 || ppc64 || ppc64le || mips64 || mips64le || riscv64) package netpoll type keventIdent = uint64 ================================================ FILE: pkg/netpoll/defs_linux.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !poll_opt package netpoll import "golang.org/x/sys/unix" type epollevent = unix.EpollEvent ================================================ FILE: pkg/netpoll/defs_linux_386.go ================================================ // created by cgo -cdefs and then converted to Go // cgo -cdefs defs2_linux.go //go:build poll_opt package netpoll type epollevent struct { events uint32 data [8]byte // to match amd64 } ================================================ FILE: pkg/netpoll/defs_linux_amd64.go ================================================ // created by cgo -cdefs and then converted to Go // cgo -cdefs defs_linux.go defs1_linux.go //go:build poll_opt package netpoll type epollevent struct { events uint32 data [8]byte // unaligned uintptr } ================================================ FILE: pkg/netpoll/defs_linux_arm.go ================================================ // Copyright 2014 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build poll_opt package netpoll type epollevent struct { events uint32 _pad uint32 data [8]byte // to match amd64 } ================================================ FILE: pkg/netpoll/defs_linux_arm64.go ================================================ // Created by cgo -cdefs and converted (by hand) to Go // ../cmd/cgo/cgo -cdefs defs_linux.go defs1_linux.go defs2_linux.go //go:build poll_opt package netpoll type epollevent struct { events uint32 _pad uint32 data [8]byte // to match amd64 } ================================================ FILE: pkg/netpoll/defs_linux_mips64x.go ================================================ // Copyright 2015 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build (mips64 || mips64le) && linux && poll_opt package netpoll type epollevent struct { events uint32 pad_cgo_0 [4]byte data [8]byte // unaligned uintptr } ================================================ FILE: pkg/netpoll/defs_linux_mipsx.go ================================================ // Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build (mips || mipsle) && linux && poll_opt package netpoll type epollevent struct { events uint32 pad_cgo_0 [4]byte data uint64 } ================================================ FILE: pkg/netpoll/defs_linux_ppc64.go ================================================ // created by cgo -cdefs and then converted to Go // cgo -cdefs defs_linux.go defs3_linux.go //go:build poll_opt package netpoll type epollevent struct { events uint32 pad_cgo_0 [4]byte data [8]byte // unaligned uintptr } ================================================ FILE: pkg/netpoll/defs_linux_ppc64le.go ================================================ // created by cgo -cdefs and then converted to Go // cgo -cdefs defs_linux.go defs3_linux.go //go:build poll_opt package netpoll type epollevent struct { events uint32 pad_cgo_0 [4]byte data [8]byte // unaligned uintptr } ================================================ FILE: pkg/netpoll/defs_linux_riscv64.go ================================================ // Generated using cgo, then manually converted into appropriate naming and code // for the Go runtime. // go tool cgo -godefs defs_linux.go defs1_linux.go defs2_linux.go //go:build poll_opt package netpoll type epollevent struct { events uint32 pad_cgo_0 [4]byte data [8]byte // unaligned uintptr } ================================================ FILE: pkg/netpoll/defs_linux_s390x.go ================================================ // Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build poll_opt package netpoll type epollevent struct { events uint32 pad_cgo_0 [4]byte data [8]byte // unaligned uintptr } ================================================ FILE: pkg/netpoll/defs_poller.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package netpoll // PollEventHandler is the callback for I/O events notified by the poller. type PollEventHandler func(int, IOEvent, IOFlags) error // PollAttachment is the user data which is about to be stored in "void *ptr" of epoll_data or "void *udata" of kevent. type PollAttachment struct { FD int Callback PollEventHandler } ================================================ FILE: pkg/netpoll/defs_poller_bsd.go ================================================ // Copyright (c) 2024 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || openbsd package netpoll // IOFlags represents the flags of IO events. type IOFlags = uint16 // IOEvent is the integer type of I/O events on BSD's. type IOEvent = int16 ================================================ FILE: pkg/netpoll/defs_poller_epoll.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build linux package netpoll import "golang.org/x/sys/unix" // IOFlags represents the flags of IO events. type IOFlags = uint16 // IOEvent is the integer type of I/O events on Linux. type IOEvent = uint32 const ( // InitPollEventsCap represents the initial capacity of poller event-list. InitPollEventsCap = 128 // MaxPollEventsCap is the maximum limitation of events that the poller can process. MaxPollEventsCap = 1024 // MinPollEventsCap is the minimum limitation of events that the poller can process. MinPollEventsCap = 32 // MaxAsyncTasksAtOneTime is the maximum amount of asynchronous tasks that the event-loop will process at one time. MaxAsyncTasksAtOneTime = 256 // ReadEvents represents readable events that are polled by epoll. ReadEvents = unix.EPOLLIN | unix.EPOLLPRI // WriteEvents represents writeable events that are polled by epoll. WriteEvents = unix.EPOLLOUT // ReadWriteEvents represents both readable and writeable events. ReadWriteEvents = ReadEvents | WriteEvents // ErrEvents represents exceptional events that occurred. ErrEvents = unix.EPOLLERR | unix.EPOLLHUP ) // IsReadEvent checks if the event is a read event. func IsReadEvent(event IOEvent) bool { return event&ReadEvents != 0 } // IsWriteEvent checks if the event is a write event. func IsWriteEvent(event IOEvent) bool { return event&WriteEvents != 0 } // IsErrorEvent checks if the event is an error event. func IsErrorEvent(event IOEvent, _ IOFlags) bool { return event&ErrEvents != 0 } type eventList struct { size int events []epollevent } func newEventList(size int) *eventList { return &eventList{size, make([]epollevent, size)} } func (el *eventList) expand() { if newSize := el.size << 1; newSize <= MaxPollEventsCap { el.size = newSize el.events = make([]epollevent, newSize) } } func (el *eventList) shrink() { if newSize := el.size >> 1; newSize >= MinPollEventsCap { el.size = newSize el.events = make([]epollevent, newSize) } } ================================================ FILE: pkg/netpoll/defs_poller_kqueue.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || netbsd || openbsd package netpoll import "golang.org/x/sys/unix" const ( // InitPollEventsCap represents the initial capacity of poller event-list. InitPollEventsCap = 64 // MaxPollEventsCap is the maximum limitation of events that the poller can process. MaxPollEventsCap = 512 // MinPollEventsCap is the minimum limitation of events that the poller can process. MinPollEventsCap = 16 // MaxAsyncTasksAtOneTime is the maximum amount of asynchronous tasks that the event-loop will process at one time. MaxAsyncTasksAtOneTime = 128 // ReadEvents represents readable events that are polled by kqueue. ReadEvents = unix.EVFILT_READ // WriteEvents represents writeable events that are polled by kqueue. WriteEvents = unix.EVFILT_WRITE // ReadWriteEvents represents both readable and writeable events. ReadWriteEvents = ReadEvents | WriteEvents // ErrEvents represents exceptional events that occurred. ErrEvents = unix.EV_EOF | unix.EV_ERROR ) // IsReadEvent checks if the event is a read event. func IsReadEvent(event IOEvent) bool { return event == ReadEvents } // IsWriteEvent checks if the event is a write event. func IsWriteEvent(event IOEvent) bool { return event == WriteEvents } // IsErrorEvent checks if the event is an error event. func IsErrorEvent(_ IOEvent, flags IOFlags) bool { return flags&ErrEvents != 0 } type eventList struct { size int events []unix.Kevent_t } func newEventList(size int) *eventList { return &eventList{size, make([]unix.Kevent_t, size)} } func (el *eventList) expand() { if newSize := el.size << 1; newSize <= MaxPollEventsCap { el.size = newSize el.events = make([]unix.Kevent_t, newSize) } } func (el *eventList) shrink() { if newSize := el.size >> 1; newSize >= MinPollEventsCap { el.size = newSize el.events = make([]unix.Kevent_t, newSize) } } ================================================ FILE: pkg/netpoll/defs_poller_netbsd.go ================================================ // Copyright (c) 2024 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package netpoll // IOEvent is the integer type of I/O events on BSD's. type IOEvent = uint32 // IOFlags represents the flags of IO events. type IOFlags = uint32 ================================================ FILE: pkg/netpoll/example_test.go ================================================ // Copyright (c) 2025 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package netpoll_test import ( "context" "fmt" "net" "os" "os/signal" "time" "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/netpoll" ) func Example() { ln, err := net.Listen("tcp", "127.0.0.1:9090") if err != nil { panic(fmt.Sprintf("Error listening: %v", err)) } defer ln.Close() //nolint:errcheck ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill) defer cancel() go func() { c, err := ln.Accept() if err != nil { panic(fmt.Sprintf("Error accepting connection: %v", err)) } defer c.Close() //nolint:errcheck buf := make([]byte, 64) for { select { case <-ctx.Done(): cancel() fmt.Printf("Signal received: %v\n", ctx.Err()) return default: } _, err := c.Read(buf) if err != nil { panic(fmt.Sprintf("Error reading data from client: %v", err)) } fmt.Printf("Received data from client: %s\n", buf) _, err = c.Write([]byte("Hello, client!")) if err != nil { panic(fmt.Sprintf("Error writing data to client: %v", err)) } fmt.Println("Sent data to client") time.Sleep(200 * time.Millisecond) } }() // Wait for the server to start running. time.Sleep(500 * time.Millisecond) poller, err := netpoll.OpenPoller() if err != nil { panic(fmt.Sprintf("Error opening poller: %v", err)) } defer poller.Close() //nolint:errcheck addr, err := net.ResolveTCPAddr("tcp", ln.Addr().String()) if err != nil { panic(fmt.Sprintf("Error resolving TCP address: %v", err)) } c, err := net.DialTCP("tcp", nil, addr) if err != nil { panic(fmt.Sprintf("Error dialing TCP address: %v", err)) } f, err := c.File() if err != nil { panic(fmt.Sprintf("Error getting file from connection: %v", err)) } closeClient := func() { c.Close() //nolint:errcheck f.Close() //nolint:errcheck } defer closeClient() sendData := true pa := netpoll.PollAttachment{ FD: int(f.Fd()), Callback: func(fd int, event netpoll.IOEvent, flags netpoll.IOFlags) error { //nolint:revive if netpoll.IsErrorEvent(event, flags) { closeClient() return errors.ErrEngineShutdown } if netpoll.IsReadEvent(event) { sendData = true buf := make([]byte, 64) _, err := c.Read(buf) if err != nil { closeClient() fmt.Println("Error reading data from server:", err) return errors.ErrEngineShutdown } fmt.Printf("Received data from server: %s\n", buf) // Process the data... } if netpoll.IsWriteEvent(event) && sendData { sendData = false // Write data to the connection... _, err := c.Write([]byte("Hello, server!")) if err != nil { closeClient() fmt.Println("Error writing data to server:", err) return errors.ErrEngineShutdown } fmt.Println("Sent data to server") } return nil }, } if err := poller.AddReadWrite(&pa, false); err != nil { panic(fmt.Sprintf("Error adding file descriptor to poller: %v", err)) } err = poller.Polling(func(fd int, event netpoll.IOEvent, flags netpoll.IOFlags) error { return pa.Callback(fd, event, flags) }) fmt.Printf("Poller exited with error: %v", err) } ================================================ FILE: pkg/netpoll/netpoll.go ================================================ // Copyright (c) 2025 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd /* Package netpoll provides a portable event-driven interface for network I/O. The underlying facility of event notification is OS-specific: - epoll on Linux - https://man7.org/linux/man-pages/man7/epoll.7.html - kqueue on *BSD/Darwin - https://man.freebsd.org/cgi/man.cgi?kqueue With the help of the netpoll package, you can easily build your own high-performance event-driven network applications based on epoll/kqueue. The Poller represents the event notification facility whose backend is epoll or kqueue. The OpenPoller function creates a new Poller instance: poller, err := netpoll.OpenPoller() if err != nil { // handle error } defer poller.Close() addr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:9090") if err != nil { // handle error } c, err := net.DialTCP("tcp", nil, addr) if err != nil { // handle error } f, err := c.File() if err != nil { // handle error } closeClient := func() { c.Close() f.Close() } defer closeClient() The PollAttachment consists of a file descriptor and its callback function. PollAttachment is used to register a file descriptor to Poller. The callback function is called when an event occurs on the file descriptor: pa := netpoll.PollAttachment{ FD: int(f.Fd()), Callback: func(fd int, event netpoll.IOEvent, flags netpoll.IOFlags) error { if netpoll.IsErrorEvent(event, flags) { closeClient() return errors.ErrEngineShutdown } if netpoll.IsReadEvent(event) { buf := make([]byte, 64) // Read data from the connection. _, err := c.Read(buf) if err != nil { closeClient() return errors.ErrEngineShutdown } // Process the data... } if netpoll.IsWriteEvent(event) { // Write data to the connection. _, err := c.Write([]byte("hello")) if err != nil { closeClient() return errors.ErrEngineShutdown } } return nil }} if err := poller.AddReadWrite(&pa, false); err != nil { // handle error } The Poller.Polling function starts the event loop monitoring file descriptors and waiting for I/O events to occur: poller.Polling(func(fd int, event netpoll.IOEvent, flags netpoll.IOFlags) error { return pa.Callback(fd, event, flags) }) Or poller.Polling() if you've enabled the build tag `poll_opt`. */ package netpoll ================================================ FILE: pkg/netpoll/poller_epoll_default.go ================================================ // Copyright (c) 2019 Andy Pan // Copyright (c) 2017 Joshua J Baker // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build linux && !poll_opt package netpoll import ( "errors" "os" "runtime" "sync/atomic" "unsafe" "golang.org/x/sys/unix" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" "github.com/panjf2000/gnet/v2/pkg/queue" ) // Poller represents a poller which is in charge of monitoring file-descriptors. type Poller struct { fd int // epoll fd efd int // eventfd efdBuf []byte // efd buffer to read an 8-byte integer wakeupCall int32 asyncTaskQueue queue.AsyncTaskQueue // queue with low priority urgentAsyncTaskQueue queue.AsyncTaskQueue // queue with high priority highPriorityEventsThreshold int32 // threshold of high-priority events } // OpenPoller instantiates a poller. func OpenPoller() (poller *Poller, err error) { poller = new(Poller) if poller.fd, err = unix.EpollCreate1(unix.EPOLL_CLOEXEC); err != nil { poller = nil err = os.NewSyscallError("epoll_create1", err) return } if poller.efd, err = unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC); err != nil { _ = poller.Close() poller = nil err = os.NewSyscallError("eventfd", err) return } poller.efdBuf = make([]byte, 8) if err = poller.AddRead(&PollAttachment{FD: poller.efd}, true); err != nil { _ = poller.Close() poller = nil return } poller.asyncTaskQueue = queue.NewLockFreeQueue() poller.urgentAsyncTaskQueue = queue.NewLockFreeQueue() poller.highPriorityEventsThreshold = MaxPollEventsCap return } // Close closes the poller. func (p *Poller) Close() error { _ = unix.Close(p.efd) return os.NewSyscallError("close", unix.Close(p.fd)) } // Make the endianness of bytes compatible with more linux OSs under different processor-architectures, // according to http://man7.org/linux/man-pages/man2/eventfd.2.html. var ( u uint64 = 1 b = (*(*[8]byte)(unsafe.Pointer(&u)))[:] ) // Trigger enqueues task and wakes up the poller to process pending tasks. // By default, any incoming task will enqueued into urgentAsyncTaskQueue // before the threshold of high-priority events is reached. When it happens, // any asks other than high-priority tasks will be shunted to asyncTaskQueue. // // Note that asyncTaskQueue is a queue of low-priority whose size may grow large and tasks in it may backlog. func (p *Poller) Trigger(priority queue.EventPriority, fn queue.Func, param any) (err error) { task := queue.GetTask() task.Exec, task.Param = fn, param if priority > queue.HighPriority && p.urgentAsyncTaskQueue.Length() >= p.highPriorityEventsThreshold { p.asyncTaskQueue.Enqueue(task) } else { // There might be some low-priority tasks overflowing into urgentAsyncTaskQueue in a flash, // but that's tolerable because it ought to be a rare case. p.urgentAsyncTaskQueue.Enqueue(task) } if atomic.CompareAndSwapInt32(&p.wakeupCall, 0, 1) { for { _, err = unix.Write(p.efd, b) if err == unix.EAGAIN { _, _ = unix.Read(p.efd, p.efdBuf) continue } break } } return os.NewSyscallError("write", err) } // Polling blocks the current goroutine, monitoring the registered file descriptors and waiting for network I/O. // When I/O occurs on any of the file descriptors, the provided callback function is invoked. func (p *Poller) Polling(callback PollEventHandler) error { el := newEventList(InitPollEventsCap) var doChores bool msec := -1 for { n, err := unix.EpollWait(p.fd, el.events, msec) if n == 0 || (n < 0 && err == unix.EINTR) { msec = -1 runtime.Gosched() continue } else if err != nil { logging.Errorf("error occurs in epoll: %v", os.NewSyscallError("epoll_wait", err)) return err } msec = 0 for i := 0; i < n; i++ { ev := &el.events[i] if fd := int(ev.Fd); fd == p.efd { // poller is awakened to run tasks in queues. doChores = true } else { err = callback(fd, ev.Events, 0) if errors.Is(err, errorx.ErrAcceptSocket) || errors.Is(err, errorx.ErrEngineShutdown) { return err } } } if doChores { doChores = false task := p.urgentAsyncTaskQueue.Dequeue() for ; task != nil; task = p.urgentAsyncTaskQueue.Dequeue() { err = task.Exec(task.Param) if errors.Is(err, errorx.ErrEngineShutdown) { return err } queue.PutTask(task) } for i := 0; i < MaxAsyncTasksAtOneTime; i++ { if task = p.asyncTaskQueue.Dequeue(); task == nil { break } err = task.Exec(task.Param) if errors.Is(err, errorx.ErrEngineShutdown) { return err } queue.PutTask(task) } atomic.StoreInt32(&p.wakeupCall, 0) if (!p.asyncTaskQueue.IsEmpty() || !p.urgentAsyncTaskQueue.IsEmpty()) && atomic.CompareAndSwapInt32(&p.wakeupCall, 0, 1) { for { _, err = unix.Write(p.efd, b) if err == unix.EAGAIN { _, _ = unix.Read(p.efd, p.efdBuf) continue } if err != nil { logging.Errorf("failed to notify next round of event-loop for leftover tasks, %v", os.NewSyscallError("write", err)) } break } } } if n == el.size { el.expand() } else if n < el.size>>1 { el.shrink() } } } // AddReadWrite registers the given file descriptor with readable and writable events to the poller. func (p *Poller) AddReadWrite(pa *PollAttachment, edgeTriggered bool) error { var ev uint32 = ReadWriteEvents if edgeTriggered { ev |= unix.EPOLLET | unix.EPOLLRDHUP } return os.NewSyscallError("epoll_ctl add", unix.EpollCtl(p.fd, unix.EPOLL_CTL_ADD, pa.FD, &unix.EpollEvent{Fd: int32(pa.FD), Events: ev})) } // AddRead registers the given file descriptor with readable event to the poller. func (p *Poller) AddRead(pa *PollAttachment, edgeTriggered bool) error { var ev uint32 = ReadEvents if edgeTriggered { ev |= unix.EPOLLET | unix.EPOLLRDHUP } return os.NewSyscallError("epoll_ctl add", unix.EpollCtl(p.fd, unix.EPOLL_CTL_ADD, pa.FD, &unix.EpollEvent{Fd: int32(pa.FD), Events: ev})) } // AddWrite registers the given file descriptor with writable event to the poller. func (p *Poller) AddWrite(pa *PollAttachment, edgeTriggered bool) error { var ev uint32 = WriteEvents if edgeTriggered { ev |= unix.EPOLLET | unix.EPOLLRDHUP } return os.NewSyscallError("epoll_ctl add", unix.EpollCtl(p.fd, unix.EPOLL_CTL_ADD, pa.FD, &unix.EpollEvent{Fd: int32(pa.FD), Events: ev})) } // ModRead modifies the given file descriptor with readable event in the poller. func (p *Poller) ModRead(pa *PollAttachment, edgeTriggered bool) error { var ev uint32 = ReadEvents if edgeTriggered { ev |= unix.EPOLLET | unix.EPOLLRDHUP } return os.NewSyscallError("epoll_ctl mod", unix.EpollCtl(p.fd, unix.EPOLL_CTL_MOD, pa.FD, &unix.EpollEvent{Fd: int32(pa.FD), Events: ev})) } // ModReadWrite modifies the given file descriptor with readable and writable events in the poller. func (p *Poller) ModReadWrite(pa *PollAttachment, edgeTriggered bool) error { var ev uint32 = ReadWriteEvents if edgeTriggered { ev |= unix.EPOLLET | unix.EPOLLRDHUP } return os.NewSyscallError("epoll_ctl mod", unix.EpollCtl(p.fd, unix.EPOLL_CTL_MOD, pa.FD, &unix.EpollEvent{Fd: int32(pa.FD), Events: ev})) } // Delete removes the given file descriptor from the poller. func (p *Poller) Delete(fd int) error { return os.NewSyscallError("epoll_ctl del", unix.EpollCtl(p.fd, unix.EPOLL_CTL_DEL, fd, nil)) } ================================================ FILE: pkg/netpoll/poller_epoll_ultimate.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build linux && poll_opt package netpoll import ( "errors" "os" "runtime" "sync/atomic" "unsafe" "golang.org/x/sys/unix" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" "github.com/panjf2000/gnet/v2/pkg/queue" ) // Poller represents a poller which is in charge of monitoring file-descriptors. type Poller struct { fd int // epoll fd epa *PollAttachment // PollAttachment for waking events efdBuf []byte // efd buffer to read an 8-byte integer wakeupCall int32 asyncTaskQueue queue.AsyncTaskQueue // queue with low priority urgentAsyncTaskQueue queue.AsyncTaskQueue // queue with high priority highPriorityEventsThreshold int32 // threshold of high-priority events } // OpenPoller instantiates a poller. func OpenPoller() (poller *Poller, err error) { poller = new(Poller) if poller.fd, err = unix.EpollCreate1(unix.EPOLL_CLOEXEC); err != nil { poller = nil err = os.NewSyscallError("epoll_create1", err) return } var efd int if efd, err = unix.Eventfd(0, unix.EFD_NONBLOCK|unix.EFD_CLOEXEC); err != nil { _ = poller.Close() poller = nil err = os.NewSyscallError("eventfd", err) return } poller.efdBuf = make([]byte, 8) poller.epa = &PollAttachment{FD: efd} if err = poller.AddRead(poller.epa, true); err != nil { _ = poller.Close() poller = nil return } poller.asyncTaskQueue = queue.NewLockFreeQueue() poller.urgentAsyncTaskQueue = queue.NewLockFreeQueue() poller.highPriorityEventsThreshold = MaxPollEventsCap return } // Close closes the poller. func (p *Poller) Close() error { _ = unix.Close(p.epa.FD) return os.NewSyscallError("close", unix.Close(p.fd)) } // Make the endianness of bytes compatible with more linux OSs under different processor-architectures, // according to http://man7.org/linux/man-pages/man2/eventfd.2.html. var ( u uint64 = 1 b = (*(*[8]byte)(unsafe.Pointer(&u)))[:] ) // Trigger enqueues task and wakes up the poller to process pending tasks. // By default, any incoming task will enqueued into urgentAsyncTaskQueue // before the threshold of high-priority events is reached. When it happens, // any asks other than high-priority tasks will be shunted to asyncTaskQueue. // // Note that asyncTaskQueue is a queue of low-priority whose size may grow large and tasks in it may backlog. func (p *Poller) Trigger(priority queue.EventPriority, fn queue.Func, param any) (err error) { task := queue.GetTask() task.Exec, task.Param = fn, param if priority > queue.HighPriority && p.urgentAsyncTaskQueue.Length() >= p.highPriorityEventsThreshold { p.asyncTaskQueue.Enqueue(task) } else { // There might be some low-priority tasks overflowing into urgentAsyncTaskQueue in a flash, // but that's tolerable because it ought to be a rare case. p.urgentAsyncTaskQueue.Enqueue(task) } if atomic.CompareAndSwapInt32(&p.wakeupCall, 0, 1) { for { _, err = unix.Write(p.epa.FD, b) if err == unix.EAGAIN { _, _ = unix.Read(p.epa.FD, p.efdBuf) continue } break } } return os.NewSyscallError("write", err) } // Polling blocks the current goroutine, monitoring the registered file descriptors and waiting for network I/O. // When I/O occurs on any of the file descriptors, the provided callback function is invoked. func (p *Poller) Polling() error { el := newEventList(InitPollEventsCap) var doChores bool msec := -1 for { n, err := epollWait(p.fd, el.events, msec) if n == 0 || (n < 0 && err == unix.EINTR) { msec = -1 runtime.Gosched() continue } else if err != nil { logging.Errorf("error occurs in epoll: %v", os.NewSyscallError("epoll_wait", err)) return err } msec = 0 for i := 0; i < n; i++ { ev := &el.events[i] pollAttachment := restorePollAttachment(unsafe.Pointer(&ev.data)) if pollAttachment.FD == p.epa.FD { // poller is awakened to run tasks in queues. doChores = true } else { err = pollAttachment.Callback(pollAttachment.FD, ev.events, 0) if errors.Is(err, errorx.ErrAcceptSocket) || errors.Is(err, errorx.ErrEngineShutdown) { return err } } } if doChores { doChores = false task := p.urgentAsyncTaskQueue.Dequeue() for ; task != nil; task = p.urgentAsyncTaskQueue.Dequeue() { err = task.Exec(task.Param) if errors.Is(err, errorx.ErrEngineShutdown) { return err } queue.PutTask(task) } for i := 0; i < MaxAsyncTasksAtOneTime; i++ { if task = p.asyncTaskQueue.Dequeue(); task == nil { break } err = task.Exec(task.Param) if errors.Is(err, errorx.ErrEngineShutdown) { return err } queue.PutTask(task) } atomic.StoreInt32(&p.wakeupCall, 0) if (!p.asyncTaskQueue.IsEmpty() || !p.urgentAsyncTaskQueue.IsEmpty()) && atomic.CompareAndSwapInt32(&p.wakeupCall, 0, 1) { for { _, err = unix.Write(p.epa.FD, b) if err == unix.EAGAIN { _, _ = unix.Read(p.epa.FD, p.efdBuf) continue } if err != nil { logging.Errorf("failed to notify next round of event-loop for leftover tasks, %v", os.NewSyscallError("write", err)) } break } } } if n == el.size { el.expand() } else if n < el.size>>1 { el.shrink() } } } // AddReadWrite registers the given file descriptor with readable and writable events to the poller. func (p *Poller) AddReadWrite(pa *PollAttachment, edgeTriggered bool) error { var ev epollevent ev.events = ReadWriteEvents if edgeTriggered { ev.events |= unix.EPOLLET | unix.EPOLLRDHUP } convertPollAttachment(unsafe.Pointer(&ev.data), pa) return os.NewSyscallError("epoll_ctl add", epollCtl(p.fd, unix.EPOLL_CTL_ADD, pa.FD, &ev)) } // AddRead registers the given file descriptor with readable event to the poller. func (p *Poller) AddRead(pa *PollAttachment, edgeTriggered bool) error { var ev epollevent ev.events = ReadEvents if edgeTriggered { ev.events |= unix.EPOLLET | unix.EPOLLRDHUP } convertPollAttachment(unsafe.Pointer(&ev.data), pa) return os.NewSyscallError("epoll_ctl add", epollCtl(p.fd, unix.EPOLL_CTL_ADD, pa.FD, &ev)) } // AddWrite registers the given file descriptor with writable event to the poller. func (p *Poller) AddWrite(pa *PollAttachment, edgeTriggered bool) error { var ev epollevent ev.events = WriteEvents if edgeTriggered { ev.events |= unix.EPOLLET | unix.EPOLLRDHUP } convertPollAttachment(unsafe.Pointer(&ev.data), pa) return os.NewSyscallError("epoll_ctl add", epollCtl(p.fd, unix.EPOLL_CTL_ADD, pa.FD, &ev)) } // ModRead modifies the given file descriptor with readable event in the poller. func (p *Poller) ModRead(pa *PollAttachment, edgeTriggered bool) error { var ev epollevent ev.events = ReadEvents if edgeTriggered { ev.events |= unix.EPOLLET | unix.EPOLLRDHUP } convertPollAttachment(unsafe.Pointer(&ev.data), pa) return os.NewSyscallError("epoll_ctl mod", epollCtl(p.fd, unix.EPOLL_CTL_MOD, pa.FD, &ev)) } // ModReadWrite modifies the given file descriptor with readable and writable events in the poller. func (p *Poller) ModReadWrite(pa *PollAttachment, edgeTriggered bool) error { var ev epollevent ev.events = ReadWriteEvents if edgeTriggered { ev.events |= unix.EPOLLET | unix.EPOLLRDHUP } convertPollAttachment(unsafe.Pointer(&ev.data), pa) return os.NewSyscallError("epoll_ctl mod", epollCtl(p.fd, unix.EPOLL_CTL_MOD, pa.FD, &ev)) } // Delete removes the given file descriptor from the poller. func (p *Poller) Delete(fd int) error { return os.NewSyscallError("epoll_ctl del", epollCtl(p.fd, unix.EPOLL_CTL_DEL, fd, nil)) } ================================================ FILE: pkg/netpoll/poller_kqueue_default.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build (darwin || dragonfly || freebsd || netbsd || openbsd) && !poll_opt package netpoll import ( "errors" "os" "runtime" "sync/atomic" "golang.org/x/sys/unix" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" "github.com/panjf2000/gnet/v2/pkg/queue" ) // Poller represents a poller which is in charge of monitoring file-descriptors. type Poller struct { fd int pipe []int wakeupCall int32 asyncTaskQueue queue.AsyncTaskQueue // queue with low priority urgentAsyncTaskQueue queue.AsyncTaskQueue // queue with high priority highPriorityEventsThreshold int32 // threshold of high-priority events } // OpenPoller instantiates a poller. func OpenPoller() (poller *Poller, err error) { poller = new(Poller) if poller.fd, err = unix.Kqueue(); err != nil { poller = nil err = os.NewSyscallError("kqueue", err) return } if err = poller.addWakeupEvent(); err != nil { _ = poller.Close() poller = nil err = os.NewSyscallError("kevent | pipe2", err) return } poller.asyncTaskQueue = queue.NewLockFreeQueue() poller.urgentAsyncTaskQueue = queue.NewLockFreeQueue() poller.highPriorityEventsThreshold = MaxPollEventsCap return } // Close closes the poller. func (p *Poller) Close() error { if len(p.pipe) == 2 { _ = unix.Close(p.pipe[0]) _ = unix.Close(p.pipe[1]) } return os.NewSyscallError("close", unix.Close(p.fd)) } // Trigger enqueues task and wakes up the poller to process pending tasks. // By default, any incoming task will enqueued into urgentAsyncTaskQueue // before the threshold of high-priority events is reached. When it happens, // any asks other than high-priority tasks will be shunted to asyncTaskQueue. // // Note that asyncTaskQueue is a queue of low-priority whose size may grow large and tasks in it may backlog. func (p *Poller) Trigger(priority queue.EventPriority, fn queue.Func, param any) (err error) { task := queue.GetTask() task.Exec, task.Param = fn, param if priority > queue.HighPriority && p.urgentAsyncTaskQueue.Length() >= p.highPriorityEventsThreshold { p.asyncTaskQueue.Enqueue(task) } else { // There might be some low-priority tasks overflowing into urgentAsyncTaskQueue in a flash, // but that's tolerable because it ought to be a rare case. p.urgentAsyncTaskQueue.Enqueue(task) } if atomic.CompareAndSwapInt32(&p.wakeupCall, 0, 1) { err = p.wakePoller() } return os.NewSyscallError("kevent | write", err) } // Polling blocks the current goroutine, monitoring the registered file descriptors and waiting for network I/O. // When I/O occurs on any of the file descriptors, the provided callback function is invoked. func (p *Poller) Polling(callback PollEventHandler) error { el := newEventList(InitPollEventsCap) var ( ts unix.Timespec tsp *unix.Timespec doChores bool ) for { n, err := unix.Kevent(p.fd, nil, el.events, tsp) if n == 0 || (n < 0 && err == unix.EINTR) { tsp = nil runtime.Gosched() continue } else if err != nil { logging.Errorf("error occurs in kqueue: %v", os.NewSyscallError("kevent wait", err)) return err } tsp = &ts for i := 0; i < n; i++ { ev := &el.events[i] if fd := int(ev.Ident); fd == 0 { // poller is awakened to run tasks in queues doChores = true p.drainWakeupEvent() } else { err = callback(fd, ev.Filter, ev.Flags) if errors.Is(err, errorx.ErrAcceptSocket) || errors.Is(err, errorx.ErrEngineShutdown) { return err } } } if doChores { doChores = false task := p.urgentAsyncTaskQueue.Dequeue() for ; task != nil; task = p.urgentAsyncTaskQueue.Dequeue() { err = task.Exec(task.Param) if errors.Is(err, errorx.ErrEngineShutdown) { return err } queue.PutTask(task) } for i := 0; i < MaxAsyncTasksAtOneTime; i++ { if task = p.asyncTaskQueue.Dequeue(); task == nil { break } err = task.Exec(task.Param) if errors.Is(err, errorx.ErrEngineShutdown) { return err } queue.PutTask(task) } atomic.StoreInt32(&p.wakeupCall, 0) if (!p.asyncTaskQueue.IsEmpty() || !p.urgentAsyncTaskQueue.IsEmpty()) && atomic.CompareAndSwapInt32(&p.wakeupCall, 0, 1) { if err = p.wakePoller(); err != nil { doChores = true } } } if n == el.size { el.expand() } else if n < el.size>>1 { el.shrink() } } } // AddReadWrite registers the given file descriptor with readable and writable events to the poller. func (p *Poller) AddReadWrite(pa *PollAttachment, edgeTriggered bool) error { var flags IOFlags = unix.EV_ADD if edgeTriggered { flags |= unix.EV_CLEAR } _, err := unix.Kevent(p.fd, []unix.Kevent_t{ {Ident: keventIdent(pa.FD), Flags: flags, Filter: unix.EVFILT_READ}, {Ident: keventIdent(pa.FD), Flags: flags, Filter: unix.EVFILT_WRITE}, }, nil, nil) return os.NewSyscallError("kevent add", err) } // AddRead registers the given file descriptor with readable event to the poller. func (p *Poller) AddRead(pa *PollAttachment, edgeTriggered bool) error { var flags IOFlags = unix.EV_ADD if edgeTriggered { flags |= unix.EV_CLEAR } _, err := unix.Kevent(p.fd, []unix.Kevent_t{ {Ident: keventIdent(pa.FD), Flags: flags, Filter: unix.EVFILT_READ}, }, nil, nil) return os.NewSyscallError("kevent add", err) } // AddWrite registers the given file descriptor with writable event to the poller. func (p *Poller) AddWrite(pa *PollAttachment, edgeTriggered bool) error { var flags IOFlags = unix.EV_ADD if edgeTriggered { flags |= unix.EV_CLEAR } _, err := unix.Kevent(p.fd, []unix.Kevent_t{ {Ident: keventIdent(pa.FD), Flags: flags, Filter: unix.EVFILT_WRITE}, }, nil, nil) return os.NewSyscallError("kevent add", err) } // ModRead modifies the given file descriptor with readable event in the poller. func (p *Poller) ModRead(pa *PollAttachment, _ bool) error { _, err := unix.Kevent(p.fd, []unix.Kevent_t{ {Ident: keventIdent(pa.FD), Flags: unix.EV_DELETE, Filter: unix.EVFILT_WRITE}, }, nil, nil) return os.NewSyscallError("kevent delete", err) } // ModReadWrite modifies the given file descriptor with readable and writable events in the poller. func (p *Poller) ModReadWrite(pa *PollAttachment, edgeTriggered bool) error { var flags IOFlags = unix.EV_ADD if edgeTriggered { flags |= unix.EV_CLEAR } _, err := unix.Kevent(p.fd, []unix.Kevent_t{ {Ident: keventIdent(pa.FD), Flags: flags, Filter: unix.EVFILT_WRITE}, }, nil, nil) return os.NewSyscallError("kevent add", err) } // Delete removes the given file descriptor from the poller. func (*Poller) Delete(_ int) error { return nil } ================================================ FILE: pkg/netpoll/poller_kqueue_ultimate.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build (darwin || dragonfly || freebsd || netbsd || openbsd) && poll_opt package netpoll import ( "errors" "os" "runtime" "sync/atomic" "unsafe" "golang.org/x/sys/unix" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/logging" "github.com/panjf2000/gnet/v2/pkg/queue" ) // Poller represents a poller which is in charge of monitoring file-descriptors. type Poller struct { fd int pipe []int wakeupCall int32 asyncTaskQueue queue.AsyncTaskQueue // queue with low priority urgentAsyncTaskQueue queue.AsyncTaskQueue // queue with high priority highPriorityEventsThreshold int32 // threshold of high-priority events } // OpenPoller instantiates a poller. func OpenPoller() (poller *Poller, err error) { poller = new(Poller) if poller.fd, err = unix.Kqueue(); err != nil { poller = nil err = os.NewSyscallError("kqueue", err) return } if err = poller.addWakeupEvent(); err != nil { _ = poller.Close() poller = nil err = os.NewSyscallError("kevent | pipe2", err) return } poller.asyncTaskQueue = queue.NewLockFreeQueue() poller.urgentAsyncTaskQueue = queue.NewLockFreeQueue() poller.highPriorityEventsThreshold = MaxPollEventsCap return } // Close closes the poller. func (p *Poller) Close() error { if len(p.pipe) == 2 { _ = unix.Close(p.pipe[0]) _ = unix.Close(p.pipe[1]) } return os.NewSyscallError("close", unix.Close(p.fd)) } // Trigger enqueues task and wakes up the poller to process pending tasks. // By default, any incoming task will enqueued into urgentAsyncTaskQueue // before the threshold of high-priority events is reached. When it happens, // any asks other than high-priority tasks will be shunted to asyncTaskQueue. // // Note that asyncTaskQueue is a queue of low-priority whose size may grow large and tasks in it may backlog. func (p *Poller) Trigger(priority queue.EventPriority, fn queue.Func, param any) (err error) { task := queue.GetTask() task.Exec, task.Param = fn, param if priority > queue.HighPriority && p.urgentAsyncTaskQueue.Length() >= p.highPriorityEventsThreshold { p.asyncTaskQueue.Enqueue(task) } else { // There might be some low-priority tasks overflowing into urgentAsyncTaskQueue in a flash, // but that's tolerable because it ought to be a rare case. p.urgentAsyncTaskQueue.Enqueue(task) } if atomic.CompareAndSwapInt32(&p.wakeupCall, 0, 1) { err = p.wakePoller() } return os.NewSyscallError("kevent | write", err) } // Polling blocks the current goroutine, monitoring the registered file descriptors and waiting for network I/O. // When I/O occurs on any of the file descriptors, the provided callback function is invoked. func (p *Poller) Polling() error { el := newEventList(InitPollEventsCap) var ( ts unix.Timespec tsp *unix.Timespec doChores bool ) for { n, err := unix.Kevent(p.fd, nil, el.events, tsp) if n == 0 || (n < 0 && err == unix.EINTR) { tsp = nil runtime.Gosched() continue } else if err != nil { logging.Errorf("error occurs in kqueue: %v", os.NewSyscallError("kevent wait", err)) return err } tsp = &ts for i := 0; i < n; i++ { ev := &el.events[i] if ev.Ident == 0 { // poller is awakened to run tasks in queues doChores = true p.drainWakeupEvent() } else { pollAttachment := restorePollAttachment(unsafe.Pointer(&ev.Udata)) err = pollAttachment.Callback(int(ev.Ident), ev.Filter, ev.Flags) if errors.Is(err, errorx.ErrAcceptSocket) || errors.Is(err, errorx.ErrEngineShutdown) { return err } } } if doChores { doChores = false task := p.urgentAsyncTaskQueue.Dequeue() for ; task != nil; task = p.urgentAsyncTaskQueue.Dequeue() { err = task.Exec(task.Param) if errors.Is(err, errorx.ErrEngineShutdown) { return err } queue.PutTask(task) } for i := 0; i < MaxAsyncTasksAtOneTime; i++ { if task = p.asyncTaskQueue.Dequeue(); task == nil { break } err = task.Exec(task.Param) if errors.Is(err, errorx.ErrEngineShutdown) { return err } queue.PutTask(task) } atomic.StoreInt32(&p.wakeupCall, 0) if (!p.asyncTaskQueue.IsEmpty() || !p.urgentAsyncTaskQueue.IsEmpty()) && atomic.CompareAndSwapInt32(&p.wakeupCall, 0, 1) { if err = p.wakePoller(); err != nil { doChores = true } } } if n == el.size { el.expand() } else if n < el.size>>1 { el.shrink() } } } // AddReadWrite registers the given file descriptor with readable and writable events to the poller. func (p *Poller) AddReadWrite(pa *PollAttachment, edgeTriggered bool) error { var evs [2]unix.Kevent_t evs[0].Ident = keventIdent(pa.FD) evs[0].Filter = unix.EVFILT_READ evs[0].Flags = unix.EV_ADD if edgeTriggered { evs[0].Flags |= unix.EV_CLEAR } convertPollAttachment(unsafe.Pointer(&evs[0].Udata), pa) evs[1] = evs[0] evs[1].Filter = unix.EVFILT_WRITE _, err := unix.Kevent(p.fd, evs[:], nil, nil) return os.NewSyscallError("kevent add", err) } // AddRead registers the given file descriptor with readable event to the poller. func (p *Poller) AddRead(pa *PollAttachment, edgeTriggered bool) error { var evs [1]unix.Kevent_t evs[0].Ident = keventIdent(pa.FD) evs[0].Filter = unix.EVFILT_READ evs[0].Flags = unix.EV_ADD if edgeTriggered { evs[0].Flags |= unix.EV_CLEAR } convertPollAttachment(unsafe.Pointer(&evs[0].Udata), pa) _, err := unix.Kevent(p.fd, evs[:], nil, nil) return os.NewSyscallError("kevent add", err) } // AddWrite registers the given file descriptor with writable event to the poller. func (p *Poller) AddWrite(pa *PollAttachment, edgeTriggered bool) error { var evs [1]unix.Kevent_t evs[0].Ident = keventIdent(pa.FD) evs[0].Filter = unix.EVFILT_WRITE evs[0].Flags = unix.EV_ADD if edgeTriggered { evs[0].Flags |= unix.EV_CLEAR } convertPollAttachment(unsafe.Pointer(&evs[0].Udata), pa) _, err := unix.Kevent(p.fd, evs[:], nil, nil) return os.NewSyscallError("kevent add", err) } // ModRead modifies the given file descriptor with readable event in the poller. func (p *Poller) ModRead(pa *PollAttachment, _ bool) error { var evs [1]unix.Kevent_t evs[0].Ident = keventIdent(pa.FD) evs[0].Filter = unix.EVFILT_WRITE evs[0].Flags = unix.EV_DELETE _, err := unix.Kevent(p.fd, evs[:], nil, nil) return os.NewSyscallError("kevent delete", err) } // ModReadWrite modifies the given file descriptor with readable and writable events in the poller. func (p *Poller) ModReadWrite(pa *PollAttachment, edgeTriggered bool) error { var evs [1]unix.Kevent_t evs[0].Ident = keventIdent(pa.FD) evs[0].Filter = unix.EVFILT_WRITE evs[0].Flags = unix.EV_ADD if edgeTriggered { evs[0].Flags |= unix.EV_CLEAR } convertPollAttachment(unsafe.Pointer(&evs[0].Udata), pa) _, err := unix.Kevent(p.fd, evs[:], nil, nil) return os.NewSyscallError("kevent add", err) } // Delete removes the given file descriptor from the poller. func (p *Poller) Delete(_ int) error { return nil } ================================================ FILE: pkg/netpoll/poller_kqueue_wakeup.go ================================================ // Copyright (c) 2024 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd package netpoll import ( "golang.org/x/sys/unix" "github.com/panjf2000/gnet/v2/pkg/logging" ) func (p *Poller) addWakeupEvent() error { _, err := unix.Kevent(p.fd, []unix.Kevent_t{{ Ident: 0, Filter: unix.EVFILT_USER, Flags: unix.EV_ADD | unix.EV_CLEAR, }}, nil, nil) return err } func (p *Poller) wakePoller() error { retry: _, err := unix.Kevent(p.fd, []unix.Kevent_t{{ Ident: 0, Filter: unix.EVFILT_USER, Fflags: unix.NOTE_TRIGGER, }}, nil, nil) if err == nil { return nil } if err == unix.EINTR { // All changes contained in the changelist should have been applied // before returning EINTR. But let's be skeptical and retry it anyway, // to make a 100% commitment. goto retry } logging.Warnf("failed to wake up the poller: %v", err) return err } func (p *Poller) drainWakeupEvent() {} ================================================ FILE: pkg/netpoll/poller_kqueue_wakeup1.go ================================================ // Copyright (c) 2024 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build netbsd || openbsd package netpoll import ( "golang.org/x/sys/unix" "github.com/panjf2000/gnet/v2/pkg/logging" ) // TODO(panjf2000): NetBSD didn't implement EVFILT_USER for user-established events // until NetBSD 10.0, check out https://www.netbsd.org/releases/formal-10/NetBSD-10.0.html // Therefore we use the pipe to wake up the kevent on NetBSD at this point. Get back here // and switch to EVFILT_USER when we bump up the minimal requirement of NetBSD to 10.0. // Alternatively, maybe we can use EVFILT_USER on the NetBSD by checking the kernel version // via uname(3) and fall back to the pipe if the kernel version is older than 10.0. func (p *Poller) addWakeupEvent() error { p.pipe = make([]int, 2) if err := unix.Pipe2(p.pipe[:], unix.O_NONBLOCK|unix.O_CLOEXEC); err != nil { logging.Fatalf("failed to create pipe for wakeup event: %v", err) } _, err := unix.Kevent(p.fd, []unix.Kevent_t{{ Ident: uint64(p.pipe[0]), Filter: unix.EVFILT_READ, Flags: unix.EV_ADD, }}, nil, nil) return err } func (p *Poller) wakePoller() error { retry: _, err := unix.Write(p.pipe[1], []byte("x")) if err == nil || err == unix.EAGAIN { return nil } if err == unix.EINTR { goto retry } logging.Warnf("failed to write to the wakeup pipe: %v", err) return err } func (p *Poller) drainWakeupEvent() { var buf [8]byte _, _ = unix.Read(p.pipe[0], buf[:]) } ================================================ FILE: pkg/netpoll/poller_unix_ultimate.go ================================================ // Copyright (c) 2024 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build (darwin || dragonfly || freebsd || linux || netbsd || openbsd) && poll_opt package netpoll import "unsafe" func convertPollAttachment(ptr unsafe.Pointer, attachment *PollAttachment) { *(**PollAttachment)(ptr) = attachment } func restorePollAttachment(ptr unsafe.Pointer) *PollAttachment { return *(**PollAttachment)(ptr) } ================================================ FILE: pkg/netpoll/syscall_epoll_generic_linux.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build !arm64 && !riscv64 && poll_opt package netpoll import ( "unsafe" "golang.org/x/sys/unix" ) func epollWait(epfd int, events []epollevent, msec int) (int, error) { var ep unsafe.Pointer if len(events) > 0 { ep = unsafe.Pointer(&events[0]) } else { ep = unsafe.Pointer(&zero) } var ( np uintptr errno unix.Errno ) if msec == 0 { // non-block system call, use RawSyscall6 to avoid getting preempted by runtime np, _, errno = unix.RawSyscall6(unix.SYS_EPOLL_WAIT, uintptr(epfd), uintptr(ep), uintptr(len(events)), 0, 0, 0) } else { np, _, errno = unix.Syscall6(unix.SYS_EPOLL_WAIT, uintptr(epfd), uintptr(ep), uintptr(len(events)), uintptr(msec), 0, 0) } if errno != 0 { return int(np), errnoErr(errno) } return int(np), nil } ================================================ FILE: pkg/netpoll/syscall_epoll_linux.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build poll_opt package netpoll import ( "unsafe" "golang.org/x/sys/unix" ) func epollCtl(epfd int, op int, fd int, event *epollevent) error { _, _, errno := unix.RawSyscall6(unix.SYS_EPOLL_CTL, uintptr(epfd), uintptr(op), uintptr(fd), uintptr(unsafe.Pointer(event)), 0, 0) if errno != 0 { return errnoErr(errno) } return nil } ================================================ FILE: pkg/netpoll/syscall_epoll_riscv64_arm64_linux.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build ((linux && arm64) || (linux && riscv64)) && poll_opt package netpoll import ( "unsafe" "golang.org/x/sys/unix" ) func epollWait(epfd int, events []epollevent, msec int) (int, error) { var ep unsafe.Pointer if len(events) > 0 { ep = unsafe.Pointer(&events[0]) } else { ep = unsafe.Pointer(&zero) } var ( np uintptr errno unix.Errno ) if msec == 0 { // non-block system call, use RawSyscall6 to avoid getting preempted by runtime np, _, errno = unix.RawSyscall6(unix.SYS_EPOLL_PWAIT, uintptr(epfd), uintptr(ep), uintptr(len(events)), 0, 0, 0) } else { np, _, errno = unix.Syscall6(unix.SYS_EPOLL_PWAIT, uintptr(epfd), uintptr(ep), uintptr(len(events)), uintptr(msec), 0, 0) } if errno != 0 { return int(np), errnoErr(errno) } return int(np), nil } ================================================ FILE: pkg/netpoll/syscall_errors_linux.go ================================================ // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. //go:build poll_opt package netpoll import "golang.org/x/sys/unix" // Do the interface allocations only once for common // Errno values. var ( errEAGAIN error = unix.EAGAIN errEINVAL error = unix.EINVAL errENOENT error = unix.ENOENT ) // errnoErr returns common boxed Errno values, to prevent // allocations at runtime. func errnoErr(e unix.Errno) error { switch e { case unix.EAGAIN: return errEAGAIN case unix.EINVAL: return errEINVAL case unix.ENOENT: return errENOENT } return e } var zero uintptr ================================================ FILE: pkg/pool/bytebuffer/bytebuffer.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package bytebuffer is a pool of bytebufferpool.ByteBuffer. package bytebuffer import "github.com/valyala/bytebufferpool" // ByteBuffer is the alias of bytebufferpool.ByteBuffer. type ByteBuffer = bytebufferpool.ByteBuffer var ( // Get returns an empty byte buffer from the pool, exported from gnet/bytebuffer. Get = bytebufferpool.Get // Put returns byte buffer to the pool, exported from gnet/bytebuffer. Put = func(b *ByteBuffer) { if b != nil { bytebufferpool.Put(b) } } ) ================================================ FILE: pkg/pool/byteslice/byteslice.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package byteslice implements a pool of byte slices consisting of sync.Pool's // that collect byte slices with different length sizes from 0 to 32 in powers of 2. package byteslice import ( "math" "math/bits" "sync" "unsafe" ) var builtinPool Pool // Pool consists of 32 sync.Pool, representing byte slices of length from 0 to 32 in powers of 2. type Pool struct { pools [32]sync.Pool } // Get returns a byte slice with given length from the built-in pool. func Get(size int) []byte { return builtinPool.Get(size) } // Put returns the byte slice to the built-in pool. func Put(buf []byte) { builtinPool.Put(buf) } // Get retrieves a byte slice of the length requested by the caller from pool or allocates a new one. func (p *Pool) Get(size int) []byte { if size <= 0 { return nil } if size > math.MaxInt32 { return make([]byte, size) } idx := index(uint32(size)) ptr, _ := p.pools[idx].Get().(*byte) if ptr == nil { return make([]byte, size, 1< math.MaxInt32 { return } idx := index(uint32(size)) if size != 1< calibrateCallsThreshold { p.calibrate() } maxSize := int(atomic.LoadUint64(&p.maxSize)) if maxSize == 0 || b.Cap() <= maxSize { b.Reset() p.pool.Put(b) } } func (p *Pool) calibrate() { if !atomic.CompareAndSwapUint64(&p.calibrating, 0, 1) { return } a := make(callSizes, 0, steps) var callsSum uint64 for i := uint64(0); i < steps; i++ { calls := atomic.SwapUint64(&p.calls[i], 0) callsSum += calls a = append(a, callSize{ calls: calls, size: minSize << i, }) } sort.Sort(a) defaultSize := a[0].size maxSize := defaultSize maxSum := uint64(float64(callsSum) * maxPercentile) callsSum = 0 for i := 0; i < steps; i++ { if callsSum > maxSum { break } callsSum += a[i].calls size := a[i].size if size > maxSize { maxSize = size } } atomic.StoreUint64(&p.defaultSize, defaultSize) atomic.StoreUint64(&p.maxSize, maxSize) atomic.StoreUint64(&p.calibrating, 0) } type callSize struct { calls uint64 size uint64 } type callSizes []callSize func (ci callSizes) Len() int { return len(ci) } func (ci callSizes) Less(i, j int) bool { return ci[i].calls > ci[j].calls } func (ci callSizes) Swap(i, j int) { ci[i], ci[j] = ci[j], ci[i] } func index(n int) int { n-- n >>= minBitSize idx := 0 if n > 0 { idx = bits.Len(uint(n)) } if idx >= steps { idx = steps - 1 } return idx } ================================================ FILE: pkg/queue/lock_free_queue.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package queue delivers an implementation of lock-free concurrent queue based on // the algorithm presented by Maged M. Michael and Michael L. Scot. in 1996: https://dl.acm.org/doi/10.1145/248052.248106 // // Pseudocode of non-blocking concurrent queue algorithm: /* structure pointer_t {ptr: pointer to node_t, count: unsigned integer} structure node_t {value: data type, next: pointer_t} structure queue_t {Head: pointer_t, Tail: pointer_t} initialize(Q: pointer to queue_t) node = new_node() // Allocate a free node node->next.ptr = NULL // Make it the only node in the linked list Q->Head.ptr = Q->Tail.ptr = node // Both Head and Tail point to it enqueue(Q: pointer to queue_t, value: data type) E1: node = new_node() // Allocate a new node from the free list E2: node->value = value // Copy enqueued value into node E3: node->next.ptr = NULL // Set next pointer of node to NULL E4: loop // Keep trying until Enqueue is done E5: tail = Q->Tail // Read Tail.ptr and Tail.count together E6: next = tail.ptr->next // Read next ptr and count fields together E7: if tail == Q->Tail // Are tail and next consistent? // Was Tail pointing to the last node? E8: if next.ptr == NULL // Try to link node at the end of the linked list E9: if CAS(&tail.ptr->next, next, ) E10: break // Enqueue is done. Exit loop E11: endif E12: else // Tail was not pointing to the last node // Try to swing Tail to the next node E13: CAS(&Q->Tail, tail, ) E14: endif E15: endif E16: endloop // Enqueue is done. Try to swing Tail to the inserted node E17: CAS(&Q->Tail, tail, ) dequeue(Q: pointer to queue_t, pvalue: pointer to data type): boolean D1: loop // Keep trying until Dequeue is done D2: head = Q->Head // Read Head D3: tail = Q->Tail // Read Tail D4: next = head.ptr->next // Read Head.ptr->next D5: if head == Q->Head // Are head, tail, and next consistent? D6: if head.ptr == tail.ptr // Is queue empty or Tail falling behind? D7: if next.ptr == NULL // Is queue empty? D8: return FALSE // Queue is empty, couldn't dequeue D9: endif // Tail is falling behind. Try to advance it D10: CAS(&Q->Tail, tail, ) D11: else // No need to deal with Tail // Read value before CAS // Otherwise, another dequeue might free the next node D12: *pvalue = next.ptr->value // Try to swing Head to the next node D13: if CAS(&Q->Head, head, ) D14: break // Dequeue is done. Exit loop D15: endif D16: endif D17: endif D18: endloop D19: free(head.ptr) // It is safe now to free the old node D20: return TRUE // Queue was not empty, dequeue succeeded */ package queue import ( "sync/atomic" "unsafe" ) // lockFreeQueue is a simple, fast, and practical non-blocking and concurrent queue with no lock. type lockFreeQueue struct { head unsafe.Pointer tail unsafe.Pointer length int32 } type node struct { value *Task next unsafe.Pointer } // NewLockFreeQueue instantiates and returns a lockFreeQueue. func NewLockFreeQueue() AsyncTaskQueue { n := unsafe.Pointer(&node{}) return &lockFreeQueue{head: n, tail: n} } // Enqueue puts the given value v at the tail of the queue. func (q *lockFreeQueue) Enqueue(task *Task) { n := &node{value: task} retry: tail := load(&q.tail) next := load(&tail.next) // Are tail and next consistent? if tail == load(&q.tail) { if next == nil { // Try to link node at the end of the linked list. if cas(&tail.next, next, n) { // enqueue is done. // Try to swing tail to the inserted node. cas(&q.tail, tail, n) atomic.AddInt32(&q.length, 1) return } } else { // tail was not pointing to the last node // Try to swing tail to the next node. cas(&q.tail, tail, next) } } goto retry } // Dequeue removes and returns the value at the head of the queue. // It returns nil if the queue is empty. func (q *lockFreeQueue) Dequeue() *Task { retry: head := load(&q.head) tail := load(&q.tail) next := load(&head.next) // Are head, tail, and next consistent? if head == load(&q.head) { // Is queue empty or tail falling behind? if head == tail { // Is queue empty? if next == nil { return nil } cas(&q.tail, tail, next) // tail is falling behind, try to advance it. } else { // Read value before CAS, otherwise another dequeue might free the next node. task := next.value if cas(&q.head, head, next) { // dequeue is done, return value. atomic.AddInt32(&q.length, -1) return task } } } goto retry } // IsEmpty indicates whether this queue is empty or not. func (q *lockFreeQueue) IsEmpty() bool { return atomic.LoadInt32(&q.length) == 0 } // Length returns the number of elements in the queue. func (q *lockFreeQueue) Length() int32 { return atomic.LoadInt32(&q.length) } func load(p *unsafe.Pointer) (n *node) { return (*node)(atomic.LoadPointer(p)) } func cas(p *unsafe.Pointer, old, new *node) bool { //nolint:revive return atomic.CompareAndSwapPointer(p, unsafe.Pointer(old), unsafe.Pointer(new)) } ================================================ FILE: pkg/queue/queue.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package queue implements a lock-free queue for asynchronous tasks. package queue import "sync" // Func is the callback function executed by poller. type Func func(any) error // Task is a wrapper that contains function and its argument. type Task struct { Exec Func Param any } var taskPool = sync.Pool{New: func() any { return new(Task) }} // GetTask gets a cached Task from pool. func GetTask() *Task { return taskPool.Get().(*Task) } // PutTask puts the trashy Task back in pool. func PutTask(task *Task) { task.Exec, task.Param = nil, nil taskPool.Put(task) } // AsyncTaskQueue is a queue storing asynchronous tasks. type AsyncTaskQueue interface { Enqueue(*Task) Dequeue() *Task IsEmpty() bool Length() int32 } // EventPriority is the priority of an event. type EventPriority int const ( // HighPriority is for the tasks expected to be executed // as soon as possible. HighPriority EventPriority = iota // LowPriority is for the tasks that won't matter much // even if they are deferred a little bit. LowPriority ) ================================================ FILE: pkg/queue/queue_test.go ================================================ package queue_test import ( "sync" "sync/atomic" "testing" "github.com/panjf2000/gnet/v2/pkg/queue" ) func TestLockFreeQueue(t *testing.T) { const taskNum = 10000 q := queue.NewLockFreeQueue() var wg sync.WaitGroup wg.Add(4) go func() { for i := 0; i < taskNum; i++ { task := &queue.Task{} q.Enqueue(task) } wg.Done() }() go func() { for i := 0; i < taskNum; i++ { task := &queue.Task{} q.Enqueue(task) } wg.Done() }() var counter int32 go func() { for { task := q.Dequeue() if task != nil { atomic.AddInt32(&counter, 1) } if task == nil && atomic.LoadInt32(&counter) == 2*taskNum { break } } wg.Done() }() go func() { for { task := q.Dequeue() if task != nil { atomic.AddInt32(&counter, 1) } if task == nil && atomic.LoadInt32(&counter) == 2*taskNum { break } } wg.Done() }() wg.Wait() t.Logf("sent and received all %d tasks", 2*taskNum) } ================================================ FILE: pkg/socket/fd_unix.go ================================================ // Copyright (c) 2020 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package socket import ( "sync/atomic" "syscall" "golang.org/x/sys/unix" ) // Dup duplicates the given fd and marks it close-on-exec. func Dup(fd int) (int, error) { return dupCloseOnExec(fd) } // tryDupCloexec indicates whether F_DUPFD_CLOEXEC should be used. // If the kernel doesn't support it, this is set to false. var tryDupCloexec atomic.Bool func init() { tryDupCloexec.Store(true) } // dupCloseOnExec duplicates the given fd and marks it close-on-exec. func dupCloseOnExec(fd int) (int, error) { if tryDupCloexec.Load() { r, err := unix.FcntlInt(uintptr(fd), unix.F_DUPFD_CLOEXEC, 0) if err == nil { return r, nil } switch err.(syscall.Errno) { case unix.EINVAL, unix.ENOSYS: // Old kernel, or js/wasm (which returns // ENOSYS). Fall back to the portable way from // now on. tryDupCloexec.Store(false) default: return -1, err } } return dupCloseOnExecOld(fd) } // dupCloseOnExecOld is the traditional way to dup an fd and // set its O_CLOEXEC bit, using two system calls. func dupCloseOnExecOld(fd int) (int, error) { syscall.ForkLock.RLock() defer syscall.ForkLock.RUnlock() newFD, err := syscall.Dup(fd) if err != nil { return -1, err } syscall.CloseOnExec(newFD) return newFD, nil } ================================================ FILE: pkg/socket/sock_bsd.go ================================================ // Copyright (c) 2020 The Gnet Authors. All rights reserved. // Copyright (c) 2017 Ma Weiwei, Max Riveiro // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || netbsd || openbsd package socket import ( "runtime" "golang.org/x/sys/unix" ) func maxListenerBacklog() int { var ( n uint32 err error ) switch runtime.GOOS { case "darwin": n, err = unix.SysctlUint32("kern.ipc.somaxconn") case "freebsd": n, err = unix.SysctlUint32("kern.ipc.soacceptqueue") } if n == 0 || err != nil { return unix.SOMAXCONN } // FreeBSD stores the backlog in a uint16, as does Linux. // Assume the other BSDs do too. Truncate number to avoid wrapping. // See issue 5030. if n > 1<<16-1 { n = 1<<16 - 1 } return int(n) } ================================================ FILE: pkg/socket/sock_cloexec.go ================================================ // Copyright (c) 2020 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build dragonfly || freebsd || linux package socket import "golang.org/x/sys/unix" func sysSocket(family, sotype, proto int) (int, error) { return unix.Socket(family, sotype|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, proto) } func sysAccept(fd int) (nfd int, sa unix.Sockaddr, err error) { return unix.Accept4(fd, unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC) } ================================================ FILE: pkg/socket/sock_linux.go ================================================ // Copyright (c) 2020 The Gnet Authors. All rights reserved. // Copyright (c) 2017 Ma Weiwei, Max Riveiro // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package socket import ( "bufio" "os" "strconv" "strings" "golang.org/x/sys/unix" ) func maxListenerBacklog() int { fd, err := os.Open("/proc/sys/net/core/somaxconn") if err != nil { return unix.SOMAXCONN } defer fd.Close() //nolint:errcheck rd := bufio.NewReader(fd) line, err := rd.ReadString('\n') if err != nil { return unix.SOMAXCONN } f := strings.Fields(line) if len(f) < 1 { return unix.SOMAXCONN } n, err := strconv.Atoi(f[0]) if err != nil || n == 0 { return unix.SOMAXCONN } // Linux stores the backlog in a uint16. // Truncate number to avoid wrapping. // See issue 5030. if n > 1<<16-1 { n = 1<<16 - 1 } return n } ================================================ FILE: pkg/socket/sock_posix.go ================================================ // Copyright (c) 2022 The Gnet Authors. All rights reserved. // Copyright 2009 The Go Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package socket import ( "net" "syscall" "golang.org/x/sys/unix" ) func ipToSockaddrInet4(ip net.IP, port int) (unix.SockaddrInet4, error) { if len(ip) == 0 { ip = net.IPv4zero } ip4 := ip.To4() if ip4 == nil { return unix.SockaddrInet4{}, &net.AddrError{Err: "non-IPv4 address", Addr: ip.String()} } sa := unix.SockaddrInet4{Port: port} copy(sa.Addr[:], ip4) return sa, nil } func ipToSockaddrInet6(ip net.IP, port int, zone string) (unix.SockaddrInet6, error) { // In general, an IP wildcard address, which is either // "0.0.0.0" or "::", means the entire IP addressing // space. For some historical reason, it is used to // specify "any available address" on some operations // of IP node. // // When the IP node supports IPv4-mapped IPv6 address, // we allow a listener to listen to the wildcard // address of both IP addressing spaces by specifying // IPv6 wildcard address. if len(ip) == 0 || ip.Equal(net.IPv4zero) { ip = net.IPv6zero } // We accept any IPv6 address including IPv4-mapped // IPv6 address. ip6 := ip.To16() if ip6 == nil { return unix.SockaddrInet6{}, &net.AddrError{Err: "non-IPv6 address", Addr: ip.String()} } sa := unix.SockaddrInet6{Port: port} copy(sa.Addr[:], ip6) iface, err := net.InterfaceByName(zone) if err != nil { return sa, nil } sa.ZoneId = uint32(iface.Index) return sa, nil } func ipToSockaddr(family int, ip net.IP, port int, zone string) (unix.Sockaddr, error) { switch family { case syscall.AF_INET: sa, err := ipToSockaddrInet4(ip, port) if err != nil { return nil, err } return &sa, nil case syscall.AF_INET6: sa, err := ipToSockaddrInet6(ip, port, zone) if err != nil { return nil, err } return &sa, nil } return nil, &net.AddrError{Err: "invalid address family", Addr: ip.String()} } ================================================ FILE: pkg/socket/sockaddr.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // Copyright (c) 2012 The Go Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // https://github.com/libp2p/go-sockaddr?tab=BSD-3-Clause-1-ov-file#readme // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package socket import ( "net" "golang.org/x/sys/unix" "github.com/panjf2000/gnet/v2/pkg/bs" bsPool "github.com/panjf2000/gnet/v2/pkg/pool/byteslice" ) // NetAddrToSockaddr converts a net.Addr to a Sockaddr. // Returns nil if the input is invalid or conversion is not possible. func NetAddrToSockaddr(addr net.Addr) unix.Sockaddr { switch addr := addr.(type) { case *net.IPAddr: return IPAddrToSockaddr(addr) case *net.TCPAddr: return TCPAddrToSockaddr(addr) case *net.UDPAddr: return UDPAddrToSockaddr(addr) case *net.UnixAddr: sa, _ := UnixAddrToSockaddr(addr) return sa default: return nil } } // IPAddrToSockaddr converts a net.IPAddr to a Sockaddr. // Returns nil if conversion fails. func IPAddrToSockaddr(addr *net.IPAddr) unix.Sockaddr { return IPToSockaddr(addr.IP, 0, addr.Zone) } // TCPAddrToSockaddr converts a net.TCPAddr to a Sockaddr. // Returns nil if conversion fails. func TCPAddrToSockaddr(addr *net.TCPAddr) unix.Sockaddr { return IPToSockaddr(addr.IP, addr.Port, addr.Zone) } // UDPAddrToSockaddr converts a net.UDPAddr to a Sockaddr. // Returns nil if conversion fails. func UDPAddrToSockaddr(addr *net.UDPAddr) unix.Sockaddr { return IPToSockaddr(addr.IP, addr.Port, addr.Zone) } // IPToSockaddr converts a net.IP (with optional IPv6 Zone) to a Sockaddr // Returns nil if conversion fails. func IPToSockaddr(ip net.IP, port int, zone string) unix.Sockaddr { // Unspecified? if ip == nil { if zone != "" { return &unix.SockaddrInet6{Port: port, ZoneId: uint32(ip6ZoneToInt(zone))} } return &unix.SockaddrInet4{Port: port} } // Valid IPv4? if ip4 := ip.To4(); ip4 != nil && zone == "" { sa := unix.SockaddrInet4{Port: port} copy(sa.Addr[:], ip4) // last 4 bytes return &sa } // Valid IPv6 address? if ip6 := ip.To16(); ip6 != nil { sa := unix.SockaddrInet6{Port: port, ZoneId: uint32(ip6ZoneToInt(zone))} copy(sa.Addr[:], ip6) return &sa } return nil } // UnixAddrToSockaddr converts a net.UnixAddr to a Sockaddr, and returns // the type (unix.SOCK_STREAM, unix.SOCK_DGRAM, unix.SOCK_SEQPACKET) // Returns (nil, 0) if conversion fails. func UnixAddrToSockaddr(addr *net.UnixAddr) (unix.Sockaddr, int) { t := 0 switch addr.Net { case "unix": t = unix.SOCK_STREAM case "unixgram": t = unix.SOCK_DGRAM case "unixpacket": t = unix.SOCK_SEQPACKET default: return nil, 0 } return &unix.SockaddrUnix{Name: addr.Name}, t } // SockaddrToTCPOrUnixAddr converts a unix.Sockaddr to a net.TCPAddr or net.UnixAddr. // Returns nil if conversion fails. func SockaddrToTCPOrUnixAddr(sa unix.Sockaddr) net.Addr { switch sa := sa.(type) { case *unix.SockaddrInet4: return &net.TCPAddr{IP: sa.Addr[0:], Port: sa.Port} case *unix.SockaddrInet6: return &net.TCPAddr{IP: sa.Addr[0:], Port: sa.Port, Zone: ip6ZoneToString(sa.ZoneId)} case *unix.SockaddrUnix: return &net.UnixAddr{Name: sa.Name, Net: "unix"} } return nil } // SockaddrToUDPAddr converts a unix.Sockaddr to a net.UDPAddr // Returns nil if conversion fails. func SockaddrToUDPAddr(sa unix.Sockaddr) net.Addr { switch sa := sa.(type) { case *unix.SockaddrInet4: return &net.UDPAddr{IP: sa.Addr[0:], Port: sa.Port} case *unix.SockaddrInet6: return &net.UDPAddr{IP: sa.Addr[0:], Port: sa.Port, Zone: ip6ZoneToString(sa.ZoneId)} } return nil } // ip6ZoneToInt converts an IP6 Zone net string to a unix int. // Returns 0 if zone is "". func ip6ZoneToInt(zone string) int { if zone == "" { return 0 } if ifi, err := net.InterfaceByName(zone); err == nil { return ifi.Index } n, _, _ := dtoi(zone, 0) return n } // ip6ZoneToString converts an IP6 Zone unix int to a net string, // Returns "" if zone is 0. func ip6ZoneToString(zone uint32) string { if zone == 0 { return "" } if ifi, err := net.InterfaceByIndex(int(zone)); err == nil { return ifi.Name } return itod(uint(zone)) } // itod converts uint to a decimal string. func itod(v uint) string { if v == 0 { // avoid string allocation return "0" } // Assemble decimal in reverse order. buf := bsPool.Get(32) i := len(buf) - 1 for ; v > 0; v /= 10 { buf[i] = byte(v%10 + '0') i-- } return bs.BytesToString(buf[i:]) } // Bigger than we need, not too big to worry about overflow. const big = 0xFFFFFF // Decimal to integer starting at &s[i0]. // Returns number, new offset, success. func dtoi(s string, i0 int) (n int, i int, ok bool) { n = 0 for i = i0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ { n = n*10 + int(s[i]-'0') if n >= big { return 0, i, false } } if i == i0 { return 0, i, false } return n, i, true } ================================================ FILE: pkg/socket/socket.go ================================================ // Copyright (c) 2020 The Gnet Authors. All rights reserved. // Copyright (c) 2017 Max Riveiro // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd // Package socket provides some handy socket-related functions. package socket import ( "net" "golang.org/x/sys/unix" ) // Option is used for setting an option on socket. type Option[T int | string] struct { SetSockOpt func(int, T) error Opt T } func execSockOpts[T int | string](fd int, opts []Option[T]) error { for _, opt := range opts { if err := opt.SetSockOpt(fd, opt.Opt); err != nil { return err } } return nil } // TCPSocket creates a TCP socket and returns a file descriptor that refers to it. // The given socket options will be set on the returned file descriptor. func TCPSocket(proto, addr string, passive bool, sockOptInts []Option[int], sockOptStrs []Option[string]) (int, net.Addr, error) { return tcpSocket(proto, addr, passive, sockOptInts, sockOptStrs) } // UDPSocket creates a UDP socket and returns a file descriptor that refers to it. // The given socket options will be set on the returned file descriptor. func UDPSocket(proto, addr string, connect bool, sockOptInts []Option[int], sockOptStrs []Option[string]) (int, net.Addr, error) { return udpSocket(proto, addr, connect, sockOptInts, sockOptStrs) } // UnixSocket creates a Unix socket and returns a file descriptor that refers to it. // The given socket options will be set on the returned file descriptor. func UnixSocket(proto, addr string, passive bool, sockOptInts []Option[int], sockOptStrs []Option[string]) (int, net.Addr, error) { return udsSocket(proto, addr, passive, sockOptInts, sockOptStrs) } // Accept accepts the next incoming socket along with setting // O_NONBLOCK and O_CLOEXEC flags on it. func Accept(fd int) (int, unix.Sockaddr, error) { return sysAccept(fd) } ================================================ FILE: pkg/socket/sockopts_bsd.go ================================================ // Copyright (c) 2024 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build dragonfly || freebsd || netbsd || openbsd package socket import errorx "github.com/panjf2000/gnet/v2/pkg/errors" // SetBindToDevice is not implemented on *BSD because there is // no equivalent of Linux's SO_BINDTODEVICE. func SetBindToDevice(_ int, _ string) error { return errorx.ErrUnsupportedOp } ================================================ FILE: pkg/socket/sockopts_darwin.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package socket import ( "errors" "os" "golang.org/x/sys/unix" errorx "github.com/panjf2000/gnet/v2/pkg/errors" ) // SetKeepAlivePeriod enables the SO_KEEPALIVE option on the socket and sets // TCP_KEEPIDLE/TCP_KEEPALIVE to the specified duration in seconds, TCP_KEEPCNT // to 5, and TCP_KEEPINTVL to secs/5. func SetKeepAlivePeriod(fd, secs int) error { if secs <= 0 { return errors.New("invalid time duration") } interval := secs / 5 if interval == 0 { interval = 1 } return SetKeepAlive(fd, true, secs, interval, 5) } // SetKeepAlive enables/disables the TCP keepalive feature on the socket. func SetKeepAlive(fd int, enabled bool, idle, intvl, cnt int) error { if enabled && (idle <= 0 || intvl <= 0 || cnt <= 0) { return errors.New("invalid time duration") } var on int if enabled { on = 1 } if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_KEEPALIVE, on); err != nil { return os.NewSyscallError("setsockopt", err) } if !enabled { // If keepalive is disabled, ignore the TCP_KEEP* options. return nil } if err := unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_KEEPALIVE, idle); err != nil { return os.NewSyscallError("setsockopt", err) } if err := unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_KEEPINTVL, intvl); err != nil { return os.NewSyscallError("setsockopt", err) } return os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_KEEPCNT, cnt)) } // SetBindToDevice is not implemented on macOS because there is // no equivalent of Linux's SO_BINDTODEVICE. func SetBindToDevice(_ int, _ string) error { return errorx.ErrUnsupportedOp } ================================================ FILE: pkg/socket/sockopts_freebsd.go ================================================ /* * Copyright (c) 2024 The Gnet Authors. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * */ package socket import ( "os" "golang.org/x/sys/unix" ) // SetReuseport enables SO_REUSEPORT_LB option on socket. func SetReuseport(fd, reusePort int) error { return os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT_LB, reusePort)) } ================================================ FILE: pkg/socket/sockopts_linux.go ================================================ // Copyright (c) 2024 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package socket import ( "os" "golang.org/x/sys/unix" ) // SetBindToDevice binds the socket to a specific network interface. // // SO_BINDTODEVICE on Linux works in both directions: only process packets // received from the particular interface along with sending them through // that interface, instead of following the default route. func SetBindToDevice(fd int, ifname string) error { return os.NewSyscallError("setsockopt", unix.BindToDevice(fd, ifname)) } ================================================ FILE: pkg/socket/sockopts_openbsd.go ================================================ // Copyright (c) 2024 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package socket import errorx "github.com/panjf2000/gnet/v2/pkg/errors" // SetKeepAlivePeriod is not implemented on OpenBSD because there are // no equivalents of Linux's TCP_KEEPIDLE, TCP_KEEPINTVL, and TCP_KEEPCNT. func SetKeepAlivePeriod(_, _ int) error { // OpenBSD has no user-settable per-socket TCP keepalive options. return errorx.ErrUnsupportedOp } // SetKeepAlive is not implemented on OpenBSD. func SetKeepAlive(_ int, _ bool, _, _, _ int) error { // OpenBSD has no user-settable per-socket TCP keepalive options. return errorx.ErrUnsupportedOp } ================================================ FILE: pkg/socket/sockopts_posix.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package socket import ( "net" "os" "syscall" "golang.org/x/sys/unix" "github.com/panjf2000/gnet/v2/pkg/errors" ) // SetNoDelay controls whether the operating system should delay // packet transmission in hopes of sending fewer packets (Nagle's algorithm). // // The default is true (no delay), meaning that data is // sent as soon as possible after a Write. func SetNoDelay(fd, noDelay int) error { return os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_NODELAY, noDelay)) } // SetRecvBuffer sets the size of the operating system's // receive buffer associated with the connection. func SetRecvBuffer(fd, size int) error { return os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_RCVBUF, size)) } // SetSendBuffer sets the size of the operating system's // transmit buffer associated with the connection. func SetSendBuffer(fd, size int) error { return os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_SNDBUF, size)) } // SetReuseAddr enables SO_REUSEADDR option on socket. func SetReuseAddr(fd, reuseAddr int) error { return os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEADDR, reuseAddr)) } // SetIPv6Only restricts a IPv6 socket to only process IPv6 requests or both IPv4 and IPv6 requests. func SetIPv6Only(fd, ipv6only int) error { return os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, unix.IPPROTO_IPV6, unix.IPV6_V6ONLY, ipv6only)) } // SetLinger sets the behavior of Close on a connection which still // has data waiting to be sent or to be acknowledged. // // If sec < 0 (the default), the operating system finishes sending the // data in the background. // // If sec == 0, the operating system discards any unsent or // unacknowledged data. // // If sec > 0, the data is sent in the background as with sec < 0. On // some operating systems after sec seconds have elapsed any remaining // unsent data may be discarded. func SetLinger(fd, sec int) error { var l unix.Linger if sec >= 0 { l.Onoff = 1 l.Linger = int32(sec) } else { l.Onoff = 0 l.Linger = 0 } return os.NewSyscallError("setsockopt", unix.SetsockoptLinger(fd, syscall.SOL_SOCKET, syscall.SO_LINGER, &l)) } // SetMulticastMembership returns with a socket option function based on the IP // version. Returns nil when multicast membership cannot be applied. func SetMulticastMembership(proto string, udpAddr *net.UDPAddr) func(int, int) error { udpVersion, err := determineUDPProto(proto, udpAddr) if err != nil { return nil } switch udpVersion { case "udp4": return func(fd int, ifIndex int) error { return SetIPv4MulticastMembership(fd, udpAddr.IP, ifIndex) } case "udp6": return func(fd int, ifIndex int) error { return SetIPv6MulticastMembership(fd, udpAddr.IP, ifIndex) } default: return nil } } // SetIPv4MulticastMembership joins fd to the specified multicast IPv4 address. // ifIndex is the index of the interface where the multicast datagrams will be // received. If ifIndex is 0 then the operating system will choose the default, // it is usually needed when the host has multiple network interfaces configured. func SetIPv4MulticastMembership(fd int, mcast net.IP, ifIndex int) error { // Multicast interfaces are selected by IP address on IPv4 (and by index on IPv6) ip, err := interfaceFirstIPv4Addr(ifIndex) if err != nil { return err } mreq := &unix.IPMreq{} copy(mreq.Multiaddr[:], mcast.To4()) copy(mreq.Interface[:], ip.To4()) if ifIndex > 0 { if err := os.NewSyscallError("setsockopt", unix.SetsockoptInet4Addr(fd, syscall.IPPROTO_IP, syscall.IP_MULTICAST_IF, mreq.Interface)); err != nil { return err } } if err := os.NewSyscallError("setsockopt", unix.SetsockoptByte(fd, syscall.IPPROTO_IP, syscall.IP_MULTICAST_LOOP, 0)); err != nil { return err } return os.NewSyscallError("setsockopt", unix.SetsockoptIPMreq(fd, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, mreq)) } // SetIPv6MulticastMembership joins fd to the specified multicast IPv6 address. // ifIndex is the index of the interface where the multicast datagrams will be // received. If ifIndex is 0 then the operating system will choose the default, // it is usually needed when the host has multiple network interfaces configured. func SetIPv6MulticastMembership(fd int, mcast net.IP, ifIndex int) error { mreq := &unix.IPv6Mreq{} mreq.Interface = uint32(ifIndex) copy(mreq.Multiaddr[:], mcast.To16()) if ifIndex > 0 { if err := os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, syscall.IPPROTO_IPV6, syscall.IPV6_MULTICAST_IF, ifIndex)); err != nil { return err } } if err := os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, syscall.IPPROTO_IPV6, syscall.IPV6_MULTICAST_LOOP, 0)); err != nil { return err } return os.NewSyscallError("setsockopt", unix.SetsockoptIPv6Mreq(fd, syscall.IPPROTO_IPV6, syscall.IPV6_JOIN_GROUP, mreq)) } // interfaceFirstIPv4Addr returns the first IPv4 address of the interface. func interfaceFirstIPv4Addr(ifIndex int) (net.IP, error) { if ifIndex == 0 { return net.IP([]byte{0, 0, 0, 0}), nil } iface, err := net.InterfaceByIndex(ifIndex) if err != nil { return nil, err } addrs, err := iface.Addrs() if err != nil { return nil, err } for _, addr := range addrs { ip, _, err := net.ParseCIDR(addr.String()) if err != nil { return nil, err } if ip.To4() != nil { return ip, nil } } return nil, errors.ErrNoIPv4AddressOnInterface } ================================================ FILE: pkg/socket/sockopts_unix.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build dragonfly || freebsd || linux || netbsd package socket import ( "errors" "os" "golang.org/x/sys/unix" ) // SetKeepAlivePeriod enables the SO_KEEPALIVE option on the socket and sets // TCP_KEEPIDLE/TCP_KEEPALIVE to the specified duration in seconds, TCP_KEEPCNT // to 5, and TCP_KEEPINTVL to secs/5. func SetKeepAlivePeriod(fd, secs int) error { if secs <= 0 { return errors.New("invalid time duration") } interval := secs / 5 if interval == 0 { interval = 1 } return SetKeepAlive(fd, true, secs, interval, 5) } // SetKeepAlive enables/disables the TCP keepalive feature on the socket. func SetKeepAlive(fd int, enabled bool, idle, intvl, cnt int) error { if enabled && (idle <= 0 || intvl <= 0 || cnt <= 0) { return errors.New("invalid time duration") } var on int if enabled { on = 1 } if err := unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_KEEPALIVE, on); err != nil { return os.NewSyscallError("setsockopt", err) } if !enabled { // If keepalive is disabled, ignore the TCP_KEEP* options. return nil } if err := unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_KEEPIDLE, idle); err != nil { return os.NewSyscallError("setsockopt", err) } if err := unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_KEEPINTVL, intvl); err != nil { return os.NewSyscallError("setsockopt", err) } return os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, unix.IPPROTO_TCP, unix.TCP_KEEPCNT, cnt)) } ================================================ FILE: pkg/socket/sockopts_unix1.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || linux || netbsd || openbsd package socket import ( "os" "golang.org/x/sys/unix" ) // SetReuseport enables SO_REUSEPORT option on socket. func SetReuseport(fd, reusePort int) error { return os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_REUSEPORT, reusePort)) } ================================================ FILE: pkg/socket/sys_cloexec.go ================================================ // Copyright (c) 2020 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || netbsd || openbsd package socket import ( "syscall" "golang.org/x/sys/unix" ) func sysSocket(family, sotype, proto int) (fd int, err error) { syscall.ForkLock.RLock() if fd, err = unix.Socket(family, sotype, proto); err == nil { unix.CloseOnExec(fd) } syscall.ForkLock.RUnlock() if err != nil { return } if err = unix.SetNonblock(fd, true); err != nil { _ = unix.Close(fd) } return } func sysAccept(fd int) (nfd int, sa unix.Sockaddr, err error) { syscall.ForkLock.RLock() if nfd, sa, err = unix.Accept(fd); err == nil { unix.CloseOnExec(nfd) } syscall.ForkLock.RUnlock() if err != nil { return } if err = unix.SetNonblock(nfd, true); err != nil { _ = unix.Close(nfd) } return } ================================================ FILE: pkg/socket/tcp_socket.go ================================================ // Copyright (c) 2020 The Gnet Authors. All rights reserved. // Copyright (c) 2017 Max Riveiro // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package socket import ( "errors" "net" "os" "golang.org/x/sys/unix" errorx "github.com/panjf2000/gnet/v2/pkg/errors" ) var listenerBacklogMaxSize = maxListenerBacklog() // GetTCPSockAddr the structured addresses based on the protocol and raw address. func GetTCPSockAddr(proto, addr string) (sa unix.Sockaddr, family int, tcpAddr *net.TCPAddr, ipv6only bool, err error) { var tcpVersion string tcpAddr, err = net.ResolveTCPAddr(proto, addr) if err != nil { return } tcpVersion, err = determineTCPProto(proto, tcpAddr) if err != nil { return } switch tcpVersion { case "tcp4": family = unix.AF_INET sa, err = ipToSockaddr(family, tcpAddr.IP, tcpAddr.Port, "") case "tcp6": ipv6only = true fallthrough case "tcp": family = unix.AF_INET6 sa, err = ipToSockaddr(family, tcpAddr.IP, tcpAddr.Port, tcpAddr.Zone) default: err = errorx.ErrUnsupportedProtocol } return } func determineTCPProto(proto string, addr *net.TCPAddr) (string, error) { // If the protocol is set to "tcp", we try to determine the actual protocol // version from the size of the resolved IP address. Otherwise, we simple use // the protocol given to us by the caller. if addr.IP.To4() != nil { return "tcp4", nil } if addr.IP.To16() != nil { return "tcp6", nil } switch proto { case "tcp", "tcp4", "tcp6": return proto, nil } return "", errorx.ErrUnsupportedTCPProtocol } // tcpSocket creates an endpoint for communication and returns a file descriptor that refers to that endpoint. func tcpSocket(proto, addr string, passive bool, sockOptInts []Option[int], sockOptStrs []Option[string]) (fd int, netAddr net.Addr, err error) { var ( family int ipv6only bool sa unix.Sockaddr ) if sa, family, netAddr, ipv6only, err = GetTCPSockAddr(proto, addr); err != nil { return } if fd, err = sysSocket(family, unix.SOCK_STREAM, unix.IPPROTO_TCP); err != nil { err = os.NewSyscallError("socket", err) return } defer func() { if err != nil { // Ignore EINPROGRESS for non-blocking socket connect, should be processed by caller if errors.Is(err, unix.EINPROGRESS) { return } _ = unix.Close(fd) } }() if family == unix.AF_INET6 && ipv6only { if err = SetIPv6Only(fd, 1); err != nil { return } } if err = execSockOpts(fd, sockOptInts); err != nil { return } if err = execSockOpts(fd, sockOptStrs); err != nil { return } if passive { if err = os.NewSyscallError("bind", unix.Bind(fd, sa)); err != nil { return } // Set backlog size to the maximum. err = os.NewSyscallError("listen", unix.Listen(fd, listenerBacklogMaxSize)) } else { err = os.NewSyscallError("connect", unix.Connect(fd, sa)) } return } ================================================ FILE: pkg/socket/udp_socket.go ================================================ // Copyright (c) 2020 The Gnet Authors. All rights reserved. // Copyright (c) 2017 Max Riveiro // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package socket import ( "errors" "net" "os" "golang.org/x/sys/unix" errorx "github.com/panjf2000/gnet/v2/pkg/errors" ) // GetUDPSockAddr the structured addresses based on the protocol and raw address. func GetUDPSockAddr(proto, addr string) (sa unix.Sockaddr, family int, udpAddr *net.UDPAddr, ipv6only bool, err error) { var udpVersion string udpAddr, err = net.ResolveUDPAddr(proto, addr) if err != nil { return } udpVersion, err = determineUDPProto(proto, udpAddr) if err != nil { return } switch udpVersion { case "udp4": family = unix.AF_INET sa, err = ipToSockaddr(family, udpAddr.IP, udpAddr.Port, "") case "udp6": ipv6only = true fallthrough case "udp": family = unix.AF_INET6 sa, err = ipToSockaddr(family, udpAddr.IP, udpAddr.Port, udpAddr.Zone) default: err = errorx.ErrUnsupportedProtocol } return } func determineUDPProto(proto string, addr *net.UDPAddr) (string, error) { // If the protocol is set to "udp", we try to determine the actual protocol // version from the size of the resolved IP address. Otherwise, we simple use // the protocol given to us by the caller. if addr.IP.To4() != nil { return "udp4", nil } if addr.IP.To16() != nil { return "udp6", nil } switch proto { case "udp", "udp4", "udp6": return proto, nil } return "", errorx.ErrUnsupportedUDPProtocol } // udpSocket creates an endpoint for communication and returns a file descriptor that refers to that endpoint. func udpSocket(proto, addr string, connect bool, sockOptInts []Option[int], sockOptStrs []Option[string]) (fd int, netAddr net.Addr, err error) { var ( family int ipv6only bool sa unix.Sockaddr ) if sa, family, netAddr, ipv6only, err = GetUDPSockAddr(proto, addr); err != nil { return } if fd, err = sysSocket(family, unix.SOCK_DGRAM, unix.IPPROTO_UDP); err != nil { err = os.NewSyscallError("socket", err) return } defer func() { if err != nil { // Ignore EINPROGRESS for non-blocking socket connect, should be processed by caller if errors.Is(err, unix.EINPROGRESS) { return } _ = unix.Close(fd) } }() if family == unix.AF_INET6 && ipv6only { if err = SetIPv6Only(fd, 1); err != nil { return } } // Allow broadcast. if err = os.NewSyscallError("setsockopt", unix.SetsockoptInt(fd, unix.SOL_SOCKET, unix.SO_BROADCAST, 1)); err != nil { return } if err = execSockOpts(fd, sockOptInts); err != nil { return } if err = execSockOpts(fd, sockOptStrs); err != nil { return } if connect { err = os.NewSyscallError("connect", unix.Connect(fd, sa)) } else { err = os.NewSyscallError("bind", unix.Bind(fd, sa)) } return } ================================================ FILE: pkg/socket/unix_socket.go ================================================ // Copyright (c) 2020 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build darwin || dragonfly || freebsd || linux || netbsd || openbsd package socket import ( "errors" "net" "os" "golang.org/x/sys/unix" errorx "github.com/panjf2000/gnet/v2/pkg/errors" ) // GetUnixSockAddr the structured addresses based on the protocol and raw address. func GetUnixSockAddr(proto, addr string) (sa unix.Sockaddr, family int, unixAddr *net.UnixAddr, err error) { unixAddr, err = net.ResolveUnixAddr(proto, addr) if err != nil { return } switch unixAddr.Network() { case "unix": sa, family = &unix.SockaddrUnix{Name: unixAddr.Name}, unix.AF_UNIX default: err = errorx.ErrUnsupportedUDSProtocol } return } // udsSocket creates an endpoint for communication and returns a file descriptor that refers to that endpoint. func udsSocket(proto, addr string, passive bool, sockOptInts []Option[int], sockOptStrs []Option[string]) (fd int, netAddr net.Addr, err error) { var ( family int sa unix.Sockaddr ) if sa, family, netAddr, err = GetUnixSockAddr(proto, addr); err != nil { return } if fd, err = sysSocket(family, unix.SOCK_STREAM, 0); err != nil { err = os.NewSyscallError("socket", err) return } defer func() { if err != nil { // Ignore EINPROGRESS for non-blocking socket connect, should be processed by caller // though there is less situation for EINPROGRESS when using unix socket if errors.Is(err, unix.EINPROGRESS) { return } _ = unix.Close(fd) } }() if err = execSockOpts(fd, sockOptInts); err != nil { return } if err = execSockOpts(fd, sockOptStrs); err != nil { return } if passive { if err = os.NewSyscallError("bind", unix.Bind(fd, sa)); err != nil { return } // Set backlog size to the maximum. err = os.NewSyscallError("listen", unix.Listen(fd, listenerBacklogMaxSize)) } else { err = os.NewSyscallError("connect", unix.Connect(fd, sa)) } return } ================================================ FILE: reactor_default.go ================================================ // Copyright (c) 2019 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build (darwin || dragonfly || freebsd || linux || netbsd || openbsd) && !poll_opt package gnet import ( "errors" "runtime" errorx "github.com/panjf2000/gnet/v2/pkg/errors" "github.com/panjf2000/gnet/v2/pkg/netpoll" ) func (el *eventloop) rotate() error { if el.engine.opts.LockOSThread { runtime.LockOSThread() defer runtime.UnlockOSThread() } err := el.poller.Polling(el.accept0) if errors.Is(err, errorx.ErrEngineShutdown) { el.getLogger().Debugf("main reactor is exiting in terms of the demand from user, %v", err) err = nil } else if err != nil { el.getLogger().Errorf("main reactor is exiting due to error: %v", err) } el.engine.shutdown(err) return err } func (el *eventloop) orbit() error { if el.engine.opts.LockOSThread { runtime.LockOSThread() defer runtime.UnlockOSThread() } err := el.poller.Polling(func(fd int, ev netpoll.IOEvent, flags netpoll.IOFlags) error { c := el.connections.getConn(fd) if c == nil { // For kqueue, this might happen when the connection has already been closed, // the file descriptor will be deleted from kqueue automatically as documented // in the manual pages. // For epoll, it somehow notified with an event for a stale fd that is not in // our connection set. We need to explicitly delete it from the epoll set. // Also print a warning log for this kind of irregularity. el.getLogger().Warnf("received event[fd=%d|ev=%d|flags=%d] of a stale connection from event-loop(%d)", fd, ev, flags, el.idx) return el.poller.Delete(fd) } return c.processIO(fd, ev, flags) }) if errors.Is(err, errorx.ErrEngineShutdown) { el.getLogger().Debugf("event-loop(%d) is exiting in terms of the demand from user, %v", el.idx, err) err = nil } else if err != nil { el.getLogger().Errorf("event-loop(%d) is exiting due to error: %v", el.idx, err) } el.closeConns() el.engine.shutdown(err) return err } func (el *eventloop) run() error { if el.engine.opts.LockOSThread { runtime.LockOSThread() defer runtime.UnlockOSThread() } err := el.poller.Polling(func(fd int, ev netpoll.IOEvent, flags netpoll.IOFlags) error { c := el.connections.getConn(fd) if c == nil { if _, ok := el.listeners[fd]; ok { return el.accept(fd, ev, flags) } // For kqueue, this might happen when the connection has already been closed, // the file descriptor will be deleted from kqueue automatically as documented // in the manual pages. // For epoll, it somehow notified with an event for a stale fd that is not in // our connection set. We need to explicitly delete it from the epoll set. // Also print a warning log for this kind of irregularity. el.getLogger().Warnf("received event[fd=%d|ev=%d|flags=%d] of a stale connection from event-loop(%d)", fd, ev, flags, el.idx) return el.poller.Delete(fd) } return c.processIO(fd, ev, flags) }) if errors.Is(err, errorx.ErrEngineShutdown) { el.getLogger().Debugf("event-loop(%d) is exiting in terms of the demand from user, %v", el.idx, err) err = nil } else if err != nil { el.getLogger().Errorf("event-loop(%d) is exiting due to error: %v", el.idx, err) } el.closeConns() el.engine.shutdown(err) return err } ================================================ FILE: reactor_ultimate.go ================================================ // Copyright (c) 2021 The Gnet Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build (darwin || dragonfly || freebsd || linux || netbsd || openbsd) && poll_opt package gnet import ( "errors" "runtime" errorx "github.com/panjf2000/gnet/v2/pkg/errors" ) func (el *eventloop) rotate() error { if el.engine.opts.LockOSThread { runtime.LockOSThread() defer runtime.UnlockOSThread() } err := el.poller.Polling() if errors.Is(err, errorx.ErrEngineShutdown) { el.getLogger().Debugf("main reactor is exiting in terms of the demand from user, %v", err) err = nil } else if err != nil { el.getLogger().Errorf("main reactor is exiting due to error: %v", err) } el.engine.shutdown(err) return err } func (el *eventloop) orbit() error { if el.engine.opts.LockOSThread { runtime.LockOSThread() defer runtime.UnlockOSThread() } err := el.poller.Polling() if errors.Is(err, errorx.ErrEngineShutdown) { el.getLogger().Debugf("event-loop(%d) is exiting in terms of the demand from user, %v", el.idx, err) err = nil } else if err != nil { el.getLogger().Errorf("event-loop(%d) is exiting due to error: %v", el.idx, err) } el.closeConns() el.engine.shutdown(err) return err } func (el *eventloop) run() error { if el.engine.opts.LockOSThread { runtime.LockOSThread() defer runtime.UnlockOSThread() } err := el.poller.Polling() if errors.Is(err, errorx.ErrEngineShutdown) { el.getLogger().Debugf("event-loop(%d) is exiting in terms of the demand from user, %v", el.idx, err) err = nil } else if err != nil { el.getLogger().Errorf("event-loop(%d) is exiting due to error: %v", el.idx, err) } el.closeConns() el.engine.shutdown(err) return err }