├── .github └── workflows │ └── go.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── cicd ├── helm │ ├── .helmignore │ ├── Chart.yaml │ ├── templates │ │ ├── NOTES.txt │ │ ├── _helpers.tpl │ │ ├── deployment.yaml │ │ ├── hpa.yaml │ │ ├── ingress.yaml │ │ ├── service.yaml │ │ ├── serviceaccount.yaml │ │ └── tests │ │ │ └── test-connection.yaml │ └── values.yaml └── tekton │ ├── pipeline.yml │ ├── run.yml │ └── trigger.yml ├── cmd ├── api │ └── main.go ├── console │ ├── README.md │ ├── conf │ │ ├── conf.d │ │ │ └── config.toml │ │ ├── conf.go │ │ └── conf.toml │ └── main.go └── gateway │ └── main.go ├── config ├── api │ ├── casbin │ │ ├── rbac_model.conf │ │ ├── rbac_policy.csv │ │ ├── rbac_with_deny_model.conf │ │ └── rbac_with_deny_policy.csv │ ├── conf.toml │ └── config.toml └── gateway │ ├── conf.yaml │ ├── db.yaml │ ├── log.yaml │ ├── pyroscope.yaml │ └── tracing.yaml ├── doc └── img │ ├── architecture.jpg │ └── gateway_model.jpg ├── gateway ├── Dockerfile └── README.md ├── go.mod ├── go.sum ├── pkg ├── api │ ├── Dockerfile │ ├── README.md │ ├── access │ │ ├── README.md │ │ ├── access.go │ │ └── proto │ │ │ ├── define.pb.go │ │ │ └── define.proto │ ├── auth │ │ ├── README.md │ │ ├── access.go │ │ └── auth.go │ ├── client │ │ ├── README.md │ │ ├── auth │ │ │ ├── auth.go │ │ │ ├── auth_test.go │ │ │ └── proto │ │ │ │ ├── auth.pb.go │ │ │ │ └── auth.proto │ │ ├── client.go │ │ ├── proto │ │ │ ├── define.pb.go │ │ │ └── define.proto │ │ └── register │ │ │ ├── proto │ │ │ ├── register.pb.go │ │ │ └── register.proto │ │ │ ├── register.go │ │ │ └── register_test.go │ ├── cluster │ │ ├── clients.go │ │ ├── cluster.go │ │ ├── proto │ │ │ ├── clients │ │ │ │ ├── clients.pb.go │ │ │ │ └── clients.proto │ │ │ ├── define.pb.go │ │ │ ├── define.proto │ │ │ ├── sessions │ │ │ │ ├── sessions.pb.go │ │ │ │ └── sessions.proto │ │ │ ├── subscriptions │ │ │ │ ├── subscriptions.pb.go │ │ │ │ └── subscriptions.proto │ │ │ └── topics │ │ │ │ ├── topics.pb.go │ │ │ │ └── topics.proto │ │ ├── sessions.go │ │ ├── subscriptions.go │ │ └── topics.go │ ├── conf │ │ └── conf.go │ └── proto │ │ ├── define.pb.go │ │ └── define.proto ├── auth │ ├── authenticator.go │ ├── mock.go │ ├── mock_test.go │ └── rpc.go ├── broker │ ├── README.md │ ├── broker.go │ ├── codec │ │ ├── codec.go │ │ ├── json │ │ │ └── json.go │ │ └── noop │ │ │ └── noop.go │ ├── kafka │ │ └── kafka.go │ ├── mock.go │ ├── mock │ │ └── mock.go │ ├── mock_test.go │ ├── options.go │ └── rabbitmq │ │ ├── channel.go │ │ ├── connection.go │ │ ├── connection_test.go │ │ ├── options.go │ │ └── rabbitmq.go ├── conf │ └── conf.go ├── gopool │ └── pool.go ├── log │ ├── default_logger.go │ └── logger.go ├── service │ ├── misc.go │ ├── options.go │ ├── server.go │ ├── service.go │ └── websocket.go ├── sessions │ ├── ackqueue.go │ ├── ackqueue_test.go │ ├── serializer.go │ ├── session.go │ ├── session_test.go │ ├── sessions.go │ ├── store.go │ └── store │ │ ├── mock.go │ │ └── redis.go ├── topics │ ├── memtopics.go │ ├── memtopics_test.go │ └── topics.go └── util │ ├── conv │ ├── conversion.go │ └── rpcx.go │ └── crypt │ └── crypt.go └── test └── benchmark └── client_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | lint: 5 | name: Lint 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Set up Go 1.15 9 | uses: actions/setup-go@v1 10 | with: 11 | go-version: 1.15 12 | id: go 13 | - name: Code 14 | uses: actions/checkout@v1 15 | - name: Intsall Golangci-lint 16 | run: curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b . latest 17 | - name: Lint 18 | run: ./golangci-lint run ./... 19 | 20 | test: 21 | #needs: Lint 22 | name: Unit Testing 23 | runs-on: ${{ matrix.os }} 24 | strategy: 25 | matrix: 26 | os: [macOS-latest,ubuntu-latest] 27 | steps: 28 | - name: Set up Go 1.15 29 | uses: actions/setup-go@v1 30 | with: 31 | go-version: 1.15 32 | id: go 33 | - name: Code 34 | uses: actions/checkout@v1 35 | - name: Go Get dependencies 36 | run: go get -v -t -d ./... 37 | - name: Go Test 38 | run: go test -race -cover -v ./... 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .idea 15 | vendor 16 | dist/ 17 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 10m 3 | skip-dirs: 4 | - protoc-gen-hb-grpc-gateway 5 | linters: 6 | disable-all: false 7 | enable-all: false 8 | enable: 9 | - megacheck 10 | - staticcheck 11 | - varcheck 12 | - gosimple 13 | - prealloc 14 | - scopelint 15 | - goimports 16 | - unconvert 17 | - govet 18 | - nakedret 19 | - structcheck 20 | - gosec 21 | - maligned 22 | - interfacer 23 | - typecheck 24 | - dupl 25 | disable: 26 | - gocritic 27 | - unused 28 | - deadcode 29 | - errcheck 30 | 31 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example .goreleaser.yml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | project_name: "gmqtt" 4 | env: 5 | - VERSION=0.0.1 6 | before: 7 | hooks: 8 | # You may remove this if you don't use go modules. 9 | - go mod tidy 10 | # you may remove this if you don't need go generate 11 | - go generate ./... 12 | builds: 13 | - # ID of the build. 14 | # Defaults to the project name. 15 | id: "gmqtt-gateway" 16 | 17 | # Path to project's (sub)directory containing Go code. 18 | # This is the working directory for the Go build command(s). 19 | # Default is `.`. 20 | dir: ./cmd/gateway 21 | 22 | # Path to main.go file or main package. 23 | # Default is `.`. 24 | main: . 25 | 26 | # Binary name. 27 | # Can be a path (e.g. `bin/app`) to wrap the binary in a directory. 28 | # Default is the name of the project directory. 29 | binary: gateway 30 | 31 | # Custom flags templates. 32 | # Default is empty. 33 | flags: -a -installsuffix cgo 34 | 35 | # Custom ldflags templates. 36 | # Default is `-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser`. 37 | ldflags: 38 | - -s -w -X main.version={{ if .IsSnapshot }}{{ .Version }}{{ else }}{{ .Env.VERSION }}{{ end }} -X main.commit={{ .ShortCommit }} -X main.date={{ .Date }} -X main.builtBy=hbchen.com 39 | 40 | # Custom environment variables to be set during the builds. 41 | # Default is empty. 42 | env: 43 | # Oracle exporter need cgo 44 | - CGO_ENABLED=0 45 | 46 | # GOOS list to build for. 47 | # For more info refer to: https://golang.org/doc/install/source#environment 48 | # Defaults are darwin and linux. 49 | goos: 50 | - linux 51 | - darwin 52 | #- windows 53 | 54 | # GOARCH to build for. 55 | # For more info refer to: https://golang.org/doc/install/source#environment 56 | # Defaults are 386 and amd64. 57 | goarch: 58 | - amd64 59 | #- arm64 60 | archives: 61 | - # ID of this archive. 62 | # Defaults to `default`. 63 | id: gmqtt 64 | replacements: 65 | darwin: Darwin 66 | linux: Linux 67 | windows: Windows 68 | 386: i386 69 | amd64: x86_64 70 | 71 | # Archive name template. 72 | # Defaults: 73 | # - if format is `tar.gz`, `tar.xz`, `gz` or `zip`: 74 | # - `{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}` 75 | # - if format is `binary`: 76 | # - `{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}` 77 | name_template: "{{ .Binary }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}{{ if .Mips }}_{{ .Mips }}{{ end }}" 78 | format: tar.gz 79 | files: 80 | - config/gateway/* 81 | 82 | # Set to true, if you want all files in the archive to be in a single directory. 83 | # If set to true and you extract the archive 'goreleaser_Linux_arm64.tar.gz', 84 | # you get a folder 'goreleaser_Linux_arm64'. 85 | # If set to false, all files are extracted separately. 86 | # You can also set it to a custom folder name (templating is supported). 87 | # Default is false. 88 | wrap_in_directory: true 89 | 90 | # Disables the binary count check. 91 | # Default: false 92 | allow_different_binary_count: true 93 | checksum: 94 | name_template: 'checksums.txt' 95 | snapshot: 96 | # Allows you to change the name of the generated snapshot 97 | # 98 | # Note that some pipes require this to be semantic version compliant (nfpm, 99 | # for example). 100 | # 101 | # Default is `{{ .Tag }}-SNAPSHOT-{{.ShortCommit}}`. 102 | name_template: '{{ .Env.VERSION }}-SNAPSHOT-{{ .ShortCommit }}' 103 | changelog: 104 | sort: asc 105 | filters: 106 | exclude: 107 | - '^docs:' 108 | - '^test:' 109 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Hobo Go 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOPATH:=$(shell go env GOPATH) 2 | 3 | .PHONY: test 4 | test: 5 | go test -race -cover -v ./... 6 | 7 | .PHONY: run 8 | run: 9 | test -n "$(cmd)" 10 | go run cmd/$(cmd)/main.go --config_patterns ./config/$(cmd)/*.yaml 11 | 12 | .PHONY: gateway 13 | gateway: 14 | CGO_ENABLED=0 GOARCH=amd64 go build -a -installsuffix cgo -ldflags '-w' -o ./bin/gateway cmd/gateway/main.go 15 | 16 | .PHONY: console 17 | console: 18 | CGO_ENABLED=0 GOARCH=amd64 go build -a -installsuffix cgo -ldflags '-w' -o ./bin/console cmd/console/main.go 19 | 20 | .PHONY: api 21 | api: 22 | CGO_ENABLED=0 GOARCH=amd64 go build -a -installsuffix cgo -ldflags '-w' -o ./bin/api cmd/api/main.go 23 | 24 | .PHONY: docker 25 | docker: build 26 | docker build . -t $(tag) 27 | 28 | .PHONY: release 29 | release: 30 | goreleaser release --config .goreleaser.yml --skip-validate --skip-publish --rm-dist 31 | 32 | .PHONY: snapshot 33 | snapshot: 34 | goreleaser release --config .goreleaser.yml --skip-publish --snapshot --rm-dist 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Micro MQ 2 | 以微服务+MQ构建支持高并发连接的分布式消息服务系统 3 | 4 | > [消息持久化及写放大模式设计](https://github.com/hb-chen/gmqtt/issues/1#issuecomment-518517597) 5 | 6 | ![micro-mq](/doc/img/architecture.jpg "micro-mq") 7 | 8 | - Gateway节点`Node`通过订阅MQ消息的方式,完成消息在节点间的转发 9 | - 根据业务场景的需求,需要考虑Node节点消息消费与生产速度的匹配 10 | - Client间的pub/sub关系比较多,如`n`个node, 发送`m`条消息/topic/node,节点消费的需求是`n`*`m`条/topic 11 | - Client端多为pub操作,而系统下发消息较少的情况,节点消费需求则比较低 12 | 13 | Gateway编程模型 14 | 15 | ![micro-mq](/doc/img/gateway_model.jpg "gateway_modem") 16 | ### Features 17 | 18 | ## 运行 19 | #### 服务依赖 20 | > 根据配置选择 conf/conf.toml 21 | - MQ `conf.broker` 22 | - [x] Kafka 23 | - 服务注册与发现 `conf.auth` 24 | - [x] Etcd 25 | - [ ] Consul 26 | 27 | #### 启动服务 28 | ```bash 29 | # 启动Gateway,[-h]帮助查看可选参数 30 | $ cd gateway 31 | $ go run -tags "consul" main.go 32 | 33 | # RPC Auth服务,[-h]帮助查看可选参数 34 | $ cd api 35 | $ go run -tags "consul" main.go 36 | ``` 37 | 38 | #### 在线演示 39 | - [MQTT Web Client](http://mqtt-client.hbchen.com/) 40 | - [Github源码](https://github.com/hb-chen/hivemq-mqtt-web-client) 41 | - Gateway 测试服务 42 | - `host`=`gmqtt.k8s.hbchen.com` 43 | - `port`=`80` 44 | 45 | ## 组件 46 | - gateway 47 | - sessions 48 | - [x] mock 49 | - [x] redis 50 | - topic 51 | - [x] mem 52 | - client auth 53 | - [x] mock 54 | - [x] rpc 55 | - pub/sub auth 56 | - [ ] mock 57 | - [ ] rpc 58 | - broker 59 | - [x] kafka 60 | 61 | - > Developing 62 | - api 63 | - auth 64 | - [ ] RPC服务间的访问控制:RBAC 65 | - client 66 | - auth 67 | - [x] client auth 68 | - [ ] pub/sub auth 69 | - register 70 | - [ ] register 71 | - [ ] unregister 72 | - cluster 73 | - nodes 74 | - 节点列表 75 | - clients 76 | - 终端列表 77 | - sessions 78 | - 会话列表 79 | - topics 80 | - 话题信息 81 | - subscriptions 82 | - 订阅信息 83 | - console 84 | - deploy 85 | 86 | ## Frameworks 87 | - [rpcx](https://github.com/smallnest/rpcx) 88 | - [Echo](https://github.com/labstack/echo) 89 | 90 | ## Benchmark 91 | - MBP开发环境 92 | - mock broker 93 | - pub timeout 3000 ms, size 64 94 | ```bash 95 | $ go test -run=TestClient$ 96 | # 1 client pub, sent 100000 messages, no sub 97 | # qos 0 98 | Total sent 100000 messages dropped 0 in 3096.074512 ms, 0.030961 ms/msg, 32298 msgs/sec 99 | # qos 1 100 | Total sent 100000 messages dropped 0 in 10411.318733 ms, 0.104113 ms/msg, 9604 msgs/sec 101 | ``` 102 | ```bash 103 | $ go test -run=TestClients$ 104 | # 1000 clients, sent 1000 messages/client, no sub 105 | # qos 0 106 | Total Sent 1000000 messages dropped 0 in 14628.666153 ms, 0.014629 ms/msg, 68358 msgs/sec 107 | # qos 1 108 | Total Sent 1000000 messages dropped 0 in 38669.812430 ms, 0.038670 ms/msg, 25859 msgs/sec 109 | 110 | # 1000 clients, sent 1000 messages/client, 1 client sub 111 | # qos 1 112 | Total Sent 1000000 messages dropped 0 in 65403.199238 ms, 0.065403 ms/msg, 15289 msgs/sec 113 | Total Received 1000000 messages in 65403.199238 ms, 0.065403 ms/msg, 15289 msgs/sec 114 | # qos 2 115 | Total Sent 1000000 messages dropped 0 in 68339.624216 ms, 0.068340 ms/msg, 14632 msgs/sec 116 | Total Received 1000000 messages in 68339.624216 ms, 0.068340 ms/msg, 14632 msgs/sec 117 | ``` 118 | 119 | ## 备用命令 120 | #### Protobuf 121 | ```bash 122 | # 在.proto有import时注意相对的路径 123 | protoc -I=$GOPATH/src:. --go_out=. api/proto/define.proto 124 | ``` 125 | #### Kafka 126 | ```bash 127 | # Start the server 128 | $ bin/zookeeper-server-start.sh config/zookeeper.properties 129 | $ bin/kafka-server-start.sh config/server.properties 130 | 131 | # List topic 132 | bin/kafka-topics.sh --list --zookeeper localhost:2181 133 | 134 | # Start a consumer 135 | bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 --topic topic --from-beginning 136 | ``` 137 | #### Kafka Manager 138 | ```bash 139 | # 启动报错,需修改conf/application.conf 140 | kafka-manager.zkhosts="localhost:2181" 141 | ``` 142 | 143 | ## 参考内容 144 | - [[转][译]百万级WebSockets和Go语言](https://colobu.com/2017/12/13/A-Million-WebSockets-and-Go/) 145 | - [[原]A Million WebSockets and Go](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) 146 | - [SurgeMQ](https://github.com/surgemq/surgemq) 147 | - [Micro](http://github.com/micro) 148 | - [Managing IoT devices with Kafka and MQTT](https://www.ibm.com/blogs/bluemix/2017/01/managing-iot-devices-with-kafka-and-mqtt/) 149 | - [MQTT QoS(服务质量)介绍](https://www.emqx.cn/blog/introduction-to-mqtt-qos) 150 | - [StreamNative 宣布开源 MoP:Apache Pulsar 支持原生 MQTT 协议](https://segmentfault.com/a/1190000025167243) 151 | -------------------------------------------------------------------------------- /cicd/helm/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *.orig 18 | *~ 19 | # Various IDEs 20 | .project 21 | .idea/ 22 | *.tmproj 23 | .vscode/ 24 | -------------------------------------------------------------------------------- /cicd/helm/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: gmqtt 3 | description: A Helm chart for Kubernetes 4 | 5 | # A chart can be either an 'application' or a 'library' chart. 6 | # 7 | # Application charts are a collection of templates that can be packaged into versioned archives 8 | # to be deployed. 9 | # 10 | # Library charts provide useful utilities or functions for the chart developer. They're included as 11 | # a dependency of application charts to inject those utilities and functions into the rendering 12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed. 13 | type: application 14 | 15 | # This is the chart version. This version number should be incremented each time you make changes 16 | # to the chart and its templates, including the app version. 17 | # Versions are expected to follow Semantic Versioning (https://semver.org/) 18 | version: 0.1.0 19 | 20 | # This is the version number of the application being deployed. This version number should be 21 | # incremented each time you make changes to the application. Versions are not expected to 22 | # follow Semantic Versioning. They should reflect the version the application is using. 23 | appVersion: 1.16.0 24 | -------------------------------------------------------------------------------- /cicd/helm/templates/NOTES.txt: -------------------------------------------------------------------------------- 1 | 1. Get the application URL by running these commands: 2 | {{- if .Values.ingress.enabled }} 3 | {{- range $host := .Values.ingress.hosts }} 4 | {{- range .paths }} 5 | http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} 6 | {{- end }} 7 | {{- end }} 8 | {{- else if contains "NodePort" .Values.service.type }} 9 | export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "gmqtt.fullname" . }}) 10 | export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") 11 | echo http://$NODE_IP:$NODE_PORT 12 | {{- else if contains "LoadBalancer" .Values.service.type }} 13 | NOTE: It may take a few minutes for the LoadBalancer IP to be available. 14 | You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "gmqtt.fullname" . }}' 15 | export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "gmqtt.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") 16 | echo http://$SERVICE_IP:{{ .Values.service.port }} 17 | {{- else if contains "ClusterIP" .Values.service.type }} 18 | export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "gmqtt.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") 19 | export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") 20 | echo "Visit http://127.0.0.1:8080 to use your application" 21 | kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT 22 | {{- end }} 23 | -------------------------------------------------------------------------------- /cicd/helm/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* 2 | Expand the name of the chart. 3 | */}} 4 | {{- define "gmqtt.name" -}} 5 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} 6 | {{- end }} 7 | 8 | {{/* 9 | Create a default fully qualified app name. 10 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 11 | If release name contains chart name it will be used as a full name. 12 | */}} 13 | {{- define "gmqtt.fullname" -}} 14 | {{- if .Values.fullnameOverride }} 15 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} 16 | {{- else }} 17 | {{- $name := default .Chart.Name .Values.nameOverride }} 18 | {{- if contains $name .Release.Name }} 19 | {{- .Release.Name | trunc 63 | trimSuffix "-" }} 20 | {{- else }} 21 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} 22 | {{- end }} 23 | {{- end }} 24 | {{- end }} 25 | 26 | {{/* 27 | Create chart name and version as used by the chart label. 28 | */}} 29 | {{- define "gmqtt.chart" -}} 30 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} 31 | {{- end }} 32 | 33 | {{/* 34 | Common labels 35 | */}} 36 | {{- define "gmqtt.labels" -}} 37 | helm.sh/chart: {{ include "gmqtt.chart" . }} 38 | {{ include "gmqtt.selectorLabels" . }} 39 | {{- if .Chart.AppVersion }} 40 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 41 | {{- end }} 42 | app.kubernetes.io/managed-by: {{ .Release.Service }} 43 | {{- end }} 44 | 45 | {{/* 46 | Selector labels 47 | */}} 48 | {{- define "gmqtt.selectorLabels" -}} 49 | app.kubernetes.io/name: {{ include "gmqtt.name" . }} 50 | app.kubernetes.io/instance: {{ .Release.Name }} 51 | {{- end }} 52 | 53 | {{/* 54 | Create the name of the service account to use 55 | */}} 56 | {{- define "gmqtt.serviceAccountName" -}} 57 | {{- if .Values.serviceAccount.create }} 58 | {{- default (include "gmqtt.fullname" .) .Values.serviceAccount.name }} 59 | {{- else }} 60 | {{- default "default" .Values.serviceAccount.name }} 61 | {{- end }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /cicd/helm/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "gmqtt.fullname" . }} 5 | labels: 6 | {{- include "gmqtt.labels" . | nindent 4 }} 7 | spec: 8 | {{- if not .Values.autoscaling.enabled }} 9 | replicas: {{ .Values.replicaCount }} 10 | {{- end }} 11 | selector: 12 | matchLabels: 13 | {{- include "gmqtt.selectorLabels" . | nindent 6 }} 14 | template: 15 | metadata: 16 | {{- with .Values.podAnnotations }} 17 | annotations: 18 | {{- toYaml . | nindent 8 }} 19 | {{- end }} 20 | labels: 21 | {{- include "gmqtt.selectorLabels" . | nindent 8 }} 22 | spec: 23 | {{- with .Values.imagePullSecrets }} 24 | imagePullSecrets: 25 | {{- toYaml . | nindent 8 }} 26 | {{- end }} 27 | serviceAccountName: {{ include "gmqtt.serviceAccountName" . }} 28 | securityContext: 29 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 30 | containers: 31 | - name: {{ .Chart.Name }} 32 | securityContext: 33 | {{- toYaml .Values.securityContext | nindent 12 }} 34 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}{{ .Values.image.digest }}" 35 | imagePullPolicy: {{ .Values.image.pullPolicy }} 36 | ports: 37 | - name: ws 38 | containerPort: 8080 39 | protocol: TCP 40 | - name: mqtt 41 | containerPort: 1883 42 | protocol: TCP 43 | livenessProbe: 44 | tcpSocket: 45 | port: 8080 46 | readinessProbe: 47 | tcpSocket: 48 | port: 8080 49 | resources: 50 | {{- toYaml .Values.resources | nindent 12 }} 51 | {{- with .Values.nodeSelector }} 52 | nodeSelector: 53 | {{- toYaml . | nindent 8 }} 54 | {{- end }} 55 | {{- with .Values.affinity }} 56 | affinity: 57 | {{- toYaml . | nindent 8 }} 58 | {{- end }} 59 | {{- with .Values.tolerations }} 60 | tolerations: 61 | {{- toYaml . | nindent 8 }} 62 | {{- end }} 63 | -------------------------------------------------------------------------------- /cicd/helm/templates/hpa.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.autoscaling.enabled }} 2 | apiVersion: autoscaling/v2beta1 3 | kind: HorizontalPodAutoscaler 4 | metadata: 5 | name: {{ include "gmqtt.fullname" . }} 6 | labels: 7 | {{- include "gmqtt.labels" . | nindent 4 }} 8 | spec: 9 | scaleTargetRef: 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | name: {{ include "gmqtt.fullname" . }} 13 | minReplicas: {{ .Values.autoscaling.minReplicas }} 14 | maxReplicas: {{ .Values.autoscaling.maxReplicas }} 15 | metrics: 16 | {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} 17 | - type: Resource 18 | resource: 19 | name: cpu 20 | targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} 21 | {{- end }} 22 | {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} 23 | - type: Resource 24 | resource: 25 | name: memory 26 | targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} 27 | {{- end }} 28 | {{- end }} 29 | -------------------------------------------------------------------------------- /cicd/helm/templates/ingress.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.ingress.enabled -}} 2 | {{- $fullName := include "gmqtt.fullname" . -}} 3 | {{- $svcPortWS := .Values.service.portWS -}} 4 | {{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} 5 | apiVersion: networking.k8s.io/v1beta1 6 | {{- else -}} 7 | apiVersion: extensions/v1beta1 8 | {{- end }} 9 | kind: Ingress 10 | metadata: 11 | name: {{ $fullName }} 12 | labels: 13 | {{- include "gmqtt.labels" . | nindent 4 }} 14 | {{- with .Values.ingress.annotations }} 15 | annotations: 16 | kubernetes.io/ingress.class: nginx 17 | {{- toYaml . | nindent 4 }} 18 | {{- end }} 19 | spec: 20 | {{- if .Values.ingress.tls }} 21 | tls: 22 | {{- range .Values.ingress.tls }} 23 | - hosts: 24 | {{- range .hosts }} 25 | - {{ . | quote }} 26 | {{- end }} 27 | secretName: {{ .secretName }} 28 | {{- end }} 29 | {{- end }} 30 | rules: 31 | {{- range .Values.ingress.hosts }} 32 | - host: {{ .host | quote }} 33 | http: 34 | paths: 35 | {{- range .paths }} 36 | - path: {{ .path }} 37 | backend: 38 | serviceName: {{ $fullName }} 39 | servicePort: {{ $svcPortWS }} 40 | {{- end }} 41 | {{- end }} 42 | {{- end }} 43 | -------------------------------------------------------------------------------- /cicd/helm/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "gmqtt.fullname" . }} 5 | labels: 6 | {{- include "gmqtt.labels" . | nindent 4 }} 7 | spec: 8 | type: {{ .Values.service.type }} 9 | ports: 10 | - port: {{ .Values.service.port }} 11 | targetPort: mqtt 12 | protocol: TCP 13 | name: mqtt 14 | - port: {{ .Values.service.portWS }} 15 | targetPort: ws 16 | protocol: TCP 17 | name: ws 18 | selector: 19 | {{- include "gmqtt.selectorLabels" . | nindent 4 }} 20 | -------------------------------------------------------------------------------- /cicd/helm/templates/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.serviceAccount.create -}} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ include "gmqtt.serviceAccountName" . }} 6 | labels: 7 | {{- include "gmqtt.labels" . | nindent 4 }} 8 | {{- with .Values.serviceAccount.annotations }} 9 | annotations: 10 | {{- toYaml . | nindent 4 }} 11 | {{- end }} 12 | {{- end }} 13 | -------------------------------------------------------------------------------- /cicd/helm/templates/tests/test-connection.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Pod 3 | metadata: 4 | name: "{{ include "gmqtt.fullname" . }}-test-connection" 5 | labels: 6 | {{- include "gmqtt.labels" . | nindent 4 }} 7 | annotations: 8 | "helm.sh/hook": test 9 | spec: 10 | containers: 11 | - name: wget 12 | image: busybox 13 | command: ['wget'] 14 | args: ['{{ include "gmqtt.fullname" . }}:{{ .Values.service.port }}'] 15 | restartPolicy: Never 16 | -------------------------------------------------------------------------------- /cicd/helm/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for gmqtt. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | replicaCount: 1 6 | 7 | image: 8 | repository: registry.cn-hangzhou.aliyuncs.com/hb-chen/gmqtt-gateway 9 | pullPolicy: Always 10 | # Overrides the image tag whose default is the chart appVersion. 11 | tag: "" 12 | digest: "" 13 | 14 | imagePullSecrets: [] 15 | nameOverride: "" 16 | fullnameOverride: "" 17 | 18 | serviceAccount: 19 | # Specifies whether a service account should be created 20 | create: true 21 | # Annotations to add to the service account 22 | annotations: {} 23 | # The name of the service account to use. 24 | # If not set and create is true, a name is generated using the fullname template 25 | name: "" 26 | 27 | podAnnotations: {} 28 | 29 | podSecurityContext: {} 30 | # fsGroup: 2000 31 | 32 | securityContext: {} 33 | # capabilities: 34 | # drop: 35 | # - ALL 36 | # readOnlyRootFilesystem: true 37 | # runAsNonRoot: true 38 | # runAsUser: 1000 39 | 40 | service: 41 | type: ClusterIP 42 | port: 1883 43 | portWS: 8080 44 | 45 | ingress: 46 | enabled: true 47 | annotations: {} 48 | # kubernetes.io/ingress.class: nginx 49 | # kubernetes.io/tls-acme: "true" 50 | hosts: 51 | - host: gmqtt.k8s.hbchen.com 52 | paths: 53 | - path: / 54 | tls: [] 55 | # - secretName: chart-example-tls 56 | # hosts: 57 | # - chart-example.local 58 | 59 | resources: {} 60 | # We usually recommend not to specify default resources and to leave this as a conscious 61 | # choice for the user. This also increases chances charts run on environments with little 62 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 63 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 64 | # limits: 65 | # cpu: 100m 66 | # memory: 128Mi 67 | # requests: 68 | # cpu: 100m 69 | # memory: 128Mi 70 | 71 | autoscaling: 72 | enabled: false 73 | minReplicas: 1 74 | maxReplicas: 100 75 | targetCPUUtilizationPercentage: 80 76 | # targetMemoryUtilizationPercentage: 80 77 | 78 | nodeSelector: {} 79 | 80 | tolerations: [] 81 | 82 | affinity: {} 83 | -------------------------------------------------------------------------------- /cicd/tekton/pipeline.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: tekton.dev/v1beta1 3 | kind: Pipeline 4 | metadata: 5 | name: gmqtt-pipeline 6 | spec: 7 | workspaces: 8 | - name: shared-workspace 9 | params: 10 | - name: url 11 | - name: revision 12 | default: master 13 | tasks: 14 | - name: fetch-repository 15 | taskRef: 16 | name: git-clone 17 | workspaces: 18 | - name: output 19 | workspace: shared-workspace 20 | params: 21 | - name: url 22 | value: $(params.url) 23 | - name: revision 24 | value: $(params.revision) 25 | - name: subdirectory 26 | value: "gmqtt" 27 | - name: deleteExisting 28 | value: "true" 29 | - name: run-test 30 | taskRef: 31 | name: golang-test 32 | runAfter: 33 | - fetch-repository 34 | workspaces: 35 | - name: source 36 | workspace: shared-workspace 37 | params: 38 | - name: package 39 | value: github.com/hb-chen/gmqtt 40 | - name: version 41 | value: 1.14.15 42 | - name: run-build 43 | taskRef: 44 | name: golang-build 45 | runAfter: 46 | - run-test 47 | workspaces: 48 | - name: source 49 | workspace: shared-workspace 50 | params: 51 | - name: package 52 | value: github.com/github.com/hb-chen/gmqtt 53 | - name: version 54 | value: 1.14.15 55 | - name: flags 56 | value: -v -o $(workspaces.source.path)/bin/gmqtt 57 | # - name: docker-build 58 | # taskRef: 59 | # name: docker-build 60 | # runAfter: 61 | # - run-build 62 | # workspaces: 63 | # - name: source 64 | # workspace: shared-workspace 65 | # params: 66 | # - name: image 67 | # value: registry.cn-hangzhou.aliyuncs.com/hb-chen/grpc-gateway 68 | # - name: insecure_registry 69 | # value: registry.cn-hangzhou.aliyuncs.com 70 | - name: docker-build 71 | taskRef: 72 | name: kaniko 73 | runAfter: 74 | - run-build 75 | workspaces: 76 | - name: source 77 | workspace: shared-workspace 78 | params: 79 | - name: IMAGE 80 | value: registry.cn-hangzhou.aliyuncs.com/hb-chen/gmqtt:latest 81 | - name: EXTRA_ARGS 82 | value: "--skip-tls-verify" 83 | - name: insecure_registry 84 | value: registry.cn-hangzhou.aliyuncs.com 85 | - name: helm-kubectl-deploy 86 | taskRef: 87 | name: helm-kubectl-deploy 88 | runAfter: 89 | - docker-build 90 | workspaces: 91 | - name: source 92 | workspace: shared-workspace 93 | params: 94 | - name: env_secret 95 | value: k8s-cluster-config 96 | - name: image_digest 97 | value: $(tasks.docker-build.results.IMAGE-DIGEST) 98 | - name: commands 99 | value: helm upgrade --install gmqtt ./helm --namespace gmqtt --no-hooks --set image.repository=registry.cn-hangzhou.aliyuncs.com/hb-chen/gmqtt --set image.tag=latest --set image.digest=@$(tasks.docker-build.results.IMAGE-DIGEST) 100 | -------------------------------------------------------------------------------- /cicd/tekton/run.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: tekton.dev/v1beta1 3 | kind: PipelineRun 4 | metadata: 5 | name: gmqtt-pipeline-run 6 | #generateName: gmqtt-pipeline-run- 7 | spec: 8 | serviceAccountName: build-bot 9 | pipelineRef: 10 | name: gmqtt-pipeline 11 | params: 12 | - name: url 13 | value: https://github.com/hb-chen/gmqtt 14 | - name: revision 15 | value: master 16 | workspaces: 17 | - name: shared-workspace 18 | persistentvolumeclaim: 19 | claimName: golang-source-pvc 20 | -------------------------------------------------------------------------------- /cicd/tekton/trigger.yml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: github-secret 6 | type: Opaque 7 | stringData: 8 | secretToken: "123456" 9 | --- 10 | apiVersion: triggers.tekton.dev/v1alpha1 11 | kind: EventListener 12 | metadata: 13 | name: github-listener-interceptor 14 | spec: 15 | triggers: 16 | - name: github-listener 17 | interceptors: 18 | - github: 19 | secretRef: 20 | secretName: github-secret 21 | secretKey: secretToken 22 | eventTypes: 23 | # CEL 过滤或扩展 Event 24 | #- cel: 25 | # filter: "body.action in ['opened', 'synchronize', 'reopened']" 26 | bindings: 27 | - ref: pipeline-binding 28 | template: 29 | ref: gmqtt-pipeline-template 30 | resources: 31 | kubernetesResource: 32 | spec: 33 | template: 34 | spec: 35 | serviceAccountName: tekton-triggers-example-sa 36 | containers: 37 | - resources: 38 | requests: 39 | memory: "64Mi" 40 | cpu: "250m" 41 | limits: 42 | memory: "128Mi" 43 | cpu: "500m" 44 | --- 45 | apiVersion: triggers.tekton.dev/v1alpha1 46 | kind: TriggerBinding 47 | metadata: 48 | name: pipeline-binding 49 | spec: 50 | params: 51 | - name: gitrepositoryurl 52 | value: $(body.repository.clone_url) 53 | --- 54 | apiVersion: triggers.tekton.dev/v1alpha1 55 | kind: TriggerTemplate 56 | metadata: 57 | name: gmqtt-pipeline-template 58 | spec: 59 | params: 60 | - name: gitrevision 61 | description: The git revision 62 | default: master 63 | - name: gitrepositoryurl 64 | description: The git repository url 65 | resourcetemplates: 66 | - apiVersion: tekton.dev/v1beta1 67 | kind: PipelineRun 68 | metadata: 69 | generateName: gmqtt-pipeline-run- 70 | spec: 71 | serviceAccountName: build-bot 72 | pipelineRef: 73 | name: gmqtt-pipeline 74 | workspaces: 75 | - name: shared-workspace 76 | persistentvolumeclaim: 77 | claimName: golang-source-pvc 78 | params: 79 | - name: url 80 | value: $(tt.params.gitrepositoryurl) 81 | - name: revision 82 | value: $(tt.params.gitrevision) 83 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "time" 6 | 7 | //"github.com/casbin/redis-adapter" 8 | "github.com/casbin/casbin/persist/file-adapter" 9 | "github.com/casbin/redis-adapter" 10 | metrics "github.com/rcrowley/go-metrics" 11 | "github.com/smallnest/rpcx/server" 12 | "github.com/smallnest/rpcx/serverplugin" 13 | 14 | "github.com/hb-chen/gmqtt/pkg/api/access" 15 | "github.com/hb-chen/gmqtt/pkg/api/auth" 16 | "github.com/hb-chen/gmqtt/pkg/api/client" 17 | "github.com/hb-chen/gmqtt/pkg/api/cluster" 18 | "github.com/hb-chen/gmqtt/pkg/api/proto" 19 | "github.com/hb-chen/gmqtt/pkg/log" 20 | "github.com/hb-chen/gmqtt/pkg/util/conv" 21 | ) 22 | 23 | var ( 24 | cmdHelp = flag.Bool("h", false, "帮助") 25 | addr = flag.String("addr", "127.0.0.1:8972", "server address") 26 | consulAddr = flag.String("consulAddr", "127.0.0.1:8500", "consul address") 27 | ) 28 | 29 | func init() { 30 | flag.Parse() 31 | } 32 | 33 | func main() { 34 | if *cmdHelp { 35 | flag.PrintDefaults() 36 | return 37 | } 38 | 39 | log.SetColor(true) 40 | log.SetLevel(log.DEBUG) 41 | 42 | s := server.NewServer() 43 | addRegistryPlugin(s) 44 | 45 | access.Register(s) 46 | client.Register(s) 47 | cluster.Register(s) 48 | 49 | // Auth 50 | var a *auth.Auth 51 | if false { 52 | // @TODO redis adapter, etcd watcher管理 53 | adapter := redisadapter.NewAdapter("tcp", "127.0.0.1:6379") 54 | a = auth.NewAuth(adapter, nil) 55 | } else { 56 | adapter := fileadapter.NewAdapter("conf/casbin/rbac_with_deny_policy.csv") 57 | a = auth.NewAuth(adapter, nil) 58 | } 59 | if err := a.Init(); err != nil { 60 | log.Fatalf("api client serve start error:", err) 61 | } 62 | s.AuthFunc = a.Verify 63 | 64 | err := s.Serve("tcp", *addr) 65 | if err != nil { 66 | log.Fatalf("api client serve exit error:%v", err) 67 | } 68 | } 69 | 70 | func addRegistryPlugin(s *server.Server) { 71 | r := serverplugin.ConsulRegisterPlugin{ 72 | ServiceAddress: "tcp@" + *addr, 73 | ConsulServers: []string{*consulAddr}, 74 | BasePath: conv.ProtoEnumsToRpcxBasePath(proto.BASE_PATH_name), 75 | Metrics: metrics.NewRegistry(), 76 | UpdateInterval: time.Minute, 77 | } 78 | err := r.Start() 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | s.Plugins.Add(r) 83 | } 84 | -------------------------------------------------------------------------------- /cmd/console/README.md: -------------------------------------------------------------------------------- 1 | ### Console 2 | 3 | - Dashboard 4 | - Nodes 5 | - Clients 6 | - Sessions 7 | - Topics 8 | - Subscriptions -------------------------------------------------------------------------------- /cmd/console/conf/conf.d/config.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb-chen/gmqtt/89754d30e5b95d5f7ca322b4cd8ad96b85791b34/cmd/console/conf/conf.d/config.toml -------------------------------------------------------------------------------- /cmd/console/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | -------------------------------------------------------------------------------- /cmd/console/conf/conf.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb-chen/gmqtt/89754d30e5b95d5f7ca322b4cd8ad96b85791b34/cmd/console/conf/conf.toml -------------------------------------------------------------------------------- /cmd/console/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | ) 6 | 7 | var ( 8 | cmdHelp = flag.Bool("h", false, "帮助") 9 | ) 10 | 11 | func init() { 12 | flag.Parse() 13 | } 14 | 15 | func main() { 16 | if *cmdHelp { 17 | flag.PrintDefaults() 18 | return 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /cmd/gateway/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/http" 7 | "net/url" 8 | "os" 9 | "runtime" 10 | "strings" 11 | "sync" 12 | 13 | "github.com/pyroscope-io/pyroscope/pkg/agent" 14 | "github.com/pyroscope-io/pyroscope/pkg/agent/profiler" 15 | "github.com/urfave/cli/v2" 16 | "go.uber.org/zap" 17 | "go.uber.org/zap/zapcore" 18 | "gopkg.in/natefinch/lumberjack.v2" 19 | 20 | "github.com/hb-chen/gmqtt/pkg/auth" 21 | "github.com/hb-chen/gmqtt/pkg/conf" 22 | "github.com/hb-chen/gmqtt/pkg/service" 23 | "github.com/hb-go/pkg/log" 24 | ) 25 | 26 | const ( 27 | logCallerSkip = 2 28 | ) 29 | 30 | var ( 31 | version = "" 32 | commit = "" 33 | date = "" 34 | builtBy = "" 35 | goVersion = runtime.Version() 36 | ) 37 | 38 | func pyroscope(logger agent.Logger) { 39 | profiler.Start(profiler.Config{ 40 | ApplicationName: "com.hbchen.gmqtt", 41 | 42 | // replace this with the address of pyroscope server 43 | ServerAddress: "http://pyroscope.pyroscope.svc.cluster.local:4040", 44 | 45 | // by default all profilers are enabled, 46 | // but you can select the ones you want to use: 47 | ProfileTypes: []profiler.ProfileType{ 48 | profiler.ProfileCPU, 49 | profiler.ProfileAllocObjects, 50 | profiler.ProfileAllocSpace, 51 | profiler.ProfileInuseObjects, 52 | profiler.ProfileInuseSpace, 53 | }, 54 | 55 | Logger: logger, 56 | }) 57 | } 58 | 59 | func initLogger(path, level string, debug, e bool) (*zap.Logger, error) { 60 | logLevel := zapcore.WarnLevel 61 | err := logLevel.UnmarshalText([]byte(level)) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | writer := logWriter(path) 67 | if e { 68 | stderr, close, err := zap.Open("stderr") 69 | if err != nil { 70 | close() 71 | return nil, err 72 | } 73 | writer = stderr 74 | } 75 | 76 | encoder := logEncoder(debug) 77 | core := zapcore.NewCore(encoder, writer, logLevel) 78 | logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(logCallerSkip)) 79 | 80 | return logger, nil 81 | } 82 | 83 | func logEncoder(debug bool) zapcore.Encoder { 84 | encoderConfig := zap.NewProductionEncoderConfig() 85 | encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder 86 | encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder 87 | 88 | if debug { 89 | encoderConfig = zap.NewDevelopmentEncoderConfig() 90 | encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder 91 | encoderConfig.EncodeCaller = zapcore.ShortCallerEncoder 92 | } 93 | 94 | return zapcore.NewConsoleEncoder(encoderConfig) 95 | } 96 | 97 | func logWriter(path string) zapcore.WriteSyncer { 98 | path = strings.TrimRight(path, "/") 99 | lumberJackLogger := &lumberjack.Logger{ 100 | Filename: path + "/gmqtt.log", 101 | MaxSize: 10, 102 | MaxBackups: 10, 103 | MaxAge: 7, 104 | Compress: false, 105 | } 106 | return zapcore.AddSync(lumberJackLogger) 107 | } 108 | 109 | func run(ctx *cli.Context) error { 110 | // 配置初始化 111 | if err := conf.InitConfig(ctx); err != nil { 112 | return err 113 | } 114 | 115 | l, err := initLogger("./log", conf.Conf.Log.Level, conf.Conf.Log.Debug, conf.Conf.Log.E) 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | if conf.Conf.Pyroscope.Enable { 121 | pyroscope(l.Sugar()) 122 | } 123 | 124 | u, err := url.Parse(conf.Conf.Server.Addr) 125 | if err != nil { 126 | return err 127 | } 128 | 129 | if conf.Conf.Auth.Provider == auth.ProviderRpc { 130 | closer := auth.NewRpcRegister(conf.Conf.App.AccessKey, conf.Conf.App.SecretKey, conf.Conf.Auth.Addrs) 131 | defer func() { 132 | if err := closer.Close(); err != nil { 133 | log.Warnf("rpc auth close error:%v", err) 134 | } 135 | }() 136 | } 137 | 138 | wg := &sync.WaitGroup{} 139 | var wsServer *http.Server 140 | if len(conf.Conf.Server.WsAddr) > 0 { 141 | handler, err := service.WebsocketHandler("/mqtt", conf.Conf.Server.Addr) 142 | if err != nil { 143 | return err 144 | } 145 | 146 | wg.Add(1) 147 | wsServer = &http.Server{Addr: conf.Conf.Server.WsAddr, Handler: handler} 148 | go func() { 149 | defer wg.Done() 150 | if err := wsServer.ListenAndServe(); err != nil { 151 | log.Fatal(err) 152 | } 153 | }() 154 | } 155 | 156 | server, err := service.NewServer() 157 | if err != nil { 158 | return err 159 | } 160 | 161 | wg.Add(1) 162 | go func() { 163 | defer wg.Done() 164 | if err = server.ListenAndServe(u.Scheme, u.Host); err != nil { 165 | log.Fatal(err) 166 | } 167 | }() 168 | 169 | <-ctx.Done() 170 | if wsServer != nil { 171 | wsServer.Shutdown(ctx.Context) 172 | } 173 | server.Close() 174 | wg.Wait() 175 | 176 | return nil 177 | } 178 | 179 | func main() { 180 | app := cli.NewApp() 181 | 182 | app.Name = "gmqtt-gateway" 183 | app.Description = "MQTT broker" 184 | app.Version = fmt.Sprintf("%v\ncommit: %v\nbuilt at: %v\nbuilt by: %v\ngo version: %v", 185 | version, 186 | commit, 187 | date, 188 | builtBy, 189 | goVersion) 190 | 191 | app.Flags = []cli.Flag{ 192 | &cli.StringSliceFlag{ 193 | Name: "config_patterns", 194 | EnvVars: []string{"GM_CONFIG_PATTERNS"}, 195 | Usage: "config files patterns.", 196 | Value: cli.NewStringSlice("./conf.d/*.yaml"), 197 | }, 198 | } 199 | 200 | app.Before = func(c *cli.Context) error { 201 | return nil 202 | } 203 | 204 | app.Action = func(ctx *cli.Context) error { 205 | 206 | return run(ctx) 207 | } 208 | 209 | app.Commands = cli.Commands{ 210 | &cli.Command{ 211 | Name: "reload", 212 | Usage: "TODO", 213 | Action: func(ctx *cli.Context) error { 214 | return nil 215 | }, 216 | }, 217 | } 218 | 219 | ctx := context.Background() 220 | if err := app.RunContext(ctx, os.Args); err != nil { 221 | log.Fatal(err) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /config/api/casbin/rbac_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act -------------------------------------------------------------------------------- /config/api/casbin/rbac_policy.csv: -------------------------------------------------------------------------------- 1 | p, alice, data1, read 2 | p, bob, data2, write 3 | p, data2_admin, data2, read 4 | p, data2_admin, data2, write 5 | g, alice, data2_admin -------------------------------------------------------------------------------- /config/api/casbin/rbac_with_deny_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act, eft 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act -------------------------------------------------------------------------------- /config/api/casbin/rbac_with_deny_policy.csv: -------------------------------------------------------------------------------- 1 | p, client, client_auth, Auth, allow 2 | p, client, client_auth, SubAuth, allow 3 | p, client, client_auth, PubAuth, allow 4 | 5 | p, client_admin, client_register, Register, allow 6 | p, client_admin, client_register, Unregister, allow 7 | 8 | p, console, cluster, Clients, allow 9 | p, console, cluster, Sessions, allow 10 | p, console, cluster, Subscribitions, allow 11 | p, console, cluster, Topics, allow 12 | 13 | g, console, client_admin -------------------------------------------------------------------------------- /config/api/conf.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb-chen/gmqtt/89754d30e5b95d5f7ca322b4cd8ad96b85791b34/config/api/conf.toml -------------------------------------------------------------------------------- /config/api/config.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb-chen/gmqtt/89754d30e5b95d5f7ca322b4cd8ad96b85791b34/config/api/config.toml -------------------------------------------------------------------------------- /config/gateway/conf.yaml: -------------------------------------------------------------------------------- 1 | app: 2 | name: gmqtt 3 | access_key: ak 4 | secret_key: sk 5 | 6 | server: 7 | addr: tcp://127.0.0.1:1883 8 | ws_addr: :8080 9 | 10 | auth: 11 | # MockSuccess、MockFailure、rpc 12 | provider: MockSuccess 13 | # rpc 注册中心地址 14 | addrs: 15 | - 127.0.0.1:8500 16 | 17 | broker: 18 | # mock、kafka,默认mock 19 | provider: mock 20 | addrs: 21 | 22 | sessions: 23 | # mock、redis,默认mock 24 | provider: mock 25 | 26 | redis: 27 | server: 127.0.0.1:6379 28 | pwd: 123456 29 | 30 | memcached: 31 | server: 127.0.0.1:11211 32 | -------------------------------------------------------------------------------- /config/gateway/db.yaml: -------------------------------------------------------------------------------- 1 | database: 2 | name: db_name 3 | user_name: user 4 | pwd: pwd 5 | host: 127.0.0.1 6 | port: 3306 7 | -------------------------------------------------------------------------------- /config/gateway/log.yaml: -------------------------------------------------------------------------------- 1 | log: 2 | # default DEBUG, DEBUG INFO WARN ERROR OFF 3 | level: DEBUG 4 | # debug 模式输出日志 5 | debug: true 6 | # 标准输出 7 | e: true 8 | -------------------------------------------------------------------------------- /config/gateway/pyroscope.yaml: -------------------------------------------------------------------------------- 1 | pyroscope: 2 | enable: false 3 | -------------------------------------------------------------------------------- /config/gateway/tracing.yaml: -------------------------------------------------------------------------------- 1 | opentracing: 2 | disable: false 3 | # jaeger or appdash 4 | type: jaeger 5 | 6 | # jaeger serviceName 7 | service_name: gmqtt 8 | 9 | # jaeger-agent 127.0.0.1:6831 10 | # appdash http://localhost:8700 11 | address: 127.0.0.1:6831 12 | -------------------------------------------------------------------------------- /doc/img/architecture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb-chen/gmqtt/89754d30e5b95d5f7ca322b4cd8ad96b85791b34/doc/img/architecture.jpg -------------------------------------------------------------------------------- /doc/img/gateway_model.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb-chen/gmqtt/89754d30e5b95d5f7ca322b4cd8ad96b85791b34/doc/img/gateway_model.jpg -------------------------------------------------------------------------------- /gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.10 2 | 3 | ADD bin/linux/gateway /opt/gmqtt/gateway 4 | ADD config/gateway /opt/gmqtt/conf.d 5 | 6 | EXPOSE 1883 7 | EXPOSE 8080 8 | WORKDIR /opt/gmqtt 9 | 10 | ENTRYPOINT [ "./gateway" ] 11 | -------------------------------------------------------------------------------- /gateway/README.md: -------------------------------------------------------------------------------- 1 | # Gateway 2 | 3 | - Client SSO -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hb-chen/gmqtt 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/Shopify/sarama v1.28.0 7 | github.com/apache/pulsar-client-go v0.5.0 8 | github.com/bsm/sarama-cluster v2.1.15+incompatible // indirect 9 | github.com/casbin/casbin v1.9.1 10 | github.com/casbin/redis-adapter v1.0.0 11 | github.com/eclipse/paho.mqtt.golang v1.3.3 12 | github.com/fatih/color v1.10.0 13 | github.com/go-redis/redis v6.15.9+incompatible 14 | github.com/golang/protobuf v1.5.2 15 | github.com/gomodule/redigo v1.8.4 // indirect 16 | github.com/hb-go/pkg v0.0.0-20210513173633-7fea6be50923 17 | github.com/mailru/easygo v0.0.0-20190618140210-3c14a0dc985f 18 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d 19 | github.com/pborman/uuid v1.2.1 20 | github.com/pyroscope-io/pyroscope v0.0.30 21 | github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 22 | github.com/smallnest/rpcx v1.6.2 23 | github.com/streadway/amqp v1.0.0 24 | github.com/stretchr/testify v1.7.0 25 | github.com/surgemq/message v0.0.0-20151017233315-2b7ca1ac6121 26 | github.com/urfave/cli/v2 v2.3.0 27 | github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 // indirect 28 | go.uber.org/zap v1.15.0 29 | golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1 30 | google.golang.org/grpc/examples v0.0.0-20210409234925-fab5982df20a // indirect 31 | gopkg.in/bsm/sarama-cluster.v2 v2.1.15 32 | gopkg.in/natefinch/lumberjack.v2 v2.0.0 33 | ) 34 | -------------------------------------------------------------------------------- /pkg/api/Dockerfile: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hb-chen/gmqtt/89754d30e5b95d5f7ca322b4cd8ad96b85791b34/pkg/api/Dockerfile -------------------------------------------------------------------------------- /pkg/api/README.md: -------------------------------------------------------------------------------- 1 | ## API 2 | 3 | ### 安全 4 | - 服务间访问控制`auth` 5 | - Token认证 6 | - 7 | - 用户访问控制`access` 8 | - 终端访问控制`client/auth` 9 | - Auth认证 10 | - 话题订阅/消费鉴权 11 | 12 | ### Gateway集群 13 | - Nodes 14 | - summary 15 | - list 16 | - detail 17 | - > Etcd / Consul / ZooKeeper,同RPC 18 | - Clients 19 | - summary 20 | - list 21 | - > MySQL / PostgreSQL / NoSQL 22 | - Sessions 23 | - list 24 | - detail 25 | - topics 26 | - > Redis: (SortedSet)ClientId => CreateTime, (String)ClientId => session 27 | - Topics 28 | - > Redis: (SortedSet)NodeId-Topic => QOS 29 | - Subscriptions 30 | - > Redis: (SortedSet)ClientId-Topic => QOS 31 | - Broker 32 | - consumer group `NodeId` 33 | - topics 34 | - offset 35 | - > kafka-manager -------------------------------------------------------------------------------- /pkg/api/access/README.md: -------------------------------------------------------------------------------- 1 | ### Access 2 | 用户访问控制 -------------------------------------------------------------------------------- /pkg/api/access/access.go: -------------------------------------------------------------------------------- 1 | package access 2 | 3 | import ( 4 | "github.com/smallnest/rpcx/server" 5 | //pbAccess "github.com/hb-chen/gmqtt/pkg/api/access/proto" 6 | ) 7 | 8 | type Access struct { 9 | } 10 | 11 | func Register(s *server.Server) { 12 | //s.RegisterName(pbAccess.SRV_access.String(), new(Access), "") 13 | } 14 | -------------------------------------------------------------------------------- /pkg/api/access/proto/define.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api/access/proto/define.proto 3 | 4 | /* 5 | Package api_access is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | api/access/proto/define.proto 9 | 10 | It has these top-level messages: 11 | */ 12 | package api_access 13 | 14 | import proto "github.com/golang/protobuf/proto" 15 | import fmt "fmt" 16 | import math "math" 17 | 18 | // Reference imports to suppress errors if they are not otherwise used. 19 | var _ = proto.Marshal 20 | var _ = fmt.Errorf 21 | var _ = math.Inf 22 | 23 | // This is a compile-time assertion to ensure that this generated file 24 | // is compatible with the proto package it is being compiled against. 25 | // A compilation error at this line likely means your copy of the 26 | // proto package needs to be updated. 27 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 28 | 29 | type SRV int32 30 | 31 | const ( 32 | SRV_access SRV = 0 33 | ) 34 | 35 | var SRV_name = map[int32]string{ 36 | 0: "access", 37 | } 38 | var SRV_value = map[string]int32{ 39 | "access": 0, 40 | } 41 | 42 | func (x SRV) String() string { 43 | return proto.EnumName(SRV_name, int32(x)) 44 | } 45 | func (SRV) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 46 | 47 | type METHOD int32 48 | 49 | const ( 50 | METHOD_Empty METHOD = 0 51 | ) 52 | 53 | var METHOD_name = map[int32]string{ 54 | 0: "Empty", 55 | } 56 | var METHOD_value = map[string]int32{ 57 | "Empty": 0, 58 | } 59 | 60 | func (x METHOD) String() string { 61 | return proto.EnumName(METHOD_name, int32(x)) 62 | } 63 | func (METHOD) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 64 | 65 | func init() { 66 | proto.RegisterEnum("api_access.SRV", SRV_name, SRV_value) 67 | proto.RegisterEnum("api_access.METHOD", METHOD_name, METHOD_value) 68 | } 69 | 70 | func init() { proto.RegisterFile("api/access/proto/define.proto", fileDescriptor0) } 71 | 72 | var fileDescriptor0 = []byte{ 73 | // 101 bytes of a gzipped FileDescriptorProto 74 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4d, 0x2c, 0xc8, 0xd4, 75 | 0x4f, 0x4c, 0x4e, 0x4e, 0x2d, 0x2e, 0xd6, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0xd7, 0x4f, 0x49, 0x4d, 76 | 0xcb, 0xcc, 0x4b, 0xd5, 0x03, 0x73, 0x84, 0xb8, 0x12, 0x0b, 0x32, 0xe3, 0x21, 0xd2, 0x5a, 0x82, 77 | 0x5c, 0xcc, 0xc1, 0x41, 0x61, 0x42, 0x5c, 0x5c, 0x6c, 0x10, 0x01, 0x01, 0x06, 0x2d, 0x61, 0x2e, 78 | 0x36, 0x5f, 0xd7, 0x10, 0x0f, 0x7f, 0x17, 0x21, 0x4e, 0x2e, 0x56, 0xd7, 0xdc, 0x82, 0x92, 0x4a, 79 | 0x01, 0x86, 0x24, 0x36, 0xb0, 0x56, 0x63, 0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0x59, 0x17, 0x0b, 80 | 0xaf, 0x5b, 0x00, 0x00, 0x00, 81 | } 82 | -------------------------------------------------------------------------------- /pkg/api/access/proto/define.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api_access; 4 | 5 | enum SRV { 6 | access = 0; 7 | } 8 | 9 | enum METHOD { 10 | Empty = 0; 11 | } -------------------------------------------------------------------------------- /pkg/api/auth/README.md: -------------------------------------------------------------------------------- 1 | ## Auth 2 | 服务认证 3 | - token有效性 4 | - service path/method调用授权认证 5 | - payload验签 -------------------------------------------------------------------------------- /pkg/api/auth/access.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | type AccessKey struct { 4 | AccessKeyId string 5 | AccessKeySecret string 6 | Roles []string 7 | } 8 | -------------------------------------------------------------------------------- /pkg/api/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "strings" 7 | "time" 8 | 9 | "github.com/casbin/casbin" 10 | "github.com/casbin/casbin/persist" 11 | "github.com/casbin/casbin/persist/file-adapter" 12 | "github.com/smallnest/rpcx/protocol" 13 | 14 | "github.com/hb-chen/gmqtt/pkg/log" 15 | "github.com/hb-chen/gmqtt/pkg/util/crypt" 16 | ) 17 | 18 | type Auth struct { 19 | cse *casbin.SyncedEnforcer 20 | } 21 | 22 | func Token(ak, sk, path string) string { 23 | return "1.0:" + ak + ":" + crypt.MD5([]byte(path+sk)) 24 | } 25 | 26 | func NewAuth(adapter persist.Adapter, w persist.Watcher) *Auth { 27 | e := casbin.NewSyncedEnforcer("conf/casbin/rbac_with_deny_model.conf", adapter, false) 28 | a := &Auth{ 29 | cse: e, 30 | } 31 | 32 | if w != nil { 33 | e.SetWatcher(w) 34 | w.SetUpdateCallback(a.casbinUpdateCallback) 35 | } 36 | 37 | return a 38 | } 39 | 40 | func (this *Auth) Init() error { 41 | err := this.cse.LoadPolicy() 42 | if err != nil { 43 | return err 44 | } 45 | 46 | // @TODO 47 | if false { 48 | filter := fileadapter.Filter{ 49 | P: []string{"a", "b"}, 50 | G: []string{"a", "b"}, 51 | } 52 | this.cse.LoadFilteredPolicy(filter) 53 | } 54 | 55 | this.cse.StartAutoLoadPolicy(time.Second * 60) 56 | 57 | return nil 58 | } 59 | 60 | func (this *Auth) Verify(ctx context.Context, req *protocol.Message, token string) error { 61 | // AK/SK查询 62 | // token验证 63 | // service path/method授权策略 64 | // message.Payload验签 65 | 66 | // {token版本/加密方式}:{ak}:{token加密} 67 | sp := strings.Split(token, ":") 68 | if len(sp) != 3 { 69 | return errors.New("invalid token split len != 3") 70 | } 71 | 72 | // AK/SK查询 73 | // @TODO AccessKey存储引擎 74 | ak := AccessKey{ 75 | AccessKeyId: sp[1], 76 | AccessKeySecret: "sk", 77 | Roles: []string{"client"}, 78 | } 79 | 80 | // token验证 81 | if token != Token(ak.AccessKeyId, ak.AccessKeySecret, req.ServicePath) { 82 | return errors.New("invalid token") 83 | } 84 | 85 | // service path/method授权策略 86 | for _, v := range ak.Roles { 87 | if this.cse.Enforce(v, req.ServicePath, req.ServiceMethod) == true { 88 | return nil 89 | } 90 | } 91 | 92 | // deny the request, show an error 93 | return errors.New("deny the request") 94 | } 95 | 96 | func (this *Auth) casbinUpdateCallback(rev string) { 97 | log.Infof("new revision detected:", rev) 98 | this.cse.LoadPolicy() 99 | } 100 | -------------------------------------------------------------------------------- /pkg/api/client/README.md: -------------------------------------------------------------------------------- 1 | ## Client 2 | 终端管理接口 3 | - Auth认证 4 | - 订阅/消费访问控制 5 | - 终端注册/注销 -------------------------------------------------------------------------------- /pkg/api/client/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/smallnest/rpcx/server" 7 | 8 | pb "github.com/hb-chen/gmqtt/pkg/api/client/auth/proto" 9 | client "github.com/hb-chen/gmqtt/pkg/api/client/proto" 10 | "github.com/hb-chen/gmqtt/pkg/log" 11 | ) 12 | 13 | func Register(s *server.Server) { 14 | s.RegisterName(client.SRV_client_auth.String(), new(auth), "") 15 | } 16 | 17 | type auth struct { 18 | } 19 | 20 | func (*auth) Auth(ctx context.Context, req *pb.AuthReq, resp *pb.AuthResp) error { 21 | log.Debugf("rpc: auth verify") 22 | resp.Verified = false 23 | if req.Name == req.Pwd { 24 | resp.Verified = true 25 | } 26 | 27 | return nil 28 | } 29 | 30 | func (*auth) SubAuth(ctx context.Context, req *pb.TopicReq, resp *pb.TopicResp) error { 31 | resp.Allow = true 32 | 33 | return nil 34 | } 35 | 36 | func (*auth) PubAuth(ctx context.Context, req *pb.TopicReq, resp *pb.TopicResp) error { 37 | resp.Allow = true 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/api/client/auth/auth_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | -------------------------------------------------------------------------------- /pkg/api/client/auth/proto/auth.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api/client/auth/proto/auth.proto 3 | 4 | /* 5 | Package auth is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | api/client/auth/proto/auth.proto 9 | 10 | It has these top-level messages: 11 | AuthReq 12 | AuthResp 13 | TopicReq 14 | TopicResp 15 | */ 16 | package auth 17 | 18 | import proto "github.com/golang/protobuf/proto" 19 | import fmt "fmt" 20 | import math "math" 21 | 22 | // Reference imports to suppress errors if they are not otherwise used. 23 | var _ = proto.Marshal 24 | var _ = fmt.Errorf 25 | var _ = math.Inf 26 | 27 | // This is a compile-time assertion to ensure that this generated file 28 | // is compatible with the proto package it is being compiled against. 29 | // A compilation error at this line likely means your copy of the 30 | // proto package needs to be updated. 31 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 32 | 33 | type METHOD int32 34 | 35 | const ( 36 | METHOD_Auth METHOD = 0 37 | METHOD_SubAuth METHOD = 1 38 | METHOD_PubAuth METHOD = 2 39 | ) 40 | 41 | var METHOD_name = map[int32]string{ 42 | 0: "Auth", 43 | 1: "SubAuth", 44 | 2: "PubAuth", 45 | } 46 | var METHOD_value = map[string]int32{ 47 | "Auth": 0, 48 | "SubAuth": 1, 49 | "PubAuth": 2, 50 | } 51 | 52 | func (x METHOD) String() string { 53 | return proto.EnumName(METHOD_name, int32(x)) 54 | } 55 | func (METHOD) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 56 | 57 | type AuthReq struct { 58 | Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"` 59 | Pwd string `protobuf:"bytes,2,opt,name=pwd" json:"pwd,omitempty"` 60 | } 61 | 62 | func (m *AuthReq) Reset() { *m = AuthReq{} } 63 | func (m *AuthReq) String() string { return proto.CompactTextString(m) } 64 | func (*AuthReq) ProtoMessage() {} 65 | func (*AuthReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 66 | 67 | func (m *AuthReq) GetName() string { 68 | if m != nil { 69 | return m.Name 70 | } 71 | return "" 72 | } 73 | 74 | func (m *AuthReq) GetPwd() string { 75 | if m != nil { 76 | return m.Pwd 77 | } 78 | return "" 79 | } 80 | 81 | type AuthResp struct { 82 | Token string `protobuf:"bytes,1,opt,name=token" json:"token,omitempty"` 83 | Verified bool `protobuf:"varint,2,opt,name=verified" json:"verified,omitempty"` 84 | } 85 | 86 | func (m *AuthResp) Reset() { *m = AuthResp{} } 87 | func (m *AuthResp) String() string { return proto.CompactTextString(m) } 88 | func (*AuthResp) ProtoMessage() {} 89 | func (*AuthResp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 90 | 91 | func (m *AuthResp) GetToken() string { 92 | if m != nil { 93 | return m.Token 94 | } 95 | return "" 96 | } 97 | 98 | func (m *AuthResp) GetVerified() bool { 99 | if m != nil { 100 | return m.Verified 101 | } 102 | return false 103 | } 104 | 105 | type TopicReq struct { 106 | ClientId string `protobuf:"bytes,1,opt,name=clientId" json:"clientId,omitempty"` 107 | Topic string `protobuf:"bytes,2,opt,name=topic" json:"topic,omitempty"` 108 | } 109 | 110 | func (m *TopicReq) Reset() { *m = TopicReq{} } 111 | func (m *TopicReq) String() string { return proto.CompactTextString(m) } 112 | func (*TopicReq) ProtoMessage() {} 113 | func (*TopicReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } 114 | 115 | func (m *TopicReq) GetClientId() string { 116 | if m != nil { 117 | return m.ClientId 118 | } 119 | return "" 120 | } 121 | 122 | func (m *TopicReq) GetTopic() string { 123 | if m != nil { 124 | return m.Topic 125 | } 126 | return "" 127 | } 128 | 129 | type TopicResp struct { 130 | Allow bool `protobuf:"varint,1,opt,name=allow" json:"allow,omitempty"` 131 | } 132 | 133 | func (m *TopicResp) Reset() { *m = TopicResp{} } 134 | func (m *TopicResp) String() string { return proto.CompactTextString(m) } 135 | func (*TopicResp) ProtoMessage() {} 136 | func (*TopicResp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } 137 | 138 | func (m *TopicResp) GetAllow() bool { 139 | if m != nil { 140 | return m.Allow 141 | } 142 | return false 143 | } 144 | 145 | func init() { 146 | proto.RegisterType((*AuthReq)(nil), "auth.AuthReq") 147 | proto.RegisterType((*AuthResp)(nil), "auth.AuthResp") 148 | proto.RegisterType((*TopicReq)(nil), "auth.TopicReq") 149 | proto.RegisterType((*TopicResp)(nil), "auth.TopicResp") 150 | proto.RegisterEnum("auth.METHOD", METHOD_name, METHOD_value) 151 | } 152 | 153 | func init() { proto.RegisterFile("api/client/auth/proto/auth.proto", fileDescriptor0) } 154 | 155 | var fileDescriptor0 = []byte{ 156 | // 214 bytes of a gzipped FileDescriptorProto 157 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x3c, 0x90, 0x31, 0x4f, 0x86, 0x30, 158 | 0x10, 0x40, 0xe5, 0x13, 0x3f, 0xca, 0xb9, 0x90, 0x8b, 0x03, 0x61, 0x42, 0x26, 0x63, 0x8c, 0x1d, 159 | 0x5c, 0x5d, 0x4c, 0x34, 0xd1, 0xc1, 0x68, 0x2a, 0x7f, 0xa0, 0x40, 0x0d, 0x8d, 0x48, 0x2b, 0x16, 160 | 0xf9, 0xfb, 0xa6, 0xbd, 0x86, 0xed, 0xbd, 0x26, 0xef, 0xee, 0x52, 0xa8, 0xa5, 0xd5, 0xbc, 0x9f, 161 | 0xb4, 0x9a, 0x1d, 0x97, 0xab, 0x1b, 0xb9, 0x5d, 0x8c, 0x33, 0x01, 0x6f, 0x03, 0x62, 0xea, 0xb9, 162 | 0xe1, 0x90, 0x3d, 0xac, 0x6e, 0x14, 0xea, 0x07, 0x11, 0xd2, 0x59, 0x7e, 0xab, 0x32, 0xa9, 0x93, 163 | 0xab, 0x5c, 0x04, 0xc6, 0x02, 0x4e, 0xed, 0x36, 0x94, 0x87, 0xf0, 0xe4, 0xb1, 0xb9, 0x07, 0x46, 164 | 0xc1, 0xaf, 0xc5, 0x0b, 0x38, 0x73, 0xe6, 0x4b, 0xcd, 0x31, 0x21, 0xc1, 0x0a, 0xd8, 0x9f, 0x5a, 165 | 0xf4, 0xa7, 0x56, 0x14, 0x32, 0xb1, 0xbb, 0xaf, 0x5b, 0x63, 0x75, 0xef, 0xf7, 0x55, 0xc0, 0xe8, 166 | 0xc0, 0x97, 0x21, 0x0e, 0xd8, 0x9d, 0x26, 0x5b, 0xdd, 0xc7, 0xcd, 0x24, 0xcd, 0x25, 0xe4, 0xb1, 167 | 0xa6, 0xe5, 0x72, 0x9a, 0xcc, 0x16, 0x5a, 0x26, 0x48, 0xae, 0x6f, 0xe0, 0xf8, 0xfa, 0xd4, 0x3e, 168 | 0xbf, 0x3d, 0x22, 0x83, 0xd4, 0x1f, 0x5a, 0x9c, 0xe0, 0x39, 0x64, 0x1f, 0x6b, 0x17, 0x24, 0xf1, 169 | 0xf2, 0x1e, 0xe5, 0xd0, 0x1d, 0xc3, 0x57, 0xdc, 0xfd, 0x07, 0x00, 0x00, 0xff, 0xff, 0xae, 0x97, 170 | 0x3b, 0x3a, 0x2e, 0x01, 0x00, 0x00, 171 | } 172 | -------------------------------------------------------------------------------- /pkg/api/client/auth/proto/auth.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package auth; 4 | 5 | enum METHOD { 6 | Auth = 0; 7 | SubAuth = 1; 8 | PubAuth = 2; 9 | } 10 | 11 | message AuthReq { 12 | string name = 1; 13 | string pwd = 2; 14 | 15 | } 16 | 17 | message AuthResp { 18 | string token = 1; 19 | bool verified = 2; 20 | } 21 | 22 | message TopicReq { 23 | string clientId = 1; 24 | string topic = 2; 25 | } 26 | 27 | message TopicResp { 28 | bool allow = 1; 29 | } -------------------------------------------------------------------------------- /pkg/api/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "github.com/smallnest/rpcx/server" 5 | 6 | "github.com/hb-chen/gmqtt/pkg/api/client/auth" 7 | "github.com/hb-chen/gmqtt/pkg/api/client/register" 8 | ) 9 | 10 | func Register(s *server.Server) { 11 | auth.Register(s) 12 | register.Register(s) 13 | } 14 | -------------------------------------------------------------------------------- /pkg/api/client/proto/define.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api/client/proto/define.proto 3 | 4 | /* 5 | Package client is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | api/client/proto/define.proto 9 | 10 | It has these top-level messages: 11 | */ 12 | package client 13 | 14 | import proto "github.com/golang/protobuf/proto" 15 | import fmt "fmt" 16 | import math "math" 17 | 18 | // Reference imports to suppress errors if they are not otherwise used. 19 | var _ = proto.Marshal 20 | var _ = fmt.Errorf 21 | var _ = math.Inf 22 | 23 | // This is a compile-time assertion to ensure that this generated file 24 | // is compatible with the proto package it is being compiled against. 25 | // A compilation error at this line likely means your copy of the 26 | // proto package needs to be updated. 27 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 28 | 29 | type SRV int32 30 | 31 | const ( 32 | SRV_client_auth SRV = 0 33 | SRV_client_register SRV = 1 34 | ) 35 | 36 | var SRV_name = map[int32]string{ 37 | 0: "client_auth", 38 | 1: "client_register", 39 | } 40 | var SRV_value = map[string]int32{ 41 | "client_auth": 0, 42 | "client_register": 1, 43 | } 44 | 45 | func (x SRV) String() string { 46 | return proto.EnumName(SRV_name, int32(x)) 47 | } 48 | func (SRV) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 49 | 50 | func init() { 51 | proto.RegisterEnum("client.SRV", SRV_name, SRV_value) 52 | } 53 | 54 | func init() { proto.RegisterFile("api/client/proto/define.proto", fileDescriptor0) } 55 | 56 | var fileDescriptor0 = []byte{ 57 | // 96 bytes of a gzipped FileDescriptorProto 58 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4d, 0x2c, 0xc8, 0xd4, 59 | 0x4f, 0xce, 0xc9, 0x4c, 0xcd, 0x2b, 0xd1, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0xd7, 0x4f, 0x49, 0x4d, 60 | 0xcb, 0xcc, 0x4b, 0xd5, 0x03, 0x73, 0x84, 0xd8, 0x20, 0x52, 0x5a, 0xda, 0x5c, 0xcc, 0xc1, 0x41, 61 | 0x61, 0x42, 0xfc, 0x5c, 0xdc, 0x10, 0x81, 0xf8, 0xc4, 0xd2, 0x92, 0x0c, 0x01, 0x06, 0x21, 0x61, 62 | 0x2e, 0x7e, 0xa8, 0x40, 0x51, 0x6a, 0x7a, 0x66, 0x71, 0x49, 0x6a, 0x91, 0x00, 0x63, 0x12, 0x1b, 63 | 0x58, 0xaf, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff, 0xc5, 0x35, 0x4b, 0x09, 0x5c, 0x00, 0x00, 0x00, 64 | } 65 | -------------------------------------------------------------------------------- /pkg/api/client/proto/define.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package client; 4 | 5 | enum SRV { 6 | client_auth = 0; 7 | client_register = 1; 8 | } 9 | -------------------------------------------------------------------------------- /pkg/api/client/register/proto/register.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api/client/register/proto/register.proto 3 | 4 | /* 5 | Package api_client_register is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | api/client/register/proto/register.proto 9 | 10 | It has these top-level messages: 11 | RegisterReq 12 | RegisterResp 13 | UnregisterReq 14 | UnregisterResp 15 | */ 16 | package api_client_register 17 | 18 | import proto "github.com/golang/protobuf/proto" 19 | import fmt "fmt" 20 | import math "math" 21 | 22 | // Reference imports to suppress errors if they are not otherwise used. 23 | var _ = proto.Marshal 24 | var _ = fmt.Errorf 25 | var _ = math.Inf 26 | 27 | // This is a compile-time assertion to ensure that this generated file 28 | // is compatible with the proto package it is being compiled against. 29 | // A compilation error at this line likely means your copy of the 30 | // proto package needs to be updated. 31 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 32 | 33 | type METHOD int32 34 | 35 | const ( 36 | METHOD_Register METHOD = 0 37 | METHOD_UnRegister METHOD = 1 38 | ) 39 | 40 | var METHOD_name = map[int32]string{ 41 | 0: "Register", 42 | 1: "UnRegister", 43 | } 44 | var METHOD_value = map[string]int32{ 45 | "Register": 0, 46 | "UnRegister": 1, 47 | } 48 | 49 | func (x METHOD) String() string { 50 | return proto.EnumName(METHOD_name, int32(x)) 51 | } 52 | func (METHOD) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 53 | 54 | type RegisterReq struct { 55 | ClientId string `protobuf:"bytes,1,opt,name=clientId" json:"clientId,omitempty"` 56 | } 57 | 58 | func (m *RegisterReq) Reset() { *m = RegisterReq{} } 59 | func (m *RegisterReq) String() string { return proto.CompactTextString(m) } 60 | func (*RegisterReq) ProtoMessage() {} 61 | func (*RegisterReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 62 | 63 | func (m *RegisterReq) GetClientId() string { 64 | if m != nil { 65 | return m.ClientId 66 | } 67 | return "" 68 | } 69 | 70 | type RegisterResp struct { 71 | ClientId string `protobuf:"bytes,1,opt,name=clientId" json:"clientId,omitempty"` 72 | Name string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"` 73 | Pwd string `protobuf:"bytes,3,opt,name=pwd" json:"pwd,omitempty"` 74 | } 75 | 76 | func (m *RegisterResp) Reset() { *m = RegisterResp{} } 77 | func (m *RegisterResp) String() string { return proto.CompactTextString(m) } 78 | func (*RegisterResp) ProtoMessage() {} 79 | func (*RegisterResp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 80 | 81 | func (m *RegisterResp) GetClientId() string { 82 | if m != nil { 83 | return m.ClientId 84 | } 85 | return "" 86 | } 87 | 88 | func (m *RegisterResp) GetName() string { 89 | if m != nil { 90 | return m.Name 91 | } 92 | return "" 93 | } 94 | 95 | func (m *RegisterResp) GetPwd() string { 96 | if m != nil { 97 | return m.Pwd 98 | } 99 | return "" 100 | } 101 | 102 | type UnregisterReq struct { 103 | ClientId string `protobuf:"bytes,1,opt,name=clientId" json:"clientId,omitempty"` 104 | } 105 | 106 | func (m *UnregisterReq) Reset() { *m = UnregisterReq{} } 107 | func (m *UnregisterReq) String() string { return proto.CompactTextString(m) } 108 | func (*UnregisterReq) ProtoMessage() {} 109 | func (*UnregisterReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } 110 | 111 | func (m *UnregisterReq) GetClientId() string { 112 | if m != nil { 113 | return m.ClientId 114 | } 115 | return "" 116 | } 117 | 118 | type UnregisterResp struct { 119 | ClientId string `protobuf:"bytes,1,opt,name=clientId" json:"clientId,omitempty"` 120 | } 121 | 122 | func (m *UnregisterResp) Reset() { *m = UnregisterResp{} } 123 | func (m *UnregisterResp) String() string { return proto.CompactTextString(m) } 124 | func (*UnregisterResp) ProtoMessage() {} 125 | func (*UnregisterResp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } 126 | 127 | func (m *UnregisterResp) GetClientId() string { 128 | if m != nil { 129 | return m.ClientId 130 | } 131 | return "" 132 | } 133 | 134 | func init() { 135 | proto.RegisterType((*RegisterReq)(nil), "api_client_register.RegisterReq") 136 | proto.RegisterType((*RegisterResp)(nil), "api_client_register.RegisterResp") 137 | proto.RegisterType((*UnregisterReq)(nil), "api_client_register.UnregisterReq") 138 | proto.RegisterType((*UnregisterResp)(nil), "api_client_register.UnregisterResp") 139 | proto.RegisterEnum("api_client_register.METHOD", METHOD_name, METHOD_value) 140 | } 141 | 142 | func init() { proto.RegisterFile("api/client/register/proto/register.proto", fileDescriptor0) } 143 | 144 | var fileDescriptor0 = []byte{ 145 | // 179 bytes of a gzipped FileDescriptorProto 146 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xd2, 0x48, 0x2c, 0xc8, 0xd4, 147 | 0x4f, 0xce, 0xc9, 0x4c, 0xcd, 0x2b, 0xd1, 0x2f, 0x4a, 0x4d, 0xcf, 0x2c, 0x2e, 0x49, 0x2d, 0xd2, 148 | 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0x87, 0x73, 0xf5, 0xc0, 0x5c, 0x21, 0xe1, 0xc4, 0x82, 0xcc, 0x78, 149 | 0x88, 0xca, 0x78, 0x98, 0x94, 0x92, 0x26, 0x17, 0x77, 0x10, 0x94, 0x1d, 0x94, 0x5a, 0x28, 0x24, 150 | 0xc5, 0xc5, 0x01, 0x51, 0xe1, 0x99, 0x22, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x19, 0x04, 0xe7, 0x2b, 151 | 0x05, 0x70, 0xf1, 0x20, 0x94, 0x16, 0x17, 0xe0, 0x53, 0x2b, 0x24, 0xc4, 0xc5, 0x92, 0x97, 0x98, 152 | 0x9b, 0x2a, 0xc1, 0x04, 0x16, 0x07, 0xb3, 0x85, 0x04, 0xb8, 0x98, 0x0b, 0xca, 0x53, 0x24, 0x98, 153 | 0xc1, 0x42, 0x20, 0xa6, 0x92, 0x36, 0x17, 0x6f, 0x68, 0x5e, 0x11, 0x91, 0xd6, 0xeb, 0x70, 0xf1, 154 | 0x21, 0x2b, 0xc6, 0xef, 0x00, 0x2d, 0x35, 0x2e, 0x36, 0x5f, 0xd7, 0x10, 0x0f, 0x7f, 0x17, 0x21, 155 | 0x1e, 0x2e, 0x0e, 0x98, 0xb3, 0x05, 0x18, 0x84, 0xf8, 0xb8, 0xb8, 0x42, 0xf3, 0xe0, 0x7c, 0xc6, 156 | 0x24, 0x36, 0x70, 0xd8, 0x18, 0x03, 0x02, 0x00, 0x00, 0xff, 0xff, 0xde, 0xff, 0x11, 0xfe, 0x47, 157 | 0x01, 0x00, 0x00, 158 | } 159 | -------------------------------------------------------------------------------- /pkg/api/client/register/proto/register.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api_client_register; 4 | 5 | enum METHOD { 6 | Register = 0; 7 | UnRegister = 1; 8 | } 9 | 10 | message RegisterReq { 11 | string clientId = 1; 12 | } 13 | 14 | message RegisterResp { 15 | string clientId = 1; 16 | string name = 2; 17 | string pwd = 3; 18 | } 19 | 20 | message UnregisterReq { 21 | string clientId = 1; 22 | } 23 | 24 | message UnregisterResp { 25 | string clientId = 1; 26 | } -------------------------------------------------------------------------------- /pkg/api/client/register/register.go: -------------------------------------------------------------------------------- 1 | package register 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/smallnest/rpcx/server" 7 | 8 | client "github.com/hb-chen/gmqtt/pkg/api/client/proto" 9 | pb "github.com/hb-chen/gmqtt/pkg/api/client/register/proto" 10 | ) 11 | 12 | func Register(s *server.Server) { 13 | s.RegisterName(client.SRV_client_register.String(), new(register), "") 14 | } 15 | 16 | type register struct { 17 | } 18 | 19 | func (*register) Register(ctx context.Context, req *pb.RegisterReq, resp *pb.RegisterResp) error { 20 | if req.ClientId != "" { 21 | resp.ClientId = "client_id" 22 | } else { 23 | resp.ClientId = req.ClientId 24 | } 25 | 26 | resp.Name = "name" 27 | resp.Pwd = "pwd" 28 | 29 | return nil 30 | } 31 | 32 | func (*register) Unregister(ctx context.Context, req *pb.UnregisterReq, resp *pb.UnregisterResp) error { 33 | if req.ClientId != "" { 34 | resp.ClientId = "client_id" 35 | } else { 36 | resp.ClientId = req.ClientId 37 | } 38 | 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /pkg/api/client/register/register_test.go: -------------------------------------------------------------------------------- 1 | package register 2 | -------------------------------------------------------------------------------- /pkg/api/cluster/clients.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "context" 5 | 6 | pb "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/clients" 7 | ) 8 | 9 | func (*cluster) Clients(ctx context.Context, req *pb.ClientsReq, resp *pb.ClientsResp) error { 10 | // @TODO mock 11 | for i := int64(0); i < req.Size; i++ { 12 | c := &pb.Client{ 13 | Id: "id", 14 | } 15 | 16 | resp.Clients = append(resp.Clients, c) 17 | } 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/api/cluster/cluster.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "github.com/smallnest/rpcx/server" 5 | 6 | pb "github.com/hb-chen/gmqtt/pkg/api/cluster/proto" 7 | ) 8 | 9 | type cluster struct { 10 | } 11 | 12 | func Register(s *server.Server) { 13 | s.RegisterName(pb.SRV_cluster.String(), new(cluster), "") 14 | } 15 | -------------------------------------------------------------------------------- /pkg/api/cluster/proto/clients/clients.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api/cluster/proto/clients/clients.proto 3 | 4 | /* 5 | Package clients is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | api/cluster/proto/clients/clients.proto 9 | 10 | It has these top-level messages: 11 | ClientsReq 12 | ClientsResp 13 | Client 14 | */ 15 | package clients 16 | 17 | import proto "github.com/golang/protobuf/proto" 18 | import fmt "fmt" 19 | import math "math" 20 | import topics "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/topics" 21 | 22 | // Reference imports to suppress errors if they are not otherwise used. 23 | var _ = proto.Marshal 24 | var _ = fmt.Errorf 25 | var _ = math.Inf 26 | 27 | // This is a compile-time assertion to ensure that this generated file 28 | // is compatible with the proto package it is being compiled against. 29 | // A compilation error at this line likely means your copy of the 30 | // proto package needs to be updated. 31 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 32 | 33 | type ClientsReq struct { 34 | Page int64 `protobuf:"varint,1,opt,name=page" json:"page,omitempty"` 35 | Size int64 `protobuf:"varint,2,opt,name=size" json:"size,omitempty"` 36 | } 37 | 38 | func (m *ClientsReq) Reset() { *m = ClientsReq{} } 39 | func (m *ClientsReq) String() string { return proto.CompactTextString(m) } 40 | func (*ClientsReq) ProtoMessage() {} 41 | func (*ClientsReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 42 | 43 | func (m *ClientsReq) GetPage() int64 { 44 | if m != nil { 45 | return m.Page 46 | } 47 | return 0 48 | } 49 | 50 | func (m *ClientsReq) GetSize() int64 { 51 | if m != nil { 52 | return m.Size 53 | } 54 | return 0 55 | } 56 | 57 | type ClientsResp struct { 58 | Clients []*Client `protobuf:"bytes,1,rep,name=clients" json:"clients,omitempty"` 59 | } 60 | 61 | func (m *ClientsResp) Reset() { *m = ClientsResp{} } 62 | func (m *ClientsResp) String() string { return proto.CompactTextString(m) } 63 | func (*ClientsResp) ProtoMessage() {} 64 | func (*ClientsResp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 65 | 66 | func (m *ClientsResp) GetClients() []*Client { 67 | if m != nil { 68 | return m.Clients 69 | } 70 | return nil 71 | } 72 | 73 | type Client struct { 74 | Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` 75 | Topics []*topics.Topic `protobuf:"bytes,2,rep,name=topics" json:"topics,omitempty"` 76 | } 77 | 78 | func (m *Client) Reset() { *m = Client{} } 79 | func (m *Client) String() string { return proto.CompactTextString(m) } 80 | func (*Client) ProtoMessage() {} 81 | func (*Client) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } 82 | 83 | func (m *Client) GetId() string { 84 | if m != nil { 85 | return m.Id 86 | } 87 | return "" 88 | } 89 | 90 | func (m *Client) GetTopics() []*topics.Topic { 91 | if m != nil { 92 | return m.Topics 93 | } 94 | return nil 95 | } 96 | 97 | func init() { 98 | proto.RegisterType((*ClientsReq)(nil), "clients.ClientsReq") 99 | proto.RegisterType((*ClientsResp)(nil), "clients.ClientsResp") 100 | proto.RegisterType((*Client)(nil), "clients.Client") 101 | } 102 | 103 | func init() { proto.RegisterFile("api/cluster/proto/clients/clients.proto", fileDescriptor0) } 104 | 105 | var fileDescriptor0 = []byte{ 106 | // 210 bytes of a gzipped FileDescriptorProto 107 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x8e, 0xbf, 0x4a, 0xc5, 0x30, 108 | 0x14, 0xc6, 0x69, 0xae, 0x54, 0x3c, 0x17, 0x15, 0x32, 0x05, 0xa7, 0x4b, 0x41, 0xac, 0x43, 0x13, 109 | 0x50, 0x07, 0xb7, 0x0e, 0xbe, 0x41, 0xf0, 0x05, 0xda, 0x34, 0xb4, 0x07, 0x5a, 0x93, 0x36, 0xe9, 110 | 0xe2, 0xd3, 0x4b, 0xf3, 0xc7, 0xe5, 0x4e, 0xdf, 0xc9, 0x77, 0x92, 0x5f, 0x7e, 0xf0, 0xd2, 0x59, 111 | 0x14, 0x6a, 0xde, 0x9d, 0xd7, 0x9b, 0xb0, 0x9b, 0xf1, 0x46, 0xa8, 0x19, 0xf5, 0x8f, 0x77, 0x39, 112 | 0x79, 0x68, 0xe9, 0x6d, 0x3a, 0x3e, 0xb5, 0x23, 0xfa, 0x69, 0xef, 0xb9, 0x32, 0x8b, 0x98, 0xfa, 113 | 0x66, 0x34, 0x62, 0x41, 0xb5, 0x99, 0x66, 0x59, 0xc5, 0x35, 0xcb, 0x1b, 0x8b, 0xca, 0xa5, 0x88, 114 | 0xa4, 0xea, 0x03, 0xe0, 0x2b, 0xb2, 0xa4, 0x5e, 0x29, 0x85, 0x1b, 0xdb, 0x8d, 0x9a, 0x15, 0x97, 115 | 0xa2, 0x3e, 0xc9, 0x30, 0x1f, 0x9d, 0xc3, 0x5f, 0xcd, 0x48, 0xec, 0x8e, 0xb9, 0xfa, 0x84, 0xf3, 116 | 0xff, 0x2b, 0x67, 0xe9, 0x2b, 0x64, 0x21, 0x56, 0x5c, 0x4e, 0xf5, 0xf9, 0xed, 0x91, 0x67, 0xdf, 117 | 0x78, 0x4d, 0xe6, 0x7d, 0xd5, 0x42, 0x19, 0x2b, 0xfa, 0x00, 0x04, 0x87, 0xf0, 0xd3, 0x9d, 0x24, 118 | 0x38, 0xd0, 0x67, 0x28, 0xa3, 0x19, 0x23, 0x81, 0x71, 0xcf, 0x93, 0xe8, 0xf7, 0x11, 0x32, 0x2d, 119 | 0xfb, 0x32, 0x78, 0xbf, 0xff, 0x05, 0x00, 0x00, 0xff, 0xff, 0x9c, 0x61, 0xc6, 0x49, 0x2c, 0x01, 120 | 0x00, 0x00, 121 | } 122 | -------------------------------------------------------------------------------- /pkg/api/cluster/proto/clients/clients.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/topics/topics.proto"; 4 | 5 | package clients; 6 | 7 | message ClientsReq { 8 | int64 page = 1; 9 | int64 size = 2; 10 | } 11 | 12 | message ClientsResp { 13 | repeated Client clients = 1; 14 | } 15 | 16 | message Client { 17 | string id = 1; 18 | repeated topics.Topic topics = 2; 19 | } 20 | -------------------------------------------------------------------------------- /pkg/api/cluster/proto/define.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api/cluster/proto/define.proto 3 | 4 | /* 5 | Package cluster is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | api/cluster/proto/define.proto 9 | 10 | It has these top-level messages: 11 | */ 12 | package cluster 13 | 14 | import proto "github.com/golang/protobuf/proto" 15 | import fmt "fmt" 16 | import math "math" 17 | 18 | // Reference imports to suppress errors if they are not otherwise used. 19 | var _ = proto.Marshal 20 | var _ = fmt.Errorf 21 | var _ = math.Inf 22 | 23 | // This is a compile-time assertion to ensure that this generated file 24 | // is compatible with the proto package it is being compiled against. 25 | // A compilation error at this line likely means your copy of the 26 | // proto package needs to be updated. 27 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 28 | 29 | type SRV int32 30 | 31 | const ( 32 | SRV_cluster SRV = 0 33 | ) 34 | 35 | var SRV_name = map[int32]string{ 36 | 0: "cluster", 37 | } 38 | var SRV_value = map[string]int32{ 39 | "cluster": 0, 40 | } 41 | 42 | func (x SRV) String() string { 43 | return proto.EnumName(SRV_name, int32(x)) 44 | } 45 | func (SRV) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 46 | 47 | type METHOD int32 48 | 49 | const ( 50 | METHOD_Clients METHOD = 0 51 | METHOD_Sessions METHOD = 1 52 | METHOD_Subscriptions METHOD = 2 53 | METHOD_Topics METHOD = 3 54 | ) 55 | 56 | var METHOD_name = map[int32]string{ 57 | 0: "Clients", 58 | 1: "Sessions", 59 | 2: "Subscriptions", 60 | 3: "Topics", 61 | } 62 | var METHOD_value = map[string]int32{ 63 | "Clients": 0, 64 | "Sessions": 1, 65 | "Subscriptions": 2, 66 | "Topics": 3, 67 | } 68 | 69 | func (x METHOD) String() string { 70 | return proto.EnumName(METHOD_name, int32(x)) 71 | } 72 | func (METHOD) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 73 | 74 | func init() { 75 | proto.RegisterEnum("cluster.SRV", SRV_name, SRV_value) 76 | proto.RegisterEnum("cluster.METHOD", METHOD_name, METHOD_value) 77 | } 78 | 79 | func init() { proto.RegisterFile("api/cluster/proto/define.proto", fileDescriptor0) } 80 | 81 | var fileDescriptor0 = []byte{ 82 | // 138 bytes of a gzipped FileDescriptorProto 83 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x4b, 0x2c, 0xc8, 0xd4, 84 | 0x4f, 0xce, 0x29, 0x2d, 0x2e, 0x49, 0x2d, 0xd2, 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0xd7, 0x4f, 0x49, 85 | 0x4d, 0xcb, 0xcc, 0x4b, 0xd5, 0x03, 0x73, 0x84, 0xd8, 0xa1, 0x72, 0x5a, 0x42, 0x5c, 0xcc, 0xc1, 86 | 0x41, 0x61, 0x42, 0xdc, 0x5c, 0x30, 0x11, 0x01, 0x06, 0x2d, 0x27, 0x2e, 0x36, 0x5f, 0xd7, 0x10, 87 | 0x0f, 0x7f, 0x17, 0x90, 0xb0, 0x73, 0x4e, 0x66, 0x6a, 0x5e, 0x49, 0xb1, 0x00, 0x83, 0x10, 0x0f, 88 | 0x17, 0x47, 0x70, 0x6a, 0x71, 0x71, 0x66, 0x7e, 0x5e, 0xb1, 0x00, 0xa3, 0x90, 0x20, 0x17, 0x6f, 89 | 0x70, 0x69, 0x52, 0x71, 0x72, 0x51, 0x66, 0x41, 0x09, 0x58, 0x88, 0x49, 0x88, 0x8b, 0x8b, 0x2d, 90 | 0x24, 0xbf, 0x20, 0x33, 0xb9, 0x58, 0x80, 0x39, 0x89, 0x0d, 0x6c, 0x8f, 0x31, 0x20, 0x00, 0x00, 91 | 0xff, 0xff, 0x4e, 0x77, 0x81, 0x5e, 0x89, 0x00, 0x00, 0x00, 92 | } 93 | -------------------------------------------------------------------------------- /pkg/api/cluster/proto/define.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package cluster; 4 | 5 | enum SRV { 6 | cluster = 0; 7 | } 8 | 9 | enum METHOD { 10 | Clients = 0; 11 | Sessions = 1; 12 | Subscriptions = 2; 13 | Topics = 3; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /pkg/api/cluster/proto/sessions/sessions.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api/cluster/proto/sessions/sessions.proto 3 | 4 | /* 5 | Package sessions is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | api/cluster/proto/sessions/sessions.proto 9 | 10 | It has these top-level messages: 11 | SessionsReq 12 | SessionsResp 13 | Session 14 | */ 15 | package sessions 16 | 17 | import proto "github.com/golang/protobuf/proto" 18 | import fmt "fmt" 19 | import math "math" 20 | import topics "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/topics" 21 | 22 | // Reference imports to suppress errors if they are not otherwise used. 23 | var _ = proto.Marshal 24 | var _ = fmt.Errorf 25 | var _ = math.Inf 26 | 27 | // This is a compile-time assertion to ensure that this generated file 28 | // is compatible with the proto package it is being compiled against. 29 | // A compilation error at this line likely means your copy of the 30 | // proto package needs to be updated. 31 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 32 | 33 | type SessionsReq struct { 34 | Page int64 `protobuf:"varint,1,opt,name=page" json:"page,omitempty"` 35 | Size int64 `protobuf:"varint,2,opt,name=size" json:"size,omitempty"` 36 | } 37 | 38 | func (m *SessionsReq) Reset() { *m = SessionsReq{} } 39 | func (m *SessionsReq) String() string { return proto.CompactTextString(m) } 40 | func (*SessionsReq) ProtoMessage() {} 41 | func (*SessionsReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 42 | 43 | func (m *SessionsReq) GetPage() int64 { 44 | if m != nil { 45 | return m.Page 46 | } 47 | return 0 48 | } 49 | 50 | func (m *SessionsReq) GetSize() int64 { 51 | if m != nil { 52 | return m.Size 53 | } 54 | return 0 55 | } 56 | 57 | type SessionsResp struct { 58 | Sessions []*Session `protobuf:"bytes,1,rep,name=sessions" json:"sessions,omitempty"` 59 | } 60 | 61 | func (m *SessionsResp) Reset() { *m = SessionsResp{} } 62 | func (m *SessionsResp) String() string { return proto.CompactTextString(m) } 63 | func (*SessionsResp) ProtoMessage() {} 64 | func (*SessionsResp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 65 | 66 | func (m *SessionsResp) GetSessions() []*Session { 67 | if m != nil { 68 | return m.Sessions 69 | } 70 | return nil 71 | } 72 | 73 | type Session struct { 74 | Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` 75 | Topics []*topics.Topic `protobuf:"bytes,2,rep,name=topics" json:"topics,omitempty"` 76 | } 77 | 78 | func (m *Session) Reset() { *m = Session{} } 79 | func (m *Session) String() string { return proto.CompactTextString(m) } 80 | func (*Session) ProtoMessage() {} 81 | func (*Session) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } 82 | 83 | func (m *Session) GetId() string { 84 | if m != nil { 85 | return m.Id 86 | } 87 | return "" 88 | } 89 | 90 | func (m *Session) GetTopics() []*topics.Topic { 91 | if m != nil { 92 | return m.Topics 93 | } 94 | return nil 95 | } 96 | 97 | func init() { 98 | proto.RegisterType((*SessionsReq)(nil), "sessions.SessionsReq") 99 | proto.RegisterType((*SessionsResp)(nil), "sessions.SessionsResp") 100 | proto.RegisterType((*Session)(nil), "sessions.Session") 101 | } 102 | 103 | func init() { proto.RegisterFile("api/cluster/proto/sessions/sessions.proto", fileDescriptor0) } 104 | 105 | var fileDescriptor0 = []byte{ 106 | // 210 bytes of a gzipped FileDescriptorProto 107 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x8e, 0xc1, 0x4e, 0xc5, 0x20, 108 | 0x10, 0x45, 0x53, 0x9e, 0x79, 0xea, 0x3c, 0x35, 0x91, 0x15, 0x71, 0xd5, 0x34, 0x31, 0xa9, 0x8b, 109 | 0x42, 0xa2, 0x71, 0x69, 0xf4, 0x1b, 0xd0, 0x1f, 0x68, 0x29, 0x69, 0x27, 0xb1, 0x42, 0x3b, 0x74, 110 | 0xe3, 0xd7, 0x9b, 0x52, 0xc4, 0x85, 0xab, 0xb9, 0x5c, 0x86, 0xc3, 0x81, 0x87, 0xd6, 0xa3, 0x32, 111 | 0x9f, 0x2b, 0x05, 0xbb, 0x28, 0xbf, 0xb8, 0xe0, 0x14, 0x59, 0x22, 0x74, 0x5f, 0x94, 0x83, 0x8c, 112 | 0x3d, 0xbf, 0xf8, 0x3d, 0xdf, 0xbd, 0x0e, 0x18, 0xc6, 0xb5, 0x93, 0xc6, 0x4d, 0x6a, 0xec, 0x9a, 113 | 0xc1, 0xa9, 0x09, 0xcd, 0xe2, 0x9a, 0x69, 0x56, 0xff, 0x71, 0xc1, 0x79, 0x34, 0x94, 0xc6, 0x8e, 114 | 0xaa, 0x9e, 0xe1, 0xf4, 0x9e, 0x60, 0xda, 0xce, 0x9c, 0xc3, 0x99, 0x6f, 0x07, 0x2b, 0x8a, 0xb2, 115 | 0xa8, 0x0f, 0x3a, 0xe6, 0xad, 0x23, 0xfc, 0xb6, 0x82, 0xed, 0xdd, 0x96, 0xab, 0x17, 0xb8, 0xfa, 116 | 0x7b, 0x46, 0x9e, 0x37, 0x90, 0x9d, 0x44, 0x51, 0x1e, 0xea, 0xd3, 0xe3, 0xad, 0xcc, 0xd2, 0x69, 117 | 0x53, 0xe7, 0x95, 0xea, 0x0d, 0xce, 0x53, 0xc9, 0x6f, 0x80, 0x61, 0x1f, 0xff, 0xbb, 0xd4, 0x0c, 118 | 0x7b, 0x7e, 0x0f, 0xc7, 0x5d, 0x50, 0xb0, 0xc8, 0xb9, 0x96, 0xc9, 0xf7, 0x63, 0x1b, 0x3a, 0x5d, 119 | 0x76, 0xc7, 0xa8, 0xff, 0xf4, 0x13, 0x00, 0x00, 0xff, 0xff, 0x83, 0x3c, 0xb9, 0x9c, 0x36, 0x01, 120 | 0x00, 0x00, 121 | } 122 | -------------------------------------------------------------------------------- /pkg/api/cluster/proto/sessions/sessions.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/topics/topics.proto"; 4 | 5 | package sessions; 6 | 7 | message SessionsReq { 8 | int64 page = 1; 9 | int64 size = 2; 10 | } 11 | 12 | message SessionsResp { 13 | repeated Session sessions = 1; 14 | } 15 | 16 | message Session { 17 | string id = 1; 18 | repeated topics.Topic topics = 2; 19 | } 20 | -------------------------------------------------------------------------------- /pkg/api/cluster/proto/subscriptions/subscriptions.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api/cluster/proto/subscriptions/subscriptions.proto 3 | 4 | /* 5 | Package subscriptions is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | api/cluster/proto/subscriptions/subscriptions.proto 9 | 10 | It has these top-level messages: 11 | SubscriptionsReq 12 | SubscriptionsResp 13 | Subscriptition 14 | */ 15 | package subscriptions 16 | 17 | import proto "github.com/golang/protobuf/proto" 18 | import fmt "fmt" 19 | import math "math" 20 | import clients "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/clients" 21 | import topics "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/topics" 22 | 23 | // Reference imports to suppress errors if they are not otherwise used. 24 | var _ = proto.Marshal 25 | var _ = fmt.Errorf 26 | var _ = math.Inf 27 | 28 | // This is a compile-time assertion to ensure that this generated file 29 | // is compatible with the proto package it is being compiled against. 30 | // A compilation error at this line likely means your copy of the 31 | // proto package needs to be updated. 32 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 33 | 34 | type SubscriptionsReq struct { 35 | Page int64 `protobuf:"varint,1,opt,name=page" json:"page,omitempty"` 36 | Size int64 `protobuf:"varint,2,opt,name=size" json:"size,omitempty"` 37 | } 38 | 39 | func (m *SubscriptionsReq) Reset() { *m = SubscriptionsReq{} } 40 | func (m *SubscriptionsReq) String() string { return proto.CompactTextString(m) } 41 | func (*SubscriptionsReq) ProtoMessage() {} 42 | func (*SubscriptionsReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 43 | 44 | func (m *SubscriptionsReq) GetPage() int64 { 45 | if m != nil { 46 | return m.Page 47 | } 48 | return 0 49 | } 50 | 51 | func (m *SubscriptionsReq) GetSize() int64 { 52 | if m != nil { 53 | return m.Size 54 | } 55 | return 0 56 | } 57 | 58 | type SubscriptionsResp struct { 59 | Subscriptions []*Subscriptition `protobuf:"bytes,1,rep,name=subscriptions" json:"subscriptions,omitempty"` 60 | } 61 | 62 | func (m *SubscriptionsResp) Reset() { *m = SubscriptionsResp{} } 63 | func (m *SubscriptionsResp) String() string { return proto.CompactTextString(m) } 64 | func (*SubscriptionsResp) ProtoMessage() {} 65 | func (*SubscriptionsResp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 66 | 67 | func (m *SubscriptionsResp) GetSubscriptions() []*Subscriptition { 68 | if m != nil { 69 | return m.Subscriptions 70 | } 71 | return nil 72 | } 73 | 74 | type Subscriptition struct { 75 | Client *clients.Client `protobuf:"bytes,1,opt,name=client" json:"client,omitempty"` 76 | Topic *topics.Topic `protobuf:"bytes,2,opt,name=topic" json:"topic,omitempty"` 77 | } 78 | 79 | func (m *Subscriptition) Reset() { *m = Subscriptition{} } 80 | func (m *Subscriptition) String() string { return proto.CompactTextString(m) } 81 | func (*Subscriptition) ProtoMessage() {} 82 | func (*Subscriptition) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } 83 | 84 | func (m *Subscriptition) GetClient() *clients.Client { 85 | if m != nil { 86 | return m.Client 87 | } 88 | return nil 89 | } 90 | 91 | func (m *Subscriptition) GetTopic() *topics.Topic { 92 | if m != nil { 93 | return m.Topic 94 | } 95 | return nil 96 | } 97 | 98 | func init() { 99 | proto.RegisterType((*SubscriptionsReq)(nil), "subscriptions.SubscriptionsReq") 100 | proto.RegisterType((*SubscriptionsResp)(nil), "subscriptions.SubscriptionsResp") 101 | proto.RegisterType((*Subscriptition)(nil), "subscriptions.Subscriptition") 102 | } 103 | 104 | func init() { 105 | proto.RegisterFile("api/cluster/proto/subscriptions/subscriptions.proto", fileDescriptor0) 106 | } 107 | 108 | var fileDescriptor0 = []byte{ 109 | // 242 bytes of a gzipped FileDescriptorProto 110 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x50, 0x3d, 0x4f, 0xc3, 0x30, 111 | 0x10, 0x55, 0x28, 0x74, 0xb8, 0xa8, 0x7c, 0x78, 0x8a, 0x2a, 0x21, 0x55, 0x61, 0xa0, 0x4b, 0x6d, 112 | 0x29, 0xdd, 0x58, 0x10, 0xea, 0x3f, 0x30, 0x0c, 0x4c, 0x48, 0x8d, 0x65, 0xa5, 0x27, 0x35, 0xb5, 113 | 0xeb, 0x73, 0x16, 0x7e, 0x3d, 0xb2, 0x9d, 0x08, 0x0c, 0x13, 0xd3, 0xbb, 0xaf, 0xf7, 0xf4, 0xde, 114 | 0xc1, 0x76, 0x6f, 0x51, 0xa8, 0xe3, 0x40, 0x5e, 0x3b, 0x61, 0x9d, 0xf1, 0x46, 0xd0, 0xd0, 0x92, 115 | 0x72, 0x68, 0x3d, 0x9a, 0x13, 0xe5, 0x1d, 0x8f, 0x17, 0x6c, 0x91, 0x0d, 0x97, 0x2f, 0x1d, 0xfa, 116 | 0xc3, 0xd0, 0x72, 0x65, 0x7a, 0x71, 0x68, 0x37, 0x9d, 0x11, 0x3d, 0x2a, 0x67, 0x36, 0xfd, 0x59, 117 | 0xfc, 0x55, 0x57, 0x47, 0xd4, 0x27, 0x4f, 0x13, 0x26, 0xc5, 0xe5, 0xf3, 0x7f, 0x24, 0xbc, 0xb1, 118 | 0xa8, 0x68, 0x84, 0x24, 0x50, 0x3f, 0xc1, 0xed, 0xeb, 0x4f, 0x53, 0x52, 0x9f, 0x19, 0x83, 0x4b, 119 | 0xbb, 0xef, 0x74, 0x55, 0xac, 0x8a, 0xf5, 0x4c, 0xc6, 0x3a, 0xcc, 0x08, 0x3f, 0x75, 0x75, 0x91, 120 | 0x66, 0xa1, 0xae, 0xdf, 0xe1, 0xee, 0x17, 0x97, 0x2c, 0xdb, 0x41, 0x9e, 0xb2, 0x2a, 0x56, 0xb3, 121 | 0x75, 0xd9, 0xdc, 0xf3, 0xfc, 0x21, 0xdf, 0xc4, 0xd0, 0xcb, 0x9c, 0x53, 0x7f, 0xc0, 0x75, 0x7e, 122 | 0xc0, 0x1e, 0x61, 0x9e, 0x92, 0x47, 0x57, 0x65, 0x73, 0xc3, 0xa7, 0x47, 0xec, 0x22, 0xca, 0x71, 123 | 0xcd, 0x1e, 0xe0, 0x2a, 0x06, 0x8c, 0x4e, 0xcb, 0x66, 0xc1, 0xc7, 0xb8, 0x6f, 0x01, 0x64, 0xda, 124 | 0xb5, 0xf3, 0x18, 0x7e, 0xfb, 0x15, 0x00, 0x00, 0xff, 0xff, 0xbe, 0x5b, 0x05, 0xea, 0xc6, 0x01, 125 | 0x00, 0x00, 126 | } 127 | -------------------------------------------------------------------------------- /pkg/api/cluster/proto/subscriptions/subscriptions.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/clients/clients.proto"; 4 | import "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/topics/topics.proto"; 5 | 6 | package subscriptions; 7 | 8 | message SubscriptionsReq { 9 | int64 page = 1; 10 | int64 size = 2; 11 | } 12 | 13 | message SubscriptionsResp { 14 | repeated Subscriptition subscriptions = 1; 15 | } 16 | 17 | message Subscriptition { 18 | clients.Client client = 1; 19 | topics.Topic topic = 2; 20 | } 21 | -------------------------------------------------------------------------------- /pkg/api/cluster/proto/topics/topics.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api/cluster/proto/topics/topics.proto 3 | 4 | /* 5 | Package topics is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | api/cluster/proto/topics/topics.proto 9 | 10 | It has these top-level messages: 11 | TopicsReq 12 | TopicsResp 13 | Topic 14 | */ 15 | package topics 16 | 17 | import proto "github.com/golang/protobuf/proto" 18 | import fmt "fmt" 19 | import math "math" 20 | 21 | // Reference imports to suppress errors if they are not otherwise used. 22 | var _ = proto.Marshal 23 | var _ = fmt.Errorf 24 | var _ = math.Inf 25 | 26 | // This is a compile-time assertion to ensure that this generated file 27 | // is compatible with the proto package it is being compiled against. 28 | // A compilation error at this line likely means your copy of the 29 | // proto package needs to be updated. 30 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 31 | 32 | type TopicsReq struct { 33 | Page int64 `protobuf:"varint,1,opt,name=page" json:"page,omitempty"` 34 | Size int64 `protobuf:"varint,2,opt,name=size" json:"size,omitempty"` 35 | } 36 | 37 | func (m *TopicsReq) Reset() { *m = TopicsReq{} } 38 | func (m *TopicsReq) String() string { return proto.CompactTextString(m) } 39 | func (*TopicsReq) ProtoMessage() {} 40 | func (*TopicsReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 41 | 42 | func (m *TopicsReq) GetPage() int64 { 43 | if m != nil { 44 | return m.Page 45 | } 46 | return 0 47 | } 48 | 49 | func (m *TopicsReq) GetSize() int64 { 50 | if m != nil { 51 | return m.Size 52 | } 53 | return 0 54 | } 55 | 56 | type TopicsResp struct { 57 | Topics []*Topic `protobuf:"bytes,1,rep,name=topics" json:"topics,omitempty"` 58 | } 59 | 60 | func (m *TopicsResp) Reset() { *m = TopicsResp{} } 61 | func (m *TopicsResp) String() string { return proto.CompactTextString(m) } 62 | func (*TopicsResp) ProtoMessage() {} 63 | func (*TopicsResp) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } 64 | 65 | func (m *TopicsResp) GetTopics() []*Topic { 66 | if m != nil { 67 | return m.Topics 68 | } 69 | return nil 70 | } 71 | 72 | type Topic struct { 73 | Topic string `protobuf:"bytes,1,opt,name=Topic" json:"Topic,omitempty"` 74 | Qos int32 `protobuf:"varint,2,opt,name=Qos" json:"Qos,omitempty"` 75 | } 76 | 77 | func (m *Topic) Reset() { *m = Topic{} } 78 | func (m *Topic) String() string { return proto.CompactTextString(m) } 79 | func (*Topic) ProtoMessage() {} 80 | func (*Topic) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } 81 | 82 | func (m *Topic) GetTopic() string { 83 | if m != nil { 84 | return m.Topic 85 | } 86 | return "" 87 | } 88 | 89 | func (m *Topic) GetQos() int32 { 90 | if m != nil { 91 | return m.Qos 92 | } 93 | return 0 94 | } 95 | 96 | func init() { 97 | proto.RegisterType((*TopicsReq)(nil), "topics.TopicsReq") 98 | proto.RegisterType((*TopicsResp)(nil), "topics.TopicsResp") 99 | proto.RegisterType((*Topic)(nil), "topics.Topic") 100 | } 101 | 102 | func init() { proto.RegisterFile("api/cluster/proto/topics/topics.proto", fileDescriptor0) } 103 | 104 | var fileDescriptor0 = []byte{ 105 | // 157 bytes of a gzipped FileDescriptorProto 106 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x34, 0x8e, 0xc1, 0x0a, 0xc2, 0x30, 107 | 0x0c, 0x40, 0x99, 0x75, 0x83, 0x45, 0x04, 0x29, 0x1e, 0x7a, 0x1c, 0x83, 0xc1, 0x4e, 0x2b, 0xd8, 108 | 0x2f, 0xb1, 0xf8, 0x03, 0x73, 0x14, 0x29, 0x08, 0x8d, 0x4b, 0xbd, 0xf8, 0xf5, 0xd2, 0x2c, 0x3b, 109 | 0xf5, 0xf5, 0xd1, 0xe6, 0x05, 0x86, 0x19, 0xa3, 0x5d, 0xde, 0x5f, 0xca, 0x61, 0xb5, 0xb8, 0xa6, 110 | 0x9c, 0x6c, 0x4e, 0x18, 0x17, 0x92, 0x63, 0x62, 0xa7, 0x9b, 0xed, 0xd6, 0x3b, 0x68, 0x1f, 0x4c, 111 | 0x3e, 0x7c, 0xb4, 0x86, 0x23, 0xce, 0xaf, 0x60, 0xaa, 0xae, 0x1a, 0x95, 0x67, 0x2e, 0x8e, 0xe2, 112 | 0x2f, 0x98, 0xc3, 0xe6, 0x0a, 0xf7, 0x0e, 0x60, 0xff, 0x44, 0xa8, 0x07, 0x90, 0x61, 0xa6, 0xea, 113 | 0xd4, 0x78, 0xba, 0x9d, 0x27, 0x29, 0xf1, 0x1b, 0xbf, 0x97, 0x2c, 0xd4, 0x2c, 0xf4, 0x55, 0x80, 114 | 0x33, 0xad, 0x17, 0x7b, 0x01, 0x75, 0x4f, 0xc4, 0x99, 0xda, 0x17, 0x7c, 0x36, 0xbc, 0xa9, 0xfb, 115 | 0x07, 0x00, 0x00, 0xff, 0xff, 0x7d, 0x9a, 0xa6, 0xa2, 0xd2, 0x00, 0x00, 0x00, 116 | } 117 | -------------------------------------------------------------------------------- /pkg/api/cluster/proto/topics/topics.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package topics; 4 | 5 | message TopicsReq { 6 | int64 page = 1; 7 | int64 size = 2; 8 | } 9 | 10 | message TopicsResp { 11 | repeated Topic topics = 1; 12 | } 13 | 14 | message Topic { 15 | string Topic = 1; 16 | int32 Qos = 2; 17 | } -------------------------------------------------------------------------------- /pkg/api/cluster/sessions.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "context" 5 | 6 | pb "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/sessions" 7 | ) 8 | 9 | func (*cluster) Sessions(ctx context.Context, req *pb.SessionsReq, resp *pb.SessionsResp) error { 10 | // @TODO mock 11 | for i := int64(0); i < req.Size; i++ { 12 | s := &pb.Session{ 13 | Id: "id", 14 | } 15 | 16 | resp.Sessions = append(resp.Sessions, s) 17 | } 18 | return nil 19 | } 20 | -------------------------------------------------------------------------------- /pkg/api/cluster/subscriptions.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "context" 5 | 6 | pbClients "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/clients" 7 | pb "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/subscriptions" 8 | pbTopics "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/topics" 9 | ) 10 | 11 | func (*cluster) Subscriptions(ctx context.Context, req *pb.SubscriptionsReq, resp *pb.SubscriptionsResp) error { 12 | // @TODO mock 13 | for i := int64(0); i < req.Size; i++ { 14 | c := &pbClients.Client{ 15 | Id: "id", 16 | } 17 | 18 | t := &pbTopics.Topic{ 19 | Topic: "topic", 20 | Qos: 0, 21 | } 22 | 23 | s := &pb.Subscriptition{ 24 | Client: c, 25 | Topic: t, 26 | } 27 | 28 | resp.Subscriptions = append(resp.Subscriptions, s) 29 | } 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/api/cluster/topics.go: -------------------------------------------------------------------------------- 1 | package cluster 2 | 3 | import ( 4 | "context" 5 | 6 | pb "github.com/hb-chen/gmqtt/pkg/api/cluster/proto/topics" 7 | ) 8 | 9 | func (*cluster) Topics(ctx context.Context, req *pb.TopicsReq, resp *pb.TopicsResp) error { 10 | // @TODO mock 11 | for i := int64(0); i < req.Size; i++ { 12 | t := &pb.Topic{ 13 | Topic: "topic", 14 | Qos: 0, 15 | } 16 | 17 | resp.Topics = append(resp.Topics, t) 18 | } 19 | return nil 20 | } 21 | -------------------------------------------------------------------------------- /pkg/api/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | -------------------------------------------------------------------------------- /pkg/api/proto/define.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: api/proto/define.proto 3 | 4 | /* 5 | Package proto is a generated protocol buffer package. 6 | 7 | It is generated from these files: 8 | api/proto/define.proto 9 | 10 | It has these top-level messages: 11 | */ 12 | package proto 13 | 14 | import proto1 "github.com/golang/protobuf/proto" 15 | import fmt "fmt" 16 | import math "math" 17 | 18 | // Reference imports to suppress errors if they are not otherwise used. 19 | var _ = proto1.Marshal 20 | var _ = fmt.Errorf 21 | var _ = math.Inf 22 | 23 | // This is a compile-time assertion to ensure that this generated file 24 | // is compatible with the proto package it is being compiled against. 25 | // A compilation error at this line likely means your copy of the 26 | // proto package needs to be updated. 27 | const _ = proto1.ProtoPackageIsVersion2 // please upgrade the proto package 28 | 29 | type BASE_PATH int32 30 | 31 | const ( 32 | BASE_PATH_api BASE_PATH = 0 33 | ) 34 | 35 | var BASE_PATH_name = map[int32]string{ 36 | 0: "api", 37 | } 38 | var BASE_PATH_value = map[string]int32{ 39 | "api": 0, 40 | } 41 | 42 | func (x BASE_PATH) String() string { 43 | return proto1.EnumName(BASE_PATH_name, int32(x)) 44 | } 45 | func (BASE_PATH) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } 46 | 47 | func init() { 48 | proto1.RegisterEnum("proto.BASE_PATH", BASE_PATH_name, BASE_PATH_value) 49 | } 50 | 51 | func init() { proto1.RegisterFile("api/proto/define.proto", fileDescriptor0) } 52 | 53 | var fileDescriptor0 = []byte{ 54 | // 75 bytes of a gzipped FileDescriptorProto 55 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x12, 0x4b, 0x2c, 0xc8, 0xd4, 56 | 0x2f, 0x28, 0xca, 0x2f, 0xc9, 0xd7, 0x4f, 0x49, 0x4d, 0xcb, 0xcc, 0x4b, 0xd5, 0x03, 0x73, 0x84, 57 | 0x58, 0xc1, 0x94, 0x96, 0x08, 0x17, 0xa7, 0x93, 0x63, 0xb0, 0x6b, 0x7c, 0x80, 0x63, 0x88, 0x87, 58 | 0x10, 0x3b, 0x17, 0x73, 0x62, 0x41, 0xa6, 0x00, 0x43, 0x12, 0x1b, 0x58, 0xd2, 0x18, 0x10, 0x00, 59 | 0x00, 0xff, 0xff, 0x2c, 0x59, 0xdd, 0xae, 0x3d, 0x00, 0x00, 0x00, 60 | } 61 | -------------------------------------------------------------------------------- /pkg/api/proto/define.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package api; 4 | 5 | enum BASE_PATH { 6 | api = 0; 7 | } -------------------------------------------------------------------------------- /pkg/auth/authenticator.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The SurgeMQ Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package auth 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | ) 21 | 22 | var ( 23 | ErrAuthCredType = errors.New("auth: Authentication cred type unsupported") 24 | ErrAuthFailure = errors.New("auth: Authentication failure") 25 | ErrAuthProviderNotFound = errors.New("auth: Authentication provider not found") 26 | 27 | providers = make(map[string]Authenticator) 28 | ) 29 | 30 | type Authenticator interface { 31 | Authenticate(id string, cred interface{}) error 32 | } 33 | 34 | func Register(name string, provider Authenticator) { 35 | if provider == nil { 36 | panic("auth: Register provide is nil") 37 | } 38 | 39 | if _, dup := providers[name]; dup { 40 | panic("auth: Register called twice for provider " + name) 41 | } 42 | 43 | providers[name] = provider 44 | } 45 | 46 | func Unregister(name string) { 47 | delete(providers, name) 48 | } 49 | 50 | type Manager struct { 51 | p Authenticator 52 | } 53 | 54 | func NewManager(providerName string) (*Manager, error) { 55 | p, ok := providers[providerName] 56 | if !ok { 57 | return nil, fmt.Errorf("auth: unknown provider %q", providerName) 58 | } 59 | 60 | return &Manager{p: p}, nil 61 | } 62 | 63 | func (this *Manager) Authenticate(id string, cred interface{}) error { 64 | return this.p.Authenticate(id, cred) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/auth/mock.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The SurgeMQ Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package auth 16 | 17 | type mockAuthenticator bool 18 | 19 | var _ Authenticator = (*mockAuthenticator)(nil) 20 | 21 | const ( 22 | ProviderMockSuccess = "MockSuccess" 23 | ProviderMockFailure = "MockFailure" 24 | ) 25 | 26 | var ( 27 | mockSuccessAuthenticator mockAuthenticator = true 28 | mockFailureAuthenticator mockAuthenticator = false 29 | ) 30 | 31 | func init() { 32 | Register(ProviderMockSuccess, mockSuccessAuthenticator) 33 | Register(ProviderMockFailure, mockFailureAuthenticator) 34 | } 35 | 36 | func (this mockAuthenticator) Authenticate(id string, cred interface{}) error { 37 | if this == true { 38 | return nil 39 | } 40 | 41 | return ErrAuthFailure 42 | } 43 | -------------------------------------------------------------------------------- /pkg/auth/mock_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The SurgeMQ Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package auth 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | ) 22 | 23 | func TestMockSuccessAuthenticator(t *testing.T) { 24 | require.NoError(t, mockSuccessAuthenticator.Authenticate("", "")) 25 | 26 | require.NoError(t, providers["mockSuccess"].Authenticate("", "")) 27 | 28 | mgr, err := NewManager("mockSuccess") 29 | require.NoError(t, err) 30 | require.NoError(t, mgr.Authenticate("", "")) 31 | } 32 | 33 | func TestMockFailureAuthenticator(t *testing.T) { 34 | require.Error(t, mockFailureAuthenticator.Authenticate("", "")) 35 | 36 | require.Error(t, providers["mockFailure"].Authenticate("", "")) 37 | 38 | mgr, err := NewManager("mockFailure") 39 | require.NoError(t, err) 40 | require.Error(t, mgr.Authenticate("", "")) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/auth/rpc.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/smallnest/rpcx/client" 8 | "github.com/smallnest/rpcx/share" 9 | 10 | "github.com/hb-chen/gmqtt/pkg/api/auth" 11 | pbAuth "github.com/hb-chen/gmqtt/pkg/api/client/auth/proto" 12 | pbClient "github.com/hb-chen/gmqtt/pkg/api/client/proto" 13 | pbApi "github.com/hb-chen/gmqtt/pkg/api/proto" 14 | "github.com/hb-chen/gmqtt/pkg/log" 15 | "github.com/hb-chen/gmqtt/pkg/util/conv" 16 | ) 17 | 18 | const ( 19 | ProviderRpc = "rpc" 20 | ) 21 | 22 | type RpcAuthenticator struct { 23 | AccessKeyId string 24 | AccessKeySecret string 25 | xClient client.XClient 26 | consulAddr []string 27 | } 28 | 29 | func NewRpcRegister(ak, sk string, addr []string) io.Closer { 30 | rpcAuth := &RpcAuthenticator{ 31 | AccessKeyId: ak, 32 | AccessKeySecret: sk, 33 | consulAddr: addr, 34 | } 35 | rpcAuth.init() 36 | Register(ProviderRpc, rpcAuth) 37 | 38 | return rpcAuth 39 | } 40 | 41 | func (a *RpcAuthenticator) init() { 42 | d, err := client.NewConsulDiscovery( 43 | conv.ProtoEnumsToRpcxBasePath(pbApi.BASE_PATH_name), 44 | pbClient.SRV_client_auth.String(), 45 | a.consulAddr, 46 | nil, 47 | ) 48 | if err != nil { 49 | panic(err) 50 | } 51 | xc := client.NewXClient(pbClient.SRV_client_auth.String(), client.Failover, client.RoundRobin, d, client.DefaultOption) 52 | xc.Auth(auth.Token(a.AccessKeyId, a.AccessKeySecret, pbClient.SRV_client_auth.String())) 53 | a.xClient = xc 54 | } 55 | 56 | func (a *RpcAuthenticator) Authenticate(id string, cred interface{}) error { 57 | if pwd, ok := cred.(string); !ok { 58 | return ErrAuthCredType 59 | } else { 60 | req := &pbAuth.AuthReq{ 61 | Name: id, 62 | Pwd: pwd, 63 | } 64 | resp := &pbAuth.AuthResp{} 65 | ctx := context.WithValue(context.Background(), share.ReqMetaDataKey, make(map[string]string)) 66 | if err := a.xClient.Call(ctx, pbAuth.METHOD_Auth.String(), req, resp); err != nil { 67 | log.Panic(err) 68 | } 69 | 70 | if !resp.Verified { 71 | return ErrAuthFailure 72 | } else { 73 | return nil 74 | } 75 | } 76 | } 77 | 78 | func (a *RpcAuthenticator) Close() error { 79 | return a.xClient.Close() 80 | } 81 | -------------------------------------------------------------------------------- /pkg/broker/README.md: -------------------------------------------------------------------------------- 1 | ### MQ Broker 2 | 3 | [go-micro/broker](https://github.com/micro/go-micro/tree/master/broker) -------------------------------------------------------------------------------- /pkg/broker/broker.go: -------------------------------------------------------------------------------- 1 | // Package broker is an interface used for asynchronous messaging 2 | package broker 3 | 4 | // Broker is an interface used for asynchronous messaging. 5 | type Broker interface { 6 | Options() Options 7 | Address() string 8 | Connect() error 9 | Disconnect() error 10 | Init(...Option) error 11 | Publish(string, *Message, ...PublishOption) error 12 | Subscribe(string, Handler, ...SubscribeOption) (Subscriber, error) 13 | String() string 14 | } 15 | 16 | // Handler is used to process messages via a subscription of a topic. 17 | // The handler is passed a publication interface which contains the 18 | // message and optional Ack method to acknowledge receipt of the message. 19 | type Handler func(Publication) error 20 | 21 | type Message struct { 22 | Header map[string]string 23 | Body []byte 24 | } 25 | 26 | // Publication is given to a subscription handler for processing 27 | type Publication interface { 28 | Topic() string 29 | Message() *Message 30 | Ack() error 31 | } 32 | 33 | // Subscriber is a convenience return type for the Subscribe method 34 | type Subscriber interface { 35 | Options() SubscribeOptions 36 | Topic() string 37 | Unsubscribe() error 38 | } 39 | 40 | var ( 41 | DefaultBroker Broker = newMockBroker() 42 | ) 43 | 44 | func NewBroker(opts ...Option) Broker { 45 | return newMockBroker() 46 | } 47 | 48 | func Init(opts ...Option) error { 49 | return DefaultBroker.Init(opts...) 50 | } 51 | 52 | func Connect() error { 53 | return DefaultBroker.Connect() 54 | } 55 | 56 | func Disconnect() error { 57 | return DefaultBroker.Disconnect() 58 | } 59 | 60 | func Publish(topic string, msg *Message, opts ...PublishOption) error { 61 | return DefaultBroker.Publish(topic, msg, opts...) 62 | } 63 | 64 | func Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) { 65 | return DefaultBroker.Subscribe(topic, handler, opts...) 66 | } 67 | 68 | func String() string { 69 | return DefaultBroker.String() 70 | } 71 | -------------------------------------------------------------------------------- /pkg/broker/codec/codec.go: -------------------------------------------------------------------------------- 1 | package codec 2 | 3 | // Codec is used for encoding where the broker doesn't natively support 4 | // headers in the message type. In this case the entire message is 5 | // encoded as the payload 6 | type Codec interface { 7 | Marshal(interface{}) ([]byte, error) 8 | Unmarshal([]byte, interface{}) error 9 | String() string 10 | } 11 | -------------------------------------------------------------------------------- /pkg/broker/codec/json/json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/hb-chen/gmqtt/pkg/broker/codec" 7 | ) 8 | 9 | type jsonCodec struct{} 10 | 11 | func (j jsonCodec) Marshal(v interface{}) ([]byte, error) { 12 | return json.Marshal(v) 13 | } 14 | 15 | func (j jsonCodec) Unmarshal(d []byte, v interface{}) error { 16 | return json.Unmarshal(d, v) 17 | } 18 | 19 | func (j jsonCodec) String() string { 20 | return "json" 21 | } 22 | 23 | func NewCodec() codec.Codec { 24 | return jsonCodec{} 25 | } 26 | -------------------------------------------------------------------------------- /pkg/broker/codec/noop/noop.go: -------------------------------------------------------------------------------- 1 | package noop 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/hb-chen/gmqtt/pkg/broker" 7 | "github.com/hb-chen/gmqtt/pkg/broker/codec" 8 | ) 9 | 10 | type noopCodec struct{} 11 | 12 | func (n noopCodec) Marshal(v interface{}) ([]byte, error) { 13 | msg, ok := v.(*broker.Message) 14 | if !ok { 15 | return nil, errors.New("invalid message") 16 | } 17 | return msg.Body, nil 18 | } 19 | 20 | func (n noopCodec) Unmarshal(d []byte, v interface{}) error { 21 | msg, ok := v.(*broker.Message) 22 | if !ok { 23 | return errors.New("invalid message") 24 | } 25 | msg.Body = d 26 | return nil 27 | } 28 | 29 | func (n noopCodec) String() string { 30 | return "noop" 31 | } 32 | 33 | func NewCodec() codec.Codec { 34 | return noopCodec{} 35 | } 36 | -------------------------------------------------------------------------------- /pkg/broker/kafka/kafka.go: -------------------------------------------------------------------------------- 1 | // +build kafka 2 | // Package kafka provides a kafka broker using sarama cluster 3 | package kafka 4 | 5 | import ( 6 | "sync" 7 | 8 | "github.com/Shopify/sarama" 9 | "github.com/pborman/uuid" 10 | sc "gopkg.in/bsm/sarama-cluster.v2" 11 | 12 | "github.com/hb-chen/gmqtt/pkg/broker" 13 | "github.com/hb-chen/gmqtt/pkg/broker/codec/json" 14 | "github.com/hb-chen/gmqtt/pkg/log" 15 | ) 16 | 17 | const BrokerKafka = "kafka" 18 | 19 | type kBroker struct { 20 | addrs []string 21 | 22 | c sarama.Client 23 | p sarama.SyncProducer 24 | sc []*sc.Client 25 | 26 | scMutex sync.Mutex 27 | opts broker.Options 28 | } 29 | 30 | type subscriber struct { 31 | s *sc.Consumer 32 | t string 33 | opts broker.SubscribeOptions 34 | } 35 | 36 | type publication struct { 37 | t string 38 | c *sc.Consumer 39 | km *sarama.ConsumerMessage 40 | m *broker.Message 41 | } 42 | 43 | func init() { 44 | } 45 | 46 | func (p *publication) Topic() string { 47 | return p.t 48 | } 49 | 50 | func (p *publication) Message() *broker.Message { 51 | return p.m 52 | } 53 | 54 | func (p *publication) Ack() error { 55 | p.c.MarkOffset(p.km, "") 56 | return nil 57 | } 58 | 59 | func (s *subscriber) Options() broker.SubscribeOptions { 60 | return s.opts 61 | } 62 | 63 | func (s *subscriber) Topic() string { 64 | return s.t 65 | } 66 | 67 | func (s *subscriber) Unsubscribe() error { 68 | return s.s.Close() 69 | } 70 | 71 | func (k *kBroker) Address() string { 72 | if len(k.addrs) > 0 { 73 | return k.addrs[0] 74 | } 75 | return "127.0.0.1:9092" 76 | } 77 | 78 | func (k *kBroker) Connect() error { 79 | if k.c != nil { 80 | return nil 81 | } 82 | 83 | pconfig := sarama.NewConfig() 84 | // For implementation reasons, the SyncProducer requires 85 | // `Producer.Return.Errors` and `Producer.Return.Successes` 86 | // to be set to true in its configuration. 87 | pconfig.Producer.Return.Successes = true 88 | pconfig.Producer.Return.Errors = true 89 | 90 | pconfig.Consumer.Return.Errors = true 91 | 92 | c, err := sarama.NewClient(k.addrs, pconfig) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | k.c = c 98 | 99 | // Client创建后出现err,做Close()操作 100 | defer func() { 101 | if err != nil { 102 | if k.p != nil { 103 | k.p.Close() 104 | } 105 | k.c.Close() 106 | } 107 | }() 108 | 109 | p, err := sarama.NewSyncProducerFromClient(c) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | k.p = p 115 | k.scMutex.Lock() 116 | defer k.scMutex.Unlock() 117 | k.sc = make([]*sc.Client, 0) 118 | 119 | return nil 120 | } 121 | 122 | func (k *kBroker) Disconnect() error { 123 | k.scMutex.Lock() 124 | defer k.scMutex.Unlock() 125 | for _, client := range k.sc { 126 | client.Close() 127 | } 128 | k.sc = nil 129 | k.p.Close() 130 | return k.c.Close() 131 | } 132 | 133 | func (k *kBroker) Init(opts ...broker.Option) error { 134 | for _, o := range opts { 135 | o(&k.opts) 136 | } 137 | return nil 138 | } 139 | 140 | func (k *kBroker) Options() broker.Options { 141 | return k.opts 142 | } 143 | 144 | func (k *kBroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error { 145 | b, err := k.opts.Codec.Marshal(msg) 146 | if err != nil { 147 | return err 148 | } 149 | partition, offset, err := k.p.SendMessage(&sarama.ProducerMessage{ 150 | Topic: topic, 151 | Value: sarama.ByteEncoder(b), 152 | }) 153 | 154 | log.Debugf("broker kafka: sent message partition:%d, offset:%d", partition, offset) 155 | return err 156 | } 157 | 158 | func (k *kBroker) getSaramaClusterClient(topic string) (*sc.Client, error) { 159 | config := sc.NewConfig() 160 | 161 | // TODO: make configurable offset as SubscriberOption 162 | config.Config.Consumer.Offsets.Initial = sarama.OffsetNewest 163 | 164 | cs, err := sc.NewClient(k.addrs, config) 165 | if err != nil { 166 | return nil, err 167 | } 168 | 169 | k.scMutex.Lock() 170 | defer k.scMutex.Unlock() 171 | k.sc = append(k.sc, cs) 172 | return cs, nil 173 | } 174 | 175 | func (k *kBroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) { 176 | opt := broker.SubscribeOptions{ 177 | AutoAck: true, 178 | Queue: uuid.NewUUID().String(), 179 | } 180 | 181 | for _, o := range opts { 182 | o(&opt) 183 | } 184 | 185 | // @TODO 多协程Consumer 186 | 187 | // we need to create a new client per consumer 188 | cs, err := k.getSaramaClusterClient(topic) 189 | if err != nil { 190 | return nil, err 191 | } 192 | 193 | c, err := sc.NewConsumerFromClient(cs, opt.Queue, []string{topic}) 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | go func() { 199 | for { 200 | select { 201 | case err := <-c.Errors(): 202 | // @TODO 根据错误类型的重试、退出、报警等 203 | if err != nil { 204 | log.Errorf("broker kafka: handler error:%v", err) 205 | continue 206 | } else { 207 | return 208 | } 209 | case sm := <-c.Messages(): 210 | // ensure message is not nil 211 | if sm == nil { 212 | continue 213 | } 214 | 215 | log.Debugf("broker kafka: receive message partition:%d, offset:%d", sm.Partition, sm.Offset) 216 | 217 | var m broker.Message 218 | if err := k.opts.Codec.Unmarshal(sm.Value, &m); err != nil { 219 | log.Errorf("broker kafka: handler unmarshal error:%v", err) 220 | continue 221 | } 222 | if err := handler(&publication{ 223 | m: &m, 224 | t: sm.Topic, 225 | c: c, 226 | km: sm, 227 | }); err == nil && opt.AutoAck { 228 | c.MarkOffset(sm, "") 229 | } else if err != nil { 230 | log.Errorf("broker kafka: handler error:%v", err) 231 | } 232 | } 233 | } 234 | }() 235 | 236 | return &subscriber{s: c, opts: opt}, nil 237 | } 238 | 239 | func (k *kBroker) String() string { 240 | return BrokerKafka 241 | } 242 | 243 | func NewBroker(opts ...broker.Option) broker.Broker { 244 | options := broker.Options{ 245 | // default to json codec 246 | Codec: json.NewCodec(), 247 | } 248 | 249 | for _, o := range opts { 250 | o(&options) 251 | } 252 | 253 | var cAddrs []string 254 | for _, addr := range options.Addrs { 255 | if len(addr) == 0 { 256 | continue 257 | } 258 | cAddrs = append(cAddrs, addr) 259 | } 260 | if len(cAddrs) == 0 { 261 | cAddrs = []string{"127.0.0.1:9092"} 262 | } 263 | 264 | return &kBroker{ 265 | addrs: cAddrs, 266 | opts: options, 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /pkg/broker/mock.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import ( 4 | "errors" 5 | "sync" 6 | 7 | "github.com/pborman/uuid" 8 | ) 9 | 10 | const BrokerMock = "mock" 11 | 12 | type mockBroker struct { 13 | opts Options 14 | 15 | sync.RWMutex 16 | connected bool 17 | Subscribers map[string][]*mockSubscriber 18 | } 19 | 20 | type mockPublication struct { 21 | topic string 22 | message *Message 23 | } 24 | 25 | type mockSubscriber struct { 26 | id string 27 | topic string 28 | exit chan bool 29 | handler Handler 30 | opts SubscribeOptions 31 | } 32 | 33 | func (m *mockBroker) Options() Options { 34 | return m.opts 35 | } 36 | 37 | func (m *mockBroker) Address() string { 38 | return "" 39 | } 40 | 41 | func (m *mockBroker) Connect() error { 42 | m.Lock() 43 | defer m.Unlock() 44 | 45 | if m.connected { 46 | return nil 47 | } 48 | 49 | m.connected = true 50 | 51 | return nil 52 | } 53 | 54 | func (m *mockBroker) Disconnect() error { 55 | m.Lock() 56 | defer m.Unlock() 57 | 58 | if !m.connected { 59 | return nil 60 | } 61 | 62 | m.connected = false 63 | 64 | return nil 65 | } 66 | 67 | func (m *mockBroker) Init(opts ...Option) error { 68 | for _, o := range opts { 69 | o(&m.opts) 70 | } 71 | return nil 72 | } 73 | 74 | func (m *mockBroker) Publish(topic string, message *Message, opts ...PublishOption) error { 75 | m.Lock() 76 | defer m.Unlock() 77 | 78 | if !m.connected { 79 | return errors.New("not connected") 80 | } 81 | 82 | subs, ok := m.Subscribers[topic] 83 | if !ok { 84 | return nil 85 | } 86 | 87 | p := &mockPublication{ 88 | topic: topic, 89 | message: message, 90 | } 91 | 92 | for _, sub := range subs { 93 | if err := sub.handler(p); err != nil { 94 | return err 95 | } 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func (m *mockBroker) Subscribe(topic string, handler Handler, opts ...SubscribeOption) (Subscriber, error) { 102 | m.Lock() 103 | defer m.Unlock() 104 | 105 | if !m.connected { 106 | return nil, errors.New("not connected") 107 | } 108 | 109 | var options SubscribeOptions 110 | for _, o := range opts { 111 | o(&options) 112 | } 113 | 114 | sub := &mockSubscriber{ 115 | exit: make(chan bool, 1), 116 | id: uuid.NewUUID().String(), 117 | topic: topic, 118 | handler: handler, 119 | opts: options, 120 | } 121 | 122 | m.Subscribers[topic] = append(m.Subscribers[topic], sub) 123 | 124 | go func() { 125 | <-sub.exit 126 | m.Lock() 127 | var newSubscribers []*mockSubscriber 128 | for _, sb := range m.Subscribers[topic] { 129 | if sb.id == sub.id { 130 | continue 131 | } 132 | newSubscribers = append(newSubscribers, sb) 133 | } 134 | m.Subscribers[topic] = newSubscribers 135 | m.Unlock() 136 | }() 137 | 138 | return sub, nil 139 | } 140 | 141 | func (m *mockBroker) String() string { 142 | return BrokerMock 143 | } 144 | 145 | func (m *mockPublication) Topic() string { 146 | return m.topic 147 | } 148 | 149 | func (m *mockPublication) Message() *Message { 150 | return m.message 151 | } 152 | 153 | func (m *mockPublication) Ack() error { 154 | return nil 155 | } 156 | 157 | func (m *mockSubscriber) Options() SubscribeOptions { 158 | return m.opts 159 | } 160 | 161 | func (m *mockSubscriber) Topic() string { 162 | return m.topic 163 | } 164 | 165 | func (m *mockSubscriber) Unsubscribe() error { 166 | m.exit <- true 167 | return nil 168 | } 169 | 170 | func newMockBroker(opts ...Option) Broker { 171 | var options Options 172 | for _, o := range opts { 173 | o(&options) 174 | } 175 | 176 | return &mockBroker{ 177 | opts: options, 178 | Subscribers: make(map[string][]*mockSubscriber), 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /pkg/broker/mock/mock.go: -------------------------------------------------------------------------------- 1 | package mock 2 | 3 | import ( 4 | "github.com/hb-chen/gmqtt/pkg/broker" 5 | ) 6 | 7 | func NewBroker(opts ...broker.Option) broker.Broker { 8 | return broker.NewBroker(opts...) 9 | } 10 | -------------------------------------------------------------------------------- /pkg/broker/mock_test.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestBroker(t *testing.T) { 9 | b := NewBroker() 10 | 11 | if err := b.Connect(); err != nil { 12 | t.Fatalf("Unexpected connect error %v", err) 13 | } 14 | 15 | topic := "test" 16 | count := 10 17 | 18 | fn := func(p Publication) error { 19 | return nil 20 | } 21 | 22 | sub, err := b.Subscribe(topic, fn) 23 | if err != nil { 24 | t.Fatalf("Unexpected error subscribing %v", err) 25 | } 26 | 27 | for i := 0; i < count; i++ { 28 | message := &Message{ 29 | Header: map[string]string{ 30 | "foo": "bar", 31 | "id": fmt.Sprintf("%d", i), 32 | }, 33 | Body: []byte(`hello world`), 34 | } 35 | 36 | if err := b.Publish(topic, message); err != nil { 37 | t.Fatalf("Unexpected error publishing %d", i) 38 | } 39 | } 40 | 41 | if err := sub.Unsubscribe(); err != nil { 42 | t.Fatalf("Unexpected error unsubscribing from %s: %v", topic, err) 43 | } 44 | 45 | if err := b.Disconnect(); err != nil { 46 | t.Fatalf("Unexpected connect error %v", err) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/broker/options.go: -------------------------------------------------------------------------------- 1 | package broker 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | 7 | "github.com/hb-chen/gmqtt/pkg/broker/codec" 8 | ) 9 | 10 | type Options struct { 11 | Addrs []string 12 | Secure bool 13 | Codec codec.Codec 14 | TLSConfig *tls.Config 15 | // Other options for implementations of the interface 16 | // can be stored in a context 17 | Context context.Context 18 | } 19 | 20 | type PublishOptions struct { 21 | // Other options for implementations of the interface 22 | // can be stored in a context 23 | Context context.Context 24 | } 25 | 26 | type SubscribeOptions struct { 27 | // AutoAck defaults to true. When a handler returns 28 | // with a nil error the message is acked. 29 | AutoAck bool 30 | // Subscribers with the same queue name 31 | // will create a shared subscription where each 32 | // receives a subset of messages. 33 | Queue string 34 | 35 | // Other options for implementations of the interface 36 | // can be stored in a context 37 | Context context.Context 38 | } 39 | 40 | type Option func(*Options) 41 | 42 | type PublishOption func(*PublishOptions) 43 | 44 | type SubscribeOption func(*SubscribeOptions) 45 | 46 | type contextKeyT string 47 | 48 | func newSubscribeOptions(opts ...SubscribeOption) SubscribeOptions { 49 | opt := SubscribeOptions{ 50 | AutoAck: true, 51 | } 52 | 53 | for _, o := range opts { 54 | o(&opt) 55 | } 56 | 57 | return opt 58 | } 59 | 60 | // Addrs sets the host addresses to be used by the broker 61 | func Addrs(addrs ...string) Option { 62 | return func(o *Options) { 63 | o.Addrs = addrs 64 | } 65 | } 66 | 67 | // Codec sets the codec used for encoding/decoding used where 68 | // a broker does not support headers 69 | func Codec(c codec.Codec) Option { 70 | return func(o *Options) { 71 | o.Codec = c 72 | } 73 | } 74 | 75 | // DisableAutoAck will disable auto acking of messages 76 | // after they have been handled. 77 | func DisableAutoAck() SubscribeOption { 78 | return func(o *SubscribeOptions) { 79 | o.AutoAck = false 80 | } 81 | } 82 | 83 | // Queue sets the name of the queue to share messages on 84 | func Queue(name string) SubscribeOption { 85 | return func(o *SubscribeOptions) { 86 | o.Queue = name 87 | } 88 | } 89 | 90 | // Secure communication with the broker 91 | func Secure(b bool) Option { 92 | return func(o *Options) { 93 | o.Secure = b 94 | } 95 | } 96 | 97 | // Specify TLS Config 98 | func TLSConfig(t *tls.Config) Option { 99 | return func(o *Options) { 100 | o.TLSConfig = t 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/broker/rabbitmq/channel.go: -------------------------------------------------------------------------------- 1 | // +build rabbitmq 2 | package rabbitmq 3 | 4 | // 5 | // All credit to Mondo 6 | // 7 | 8 | import ( 9 | "errors" 10 | 11 | "github.com/nu7hatch/gouuid" 12 | "github.com/streadway/amqp" 13 | ) 14 | 15 | type rabbitMQChannel struct { 16 | uuid string 17 | connection *amqp.Connection 18 | channel *amqp.Channel 19 | } 20 | 21 | func newRabbitChannel(conn *amqp.Connection) (*rabbitMQChannel, error) { 22 | id, err := uuid.NewV4() 23 | if err != nil { 24 | return nil, err 25 | } 26 | rabbitCh := &rabbitMQChannel{ 27 | uuid: id.String(), 28 | connection: conn, 29 | } 30 | if err := rabbitCh.Connect(); err != nil { 31 | return nil, err 32 | } 33 | return rabbitCh, nil 34 | 35 | } 36 | 37 | func (r *rabbitMQChannel) Connect() error { 38 | var err error 39 | r.channel, err = r.connection.Channel() 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } 45 | 46 | func (r *rabbitMQChannel) Close() error { 47 | if r.channel == nil { 48 | return errors.New("Channel is nil") 49 | } 50 | return r.channel.Close() 51 | } 52 | 53 | func (r *rabbitMQChannel) Publish(exchange, key string, message amqp.Publishing) error { 54 | if r.channel == nil { 55 | return errors.New("Channel is nil") 56 | } 57 | return r.channel.Publish(exchange, key, false, false, message) 58 | } 59 | 60 | func (r *rabbitMQChannel) DeclareExchange(exchange string) error { 61 | return r.channel.ExchangeDeclare( 62 | exchange, // name 63 | "topic", // kind 64 | false, // durable 65 | false, // autoDelete 66 | false, // internal 67 | false, // noWait 68 | nil, // args 69 | ) 70 | } 71 | 72 | func (r *rabbitMQChannel) DeclareQueue(queue string) error { 73 | _, err := r.channel.QueueDeclare( 74 | queue, // name 75 | false, // durable 76 | true, // autoDelete 77 | false, // exclusive 78 | false, // noWait 79 | nil, // args 80 | ) 81 | return err 82 | } 83 | 84 | func (r *rabbitMQChannel) DeclareDurableQueue(queue string) error { 85 | _, err := r.channel.QueueDeclare( 86 | queue, // name 87 | true, // durable 88 | false, // autoDelete 89 | false, // exclusive 90 | false, // noWait 91 | nil, // args 92 | ) 93 | return err 94 | } 95 | 96 | func (r *rabbitMQChannel) DeclareReplyQueue(queue string) error { 97 | _, err := r.channel.QueueDeclare( 98 | queue, // name 99 | false, // durable 100 | true, // autoDelete 101 | true, // exclusive 102 | false, // noWait 103 | nil, // args 104 | ) 105 | return err 106 | } 107 | 108 | func (r *rabbitMQChannel) ConsumeQueue(queue string, autoAck bool) (<-chan amqp.Delivery, error) { 109 | return r.channel.Consume( 110 | queue, // queue 111 | r.uuid, // consumer 112 | autoAck, // autoAck 113 | false, // exclusive 114 | false, // nolocal 115 | false, // nowait 116 | nil, // args 117 | ) 118 | } 119 | 120 | func (r *rabbitMQChannel) BindQueue(queue, key, exchange string, args amqp.Table) error { 121 | return r.channel.QueueBind( 122 | queue, // name 123 | key, // key 124 | exchange, // exchange 125 | false, // noWait 126 | args, // args 127 | ) 128 | } 129 | -------------------------------------------------------------------------------- /pkg/broker/rabbitmq/connection.go: -------------------------------------------------------------------------------- 1 | // +build rabbitmq 2 | package rabbitmq 3 | 4 | // 5 | // All credit to Mondo 6 | // 7 | 8 | import ( 9 | "crypto/tls" 10 | "regexp" 11 | "strings" 12 | "sync" 13 | "time" 14 | 15 | "github.com/streadway/amqp" 16 | ) 17 | 18 | var ( 19 | DefaultExchange = "micro" 20 | DefaultRabbitURL = "amqp://guest:guest@127.0.0.1:5672" 21 | 22 | dial = amqp.Dial 23 | dialTLS = amqp.DialTLS 24 | ) 25 | 26 | type rabbitMQConn struct { 27 | Connection *amqp.Connection 28 | Channel *rabbitMQChannel 29 | ExchangeChannel *rabbitMQChannel 30 | exchange string 31 | url string 32 | 33 | sync.Mutex 34 | connected bool 35 | close chan bool 36 | } 37 | 38 | func newRabbitMQConn(exchange string, urls []string) *rabbitMQConn { 39 | var url string 40 | 41 | if len(urls) > 0 && regexp.MustCompile("^amqp(s)?://.*").MatchString(urls[0]) { 42 | url = urls[0] 43 | } else { 44 | url = DefaultRabbitURL 45 | } 46 | 47 | if len(exchange) == 0 { 48 | exchange = DefaultExchange 49 | } 50 | 51 | return &rabbitMQConn{ 52 | exchange: exchange, 53 | url: url, 54 | close: make(chan bool), 55 | } 56 | } 57 | 58 | func (r *rabbitMQConn) connect(secure bool, config *tls.Config) error { 59 | // try connect 60 | if err := r.tryConnect(secure, config); err != nil { 61 | return err 62 | } 63 | 64 | // connected 65 | r.Lock() 66 | r.connected = true 67 | r.Unlock() 68 | 69 | // create reconnect loop 70 | go r.reconnect(secure, config) 71 | return nil 72 | } 73 | 74 | func (r *rabbitMQConn) reconnect(secure bool, config *tls.Config) { 75 | // skip first connect 76 | var connect bool 77 | 78 | for { 79 | if connect { 80 | // try reconnect 81 | if err := r.tryConnect(secure, config); err != nil { 82 | time.Sleep(1 * time.Second) 83 | continue 84 | } 85 | 86 | // connected 87 | r.Lock() 88 | r.connected = true 89 | r.Unlock() 90 | } 91 | 92 | connect = true 93 | notifyClose := make(chan *amqp.Error) 94 | r.Connection.NotifyClose(notifyClose) 95 | 96 | // block until closed 97 | select { 98 | case <-notifyClose: 99 | r.Lock() 100 | r.connected = false 101 | r.Unlock() 102 | case <-r.close: 103 | return 104 | } 105 | } 106 | } 107 | 108 | func (r *rabbitMQConn) Connect(secure bool, config *tls.Config) error { 109 | r.Lock() 110 | 111 | // already connected 112 | if r.connected { 113 | r.Unlock() 114 | return nil 115 | } 116 | 117 | // check it was closed 118 | select { 119 | case <-r.close: 120 | r.close = make(chan bool) 121 | default: 122 | // no op 123 | // new conn 124 | } 125 | 126 | r.Unlock() 127 | 128 | return r.connect(secure, config) 129 | } 130 | 131 | func (r *rabbitMQConn) Close() error { 132 | r.Lock() 133 | defer r.Unlock() 134 | 135 | select { 136 | case <-r.close: 137 | return nil 138 | default: 139 | close(r.close) 140 | r.connected = false 141 | } 142 | 143 | return r.Connection.Close() 144 | } 145 | 146 | func (r *rabbitMQConn) tryConnect(secure bool, config *tls.Config) error { 147 | var err error 148 | 149 | if secure || config != nil || strings.HasPrefix(r.url, "amqps://") { 150 | if config == nil { 151 | config = &tls.Config{ 152 | InsecureSkipVerify: true, 153 | } 154 | } 155 | 156 | url := strings.Replace(r.url, "amqp://", "amqps://", 1) 157 | r.Connection, err = dialTLS(url, config) 158 | } else { 159 | r.Connection, err = dial(r.url) 160 | } 161 | 162 | if err != nil { 163 | return err 164 | } 165 | 166 | if r.Channel, err = newRabbitChannel(r.Connection); err != nil { 167 | return err 168 | } 169 | 170 | r.Channel.DeclareExchange(r.exchange) 171 | r.ExchangeChannel, err = newRabbitChannel(r.Connection) 172 | 173 | return err 174 | } 175 | 176 | func (r *rabbitMQConn) Consume(queue, key string, headers amqp.Table, autoAck, durableQueue bool) (*rabbitMQChannel, <-chan amqp.Delivery, error) { 177 | consumerChannel, err := newRabbitChannel(r.Connection) 178 | if err != nil { 179 | return nil, nil, err 180 | } 181 | 182 | if durableQueue { 183 | err = consumerChannel.DeclareDurableQueue(queue) 184 | } else { 185 | err = consumerChannel.DeclareQueue(queue) 186 | } 187 | 188 | if err != nil { 189 | return nil, nil, err 190 | } 191 | 192 | deliveries, err := consumerChannel.ConsumeQueue(queue, autoAck) 193 | if err != nil { 194 | return nil, nil, err 195 | } 196 | 197 | err = consumerChannel.BindQueue(queue, key, r.exchange, headers) 198 | if err != nil { 199 | return nil, nil, err 200 | } 201 | 202 | return consumerChannel, deliveries, nil 203 | } 204 | 205 | func (r *rabbitMQConn) Publish(exchange, key string, msg amqp.Publishing) error { 206 | return r.ExchangeChannel.Publish(exchange, key, msg) 207 | } 208 | -------------------------------------------------------------------------------- /pkg/broker/rabbitmq/connection_test.go: -------------------------------------------------------------------------------- 1 | // +build rabbitmq 2 | package rabbitmq 3 | 4 | import ( 5 | "crypto/tls" 6 | "errors" 7 | "testing" 8 | 9 | "github.com/streadway/amqp" 10 | ) 11 | 12 | func TestNewRabbitMQConnURL(t *testing.T) { 13 | testcases := []struct { 14 | title string 15 | urls []string 16 | want string 17 | }{ 18 | {"Multiple URLs", []string{"amqp://example.com/one", "amqp://example.com/two"}, "amqp://example.com/one"}, 19 | {"Insecure URL", []string{"amqp://example.com"}, "amqp://example.com"}, 20 | {"Secure URL", []string{"amqps://example.com"}, "amqps://example.com"}, 21 | {"Invalid URL", []string{"http://example.com"}, DefaultRabbitURL}, 22 | {"No URLs", []string{}, DefaultRabbitURL}, 23 | } 24 | 25 | for _, test := range testcases { 26 | conn := newRabbitMQConn("exchange", test.urls) 27 | 28 | if have, want := conn.url, test.want; have != want { 29 | t.Errorf("%s: invalid url, want %q, have %q", test.title, want, have) 30 | } 31 | } 32 | } 33 | 34 | func TestTryToConnectTLS(t *testing.T) { 35 | var ( 36 | dialCount, dialTLSCount int 37 | 38 | err = errors.New("stop connect here") 39 | ) 40 | 41 | dial = func(_ string) (*amqp.Connection, error) { 42 | dialCount++ 43 | return nil, err 44 | } 45 | 46 | dialTLS = func(_ string, _ *tls.Config) (*amqp.Connection, error) { 47 | dialTLSCount++ 48 | return nil, err 49 | } 50 | 51 | testcases := []struct { 52 | title string 53 | url string 54 | secure bool 55 | tlsConfig *tls.Config 56 | wantTLS bool 57 | }{ 58 | {"unsecure url, secure false, no tls config", "amqp://example.com", false, nil, false}, 59 | {"secure url, secure false, no tls config", "amqps://example.com", false, nil, true}, 60 | {"unsecure url, secure true, no tls config", "amqp://example.com", true, nil, true}, 61 | {"unsecure url, secure false, tls config", "amqp://example.com", false, &tls.Config{}, true}, 62 | } 63 | 64 | for _, test := range testcases { 65 | dialCount, dialTLSCount = 0, 0 66 | 67 | conn := newRabbitMQConn("exchange", []string{test.url}) 68 | conn.tryConnect(test.secure, test.tlsConfig) 69 | 70 | have := dialCount 71 | if test.wantTLS { 72 | have = dialTLSCount 73 | } 74 | 75 | if have != 1 { 76 | t.Errorf("%s: used wrong dialer, Dial called %d times, DialTLS called %d times", test.title, dialCount, dialTLSCount) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pkg/broker/rabbitmq/options.go: -------------------------------------------------------------------------------- 1 | // +build rabbitmq 2 | package rabbitmq 3 | 4 | import ( 5 | "context" 6 | 7 | "github.com/hb-chen/gmqtt/pkg/broker" 8 | ) 9 | 10 | type durableQueueKey struct{} 11 | type headersKey struct{} 12 | type exchangeKey struct{} 13 | 14 | // DurableQueue creates a durable queue when subscribing. 15 | func DurableQueue() broker.SubscribeOption { 16 | return func(o *broker.SubscribeOptions) { 17 | if o.Context == nil { 18 | o.Context = context.Background() 19 | } 20 | o.Context = context.WithValue(o.Context, durableQueueKey{}, true) 21 | } 22 | } 23 | 24 | // Headers adds headers used by the headers exchange 25 | func Headers(h map[string]interface{}) broker.SubscribeOption { 26 | return func(o *broker.SubscribeOptions) { 27 | if o.Context == nil { 28 | o.Context = context.Background() 29 | } 30 | o.Context = context.WithValue(o.Context, headersKey{}, h) 31 | } 32 | } 33 | 34 | // Exchange is an option to set the Exchange 35 | func Exchange(e string) broker.Option { 36 | return func(o *broker.Options) { 37 | if o.Context == nil { 38 | o.Context = context.Background() 39 | } 40 | o.Context = context.WithValue(o.Context, exchangeKey{}, e) 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /pkg/broker/rabbitmq/rabbitmq.go: -------------------------------------------------------------------------------- 1 | // +build rabbitmq 2 | // Package rabbitmq provides a RabbitMQ broker 3 | package rabbitmq 4 | 5 | import ( 6 | "context" 7 | "errors" 8 | 9 | "github.com/streadway/amqp" 10 | 11 | "github.com/hb-chen/gmqtt/pkg/broker" 12 | ) 13 | 14 | type rbroker struct { 15 | conn *rabbitMQConn 16 | addrs []string 17 | opts broker.Options 18 | } 19 | 20 | type subscriber struct { 21 | opts broker.SubscribeOptions 22 | topic string 23 | ch *rabbitMQChannel 24 | } 25 | 26 | type publication struct { 27 | d amqp.Delivery 28 | m *broker.Message 29 | t string 30 | } 31 | 32 | func init() { 33 | } 34 | 35 | func (p *publication) Ack() error { 36 | return p.d.Ack(false) 37 | } 38 | 39 | func (p *publication) Topic() string { 40 | return p.t 41 | } 42 | 43 | func (p *publication) Message() *broker.Message { 44 | return p.m 45 | } 46 | 47 | func (s *subscriber) Options() broker.SubscribeOptions { 48 | return s.opts 49 | } 50 | 51 | func (s *subscriber) Topic() string { 52 | return s.topic 53 | } 54 | 55 | func (s *subscriber) Unsubscribe() error { 56 | return s.ch.Close() 57 | } 58 | 59 | func (r *rbroker) Publish(topic string, msg *broker.Message, opts ...broker.PublishOption) error { 60 | m := amqp.Publishing{ 61 | Body: msg.Body, 62 | Headers: amqp.Table{}, 63 | } 64 | 65 | for k, v := range msg.Header { 66 | m.Headers[k] = v 67 | } 68 | 69 | if r.conn == nil { 70 | return errors.New("connection is nil") 71 | } 72 | 73 | return r.conn.Publish(r.conn.exchange, topic, m) 74 | } 75 | 76 | func (r *rbroker) Subscribe(topic string, handler broker.Handler, opts ...broker.SubscribeOption) (broker.Subscriber, error) { 77 | opt := broker.SubscribeOptions{ 78 | AutoAck: true, 79 | } 80 | 81 | for _, o := range opts { 82 | o(&opt) 83 | } 84 | 85 | durableQueue := false 86 | if opt.Context != nil { 87 | durableQueue, _ = opt.Context.Value(durableQueueKey{}).(bool) 88 | } 89 | 90 | var headers map[string]interface{} 91 | if opt.Context != nil { 92 | if h, ok := opt.Context.Value(headersKey{}).(map[string]interface{}); ok { 93 | headers = h 94 | } 95 | } 96 | 97 | if r.conn == nil { 98 | return nil, errors.New("connection is nil") 99 | } 100 | 101 | ch, sub, err := r.conn.Consume( 102 | opt.Queue, 103 | topic, 104 | headers, 105 | opt.AutoAck, 106 | durableQueue, 107 | ) 108 | if err != nil { 109 | return nil, err 110 | } 111 | 112 | fn := func(msg amqp.Delivery) { 113 | header := make(map[string]string) 114 | for k, v := range msg.Headers { 115 | header[k], _ = v.(string) 116 | } 117 | m := &broker.Message{ 118 | Header: header, 119 | Body: msg.Body, 120 | } 121 | handler(&publication{d: msg, m: m, t: msg.RoutingKey}) 122 | } 123 | 124 | go func() { 125 | for d := range sub { 126 | go fn(d) 127 | } 128 | }() 129 | 130 | return &subscriber{ch: ch, topic: topic, opts: opt}, nil 131 | } 132 | 133 | func (r *rbroker) Options() broker.Options { 134 | return r.opts 135 | } 136 | 137 | func (r *rbroker) String() string { 138 | return "rabbitmq" 139 | } 140 | 141 | func (r *rbroker) Address() string { 142 | if len(r.addrs) > 0 { 143 | return r.addrs[0] 144 | } 145 | return "" 146 | } 147 | 148 | func (r *rbroker) Init(opts ...broker.Option) error { 149 | for _, o := range opts { 150 | o(&r.opts) 151 | } 152 | return nil 153 | } 154 | 155 | func (r *rbroker) Connect() error { 156 | if r.conn == nil { 157 | r.conn = newRabbitMQConn(r.getExchange(), r.opts.Addrs) 158 | } 159 | return r.conn.Connect(r.opts.Secure, r.opts.TLSConfig) 160 | } 161 | 162 | func (r *rbroker) Disconnect() error { 163 | if r.conn == nil { 164 | return errors.New("connection is nil") 165 | } 166 | return r.conn.Close() 167 | } 168 | 169 | func NewBroker(opts ...broker.Option) broker.Broker { 170 | options := broker.Options{ 171 | Context: context.Background(), 172 | } 173 | 174 | for _, o := range opts { 175 | o(&options) 176 | } 177 | 178 | return &rbroker{ 179 | addrs: options.Addrs, 180 | opts: options, 181 | } 182 | } 183 | 184 | func (r *rbroker) getExchange() string { 185 | if e, ok := r.opts.Context.Value(exchangeKey{}).(string); ok { 186 | return e 187 | } 188 | return DefaultExchange 189 | } 190 | -------------------------------------------------------------------------------- /pkg/conf/conf.go: -------------------------------------------------------------------------------- 1 | package conf 2 | 3 | import ( 4 | "github.com/hb-go/pkg/config" 5 | "github.com/hb-go/pkg/config/source" 6 | cliSource "github.com/hb-go/pkg/config/source/cli" 7 | "github.com/hb-go/pkg/config/source/file" 8 | "github.com/hb-go/pkg/log" 9 | "github.com/pborman/uuid" 10 | "github.com/urfave/cli/v2" 11 | "os" 12 | "path/filepath" 13 | ) 14 | 15 | var ( 16 | Conf Config // holds the global app config. 17 | ) 18 | 19 | type Config struct { 20 | Log Log 21 | Pyroscope Pyroscope 22 | 23 | logLevel string `json:"log_level"` 24 | 25 | SessionStore string `json:"session_store"` 26 | CacheStore string `json:"cache_store"` 27 | 28 | // 应用配置 29 | App app 30 | 31 | Server server 32 | 33 | Auth auth 34 | 35 | Broker broker 36 | 37 | Sessions sessions 38 | 39 | // MySQL、PostgreSQL 40 | DB database `json:"database"` 41 | 42 | // Redis 43 | Redis redis 44 | 45 | // Opentracing 46 | Opentracing opentracing 47 | } 48 | 49 | type Log struct { 50 | Level string 51 | Debug bool 52 | E bool 53 | } 54 | 55 | type Pyroscope struct { 56 | Enable bool 57 | } 58 | 59 | type app struct { 60 | Name string `json:"name"` 61 | AccessKey string `json:"access_key"` 62 | SecretKey string `json:"secret_key"` 63 | } 64 | 65 | type server struct { 66 | Id string 67 | Addr string `json:"addr"` 68 | WsAddr string `json:"ws_addr"` 69 | } 70 | 71 | type auth struct { 72 | Provider string `json:"provider"` // MockSuccess、MockFailure、rpc 73 | Addrs []string `json:"addrs"` 74 | } 75 | 76 | type broker struct { 77 | Provider string `json:"provider"` // mock、kafka 78 | Addrs []string `json:"addrs"` 79 | } 80 | 81 | type sessions struct { 82 | Provider string `json:"provider"` // mock、redis 83 | } 84 | 85 | type database struct { 86 | Name string `json:"name"` 87 | UserName string `json:"user_name"` 88 | Pwd string `json:"pwd"` 89 | Host string `json:"host"` 90 | Port string `json:"port"` 91 | } 92 | 93 | type redis struct { 94 | Server string `json:"server"` 95 | Pwd string `json:"pwd"` 96 | } 97 | 98 | type memcached struct { 99 | Server string `json:"server"` 100 | } 101 | 102 | type opentracing struct { 103 | Disable bool `json:"disable"` 104 | Type string `json:"type"` 105 | ServiceName string `json:"service_name"` 106 | Address string `json:"address"` 107 | } 108 | 109 | func init() { 110 | } 111 | 112 | // initConfig initializes the app configuration by first setting defaults, 113 | // then overriding settings from the app config file, then overriding 114 | // It returns an error if any. 115 | func InitConfig(ctx *cli.Context) error { 116 | c, err := config.NewConfig() 117 | if err != nil { 118 | return err 119 | } 120 | 121 | sources := make([]source.Source, 0) 122 | 123 | // 1.files source 124 | patterns := ctx.StringSlice("config_patterns") 125 | for _, p := range patterns { 126 | files, err := filepath.Glob(p) 127 | if err != nil { 128 | return err 129 | } 130 | 131 | for _, f := range files { 132 | fileInfo, err := os.Lstat(f) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | if fileInfo.IsDir() { 138 | log.Debugf("skipping directory: %s", f) 139 | continue 140 | } 141 | 142 | sources = append(sources, file.NewSource(file.WithPath(f))) 143 | } 144 | } 145 | 146 | // 2.cli source 147 | sources = append(sources, cliSource.WithContext(ctx)) 148 | c.Load( 149 | sources..., 150 | ) 151 | 152 | // Set defaults. 153 | Conf = Config{ 154 | logLevel: "DEBUG", 155 | } 156 | 157 | c.Scan(&Conf) 158 | 159 | // @TODO 实例ID 160 | if len(Conf.Server.Id) == 0 { 161 | Conf.Server.Id = uuid.NewUUID().String() 162 | } 163 | 164 | // @TODO 配置检查 165 | log.Infof("config data:%v", Conf) 166 | 167 | return nil 168 | } 169 | 170 | func (c Config) LogLvl() log.Lvl { 171 | //DEBUG INFO WARN ERROR OFF 172 | switch c.logLevel { 173 | case "DEBUG": 174 | return log.DEBUG 175 | case "INFO": 176 | return log.INFO 177 | case "WARN": 178 | return log.WARN 179 | case "ERROR": 180 | return log.ERROR 181 | case "OFF": 182 | return log.OFF 183 | default: 184 | return log.INFO 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /pkg/gopool/pool.go: -------------------------------------------------------------------------------- 1 | // Package gopool contains tools for goroutine reuse. 2 | // It is implemented only for examples of github.com/gobwas/ws usage. 3 | package gopool 4 | 5 | import ( 6 | "fmt" 7 | "time" 8 | ) 9 | 10 | // ErrScheduleTimeout returned by Pool to indicate that there no free 11 | // goroutines during some period of time. 12 | var ErrScheduleTimeout = fmt.Errorf("schedule error: timed out") 13 | 14 | // Pool contains logic of goroutine reuse. 15 | type Pool struct { 16 | sem chan struct{} 17 | work chan func() 18 | } 19 | 20 | // NewPool creates new goroutine pool with given size. It also creates a work 21 | // queue of given size. Finally, it spawns given amount of goroutines 22 | // immediately. 23 | func NewPool(size, queue, spawn int) *Pool { 24 | if spawn <= 0 && queue > 0 { 25 | panic("dead queue configuration detected") 26 | } 27 | if spawn > size { 28 | panic("spawn > workers") 29 | } 30 | p := &Pool{ 31 | sem: make(chan struct{}, size), 32 | work: make(chan func(), queue), 33 | } 34 | for i := 0; i < spawn; i++ { 35 | p.sem <- struct{}{} 36 | go p.worker(func() {}) 37 | } 38 | 39 | return p 40 | } 41 | 42 | // Schedule schedules task to be executed over pool's workers. 43 | func (p *Pool) Schedule(task func()) error { 44 | return p.schedule(task, nil) 45 | } 46 | 47 | // ScheduleTimeout schedules task to be executed over pool's workers. 48 | // It returns ErrScheduleTimeout when no free workers met during given timeout. 49 | func (p *Pool) ScheduleTimeout(timeout time.Duration, task func()) error { 50 | return p.schedule(task, time.After(timeout)) 51 | } 52 | 53 | func (p *Pool) schedule(task func(), timeout <-chan time.Time) error { 54 | select { 55 | case <-timeout: 56 | return ErrScheduleTimeout 57 | case p.work <- task: 58 | return nil 59 | case p.sem <- struct{}{}: 60 | go p.worker(task) 61 | return nil 62 | } 63 | } 64 | 65 | func (p *Pool) worker(task func()) { 66 | defer func() { 67 | <-p.sem 68 | }() 69 | 70 | for { 71 | task() 72 | task = <-p.work 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /pkg/log/default_logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | type defaultLogger struct { 10 | *log.Logger 11 | calldepth int 12 | } 13 | 14 | func (l *defaultLogger) Debug(v ...interface{}) { 15 | l.output(DEBUG, v...) 16 | } 17 | 18 | func (l *defaultLogger) Debugf(format string, v ...interface{}) { 19 | l.outputf(DEBUG, format, v...) 20 | } 21 | 22 | func (l *defaultLogger) Info(v ...interface{}) { 23 | l.output(INFO, v...) 24 | } 25 | 26 | func (l *defaultLogger) Infof(format string, v ...interface{}) { 27 | l.outputf(INFO, format, v...) 28 | } 29 | 30 | func (l *defaultLogger) Warn(v ...interface{}) { 31 | l.output(WARN, v...) 32 | } 33 | 34 | func (l *defaultLogger) Warnf(format string, v ...interface{}) { 35 | l.outputf(WARN, format, v...) 36 | } 37 | 38 | func (l *defaultLogger) Error(v ...interface{}) { 39 | l.output(ERROR, v...) 40 | } 41 | 42 | func (l *defaultLogger) Errorf(format string, v ...interface{}) { 43 | l.outputf(ERROR, format, v...) 44 | } 45 | 46 | func (l *defaultLogger) Fatal(v ...interface{}) { 47 | l.output(fatalLvl, v...) 48 | os.Exit(1) 49 | } 50 | 51 | func (l *defaultLogger) Fatalf(format string, v ...interface{}) { 52 | l.outputf(fatalLvl, format, v...) 53 | os.Exit(1) 54 | } 55 | 56 | func (l *defaultLogger) Panic(v ...interface{}) { 57 | l.Logger.Panic(v) 58 | } 59 | 60 | func (l *defaultLogger) Panicf(format string, v ...interface{}) { 61 | l.Logger.Panicf(format, v...) 62 | } 63 | 64 | func (l *defaultLogger) output(lvl Lvl, v ...interface{}) { 65 | if lvl < level { 66 | return 67 | } 68 | l.Output(calldepth, header(lvl, fmt.Sprint(v...))) 69 | } 70 | 71 | func (l *defaultLogger) outputf(lvl Lvl, format string, v ...interface{}) { 72 | if lvl < level { 73 | return 74 | } 75 | l.Output(calldepth, header(lvl, fmt.Sprintf(format, v...))) 76 | } 77 | 78 | func header(lvl Lvl, msg string) string { 79 | return fmt.Sprintf("%s: %s", lvl.String(), msg) 80 | } 81 | -------------------------------------------------------------------------------- /pkg/log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/fatih/color" 8 | l "github.com/smallnest/rpcx/log" 9 | ) 10 | 11 | const ( 12 | DEBUG Lvl = iota 13 | INFO 14 | WARN 15 | ERROR 16 | OFF 17 | fatalLvl 18 | panicLvl 19 | ) 20 | 21 | const ( 22 | calldepth = 5 23 | ) 24 | 25 | var ( 26 | level = DEBUG 27 | colorEnable = true 28 | ) 29 | 30 | func init() { 31 | logger := &defaultLogger{Logger: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile), calldepth: calldepth} 32 | l.SetLogger(logger) 33 | } 34 | 35 | type ( 36 | Lvl uint 37 | colorFunc func(format string, a ...interface{}) string 38 | ) 39 | 40 | func (lvl Lvl) String() string { 41 | switch lvl { 42 | case DEBUG: 43 | return "DEBUG" 44 | case INFO: 45 | return lvl.colorString("INFO", color.GreenString) 46 | case WARN: 47 | return lvl.colorString("WARN", color.YellowString) 48 | case ERROR: 49 | return lvl.colorString("ERROR", color.RedString) 50 | case fatalLvl: 51 | return lvl.colorString("FATAL", color.MagentaString) 52 | case panicLvl: 53 | return "PANIC" 54 | default: 55 | return "" 56 | } 57 | } 58 | 59 | func (lvl Lvl) colorString(str string, f colorFunc) string { 60 | if colorEnable { 61 | return f(str) 62 | } else { 63 | return str 64 | } 65 | } 66 | 67 | func SetLevel(lvl Lvl) { 68 | level = lvl 69 | } 70 | 71 | func SetColor(enable bool) { 72 | colorEnable = enable 73 | } 74 | 75 | func Debug(v ...interface{}) { 76 | l.Debug(v...) 77 | } 78 | func Debugf(format string, v ...interface{}) { 79 | l.Debugf(format, v...) 80 | } 81 | 82 | func Info(v ...interface{}) { 83 | l.Info(v...) 84 | } 85 | func Infof(format string, v ...interface{}) { 86 | l.Infof(format, v...) 87 | } 88 | 89 | func Warn(v ...interface{}) { 90 | l.Warn(v...) 91 | } 92 | func Warnf(format string, v ...interface{}) { 93 | l.Warnf(format, v...) 94 | } 95 | 96 | func Error(v ...interface{}) { 97 | l.Error(v...) 98 | } 99 | func Errorf(format string, v ...interface{}) { 100 | l.Errorf(format, v...) 101 | } 102 | 103 | func Fatal(v ...interface{}) { 104 | l.Fatal(v...) 105 | } 106 | func Fatalf(format string, v ...interface{}) { 107 | l.Fatalf(format, v...) 108 | } 109 | 110 | func Panic(v ...interface{}) { 111 | l.Panic(v...) 112 | } 113 | func Panicf(format string, v ...interface{}) { 114 | l.Panicf(format, v...) 115 | } 116 | -------------------------------------------------------------------------------- /pkg/service/misc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The SurgeMQ Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package service 16 | 17 | import ( 18 | "encoding/binary" 19 | "fmt" 20 | "io" 21 | "net" 22 | 23 | "github.com/surgemq/message" 24 | 25 | "github.com/hb-chen/gmqtt/pkg/log" 26 | ) 27 | 28 | func getConnectMessage(conn io.Reader) (*message.ConnectMessage, error) { 29 | buf, err := getMessageBuffer(conn) 30 | if err != nil { 31 | log.Infof("Receive error: %v", err) 32 | return nil, err 33 | } 34 | 35 | msg := message.NewConnectMessage() 36 | 37 | _, err = msg.Decode(buf) 38 | log.Debugf("Received: %s", msg) 39 | return msg, err 40 | } 41 | 42 | func getConnackMessage(conn io.Reader) (*message.ConnackMessage, error) { 43 | buf, err := getMessageBuffer(conn) 44 | if err != nil { 45 | log.Infof("Receive error: %v", err) 46 | return nil, err 47 | } 48 | 49 | msg := message.NewConnackMessage() 50 | 51 | _, err = msg.Decode(buf) 52 | log.Debugf("Received: %s", msg) 53 | return msg, err 54 | } 55 | 56 | func writeMessage(conn io.Writer, msg message.Message) error { 57 | buf := make([]byte, msg.Len()) 58 | _, err := msg.Encode(buf) 59 | if err != nil { 60 | log.Infof("Write error: %v", err) 61 | return err 62 | } 63 | log.Debugf("Writing: %s", msg) 64 | 65 | return writeMessageBuffer(conn, buf) 66 | } 67 | 68 | func getMessageBuffer(c io.Reader) ([]byte, error) { 69 | if c == nil { 70 | return nil, ErrInvalidConnectionType 71 | } 72 | //conn, ok := c.(net.Conn) 73 | //if !ok { 74 | // return nil, ErrInvalidConnectionType 75 | //} 76 | 77 | var ( 78 | // the message buffer 79 | buf []byte 80 | 81 | // tmp buffer to read a single byte 82 | b []byte = make([]byte, 1) 83 | 84 | // total bytes read 85 | l int = 0 86 | ) 87 | 88 | // Let's read enough bytes to get the message header (msg type, remaining length) 89 | for { 90 | // If we have read 5 bytes and still not done, then there's a problem. 91 | if l > 5 { 92 | return nil, fmt.Errorf("connect/getMessage: 4th byte of remaining length has continuation bit set") 93 | } 94 | 95 | n, err := c.Read(b[0:]) 96 | if err != nil { 97 | //log.Debugf("Read error: %v", err) 98 | return nil, err 99 | } 100 | 101 | // Technically i don't think we will ever get here 102 | if n == 0 { 103 | continue 104 | } 105 | 106 | buf = append(buf, b...) 107 | l += n 108 | 109 | // Check the remlen byte (1+) to see if the continuation bit is set. If so, 110 | // increment cnt and continue reading. Otherwise break. 111 | if l > 1 && b[0] < 0x80 { 112 | break 113 | } 114 | } 115 | 116 | // Get the remaining length of the message 117 | remlen, _ := binary.Uvarint(buf[1:]) 118 | buf = append(buf, make([]byte, remlen)...) 119 | 120 | for l < len(buf) { 121 | n, err := c.Read(buf[l:]) 122 | if err != nil { 123 | return nil, err 124 | } 125 | l += n 126 | } 127 | 128 | return buf, nil 129 | } 130 | 131 | func writeMessageBuffer(c io.Writer, b []byte) error { 132 | if c == nil { 133 | return ErrInvalidConnectionType 134 | } 135 | 136 | //conn, ok := c.(net.Conn) 137 | //if !ok { 138 | // return ErrInvalidConnectionType 139 | //} 140 | 141 | _, err := c.Write(b) 142 | return err 143 | } 144 | 145 | // Copied from http://golang.org/src/pkg/net/timeout_test.go 146 | func isTimeout(err error) bool { 147 | e, ok := err.(net.Error) 148 | return ok && e.Timeout() 149 | } 150 | -------------------------------------------------------------------------------- /pkg/service/options.go: -------------------------------------------------------------------------------- 1 | package service 2 | -------------------------------------------------------------------------------- /pkg/service/websocket.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "io" 5 | "net" 6 | "net/http" 7 | "net/url" 8 | 9 | "golang.org/x/net/websocket" 10 | 11 | "github.com/hb-chen/gmqtt/pkg/log" 12 | ) 13 | 14 | func WebsocketHandler(urlPattern string, uri string) (websocket.Handler, error) { 15 | log.Debugf("AddWebsocketHandler urlPattern=%s, uri=%s", urlPattern, uri) 16 | u, err := url.Parse(uri) 17 | if err != nil { 18 | log.Errorf("web socket add handler error: %v", err) 19 | return nil, err 20 | } 21 | 22 | h := func(ws *websocket.Conn) { 23 | WebsocketTcpProxy(ws, u.Scheme, u.Host) 24 | } 25 | return h, nil 26 | } 27 | 28 | /* start a listener that proxies websocket <-> tcp */ 29 | func ListenAndServeWebsocket(addr string) error { 30 | err := http.ListenAndServe(addr, nil) 31 | if err != nil { 32 | log.Errorf("web socket listen&serve error:%v", err) 33 | } 34 | 35 | return err 36 | } 37 | 38 | /* starts an HTTPS listener */ 39 | func ListenAndServeWebsocketSecure(addr string, cert string, key string) error { 40 | return http.ListenAndServeTLS(addr, cert, key, nil) 41 | } 42 | 43 | /* copy from websocket to writer, this copies the binary frames as is */ 44 | func io_copy_ws(src *websocket.Conn, dst io.Writer) (int, error) { 45 | var buffer []byte 46 | count := 0 47 | for { 48 | err := websocket.Message.Receive(src, &buffer) 49 | if err != nil { 50 | return count, err 51 | } 52 | n := len(buffer) 53 | count += n 54 | i, err := dst.Write(buffer) 55 | if err != nil || i < 1 { 56 | return count, err 57 | } 58 | } 59 | return count, nil 60 | } 61 | 62 | /* copy from reader to websocket, this copies the binary frames as is */ 63 | func io_ws_copy(src io.Reader, dst *websocket.Conn) (int, error) { 64 | buffer := make([]byte, 2048) 65 | count := 0 66 | for { 67 | n, err := src.Read(buffer) 68 | if err != nil || n < 1 { 69 | return count, err 70 | } 71 | count += n 72 | err = websocket.Message.Send(dst, buffer[0:n]) 73 | if err != nil { 74 | return count, err 75 | } 76 | } 77 | return count, nil 78 | } 79 | 80 | /* handler that proxies websocket <-> unix domain socket */ 81 | func WebsocketTcpProxy(ws *websocket.Conn, nettype string, host string) error { 82 | client, err := net.Dial(nettype, host) 83 | if err != nil { 84 | return err 85 | } 86 | defer client.Close() 87 | defer ws.Close() 88 | chDone := make(chan bool) 89 | 90 | go func() { 91 | io_ws_copy(client, ws) 92 | chDone <- true 93 | }() 94 | go func() { 95 | io_copy_ws(ws, client) 96 | chDone <- true 97 | }() 98 | <-chDone 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/sessions/ackqueue.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The SurgeMQ Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sessions 16 | 17 | import ( 18 | "errors" 19 | "fmt" 20 | "math" 21 | "sync" 22 | 23 | "github.com/surgemq/message" 24 | ) 25 | 26 | var ( 27 | errQueueFull error = errors.New("queue full") 28 | errQueueEmpty error = errors.New("queue empty") 29 | errWaitMessage error = errors.New("Invalid message to wait for ack") 30 | errAckMessage error = errors.New("Invalid message for acking") 31 | ) 32 | 33 | type ackmsg struct { 34 | // Message type of the message waiting for ack 35 | Mtype message.MessageType 36 | 37 | // Current state of the ack-waiting message 38 | State message.MessageType 39 | 40 | // Packet ID of the message. Every message that require ack'ing must have a valid 41 | // packet ID. Messages that have message I 42 | Pktid uint16 43 | 44 | // Slice containing the message bytes 45 | Msgbuf []byte 46 | 47 | // Slice containing the ack message bytes 48 | Ackbuf []byte 49 | 50 | // When ack cycle completes, call this function 51 | OnComplete interface{} 52 | } 53 | 54 | // Ackqueue is a growing queue implemented based on a ring buffer. As the buffer 55 | // gets full, it will auto-grow. 56 | // 57 | // Ackqueue is used to store messages that are waiting for acks to come back. There 58 | // are a few scenarios in which acks are required. 59 | // 1. Client sends SUBSCRIBE message to server, waits for SUBACK. 60 | // 2. Client sends UNSUBSCRIBE message to server, waits for UNSUBACK. 61 | // 3. Client sends PUBLISH QoS 1 message to server, waits for PUBACK. 62 | // 4. Server sends PUBLISH QoS 1 message to client, waits for PUBACK. 63 | // 5. Client sends PUBLISH QoS 2 message to server, waits for PUBREC. 64 | // 6. Server sends PUBREC message to client, waits for PUBREL. 65 | // 7. Client sends PUBREL message to server, waits for PUBCOMP. 66 | // 8. Server sends PUBLISH QoS 2 message to client, waits for PUBREC. 67 | // 9. Client sends PUBREC message to server, waits for PUBREL. 68 | // 10. Server sends PUBREL message to client, waits for PUBCOMP. 69 | // 11. Client sends PINGREQ message to server, waits for PINGRESP. 70 | type Ackqueue struct { 71 | size int64 72 | mask int64 73 | count int64 74 | head int64 75 | tail int64 76 | 77 | ping ackmsg 78 | ring []ackmsg 79 | emap map[uint16]int64 80 | 81 | ackdone []ackmsg 82 | 83 | mu sync.Mutex 84 | } 85 | 86 | func newAckqueue(n int) *Ackqueue { 87 | m := int64(n) 88 | if !powerOfTwo64(m) { 89 | m = roundUpPowerOfTwo64(m) 90 | } 91 | 92 | return &Ackqueue{ 93 | size: m, 94 | mask: m - 1, 95 | count: 0, 96 | head: 0, 97 | tail: 0, 98 | ring: make([]ackmsg, m), 99 | emap: make(map[uint16]int64, m), 100 | ackdone: make([]ackmsg, 0), 101 | } 102 | } 103 | 104 | // Wait() copies the message into a waiting queue, and waits for the corresponding 105 | // ack message to be received. 106 | func (this *Ackqueue) Wait(msg message.Message, onComplete interface{}) error { 107 | this.mu.Lock() 108 | defer this.mu.Unlock() 109 | 110 | switch msg := msg.(type) { 111 | case *message.PublishMessage: 112 | if msg.QoS() == message.QosAtMostOnce { 113 | //return fmt.Errorf("QoS 0 messages don't require ack") 114 | return errWaitMessage 115 | } 116 | 117 | this.insert(msg.PacketId(), msg, onComplete) 118 | 119 | case *message.SubscribeMessage: 120 | this.insert(msg.PacketId(), msg, onComplete) 121 | 122 | case *message.UnsubscribeMessage: 123 | this.insert(msg.PacketId(), msg, onComplete) 124 | 125 | case *message.PingreqMessage: 126 | this.ping = ackmsg{ 127 | Mtype: message.PINGREQ, 128 | State: message.RESERVED, 129 | OnComplete: onComplete, 130 | } 131 | 132 | default: 133 | return errWaitMessage 134 | } 135 | 136 | return nil 137 | } 138 | 139 | // Ack() takes the ack message supplied and updates the status of messages waiting. 140 | func (this *Ackqueue) Ack(msg message.Message) error { 141 | this.mu.Lock() 142 | defer this.mu.Unlock() 143 | 144 | switch msg.Type() { 145 | case message.PUBACK, message.PUBREC, message.PUBREL, message.PUBCOMP, message.SUBACK, message.UNSUBACK: 146 | // Check to see if the message w/ the same packet ID is in the queue 147 | i, ok := this.emap[msg.PacketId()] 148 | if ok { 149 | // If message w/ the packet ID exists, update the message state and copy 150 | // the ack message 151 | this.ring[i].State = msg.Type() 152 | 153 | ml := msg.Len() 154 | this.ring[i].Ackbuf = make([]byte, ml) 155 | 156 | _, err := msg.Encode(this.ring[i].Ackbuf) 157 | if err != nil { 158 | return err 159 | } 160 | //glog.Debugf("Acked: %v", msg) 161 | //} else { 162 | //glog.Debugf("Cannot ack %s message with packet ID %d", msg.Type(), msg.PacketId()) 163 | } 164 | 165 | case message.PINGRESP: 166 | if this.ping.Mtype == message.PINGREQ { 167 | this.ping.State = message.PINGRESP 168 | } 169 | 170 | default: 171 | return errAckMessage 172 | } 173 | 174 | return nil 175 | } 176 | 177 | // Acked() returns the list of messages that have completed the ack cycle. 178 | func (this *Ackqueue) Acked() []ackmsg { 179 | this.mu.Lock() 180 | defer this.mu.Unlock() 181 | 182 | this.ackdone = this.ackdone[0:0] 183 | 184 | if this.ping.State == message.PINGRESP { 185 | this.ackdone = append(this.ackdone, this.ping) 186 | this.ping = ackmsg{} 187 | } 188 | 189 | FORNOTEMPTY: 190 | for !this.empty() { 191 | switch this.ring[this.head].State { 192 | case message.PUBACK, message.PUBREL, message.PUBCOMP, message.SUBACK, message.UNSUBACK: 193 | this.ackdone = append(this.ackdone, this.ring[this.head]) 194 | this.removeHead() 195 | 196 | default: 197 | break FORNOTEMPTY 198 | } 199 | } 200 | 201 | return this.ackdone 202 | } 203 | 204 | func (this *Ackqueue) insert(pktid uint16, msg message.Message, onComplete interface{}) error { 205 | if this.full() { 206 | this.grow() 207 | } 208 | 209 | if _, ok := this.emap[pktid]; !ok { 210 | // message length 211 | ml := msg.Len() 212 | 213 | // ackmsg 214 | am := ackmsg{ 215 | Mtype: msg.Type(), 216 | State: message.RESERVED, 217 | Pktid: msg.PacketId(), 218 | Msgbuf: make([]byte, ml), 219 | OnComplete: onComplete, 220 | } 221 | 222 | if _, err := msg.Encode(am.Msgbuf); err != nil { 223 | return err 224 | } 225 | 226 | this.ring[this.tail] = am 227 | this.emap[pktid] = this.tail 228 | this.tail = this.increment(this.tail) 229 | this.count++ 230 | } else { 231 | // If packet w/ pktid already exist, then this must be a PUBLISH message 232 | // Other message types should never send with the same packet ID 233 | pm, ok := msg.(*message.PublishMessage) 234 | if !ok { 235 | return fmt.Errorf("ack/insert: duplicate packet ID for %s message", msg.Name()) 236 | } 237 | 238 | // If this is a publish message, then the DUP flag must be set. This is the 239 | // only scenario in which we will receive duplicate messages. 240 | if pm.Dup() { 241 | return fmt.Errorf("ack/insert: duplicate packet ID for PUBLISH message, but DUP flag is not set") 242 | } 243 | 244 | // Since it's a dup, there's really nothing we need to do. Moving on... 245 | } 246 | 247 | return nil 248 | } 249 | 250 | func (this *Ackqueue) removeHead() error { 251 | if this.empty() { 252 | return errQueueEmpty 253 | } 254 | 255 | it := this.ring[this.head] 256 | // set this to empty ackmsg{} to ensure GC will collect the buffer 257 | this.ring[this.head] = ackmsg{} 258 | this.head = this.increment(this.head) 259 | this.count-- 260 | delete(this.emap, it.Pktid) 261 | 262 | return nil 263 | } 264 | 265 | func (this *Ackqueue) grow() { 266 | if math.MaxInt64/2 < this.size { 267 | panic("new size will overflow int64") 268 | } 269 | 270 | newsize := this.size << 1 271 | newmask := newsize - 1 272 | newring := make([]ackmsg, newsize) 273 | 274 | if this.tail > this.head { 275 | copy(newring, this.ring[this.head:this.tail]) 276 | } else { 277 | copy(newring, this.ring[this.head:]) 278 | copy(newring[this.size-this.head:], this.ring[:this.tail]) 279 | } 280 | 281 | this.size = newsize 282 | this.mask = newmask 283 | this.ring = newring 284 | this.head = 0 285 | this.tail = this.count 286 | 287 | this.emap = make(map[uint16]int64, this.size) 288 | 289 | for i := int64(0); i < this.tail; i++ { 290 | this.emap[this.ring[i].Pktid] = i 291 | } 292 | } 293 | 294 | func (this *Ackqueue) len() int { 295 | return int(this.count) 296 | } 297 | 298 | func (this *Ackqueue) cap() int { 299 | return int(this.size) 300 | } 301 | 302 | func (this *Ackqueue) index(n int64) int64 { 303 | return n & this.mask 304 | } 305 | 306 | func (this *Ackqueue) full() bool { 307 | return this.count == this.size 308 | } 309 | 310 | func (this *Ackqueue) empty() bool { 311 | return this.count == 0 312 | } 313 | 314 | func (this *Ackqueue) increment(n int64) int64 { 315 | return this.index(n + 1) 316 | } 317 | 318 | func powerOfTwo64(n int64) bool { 319 | return n != 0 && (n&(n-1)) == 0 320 | } 321 | 322 | func roundUpPowerOfTwo64(n int64) int64 { 323 | n-- 324 | n |= n >> 1 325 | n |= n >> 2 326 | n |= n >> 4 327 | n |= n >> 8 328 | n |= n >> 16 329 | n |= n >> 32 330 | n++ 331 | 332 | return n 333 | } 334 | -------------------------------------------------------------------------------- /pkg/sessions/ackqueue_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The SurgeMQ Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sessions 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | "github.com/surgemq/message" 22 | ) 23 | 24 | func TestAckQueueOutOfOrder(t *testing.T) { 25 | q := newAckqueue(5) 26 | require.Equal(t, 8, q.cap()) 27 | 28 | for i := 0; i < 12; i++ { 29 | msg := newPublishMessage(uint16(i), 1) 30 | q.Wait(msg, nil) 31 | } 32 | 33 | require.Equal(t, 12, q.len()) 34 | 35 | ack1 := message.NewPubackMessage() 36 | ack1.SetPacketId(1) 37 | q.Ack(ack1) 38 | 39 | acked := q.Acked() 40 | 41 | require.Equal(t, 0, len(acked)) 42 | 43 | ack0 := message.NewPubackMessage() 44 | ack0.SetPacketId(0) 45 | q.Ack(ack0) 46 | 47 | acked = q.Acked() 48 | 49 | require.Equal(t, 2, len(acked)) 50 | } 51 | -------------------------------------------------------------------------------- /pkg/sessions/serializer.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | func (this *Session) Serialize() ([]byte, error) { 8 | // json 9 | return json.Marshal(this) 10 | } 11 | 12 | func (this *Session) Deserialize(byt []byte) (err error) { 13 | // json 14 | return json.Unmarshal(byt, this) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/sessions/session.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The SurgeMQ Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sessions 16 | 17 | import ( 18 | "fmt" 19 | "sync" 20 | 21 | "github.com/surgemq/message" 22 | ) 23 | 24 | const ( 25 | // Queue size for the ack queue 26 | defaultQueueSize = 16 27 | ) 28 | 29 | type Session struct { 30 | // Ack queue for outgoing PUBLISH QoS 1 messages 31 | Pub1ack *Ackqueue `json:"-"` 32 | 33 | // Ack queue for incoming PUBLISH QoS 2 messages 34 | Pub2in *Ackqueue `json:"-"` 35 | 36 | // Ack queue for outgoing PUBLISH QoS 2 messages 37 | Pub2out *Ackqueue `json:"-"` 38 | 39 | // Ack queue for outgoing SUBSCRIBE messages 40 | Suback *Ackqueue `json:"-"` 41 | 42 | // Ack queue for outgoing UNSUBSCRIBE messages 43 | Unsuback *Ackqueue `json:"-"` 44 | 45 | // Ack queue for outgoing PINGREQ messages 46 | Pingack *Ackqueue `json:"-"` 47 | 48 | // cmsg is the CONNECT message 49 | Cmsg *message.ConnectMessage `json:"-"` 50 | 51 | // Will message to publish if connect is closed unexpectedly 52 | Will *message.PublishMessage `json:"-"` 53 | 54 | // Retained publish message 55 | Retained *message.PublishMessage `json:"-"` 56 | 57 | // cbuf is the CONNECT message buffer, this is for storing all the will stuff 58 | cbuf []byte `json:"-"` 59 | 60 | // rbuf is the retained PUBLISH message buffer 61 | rbuf []byte `json:"-"` 62 | 63 | // topics stores all the topics for this session/client 64 | topics map[string]byte `json:"topics"` 65 | 66 | // Initialized? 67 | initted bool `json:"-"` 68 | 69 | // Serialize access to this session 70 | mu sync.Mutex `json:"-"` 71 | 72 | id string `json:"id"` 73 | } 74 | 75 | func (this *Session) Init(msg *message.ConnectMessage) error { 76 | this.mu.Lock() 77 | defer this.mu.Unlock() 78 | 79 | if this.initted { 80 | return fmt.Errorf("Session already initialized") 81 | } 82 | 83 | this.cbuf = make([]byte, msg.Len()) 84 | this.Cmsg = message.NewConnectMessage() 85 | 86 | if _, err := msg.Encode(this.cbuf); err != nil { 87 | return err 88 | } 89 | 90 | if _, err := this.Cmsg.Decode(this.cbuf); err != nil { 91 | return err 92 | } 93 | 94 | if this.Cmsg.WillFlag() { 95 | this.Will = message.NewPublishMessage() 96 | this.Will.SetQoS(this.Cmsg.WillQos()) 97 | this.Will.SetTopic(this.Cmsg.WillTopic()) 98 | this.Will.SetPayload(this.Cmsg.WillMessage()) 99 | this.Will.SetRetain(this.Cmsg.WillRetain()) 100 | } 101 | 102 | this.topics = make(map[string]byte, 1) 103 | 104 | this.id = string(msg.ClientId()) 105 | 106 | this.Pub1ack = newAckqueue(defaultQueueSize) 107 | this.Pub2in = newAckqueue(defaultQueueSize) 108 | this.Pub2out = newAckqueue(defaultQueueSize) 109 | this.Suback = newAckqueue(defaultQueueSize) 110 | this.Unsuback = newAckqueue(defaultQueueSize) 111 | this.Pingack = newAckqueue(defaultQueueSize) 112 | 113 | this.initted = true 114 | 115 | return nil 116 | } 117 | 118 | func (this *Session) Update(msg *message.ConnectMessage) error { 119 | this.mu.Lock() 120 | defer this.mu.Unlock() 121 | 122 | this.cbuf = make([]byte, msg.Len()) 123 | this.Cmsg = message.NewConnectMessage() 124 | 125 | if _, err := msg.Encode(this.cbuf); err != nil { 126 | return err 127 | } 128 | 129 | if _, err := this.Cmsg.Decode(this.cbuf); err != nil { 130 | return err 131 | } 132 | 133 | return nil 134 | } 135 | 136 | func (this *Session) RetainMessage(msg *message.PublishMessage) error { 137 | this.mu.Lock() 138 | defer this.mu.Unlock() 139 | 140 | this.rbuf = make([]byte, msg.Len()) 141 | this.Retained = message.NewPublishMessage() 142 | 143 | if _, err := msg.Encode(this.rbuf); err != nil { 144 | return err 145 | } 146 | 147 | if _, err := this.Retained.Decode(this.rbuf); err != nil { 148 | return err 149 | } 150 | 151 | return nil 152 | } 153 | 154 | func (this *Session) AddTopic(topic string, qos byte) error { 155 | this.mu.Lock() 156 | defer this.mu.Unlock() 157 | 158 | if !this.initted { 159 | return fmt.Errorf("Session not yet initialized") 160 | } 161 | 162 | this.topics[topic] = qos 163 | 164 | // @TODO Update Session Store 165 | 166 | return nil 167 | } 168 | 169 | func (this *Session) RemoveTopic(topic string) error { 170 | this.mu.Lock() 171 | defer this.mu.Unlock() 172 | 173 | if !this.initted { 174 | return fmt.Errorf("Session not yet initialized") 175 | } 176 | 177 | delete(this.topics, topic) 178 | 179 | // @TODO Update Session Store 180 | 181 | return nil 182 | } 183 | 184 | func (this *Session) Topics() ([]string, []byte, error) { 185 | this.mu.Lock() 186 | defer this.mu.Unlock() 187 | 188 | if !this.initted { 189 | return nil, nil, fmt.Errorf("Session not yet initialized") 190 | } 191 | 192 | var ( 193 | topics []string 194 | qoss []byte 195 | ) 196 | 197 | for k, v := range this.topics { 198 | topics = append(topics, k) 199 | qoss = append(qoss, v) 200 | } 201 | 202 | return topics, qoss, nil 203 | } 204 | 205 | func (this *Session) ID() string { 206 | return string(this.Cmsg.ClientId()) 207 | } 208 | -------------------------------------------------------------------------------- /pkg/sessions/session_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The SurgeMQ Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sessions 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/require" 21 | "github.com/surgemq/message" 22 | ) 23 | 24 | func TestSessionInit(t *testing.T) { 25 | sess := &Session{} 26 | cmsg := newConnectMessage() 27 | 28 | err := sess.Init(cmsg) 29 | require.NoError(t, err) 30 | require.Equal(t, len(sess.cbuf), cmsg.Len()) 31 | require.Equal(t, cmsg.WillQos(), sess.Cmsg.WillQos()) 32 | require.Equal(t, cmsg.Version(), sess.Cmsg.Version()) 33 | require.Equal(t, cmsg.CleanSession(), sess.Cmsg.CleanSession()) 34 | require.Equal(t, cmsg.ClientId(), sess.Cmsg.ClientId()) 35 | require.Equal(t, cmsg.KeepAlive(), sess.Cmsg.KeepAlive()) 36 | require.Equal(t, cmsg.WillTopic(), sess.Cmsg.WillTopic()) 37 | require.Equal(t, cmsg.WillMessage(), sess.Cmsg.WillMessage()) 38 | require.Equal(t, cmsg.Username(), sess.Cmsg.Username()) 39 | require.Equal(t, cmsg.Password(), sess.Cmsg.Password()) 40 | require.Equal(t, []byte("will"), sess.Will.Topic()) 41 | require.Equal(t, cmsg.WillQos(), sess.Will.QoS()) 42 | 43 | sess.AddTopic("test", 1) 44 | require.Equal(t, 1, len(sess.topics)) 45 | 46 | topics, qoss, err := sess.Topics() 47 | require.NoError(t, err) 48 | require.Equal(t, 1, len(topics)) 49 | require.Equal(t, 1, len(qoss)) 50 | require.Equal(t, "test", topics[0]) 51 | require.Equal(t, 1, int(qoss[0])) 52 | 53 | sess.RemoveTopic("test") 54 | require.Equal(t, 0, len(sess.topics)) 55 | } 56 | 57 | func TestSessionPublishAckqueue(t *testing.T) { 58 | sess := &Session{} 59 | cmsg := newConnectMessage() 60 | err := sess.Init(cmsg) 61 | require.NoError(t, err) 62 | 63 | for i := 0; i < 12; i++ { 64 | msg := newPublishMessage(uint16(i), 1) 65 | sess.Pub1ack.Wait(msg, nil) 66 | } 67 | 68 | require.Equal(t, 12, sess.Pub1ack.len()) 69 | 70 | ack1 := message.NewPubackMessage() 71 | ack1.SetPacketId(1) 72 | sess.Pub1ack.Ack(ack1) 73 | 74 | acked := sess.Pub1ack.Acked() 75 | require.Equal(t, 0, len(acked)) 76 | 77 | ack0 := message.NewPubackMessage() 78 | ack0.SetPacketId(0) 79 | sess.Pub1ack.Ack(ack0) 80 | 81 | acked = sess.Pub1ack.Acked() 82 | require.Equal(t, 2, len(acked)) 83 | } 84 | 85 | func newConnectMessage() *message.ConnectMessage { 86 | msg := message.NewConnectMessage() 87 | msg.SetWillQos(1) 88 | msg.SetVersion(4) 89 | msg.SetCleanSession(true) 90 | msg.SetClientId([]byte("surgemq")) 91 | msg.SetKeepAlive(10) 92 | msg.SetWillTopic([]byte("will")) 93 | msg.SetWillMessage([]byte("send me home")) 94 | msg.SetUsername([]byte("surgemq")) 95 | msg.SetPassword([]byte("verysecret")) 96 | 97 | return msg 98 | } 99 | 100 | func newPublishMessage(pktid uint16, qos byte) *message.PublishMessage { 101 | msg := message.NewPublishMessage() 102 | msg.SetPacketId(pktid) 103 | msg.SetTopic([]byte("abc")) 104 | msg.SetPayload([]byte("abc")) 105 | msg.SetQoS(qos) 106 | 107 | return msg 108 | } 109 | -------------------------------------------------------------------------------- /pkg/sessions/sessions.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The SurgeMQ Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sessions 16 | 17 | import ( 18 | "crypto/rand" 19 | "encoding/base64" 20 | "errors" 21 | "io" 22 | ) 23 | 24 | var ( 25 | ErrSessionsProviderNotFound = errors.New("Session: Session provider not found") 26 | ErrKeyNotAvailable = errors.New("Session: not item found for key.") 27 | 28 | providers = make(map[string]SessionsProvider) 29 | ) 30 | 31 | type SessionsProvider interface { 32 | New(id string) (*Session, error) 33 | Get(id string) (*Session, error) 34 | Del(id string) 35 | Save(id string) error 36 | Count() int 37 | Close() error 38 | } 39 | 40 | // Register makes a session provider available by the provided name. 41 | // If a Register is called twice with the same name or if the driver is nil, 42 | // it panics. 43 | func Register(name string, provider SessionsProvider) { 44 | if provider == nil { 45 | panic("session: Register provide is nil") 46 | } 47 | 48 | if _, dup := providers[name]; dup { 49 | panic("session: Register called twice for provider " + name) 50 | } 51 | 52 | providers[name] = provider 53 | } 54 | 55 | func Unregister(name string) { 56 | delete(providers, name) 57 | } 58 | 59 | type Manager struct { 60 | store Store 61 | } 62 | 63 | func NewManager(store Store) *Manager { 64 | return &Manager{store: store} 65 | } 66 | 67 | func (this *Manager) New(id string) (*Session, error) { 68 | if id == "" { 69 | id = this.sessionId() 70 | } 71 | 72 | sess := &Session{id: id} 73 | err := this.store.Set(id, sess) 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | return sess, nil 79 | } 80 | 81 | func (this *Manager) Get(id string) (*Session, error) { 82 | return this.store.Get(id) 83 | } 84 | 85 | func (this *Manager) Del(id string) error { 86 | return this.store.Del(id) 87 | } 88 | 89 | func (this *Manager) Save(id string, sess *Session) error { 90 | return this.store.Set(id, sess) 91 | } 92 | 93 | func (this *Manager) Count() (int64, error) { 94 | return this.store.Count() 95 | } 96 | 97 | func (this *Manager) Close() error { 98 | return this.store.Close() 99 | } 100 | 101 | func (manager *Manager) sessionId() string { 102 | b := make([]byte, 15) 103 | if _, err := io.ReadFull(rand.Reader, b); err != nil { 104 | return "" 105 | } 106 | return base64.URLEncoding.EncodeToString(b) 107 | } 108 | -------------------------------------------------------------------------------- /pkg/sessions/store.go: -------------------------------------------------------------------------------- 1 | package sessions 2 | 3 | type Store interface { 4 | Get(id string) (*Session, error) 5 | Set(id string, session *Session) error 6 | Del(id string) error 7 | Range(page, size int64) ([]*Session, error) 8 | Count() (int64, error) 9 | Close() error 10 | } 11 | -------------------------------------------------------------------------------- /pkg/sessions/store/mock.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync" 7 | 8 | "github.com/hb-chen/gmqtt/pkg/sessions" 9 | ) 10 | 11 | type MockStore struct { 12 | st map[string]*sessions.Session 13 | mu sync.RWMutex 14 | } 15 | 16 | func NewMockStore() (*MockStore, error) { 17 | mock := &MockStore{ 18 | st: make(map[string]*sessions.Session), 19 | } 20 | 21 | return mock, nil 22 | } 23 | 24 | func (this *MockStore) Get(id string) (*sessions.Session, error) { 25 | this.mu.RLock() 26 | defer this.mu.RUnlock() 27 | 28 | sess, ok := this.st[id] 29 | if !ok { 30 | return nil, fmt.Errorf("store/Get: No session found for key %s", id) 31 | } 32 | 33 | return sess, nil 34 | } 35 | 36 | func (this *MockStore) Set(id string, sess *sessions.Session) error { 37 | this.mu.Lock() 38 | defer this.mu.Unlock() 39 | 40 | this.st[id] = sess 41 | return nil 42 | } 43 | 44 | func (this *MockStore) Del(id string) error { 45 | this.mu.Lock() 46 | defer this.mu.Unlock() 47 | delete(this.st, id) 48 | return nil 49 | } 50 | 51 | func (this *MockStore) Range(page, size int64) ([]*sessions.Session, error) { 52 | keys := reflect.ValueOf(this.st).MapKeys() 53 | 54 | start := page * size 55 | stop := start + size 56 | l := int64(len(keys)) 57 | if stop > l { 58 | stop = l 59 | } 60 | 61 | sesses := make([]*sessions.Session, start-stop) 62 | for _, k := range keys { 63 | sesses = append(sesses, this.st[k.String()]) 64 | } 65 | 66 | return sesses, nil 67 | } 68 | 69 | func (this *MockStore) Count() (int64, error) { 70 | return int64(len(this.st)), nil 71 | } 72 | 73 | func (this *MockStore) Close() error { 74 | this.st = make(map[string]*sessions.Session) 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /pkg/sessions/store/redis.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/go-redis/redis" 7 | 8 | "github.com/hb-chen/gmqtt/pkg/sessions" 9 | ) 10 | 11 | const sorted_set_key = "sorted_set_key" 12 | 13 | type RedisStore struct { 14 | client *redis.Client 15 | } 16 | 17 | // NewRedisStore("127.0.0.1:6379","123456") 18 | func NewRedisStore(addr string, password string) (*RedisStore, error) { 19 | client := redis.NewClient(&redis.Options{ 20 | Addr: addr, 21 | Password: password, // no password set 22 | DB: 0, // use default DB 23 | }) 24 | 25 | _, err := client.Ping().Result() 26 | if err != nil { 27 | return nil, nil 28 | } 29 | 30 | rs := &RedisStore{ 31 | client: client, 32 | } 33 | 34 | return rs, nil 35 | } 36 | 37 | func (this *RedisStore) Get(id string) (*sessions.Session, error) { 38 | val, err := this.client.Get(id).Bytes() 39 | 40 | sess := &sessions.Session{} 41 | err = sess.Deserialize(val) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return sess, err 47 | } 48 | 49 | func (this *RedisStore) Set(id string, sess *sessions.Session) error { 50 | b, err := sess.Serialize() 51 | if err != nil { 52 | return err 53 | } 54 | 55 | // @TODO 是否需要事务 56 | z := redis.Z{Score: float64(time.Now().Nanosecond()), Member: id} 57 | err = this.client.ZAdd(sorted_set_key, z).Err() 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return this.client.Set(id, b, 0).Err() 63 | } 64 | 65 | func (this *RedisStore) Del(id string) error { 66 | // @TODO 是否需要事务 67 | err := this.client.ZRem(sorted_set_key, id).Err() 68 | if err != nil { 69 | return err 70 | } 71 | 72 | return this.client.Del(id).Err() 73 | } 74 | 75 | func (this *RedisStore) Range(page, size int64) ([]*sessions.Session, error) { 76 | start := page * size 77 | stop := start + size 78 | vals, err := this.client.ZRange(sorted_set_key, start, stop).Result() 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | sesses := make([]*sessions.Session, len(vals)) 84 | for _, val := range vals { 85 | sess := &sessions.Session{} 86 | err = sess.Deserialize([]byte(val)) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | sesses = append(sesses, sess) 92 | } 93 | 94 | return sesses, nil 95 | } 96 | 97 | func (this *RedisStore) Count() (int64, error) { 98 | return this.client.ZCount("key", "-inf", "+inf").Result() 99 | } 100 | 101 | func (this *RedisStore) Close() error { 102 | return this.client.Close() 103 | } 104 | -------------------------------------------------------------------------------- /pkg/topics/topics.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014 The SurgeMQ Authors. All rights reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package topics deals with MQTT topic names, topic filters and subscriptions. 16 | // - "Topic name" is a / separated string that could contain #, * and $ 17 | // - / in topic name separates the string into "topic levels" 18 | // - # is a multi-level wildcard, and it must be the last character in the 19 | // topic name. It represents the parent and all children levels. 20 | // - + is a single level wildwcard. It must be the only character in the 21 | // topic level. It represents all names in the current level. 22 | // - $ is a special character that says the topic is a system level topic 23 | package topics 24 | 25 | import ( 26 | "errors" 27 | "fmt" 28 | "strings" 29 | "sync" 30 | 31 | "github.com/surgemq/message" 32 | 33 | "github.com/hb-chen/gmqtt/pkg/broker" 34 | . "github.com/hb-chen/gmqtt/pkg/conf" 35 | "github.com/hb-chen/gmqtt/pkg/log" 36 | ) 37 | 38 | const ( 39 | // MWC is the multi-level wildcard 40 | MWC = "#" 41 | 42 | // SWC is the single level wildcard 43 | SWC = "+" 44 | 45 | // SEP is the topic level separator 46 | SEP = "/" 47 | 48 | // SYS is the starting character of the system level topics 49 | SYS = "$" 50 | 51 | // Both wildcards 52 | _WC = "#+" 53 | 54 | // MQTT <=> MQ 55 | MQHeaderMQTTQos = "MQTT_Qos" 56 | MQHeaderMQTTTopic = "MQTT_Topic" 57 | ) 58 | 59 | var ( 60 | // ErrAuthFailure is returned when the user/pass supplied are invalid 61 | ErrAuthFailure = errors.New("auth: Authentication failure") 62 | 63 | // ErrAuthProviderNotFound is returned when the requested provider does not exist. 64 | // It probably hasn't been registered yet. 65 | ErrAuthProviderNotFound = errors.New("auth: Authentication provider not found") 66 | 67 | providers = make(map[string]TopicsProvider) 68 | ) 69 | 70 | // TopicsProvider 71 | type TopicsProvider interface { 72 | Subscribe(topic []byte, qos byte, subscriber interface{}) (byte, error) 73 | Unsubscribe(topic []byte, subscriber interface{}) (int, error) 74 | Subscribers(topic []byte, qos byte, subs *[]interface{}, qoss *[]byte) error 75 | Retain(msg *message.PublishMessage) error 76 | Retained(topic []byte, msgs *[]*message.PublishMessage) error 77 | Close() error 78 | } 79 | 80 | func Register(name string, provider TopicsProvider) { 81 | if provider == nil { 82 | panic("topics: Register provide is nil") 83 | } 84 | 85 | if _, dup := providers[name]; dup { 86 | panic("topics: Register called twice for provider " + name) 87 | } 88 | 89 | providers[name] = provider 90 | } 91 | 92 | func Unregister(name string) { 93 | delete(providers, name) 94 | } 95 | 96 | type Manager struct { 97 | p TopicsProvider 98 | 99 | broker broker.Broker 100 | subers sync.Map 101 | 102 | subHandler broker.Handler 103 | 104 | subs []interface{} 105 | qoss []byte 106 | } 107 | 108 | func NewManager(providerName string, b broker.Broker, h broker.Handler) (*Manager, error) { 109 | p, ok := providers[providerName] 110 | if !ok { 111 | return nil, fmt.Errorf("topics: unknown provider %q", providerName) 112 | } 113 | 114 | return &Manager{ 115 | p: p, 116 | broker: b, 117 | subHandler: h, 118 | }, nil 119 | } 120 | 121 | func (this *Manager) Subscribe(topic []byte, qos byte, subscriber interface{}) (byte, error) { 122 | // @TODO 订阅鉴权 123 | 124 | // broker订阅 125 | // 检查topic是否已订阅 126 | // broker topic匹配规则转换 127 | if this.broker != nil && this.subHandler != nil { 128 | brTopic := TopicToBrokerTopic(topic) 129 | log.Debugf("broker topic:%v subscribe", brTopic) 130 | _, ok := this.subers.Load(brTopic) 131 | if !ok { 132 | // broker只订阅一级话题,通过header传递MQTT特有属性QoS、Topic 133 | // @TODO Broker队列与Gateway实例ID绑定,即Kafka的Consumer Group 134 | suber, err := this.broker.Subscribe(brTopic, this.subHandler, broker.Queue(Conf.Server.Id)) 135 | if err != nil { 136 | log.Errorf("broker topic:%v subscribe error:%v", brTopic, err) 137 | return message.QosFailure, err 138 | } else { 139 | log.Infof("broker topic:%v subscribed", brTopic) 140 | this.subers.Store(brTopic, suber) 141 | } 142 | } 143 | } 144 | 145 | log.Debugf("topic:%v subscribe", string(topic)) 146 | qos, err := this.p.Subscribe(topic, qos, subscriber) 147 | if err != nil { 148 | log.Errorf("topic:%v subscribe error:%v", string(topic), err) 149 | // @TODO 节点内订阅失败,broker是否需要取消订阅 150 | } 151 | 152 | return qos, err 153 | } 154 | 155 | func (this *Manager) Unsubscribe(topic []byte, subscriber interface{}) error { 156 | nodesNum, err := this.p.Unsubscribe(topic, subscriber) 157 | if err != nil { 158 | // @TODO 排除"No topic found"错误 159 | return err 160 | } 161 | 162 | // 节点内取消订阅后刷新broker订阅 163 | // topic下所有snodes数为0时取消订阅 164 | if this.broker != nil && this.subHandler != nil { 165 | if nodesNum <= 0 { 166 | brTopic := TopicToBrokerTopic(topic) 167 | v, ok := this.subers.Load(brTopic) 168 | if ok { 169 | suber, ok := v.(broker.Subscriber) 170 | if ok { 171 | if err = suber.Unsubscribe(); err != nil { 172 | return err 173 | } 174 | } 175 | 176 | this.subers.Delete(brTopic) 177 | } 178 | } 179 | } 180 | 181 | return err 182 | } 183 | 184 | func (this *Manager) Subscribers(topic []byte, qos byte, subs *[]interface{}, qoss *[]byte) error { 185 | return this.p.Subscribers(topic, qos, subs, qoss) 186 | } 187 | 188 | func (this *Manager) Retain(msg *message.PublishMessage) error { 189 | return this.p.Retain(msg) 190 | } 191 | 192 | func (this *Manager) Retained(topic []byte, msgs *[]*message.PublishMessage) error { 193 | return this.p.Retained(topic, msgs) 194 | } 195 | 196 | func (this *Manager) Close() error { 197 | return this.p.Close() 198 | } 199 | 200 | func TopicToBrokerTopic(topic []byte) string { 201 | // @TODO topic对应关系,n:1时Unsubscribe()需要重新设计 202 | str := string(topic) 203 | subs := strings.Split(str, "/") 204 | 205 | //log.Errorf("topic subs:%v", subs) 206 | 207 | if len(subs) > 0 { 208 | return strings.Trim(subs[0], "/") 209 | } else { 210 | // @TODO 默认topic或增加ERR 211 | return "topic" 212 | } 213 | } 214 | 215 | func BrokerTopicToTopic(topic string) []byte { 216 | topic = strings.Replace(topic, ".", "/", -1) 217 | 218 | return []byte(topic) 219 | } 220 | -------------------------------------------------------------------------------- /pkg/util/conv/conversion.go: -------------------------------------------------------------------------------- 1 | package conv 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | func IntPtrTo64(ptr interface{}) (value int64) { 8 | if v := reflect.ValueOf(ptr); v.Kind() == reflect.Ptr { 9 | p := v.Elem() 10 | switch p.Kind() { 11 | case reflect.Int: 12 | value = int64(*ptr.(*int)) 13 | case reflect.Int8: 14 | value = int64(*ptr.(*int8)) 15 | case reflect.Int16: 16 | value = int64(*ptr.(*int16)) 17 | case reflect.Int32: 18 | value = int64(*ptr.(*int32)) 19 | case reflect.Int64: 20 | value = *ptr.(*int64) 21 | } 22 | } 23 | return 24 | } 25 | 26 | func UintPtrTo64(ptr interface{}) (value uint64) { 27 | if v := reflect.ValueOf(ptr); v.Kind() == reflect.Ptr { 28 | p := v.Elem() 29 | switch p.Kind() { 30 | case reflect.Uint: 31 | value = uint64(*ptr.(*uint)) 32 | case reflect.Uint8: 33 | value = uint64(*ptr.(*uint8)) 34 | case reflect.Uint16: 35 | value = uint64(*ptr.(*uint16)) 36 | case reflect.Uint32: 37 | value = uint64(*ptr.(*uint32)) 38 | case reflect.Uint64: 39 | value = *ptr.(*uint64) 40 | } 41 | } 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /pkg/util/conv/rpcx.go: -------------------------------------------------------------------------------- 1 | package conv 2 | 3 | func ProtoEnumsToRpcxBasePath(enums map[int32]string) (path string) { 4 | path = "" 5 | for _, v := range enums { 6 | if len(v) > 0 { 7 | path += "/" + v 8 | } 9 | } 10 | 11 | return path 12 | } 13 | -------------------------------------------------------------------------------- /pkg/util/crypt/crypt.go: -------------------------------------------------------------------------------- 1 | package crypt 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/md5" 6 | "crypto/sha1" 7 | "encoding/base64" 8 | "encoding/hex" 9 | "fmt" 10 | "io" 11 | ) 12 | 13 | // 对字符串进行sha1 计算 14 | func Sha1(data string) string { 15 | t := sha1.New() 16 | io.WriteString(t, data) 17 | return fmt.Sprintf("%x", t.Sum(nil)) 18 | } 19 | 20 | // 对数据进行md5计算 21 | func MD5(byteMessage []byte) string { 22 | h := md5.New() 23 | h.Write(byteMessage) 24 | return hex.EncodeToString(h.Sum(nil)) 25 | } 26 | 27 | func HamSha1(data string, key []byte) string { 28 | hmac := hmac.New(sha1.New, key) 29 | hmac.Write([]byte(data)) 30 | 31 | return base64.StdEncoding.EncodeToString(hmac.Sum(nil)) 32 | } 33 | -------------------------------------------------------------------------------- /test/benchmark/client_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2013 IBM Corp. 3 | * 4 | * All rights reserved. This program and the accompanying materials 5 | * are made available under the terms of the Eclipse Public License v1.0 6 | * which accompanies this distribution, and is available at 7 | * http://www.eclipse.org/legal/epl-v10.html 8 | * 9 | * Contributors: 10 | * Seth Hoenig 11 | * Allan Stockdill-Mander 12 | * Mike Robertson 13 | */ 14 | 15 | // This is originally from 16 | // git.eclipse.org/gitroot/paho/org.eclipse.paho.mqtt.golang.git/samples/simple.go 17 | // I turned it into a test that I can run from `go test` 18 | package benchmark 19 | 20 | import ( 21 | "errors" 22 | "fmt" 23 | "math/rand" 24 | "sync" 25 | "sync/atomic" 26 | "testing" 27 | "time" 28 | 29 | MQTT "github.com/eclipse/paho.mqtt.golang" 30 | "github.com/stretchr/testify/require" 31 | 32 | "github.com/hb-chen/gmqtt/pkg/log" 33 | ) 34 | 35 | var f MQTT.MessageHandler = func(client MQTT.Client, msg MQTT.Message) { 36 | fmt.Printf("TOPIC: %s\n", msg.Topic()) 37 | fmt.Printf("MSG: %s\n", msg.Payload()) 38 | } 39 | 40 | var ( 41 | messages int64 = 10 42 | publishers int64 = 10 43 | subscribers int64 = 0 // =0不消费消息 44 | size int = 64 45 | topic []byte = []byte("testtopic/1") 46 | qos byte = 0 47 | order bool = true 48 | nap int64 = 100 49 | host string = "127.0.0.1" //121.37.240.135,124.71.17.65 50 | port int = 1883 51 | user string = "name" 52 | pass string = "pwd" 53 | version int = 4 54 | pubTimeout time.Duration = 3000 55 | 56 | subdone, pubdone, rcvdone, sentdone int64 57 | 58 | sReady, pReady chan struct{} 59 | rDone, sDone, rTimeOut chan struct{} 60 | 61 | totalSent, 62 | totalDropped, 63 | totalSentTime, 64 | totalRcvd, 65 | totalRcvdTime, 66 | sentSince, 67 | rcvdSince int64 68 | 69 | statMu sync.Mutex 70 | ) 71 | 72 | func init() { 73 | log.SetColor(true) 74 | log.SetLevel(log.INFO) 75 | } 76 | 77 | // Usage: go test -run=TestClient$ 78 | func TestClient(t *testing.T) { 79 | rDone = make(chan struct{}) 80 | 81 | var wg sync.WaitGroup 82 | 83 | addr := fmt.Sprintf("tcp://%s:%d", host, port) 84 | opts := MQTT.NewClientOptions().AddBroker(addr).SetClientID("clientId").SetOrderMatters(order) 85 | opts.SetDefaultPublishHandler(f) 86 | opts.SetPingTimeout(3 * time.Second) 87 | opts.SetConnectTimeout(3 * time.Second) 88 | 89 | c := MQTT.NewClient(opts) 90 | token := c.Connect() 91 | token.Wait() 92 | require.NoError(t, token.Error()) 93 | 94 | t.Log("connected") 95 | 96 | totalDropped = 0 97 | 98 | // 消息消费 99 | received := int64(0) 100 | rNow := time.Now() 101 | rSince := time.Since(rNow).Nanoseconds() 102 | if subscribers > 0 { 103 | token = c.Subscribe(string(topic), qos, func(client MQTT.Client, message MQTT.Message) { 104 | if received == 0 { 105 | rNow = time.Now() 106 | } 107 | 108 | received++ 109 | rSince = time.Since(rNow).Nanoseconds() 110 | log.Debugf("%d MSG: %s\n", received, message.Payload()) 111 | 112 | if received >= messages-totalDropped { 113 | 114 | close(rDone) 115 | } 116 | }) 117 | token.WaitTimeout(time.Millisecond * 100) 118 | token.Wait() 119 | require.NoError(t, token.Error()) 120 | } 121 | 122 | // 消息发布 123 | now := time.Now() 124 | since := time.Since(now).Nanoseconds() 125 | for j := 0; j < 1; j++ { 126 | wg.Add(1) 127 | go func() { 128 | defer wg.Done() 129 | 130 | drop := int64(0) 131 | payload := make([]byte, size) 132 | for i := int64(0); i < messages; i++ { 133 | //wait := time.After(time.Millisecond * 10) 134 | //<-wait 135 | 136 | msg := fmt.Sprintf("%d", i) 137 | copy(payload, []byte(msg)) 138 | token = c.Publish(string(topic), qos, false, payload) 139 | tTimeout := token.WaitTimeout(time.Millisecond * pubTimeout) 140 | require.NoError(t, token.Error()) 141 | 142 | if !tTimeout { 143 | drop++ 144 | log.Warnf("publish token wait/timeout msg:%v", msg) 145 | } 146 | } 147 | 148 | statMu.Lock() 149 | since = time.Since(now).Nanoseconds() 150 | totalSent += messages - drop 151 | totalDropped += drop 152 | statMu.Unlock() 153 | }() 154 | } 155 | 156 | wg.Wait() 157 | 158 | if subscribers > 0 { 159 | select { 160 | case <-rDone: 161 | case <-time.After(time.Millisecond * 1000 * time.Duration(messages)): 162 | log.Errorf("receiver wait time out") 163 | } 164 | } 165 | 166 | time.Sleep(3 * time.Second) 167 | 168 | if subscribers > 0 { 169 | token = c.Unsubscribe(string(topic)) 170 | if ok := token.WaitTimeout(time.Second * 10); !ok { 171 | log.Errorf("unsubscribe time out") 172 | } 173 | require.NoError(t, token.Error()) 174 | } 175 | 176 | c.Disconnect(250) 177 | 178 | log.Infof("Total sent %d messages dropped %d in %f ms, %f ms/msg, %d msgs/sec", totalSent, totalDropped, float64(since)/float64(time.Millisecond), float64(since)/float64(time.Millisecond)/float64(totalSent), int(float64(totalSent)/(float64(since)/float64(time.Second)))) 179 | log.Infof("Total received %d messages in %f ms, %f ms/msg, %d msgs/sec", received, float64(rSince)/float64(time.Millisecond), float64(rSince)/float64(time.Millisecond)/float64(received), int(float64(received)/(float64(rSince)/float64(time.Second)))) 180 | } 181 | 182 | // Usage: go test -run=TestClients$ 183 | func TestClients(t *testing.T) { 184 | 185 | var wg1 sync.WaitGroup 186 | var wg2 sync.WaitGroup 187 | 188 | totalSent = 0 189 | totalDropped = 0 190 | totalRcvd = 0 191 | totalSentTime = 0 192 | totalRcvdTime = 0 193 | sentSince = 0 194 | rcvdSince = 0 195 | 196 | subdone = 0 197 | pubdone = 0 198 | rcvdone = 0 199 | sentdone = 0 200 | 201 | sReady = make(chan struct{}) 202 | pReady = make(chan struct{}) 203 | rDone = make(chan struct{}) 204 | sDone = make(chan struct{}) 205 | rTimeOut = make(chan struct{}) 206 | 207 | if subscribers > 0 { 208 | for i := int64(1); i < subscribers+1; i++ { 209 | time.Sleep(time.Millisecond * 20) 210 | wg1.Add(1) 211 | go startSubscriber(t, i, &wg1) 212 | } 213 | 214 | <-sReady 215 | } 216 | 217 | for i := subscribers + 1; i < publishers+subscribers+1; i++ { 218 | time.Sleep(time.Millisecond * 20) 219 | wg2.Add(1) 220 | go startPublisher(t, i, &wg2) 221 | } 222 | 223 | wg1.Wait() 224 | wg2.Wait() 225 | 226 | log.Infof("Total Sent %d messages dropped %d in %f ms, %f ms/msg, %d msgs/sec", totalSent, totalDropped, float64(sentSince)/float64(time.Millisecond), float64(sentSince)/float64(time.Millisecond)/float64(totalSent), int(float64(totalSent)/(float64(sentSince)/float64(time.Second)))) 227 | log.Infof("Total Received %d messages in %f ms, %f ms/msg, %d msgs/sec", totalRcvd, float64(sentSince)/float64(time.Millisecond), float64(sentSince)/float64(time.Millisecond)/float64(totalRcvd), int(float64(totalRcvd)/(float64(sentSince)/float64(time.Second)))) 228 | } 229 | 230 | func startSubscriber(t testing.TB, cid int64, wg *sync.WaitGroup) { 231 | defer wg.Done() 232 | 233 | clientId := fmt.Sprintf("clientId-%d", cid) 234 | addr := fmt.Sprintf("tcp://%s:%d", host, port) 235 | opts := MQTT.NewClientOptions().AddBroker(addr).SetClientID(clientId).SetOrderMatters(order) 236 | opts.SetDefaultPublishHandler(f) 237 | 238 | c := MQTT.NewClient(opts) 239 | token := c.Connect() 240 | ok := token.WaitTimeout(time.Second * 10) 241 | if !ok { 242 | log.Errorf("subscriber connect time out") 243 | require.NoError(t, errors.New("subscriber connect time out")) 244 | return 245 | } 246 | require.NoError(t, token.Error()) 247 | 248 | now := time.Now() 249 | since := time.Since(now).Nanoseconds() 250 | cnt := messages * publishers 251 | received := int64(0) 252 | done := int64(0) 253 | 254 | filters := map[string]byte{string(topic): qos} 255 | token = c.SubscribeMultiple(filters, func(client MQTT.Client, message MQTT.Message) { 256 | if received == 0 { 257 | now = time.Now() 258 | } 259 | 260 | received++ 261 | since = time.Since(now).Nanoseconds() 262 | 263 | log.Debugf("(cid:%d) messages received:%d, msg:%v", cid, received, string(message.Payload())) 264 | if received%int64(float64(cnt)*0.1) == 0 { 265 | log.Infof("(cid:%d) messages received:%g%%", cid, float64(received)/float64(cnt)*100.0) 266 | } 267 | 268 | if done == 0 && received >= cnt-totalDropped { 269 | doit := atomic.CompareAndSwapInt64(&done, 0, 1) 270 | if doit { 271 | rcvd := atomic.AddInt64(&rcvdone, 1) 272 | if rcvd == subscribers { 273 | close(rDone) 274 | } 275 | } 276 | } 277 | }) 278 | token.WaitTimeout(time.Millisecond * 100) 279 | token.Wait() 280 | require.NoError(t, token.Error()) 281 | 282 | log.Infof("(cid:%d) subscriber ready", cid) 283 | 284 | subs := atomic.AddInt64(&subdone, 1) 285 | if subs == subscribers { 286 | log.Infof("subscribers sub done") 287 | close(sReady) 288 | } 289 | 290 | <-sDone 291 | 292 | allDone := false 293 | if done == 0 && received >= cnt-totalDropped { 294 | doit := atomic.CompareAndSwapInt64(&done, 0, 1) 295 | if doit { 296 | rcvd := atomic.AddInt64(&rcvdone, 1) 297 | if rcvd == subscribers { 298 | close(rDone) 299 | allDone = true 300 | } 301 | } else { 302 | if rcvdone == subscribers { 303 | allDone = true 304 | } 305 | } 306 | } 307 | 308 | if !allDone { 309 | select { 310 | case <-rDone: 311 | case <-time.After(time.Millisecond * time.Duration(nap*publishers)): 312 | close(rTimeOut) 313 | log.Warnf("(cid:%d) Timed out waiting for messages to be received.", cid) 314 | } 315 | } 316 | 317 | token = c.Unsubscribe(string(topic)) 318 | if ok := token.WaitTimeout(time.Second * 5); !ok { 319 | log.Errorf("subscriber unsub time out") 320 | } 321 | require.NoError(t, token.Error()) 322 | 323 | time.Sleep(3 * time.Second) 324 | c.Disconnect(250) 325 | 326 | statMu.Lock() 327 | totalRcvd += int64(received) 328 | totalRcvdTime += int64(since) 329 | if since > rcvdSince { 330 | rcvdSince = since 331 | } 332 | statMu.Unlock() 333 | 334 | log.Infof("(cid:%d) Received %d messages in %f ms, %f ms/msg, %d msgs/sec", cid, received, float64(since)/float64(time.Millisecond), float64(since)/float64(time.Millisecond)/float64(cnt), int(float64(received)/(float64(since)/float64(time.Second)))) 335 | } 336 | 337 | func startPublisher(t testing.TB, cid int64, wg *sync.WaitGroup) { 338 | defer wg.Done() 339 | 340 | clientId := fmt.Sprintf("clientId-%d", cid) 341 | addr := fmt.Sprintf("tcp://%s:%d", host, port) 342 | opts := MQTT.NewClientOptions().AddBroker(addr).SetClientID(clientId).SetOrderMatters(order).SetMessageChannelDepth(1000) 343 | opts.SetDefaultPublishHandler(f) 344 | 345 | c := MQTT.NewClient(opts) 346 | token := c.Connect() 347 | ok := token.WaitTimeout(time.Millisecond * 1000) 348 | if !ok { 349 | log.Errorf("publisher connect time out") 350 | require.NoError(t, errors.New("publisher connect time out")) 351 | return 352 | } 353 | require.NoError(t, token.Error()) 354 | 355 | pubs := atomic.AddInt64(&pubdone, 1) 356 | log.Infof("(cid:%d) publisher ready, total:%d (%d)", cid, pubs, cid-pubs) 357 | if pubs == int64(publishers) { 358 | log.Infof("publishers pub done") 359 | close(pReady) 360 | } else { 361 | <-pReady 362 | } 363 | 364 | now := time.Now() 365 | since := time.Since(now).Nanoseconds() 366 | cnt := messages 367 | sent := 0 368 | dropped := 0 369 | payload := make([]byte, size) 370 | for i := int64(0); i < cnt; i++ { 371 | // 随机延时发送 372 | if false { 373 | r := int64(100) 374 | if false { 375 | r += rand.Int63n(100) 376 | } 377 | wait := time.After(time.Millisecond * time.Duration(r)) 378 | <-wait 379 | } 380 | 381 | msg := fmt.Sprintf("cid:%d, msg:%d", cid, i) 382 | copy(payload, []byte(msg)) 383 | token = c.Publish(string(topic), qos, false, payload) 384 | tTimeout := token.WaitTimeout(time.Millisecond * pubTimeout) 385 | require.NoError(t, token.Error()) 386 | 387 | if !tTimeout { 388 | dropped++ 389 | log.Warnf("(cid:%d) publish wait timeout", cid) 390 | } else { 391 | sent++ 392 | } 393 | 394 | since = time.Since(now).Nanoseconds() 395 | } 396 | 397 | statMu.Lock() 398 | totalDropped += int64(dropped) 399 | statMu.Unlock() 400 | 401 | sends := atomic.AddInt64(&sentdone, 1) 402 | log.Infof("(cid:%d) publish done, total:%d", cid, sends) 403 | if sends == int64(publishers) { 404 | log.Infof("publishers sent done") 405 | close(sDone) 406 | } 407 | 408 | if subscribers > 0 { 409 | select { 410 | case <-rDone: 411 | case <-rTimeOut: 412 | } 413 | } 414 | 415 | time.Sleep(3 * time.Second) 416 | c.Disconnect(250) 417 | 418 | statMu.Lock() 419 | totalSent += int64(sent) 420 | totalSentTime += int64(since) 421 | if since > sentSince { 422 | sentSince = since 423 | } 424 | statMu.Unlock() 425 | 426 | log.Infof("(cid:%d) Sent %d messages dropped %d in %f ms, %f ms/msg, %d msgs/sec", cid, sent, dropped, float64(since)/float64(time.Millisecond), float64(since)/float64(time.Millisecond)/float64(cnt), int(float64(sent)/(float64(since)/float64(time.Second)))) 427 | } 428 | --------------------------------------------------------------------------------