├── .gitignore ├── define ├── common.go └── retcode │ └── retcode.go ├── conf ├── app.ini └── app.cluster.ini ├── tools ├── util │ ├── util_test.go │ └── util.go ├── crypto │ ├── crypto_test.go │ └── crypto.go └── log │ └── log.go ├── Dockerfile ├── .travis.yml ├── api ├── closeclient │ ├── closeclient.go │ └── closeclient_test.go ├── register │ └── register.go ├── getonlinelist │ ├── getonelinelist.go │ └── getonelinelist_test.go ├── bind2group │ ├── bind2group.go │ └── bind2group_test.go ├── send2client │ ├── send2client.go │ └── send2client_test.go ├── send2clients │ ├── send2clients.go │ └── send2clients_test.go ├── send2group │ ├── send2group.go │ └── send2group_test.go └── controller.go ├── pkg ├── etcd │ ├── etcd.go │ ├── etcddis.go │ └── etcdreg.go └── setting │ └── setting.go ├── servers ├── client.go ├── account.go ├── connect.go ├── grpc.proto ├── rpcserver.go ├── clientmanager_test.go ├── rpcclient.go ├── server.go ├── clientmanager.go └── pb │ └── grpc.pb.go ├── routers ├── middleware.go └── routers.go ├── main.go ├── docker-compose.yaml ├── docs ├── introduction.md └── api.md ├── go.mod ├── README.MD ├── LICENSE └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | go-websocket 2 | error.log 3 | log.log 4 | log 5 | -------------------------------------------------------------------------------- /define/common.go: -------------------------------------------------------------------------------- 1 | package define 2 | 3 | const ( 4 | //ETCD服务列表路径 5 | ETCD_SERVER_LIST = "/wsServers/" 6 | //账号信息前缀 7 | ETCD_PREFIX_ACCOUNT_INFO = "ws/account/" 8 | ) 9 | -------------------------------------------------------------------------------- /conf/app.ini: -------------------------------------------------------------------------------- 1 | [common] 2 | HttpPort = 7800 3 | RPCPort = 7900 4 | # 是否集群,单机则设为false 5 | Cluster = false 6 | # 对称加密key 只允许16、24、32字节长度 7 | CryptoKey = Adba723b7fe06819 8 | 9 | [etcd] 10 | Endpoints = -------------------------------------------------------------------------------- /define/retcode/retcode.go: -------------------------------------------------------------------------------- 1 | package retcode 2 | 3 | const ( 4 | SUCCESS = 0 5 | FAIL = -1 6 | 7 | SYSTEM_ID_ERROR = -1001 8 | ONLINE_MESSAGE_CODE = 1001 9 | OFFLINE_MESSAGE_CODE = 1002 10 | ) 11 | -------------------------------------------------------------------------------- /conf/app.cluster.ini: -------------------------------------------------------------------------------- 1 | [common] 2 | HttpPort = 7800 3 | RPCPort = 7900 4 | # 是否集群,单机则设为false 5 | Cluster = true 6 | # 对称加密key 只允许16、24、32字节长度 7 | CryptoKey = Adba723b7fe06819 8 | 9 | [etcd] 10 | Endpoints = ws_etcd1:2379, ws_etcd2:2379, ws_etcd3:2379 -------------------------------------------------------------------------------- /tools/util/util_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | . "github.com/smartystreets/goconvey/convey" 5 | "testing" 6 | ) 7 | 8 | func TestGenUUID(t *testing.T) { 9 | Convey("生成uuid", t, func() { 10 | uuid := GenUUID() 11 | Convey("验证长度", func() { 12 | So(len(uuid), ShouldBeGreaterThan, 0) 13 | }) 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13 AS build-dist 2 | ENV GOPROXY='https://mirrors.aliyun.com/goproxy' 3 | WORKDIR /data/release 4 | COPY . . 5 | RUN go build 6 | 7 | FROM centos:latest as prod 8 | WORKDIR /data/go-websocket 9 | COPY --from=build-dist /data/release/go-websocket ./ 10 | COPY --from=build-dist /data/release/conf /data/go-websocket/conf 11 | 12 | EXPOSE 6000 13 | 14 | CMD ["/data/go-websocket/go-websocket","-c","./conf/app.ini"] -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.13.x 5 | 6 | script: 7 | - ls -lh 8 | - go test -v ./... 9 | 10 | before_deploy: 11 | - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o go-websocket-linux-amd64 12 | - CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o go-websocket-windows-amd64.exe 13 | - CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o go-websocket-darwin-amd64 14 | - zip -r go-websocket-linux-amd64.zip go-websocket-linux-amd64 conf/* 15 | - zip -r go-websocket-windows-amd64.zip go-websocket-windows-amd64.exe conf/* 16 | - zip -r go-websocket-darwin-amd64.zip go-websocket-darwin-amd64 conf/* 17 | 18 | deploy: 19 | provider: releases 20 | api_key: ${GITHUB_TOKEN} 21 | file: 22 | - go-websocket-linux-amd64.zip 23 | - go-websocket-windows-amd64.zip 24 | - go-websocket-darwin-amd64.zip 25 | skip_cleanup: true 26 | on: 27 | tags: true 28 | 29 | after_deploy: 30 | - ls -lh -------------------------------------------------------------------------------- /api/closeclient/closeclient.go: -------------------------------------------------------------------------------- 1 | package closeclient 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/woodylan/go-websocket/api" 6 | "github.com/woodylan/go-websocket/define/retcode" 7 | "github.com/woodylan/go-websocket/servers" 8 | "net/http" 9 | ) 10 | 11 | type Controller struct { 12 | } 13 | 14 | type inputData struct { 15 | ClientId string `json:"clientId" validate:"required"` 16 | } 17 | 18 | func (c *Controller) Run(w http.ResponseWriter, r *http.Request) { 19 | var inputData inputData 20 | if err := json.NewDecoder(r.Body).Decode(&inputData); err != nil { 21 | w.WriteHeader(http.StatusBadRequest) 22 | return 23 | } 24 | 25 | err := api.Validate(inputData) 26 | if err != nil { 27 | api.Render(w, retcode.FAIL, err.Error(), []string{}) 28 | return 29 | } 30 | 31 | systemId := r.Header.Get("SystemId") 32 | 33 | //发送信息 34 | servers.CloseClient(inputData.ClientId, systemId) 35 | 36 | api.Render(w, retcode.SUCCESS, "success", map[string]string{}) 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /api/register/register.go: -------------------------------------------------------------------------------- 1 | package register 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/woodylan/go-websocket/api" 6 | "github.com/woodylan/go-websocket/define/retcode" 7 | "github.com/woodylan/go-websocket/servers" 8 | "net/http" 9 | ) 10 | 11 | type Controller struct { 12 | } 13 | 14 | type inputData struct { 15 | SystemId string `json:"systemId" validate:"required"` 16 | } 17 | 18 | func (c *Controller) Run(w http.ResponseWriter, r *http.Request) { 19 | var inputData inputData 20 | if err := json.NewDecoder(r.Body).Decode(&inputData); err != nil { 21 | w.WriteHeader(http.StatusBadRequest) 22 | return 23 | } 24 | 25 | err := api.Validate(inputData) 26 | if err != nil { 27 | api.Render(w, retcode.FAIL, err.Error(), []string{}) 28 | return 29 | } 30 | 31 | err = servers.Register(inputData.SystemId) 32 | if err != nil { 33 | api.Render(w, retcode.FAIL, err.Error(), []string{}) 34 | return 35 | } 36 | 37 | api.Render(w, retcode.SUCCESS, "success", []string{}) 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /pkg/etcd/etcd.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "github.com/coreos/etcd/clientv3" 6 | log "github.com/sirupsen/logrus" 7 | "github.com/woodylan/go-websocket/pkg/setting" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | var etcdKvClient *clientv3.Client 13 | var mu sync.Mutex 14 | 15 | func GetInstance() *clientv3.Client { 16 | if etcdKvClient == nil { 17 | if client, err := clientv3.New(clientv3.Config{ 18 | Endpoints: setting.EtcdSetting.Endpoints, 19 | DialTimeout: 5 * time.Second, 20 | }); err != nil { 21 | log.Error(err) 22 | return nil 23 | } else { 24 | //创建时才加锁 25 | mu.Lock() 26 | defer mu.Unlock() 27 | etcdKvClient = client 28 | return etcdKvClient 29 | } 30 | 31 | } 32 | return etcdKvClient 33 | } 34 | 35 | func Put(key, value string) error { 36 | _, err := GetInstance().Put(context.Background(), key, value) 37 | return err 38 | } 39 | 40 | func Get(key string) (resp *clientv3.GetResponse, err error) { 41 | resp, err = GetInstance().Get(context.Background(), key) 42 | return resp, err 43 | } 44 | -------------------------------------------------------------------------------- /api/getonlinelist/getonelinelist.go: -------------------------------------------------------------------------------- 1 | package getonlinelist 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/woodylan/go-websocket/api" 6 | "github.com/woodylan/go-websocket/define/retcode" 7 | "github.com/woodylan/go-websocket/servers" 8 | "net/http" 9 | ) 10 | 11 | type Controller struct { 12 | } 13 | 14 | type inputData struct { 15 | GroupName string `json:"groupName" validate:"required"` 16 | Code int `json:"code"` 17 | Msg string `json:"msg"` 18 | Data interface{} `json:"data"` 19 | } 20 | 21 | func (c *Controller) Run(w http.ResponseWriter, r *http.Request) { 22 | var inputData inputData 23 | if err := json.NewDecoder(r.Body).Decode(&inputData); err != nil { 24 | w.WriteHeader(http.StatusBadRequest) 25 | return 26 | } 27 | 28 | err := api.Validate(inputData) 29 | if err != nil { 30 | api.Render(w, retcode.FAIL, err.Error(), []string{}) 31 | return 32 | } 33 | 34 | systemId := r.Header.Get("SystemId") 35 | ret := servers.GetOnlineList(&systemId, &inputData.GroupName) 36 | 37 | api.Render(w, retcode.SUCCESS, "success", ret) 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /tools/crypto/crypto_test.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestEncrypt(t *testing.T) { 8 | raw := []byte("password123") 9 | key := []byte("asdf1234qwer7894") 10 | str, err := Encrypt(raw, key) 11 | if err == nil { 12 | t.Log("suc", str) 13 | } else { 14 | t.Fatal("fail", err) 15 | } 16 | } 17 | 18 | func TestDncrypt(t *testing.T) { 19 | raw := "pqjPM0GJUjlgryzMaslqBAzIknumcdgey1MN+ylWHqY=" 20 | key := []byte("asdf1234qwer7894") 21 | str, err := Decrypt(raw, key) 22 | if err == nil { 23 | t.Log("suc", str) 24 | } else { 25 | t.Fatal("fail", err) 26 | } 27 | } 28 | 29 | func Benchmark_CBCEncrypt(b *testing.B) { 30 | b.StopTimer() 31 | raw := []byte("146576885") 32 | key := []byte("gdgghf") 33 | 34 | b.StartTimer() //重新开始时间 35 | for i := 0; i < b.N; i++ { 36 | _, _ = Encrypt(raw, key) 37 | } 38 | } 39 | 40 | func Benchmark_CBCDecrypt(b *testing.B) { 41 | b.StopTimer() 42 | raw := "CDl4Uas8ZyaGXaoOhPZ9NLDvcsIkyvvd++TONd8UPZc=" 43 | key := []byte("gdgghf") 44 | 45 | b.StartTimer() //重新开始时间 46 | for i := 0; i < b.N; i++ { 47 | _, _ = Decrypt(raw, key) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /api/bind2group/bind2group.go: -------------------------------------------------------------------------------- 1 | package bind2group 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/woodylan/go-websocket/api" 6 | "github.com/woodylan/go-websocket/define/retcode" 7 | "github.com/woodylan/go-websocket/servers" 8 | "net/http" 9 | ) 10 | 11 | type Controller struct { 12 | } 13 | 14 | type inputData struct { 15 | ClientId string `json:"clientId" validate:"required"` 16 | GroupName string `json:"groupName" validate:"required"` 17 | UserId string `json:"userId"` 18 | Extend string `json:"extend"` // 拓展字段,方便业务存储数据 19 | } 20 | 21 | func (c *Controller) Run(w http.ResponseWriter, r *http.Request) { 22 | var inputData inputData 23 | if err := json.NewDecoder(r.Body).Decode(&inputData); err != nil { 24 | w.WriteHeader(http.StatusBadRequest) 25 | return 26 | } 27 | 28 | err := api.Validate(inputData) 29 | if err != nil { 30 | api.Render(w, retcode.FAIL, err.Error(), []string{}) 31 | return 32 | } 33 | 34 | systemId := r.Header.Get("SystemId") 35 | servers.AddClient2Group(systemId, inputData.GroupName, inputData.ClientId, inputData.UserId, inputData.Extend) 36 | 37 | api.Render(w, retcode.SUCCESS, "success", []string{}) 38 | } 39 | -------------------------------------------------------------------------------- /api/send2client/send2client.go: -------------------------------------------------------------------------------- 1 | package send2client 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/woodylan/go-websocket/api" 6 | "github.com/woodylan/go-websocket/define/retcode" 7 | "github.com/woodylan/go-websocket/servers" 8 | "net/http" 9 | ) 10 | 11 | type Controller struct { 12 | } 13 | 14 | type inputData struct { 15 | ClientId string `json:"clientId" validate:"required"` 16 | SendUserId string `json:"sendUserId"` 17 | Code int `json:"code"` 18 | Msg string `json:"msg"` 19 | Data string `json:"data"` 20 | } 21 | 22 | func (c *Controller) Run(w http.ResponseWriter, r *http.Request) { 23 | var inputData inputData 24 | if err := json.NewDecoder(r.Body).Decode(&inputData); err != nil { 25 | w.WriteHeader(http.StatusBadRequest) 26 | return 27 | } 28 | 29 | err := api.Validate(inputData) 30 | if err != nil { 31 | api.Render(w, retcode.FAIL, err.Error(), []string{}) 32 | return 33 | } 34 | 35 | //发送信息 36 | messageId := servers.SendMessage2Client(inputData.ClientId, inputData.SendUserId, inputData.Code, inputData.Msg, &inputData.Data) 37 | 38 | api.Render(w, retcode.SUCCESS, "success", map[string]string{ 39 | "messageId": messageId, 40 | }) 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /api/send2clients/send2clients.go: -------------------------------------------------------------------------------- 1 | package send2clients 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/woodylan/go-websocket/api" 6 | "github.com/woodylan/go-websocket/define/retcode" 7 | "github.com/woodylan/go-websocket/servers" 8 | "net/http" 9 | ) 10 | 11 | type Controller struct { 12 | } 13 | 14 | type inputData struct { 15 | ClientIds []string `json:"clientIds" validate:"required"` 16 | SendUserId string `json:"sendUserId"` 17 | Code int `json:"code"` 18 | Msg string `json:"msg"` 19 | Data string `json:"data"` 20 | } 21 | 22 | func (c *Controller) Run(w http.ResponseWriter, r *http.Request) { 23 | var inputData inputData 24 | if err := json.NewDecoder(r.Body).Decode(&inputData); err != nil { 25 | w.WriteHeader(http.StatusBadRequest) 26 | return 27 | } 28 | 29 | err := api.Validate(inputData) 30 | if err != nil { 31 | api.Render(w, retcode.FAIL, err.Error(), []string{}) 32 | return 33 | } 34 | 35 | for _, clientId := range inputData.ClientIds { 36 | //发送信息 37 | _ = servers.SendMessage2Client(clientId, inputData.SendUserId, inputData.Code, inputData.Msg, &inputData.Data) 38 | } 39 | 40 | api.Render(w, retcode.SUCCESS, "success", []string{}) 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /api/send2group/send2group.go: -------------------------------------------------------------------------------- 1 | package send2group 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/woodylan/go-websocket/api" 6 | "github.com/woodylan/go-websocket/define/retcode" 7 | "github.com/woodylan/go-websocket/servers" 8 | "net/http" 9 | ) 10 | 11 | type Controller struct { 12 | } 13 | 14 | type inputData struct { 15 | SendUserId string `json:"sendUserId"` 16 | GroupName string `json:"groupName" validate:"required"` 17 | Code int `json:"code"` 18 | Msg string `json:"msg"` 19 | Data string `json:"data"` 20 | } 21 | 22 | func (c *Controller) Run(w http.ResponseWriter, r *http.Request) { 23 | var inputData inputData 24 | if err := json.NewDecoder(r.Body).Decode(&inputData); err != nil { 25 | w.WriteHeader(http.StatusBadRequest) 26 | return 27 | } 28 | 29 | err := api.Validate(inputData) 30 | if err != nil { 31 | api.Render(w, retcode.FAIL, err.Error(), []string{}) 32 | return 33 | } 34 | 35 | systemId := r.Header.Get("SystemId") 36 | messageId := servers.SendMessage2Group(systemId, inputData.SendUserId, inputData.GroupName, inputData.Code, inputData.Msg, &inputData.Data) 37 | 38 | api.Render(w, retcode.SUCCESS, "success", map[string]string{ 39 | "messageId": messageId, 40 | }) 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /servers/client.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | "time" 6 | ) 7 | 8 | type Client struct { 9 | ClientId string // 标识ID 10 | SystemId string // 系统ID 11 | Socket *websocket.Conn // 用户连接 12 | ConnectTime uint64 // 首次连接时间 13 | IsDeleted bool // 是否删除或下线 14 | UserId string // 业务端标识用户ID 15 | Extend string // 扩展字段,用户可以自定义 16 | GroupList []string 17 | } 18 | 19 | type SendData struct { 20 | Code int 21 | Msg string 22 | Data *interface{} 23 | } 24 | 25 | func NewClient(clientId string, systemId string, socket *websocket.Conn) *Client { 26 | return &Client{ 27 | ClientId: clientId, 28 | SystemId: systemId, 29 | Socket: socket, 30 | ConnectTime: uint64(time.Now().Unix()), 31 | IsDeleted: false, 32 | } 33 | } 34 | 35 | func (c *Client) Read() { 36 | go func() { 37 | for { 38 | messageType, _, err := c.Socket.ReadMessage() 39 | if err != nil { 40 | if messageType == -1 && websocket.IsCloseError(err, websocket.CloseGoingAway, websocket.CloseNormalClosure, websocket.CloseNoStatusReceived) { 41 | Manager.DisConnect <- c 42 | return 43 | } else if messageType != websocket.PingMessage { 44 | return 45 | } 46 | } 47 | } 48 | }() 49 | } 50 | -------------------------------------------------------------------------------- /servers/account.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/woodylan/go-websocket/define" 7 | "github.com/woodylan/go-websocket/pkg/etcd" 8 | "github.com/woodylan/go-websocket/tools/util" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | type accountInfo struct { 14 | SystemId string `json:"systemId"` 15 | RegisterTime int64 `json:"registerTime"` 16 | } 17 | 18 | var SystemMap sync.Map 19 | 20 | func Register(systemId string) (err error) { 21 | //校验是否为空 22 | if len(systemId) == 0 { 23 | return errors.New("系统ID不能为空") 24 | } 25 | 26 | accountInfo := accountInfo{ 27 | SystemId: systemId, 28 | RegisterTime: time.Now().Unix(), 29 | } 30 | 31 | if util.IsCluster() { 32 | //判断是否被注册 33 | resp, err := etcd.Get(define.ETCD_PREFIX_ACCOUNT_INFO + systemId) 34 | if err != nil { 35 | return err 36 | } 37 | 38 | if resp.Count > 0 { 39 | return errors.New("该系统ID已被注册") 40 | } 41 | 42 | jsonBytes, _ := json.Marshal(accountInfo) 43 | 44 | //注册 45 | err = etcd.Put(define.ETCD_PREFIX_ACCOUNT_INFO+systemId, string(jsonBytes)) 46 | if err != nil { 47 | panic(err) 48 | return err 49 | } 50 | } else { 51 | if _, ok := SystemMap.Load(systemId); ok { 52 | return errors.New("该系统ID已被注册") 53 | } 54 | 55 | SystemMap.Store(systemId, accountInfo) 56 | } 57 | 58 | return nil 59 | } 60 | -------------------------------------------------------------------------------- /routers/middleware.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/woodylan/go-websocket/api" 5 | "github.com/woodylan/go-websocket/define" 6 | "github.com/woodylan/go-websocket/define/retcode" 7 | "github.com/woodylan/go-websocket/pkg/etcd" 8 | "github.com/woodylan/go-websocket/servers" 9 | "github.com/woodylan/go-websocket/tools/util" 10 | "net/http" 11 | ) 12 | 13 | func AccessTokenMiddleware(next http.HandlerFunc) http.HandlerFunc { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | if r.Method != http.MethodPost { 16 | w.WriteHeader(http.StatusMethodNotAllowed) 17 | return 18 | } 19 | 20 | //检查header是否设置SystemId 21 | systemId := r.Header.Get("SystemId") 22 | if len(systemId) == 0 { 23 | api.Render(w, retcode.FAIL, "系统ID不能为空", []string{}) 24 | return 25 | } 26 | 27 | //判断是否被注册 28 | if util.IsCluster() { 29 | resp, err := etcd.Get(define.ETCD_PREFIX_ACCOUNT_INFO + systemId) 30 | if err != nil { 31 | api.Render(w, retcode.FAIL, "etcd服务器错误", []string{}) 32 | return 33 | } 34 | 35 | if resp.Count == 0 { 36 | api.Render(w, retcode.FAIL, "系统ID无效", []string{}) 37 | return 38 | } 39 | } else { 40 | if _, ok := servers.SystemMap.Load(systemId); !ok { 41 | api.Render(w, retcode.FAIL, "系统ID无效", []string{}) 42 | return 43 | } 44 | } 45 | 46 | next.ServeHTTP(w, r) 47 | }) 48 | } 49 | -------------------------------------------------------------------------------- /routers/routers.go: -------------------------------------------------------------------------------- 1 | package routers 2 | 3 | import ( 4 | "github.com/woodylan/go-websocket/api/bind2group" 5 | "github.com/woodylan/go-websocket/api/closeclient" 6 | "github.com/woodylan/go-websocket/api/getonlinelist" 7 | "github.com/woodylan/go-websocket/api/register" 8 | "github.com/woodylan/go-websocket/api/send2client" 9 | "github.com/woodylan/go-websocket/api/send2clients" 10 | "github.com/woodylan/go-websocket/api/send2group" 11 | "github.com/woodylan/go-websocket/servers" 12 | "net/http" 13 | ) 14 | 15 | func Init() { 16 | registerHandler := ®ister.Controller{} 17 | sendToClientHandler := &send2client.Controller{} 18 | sendToClientsHandler := &send2clients.Controller{} 19 | sendToGroupHandler := &send2group.Controller{} 20 | bindToGroupHandler := &bind2group.Controller{} 21 | getGroupListHandler := &getonlinelist.Controller{} 22 | closeClientHandler := &closeclient.Controller{} 23 | 24 | http.HandleFunc("/api/register", registerHandler.Run) 25 | http.HandleFunc("/api/send_to_client", AccessTokenMiddleware(sendToClientHandler.Run)) 26 | http.HandleFunc("/api/send_to_clients", AccessTokenMiddleware(sendToClientsHandler.Run)) 27 | http.HandleFunc("/api/send_to_group", AccessTokenMiddleware(sendToGroupHandler.Run)) 28 | http.HandleFunc("/api/bind_to_group", AccessTokenMiddleware(bindToGroupHandler.Run)) 29 | http.HandleFunc("/api/get_online_list", AccessTokenMiddleware(getGroupListHandler.Run)) 30 | http.HandleFunc("/api/close_client", AccessTokenMiddleware(closeClientHandler.Run)) 31 | 32 | servers.StartWebSocket() 33 | 34 | go servers.WriteMessage() 35 | } 36 | -------------------------------------------------------------------------------- /servers/connect.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/woodylan/go-websocket/api" 7 | "github.com/woodylan/go-websocket/define/retcode" 8 | "github.com/woodylan/go-websocket/tools/util" 9 | "net/http" 10 | ) 11 | 12 | const ( 13 | // 最大的消息大小 14 | maxMessageSize = 8192 15 | ) 16 | 17 | type Controller struct { 18 | } 19 | 20 | type renderData struct { 21 | ClientId string `json:"clientId"` 22 | } 23 | 24 | func (c *Controller) Run(w http.ResponseWriter, r *http.Request) { 25 | conn, err := (&websocket.Upgrader{ 26 | ReadBufferSize: 1024, 27 | WriteBufferSize: 1024, 28 | // 允许所有CORS跨域请求 29 | CheckOrigin: func(r *http.Request) bool { 30 | return true 31 | }, 32 | }).Upgrade(w, r, nil) 33 | if err != nil { 34 | log.Errorf("upgrade error: %v", err) 35 | http.NotFound(w, r) 36 | return 37 | } 38 | 39 | //设置读取消息大小上线 40 | conn.SetReadLimit(maxMessageSize) 41 | 42 | //解析参数 43 | systemId := r.FormValue("systemId") 44 | if len(systemId) == 0 { 45 | _ = Render(conn, "", "", retcode.SYSTEM_ID_ERROR, "系统ID不能为空", []string{}) 46 | _ = conn.Close() 47 | return 48 | } 49 | 50 | clientId := util.GenClientId() 51 | 52 | clientSocket := NewClient(clientId, systemId, conn) 53 | 54 | Manager.AddClient2SystemClient(systemId, clientSocket) 55 | 56 | //读取客户端消息 57 | clientSocket.Read() 58 | 59 | if err = api.ConnRender(conn, renderData{ClientId: clientId}); err != nil { 60 | _ = conn.Close() 61 | return 62 | } 63 | 64 | // 用户连接事件 65 | Manager.Connect <- clientSocket 66 | } 67 | -------------------------------------------------------------------------------- /api/closeclient/closeclient_test.go: -------------------------------------------------------------------------------- 1 | package closeclient 2 | 3 | import ( 4 | "encoding/json" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "github.com/woodylan/go-websocket/pkg/setting" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | type testServer struct { 15 | *httptest.Server 16 | ClientURL string 17 | } 18 | 19 | type retMessage struct { 20 | Code int `json:"code"` 21 | Msg string `json:"msg"` 22 | } 23 | 24 | func newServer(t *testing.T) *testServer { 25 | var s testServer 26 | setting.Default() 27 | 28 | controller := &Controller{} 29 | s.Server = httptest.NewServer(http.HandlerFunc(controller.Run)) 30 | s.ClientURL = s.Server.URL + "/api/close_client" 31 | return &s 32 | } 33 | 34 | func TestRun(t *testing.T) { 35 | s := newServer(t) 36 | defer s.Close() 37 | 38 | testContent := `{"clientId":"ade447d79f6489b5"}` 39 | 40 | resp, err := http.Post(s.ClientURL, "application/json", strings.NewReader(testContent)) 41 | Convey("测试关闭指定连接", t, func() { 42 | Convey("是否有报错", func() { 43 | So(err, ShouldBeNil) 44 | }) 45 | }) 46 | defer resp.Body.Close() 47 | 48 | retMessage := retMessage{} 49 | message, err := ioutil.ReadAll(resp.Body) 50 | 51 | err = json.Unmarshal(message, &retMessage) 52 | 53 | Convey("验证json解析返回的内容", t, func() { 54 | err := json.Unmarshal(message, &retMessage) 55 | Convey("是否解析成功", func() { 56 | So(err, ShouldBeNil) 57 | }) 58 | 59 | Convey("Code格式", func() { 60 | So(retMessage.Code, ShouldEqual, 0) 61 | }) 62 | 63 | Convey("Msg格式", func() { 64 | So(retMessage.Msg, ShouldEqual, "success") 65 | }) 66 | 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /api/getonlinelist/getonelinelist_test.go: -------------------------------------------------------------------------------- 1 | package getonlinelist 2 | 3 | import ( 4 | "encoding/json" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "github.com/woodylan/go-websocket/pkg/setting" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | type testServer struct { 15 | *httptest.Server 16 | ClientURL string 17 | } 18 | 19 | type retMessage struct { 20 | Code int `json:"code"` 21 | Msg string `json:"msg"` 22 | } 23 | 24 | func newServer(t *testing.T) *testServer { 25 | var s testServer 26 | setting.Default() 27 | 28 | controller := &Controller{} 29 | s.Server = httptest.NewServer(http.HandlerFunc(controller.Run)) 30 | s.ClientURL = s.Server.URL + "/api/get_online_list" 31 | return &s 32 | } 33 | 34 | func TestRun(t *testing.T) { 35 | s := newServer(t) 36 | defer s.Close() 37 | 38 | testContent := `{"groupName":"im"}` 39 | 40 | resp, err := http.Post(s.ClientURL, "application/json", strings.NewReader(testContent)) 41 | Convey("测试发送消息到指定分组", t, func() { 42 | Convey("是否有报错", func() { 43 | So(err, ShouldBeNil) 44 | }) 45 | }) 46 | defer resp.Body.Close() 47 | 48 | retMessage := retMessage{} 49 | message, err := ioutil.ReadAll(resp.Body) 50 | 51 | err = json.Unmarshal(message, &retMessage) 52 | 53 | Convey("验证json解析返回的内容", t, func() { 54 | err := json.Unmarshal(message, &retMessage) 55 | Convey("是否解析成功", func() { 56 | So(err, ShouldBeNil) 57 | }) 58 | 59 | Convey("Code格式", func() { 60 | So(retMessage.Code, ShouldEqual, 0) 61 | }) 62 | 63 | Convey("Msg格式", func() { 64 | So(retMessage.Msg, ShouldEqual, "success") 65 | }) 66 | 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /api/controller.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | zhongwen "github.com/go-playground/locales/zh" 6 | ut "github.com/go-playground/universal-translator" 7 | "github.com/gorilla/websocket" 8 | "github.com/pkg/errors" 9 | "github.com/woodylan/go-websocket/define/retcode" 10 | "gopkg.in/go-playground/validator.v9" 11 | zh2 "gopkg.in/go-playground/validator.v9/translations/zh" 12 | "io" 13 | "net/http" 14 | ) 15 | 16 | type RetData struct { 17 | Code int `json:"code"` 18 | Msg string `json:"msg"` 19 | Data interface{} `json:"data"` 20 | } 21 | 22 | func ConnRender(conn *websocket.Conn, data interface{}) (err error) { 23 | err = conn.WriteJSON(RetData{ 24 | Code: retcode.SUCCESS, 25 | Msg: "success", 26 | Data: data, 27 | }) 28 | 29 | return 30 | } 31 | 32 | func Render(w http.ResponseWriter, code int, msg string, data interface{}) (str string) { 33 | var retData RetData 34 | 35 | retData.Code = code 36 | retData.Msg = msg 37 | retData.Data = data 38 | 39 | retJson, _ := json.Marshal(retData) 40 | str = string(retJson) 41 | 42 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 43 | _, _ = io.WriteString(w, str) 44 | return 45 | } 46 | 47 | func Validate(inputData interface{}) error { 48 | 49 | validate := validator.New() 50 | zh := zhongwen.New() 51 | uni := ut.New(zh, zh) 52 | trans, _ := uni.GetTranslator("zh") 53 | 54 | _ = zh2.RegisterDefaultTranslations(validate, trans) 55 | 56 | err := validate.Struct(inputData) 57 | if err != nil { 58 | for _, err := range err.(validator.ValidationErrors) { 59 | return errors.New(err.Translate(trans)) 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /api/send2group/send2group_test.go: -------------------------------------------------------------------------------- 1 | package send2group 2 | 3 | import ( 4 | "encoding/json" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "github.com/woodylan/go-websocket/pkg/setting" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | type testServer struct { 15 | *httptest.Server 16 | ClientURL string 17 | } 18 | 19 | type retMessage struct { 20 | Code int `json:"code"` 21 | Msg string `json:"msg"` 22 | } 23 | 24 | func newServer(t *testing.T) *testServer { 25 | var s testServer 26 | setting.Default() 27 | 28 | controller := &Controller{} 29 | s.Server = httptest.NewServer(http.HandlerFunc(controller.Run)) 30 | s.ClientURL = s.Server.URL + "/api/send_to_group" 31 | return &s 32 | } 33 | 34 | func TestRun(t *testing.T) { 35 | s := newServer(t) 36 | defer s.Close() 37 | 38 | testContent := `{"groupName":"im","code":0,"msg":"success","data":"im分组接收消息"}` 39 | 40 | resp, err := http.Post(s.ClientURL, "application/json", strings.NewReader(testContent)) 41 | Convey("测试发送消息到指定分组", t, func() { 42 | Convey("是否有报错", func() { 43 | So(err, ShouldBeNil) 44 | }) 45 | }) 46 | defer resp.Body.Close() 47 | 48 | retMessage := retMessage{} 49 | message, err := ioutil.ReadAll(resp.Body) 50 | 51 | err = json.Unmarshal(message, &retMessage) 52 | 53 | Convey("验证json解析返回的内容", t, func() { 54 | err := json.Unmarshal(message, &retMessage) 55 | Convey("是否解析成功", func() { 56 | So(err, ShouldBeNil) 57 | }) 58 | 59 | Convey("Code格式", func() { 60 | So(retMessage.Code, ShouldEqual, 0) 61 | }) 62 | 63 | Convey("Msg格式", func() { 64 | So(retMessage.Msg, ShouldEqual, "success") 65 | }) 66 | 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /api/bind2group/bind2group_test.go: -------------------------------------------------------------------------------- 1 | package bind2group 2 | 3 | import ( 4 | "encoding/json" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "github.com/woodylan/go-websocket/pkg/setting" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | type testServer struct { 15 | *httptest.Server 16 | ClientURL string 17 | } 18 | 19 | type retMessage struct { 20 | Code int `json:"code"` 21 | Msg string `json:"msg"` 22 | } 23 | 24 | func newServer(t *testing.T) *testServer { 25 | var s testServer 26 | setting.Default() 27 | 28 | controller := &Controller{} 29 | s.Server = httptest.NewServer(http.HandlerFunc(controller.Run)) 30 | s.ClientURL = s.Server.URL + "/api/bind_to_group" 31 | return &s 32 | } 33 | 34 | func TestRun(t *testing.T) { 35 | s := newServer(t) 36 | defer s.Close() 37 | 38 | testContent := `{"clientId":"ade447d79f6489b5","groupName":"im","userId":"userId"}` 39 | 40 | resp, err := http.Post(s.ClientURL, "application/json", strings.NewReader(testContent)) 41 | Convey("测试发送绑定分组消息", t, func() { 42 | Convey("是否有报错", func() { 43 | So(err, ShouldBeNil) 44 | }) 45 | }) 46 | defer resp.Body.Close() 47 | 48 | retMessage := retMessage{} 49 | message, err := ioutil.ReadAll(resp.Body) 50 | 51 | err = json.Unmarshal(message, &retMessage) 52 | 53 | Convey("验证json解析返回的内容", t, func() { 54 | err := json.Unmarshal(message, &retMessage) 55 | Convey("是否解析成功", func() { 56 | So(err, ShouldBeNil) 57 | }) 58 | 59 | Convey("Code格式", func() { 60 | So(retMessage.Code, ShouldEqual, 0) 61 | }) 62 | 63 | Convey("Msg格式", func() { 64 | So(retMessage.Msg, ShouldEqual, "success") 65 | }) 66 | 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /api/send2client/send2client_test.go: -------------------------------------------------------------------------------- 1 | package send2client 2 | 3 | import ( 4 | "encoding/json" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "github.com/woodylan/go-websocket/pkg/setting" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | type testServer struct { 15 | *httptest.Server 16 | ClientURL string 17 | } 18 | 19 | type retMessage struct { 20 | Code int `json:"code"` 21 | Msg string `json:"msg"` 22 | } 23 | 24 | func newServer(t *testing.T) *testServer { 25 | var s testServer 26 | setting.Default() 27 | 28 | controller := &Controller{} 29 | s.Server = httptest.NewServer(http.HandlerFunc(controller.Run)) 30 | s.ClientURL = s.Server.URL + "/api/send_to_client" 31 | return &s 32 | } 33 | 34 | func TestRun(t *testing.T) { 35 | s := newServer(t) 36 | defer s.Close() 37 | 38 | testContent := `{"clientId":"ade447d79f6489b5","code":0,"msg":"success","data":"发送给指定客户端"}` 39 | 40 | resp, err := http.Post(s.ClientURL, "application/json", strings.NewReader(testContent)) 41 | Convey("测试发送消息给指定客户端", t, func() { 42 | Convey("是否有报错", func() { 43 | So(err, ShouldBeNil) 44 | }) 45 | }) 46 | defer resp.Body.Close() 47 | 48 | retMessage := retMessage{} 49 | message, err := ioutil.ReadAll(resp.Body) 50 | 51 | err = json.Unmarshal(message, &retMessage) 52 | 53 | Convey("验证json解析返回的内容", t, func() { 54 | err := json.Unmarshal(message, &retMessage) 55 | Convey("是否解析成功", func() { 56 | So(err, ShouldBeNil) 57 | }) 58 | 59 | Convey("Code格式", func() { 60 | So(retMessage.Code, ShouldEqual, 0) 61 | }) 62 | 63 | Convey("Msg格式", func() { 64 | So(retMessage.Msg, ShouldEqual, "success") 65 | }) 66 | 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /api/send2clients/send2clients_test.go: -------------------------------------------------------------------------------- 1 | package send2clients 2 | 3 | import ( 4 | "encoding/json" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "github.com/woodylan/go-websocket/pkg/setting" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | ) 13 | 14 | type testServer struct { 15 | *httptest.Server 16 | ClientURL string 17 | } 18 | 19 | type retMessage struct { 20 | Code int `json:"code"` 21 | Msg string `json:"msg"` 22 | } 23 | 24 | func newServer(t *testing.T) *testServer { 25 | var s testServer 26 | setting.Default() 27 | 28 | controller := &Controller{} 29 | s.Server = httptest.NewServer(http.HandlerFunc(controller.Run)) 30 | s.ClientURL = s.Server.URL + "/api/send_to_clients" 31 | return &s 32 | } 33 | 34 | func TestRun(t *testing.T) { 35 | s := newServer(t) 36 | defer s.Close() 37 | 38 | testContent := `{"clientIds":["ade447d79f6489b5"],"code":0,"msg":"success","data":"发送给指定客户端"}` 39 | 40 | resp, err := http.Post(s.ClientURL, "application/json", strings.NewReader(testContent)) 41 | Convey("测试发送消息给指定客户端", t, func() { 42 | Convey("是否有报错", func() { 43 | So(err, ShouldBeNil) 44 | }) 45 | }) 46 | defer resp.Body.Close() 47 | 48 | retMessage := retMessage{} 49 | message, err := ioutil.ReadAll(resp.Body) 50 | 51 | err = json.Unmarshal(message, &retMessage) 52 | 53 | Convey("验证json解析返回的内容", t, func() { 54 | err := json.Unmarshal(message, &retMessage) 55 | Convey("是否解析成功", func() { 56 | So(err, ShouldBeNil) 57 | }) 58 | 59 | Convey("Code格式", func() { 60 | So(retMessage.Code, ShouldEqual, 0) 61 | }) 62 | 63 | Convey("Msg格式", func() { 64 | So(retMessage.Msg, ShouldEqual, "success") 65 | }) 66 | 67 | }) 68 | } 69 | -------------------------------------------------------------------------------- /tools/log/log.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | rotatelogs "github.com/lestrrat-go/file-rotatelogs" 5 | "github.com/pkg/errors" 6 | "github.com/rifflock/lfshook" 7 | "github.com/sirupsen/logrus" 8 | "github.com/woodylan/go-websocket/pkg/setting" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | func Setup() { 15 | basePath := getCurrentDirectory() 16 | 17 | writer, err := rotatelogs.New( 18 | basePath+"/log/info/"+"%Y-%m-%d"+".log", 19 | rotatelogs.WithLinkName("log.log"), // 生成软链,指向最新日志文件 20 | //rotatelogs.WithMaxAge(maxAge), // 文件最大保存时间 21 | ) 22 | if err != nil { 23 | logrus.Errorf("config local file system logger error. %+v", errors.WithStack(err)) 24 | } 25 | 26 | errorWriter, err := rotatelogs.New( 27 | basePath+"/log/error/"+"%Y-%m-%d"+".log", 28 | rotatelogs.WithLinkName("error.log"), // 生成软链,指向最新日志文件 29 | //rotatelogs.WithMaxAge(maxAge), // 文件最大保存时间 30 | ) 31 | if err != nil { 32 | logrus.Errorf("config local file system logger error. %+v", errors.WithStack(err)) 33 | } 34 | lfHook := lfshook.NewHook(lfshook.WriterMap{ 35 | logrus.DebugLevel: writer, // 为不同级别设置不同的输出目的 36 | logrus.InfoLevel: writer, 37 | logrus.WarnLevel: writer, 38 | logrus.ErrorLevel: errorWriter, 39 | logrus.FatalLevel: writer, 40 | logrus.PanicLevel: writer, 41 | }, &logrus.JSONFormatter{ 42 | TimestampFormat: "2006-01-02 15:04:05", 43 | PrettyPrint: false, //是否格式化json格式 44 | FieldMap: logrus.FieldMap{ 45 | "host": setting.GlobalSetting.LocalHost, 46 | }, 47 | }) 48 | //logrus.SetReportCaller(true) //是否记录代码位置 49 | logrus.AddHook(lfHook) 50 | } 51 | 52 | //获取当前程序运行的文件夹 53 | func getCurrentDirectory() string { 54 | dir, _ := filepath.Abs(filepath.Dir(os.Args[0])) 55 | return strings.Replace(dir, "\\", "/", -1) 56 | } 57 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/woodylan/go-websocket/define" 6 | "github.com/woodylan/go-websocket/pkg/etcd" 7 | "github.com/woodylan/go-websocket/pkg/setting" 8 | "github.com/woodylan/go-websocket/routers" 9 | "github.com/woodylan/go-websocket/servers" 10 | "github.com/woodylan/go-websocket/tools/log" 11 | "github.com/woodylan/go-websocket/tools/util" 12 | "net" 13 | "net/http" 14 | ) 15 | 16 | func init() { 17 | setting.Setup() 18 | log.Setup() 19 | } 20 | 21 | func main() { 22 | //初始化RPC服务 23 | initRPCServer() 24 | 25 | //将服务器地址、端口注册到etcd中 26 | registerServer() 27 | 28 | //初始化路由 29 | routers.Init() 30 | 31 | //启动一个定时器用来发送心跳 32 | servers.PingTimer() 33 | 34 | fmt.Printf("服务器启动成功,端口号:%s\n", setting.CommonSetting.HttpPort) 35 | 36 | if err := http.ListenAndServe(":"+setting.CommonSetting.HttpPort, nil); err != nil { 37 | panic(err) 38 | } 39 | } 40 | 41 | func initRPCServer() { 42 | //如果是集群,则启用RPC进行通讯 43 | if util.IsCluster() { 44 | //初始化RPC服务 45 | servers.InitGRpcServer() 46 | fmt.Printf("启动RPC,端口号:%s\n", setting.CommonSetting.RPCPort) 47 | } 48 | } 49 | 50 | //ETCD注册发现服务 51 | func registerServer() { 52 | if util.IsCluster() { 53 | //注册租约 54 | ser, err := etcd.NewServiceReg(setting.EtcdSetting.Endpoints, 5) 55 | if err != nil { 56 | panic(err) 57 | } 58 | 59 | hostPort := net.JoinHostPort(setting.GlobalSetting.LocalHost, setting.CommonSetting.RPCPort) 60 | //添加key 61 | err = ser.PutService(define.ETCD_SERVER_LIST+hostPort, hostPort) 62 | if err != nil { 63 | panic(err) 64 | } 65 | 66 | cli, err := etcd.NewClientDis(setting.EtcdSetting.Endpoints) 67 | if err != nil { 68 | panic(err) 69 | } 70 | _, err = cli.GetService(define.ETCD_SERVER_LIST) 71 | if err != nil { 72 | panic(err) 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /servers/grpc.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option go_package = "servers/pb"; 4 | 5 | message Send2ClientReq { 6 | string systemId = 1; 7 | string messageId = 2; 8 | string sendUserId = 3; 9 | string clientId = 4; 10 | int32 code = 5; 11 | string message = 6; 12 | string data = 7; 13 | } 14 | 15 | message CloseClientReq { 16 | string systemId = 1; 17 | string clientId = 2; 18 | } 19 | 20 | message BindGroupReq { 21 | string systemId = 1; 22 | string groupName = 2; 23 | string clientId = 3; 24 | string userId = 4; 25 | string extend = 5; 26 | } 27 | 28 | message Send2GroupReq { 29 | string systemId = 1; 30 | string messageId = 2; 31 | string sendUserId = 3; 32 | string groupName = 4; 33 | int32 code = 5; 34 | string message = 6; 35 | string data = 7; 36 | } 37 | 38 | message Send2SystemReq { 39 | string systemId = 1; 40 | string messageId = 2; 41 | string sendUserId = 3; 42 | int32 code = 4; 43 | string message = 5; 44 | string data = 6; 45 | } 46 | 47 | message GetGroupClientsReq { 48 | string systemId = 1; 49 | string groupName = 2; 50 | } 51 | 52 | message Send2ClientReply { 53 | } 54 | 55 | message CloseClientReply { 56 | } 57 | 58 | message BindGroupReply { 59 | } 60 | 61 | message Send2GroupReply { 62 | } 63 | 64 | message Send2SystemReply { 65 | } 66 | 67 | message GetGroupClientsReply { 68 | repeated string list = 1; 69 | } 70 | 71 | service CommonService { 72 | rpc Send2Client (Send2ClientReq) returns (Send2ClientReply) { 73 | } 74 | rpc CloseClient (CloseClientReq) returns (CloseClientReply) { 75 | } 76 | rpc BindGroup (BindGroupReq) returns (BindGroupReply) { 77 | } 78 | rpc Send2Group (Send2GroupReq) returns (Send2GroupReply) { 79 | } 80 | rpc Send2System (Send2SystemReq) returns (Send2SystemReply) { 81 | } 82 | rpc GetGroupClients (GetGroupClientsReq) returns (GetGroupClientsReply) { 83 | } 84 | } -------------------------------------------------------------------------------- /tools/util/util.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "errors" 5 | uuid "github.com/satori/go.uuid" 6 | "github.com/woodylan/go-websocket/pkg/setting" 7 | "github.com/woodylan/go-websocket/tools/crypto" 8 | "strings" 9 | ) 10 | 11 | //GenUUID 生成uuid 12 | func GenUUID() string { 13 | uuidFunc := uuid.NewV4() 14 | uuidStr := uuidFunc.String() 15 | uuidStr = strings.Replace(uuidStr, "-", "", -1) 16 | uuidByt := []rune(uuidStr) 17 | return string(uuidByt[8:24]) 18 | } 19 | 20 | //对称加密IP和端口,当做clientId 21 | func GenClientId() string { 22 | raw := []byte(setting.GlobalSetting.LocalHost + ":" + setting.CommonSetting.RPCPort) 23 | str, err := crypto.Encrypt(raw, []byte(setting.CommonSetting.CryptoKey)) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | return str 29 | } 30 | 31 | //解析redis的地址格式 32 | func ParseRedisAddrValue(redisValue string) (host string, port string, err error) { 33 | if redisValue == "" { 34 | err = errors.New("解析地址错误") 35 | return 36 | } 37 | addr := strings.Split(redisValue, ":") 38 | if len(addr) != 2 { 39 | err = errors.New("解析地址错误") 40 | return 41 | } 42 | host, port = addr[0], addr[1] 43 | 44 | return 45 | } 46 | 47 | //判断地址是否为本机 48 | func IsAddrLocal(host string, port string) bool { 49 | return host == setting.GlobalSetting.LocalHost && port == setting.CommonSetting.RPCPort 50 | } 51 | 52 | //是否集群 53 | func IsCluster() bool { 54 | return setting.CommonSetting.Cluster 55 | } 56 | 57 | //获取client key地址信息 58 | func GetAddrInfoAndIsLocal(clientId string) (addr string, host string, port string, isLocal bool, err error) { 59 | //解密ClientId 60 | addr, err = crypto.Decrypt(clientId, []byte(setting.CommonSetting.CryptoKey)) 61 | if err != nil { 62 | return 63 | } 64 | 65 | host, port, err = ParseRedisAddrValue(addr) 66 | if err != nil { 67 | return 68 | } 69 | 70 | isLocal = IsAddrLocal(host, port) 71 | return 72 | } 73 | 74 | func GenGroupKey(systemId, groupName string) string { 75 | return systemId + ":" + groupName 76 | } 77 | -------------------------------------------------------------------------------- /pkg/setting/setting.go: -------------------------------------------------------------------------------- 1 | package setting 2 | 3 | import ( 4 | "flag" 5 | "github.com/go-ini/ini" 6 | "log" 7 | "net" 8 | "sync" 9 | ) 10 | 11 | type commonConf struct { 12 | HttpPort string 13 | RPCPort string 14 | Cluster bool 15 | CryptoKey string 16 | } 17 | 18 | var CommonSetting = &commonConf{} 19 | 20 | type etcdConf struct { 21 | Endpoints []string 22 | } 23 | 24 | var EtcdSetting = &etcdConf{} 25 | 26 | type global struct { 27 | LocalHost string //本机内网IP 28 | ServerList map[string]string 29 | ServerListLock sync.RWMutex 30 | } 31 | 32 | var GlobalSetting = &global{} 33 | 34 | var cfg *ini.File 35 | 36 | func Setup() { 37 | configFile := flag.String("c", "conf/app.ini", "-c conf/app.ini") 38 | flag.Parse() 39 | 40 | var err error 41 | cfg, err = ini.Load(*configFile) 42 | if err != nil { 43 | log.Fatalf("setting.Setup, fail to parse 'conf/app.ini': %v", err) 44 | } 45 | 46 | mapTo("common", CommonSetting) 47 | mapTo("etcd", EtcdSetting) 48 | 49 | GlobalSetting = &global{ 50 | LocalHost: getIntranetIp(), 51 | ServerList: make(map[string]string), 52 | } 53 | } 54 | 55 | func Default() { 56 | CommonSetting = &commonConf{ 57 | HttpPort: "6000", 58 | RPCPort: "7000", 59 | Cluster: false, 60 | CryptoKey: "Adba723b7fe06819", 61 | } 62 | 63 | GlobalSetting = &global{ 64 | LocalHost: getIntranetIp(), 65 | ServerList: make(map[string]string), 66 | } 67 | } 68 | 69 | // mapTo map section 70 | func mapTo(section string, v interface{}) { 71 | err := cfg.Section(section).MapTo(v) 72 | if err != nil { 73 | log.Fatalf("Cfg.MapTo %s err: %v", section, err) 74 | } 75 | } 76 | 77 | //获取本机内网IP 78 | func getIntranetIp() string { 79 | addrs, _ := net.InterfaceAddrs() 80 | 81 | for _, addr := range addrs { 82 | // 检查ip地址判断是否回环地址 83 | if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 84 | if ipnet.IP.To4() != nil { 85 | return ipnet.IP.String() 86 | } 87 | 88 | } 89 | } 90 | 91 | return "" 92 | } 93 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | services: 3 | ws_etcd1: 4 | image: quay.io/coreos/etcd 5 | container_name: ws_etcd1 6 | command: etcd -name ws_etcd1 -advertise-client-urls http://0.0.0.0:2379 -listen-client-urls http://0.0.0.0:2379 -listen-peer-urls http://0.0.0.0:2380 -initial-cluster-token etcd-cluster -initial-cluster "ws_etcd1=http://ws_etcd1:2380,ws_etcd2=http://ws_etcd2:2380,ws_etcd3=http://ws_etcd3:2380" -initial-cluster-state new 7 | ports: 8 | - "23791:2379" 9 | - "23801:2380" 10 | networks: 11 | - ws-net 12 | 13 | ws_etcd2: 14 | image: quay.io/coreos/etcd 15 | container_name: ws_etcd2 16 | command: etcd -name ws_etcd2 -advertise-client-urls http://0.0.0.0:2379 -listen-client-urls http://0.0.0.0:2379 -listen-peer-urls http://0.0.0.0:2380 -initial-cluster-token etcd-cluster -initial-cluster "ws_etcd1=http://ws_etcd1:2380,ws_etcd2=http://ws_etcd2:2380,ws_etcd3=http://ws_etcd3:2380" -initial-cluster-state new 17 | ports: 18 | - "23792:2379" 19 | - "23802:2380" 20 | networks: 21 | - ws-net 22 | 23 | ws_etcd3: 24 | image: quay.io/coreos/etcd 25 | container_name: ws_etcd3 26 | command: etcd -name ws_etcd3 -advertise-client-urls http://0.0.0.0:2379 -listen-client-urls http://0.0.0.0:2379 -listen-peer-urls http://0.0.0.0:2380 -initial-cluster-token etcd-cluster -initial-cluster "ws_etcd1=http://ws_etcd1:2380,ws_etcd2=http://ws_etcd2:2380,ws_etcd3=http://ws_etcd3:2380" -initial-cluster-state new 27 | ports: 28 | - "23793:2379" 29 | - "23803:2380" 30 | networks: 31 | - ws-net 32 | 33 | webapp: 34 | container_name: go-websocket 35 | build: 36 | context: ./ 37 | dockerfile: Dockerfile 38 | command: /data/go-websocket/go-websocket -c /data/go-websocket/conf/app.cluster.ini 39 | depends_on: 40 | - ws_etcd1 41 | - ws_etcd2 42 | - ws_etcd3 43 | ports: 44 | - "6000:6000" 45 | networks: 46 | - ws-net 47 | 48 | networks: 49 | ws-net: 50 | name: ws-net 51 | -------------------------------------------------------------------------------- /pkg/etcd/etcddis.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "github.com/coreos/etcd/clientv3" 6 | "github.com/coreos/etcd/mvcc/mvccpb" 7 | log "github.com/sirupsen/logrus" 8 | "github.com/woodylan/go-websocket/pkg/setting" 9 | "time" 10 | ) 11 | 12 | type ClientDis struct { 13 | client *clientv3.Client 14 | } 15 | 16 | func NewClientDis(addr []string) (*ClientDis, error) { 17 | conf := clientv3.Config{ 18 | Endpoints: addr, 19 | DialTimeout: 5 * time.Second, 20 | } 21 | if client, err := clientv3.New(conf); err == nil { 22 | return &ClientDis{ 23 | client: client, 24 | }, nil 25 | } else { 26 | return nil, err 27 | } 28 | } 29 | 30 | func (this *ClientDis) GetService(prefix string) ([]string, error) { 31 | resp, err := this.client.Get(context.Background(), prefix, clientv3.WithPrefix()) 32 | if err != nil { 33 | return nil, err 34 | } 35 | addrs := this.extractAddrs(resp) 36 | 37 | go this.watcher(prefix) 38 | return addrs, nil 39 | } 40 | 41 | func (this *ClientDis) watcher(prefix string) { 42 | rch := this.client.Watch(context.Background(), prefix, clientv3.WithPrefix()) 43 | for wresp := range rch { 44 | for _, ev := range wresp.Events { 45 | switch ev.Type { 46 | case mvccpb.PUT: 47 | this.SetServiceList(string(ev.Kv.Key), string(ev.Kv.Value)) 48 | case mvccpb.DELETE: 49 | this.DelServiceList(string(ev.Kv.Key)) 50 | } 51 | } 52 | } 53 | } 54 | 55 | func (this *ClientDis) extractAddrs(resp *clientv3.GetResponse) []string { 56 | addrs := make([]string, 0) 57 | if resp == nil || resp.Kvs == nil { 58 | return addrs 59 | } 60 | for i := range resp.Kvs { 61 | if v := resp.Kvs[i].Value; v != nil { 62 | this.SetServiceList(string(resp.Kvs[i].Key), string(resp.Kvs[i].Value)) 63 | addrs = append(addrs, string(v)) 64 | } 65 | } 66 | return addrs 67 | } 68 | 69 | func (this *ClientDis) SetServiceList(key, val string) { 70 | setting.GlobalSetting.ServerListLock.Lock() 71 | defer setting.GlobalSetting.ServerListLock.Unlock() 72 | setting.GlobalSetting.ServerList[key] = val 73 | log.Info("发现服务:", key, " 地址:", val) 74 | } 75 | 76 | func (this *ClientDis) DelServiceList(key string) { 77 | setting.GlobalSetting.ServerListLock.Lock() 78 | defer setting.GlobalSetting.ServerListLock.Unlock() 79 | delete(setting.GlobalSetting.ServerList, key) 80 | log.Println("服务下线:", key) 81 | } 82 | -------------------------------------------------------------------------------- /pkg/etcd/etcdreg.go: -------------------------------------------------------------------------------- 1 | package etcd 2 | 3 | import ( 4 | "context" 5 | "github.com/coreos/etcd/clientv3" 6 | log "github.com/sirupsen/logrus" 7 | "time" 8 | ) 9 | 10 | //注册租约服务 11 | type ServiceReg struct { 12 | client *clientv3.Client 13 | lease clientv3.Lease //租约 14 | leaseResp *clientv3.LeaseGrantResponse 15 | canclefunc func() 16 | keepAliveChan <-chan *clientv3.LeaseKeepAliveResponse 17 | } 18 | 19 | func NewServiceReg(addr []string, timeNum int64) (*ServiceReg, error) { 20 | var ( 21 | err error 22 | client *clientv3.Client 23 | ) 24 | 25 | if client, err = clientv3.New(clientv3.Config{ 26 | Endpoints: addr, 27 | DialTimeout: 5 * time.Second, 28 | }); err != nil { 29 | return nil, err 30 | } 31 | 32 | ser := &ServiceReg{ 33 | client: client, 34 | } 35 | 36 | if err := ser.setLease(timeNum); err != nil { 37 | return nil, err 38 | } 39 | go ser.ListenLeaseRespChan() 40 | return ser, nil 41 | } 42 | 43 | //设置租约 44 | func (this *ServiceReg) setLease(timeNum int64) error { 45 | lease := clientv3.NewLease(this.client) 46 | 47 | ctx, cancel := context.WithTimeout(context.TODO(), 2*time.Second) 48 | leaseResp, err := lease.Grant(ctx, timeNum) 49 | if err != nil { 50 | cancel() 51 | return err 52 | } 53 | 54 | ctx, cancelFunc := context.WithCancel(context.TODO()) 55 | leaseRespChan, err := lease.KeepAlive(ctx, leaseResp.ID) 56 | 57 | if err != nil { 58 | return err 59 | } 60 | 61 | this.lease = lease 62 | this.leaseResp = leaseResp 63 | this.canclefunc = cancelFunc 64 | this.keepAliveChan = leaseRespChan 65 | return nil 66 | } 67 | 68 | //监听续租情况 69 | func (this *ServiceReg) ListenLeaseRespChan() { 70 | for { 71 | select { 72 | case leaseKeepResp := <-this.keepAliveChan: 73 | if leaseKeepResp == nil { 74 | log.Error("已经关闭续租功能") 75 | return 76 | } else { 77 | //log.Info("续租成功") 78 | } 79 | } 80 | } 81 | } 82 | 83 | //注册租约 84 | func (this *ServiceReg) PutService(key, val string) error { 85 | kv := clientv3.NewKV(this.client) 86 | _, err := kv.Put(context.TODO(), key, val, clientv3.WithLease(this.leaseResp.ID)) 87 | return err 88 | } 89 | 90 | //撤销租约 91 | func (this *ServiceReg) RevokeLease() error { 92 | this.canclefunc() 93 | time.Sleep(2 * time.Second) 94 | _, err := this.lease.Revoke(context.TODO(), this.leaseResp.ID) 95 | return err 96 | } 97 | -------------------------------------------------------------------------------- /docs/introduction.md: -------------------------------------------------------------------------------- 1 | ## 使用场景 2 | 3 | 在实现业务的时候,我们常常有些需求需要系统主动发送消息给客户端,方案有轮询和长连接,但轮询需要不断的创建销毁http连接,对客户端、对服务器来说都挺消耗资源的,消息推送也不够实时。这里我们选择了WebSocket长连接的方案。 4 | 5 | 有大量的项目需要服务端主动向客户端推送消息,为了减少重复开发,我们做成了微服务。 6 | 7 | 使用于服务器需要主动向客户端推送消息、客户端需要实时获取消息的请求。例如聊天、广播消息、多人游戏消息推送、任务执行结果推送等方面。 8 | 9 | 10 | 11 | ## 分布式方案 12 | 13 | 维持大量的长连接对单台服务器的压力也挺大的,这里也就要求该服务需要可以扩容,也就是分布式地扩展。分布式对于可存储的公共资源有一套完整的解决方案,但对于WebSocket来说,操作对象就是每一个连接,它是维持在每一个程序中的。每一个连接不能存储起来共享、不能在不同的程序之间共享。所以我能想到的方案是不同程序之间进行通讯。 14 | 15 | 那么,怎样知道某个连接在哪个应用呢?答案是通过client id去判断。那么通过client id又是如何知道的呢?有以下几种方案: 16 | 17 | 1. 一致性hash算法 18 | 19 | 一致性hash算法是将整个哈希值空间组织成一个虚拟的圆环,在redis集群中哈希函数的值空间为0-2^32-1(32位无符号整型)。把服务器的IP或主机名作为关键字,通过哈希函数计算出相应的值,对应到这个虚拟的圆环空间。我们再通过哈希函数计算key的值,得到一个在圆环空间的位置,按顺时针方向找到的第一个节点就是存放该key数据的服务器节点。 20 | 21 | 在没有节点的增减的时候,可以满足我们的需求,但如果此时一个节点挂掉了或者新增一个机器怎么办?节点挂点之后,会在圆环上删除节点,增加节点则反之。这时候按顺时针方向找的数据就不准确,在某些业务上来说可以接受,但在WebSocket微服务上来说,影响范围内的连接会断掉,如果要求没那么高,客户端再进行重连也可以。 22 | 23 | 2. hash slot(哈希槽) 24 | 25 | 服务器的IP或者主机名作为key,对每个key进行计算CRC16值,然后对16384进行取模,得出一个对应key的hash slot。 26 | 27 | ``` 28 | HASH_SLOT = CRC16(key) mod 16384 29 | ``` 30 | 31 | 我们根据节点的数量,给每个节点划分范围,这个范围是0-16384。hash slot的重点就在这个虚拟表,key对应的hash slot是永不变的,增减节点就是维护这张虚拟表。 32 | 33 | 以上两种方案都可以实现需求,但一致性hash算法的方案会使部分key找到的节点不准确;hash slot的方案需要维护一张虚拟表,在实现起来需要有一个功能去判断服务器是否挂了,然后修改这张虚拟表,新增节点也一样,在实现起来会遇到很多问题。 34 | 35 | 然后我采取的方案是,每个连接都保存在本应用,然后用对称加密加密服务器IP和端口,得到的值作为client id。对指定client id进行操作时,只需要解密这个key,就能得到相应的IP和端口。判断是否为本机,不是本机的话进行RPC通讯告诉相应的程序。长连接的连接数据不可迁移,程序挂掉了相应的连接也就挂了,在该程序上的连接也就断开了,这时重连的话会找到另一个可用的程序。 36 | 37 | 38 | 39 | ### 架构图 40 | 41 | **单机服务** 42 | ![WebSocket单机服务架构图](http://cdn.wugenglong.com/assets/2020/0220rnY3bm%20.jpg) 43 | 44 | **分布式** 45 | 46 | ![WebSocket分布式服务架构图](http://cdn.wugenglong.com/assets/2020/02203m4ehn%20.jpg) 47 | 48 | 49 | 50 | ### 时序图 51 | 52 | **单发消息** 53 | 54 | 1. 客户端发送连接请求,连接请求通过nginx负载均衡找到一台ws服务器; 55 | 2. ws服务器响应连接请求,通过对称加密服务器IP和端口号,得到的值作为client id,并返回。 56 | 3. 客户端拿到client id之后,交给业务系统; 57 | 4. 业务系统拿到client id之后,通过http发送相关消息,经过nginx负载分配到一台ws服务器; 58 | 5. 这台ws服务器拿到clinet id和消息,解密出对应的服务器IP和端口; 59 | 6. 拿到IP地址和端口,通过PRC协议给指定ws程序发送信息; 60 | 7. 该ws程序接收到client id和信息,给指定的连接发送信息; 61 | 8. 客户端收到信息。 62 | 63 | ![WebSocket微服务单发时序图](http://cdn.wugenglong.com/assets/2020/02206m9kwZ%20.jpg) 64 | 65 | **群发消息** 66 | 67 | 1. 前3个步骤跟单发的一样; 68 | 2. 业务系统拿到client id之后,通过http给指定分组发送消息,经过nginx负载分配到一台ws服务器; 69 | 3. 这台ws服务器拿到分组ID和消息,去ETCD获取服务器列表,然后发送RPC广播; 70 | 4. 所有收到广播的服务,找到本机所有该分组的连接; 71 | 5. 给所有这些连接发送消息; 72 | 6. 客户端收到信息。 73 | 74 | ![WebSocket微服务群发消息时序图](http://cdn.wugenglong.com/assets/2020/02202VrOra%20.jpg) 75 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/woodylan/go-websocket 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/coreos/bbolt v1.3.3 // indirect 7 | github.com/coreos/etcd v3.3.17+incompatible 8 | github.com/coreos/go-semver v0.2.0 // indirect 9 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f // indirect 10 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f // indirect 11 | github.com/dgrijalva/jwt-go v3.2.0+incompatible // indirect 12 | github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 // indirect 13 | github.com/go-ini/ini v1.61.0 14 | github.com/go-playground/locales v0.13.0 15 | github.com/go-playground/universal-translator v0.17.0 16 | github.com/gogo/protobuf v1.3.2 // indirect 17 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect 18 | github.com/golang/protobuf v1.4.3 19 | github.com/google/btree v1.0.0 // indirect 20 | github.com/google/uuid v1.1.1 // indirect 21 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect 22 | github.com/gorilla/websocket v1.4.1 23 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 // indirect 24 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect 25 | github.com/grpc-ecosystem/grpc-gateway v1.9.5 // indirect 26 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect 27 | github.com/jonboulle/clockwork v0.1.0 // indirect 28 | github.com/leodido/go-urn v1.2.0 // indirect 29 | github.com/lestrrat-go/file-rotatelogs v2.3.0+incompatible 30 | github.com/lestrrat-go/strftime v1.0.1 // indirect 31 | github.com/pkg/errors v0.9.1 32 | github.com/prometheus/client_golang v1.11.1 // indirect 33 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 34 | github.com/satori/go.uuid v1.2.0 35 | github.com/sirupsen/logrus v1.6.0 36 | github.com/smartystreets/goconvey v1.6.4 37 | github.com/soheilhy/cmux v0.1.4 // indirect 38 | github.com/tebeka/strftime v0.1.3 // indirect 39 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect 40 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect 41 | go.etcd.io/bbolt v1.3.1-etcd.8 // indirect 42 | go.uber.org/zap v1.12.0 // indirect 43 | golang.org/x/net v0.7.0 // indirect 44 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect 45 | google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 // indirect 46 | google.golang.org/grpc v1.26.0 47 | gopkg.in/go-playground/assert.v1 v1.2.1 // indirect 48 | gopkg.in/go-playground/validator.v9 v9.31.0 49 | gopkg.in/ini.v1 v1.61.0 // indirect 50 | sigs.k8s.io/yaml v1.1.0 // indirect 51 | ) 52 | -------------------------------------------------------------------------------- /tools/crypto/crypto.go: -------------------------------------------------------------------------------- 1 | package crypto 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "encoding/base64" 9 | "errors" 10 | "io" 11 | ) 12 | 13 | func Encrypt(rawData, key []byte) (string, error) { 14 | data, err := aesCBCEncrypt(rawData, key) 15 | if err != nil { 16 | return "", err 17 | } 18 | return base64.StdEncoding.EncodeToString(data), nil 19 | } 20 | 21 | func Decrypt(rawData string, key []byte) (string, error) { 22 | data, err := base64.StdEncoding.DecodeString(rawData) 23 | if err != nil { 24 | return "", err 25 | } 26 | dnData, err := aesCBCDncrypt(data, key) 27 | if err != nil { 28 | return "", err 29 | } 30 | return string(dnData), nil 31 | } 32 | 33 | func pKCS7Padding(ciphertext []byte, blockSize int) []byte { 34 | padding := blockSize - len(ciphertext)%blockSize 35 | padtext := bytes.Repeat([]byte{byte(padding)}, padding) 36 | return append(ciphertext, padtext...) 37 | } 38 | 39 | func pKCS7UnPadding(origData []byte) ([]byte, error) { 40 | length := len(origData) 41 | unpadding := int(origData[length-1]) 42 | if length-unpadding < 0 || length-unpadding > len(origData) { 43 | return nil, errors.New("unPadding error") 44 | } 45 | 46 | return origData[:(length - unpadding)], nil 47 | } 48 | 49 | //AES加密 50 | func aesCBCEncrypt(rawData, key []byte) ([]byte, error) { 51 | block, err := aes.NewCipher(key) 52 | if err != nil { 53 | return []byte{}, err 54 | } 55 | 56 | //填充原文 57 | blockSize := block.BlockSize() 58 | 59 | rawData = pKCS7Padding(rawData, blockSize) 60 | //初始向量IV必须是唯一,但不需要保密 61 | cipherText := make([]byte, blockSize+len(rawData)) 62 | //block大小 16 63 | iv := cipherText[:blockSize] 64 | if _, err := io.ReadFull(rand.Reader, iv); err != nil { 65 | return []byte{}, err 66 | } 67 | 68 | //block大小和初始向量大小一定要一致 69 | mode := cipher.NewCBCEncrypter(block, iv) 70 | mode.CryptBlocks(cipherText[blockSize:], rawData) 71 | 72 | return cipherText, nil 73 | } 74 | 75 | //AES解密 76 | func aesCBCDncrypt(encryptData, key []byte) ([]byte, error) { 77 | block, err := aes.NewCipher(key) 78 | if err != nil { 79 | return []byte{}, err 80 | } 81 | 82 | blockSize := block.BlockSize() 83 | 84 | if len(encryptData) < blockSize { 85 | return []byte{}, errors.New("ciphertext too short") 86 | } 87 | iv := encryptData[:blockSize] 88 | encryptData = encryptData[blockSize:] 89 | 90 | if len(encryptData)%blockSize != 0 { 91 | return []byte{}, errors.New("ciphertext is not a multiple of the block size") 92 | } 93 | 94 | mode := cipher.NewCBCDecrypter(block, iv) 95 | 96 | mode.CryptBlocks(encryptData, encryptData) 97 | //解填充 98 | encryptData, err = pKCS7UnPadding(encryptData) 99 | return encryptData, err 100 | } 101 | -------------------------------------------------------------------------------- /servers/rpcserver.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/woodylan/go-websocket/pkg/setting" 7 | "github.com/woodylan/go-websocket/servers/pb" 8 | "github.com/woodylan/go-websocket/tools/util" 9 | "google.golang.org/grpc" 10 | "net" 11 | ) 12 | 13 | type CommonServiceServer struct{} 14 | 15 | func (this *CommonServiceServer) Send2Client(ctx context.Context, req *pb.Send2ClientReq) (*pb.Send2ClientReply, error) { 16 | log.WithFields(log.Fields{ 17 | "host": setting.GlobalSetting.LocalHost, 18 | "port": setting.CommonSetting.HttpPort, 19 | "clientId": req.ClientId, 20 | }).Info("接收到RPC指定客户端消息") 21 | SendMessage2LocalClient(req.MessageId, req.ClientId, req.SendUserId, int(req.Code), req.Message, &req.Data) 22 | return &pb.Send2ClientReply{}, nil 23 | } 24 | 25 | func (this *CommonServiceServer) CloseClient(ctx context.Context, req *pb.CloseClientReq) (*pb.CloseClientReply, error) { 26 | log.WithFields(log.Fields{ 27 | "host": setting.GlobalSetting.LocalHost, 28 | "port": setting.CommonSetting.HttpPort, 29 | "clientId": req.ClientId, 30 | }).Info("接收到RPC关闭连接") 31 | CloseLocalClient(req.ClientId, req.SystemId) 32 | return &pb.CloseClientReply{}, nil 33 | } 34 | 35 | //添加分组到group 36 | func (this *CommonServiceServer) BindGroup(ctx context.Context, req *pb.BindGroupReq) (*pb.BindGroupReply, error) { 37 | if client, err := Manager.GetByClientId(req.ClientId); err == nil { 38 | //添加到本地 39 | Manager.AddClient2LocalGroup(req.GroupName, client, req.UserId, req.Extend) 40 | } else { 41 | log.Error("添加分组失败" + err.Error()) 42 | } 43 | return &pb.BindGroupReply{}, nil 44 | } 45 | 46 | func (this *CommonServiceServer) Send2Group(ctx context.Context, req *pb.Send2GroupReq) (*pb.Send2GroupReply, error) { 47 | log.WithFields(log.Fields{ 48 | "host": setting.GlobalSetting.LocalHost, 49 | "port": setting.CommonSetting.HttpPort, 50 | }).Info("接收到RPC发送分组消息") 51 | Manager.SendMessage2LocalGroup(req.SystemId, req.MessageId, req.SendUserId, req.GroupName, int(req.Code), req.Message, &req.Data) 52 | return &pb.Send2GroupReply{}, nil 53 | } 54 | 55 | func (this *CommonServiceServer) Send2System(ctx context.Context, req *pb.Send2SystemReq) (*pb.Send2SystemReply, error) { 56 | log.WithFields(log.Fields{ 57 | "host": setting.GlobalSetting.LocalHost, 58 | "port": setting.CommonSetting.HttpPort, 59 | }).Info("接收到RPC发送系统消息") 60 | Manager.SendMessage2LocalSystem(req.SystemId, req.MessageId, req.SendUserId, int(req.Code), req.Message, &req.Data) 61 | return &pb.Send2SystemReply{}, nil 62 | } 63 | 64 | //获取分组在线用户列表 65 | func (this *CommonServiceServer) GetGroupClients(ctx context.Context, req *pb.GetGroupClientsReq) (*pb.GetGroupClientsReply, error) { 66 | response := pb.GetGroupClientsReply{} 67 | response.List = Manager.GetGroupClientList(util.GenGroupKey(req.SystemId, req.GroupName)) 68 | return &response, nil 69 | } 70 | 71 | func InitGRpcServer() { 72 | go createGRPCServer(":" + setting.CommonSetting.RPCPort) 73 | } 74 | 75 | func createGRPCServer(port string) { 76 | lis, err := net.Listen("tcp", port) 77 | if err != nil { 78 | panic(err) 79 | } 80 | 81 | s := grpc.NewServer() 82 | pb.RegisterCommonServiceServer(s, &CommonServiceServer{}) 83 | 84 | err = s.Serve(lis) 85 | if err != nil { 86 | panic(err) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Golang实现的分布式WebSocket微服务 2 | 3 | [![Go](https://img.shields.io/badge/Go-1.13-blue.svg)](https://golang.google.cn) 4 | ![GitHub release](https://img.shields.io/github/v/release/woodylan/go-websocket) 5 | ![Travis (.org)](https://api.travis-ci.com/woodylan/go-websocket.svg?branch=master) 6 | [![star](https://img.shields.io/github/stars/woodylan/go-websocket?style=social)](https://github.com/woodylan/go-websocket/stargazers) 7 | 8 | ## 简介 9 | 10 | 本系统基于Golang、ETCD、RPC实现分布式WebSocket微服务,也可以单机部署,单机部署不需要ETCD、RPC。分布式部署可以支持nginx负责均衡、水平扩容部署,程序之间使用RPC通信。 11 | 12 | 基本流程为:用ws协议连接本服务,得到一个clientId,由客户端上报这个clinetId给服务端,服务端拿到这个clientId之后,可以给这个客户端发送信息,绑定这个客户端都分组,给分组发送消息。 13 | 14 | 目前实现的功能有,给指定客户端发送消息、绑定客户端到分组、给分组里的客户端批量发送消息、获取在线的客户端、上下线自动通知。适用于长连接的大部分场景,分组可以理解为聊天室,绑定客户端到分组相当于把客户端添加到聊天室,给分组发送信息相当于给聊天室的每个人发送消息。 15 | 16 | 17 | 18 | ## 文档 19 | 20 | 1. [技术方案架构](docs/introduction.md) 21 | 2. [接口文档](docs/api.md) 22 | 23 | 24 | 25 | ## SDK 26 | 27 | 1. PHP版:https://github.com/woodylan/go-websocket-php-sdk 28 | 29 | 30 | 31 | 32 | ## 使用 33 | 34 | **下载本项目:** 35 | 36 | 这里已经打包好了,下载相应的环境,支持Linux、Windows、MacOS环境。 37 | 38 | 39 | 40 | **你也可以选择自己编译:** 41 | 42 | ```shell 43 | git clone https://github.com/woodylan/go-websocket.git 44 | ``` 45 | 46 | **编译:** 47 | 48 | ```shell 49 | // 编译适用于本机的版本 50 | go build 51 | 52 | // 编译Linux版本 53 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build 54 | 55 | // 编译Windows 64位版本 56 | CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build 57 | 58 | // 编译MacOS版本 59 | CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build 60 | ``` 61 | 62 | **执行:** 63 | 64 | 编译成功之后会得到一个二进制文件`go-websocket`,执行该二进制文件。 65 | 66 | ```shell 67 | ./go-websocket -c ./conf/app.ini 68 | ``` 69 | 70 | **连接测试:** 71 | 72 | 打开支持Websocket的客户端,输入 `ws://127.0.0.1:7800/ws?systemId=xxx` 进行连接,连接成功会返回`clientId`。 73 | 74 | 75 | 76 | ## docker体验 77 | 78 | ### 体验单机 79 | 80 | 1. 构建镜像 81 | 82 | ```shell 83 | docker build -t go-websocket . 84 | ``` 85 | 86 | 2. 基于镜像运行容器 87 | 88 | ```shell 89 | docker run -tid -p 7800:7800 go-websocket 90 | ``` 91 | 92 | ### 体验集群,同时运行ETCD集群 93 | 在当前目录下,直接运行` docker-compose up` 即可体验。 94 | 95 | 96 | 97 | ## 配置 98 | 99 | **配置文件:** 100 | 101 | 配置文件位于项目根目录的`conf/app.ini`。 102 | 103 | ```ini 104 | [common] 105 | HttpPort = 6000 106 | RPCPort = 7000 107 | # 是否集群,单机则设为false 108 | Cluster = true 109 | # 对称加密key 只允许16、24、32字节长度 110 | CryptoKey = Adba723b7fe06819 111 | 112 | [etcd] 113 | Endpoints = 127.0.0.1:2379, 127.0.0.2:2379, 127.0.0.3:2379 114 | ``` 115 | 116 | **运行项目:** 117 | 118 | 在不同的机器运行本项目,注意配置号端口号,项目如果在同一机器,则必须用不同的端口。你可以用`supervisor`做进程管理。 119 | 120 | **配置Nginx负载均衡:** 121 | 122 | ```nginx 123 | upstream ws_cluster { 124 | server 127.0.0.1:7800; 125 | #server 127.0.0.1:7801; 126 | } 127 | 128 | server { 129 | listen 7000; 130 | server_name ws.example.com; 131 | 132 | access_log /logs/access.log; 133 | error_log /logs/error.log; 134 | 135 | location /ws { 136 | proxy_pass http://ws_cluster; # 代理转发地址 137 | proxy_http_version 1.1; 138 | 139 | proxy_read_timeout 60s; # 超时设置 140 | 141 | # 启用支持websocket连接 142 | proxy_set_header Upgrade $http_upgrade; 143 | proxy_set_header Connection "upgrade"; 144 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 145 | } 146 | 147 | location /api { 148 | proxy_set_header X-Forwarded-For $remote_addr; 149 | proxy_set_header Host $http_host; 150 | 151 | proxy_pass http://ws_cluster; # 代理转发地址 152 | } 153 | } 154 | ``` 155 | 156 | 至此,项目部署完成。 157 | 158 | 159 | 160 | 161 | ## 实现的功能 162 | 163 | - [x] 分布式 164 | - [x] 账户授权模式 165 | - [x] 不同业务系统隔离 166 | - [x] 发送给指定客户端 167 | - [x] 发送给指定分组 168 | - [x] 上下线通知 169 | - [x] 群广播 170 | - [x] 错误日志 171 | - [x] 参数校验 172 | - [x] 关闭某个连接 173 | - [x] 支持docker 174 | - [ ] 查询某个客户端是否在线 175 | 176 | 177 | 178 | ## 沟通交流 179 | QQ群:1028314856 180 | 181 | 182 | Golang websocket分布式交流群 -------------------------------------------------------------------------------- /servers/clientmanager_test.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | . "github.com/smartystreets/goconvey/convey" 6 | "github.com/woodylan/go-websocket/pkg/setting" 7 | "github.com/woodylan/go-websocket/tools/util" 8 | "testing" 9 | ) 10 | 11 | func TestAddClient(t *testing.T) { 12 | clientId := "clientId" 13 | systemId := "publishSystem" 14 | var manager = NewClientManager() // 管理者 15 | conn := &websocket.Conn{} 16 | clientSocket := NewClient(clientId, systemId, conn) 17 | 18 | manager.AddClient(clientSocket) 19 | 20 | Convey("测试添加客户端", t, func() { 21 | Convey("长度是否够", func() { 22 | So(len(manager.ClientIdMap), ShouldEqual, 1) 23 | }) 24 | 25 | Convey("clientId是否存在", func() { 26 | _, ok := manager.ClientIdMap[clientId] 27 | So(ok, ShouldBeTrue) 28 | }) 29 | }) 30 | } 31 | 32 | func TestDelClient(t *testing.T) { 33 | clientId := "clientId" 34 | systemId := "publishSystem" 35 | var manager = NewClientManager() // 管理者 36 | conn := &websocket.Conn{} 37 | clientSocket := NewClient(clientId, systemId, conn) 38 | manager.AddClient(clientSocket) 39 | 40 | manager.DelClient(clientSocket) 41 | 42 | Convey("测试删除客户端", t, func() { 43 | Convey("长度是否够", func() { 44 | So(len(manager.ClientIdMap), ShouldEqual, 0) 45 | }) 46 | 47 | Convey("clientId是否存在", func() { 48 | _, ok := manager.ClientIdMap[clientId] 49 | So(ok, ShouldBeFalse) 50 | }) 51 | }) 52 | } 53 | 54 | func TestCount(t *testing.T) { 55 | clientId := "clientId" 56 | systemId := "publishSystem" 57 | var manager = NewClientManager() // 管理者 58 | conn := &websocket.Conn{} 59 | clientSocket := NewClient(clientId, systemId, conn) 60 | 61 | Convey("测试获取客户端数量", t, func() { 62 | Convey("添加一个客户端后", func() { 63 | manager.AddClient(clientSocket) 64 | So(manager.Count(), ShouldEqual, 1) 65 | }) 66 | 67 | Convey("删除一个客户端后", func() { 68 | manager.DelClient(clientSocket) 69 | So(manager.Count(), ShouldEqual, 0) 70 | }) 71 | 72 | Convey("再添加两个客户端后", func() { 73 | manager.AddClient(clientSocket) 74 | manager.AddClient(clientSocket) 75 | So(manager.Count(), ShouldEqual, 1) 76 | }) 77 | }) 78 | } 79 | 80 | func TestGetByClientId(t *testing.T) { 81 | clientId := "clientId" 82 | systemId := "publishSystem" 83 | var manager = NewClientManager() // 管理者 84 | conn := &websocket.Conn{} 85 | clientSocket := NewClient(clientId, systemId, conn) 86 | 87 | Convey("测试通过clientId获取客户端", t, func() { 88 | Convey("获取一个存在的clientId", func() { 89 | manager.AddClient(clientSocket) 90 | _, err := manager.GetByClientId(clientId) 91 | So(err, ShouldBeNil) 92 | }) 93 | 94 | Convey("获取一个不存在的clientId", func() { 95 | _, err := manager.GetByClientId("notExistId") 96 | So(err, ShouldNotBeNil) 97 | }) 98 | }) 99 | } 100 | 101 | func TestAddClient2LocalGroup(t *testing.T) { 102 | setting.Default() 103 | clientId := "clientId" 104 | systemId := "publishSystem" 105 | userId := "userId" 106 | var manager = NewClientManager() // 管理者 107 | conn := &websocket.Conn{} 108 | clientSocket := NewClient(clientId, systemId, conn) 109 | manager.AddClient(clientSocket) 110 | groupName := "testGroup" 111 | 112 | Convey("测试添加分组", t, func() { 113 | Convey("添加一个客户端到分组", func() { 114 | manager.AddClient2LocalGroup(groupName, clientSocket, userId, "") 115 | So(len(manager.Groups[util.GenGroupKey(systemId, groupName)]), ShouldEqual, 1) 116 | }) 117 | 118 | Convey("再添加一个客户端到分组", func() { 119 | manager.AddClient2LocalGroup(groupName, clientSocket, userId, "") 120 | So(len(manager.Groups[util.GenGroupKey(systemId, groupName)]), ShouldEqual, 1) 121 | }) 122 | }) 123 | } 124 | 125 | func TestGetGroupClientList(t *testing.T) { 126 | clientId := "clientId" 127 | systemId := "publishSystem" 128 | userId := "userId" 129 | var manager = NewClientManager() // 管理者 130 | conn := &websocket.Conn{} 131 | clientSocket := NewClient(clientId, systemId, conn) 132 | manager.AddClient(clientSocket) 133 | groupName := "testGroup" 134 | 135 | Convey("测试添加分组", t, func() { 136 | Convey("获取一个存在的分组", func() { 137 | manager.AddClient2LocalGroup(groupName, clientSocket, userId, "") 138 | clientIds := manager.GetGroupClientList(util.GenGroupKey(systemId, groupName)) 139 | So(len(clientIds), ShouldEqual, 1) 140 | }) 141 | 142 | Convey("获取一个不存在的clientId", func() { 143 | clientIds := manager.GetGroupClientList("notExistId") 144 | So(len(clientIds), ShouldEqual, 0) 145 | }) 146 | }) 147 | } 148 | -------------------------------------------------------------------------------- /servers/rpcclient.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "context" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/woodylan/go-websocket/pkg/setting" 7 | "github.com/woodylan/go-websocket/servers/pb" 8 | "google.golang.org/grpc" 9 | "sync" 10 | ) 11 | 12 | func grpcConn(addr string) *grpc.ClientConn { 13 | conn, err := grpc.Dial(addr, grpc.WithInsecure()) 14 | if err != nil { 15 | log.Errorf("did not connect: %v", err) 16 | } 17 | return conn 18 | } 19 | 20 | func SendRpc2Client(addr string, messageId, sendUserId, clientId string, code int, message string, data *string) { 21 | conn := grpcConn(addr) 22 | defer conn.Close() 23 | 24 | log.WithFields(log.Fields{ 25 | "host": setting.GlobalSetting.LocalHost, 26 | "port": setting.CommonSetting.HttpPort, 27 | "add": addr, 28 | "clientId": clientId, 29 | "msg": data, 30 | }).Info("发送到服务器") 31 | 32 | c := pb.NewCommonServiceClient(conn) 33 | _, err := c.Send2Client(context.Background(), &pb.Send2ClientReq{ 34 | MessageId: messageId, 35 | SendUserId: sendUserId, 36 | ClientId: clientId, 37 | Code: int32(code), 38 | Message: message, 39 | Data: *data, 40 | }) 41 | if err != nil { 42 | log.Errorf("failed to call: %v", err) 43 | } 44 | } 45 | 46 | func CloseRpcClient(addr string, clientId, systemId string) { 47 | conn := grpcConn(addr) 48 | defer conn.Close() 49 | 50 | log.WithFields(log.Fields{ 51 | "host": setting.GlobalSetting.LocalHost, 52 | "port": setting.CommonSetting.HttpPort, 53 | "add": addr, 54 | "clientId": clientId, 55 | }).Info("发送关闭连接到服务器") 56 | 57 | c := pb.NewCommonServiceClient(conn) 58 | _, err := c.CloseClient(context.Background(), &pb.CloseClientReq{ 59 | SystemId: systemId, 60 | ClientId: clientId, 61 | }) 62 | if err != nil { 63 | log.Errorf("failed to call: %v", err) 64 | } 65 | } 66 | 67 | //绑定分组 68 | func SendRpcBindGroup(addr string, systemId string, groupName string, clientId string, userId string, extend string) { 69 | conn := grpcConn(addr) 70 | defer conn.Close() 71 | 72 | c := pb.NewCommonServiceClient(conn) 73 | _, err := c.BindGroup(context.Background(), &pb.BindGroupReq{ 74 | SystemId: systemId, 75 | GroupName: groupName, 76 | ClientId: clientId, 77 | UserId: userId, 78 | Extend: extend, 79 | }) 80 | if err != nil { 81 | log.Errorf("failed to call: %v", err) 82 | } 83 | } 84 | 85 | //发送分组消息 86 | func SendGroupBroadcast(systemId string, messageId, sendUserId, groupName string, code int, message string, data *string) { 87 | setting.GlobalSetting.ServerListLock.Lock() 88 | defer setting.GlobalSetting.ServerListLock.Unlock() 89 | for _, addr := range setting.GlobalSetting.ServerList { 90 | conn := grpcConn(addr) 91 | defer conn.Close() 92 | 93 | c := pb.NewCommonServiceClient(conn) 94 | _, err := c.Send2Group(context.Background(), &pb.Send2GroupReq{ 95 | SystemId: systemId, 96 | MessageId: messageId, 97 | SendUserId: sendUserId, 98 | GroupName: groupName, 99 | Code: int32(code), 100 | Message: message, 101 | Data: *data, 102 | }) 103 | if err != nil { 104 | log.Errorf("failed to call: %v", err) 105 | } 106 | } 107 | } 108 | 109 | //发送系统信息 110 | func SendSystemBroadcast(systemId string, messageId, sendUserId string, code int, message string, data *string) { 111 | setting.GlobalSetting.ServerListLock.Lock() 112 | defer setting.GlobalSetting.ServerListLock.Unlock() 113 | for _, addr := range setting.GlobalSetting.ServerList { 114 | conn := grpcConn(addr) 115 | defer conn.Close() 116 | 117 | c := pb.NewCommonServiceClient(conn) 118 | _, err := c.Send2System(context.Background(), &pb.Send2SystemReq{ 119 | SystemId: systemId, 120 | MessageId: messageId, 121 | SendUserId: sendUserId, 122 | Code: int32(code), 123 | Message: message, 124 | Data: *data, 125 | }) 126 | if err != nil { 127 | log.Errorf("failed to call: %v", err) 128 | } 129 | } 130 | } 131 | 132 | func GetOnlineListBroadcast(systemId *string, groupName *string) (clientIdList []string) { 133 | setting.GlobalSetting.ServerListLock.Lock() 134 | defer setting.GlobalSetting.ServerListLock.Unlock() 135 | 136 | serverCount := len(setting.GlobalSetting.ServerList) 137 | 138 | onlineListChan := make(chan []string, serverCount) 139 | var wg sync.WaitGroup 140 | 141 | wg.Add(serverCount) 142 | for _, addr := range setting.GlobalSetting.ServerList { 143 | go func(addr string) { 144 | conn := grpcConn(addr) 145 | defer conn.Close() 146 | c := pb.NewCommonServiceClient(conn) 147 | response, err := c.GetGroupClients(context.Background(), &pb.GetGroupClientsReq{ 148 | SystemId: *systemId, 149 | GroupName: *groupName, 150 | }) 151 | if err != nil { 152 | log.Errorf("failed to call: %v", err) 153 | } else { 154 | onlineListChan <- response.List 155 | } 156 | wg.Done() 157 | 158 | }(addr) 159 | } 160 | 161 | wg.Wait() 162 | 163 | for i := 1; i <= serverCount; i++ { 164 | list, ok := <-onlineListChan 165 | if ok { 166 | clientIdList = append(clientIdList, list...) 167 | } else { 168 | return 169 | } 170 | } 171 | close(onlineListChan) 172 | 173 | return 174 | } 175 | -------------------------------------------------------------------------------- /docs/api.md: -------------------------------------------------------------------------------- 1 | ## 接口 2 | 3 | #### 连接接口 4 | 5 | **请求地址:**/ws?systemId=xxx 6 | 7 | **协议:** websocket 8 | 9 | **请求参数**:systemId 系统ID 10 | 11 | **响应示例:** 12 | 13 | ```json 14 | { 15 | "code": 0, 16 | "msg": "success", 17 | "data": { 18 | "clientId": "9fa54bdbbf2778cb" 19 | } 20 | } 21 | ``` 22 | 23 | #### 注册系统 24 | 25 | **请求地址:**/api/register 26 | 27 | **请求方式:** POST 28 | 29 | **Content-Type:** application/json; charset=UTF-8 30 | 31 | **请求头Body** 32 | 33 | | 字段 | 类型 | 是否必须 | 说明 | 34 | | -------- | ------ | -------- | -------- | 35 | | systemId | string | 是 | 系统ID | 36 | 37 | **响应示例:** 38 | 39 | ```json 40 | { 41 | "code": 0, 42 | "msg": "success", 43 | "data": [] 44 | } 45 | ``` 46 | 47 | #### 发送信息给指定客户端 48 | 49 | **请求地址:**/api/send_to_client 50 | 51 | **请求方式:** POST 52 | 53 | **Content-Type:** application/json; charset=UTF-8 54 | 55 | **请求头Header** 56 | 57 | | 字段 | 类型 | 是否必须 | 说明 | 58 | | -------- | ------ | -------- | -------- | 59 | | systemId | string | 是 | 系统ID | 60 | 61 | **请求头Body** 62 | 63 | | 字段 | 类型 | 是否必须 | 说明 | 64 | | -------- | ------ | -------- | -------- | 65 | | clientId | string | 是 | 客户端ID | 66 | | sendUserId | string | 是 | 发送者ID | 67 | | code | integer | 是 | 自定义的状态码 | 68 | | msg | string | 是 | 自定义的状态消息 | 69 | | data | sring、array、object | 是 | 消息内容 | 70 | 71 | **响应示例:** 72 | 73 | ```json 74 | { 75 | "code": 0, 76 | "msg": "success", 77 | "data": { 78 | "messageId": "5b4646dd8328f4b1" 79 | } 80 | } 81 | ``` 82 | 83 | #### 批量发送信息给指定客户端 84 | 85 | **请求地址:**/api/send_to_clients 86 | 87 | **请求方式:** POST 88 | 89 | **Content-Type:** application/json; charset=UTF-8 90 | 91 | **请求头Header** 92 | 93 | | 字段 | 类型 | 是否必须 | 说明 | 94 | | -------- | ------ | -------- | -------- | 95 | | systemId | string | 是 | 系统ID | 96 | 97 | **请求头Body** 98 | 99 | | 字段 | 类型 | 是否必须 | 说明 | 100 | | -------- | ------ | -------- | -------- | 101 | | clientIds | array | 是 | 客户端ID列表 | 102 | | sendUserId | string | 是 | 发送者ID | 103 | | code | integer | 是 | 自定义的状态码 | 104 | | msg | string | 是 | 自定义的状态消息 | 105 | | data | sring、array、object | 是 | 消息内容 | 106 | 107 | **响应示例:** 108 | 109 | ```json 110 | { 111 | "code": 0, 112 | "msg": "success", 113 | "data": { 114 | "messageId": "5b4646dd8328f4b1" 115 | } 116 | } 117 | ``` 118 | 119 | #### 绑定客户端到分组 120 | 121 | **请求地址:**/api/bind_to_group 122 | 123 | **请求方式:** POST 124 | 125 | **Content-Type:** application/json; charset=UTF-8 126 | 127 | **请求头Header** 128 | 129 | | 字段 | 类型 | 是否必须 | 说明 | 130 | | -------- | ------ | -------- | -------- | 131 | | systemId | string | 是 | 系统ID | 132 | 133 | **请求头Body** 134 | 135 | | 字段 | 类型 | 是否必须 | 说明 | 136 | | -------- | ------ | -------- | -------- | 137 | | sendUserId | string | 是 | 发送者ID | 138 | | clientId | string | 是 | 客户端ID | 139 | | groupName | string | 是 | 分组名 | 140 | 141 | **响应示例:** 142 | 143 | ```json 144 | { 145 | "code": 0, 146 | "msg": "success", 147 | "data": [] 148 | } 149 | ``` 150 | 151 | #### 发送信息给指定分组 152 | 153 | **请求地址:**/api/send_to_group 154 | 155 | **请求方式:** POST 156 | 157 | **Content-Type:** application/json; charset=UTF-8 158 | 159 | **请求头Header** 160 | 161 | | 字段 | 类型 | 是否必须 | 说明 | 162 | | -------- | ------ | -------- | -------- | 163 | | systemId | string | 是 | 系统ID | 164 | 165 | **请求头Body** 166 | 167 | | 字段 | 类型 | 是否必须 | 说明 | 168 | | -------- | ------ | -------- | -------- | 169 | | sendUserId | string | 是 | 发送者ID | 170 | | groupName | string | 是 | 分组名 | 171 | | code | integer | 是 | 自定义的状态码 | 172 | | msg | string | 是 | 自定义的状态消息 | 173 | | data | sring、array、object | 是 | 消息内容 | 174 | 175 | **响应示例:** 176 | 177 | ```json 178 | { 179 | "code": 0, 180 | "msg": "success", 181 | "data": { 182 | "messageId": "5b4646dd8328f4b1" 183 | } 184 | } 185 | ``` 186 | 187 | #### 获取在线的客户端列表 188 | 189 | **请求地址:**/api/get_online_list 190 | 191 | **请求方式:** POST 192 | 193 | **Content-Type:** application/json; charset=UTF-8 194 | 195 | **请求头Header** 196 | 197 | | 字段 | 类型 | 是否必须 | 说明 | 198 | | -------- | ------ | -------- | -------- | 199 | | systemId | string | 是 | 系统ID | 200 | 201 | **请求头Body** 202 | 203 | | 字段 | 类型 | 是否必须 | 说明 | 204 | | -------- | ------ | -------- | -------- | 205 | | groupName | string | 是 | 分组名 | 206 | | code | integer | 是 | 自定义的状态码 | 207 | | msg | string | 是 | 自定义的状态消息 | 208 | | data | sring、array、object | 是 | 消息内容 | 209 | 210 | **响应示例:** 211 | 212 | ```json 213 | { 214 | "code": 0, 215 | "msg": "success", 216 | "data": { 217 | "count": 2, 218 | "list": [ 219 | "WQReWw6m+wct+eKk/2rDiWcU4maU8JRTRZEX8c7Te6LzCa//VCXr/0KeVyO0sdNt", 220 | "j6YdsGFH4rfbYN/vS6UavJ5fVclWIB9W+Gqg9R/92cLJqgAp2ZPkvMbQiwQBJmDc" 221 | ] 222 | } 223 | } 224 | ``` 225 | 226 | #### 发送指定连接 227 | 228 | **请求地址:**/api/close_client 229 | 230 | **请求方式:** POST 231 | 232 | **Content-Type:** application/json; charset=UTF-8 233 | 234 | **请求头Header** 235 | 236 | | 字段 | 类型 | 是否必须 | 说明 | 237 | | -------- | ------ | -------- | -------- | 238 | | systemId | string | 是 | 系统ID | 239 | 240 | **请求头Body** 241 | 242 | | 字段 | 类型 | 是否必须 | 说明 | 243 | | -------- | ------ | -------- | -------- | 244 | | clientId | string | 是 | 客户端ID | 245 | 246 | **响应示例:** 247 | 248 | ```json 249 | { 250 | "code": 0, 251 | "msg": "success", 252 | "data": {} 253 | } 254 | ``` -------------------------------------------------------------------------------- /servers/server.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "github.com/gorilla/websocket" 5 | log "github.com/sirupsen/logrus" 6 | "github.com/woodylan/go-websocket/pkg/setting" 7 | "github.com/woodylan/go-websocket/tools/util" 8 | "net/http" 9 | "time" 10 | ) 11 | 12 | //channel通道 13 | var ToClientChan chan clientInfo 14 | 15 | //channel通道结构体 16 | type clientInfo struct { 17 | ClientId string 18 | SendUserId string 19 | MessageId string 20 | Code int 21 | Msg string 22 | Data *string 23 | } 24 | 25 | type RetData struct { 26 | MessageId string `json:"messageId"` 27 | SendUserId string `json:"sendUserId"` 28 | Code int `json:"code"` 29 | Msg string `json:"msg"` 30 | Data interface{} `json:"data"` 31 | } 32 | 33 | // 心跳间隔 34 | var heartbeatInterval = 25 * time.Second 35 | 36 | func init() { 37 | ToClientChan = make(chan clientInfo, 1000) 38 | } 39 | 40 | var Manager = NewClientManager() // 管理者 41 | 42 | func StartWebSocket() { 43 | websocketHandler := &Controller{} 44 | http.HandleFunc("/ws", websocketHandler.Run) 45 | 46 | go Manager.Start() 47 | } 48 | 49 | //发送信息到指定客户端 50 | func SendMessage2Client(clientId string, sendUserId string, code int, msg string, data *string) (messageId string) { 51 | messageId = util.GenUUID() 52 | if util.IsCluster() { 53 | addr, _, _, isLocal, err := util.GetAddrInfoAndIsLocal(clientId) 54 | if err != nil { 55 | log.Errorf("%s", err) 56 | return 57 | } 58 | 59 | //如果是本机则发送到本机 60 | if isLocal { 61 | SendMessage2LocalClient(messageId, clientId, sendUserId, code, msg, data) 62 | } else { 63 | //发送到指定机器 64 | SendRpc2Client(addr, messageId, sendUserId, clientId, code, msg, data) 65 | } 66 | } else { 67 | //如果是单机服务,则只发送到本机 68 | SendMessage2LocalClient(messageId, clientId, sendUserId, code, msg, data) 69 | } 70 | 71 | return 72 | } 73 | 74 | //关闭客户端 75 | func CloseClient(clientId, systemId string) { 76 | if util.IsCluster() { 77 | addr, _, _, isLocal, err := util.GetAddrInfoAndIsLocal(clientId) 78 | if err != nil { 79 | log.Errorf("%s", err) 80 | return 81 | } 82 | 83 | //如果是本机则发送到本机 84 | if isLocal { 85 | CloseLocalClient(clientId, systemId) 86 | } else { 87 | //发送到指定机器 88 | CloseRpcClient(addr, clientId, systemId) 89 | } 90 | } else { 91 | //如果是单机服务,则只发送到本机 92 | CloseLocalClient(clientId, systemId) 93 | } 94 | 95 | return 96 | } 97 | 98 | //添加客户端到分组 99 | func AddClient2Group(systemId string, groupName string, clientId string, userId string, extend string) { 100 | //如果是集群则用redis共享数据 101 | if util.IsCluster() { 102 | //判断key是否存在 103 | addr, _, _, isLocal, err := util.GetAddrInfoAndIsLocal(clientId) 104 | if err != nil { 105 | log.Errorf("%s", err) 106 | return 107 | } 108 | 109 | if isLocal { 110 | if client, err := Manager.GetByClientId(clientId); err == nil { 111 | //添加到本地 112 | Manager.AddClient2LocalGroup(groupName, client, userId, extend) 113 | } else { 114 | log.Error(err) 115 | } 116 | } else { 117 | //发送到指定的机器 118 | SendRpcBindGroup(addr, systemId, groupName, clientId, userId, extend) 119 | } 120 | } else { 121 | if client, err := Manager.GetByClientId(clientId); err == nil { 122 | //如果是单机,就直接添加到本地group了 123 | Manager.AddClient2LocalGroup(groupName, client, userId, extend) 124 | } 125 | } 126 | } 127 | 128 | //发送信息到指定分组 129 | func SendMessage2Group(systemId, sendUserId, groupName string, code int, msg string, data *string) (messageId string) { 130 | messageId = util.GenUUID() 131 | if util.IsCluster() { 132 | //发送分组消息给指定广播 133 | go SendGroupBroadcast(systemId, messageId, sendUserId, groupName, code, msg, data) 134 | } else { 135 | //如果是单机服务,则只发送到本机 136 | Manager.SendMessage2LocalGroup(systemId, messageId, sendUserId, groupName, code, msg, data) 137 | } 138 | return 139 | } 140 | 141 | //发送信息到指定系统 142 | func SendMessage2System(systemId, sendUserId string, code int, msg string, data string) { 143 | messageId := util.GenUUID() 144 | if util.IsCluster() { 145 | //发送到系统广播 146 | SendSystemBroadcast(systemId, messageId, sendUserId, code, msg, &data) 147 | } else { 148 | //如果是单机服务,则只发送到本机 149 | Manager.SendMessage2LocalSystem(systemId, messageId, sendUserId, code, msg, &data) 150 | } 151 | } 152 | 153 | //获取分组列表 154 | func GetOnlineList(systemId *string, groupName *string) map[string]interface{} { 155 | var clientList []string 156 | if util.IsCluster() { 157 | //发送到系统广播 158 | clientList = GetOnlineListBroadcast(systemId, groupName) 159 | } else { 160 | //如果是单机服务,则只发送到本机 161 | retList := Manager.GetGroupClientList(util.GenGroupKey(*systemId, *groupName)) 162 | clientList = append(clientList, retList...) 163 | } 164 | 165 | return map[string]interface{}{ 166 | "count": len(clientList), 167 | "list": clientList, 168 | } 169 | } 170 | 171 | //通过本服务器发送信息 172 | func SendMessage2LocalClient(messageId, clientId string, sendUserId string, code int, msg string, data *string) { 173 | log.WithFields(log.Fields{ 174 | "host": setting.GlobalSetting.LocalHost, 175 | "port": setting.CommonSetting.HttpPort, 176 | "clientId": clientId, 177 | }).Info("发送到通道") 178 | ToClientChan <- clientInfo{ClientId: clientId, MessageId: messageId, SendUserId: sendUserId, Code: code, Msg: msg, Data: data} 179 | return 180 | } 181 | 182 | //发送关闭信号 183 | func CloseLocalClient(clientId, systemId string) { 184 | if conn, err := Manager.GetByClientId(clientId); err == nil && conn != nil { 185 | if conn.SystemId != systemId { 186 | return 187 | } 188 | Manager.DisConnect <- conn 189 | log.WithFields(log.Fields{ 190 | "host": setting.GlobalSetting.LocalHost, 191 | "port": setting.CommonSetting.HttpPort, 192 | "clientId": clientId, 193 | }).Info("主动踢掉客户端") 194 | } 195 | return 196 | } 197 | 198 | //监听并发送给客户端信息 199 | func WriteMessage() { 200 | for { 201 | clientInfo := <-ToClientChan 202 | log.WithFields(log.Fields{ 203 | "host": setting.GlobalSetting.LocalHost, 204 | "port": setting.CommonSetting.HttpPort, 205 | "clientId": clientInfo.ClientId, 206 | "messageId": clientInfo.MessageId, 207 | "sendUserId": clientInfo.SendUserId, 208 | "code": clientInfo.Code, 209 | "msg": clientInfo.Msg, 210 | "data": clientInfo.Data, 211 | }).Info("发送到本机") 212 | if conn, err := Manager.GetByClientId(clientInfo.ClientId); err == nil && conn != nil { 213 | if err := Render(conn.Socket, clientInfo.MessageId, clientInfo.SendUserId, clientInfo.Code, clientInfo.Msg, clientInfo.Data); err != nil { 214 | Manager.DisConnect <- conn 215 | log.WithFields(log.Fields{ 216 | "host": setting.GlobalSetting.LocalHost, 217 | "port": setting.CommonSetting.HttpPort, 218 | "clientId": clientInfo.ClientId, 219 | "msg": clientInfo.Msg, 220 | }).Error("客户端异常离线:" + err.Error()) 221 | } 222 | } 223 | } 224 | } 225 | 226 | func Render(conn *websocket.Conn, messageId string, sendUserId string, code int, message string, data interface{}) error { 227 | return conn.WriteJSON(RetData{ 228 | Code: code, 229 | MessageId: messageId, 230 | SendUserId: sendUserId, 231 | Msg: message, 232 | Data: data, 233 | }) 234 | } 235 | 236 | //启动定时器进行心跳检测 237 | func PingTimer() { 238 | go func() { 239 | ticker := time.NewTicker(heartbeatInterval) 240 | defer ticker.Stop() 241 | for { 242 | <-ticker.C 243 | //发送心跳 244 | for clientId, conn := range Manager.AllClient() { 245 | if err := conn.Socket.WriteControl(websocket.PingMessage, []byte{}, time.Now().Add(time.Second)); err != nil { 246 | Manager.DisConnect <- conn 247 | log.Errorf("发送心跳失败: %s 总连接数:%d", clientId, Manager.Count()) 248 | } 249 | } 250 | } 251 | 252 | }() 253 | } 254 | -------------------------------------------------------------------------------- /servers/clientmanager.go: -------------------------------------------------------------------------------- 1 | package servers 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | log "github.com/sirupsen/logrus" 7 | "github.com/woodylan/go-websocket/define/retcode" 8 | "github.com/woodylan/go-websocket/pkg/setting" 9 | "github.com/woodylan/go-websocket/tools/util" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // 连接管理 15 | type ClientManager struct { 16 | ClientIdMap map[string]*Client // 全部的连接 17 | ClientIdMapLock sync.RWMutex // 读写锁 18 | 19 | Connect chan *Client // 连接处理 20 | DisConnect chan *Client // 断开连接处理 21 | 22 | GroupLock sync.RWMutex 23 | Groups map[string][]string 24 | 25 | SystemClientsLock sync.RWMutex 26 | SystemClients map[string][]string 27 | } 28 | 29 | func NewClientManager() (clientManager *ClientManager) { 30 | clientManager = &ClientManager{ 31 | ClientIdMap: make(map[string]*Client), 32 | Connect: make(chan *Client, 10000), 33 | DisConnect: make(chan *Client, 10000), 34 | Groups: make(map[string][]string, 100), 35 | SystemClients: make(map[string][]string, 100), 36 | } 37 | 38 | return 39 | } 40 | 41 | // 管道处理程序 42 | func (manager *ClientManager) Start() { 43 | for { 44 | select { 45 | case client := <-manager.Connect: 46 | // 建立连接事件 47 | manager.EventConnect(client) 48 | case conn := <-manager.DisConnect: 49 | // 断开连接事件 50 | manager.EventDisconnect(conn) 51 | } 52 | } 53 | } 54 | 55 | // 建立连接事件 56 | func (manager *ClientManager) EventConnect(client *Client) { 57 | manager.AddClient(client) 58 | 59 | log.WithFields(log.Fields{ 60 | "host": setting.GlobalSetting.LocalHost, 61 | "port": setting.CommonSetting.HttpPort, 62 | "clientId": client.ClientId, 63 | "counts": Manager.Count(), 64 | }).Info("客户端已连接") 65 | } 66 | 67 | // 断开连接时间 68 | func (manager *ClientManager) EventDisconnect(client *Client) { 69 | //关闭连接 70 | _ = client.Socket.Close() 71 | manager.DelClient(client) 72 | 73 | mJson, _ := json.Marshal(map[string]string{ 74 | "clientId": client.ClientId, 75 | "userId": client.UserId, 76 | "extend": client.Extend, 77 | }) 78 | data := string(mJson) 79 | sendUserId := "" 80 | 81 | //发送下线通知 82 | if len(client.GroupList) > 0 { 83 | for _, groupName := range client.GroupList { 84 | SendMessage2Group(client.SystemId, sendUserId, groupName, retcode.OFFLINE_MESSAGE_CODE, "客户端下线", &data) 85 | } 86 | } 87 | 88 | log.WithFields(log.Fields{ 89 | "host": setting.GlobalSetting.LocalHost, 90 | "port": setting.CommonSetting.HttpPort, 91 | "clientId": client.ClientId, 92 | "counts": Manager.Count(), 93 | "seconds": uint64(time.Now().Unix()) - client.ConnectTime, 94 | }).Info("客户端已断开") 95 | 96 | //标记销毁 97 | client.IsDeleted = true 98 | client = nil 99 | } 100 | 101 | // 添加客户端 102 | func (manager *ClientManager) AddClient(client *Client) { 103 | manager.ClientIdMapLock.Lock() 104 | defer manager.ClientIdMapLock.Unlock() 105 | 106 | manager.ClientIdMap[client.ClientId] = client 107 | } 108 | 109 | // 获取所有的客户端 110 | func (manager *ClientManager) AllClient() map[string]*Client { 111 | manager.ClientIdMapLock.RLock() 112 | defer manager.ClientIdMapLock.RUnlock() 113 | 114 | return manager.ClientIdMap 115 | } 116 | 117 | // 客户端数量 118 | func (manager *ClientManager) Count() int { 119 | manager.ClientIdMapLock.RLock() 120 | defer manager.ClientIdMapLock.RUnlock() 121 | return len(manager.ClientIdMap) 122 | } 123 | 124 | // 删除客户端 125 | func (manager *ClientManager) DelClient(client *Client) { 126 | manager.delClientIdMap(client.ClientId) 127 | 128 | //删除所在的分组 129 | if len(client.GroupList) > 0 { 130 | for _, groupName := range client.GroupList { 131 | manager.delGroupClient(util.GenGroupKey(client.SystemId, groupName), client.ClientId) 132 | } 133 | } 134 | 135 | // 删除系统里的客户端 136 | manager.delSystemClient(client) 137 | } 138 | 139 | // 删除clientIdMap 140 | func (manager *ClientManager) delClientIdMap(clientId string) { 141 | manager.ClientIdMapLock.Lock() 142 | defer manager.ClientIdMapLock.Unlock() 143 | 144 | delete(manager.ClientIdMap, clientId) 145 | } 146 | 147 | // 通过clientId获取 148 | func (manager *ClientManager) GetByClientId(clientId string) (*Client, error) { 149 | manager.ClientIdMapLock.RLock() 150 | defer manager.ClientIdMapLock.RUnlock() 151 | 152 | if client, ok := manager.ClientIdMap[clientId]; !ok { 153 | return nil, errors.New("客户端不存在") 154 | } else { 155 | return client, nil 156 | } 157 | } 158 | 159 | // 发送到本机分组 160 | func (manager *ClientManager) SendMessage2LocalGroup(systemId, messageId, sendUserId, groupName string, code int, msg string, data *string) { 161 | if len(groupName) > 0 { 162 | clientIds := manager.GetGroupClientList(util.GenGroupKey(systemId, groupName)) 163 | if len(clientIds) > 0 { 164 | for _, clientId := range clientIds { 165 | if _, err := Manager.GetByClientId(clientId); err == nil { 166 | //添加到本地 167 | SendMessage2LocalClient(messageId, clientId, sendUserId, code, msg, data) 168 | } else { 169 | //删除分组 170 | manager.delGroupClient(util.GenGroupKey(systemId, groupName), clientId) 171 | } 172 | } 173 | } 174 | } 175 | } 176 | 177 | //发送给指定业务系统 178 | func (manager *ClientManager) SendMessage2LocalSystem(systemId, messageId string, sendUserId string, code int, msg string, data *string) { 179 | if len(systemId) > 0 { 180 | clientIds := Manager.GetSystemClientList(systemId) 181 | if len(clientIds) > 0 { 182 | for _, clientId := range clientIds { 183 | SendMessage2LocalClient(messageId, clientId, sendUserId, code, msg, data) 184 | } 185 | } 186 | } 187 | } 188 | 189 | // 添加到本地分组 190 | func (manager *ClientManager) AddClient2LocalGroup(groupName string, client *Client, userId string, extend string) { 191 | //标记当前客户端的userId 192 | client.UserId = userId 193 | client.Extend = extend 194 | 195 | //判断之前是否有添加过 196 | for _, groupValue := range client.GroupList { 197 | if groupValue == groupName { 198 | return 199 | } 200 | } 201 | 202 | // 为属性添加分组信息 203 | groupKey := util.GenGroupKey(client.SystemId, groupName) 204 | 205 | manager.addClient2Group(groupKey, client) 206 | 207 | client.GroupList = append(client.GroupList, groupName) 208 | 209 | mJson, _ := json.Marshal(map[string]string{ 210 | "clientId": client.ClientId, 211 | "userId": client.UserId, 212 | "extend": client.Extend, 213 | }) 214 | data := string(mJson) 215 | sendUserId := "" 216 | 217 | //发送系统通知 218 | SendMessage2Group(client.SystemId, sendUserId, groupName, retcode.ONLINE_MESSAGE_CODE, "客户端上线", &data) 219 | } 220 | 221 | // 添加到本地分组 222 | func (manager *ClientManager) addClient2Group(groupKey string, client *Client) { 223 | manager.GroupLock.Lock() 224 | defer manager.GroupLock.Unlock() 225 | manager.Groups[groupKey] = append(manager.Groups[groupKey], client.ClientId) 226 | } 227 | 228 | // 删除分组里的客户端 229 | func (manager *ClientManager) delGroupClient(groupKey string, clientId string) { 230 | manager.GroupLock.Lock() 231 | defer manager.GroupLock.Unlock() 232 | 233 | for index, groupClientId := range manager.Groups[groupKey] { 234 | if groupClientId == clientId { 235 | manager.Groups[groupKey] = append(manager.Groups[groupKey][:index], manager.Groups[groupKey][index+1:]...) 236 | } 237 | } 238 | } 239 | 240 | // 获取本地分组的成员 241 | func (manager *ClientManager) GetGroupClientList(groupKey string) []string { 242 | manager.GroupLock.RLock() 243 | defer manager.GroupLock.RUnlock() 244 | return manager.Groups[groupKey] 245 | } 246 | 247 | // 添加到系统客户端列表 248 | func (manager *ClientManager) AddClient2SystemClient(systemId string, client *Client) { 249 | manager.SystemClientsLock.Lock() 250 | defer manager.SystemClientsLock.Unlock() 251 | manager.SystemClients[systemId] = append(manager.SystemClients[systemId], client.ClientId) 252 | } 253 | 254 | // 删除系统里的客户端 255 | func (manager *ClientManager) delSystemClient(client *Client) { 256 | manager.SystemClientsLock.Lock() 257 | defer manager.SystemClientsLock.Unlock() 258 | 259 | for index, clientId := range manager.SystemClients[client.SystemId] { 260 | if clientId == client.ClientId { 261 | manager.SystemClients[client.SystemId] = append(manager.SystemClients[client.SystemId][:index], manager.SystemClients[client.SystemId][index+1:]...) 262 | } 263 | } 264 | } 265 | 266 | // 获取指定系统的客户端列表 267 | func (manager *ClientManager) GetSystemClientList(systemId string) []string { 268 | manager.SystemClientsLock.RLock() 269 | defer manager.SystemClientsLock.RUnlock() 270 | return manager.SystemClients[systemId] 271 | } 272 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 3 | github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= 4 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 7 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 8 | github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 9 | github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= 10 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 11 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 12 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 13 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 14 | github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 15 | github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= 16 | github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 18 | github.com/coreos/bbolt v1.3.3 h1:n6AiVyVRKQFNb6mJlwESEvvLoDyiTzXX7ORAUlkeBdY= 19 | github.com/coreos/bbolt v1.3.3/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 20 | github.com/coreos/etcd v3.3.17+incompatible h1:f/Z3EoDSx1yjaIjLQGo1diYUlQYSBrrAQ5vP8NjwXwo= 21 | github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 22 | github.com/coreos/go-semver v0.2.0 h1:3Jm3tLmsgAYcjC+4Up7hJrFBPr+n7rAqYeSw/SZazuY= 23 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 24 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= 25 | github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 26 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f h1:lBNOc5arjvs8E5mO2tbpBpLoyyu8B6e44T7hJy6potg= 27 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 28 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 29 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 30 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 31 | github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= 32 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 33 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 34 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 35 | github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239 h1:Ghm4eQYC0nEPnSJdVkTrXpu9KtoVCSo1hg7mtI7G9KU= 36 | github.com/fastly/go-utils v0.0.0-20180712184237-d95a45783239/go.mod h1:Gdwt2ce0yfBxPvZrHkprdPPTTS3N5rwmLE8T22KBXlw= 37 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 38 | github.com/go-ini/ini v1.61.0 h1:+IytwU4FcXqB+i5Vqiu/Ybf/Jdin9Pwzdxs5lmuT10o= 39 | github.com/go-ini/ini v1.61.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= 40 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 41 | github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 42 | github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= 43 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 44 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 45 | github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= 46 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 47 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 48 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 49 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 50 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 51 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 52 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 53 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 54 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= 55 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 56 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= 57 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 58 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 59 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 60 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 61 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 62 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 63 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 64 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 65 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 66 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 67 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 68 | github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= 69 | github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 70 | github.com/google/btree v1.0.0 h1:0udJVsspx3VBr5FwtLhQQtuAsVc79tTq0ocGIPAU6qo= 71 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 72 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 73 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 74 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 75 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 76 | github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 77 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 78 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 79 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 80 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 81 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 82 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 83 | github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 84 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= 85 | github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 86 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 87 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 88 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= 89 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 90 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= 91 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 92 | github.com/grpc-ecosystem/grpc-gateway v1.9.5 h1:UImYN5qQ8tuGpGE16ZmjvcTtTw24zw1QAp/SlnNrZhI= 93 | github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 94 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 h1:IPJ3dvxmJ4uczJe5YQdrYB16oTJlGSC/OyZDqUk9xX4= 95 | github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869/go.mod h1:cJ6Cj7dQo+O6GJNiMx+Pa94qKj+TG8ONdKHgMNIyyag= 96 | github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= 97 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 98 | github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= 99 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 100 | github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 101 | github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= 102 | github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 103 | github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 104 | github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 105 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 106 | github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= 107 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 108 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 109 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 110 | github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= 111 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 112 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 113 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 114 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 115 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 116 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 117 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 118 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 119 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 120 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= 121 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= 122 | github.com/lestrrat-go/file-rotatelogs v2.3.0+incompatible h1:4mNlp+/SvALIPFpbXV3kxNJJno9iKFWGxSDE13Kl66Q= 123 | github.com/lestrrat-go/file-rotatelogs v2.3.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= 124 | github.com/lestrrat-go/strftime v1.0.1 h1:o7qz5pmLzPDLyGW4lG6JvTKPUfTFXwe+vOamIYWtnVU= 125 | github.com/lestrrat-go/strftime v1.0.1/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= 126 | github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= 127 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 128 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 129 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 130 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 131 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 132 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 133 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 134 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 135 | github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 136 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 137 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 138 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 139 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 140 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 141 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 142 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 143 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 144 | github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= 145 | github.com/prometheus/client_golang v1.11.1 h1:+4eQaD7vAZ6DsfsxB15hbE0odUjGI5ARs9yskGu1v4s= 146 | github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= 147 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 148 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 149 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 150 | github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= 151 | github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 152 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 153 | github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= 154 | github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= 155 | github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= 156 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 157 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 158 | github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= 159 | github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= 160 | github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= 161 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo= 162 | github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM= 163 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 164 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 165 | github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= 166 | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= 167 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 168 | github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= 169 | github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= 170 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= 171 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM= 172 | github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= 173 | github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s= 174 | github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= 175 | github.com/soheilhy/cmux v0.1.4 h1:0HKaf1o97UwFjHH9o5XsHUOF+tqmdA7KEzXLpiyaw0E= 176 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 177 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 178 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 179 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 180 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 181 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 182 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 183 | github.com/tebeka/strftime v0.1.3 h1:5HQXOqWKYRFfNyBMNVc9z5+QzuBtIXy03psIhtdJYto= 184 | github.com/tebeka/strftime v0.1.3/go.mod h1:7wJm3dZlpr4l/oVK0t1HYIc4rMzQ2XJlOMIUJUJH6XQ= 185 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ= 186 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 187 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= 188 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 189 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 190 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 191 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 192 | go.etcd.io/bbolt v1.3.1-etcd.8 h1:6J7QAKqfFBGnU80KRnuQxfjjeE5xAGE/qB810I3FQHQ= 193 | go.etcd.io/bbolt v1.3.1-etcd.8/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 194 | go.uber.org/atomic v1.5.0 h1:OI5t8sDa1Or+q8AeE+yKeB/SDYioSHAgcVljj9JIETY= 195 | go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 196 | go.uber.org/multierr v1.3.0 h1:sFPn2GLc3poCkfrpIXGhBD2X0CMIo4Q/zSULXrj/+uc= 197 | go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= 198 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee h1:0mgffUl7nfd+FpvXMVz4IDEaUSmT1ysygQC7qYo7sG4= 199 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 200 | go.uber.org/zap v1.12.0 h1:dySoUQPFBGj6xwjmBzageVL8jGi8uxc6bEmJQjA06bw= 201 | go.uber.org/zap v1.12.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= 202 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 203 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 204 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 205 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 206 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 207 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519 h1:7I4JAnoQBe7ZtJcBaYHi5UtiO8tQHbUSXxL+pnGRANg= 208 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 209 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 210 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 211 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 212 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 213 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 214 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 215 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 216 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 217 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 218 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= 219 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 220 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 221 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 222 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 223 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 224 | golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 225 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 226 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 227 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 228 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 229 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 230 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 231 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 232 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 233 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 234 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 235 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 236 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 237 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 238 | golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= 239 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 240 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 241 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 242 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 243 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 244 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 245 | golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 246 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 247 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 248 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 249 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 250 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 251 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 252 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 253 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 255 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 257 | golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 258 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 259 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 260 | golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 261 | golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 262 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 263 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 264 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 265 | golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= 266 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 267 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 268 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 269 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 270 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 271 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 272 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 273 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 274 | golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= 275 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 276 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= 277 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 278 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 279 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 280 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 281 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 282 | golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 283 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 284 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 285 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 286 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 287 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 288 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 289 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 290 | golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU= 291 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 292 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 293 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 294 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 295 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 296 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 297 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 298 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 299 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 300 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 301 | google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6 h1:UXl+Zk3jqqcbEVV7ace5lrt4YdA4tXiz3f/KbmD29Vo= 302 | google.golang.org/genproto v0.0.0-20191028173616-919d9bdd9fe6/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= 303 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 304 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 305 | google.golang.org/grpc v1.26.0 h1:2dTRdpdFEEhJYQD8EMLB61nnrzSCTbG38PhqdhvOltg= 306 | google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 307 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 308 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 309 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 310 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 311 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 312 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 313 | google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= 314 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 315 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 316 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 317 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 318 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 319 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 320 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 321 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 322 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 323 | gopkg.in/go-playground/validator.v9 v9.31.0 h1:bmXmP2RSNtFES+bn4uYuHT7iJFJv7Vj+an+ZQdDaD1M= 324 | gopkg.in/go-playground/validator.v9 v9.31.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 325 | gopkg.in/ini.v1 v1.61.0 h1:LBCdW4FmFYL4s/vDZD1RQYX7oAR6IjujCYgMdbHBR10= 326 | gopkg.in/ini.v1 v1.61.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 327 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 328 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 329 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 330 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 331 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 332 | gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 333 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 334 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 335 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 336 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 337 | honnef.co/go/tools v0.0.1-2019.2.3 h1:3JgtbtFHMiCmsznwGVTUWbgGov+pVqnlf1dEJTNAXeM= 338 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 339 | sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= 340 | sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= 341 | -------------------------------------------------------------------------------- /servers/pb/grpc.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: servers/grpc.proto 3 | 4 | package pb 5 | 6 | import ( 7 | context "context" 8 | fmt "fmt" 9 | proto "github.com/golang/protobuf/proto" 10 | grpc "google.golang.org/grpc" 11 | math "math" 12 | ) 13 | 14 | // Reference imports to suppress errors if they are not otherwise used. 15 | var _ = proto.Marshal 16 | var _ = fmt.Errorf 17 | var _ = math.Inf 18 | 19 | // This is a compile-time assertion to ensure that this generated file 20 | // is compatible with the proto package it is being compiled against. 21 | // A compilation error at this line likely means your copy of the 22 | // proto package needs to be updated. 23 | const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package 24 | 25 | type Send2ClientReq struct { 26 | SystemId string `protobuf:"bytes,1,opt,name=systemId,proto3" json:"systemId,omitempty"` 27 | MessageId string `protobuf:"bytes,2,opt,name=messageId,proto3" json:"messageId,omitempty"` 28 | SendUserId string `protobuf:"bytes,3,opt,name=sendUserId,proto3" json:"sendUserId,omitempty"` 29 | ClientId string `protobuf:"bytes,4,opt,name=clientId,proto3" json:"clientId,omitempty"` 30 | Code int32 `protobuf:"varint,5,opt,name=code,proto3" json:"code,omitempty"` 31 | Message string `protobuf:"bytes,6,opt,name=message,proto3" json:"message,omitempty"` 32 | Data string `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"` 33 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 34 | XXX_unrecognized []byte `json:"-"` 35 | XXX_sizecache int32 `json:"-"` 36 | } 37 | 38 | func (m *Send2ClientReq) Reset() { *m = Send2ClientReq{} } 39 | func (m *Send2ClientReq) String() string { return proto.CompactTextString(m) } 40 | func (*Send2ClientReq) ProtoMessage() {} 41 | func (*Send2ClientReq) Descriptor() ([]byte, []int) { 42 | return fileDescriptor_30edbedfc0173ed4, []int{0} 43 | } 44 | 45 | func (m *Send2ClientReq) XXX_Unmarshal(b []byte) error { 46 | return xxx_messageInfo_Send2ClientReq.Unmarshal(m, b) 47 | } 48 | func (m *Send2ClientReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 49 | return xxx_messageInfo_Send2ClientReq.Marshal(b, m, deterministic) 50 | } 51 | func (m *Send2ClientReq) XXX_Merge(src proto.Message) { 52 | xxx_messageInfo_Send2ClientReq.Merge(m, src) 53 | } 54 | func (m *Send2ClientReq) XXX_Size() int { 55 | return xxx_messageInfo_Send2ClientReq.Size(m) 56 | } 57 | func (m *Send2ClientReq) XXX_DiscardUnknown() { 58 | xxx_messageInfo_Send2ClientReq.DiscardUnknown(m) 59 | } 60 | 61 | var xxx_messageInfo_Send2ClientReq proto.InternalMessageInfo 62 | 63 | func (m *Send2ClientReq) GetSystemId() string { 64 | if m != nil { 65 | return m.SystemId 66 | } 67 | return "" 68 | } 69 | 70 | func (m *Send2ClientReq) GetMessageId() string { 71 | if m != nil { 72 | return m.MessageId 73 | } 74 | return "" 75 | } 76 | 77 | func (m *Send2ClientReq) GetSendUserId() string { 78 | if m != nil { 79 | return m.SendUserId 80 | } 81 | return "" 82 | } 83 | 84 | func (m *Send2ClientReq) GetClientId() string { 85 | if m != nil { 86 | return m.ClientId 87 | } 88 | return "" 89 | } 90 | 91 | func (m *Send2ClientReq) GetCode() int32 { 92 | if m != nil { 93 | return m.Code 94 | } 95 | return 0 96 | } 97 | 98 | func (m *Send2ClientReq) GetMessage() string { 99 | if m != nil { 100 | return m.Message 101 | } 102 | return "" 103 | } 104 | 105 | func (m *Send2ClientReq) GetData() string { 106 | if m != nil { 107 | return m.Data 108 | } 109 | return "" 110 | } 111 | 112 | type CloseClientReq struct { 113 | SystemId string `protobuf:"bytes,1,opt,name=systemId,proto3" json:"systemId,omitempty"` 114 | ClientId string `protobuf:"bytes,2,opt,name=clientId,proto3" json:"clientId,omitempty"` 115 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 116 | XXX_unrecognized []byte `json:"-"` 117 | XXX_sizecache int32 `json:"-"` 118 | } 119 | 120 | func (m *CloseClientReq) Reset() { *m = CloseClientReq{} } 121 | func (m *CloseClientReq) String() string { return proto.CompactTextString(m) } 122 | func (*CloseClientReq) ProtoMessage() {} 123 | func (*CloseClientReq) Descriptor() ([]byte, []int) { 124 | return fileDescriptor_30edbedfc0173ed4, []int{1} 125 | } 126 | 127 | func (m *CloseClientReq) XXX_Unmarshal(b []byte) error { 128 | return xxx_messageInfo_CloseClientReq.Unmarshal(m, b) 129 | } 130 | func (m *CloseClientReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 131 | return xxx_messageInfo_CloseClientReq.Marshal(b, m, deterministic) 132 | } 133 | func (m *CloseClientReq) XXX_Merge(src proto.Message) { 134 | xxx_messageInfo_CloseClientReq.Merge(m, src) 135 | } 136 | func (m *CloseClientReq) XXX_Size() int { 137 | return xxx_messageInfo_CloseClientReq.Size(m) 138 | } 139 | func (m *CloseClientReq) XXX_DiscardUnknown() { 140 | xxx_messageInfo_CloseClientReq.DiscardUnknown(m) 141 | } 142 | 143 | var xxx_messageInfo_CloseClientReq proto.InternalMessageInfo 144 | 145 | func (m *CloseClientReq) GetSystemId() string { 146 | if m != nil { 147 | return m.SystemId 148 | } 149 | return "" 150 | } 151 | 152 | func (m *CloseClientReq) GetClientId() string { 153 | if m != nil { 154 | return m.ClientId 155 | } 156 | return "" 157 | } 158 | 159 | type BindGroupReq struct { 160 | SystemId string `protobuf:"bytes,1,opt,name=systemId,proto3" json:"systemId,omitempty"` 161 | GroupName string `protobuf:"bytes,2,opt,name=groupName,proto3" json:"groupName,omitempty"` 162 | ClientId string `protobuf:"bytes,3,opt,name=clientId,proto3" json:"clientId,omitempty"` 163 | UserId string `protobuf:"bytes,4,opt,name=userId,proto3" json:"userId,omitempty"` 164 | Extend string `protobuf:"bytes,5,opt,name=extend,proto3" json:"extend,omitempty"` 165 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 166 | XXX_unrecognized []byte `json:"-"` 167 | XXX_sizecache int32 `json:"-"` 168 | } 169 | 170 | func (m *BindGroupReq) Reset() { *m = BindGroupReq{} } 171 | func (m *BindGroupReq) String() string { return proto.CompactTextString(m) } 172 | func (*BindGroupReq) ProtoMessage() {} 173 | func (*BindGroupReq) Descriptor() ([]byte, []int) { 174 | return fileDescriptor_30edbedfc0173ed4, []int{2} 175 | } 176 | 177 | func (m *BindGroupReq) XXX_Unmarshal(b []byte) error { 178 | return xxx_messageInfo_BindGroupReq.Unmarshal(m, b) 179 | } 180 | func (m *BindGroupReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 181 | return xxx_messageInfo_BindGroupReq.Marshal(b, m, deterministic) 182 | } 183 | func (m *BindGroupReq) XXX_Merge(src proto.Message) { 184 | xxx_messageInfo_BindGroupReq.Merge(m, src) 185 | } 186 | func (m *BindGroupReq) XXX_Size() int { 187 | return xxx_messageInfo_BindGroupReq.Size(m) 188 | } 189 | func (m *BindGroupReq) XXX_DiscardUnknown() { 190 | xxx_messageInfo_BindGroupReq.DiscardUnknown(m) 191 | } 192 | 193 | var xxx_messageInfo_BindGroupReq proto.InternalMessageInfo 194 | 195 | func (m *BindGroupReq) GetSystemId() string { 196 | if m != nil { 197 | return m.SystemId 198 | } 199 | return "" 200 | } 201 | 202 | func (m *BindGroupReq) GetGroupName() string { 203 | if m != nil { 204 | return m.GroupName 205 | } 206 | return "" 207 | } 208 | 209 | func (m *BindGroupReq) GetClientId() string { 210 | if m != nil { 211 | return m.ClientId 212 | } 213 | return "" 214 | } 215 | 216 | func (m *BindGroupReq) GetUserId() string { 217 | if m != nil { 218 | return m.UserId 219 | } 220 | return "" 221 | } 222 | 223 | func (m *BindGroupReq) GetExtend() string { 224 | if m != nil { 225 | return m.Extend 226 | } 227 | return "" 228 | } 229 | 230 | type Send2GroupReq struct { 231 | SystemId string `protobuf:"bytes,1,opt,name=systemId,proto3" json:"systemId,omitempty"` 232 | MessageId string `protobuf:"bytes,2,opt,name=messageId,proto3" json:"messageId,omitempty"` 233 | SendUserId string `protobuf:"bytes,3,opt,name=sendUserId,proto3" json:"sendUserId,omitempty"` 234 | GroupName string `protobuf:"bytes,4,opt,name=groupName,proto3" json:"groupName,omitempty"` 235 | Code int32 `protobuf:"varint,5,opt,name=code,proto3" json:"code,omitempty"` 236 | Message string `protobuf:"bytes,6,opt,name=message,proto3" json:"message,omitempty"` 237 | Data string `protobuf:"bytes,7,opt,name=data,proto3" json:"data,omitempty"` 238 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 239 | XXX_unrecognized []byte `json:"-"` 240 | XXX_sizecache int32 `json:"-"` 241 | } 242 | 243 | func (m *Send2GroupReq) Reset() { *m = Send2GroupReq{} } 244 | func (m *Send2GroupReq) String() string { return proto.CompactTextString(m) } 245 | func (*Send2GroupReq) ProtoMessage() {} 246 | func (*Send2GroupReq) Descriptor() ([]byte, []int) { 247 | return fileDescriptor_30edbedfc0173ed4, []int{3} 248 | } 249 | 250 | func (m *Send2GroupReq) XXX_Unmarshal(b []byte) error { 251 | return xxx_messageInfo_Send2GroupReq.Unmarshal(m, b) 252 | } 253 | func (m *Send2GroupReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 254 | return xxx_messageInfo_Send2GroupReq.Marshal(b, m, deterministic) 255 | } 256 | func (m *Send2GroupReq) XXX_Merge(src proto.Message) { 257 | xxx_messageInfo_Send2GroupReq.Merge(m, src) 258 | } 259 | func (m *Send2GroupReq) XXX_Size() int { 260 | return xxx_messageInfo_Send2GroupReq.Size(m) 261 | } 262 | func (m *Send2GroupReq) XXX_DiscardUnknown() { 263 | xxx_messageInfo_Send2GroupReq.DiscardUnknown(m) 264 | } 265 | 266 | var xxx_messageInfo_Send2GroupReq proto.InternalMessageInfo 267 | 268 | func (m *Send2GroupReq) GetSystemId() string { 269 | if m != nil { 270 | return m.SystemId 271 | } 272 | return "" 273 | } 274 | 275 | func (m *Send2GroupReq) GetMessageId() string { 276 | if m != nil { 277 | return m.MessageId 278 | } 279 | return "" 280 | } 281 | 282 | func (m *Send2GroupReq) GetSendUserId() string { 283 | if m != nil { 284 | return m.SendUserId 285 | } 286 | return "" 287 | } 288 | 289 | func (m *Send2GroupReq) GetGroupName() string { 290 | if m != nil { 291 | return m.GroupName 292 | } 293 | return "" 294 | } 295 | 296 | func (m *Send2GroupReq) GetCode() int32 { 297 | if m != nil { 298 | return m.Code 299 | } 300 | return 0 301 | } 302 | 303 | func (m *Send2GroupReq) GetMessage() string { 304 | if m != nil { 305 | return m.Message 306 | } 307 | return "" 308 | } 309 | 310 | func (m *Send2GroupReq) GetData() string { 311 | if m != nil { 312 | return m.Data 313 | } 314 | return "" 315 | } 316 | 317 | type Send2SystemReq struct { 318 | SystemId string `protobuf:"bytes,1,opt,name=systemId,proto3" json:"systemId,omitempty"` 319 | MessageId string `protobuf:"bytes,2,opt,name=messageId,proto3" json:"messageId,omitempty"` 320 | SendUserId string `protobuf:"bytes,3,opt,name=sendUserId,proto3" json:"sendUserId,omitempty"` 321 | Code int32 `protobuf:"varint,4,opt,name=code,proto3" json:"code,omitempty"` 322 | Message string `protobuf:"bytes,5,opt,name=message,proto3" json:"message,omitempty"` 323 | Data string `protobuf:"bytes,6,opt,name=data,proto3" json:"data,omitempty"` 324 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 325 | XXX_unrecognized []byte `json:"-"` 326 | XXX_sizecache int32 `json:"-"` 327 | } 328 | 329 | func (m *Send2SystemReq) Reset() { *m = Send2SystemReq{} } 330 | func (m *Send2SystemReq) String() string { return proto.CompactTextString(m) } 331 | func (*Send2SystemReq) ProtoMessage() {} 332 | func (*Send2SystemReq) Descriptor() ([]byte, []int) { 333 | return fileDescriptor_30edbedfc0173ed4, []int{4} 334 | } 335 | 336 | func (m *Send2SystemReq) XXX_Unmarshal(b []byte) error { 337 | return xxx_messageInfo_Send2SystemReq.Unmarshal(m, b) 338 | } 339 | func (m *Send2SystemReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 340 | return xxx_messageInfo_Send2SystemReq.Marshal(b, m, deterministic) 341 | } 342 | func (m *Send2SystemReq) XXX_Merge(src proto.Message) { 343 | xxx_messageInfo_Send2SystemReq.Merge(m, src) 344 | } 345 | func (m *Send2SystemReq) XXX_Size() int { 346 | return xxx_messageInfo_Send2SystemReq.Size(m) 347 | } 348 | func (m *Send2SystemReq) XXX_DiscardUnknown() { 349 | xxx_messageInfo_Send2SystemReq.DiscardUnknown(m) 350 | } 351 | 352 | var xxx_messageInfo_Send2SystemReq proto.InternalMessageInfo 353 | 354 | func (m *Send2SystemReq) GetSystemId() string { 355 | if m != nil { 356 | return m.SystemId 357 | } 358 | return "" 359 | } 360 | 361 | func (m *Send2SystemReq) GetMessageId() string { 362 | if m != nil { 363 | return m.MessageId 364 | } 365 | return "" 366 | } 367 | 368 | func (m *Send2SystemReq) GetSendUserId() string { 369 | if m != nil { 370 | return m.SendUserId 371 | } 372 | return "" 373 | } 374 | 375 | func (m *Send2SystemReq) GetCode() int32 { 376 | if m != nil { 377 | return m.Code 378 | } 379 | return 0 380 | } 381 | 382 | func (m *Send2SystemReq) GetMessage() string { 383 | if m != nil { 384 | return m.Message 385 | } 386 | return "" 387 | } 388 | 389 | func (m *Send2SystemReq) GetData() string { 390 | if m != nil { 391 | return m.Data 392 | } 393 | return "" 394 | } 395 | 396 | type GetGroupClientsReq struct { 397 | SystemId string `protobuf:"bytes,1,opt,name=systemId,proto3" json:"systemId,omitempty"` 398 | GroupName string `protobuf:"bytes,2,opt,name=groupName,proto3" json:"groupName,omitempty"` 399 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 400 | XXX_unrecognized []byte `json:"-"` 401 | XXX_sizecache int32 `json:"-"` 402 | } 403 | 404 | func (m *GetGroupClientsReq) Reset() { *m = GetGroupClientsReq{} } 405 | func (m *GetGroupClientsReq) String() string { return proto.CompactTextString(m) } 406 | func (*GetGroupClientsReq) ProtoMessage() {} 407 | func (*GetGroupClientsReq) Descriptor() ([]byte, []int) { 408 | return fileDescriptor_30edbedfc0173ed4, []int{5} 409 | } 410 | 411 | func (m *GetGroupClientsReq) XXX_Unmarshal(b []byte) error { 412 | return xxx_messageInfo_GetGroupClientsReq.Unmarshal(m, b) 413 | } 414 | func (m *GetGroupClientsReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 415 | return xxx_messageInfo_GetGroupClientsReq.Marshal(b, m, deterministic) 416 | } 417 | func (m *GetGroupClientsReq) XXX_Merge(src proto.Message) { 418 | xxx_messageInfo_GetGroupClientsReq.Merge(m, src) 419 | } 420 | func (m *GetGroupClientsReq) XXX_Size() int { 421 | return xxx_messageInfo_GetGroupClientsReq.Size(m) 422 | } 423 | func (m *GetGroupClientsReq) XXX_DiscardUnknown() { 424 | xxx_messageInfo_GetGroupClientsReq.DiscardUnknown(m) 425 | } 426 | 427 | var xxx_messageInfo_GetGroupClientsReq proto.InternalMessageInfo 428 | 429 | func (m *GetGroupClientsReq) GetSystemId() string { 430 | if m != nil { 431 | return m.SystemId 432 | } 433 | return "" 434 | } 435 | 436 | func (m *GetGroupClientsReq) GetGroupName() string { 437 | if m != nil { 438 | return m.GroupName 439 | } 440 | return "" 441 | } 442 | 443 | type Send2ClientReply struct { 444 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 445 | XXX_unrecognized []byte `json:"-"` 446 | XXX_sizecache int32 `json:"-"` 447 | } 448 | 449 | func (m *Send2ClientReply) Reset() { *m = Send2ClientReply{} } 450 | func (m *Send2ClientReply) String() string { return proto.CompactTextString(m) } 451 | func (*Send2ClientReply) ProtoMessage() {} 452 | func (*Send2ClientReply) Descriptor() ([]byte, []int) { 453 | return fileDescriptor_30edbedfc0173ed4, []int{6} 454 | } 455 | 456 | func (m *Send2ClientReply) XXX_Unmarshal(b []byte) error { 457 | return xxx_messageInfo_Send2ClientReply.Unmarshal(m, b) 458 | } 459 | func (m *Send2ClientReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 460 | return xxx_messageInfo_Send2ClientReply.Marshal(b, m, deterministic) 461 | } 462 | func (m *Send2ClientReply) XXX_Merge(src proto.Message) { 463 | xxx_messageInfo_Send2ClientReply.Merge(m, src) 464 | } 465 | func (m *Send2ClientReply) XXX_Size() int { 466 | return xxx_messageInfo_Send2ClientReply.Size(m) 467 | } 468 | func (m *Send2ClientReply) XXX_DiscardUnknown() { 469 | xxx_messageInfo_Send2ClientReply.DiscardUnknown(m) 470 | } 471 | 472 | var xxx_messageInfo_Send2ClientReply proto.InternalMessageInfo 473 | 474 | type CloseClientReply struct { 475 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 476 | XXX_unrecognized []byte `json:"-"` 477 | XXX_sizecache int32 `json:"-"` 478 | } 479 | 480 | func (m *CloseClientReply) Reset() { *m = CloseClientReply{} } 481 | func (m *CloseClientReply) String() string { return proto.CompactTextString(m) } 482 | func (*CloseClientReply) ProtoMessage() {} 483 | func (*CloseClientReply) Descriptor() ([]byte, []int) { 484 | return fileDescriptor_30edbedfc0173ed4, []int{7} 485 | } 486 | 487 | func (m *CloseClientReply) XXX_Unmarshal(b []byte) error { 488 | return xxx_messageInfo_CloseClientReply.Unmarshal(m, b) 489 | } 490 | func (m *CloseClientReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 491 | return xxx_messageInfo_CloseClientReply.Marshal(b, m, deterministic) 492 | } 493 | func (m *CloseClientReply) XXX_Merge(src proto.Message) { 494 | xxx_messageInfo_CloseClientReply.Merge(m, src) 495 | } 496 | func (m *CloseClientReply) XXX_Size() int { 497 | return xxx_messageInfo_CloseClientReply.Size(m) 498 | } 499 | func (m *CloseClientReply) XXX_DiscardUnknown() { 500 | xxx_messageInfo_CloseClientReply.DiscardUnknown(m) 501 | } 502 | 503 | var xxx_messageInfo_CloseClientReply proto.InternalMessageInfo 504 | 505 | type BindGroupReply struct { 506 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 507 | XXX_unrecognized []byte `json:"-"` 508 | XXX_sizecache int32 `json:"-"` 509 | } 510 | 511 | func (m *BindGroupReply) Reset() { *m = BindGroupReply{} } 512 | func (m *BindGroupReply) String() string { return proto.CompactTextString(m) } 513 | func (*BindGroupReply) ProtoMessage() {} 514 | func (*BindGroupReply) Descriptor() ([]byte, []int) { 515 | return fileDescriptor_30edbedfc0173ed4, []int{8} 516 | } 517 | 518 | func (m *BindGroupReply) XXX_Unmarshal(b []byte) error { 519 | return xxx_messageInfo_BindGroupReply.Unmarshal(m, b) 520 | } 521 | func (m *BindGroupReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 522 | return xxx_messageInfo_BindGroupReply.Marshal(b, m, deterministic) 523 | } 524 | func (m *BindGroupReply) XXX_Merge(src proto.Message) { 525 | xxx_messageInfo_BindGroupReply.Merge(m, src) 526 | } 527 | func (m *BindGroupReply) XXX_Size() int { 528 | return xxx_messageInfo_BindGroupReply.Size(m) 529 | } 530 | func (m *BindGroupReply) XXX_DiscardUnknown() { 531 | xxx_messageInfo_BindGroupReply.DiscardUnknown(m) 532 | } 533 | 534 | var xxx_messageInfo_BindGroupReply proto.InternalMessageInfo 535 | 536 | type Send2GroupReply struct { 537 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 538 | XXX_unrecognized []byte `json:"-"` 539 | XXX_sizecache int32 `json:"-"` 540 | } 541 | 542 | func (m *Send2GroupReply) Reset() { *m = Send2GroupReply{} } 543 | func (m *Send2GroupReply) String() string { return proto.CompactTextString(m) } 544 | func (*Send2GroupReply) ProtoMessage() {} 545 | func (*Send2GroupReply) Descriptor() ([]byte, []int) { 546 | return fileDescriptor_30edbedfc0173ed4, []int{9} 547 | } 548 | 549 | func (m *Send2GroupReply) XXX_Unmarshal(b []byte) error { 550 | return xxx_messageInfo_Send2GroupReply.Unmarshal(m, b) 551 | } 552 | func (m *Send2GroupReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 553 | return xxx_messageInfo_Send2GroupReply.Marshal(b, m, deterministic) 554 | } 555 | func (m *Send2GroupReply) XXX_Merge(src proto.Message) { 556 | xxx_messageInfo_Send2GroupReply.Merge(m, src) 557 | } 558 | func (m *Send2GroupReply) XXX_Size() int { 559 | return xxx_messageInfo_Send2GroupReply.Size(m) 560 | } 561 | func (m *Send2GroupReply) XXX_DiscardUnknown() { 562 | xxx_messageInfo_Send2GroupReply.DiscardUnknown(m) 563 | } 564 | 565 | var xxx_messageInfo_Send2GroupReply proto.InternalMessageInfo 566 | 567 | type Send2SystemReply struct { 568 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 569 | XXX_unrecognized []byte `json:"-"` 570 | XXX_sizecache int32 `json:"-"` 571 | } 572 | 573 | func (m *Send2SystemReply) Reset() { *m = Send2SystemReply{} } 574 | func (m *Send2SystemReply) String() string { return proto.CompactTextString(m) } 575 | func (*Send2SystemReply) ProtoMessage() {} 576 | func (*Send2SystemReply) Descriptor() ([]byte, []int) { 577 | return fileDescriptor_30edbedfc0173ed4, []int{10} 578 | } 579 | 580 | func (m *Send2SystemReply) XXX_Unmarshal(b []byte) error { 581 | return xxx_messageInfo_Send2SystemReply.Unmarshal(m, b) 582 | } 583 | func (m *Send2SystemReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 584 | return xxx_messageInfo_Send2SystemReply.Marshal(b, m, deterministic) 585 | } 586 | func (m *Send2SystemReply) XXX_Merge(src proto.Message) { 587 | xxx_messageInfo_Send2SystemReply.Merge(m, src) 588 | } 589 | func (m *Send2SystemReply) XXX_Size() int { 590 | return xxx_messageInfo_Send2SystemReply.Size(m) 591 | } 592 | func (m *Send2SystemReply) XXX_DiscardUnknown() { 593 | xxx_messageInfo_Send2SystemReply.DiscardUnknown(m) 594 | } 595 | 596 | var xxx_messageInfo_Send2SystemReply proto.InternalMessageInfo 597 | 598 | type GetGroupClientsReply struct { 599 | List []string `protobuf:"bytes,1,rep,name=list,proto3" json:"list,omitempty"` 600 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 601 | XXX_unrecognized []byte `json:"-"` 602 | XXX_sizecache int32 `json:"-"` 603 | } 604 | 605 | func (m *GetGroupClientsReply) Reset() { *m = GetGroupClientsReply{} } 606 | func (m *GetGroupClientsReply) String() string { return proto.CompactTextString(m) } 607 | func (*GetGroupClientsReply) ProtoMessage() {} 608 | func (*GetGroupClientsReply) Descriptor() ([]byte, []int) { 609 | return fileDescriptor_30edbedfc0173ed4, []int{11} 610 | } 611 | 612 | func (m *GetGroupClientsReply) XXX_Unmarshal(b []byte) error { 613 | return xxx_messageInfo_GetGroupClientsReply.Unmarshal(m, b) 614 | } 615 | func (m *GetGroupClientsReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 616 | return xxx_messageInfo_GetGroupClientsReply.Marshal(b, m, deterministic) 617 | } 618 | func (m *GetGroupClientsReply) XXX_Merge(src proto.Message) { 619 | xxx_messageInfo_GetGroupClientsReply.Merge(m, src) 620 | } 621 | func (m *GetGroupClientsReply) XXX_Size() int { 622 | return xxx_messageInfo_GetGroupClientsReply.Size(m) 623 | } 624 | func (m *GetGroupClientsReply) XXX_DiscardUnknown() { 625 | xxx_messageInfo_GetGroupClientsReply.DiscardUnknown(m) 626 | } 627 | 628 | var xxx_messageInfo_GetGroupClientsReply proto.InternalMessageInfo 629 | 630 | func (m *GetGroupClientsReply) GetList() []string { 631 | if m != nil { 632 | return m.List 633 | } 634 | return nil 635 | } 636 | 637 | func init() { 638 | proto.RegisterType((*Send2ClientReq)(nil), "Send2ClientReq") 639 | proto.RegisterType((*CloseClientReq)(nil), "CloseClientReq") 640 | proto.RegisterType((*BindGroupReq)(nil), "BindGroupReq") 641 | proto.RegisterType((*Send2GroupReq)(nil), "Send2GroupReq") 642 | proto.RegisterType((*Send2SystemReq)(nil), "Send2SystemReq") 643 | proto.RegisterType((*GetGroupClientsReq)(nil), "GetGroupClientsReq") 644 | proto.RegisterType((*Send2ClientReply)(nil), "Send2ClientReply") 645 | proto.RegisterType((*CloseClientReply)(nil), "CloseClientReply") 646 | proto.RegisterType((*BindGroupReply)(nil), "BindGroupReply") 647 | proto.RegisterType((*Send2GroupReply)(nil), "Send2GroupReply") 648 | proto.RegisterType((*Send2SystemReply)(nil), "Send2SystemReply") 649 | proto.RegisterType((*GetGroupClientsReply)(nil), "GetGroupClientsReply") 650 | } 651 | 652 | func init() { proto.RegisterFile("servers/grpc.proto", fileDescriptor_30edbedfc0173ed4) } 653 | 654 | var fileDescriptor_30edbedfc0173ed4 = []byte{ 655 | // 477 bytes of a gzipped FileDescriptorProto 656 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xb4, 0x54, 0xcb, 0x6e, 0x1a, 0x31, 657 | 0x14, 0x9d, 0xe1, 0x95, 0x72, 0x1b, 0x18, 0xb8, 0x7d, 0xc8, 0x1a, 0x55, 0x15, 0x9a, 0x55, 0x54, 658 | 0xa9, 0x6e, 0x95, 0x7c, 0x40, 0xa5, 0xb0, 0x48, 0xd9, 0x64, 0x01, 0xea, 0xa6, 0x3b, 0x82, 0xaf, 659 | 0x10, 0xd2, 0xbc, 0x3a, 0x9e, 0x44, 0xe5, 0x3f, 0xfa, 0x19, 0xfd, 0x90, 0x76, 0xd7, 0x4f, 0xaa, 660 | 0xec, 0xc1, 0x83, 0x3d, 0xb0, 0x40, 0x42, 0xd9, 0xf9, 0x1e, 0xbf, 0xce, 0x39, 0x3e, 0xd7, 0x80, 661 | 0x92, 0x8a, 0x27, 0x2a, 0xe4, 0xa7, 0x75, 0x91, 0xaf, 0x78, 0x5e, 0x64, 0x65, 0x16, 0xfd, 0xf1, 662 | 0x61, 0xb8, 0xa0, 0x54, 0x5c, 0x4f, 0xe3, 0x0d, 0xa5, 0xe5, 0x9c, 0x7e, 0x60, 0x08, 0x2f, 0xe4, 663 | 0x56, 0x96, 0x94, 0xcc, 0x04, 0xf3, 0x27, 0xfe, 0x55, 0x7f, 0x5e, 0xd7, 0xf8, 0x0e, 0xfa, 0x09, 664 | 0x49, 0xb9, 0x5c, 0xd3, 0x4c, 0xb0, 0x96, 0x9e, 0xdc, 0x03, 0xf8, 0x1e, 0x40, 0x52, 0x2a, 0xbe, 665 | 0x49, 0x2a, 0x66, 0x82, 0xb5, 0xf5, 0xb4, 0x85, 0xa8, 0x93, 0x57, 0xfa, 0x9a, 0x99, 0x60, 0x9d, 666 | 0xea, 0x64, 0x53, 0x23, 0x42, 0x67, 0x95, 0x09, 0x62, 0xdd, 0x89, 0x7f, 0xd5, 0x9d, 0xeb, 0x31, 667 | 0x32, 0xb8, 0xd8, 0x1d, 0xce, 0x7a, 0x7a, 0xb9, 0x29, 0xd5, 0x6a, 0xb1, 0x2c, 0x97, 0xec, 0x42, 668 | 0xc3, 0x7a, 0x1c, 0x7d, 0x85, 0xe1, 0x34, 0xce, 0x24, 0x9d, 0xa6, 0xc4, 0xe6, 0xd2, 0x72, 0xb9, 669 | 0x44, 0xbf, 0x7c, 0xb8, 0xbc, 0xdd, 0xa4, 0xe2, 0xae, 0xc8, 0x1e, 0xf3, 0x13, 0x2c, 0x59, 0xab, 670 | 0x75, 0xf7, 0xcb, 0x84, 0x8c, 0x25, 0x35, 0xe0, 0x5c, 0xd3, 0x6e, 0x48, 0x7e, 0x0b, 0xbd, 0xc7, 671 | 0xca, 0xaa, 0xca, 0x8c, 0x5d, 0xa5, 0x70, 0xfa, 0x59, 0x52, 0x2a, 0xb4, 0x19, 0xfd, 0xf9, 0xae, 672 | 0x8a, 0xfe, 0xfa, 0x30, 0xd0, 0x6f, 0x75, 0x2a, 0xaf, 0x33, 0x9e, 0xca, 0x51, 0xd5, 0x69, 0xaa, 673 | 0x3a, 0xff, 0xb1, 0x7e, 0x9b, 0xdc, 0x2d, 0x34, 0xdf, 0xe7, 0x15, 0x63, 0xe8, 0x76, 0x8e, 0xd3, 674 | 0xed, 0x1e, 0xa7, 0xdb, 0xb3, 0xe8, 0xde, 0x03, 0xde, 0x51, 0xa9, 0x7d, 0xaf, 0xe2, 0x25, 0xcf, 675 | 0x8a, 0x45, 0x84, 0x30, 0x72, 0xba, 0x2e, 0x8f, 0xb7, 0x0a, 0x73, 0xf2, 0xab, 0xb0, 0x11, 0x0c, 676 | 0xad, 0x20, 0x2a, 0x64, 0x0c, 0x81, 0x9d, 0x81, 0xdd, 0x46, 0xc7, 0x4a, 0x85, 0x7d, 0x80, 0xd7, 677 | 0x07, 0x84, 0xf3, 0x78, 0xab, 0xc4, 0xc5, 0x1b, 0x59, 0x32, 0x7f, 0xd2, 0x56, 0xe2, 0xd4, 0xf8, 678 | 0xfa, 0x5f, 0x0b, 0x06, 0xd3, 0x2c, 0x49, 0xb2, 0x74, 0x41, 0xc5, 0xd3, 0x66, 0x45, 0x78, 0x03, 679 | 0x2f, 0x2d, 0x7a, 0x18, 0x70, 0xf7, 0x8b, 0x08, 0xc7, 0xfc, 0x80, 0xbd, 0xa7, 0x36, 0x59, 0xfc, 680 | 0x31, 0xe0, 0x6e, 0x37, 0x86, 0x63, 0x7e, 0x20, 0xcf, 0xc3, 0x8f, 0xd0, 0xaf, 0x05, 0xe2, 0x80, 681 | 0xdb, 0x5d, 0x17, 0x06, 0xbc, 0xa1, 0xdd, 0xc3, 0xcf, 0x00, 0x7b, 0xf5, 0x38, 0xe4, 0x4e, 0x3b, 682 | 0x84, 0x23, 0xde, 0xb4, 0xc6, 0xab, 0xa5, 0x54, 0xe6, 0x18, 0x29, 0x75, 0xea, 0x8c, 0x14, 0xdb, 683 | 0x3b, 0x0f, 0xbf, 0x40, 0xd0, 0x70, 0x0f, 0x5f, 0xf1, 0xc3, 0x00, 0x84, 0x6f, 0xf8, 0x31, 0x93, 684 | 0x23, 0xef, 0xf6, 0xf2, 0x3b, 0x98, 0xcf, 0x36, 0x7f, 0x78, 0xe8, 0xe9, 0xbf, 0xf6, 0xe6, 0x7f, 685 | 0x00, 0x00, 0x00, 0xff, 0xff, 0x91, 0x68, 0x55, 0x86, 0x81, 0x05, 0x00, 0x00, 686 | } 687 | 688 | // Reference imports to suppress errors if they are not otherwise used. 689 | var _ context.Context 690 | var _ grpc.ClientConn 691 | 692 | // This is a compile-time assertion to ensure that this generated file 693 | // is compatible with the grpc package it is being compiled against. 694 | const _ = grpc.SupportPackageIsVersion4 695 | 696 | // CommonServiceClient is the client API for CommonService service. 697 | // 698 | // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. 699 | type CommonServiceClient interface { 700 | Send2Client(ctx context.Context, in *Send2ClientReq, opts ...grpc.CallOption) (*Send2ClientReply, error) 701 | CloseClient(ctx context.Context, in *CloseClientReq, opts ...grpc.CallOption) (*CloseClientReply, error) 702 | BindGroup(ctx context.Context, in *BindGroupReq, opts ...grpc.CallOption) (*BindGroupReply, error) 703 | Send2Group(ctx context.Context, in *Send2GroupReq, opts ...grpc.CallOption) (*Send2GroupReply, error) 704 | Send2System(ctx context.Context, in *Send2SystemReq, opts ...grpc.CallOption) (*Send2SystemReply, error) 705 | GetGroupClients(ctx context.Context, in *GetGroupClientsReq, opts ...grpc.CallOption) (*GetGroupClientsReply, error) 706 | } 707 | 708 | type commonServiceClient struct { 709 | cc *grpc.ClientConn 710 | } 711 | 712 | func NewCommonServiceClient(cc *grpc.ClientConn) CommonServiceClient { 713 | return &commonServiceClient{cc} 714 | } 715 | 716 | func (c *commonServiceClient) Send2Client(ctx context.Context, in *Send2ClientReq, opts ...grpc.CallOption) (*Send2ClientReply, error) { 717 | out := new(Send2ClientReply) 718 | err := c.cc.Invoke(ctx, "/CommonService/Send2Client", in, out, opts...) 719 | if err != nil { 720 | return nil, err 721 | } 722 | return out, nil 723 | } 724 | 725 | func (c *commonServiceClient) CloseClient(ctx context.Context, in *CloseClientReq, opts ...grpc.CallOption) (*CloseClientReply, error) { 726 | out := new(CloseClientReply) 727 | err := c.cc.Invoke(ctx, "/CommonService/CloseClient", in, out, opts...) 728 | if err != nil { 729 | return nil, err 730 | } 731 | return out, nil 732 | } 733 | 734 | func (c *commonServiceClient) BindGroup(ctx context.Context, in *BindGroupReq, opts ...grpc.CallOption) (*BindGroupReply, error) { 735 | out := new(BindGroupReply) 736 | err := c.cc.Invoke(ctx, "/CommonService/BindGroup", in, out, opts...) 737 | if err != nil { 738 | return nil, err 739 | } 740 | return out, nil 741 | } 742 | 743 | func (c *commonServiceClient) Send2Group(ctx context.Context, in *Send2GroupReq, opts ...grpc.CallOption) (*Send2GroupReply, error) { 744 | out := new(Send2GroupReply) 745 | err := c.cc.Invoke(ctx, "/CommonService/Send2Group", in, out, opts...) 746 | if err != nil { 747 | return nil, err 748 | } 749 | return out, nil 750 | } 751 | 752 | func (c *commonServiceClient) Send2System(ctx context.Context, in *Send2SystemReq, opts ...grpc.CallOption) (*Send2SystemReply, error) { 753 | out := new(Send2SystemReply) 754 | err := c.cc.Invoke(ctx, "/CommonService/Send2System", in, out, opts...) 755 | if err != nil { 756 | return nil, err 757 | } 758 | return out, nil 759 | } 760 | 761 | func (c *commonServiceClient) GetGroupClients(ctx context.Context, in *GetGroupClientsReq, opts ...grpc.CallOption) (*GetGroupClientsReply, error) { 762 | out := new(GetGroupClientsReply) 763 | err := c.cc.Invoke(ctx, "/CommonService/GetGroupClients", in, out, opts...) 764 | if err != nil { 765 | return nil, err 766 | } 767 | return out, nil 768 | } 769 | 770 | // CommonServiceServer is the server API for CommonService service. 771 | type CommonServiceServer interface { 772 | Send2Client(context.Context, *Send2ClientReq) (*Send2ClientReply, error) 773 | CloseClient(context.Context, *CloseClientReq) (*CloseClientReply, error) 774 | BindGroup(context.Context, *BindGroupReq) (*BindGroupReply, error) 775 | Send2Group(context.Context, *Send2GroupReq) (*Send2GroupReply, error) 776 | Send2System(context.Context, *Send2SystemReq) (*Send2SystemReply, error) 777 | GetGroupClients(context.Context, *GetGroupClientsReq) (*GetGroupClientsReply, error) 778 | } 779 | 780 | func RegisterCommonServiceServer(s *grpc.Server, srv CommonServiceServer) { 781 | s.RegisterService(&_CommonService_serviceDesc, srv) 782 | } 783 | 784 | func _CommonService_Send2Client_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 785 | in := new(Send2ClientReq) 786 | if err := dec(in); err != nil { 787 | return nil, err 788 | } 789 | if interceptor == nil { 790 | return srv.(CommonServiceServer).Send2Client(ctx, in) 791 | } 792 | info := &grpc.UnaryServerInfo{ 793 | Server: srv, 794 | FullMethod: "/CommonService/Send2Client", 795 | } 796 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 797 | return srv.(CommonServiceServer).Send2Client(ctx, req.(*Send2ClientReq)) 798 | } 799 | return interceptor(ctx, in, info, handler) 800 | } 801 | 802 | func _CommonService_CloseClient_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 803 | in := new(CloseClientReq) 804 | if err := dec(in); err != nil { 805 | return nil, err 806 | } 807 | if interceptor == nil { 808 | return srv.(CommonServiceServer).CloseClient(ctx, in) 809 | } 810 | info := &grpc.UnaryServerInfo{ 811 | Server: srv, 812 | FullMethod: "/CommonService/CloseClient", 813 | } 814 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 815 | return srv.(CommonServiceServer).CloseClient(ctx, req.(*CloseClientReq)) 816 | } 817 | return interceptor(ctx, in, info, handler) 818 | } 819 | 820 | func _CommonService_BindGroup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 821 | in := new(BindGroupReq) 822 | if err := dec(in); err != nil { 823 | return nil, err 824 | } 825 | if interceptor == nil { 826 | return srv.(CommonServiceServer).BindGroup(ctx, in) 827 | } 828 | info := &grpc.UnaryServerInfo{ 829 | Server: srv, 830 | FullMethod: "/CommonService/BindGroup", 831 | } 832 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 833 | return srv.(CommonServiceServer).BindGroup(ctx, req.(*BindGroupReq)) 834 | } 835 | return interceptor(ctx, in, info, handler) 836 | } 837 | 838 | func _CommonService_Send2Group_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 839 | in := new(Send2GroupReq) 840 | if err := dec(in); err != nil { 841 | return nil, err 842 | } 843 | if interceptor == nil { 844 | return srv.(CommonServiceServer).Send2Group(ctx, in) 845 | } 846 | info := &grpc.UnaryServerInfo{ 847 | Server: srv, 848 | FullMethod: "/CommonService/Send2Group", 849 | } 850 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 851 | return srv.(CommonServiceServer).Send2Group(ctx, req.(*Send2GroupReq)) 852 | } 853 | return interceptor(ctx, in, info, handler) 854 | } 855 | 856 | func _CommonService_Send2System_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 857 | in := new(Send2SystemReq) 858 | if err := dec(in); err != nil { 859 | return nil, err 860 | } 861 | if interceptor == nil { 862 | return srv.(CommonServiceServer).Send2System(ctx, in) 863 | } 864 | info := &grpc.UnaryServerInfo{ 865 | Server: srv, 866 | FullMethod: "/CommonService/Send2System", 867 | } 868 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 869 | return srv.(CommonServiceServer).Send2System(ctx, req.(*Send2SystemReq)) 870 | } 871 | return interceptor(ctx, in, info, handler) 872 | } 873 | 874 | func _CommonService_GetGroupClients_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { 875 | in := new(GetGroupClientsReq) 876 | if err := dec(in); err != nil { 877 | return nil, err 878 | } 879 | if interceptor == nil { 880 | return srv.(CommonServiceServer).GetGroupClients(ctx, in) 881 | } 882 | info := &grpc.UnaryServerInfo{ 883 | Server: srv, 884 | FullMethod: "/CommonService/GetGroupClients", 885 | } 886 | handler := func(ctx context.Context, req interface{}) (interface{}, error) { 887 | return srv.(CommonServiceServer).GetGroupClients(ctx, req.(*GetGroupClientsReq)) 888 | } 889 | return interceptor(ctx, in, info, handler) 890 | } 891 | 892 | var _CommonService_serviceDesc = grpc.ServiceDesc{ 893 | ServiceName: "CommonService", 894 | HandlerType: (*CommonServiceServer)(nil), 895 | Methods: []grpc.MethodDesc{ 896 | { 897 | MethodName: "Send2Client", 898 | Handler: _CommonService_Send2Client_Handler, 899 | }, 900 | { 901 | MethodName: "CloseClient", 902 | Handler: _CommonService_CloseClient_Handler, 903 | }, 904 | { 905 | MethodName: "BindGroup", 906 | Handler: _CommonService_BindGroup_Handler, 907 | }, 908 | { 909 | MethodName: "Send2Group", 910 | Handler: _CommonService_Send2Group_Handler, 911 | }, 912 | { 913 | MethodName: "Send2System", 914 | Handler: _CommonService_Send2System_Handler, 915 | }, 916 | { 917 | MethodName: "GetGroupClients", 918 | Handler: _CommonService_GetGroupClients_Handler, 919 | }, 920 | }, 921 | Streams: []grpc.StreamDesc{}, 922 | Metadata: "servers/grpc.proto", 923 | } 924 | --------------------------------------------------------------------------------