├── utility └── .gitkeep ├── internal ├── model │ ├── .gitkeep │ └── entity │ │ └── .gitkeep ├── service │ ├── .gitkeep │ └── internal │ │ ├── dao │ │ └── .gitkeep │ │ └── do │ │ └── .gitkeep ├── consts │ └── consts.go ├── packed │ ├── packed.go │ └── websocket │ │ ├── model.go │ │ ├── router.go │ │ ├── init.go │ │ ├── controller.go │ │ ├── client.go │ │ └── client_manager.go ├── controller │ └── hello.go └── cmd │ └── cmd.go ├── resource ├── i18n │ └── .gitkeep ├── template │ └── .gitkeep └── public │ ├── html │ └── .gitkeep │ ├── plugin │ └── .gitkeep │ └── resource │ ├── css │ └── .gitkeep │ ├── js │ └── .gitkeep │ └── image │ └── .gitkeep ├── .gitattributes ├── manifest ├── docker │ ├── docker.sh │ └── Dockerfile ├── deploy │ └── kustomize │ │ ├── base │ │ ├── kustomization.yaml │ │ ├── service.yaml │ │ └── deployment.yaml │ │ └── overlays │ │ └── develop │ │ ├── deployment.yaml │ │ ├── kustomization.yaml │ │ └── configmap.yaml └── config │ └── config.yaml ├── go.mod ├── main.go ├── README.MD ├── .gitignore ├── api └── v1 │ └── hello.go ├── Makefile └── go.sum /utility/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/model/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resource/i18n/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/service/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resource/template/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/model/entity/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resource/public/html/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resource/public/plugin/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-language=GO -------------------------------------------------------------------------------- /internal/service/internal/dao/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/service/internal/do/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resource/public/resource/css/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resource/public/resource/js/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /resource/public/resource/image/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /internal/consts/consts.go: -------------------------------------------------------------------------------- 1 | package consts 2 | -------------------------------------------------------------------------------- /internal/packed/packed.go: -------------------------------------------------------------------------------- 1 | package packed 2 | -------------------------------------------------------------------------------- /manifest/docker/docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This shell is executed before docker build. 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gf-websocket 2 | 3 | go 1.15 4 | 5 | require ( 6 | github.com/gogf/gf/v2 v2.0.4 7 | github.com/gorilla/websocket v1.5.0 // indirect 8 | ) 9 | -------------------------------------------------------------------------------- /manifest/deploy/kustomize/base/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | resources: 4 | - deployment.yaml 5 | - service.yaml 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /manifest/config/config.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | address: ":8800" 3 | openapiPath: "/api.json" 4 | swaggerPath: "/swagger" 5 | 6 | logger: 7 | level : "all" 8 | stdout: true 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "gf-websocket/internal/packed" 5 | 6 | "github.com/gogf/gf/v2/os/gctx" 7 | 8 | "gf-websocket/internal/cmd" 9 | ) 10 | 11 | func main() { 12 | cmd.Main.Run(gctx.New()) 13 | } 14 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # GoFrame websocket 的实现 2 | 3 | ## 实现功能 4 | 5 | - 全局广播 6 | - 单个房间推送 7 | - 单个用户推送 8 | - 单个客户端推送 9 | 10 | ## 客户端发送数据格式 11 | 12 | ```json 13 | { 14 | "e": "event_name", 15 | "d": { 16 | "key": "value" 17 | } 18 | } 19 | ``` 20 | -------------------------------------------------------------------------------- /manifest/deploy/kustomize/base/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: template-single 5 | spec: 6 | ports: 7 | - port: 80 8 | protocol: TCP 9 | targetPort: 8000 10 | selector: 11 | app: template-single 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .buildpath 2 | .hgignore.swp 3 | .project 4 | .orig 5 | .swp 6 | .idea/ 7 | .settings/ 8 | .vscode/ 9 | vendor/ 10 | composer.lock 11 | gitpush.sh 12 | pkg/ 13 | bin/ 14 | cbuild 15 | **/.DS_Store 16 | .test/ 17 | main 18 | output/ 19 | manifest/output/ 20 | temp/ -------------------------------------------------------------------------------- /manifest/deploy/kustomize/overlays/develop/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: template-single 5 | spec: 6 | template: 7 | spec: 8 | containers: 9 | - name : main 10 | image: template-single:develop -------------------------------------------------------------------------------- /manifest/deploy/kustomize/overlays/develop/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - ../../base 6 | - configmap.yaml 7 | 8 | patchesStrategicMerge: 9 | - deployment.yaml 10 | 11 | namespace: default 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /api/v1/hello.go: -------------------------------------------------------------------------------- 1 | package v1 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/frame/g" 5 | ) 6 | 7 | type HelloReq struct { 8 | g.Meta `path:"/hello" tags:"Hello" method:"get" summary:"You first hello api"` 9 | } 10 | type HelloRes struct { 11 | g.Meta `mime:"text/html" example:"string"` 12 | } 13 | -------------------------------------------------------------------------------- /manifest/deploy/kustomize/overlays/develop/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: template-single-configmap 5 | data: 6 | config.yaml: | 7 | server: 8 | address: ":8000" 9 | openapiPath: "/api.json" 10 | swaggerPath: "/swagger" 11 | 12 | logger: 13 | level : "all" 14 | stdout: true 15 | -------------------------------------------------------------------------------- /internal/controller/hello.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gogf/gf/v2/frame/g" 7 | 8 | "gf-websocket/api/v1" 9 | ) 10 | 11 | var ( 12 | Hello = cHello{} 13 | ) 14 | 15 | type cHello struct{} 16 | 17 | func (c *cHello) Hello(ctx context.Context, req *v1.HelloReq) (res *v1.HelloRes, err error) { 18 | g.RequestFromCtx(ctx).Response.Writeln("Hello World!") 19 | return 20 | } 21 | -------------------------------------------------------------------------------- /manifest/deploy/kustomize/base/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: template-single 5 | labels: 6 | app: template-single 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: template-single 12 | template: 13 | metadata: 14 | labels: 15 | app: template-single 16 | spec: 17 | containers: 18 | - name : main 19 | image: template-single 20 | imagePullPolicy: Always 21 | 22 | -------------------------------------------------------------------------------- /internal/packed/websocket/model.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import "github.com/gogf/gf/v2/frame/g" 4 | 5 | // 当前输入对象 6 | type request struct { 7 | Event string `json:"e"` //事件名称 8 | Data g.Map `json:"d"` //数据 9 | } 10 | 11 | // WResponse 输出对象 12 | type WResponse struct { 13 | Event string `json:"e"` //事件名称 14 | Data interface{} `json:"d"` //数据 15 | } 16 | 17 | type TagWResponse struct { 18 | Tag string 19 | WResponse *WResponse 20 | } 21 | 22 | type UserWResponse struct { 23 | UserID uint64 24 | WResponse *WResponse 25 | } 26 | 27 | type ClientWResponse struct { 28 | ID string 29 | WResponse *WResponse 30 | } 31 | -------------------------------------------------------------------------------- /manifest/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM loads/alpine:3.8 2 | 3 | ############################################################################### 4 | # INSTALLATION 5 | ############################################################################### 6 | 7 | ENV WORKDIR /app 8 | ADD resource $WORKDIR/ 9 | ADD ./temp/linux_amd64/main $WORKDIR/main 10 | RUN chmod +x $WORKDIR/main 11 | 12 | ############################################################################### 13 | # START 14 | ############################################################################### 15 | WORKDIR $WORKDIR 16 | CMD ./main 17 | -------------------------------------------------------------------------------- /internal/cmd/cmd.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "context" 5 | "gf-websocket/internal/packed/websocket" 6 | 7 | "github.com/gogf/gf/v2/frame/g" 8 | "github.com/gogf/gf/v2/net/ghttp" 9 | "github.com/gogf/gf/v2/os/gcmd" 10 | 11 | "gf-websocket/internal/controller" 12 | ) 13 | 14 | var ( 15 | Main = gcmd.Command{ 16 | Name: "main", 17 | Usage: "main", 18 | Brief: "start http server", 19 | Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { 20 | 21 | //启动服务 22 | websocket.StartWebSocket(ctx) 23 | s := g.Server() 24 | 25 | s.Group("/", func(group *ghttp.RouterGroup) { 26 | group.Middleware(ghttp.MiddlewareHandlerResponse) 27 | group.Bind( 28 | controller.Hello, 29 | ) 30 | //注册路由 31 | group.ALL("/socket", websocket.WsPage) 32 | }) 33 | s.Run() 34 | return nil 35 | }, 36 | } 37 | ) 38 | -------------------------------------------------------------------------------- /internal/packed/websocket/router.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gogf/gf/v2/util/gconv" 6 | ) 7 | 8 | const ( 9 | Error = "error" 10 | Login = "login" 11 | Join = "join" 12 | Quit = "quit" 13 | IsApp = "is_app" 14 | Ping = "ping" 15 | ) 16 | 17 | // ProcessData 处理数据 18 | func ProcessData(client *Client, message []byte) { 19 | defer func() { 20 | if r := recover(); r != nil { 21 | fmt.Println("处理数据 stop", r) 22 | } 23 | }() 24 | request := &request{} 25 | err := gconv.Struct(message, request) 26 | if err != nil { 27 | fmt.Println("数据解析失败:", err) 28 | return 29 | } 30 | switch request.Event { 31 | case Login: 32 | LoginController(client, request) 33 | break 34 | case Join: 35 | JoinController(client, request) 36 | break 37 | case Quit: 38 | QuitController(client, request) 39 | break 40 | case IsApp: 41 | IsAppController(client) 42 | break 43 | case Ping: 44 | PingController(client) 45 | break 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/packed/websocket/init.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "context" 5 | "github.com/gogf/gf/v2/frame/g" 6 | "github.com/gogf/gf/v2/net/ghttp" 7 | "github.com/gogf/gf/v2/os/gtime" 8 | "github.com/gorilla/websocket" 9 | "net/http" 10 | ) 11 | 12 | var ( 13 | clientManager = NewClientManager() // 管理者 14 | ) 15 | var upGrader = websocket.Upgrader{ 16 | ReadBufferSize: 1024, 17 | WriteBufferSize: 1024, 18 | CheckOrigin: func(r *http.Request) bool { 19 | return true 20 | }, 21 | } 22 | 23 | func StartWebSocket(ctx context.Context) { 24 | g.Log().Info(ctx, "启动:WebSocket") 25 | go clientManager.start() 26 | go clientManager.ping(ctx) 27 | } 28 | 29 | func WsPage(r *ghttp.Request) { 30 | conn, err := upGrader.Upgrade(r.Response.ResponseWriter, r.Request, nil) 31 | if err != nil { 32 | return 33 | } 34 | currentTime := uint64(gtime.Now().Unix()) 35 | client := NewClient(conn.RemoteAddr().String(), conn, currentTime) 36 | go client.read() 37 | go client.write() 38 | // 用户连接事件 39 | clientManager.Register <- client 40 | } 41 | -------------------------------------------------------------------------------- /internal/packed/websocket/controller.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "github.com/gogf/gf/v2/os/gtime" 5 | "github.com/gogf/gf/v2/util/gconv" 6 | ) 7 | 8 | // LoginController 用户登录 9 | func LoginController(client *Client, req *request) { 10 | 11 | userId := gconv.Uint64(0) 12 | 13 | // 用户登录 14 | login := &login{ 15 | UserId: userId, 16 | Client: client, 17 | } 18 | clientManager.Login <- login 19 | 20 | client.SendMsg(&WResponse{ 21 | Event: Login, 22 | Data: "success", 23 | }) 24 | } 25 | 26 | func IsAppController(client *Client) { 27 | client.isApp = true 28 | } 29 | 30 | // JoinController 加入 31 | func JoinController(client *Client, req *request) { 32 | name := gconv.String(req.Data["name"]) 33 | 34 | if !client.tags.Contains(name) { 35 | client.tags.Append(name) 36 | } 37 | client.SendMsg(&WResponse{ 38 | Event: Join, 39 | Data: client.tags.Slice(), 40 | }) 41 | } 42 | 43 | // QuitController 退出 44 | func QuitController(client *Client, req *request) { 45 | name := gconv.String(req.Data["name"]) 46 | if client.tags.Contains(name) { 47 | client.tags.RemoveValue(name) 48 | } 49 | client.SendMsg(&WResponse{ 50 | Event: Quit, 51 | Data: client.tags.Slice(), 52 | }) 53 | } 54 | func PingController(client *Client) { 55 | currentTime := uint64(gtime.Now().Unix()) 56 | client.Heartbeat(currentTime) 57 | } 58 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ROOT_DIR = $(shell pwd) 2 | NAMESPACE = "default" 3 | DEPLOY_NAME = "template-single" 4 | DOCKER_NAME = "template-single" 5 | 6 | # Install/Update to the latest CLI tool. 7 | .PHONY: cli 8 | cli: 9 | @set -e; \ 10 | wget -O gf https://github.com/gogf/gf/releases/latest/download/gf_$(shell go env GOOS)_$(shell go env GOARCH) && \ 11 | chmod +x gf && \ 12 | ./gf install -y && \ 13 | rm ./gf 14 | 15 | 16 | # Check and install CLI tool. 17 | .PHONY: cli.install 18 | cli.install: 19 | @set -e; \ 20 | gf -v > /dev/null 2>&1 || if [[ "$?" -ne "0" ]]; then \ 21 | echo "GoFame CLI is not installed, start proceeding auto installation..."; \ 22 | make cli; \ 23 | fi; 24 | 25 | 26 | # Generate Go files for DAO/DO/Entity. 27 | .PHONY: dao 28 | dao: cli.install 29 | @gf gen dao 30 | 31 | 32 | 33 | # Build image, deploy image and yaml to current kubectl environment and make port forward to local machine. 34 | .PHONY: start 35 | start: 36 | @set -e; \ 37 | make image; \ 38 | make deploy; \ 39 | make port; 40 | 41 | # Build docker image. 42 | .PHONY: image 43 | image: cli.install 44 | $(eval _TAG = $(shell git log -1 --format="%cd.%h" --date=format:"%Y%m%d%H%M%S")) 45 | ifneq (, $(shell git status --porcelain 2>/dev/null)) 46 | $(eval _TAG = $(_TAG).dirty) 47 | endif 48 | $(eval _TAG = $(if ${TAG}, ${TAG}, $(_TAG))) 49 | $(eval _PUSH = $(if ${PUSH}, ${PUSH}, )) 50 | @gf docker -p -b "-a amd64 -s linux -p temp" -t $(DOCKER_NAME):${_TAG}; 51 | 52 | 53 | # Build docker image and automatically push to docker repo. 54 | .PHONY: image.push 55 | image.push: 56 | @make image PUSH=-p; 57 | 58 | 59 | # Deploy image and yaml to current kubectl environment. 60 | .PHONY: deploy 61 | deploy: 62 | $(eval _ENV = $(if ${ENV}, ${ENV}, develop)) 63 | 64 | @set -e; \ 65 | mkdir -p $(ROOT_DIR)/temp/kustomize;\ 66 | cd $(ROOT_DIR)/manifest/deploy/kustomize/overlays/${_ENV};\ 67 | kustomize build > $(ROOT_DIR)/temp/kustomize.yaml;\ 68 | kubectl apply -f $(ROOT_DIR)/temp/kustomize.yaml; \ 69 | kubectl patch -n $(NAMESPACE) deployment/$(DEPLOY_NAME) -p "{\"spec\":{\"template\":{\"metadata\":{\"labels\":{\"date\":\"$(shell date +%s)\"}}}}}"; 70 | 71 | 72 | -------------------------------------------------------------------------------- /internal/packed/websocket/client.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "fmt" 5 | "github.com/gogf/gf/v2/container/garray" 6 | "github.com/gogf/gf/v2/util/guid" 7 | "github.com/gorilla/websocket" 8 | "runtime/debug" 9 | ) 10 | 11 | const ( 12 | // 用户连接超时时间 13 | heartbeatExpirationTime = 6 * 60 14 | ) 15 | 16 | // 用户登录 17 | type login struct { 18 | UserId uint64 19 | Client *Client 20 | } 21 | 22 | // GetKey 读取客户端数据 23 | func (l *login) GetKey() (key string) { 24 | key = GetUserKey(l.UserId) 25 | return 26 | } 27 | 28 | // Client 客户端连接 29 | type Client struct { 30 | Addr string // 客户端地址 31 | ID string // 连接唯一标识 32 | Socket *websocket.Conn // 用户连接 33 | Send chan *WResponse // 待发送的数据 34 | SendClose bool // 发送是否关闭 35 | UserId uint64 // 用户ID,用户登录以后才有 36 | FirstTime uint64 // 首次连接事件 37 | HeartbeatTime uint64 // 用户上次心跳时间 38 | LoginTime uint64 // 登录时间 登录以后才有 39 | isApp bool // 是否是app 40 | tags garray.StrArray // 标签 41 | } 42 | 43 | // NewClient 初始化 44 | func NewClient(addr string, socket *websocket.Conn, firstTime uint64) (client *Client) { 45 | client = &Client{ 46 | Addr: addr, 47 | ID: guid.S(), 48 | Socket: socket, 49 | Send: make(chan *WResponse, 100), 50 | SendClose: false, 51 | FirstTime: firstTime, 52 | HeartbeatTime: firstTime, 53 | } 54 | return 55 | } 56 | 57 | // 读取客户端数据 58 | func (c *Client) read() { 59 | defer func() { 60 | if r := recover(); r != nil { 61 | fmt.Println("write stop", string(debug.Stack()), r) 62 | } 63 | }() 64 | 65 | defer func() { 66 | c.close() 67 | }() 68 | 69 | for { 70 | _, message, err := c.Socket.ReadMessage() 71 | if err != nil { 72 | return 73 | } 74 | // 处理程序 75 | fmt.Println(message) 76 | ProcessData(c, message) 77 | } 78 | } 79 | 80 | // 向客户端写数据 81 | func (c *Client) write() { 82 | defer func() { 83 | if r := recover(); r != nil { 84 | fmt.Println("write stop", string(debug.Stack()), r) 85 | } 86 | }() 87 | defer func() { 88 | clientManager.Unregister <- c 89 | _ = c.Socket.Close() 90 | }() 91 | for { 92 | select { 93 | case message, ok := <-c.Send: 94 | if !ok { 95 | // 发送数据错误 关闭连接 96 | return 97 | } 98 | _ = c.Socket.WriteJSON(message) 99 | } 100 | } 101 | } 102 | 103 | // SendMsg 发送数据 104 | func (c *Client) SendMsg(msg *WResponse) { 105 | if c == nil || c.SendClose { 106 | return 107 | } 108 | defer func() { 109 | if r := recover(); r != nil { 110 | fmt.Println("SendMsg stop:", r, string(debug.Stack())) 111 | } 112 | }() 113 | c.Send <- msg 114 | } 115 | 116 | // Heartbeat 心跳更新 117 | func (c *Client) Heartbeat(currentTime uint64) { 118 | c.HeartbeatTime = currentTime 119 | return 120 | } 121 | 122 | // IsHeartbeatTimeout 心跳是否超时 123 | func (c *Client) IsHeartbeatTimeout(currentTime uint64) (timeout bool) { 124 | if c.HeartbeatTime+heartbeatExpirationTime <= currentTime { 125 | timeout = true 126 | } 127 | return 128 | } 129 | 130 | // 关闭客户端 131 | func (c *Client) close() { 132 | if c.SendClose { 133 | return 134 | } 135 | c.SendClose = true 136 | close(c.Send) 137 | } 138 | -------------------------------------------------------------------------------- /internal/packed/websocket/client_manager.go: -------------------------------------------------------------------------------- 1 | package websocket 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/gogf/gf/v2/frame/g" 7 | "github.com/gogf/gf/v2/os/gcron" 8 | "github.com/gogf/gf/v2/os/gtime" 9 | "sync" 10 | ) 11 | 12 | // ClientManager 客户端管理 13 | type ClientManager struct { 14 | Clients map[*Client]bool // 全部的连接 15 | ClientsLock sync.RWMutex // 读写锁 16 | Users map[string]*Client // 登录的用户 // uuid 17 | UserLock sync.RWMutex // 读写锁 18 | Register chan *Client // 连接连接处理 19 | Login chan *login // 用户登录处理 20 | Unregister chan *Client // 断开连接处理程序 21 | Broadcast chan *WResponse // 广播 向全部成员发送数据 22 | ClientBroadcast chan *ClientWResponse // 广播 向某个客户端发送数据 23 | TagBroadcast chan *TagWResponse // 广播 向某个标签成员发送数据 24 | UserBroadcast chan *UserWResponse // 广播 向某个用户的所有链接发送数据 25 | } 26 | 27 | func NewClientManager() (clientManager *ClientManager) { 28 | clientManager = &ClientManager{ 29 | Clients: make(map[*Client]bool), 30 | Users: make(map[string]*Client), 31 | Register: make(chan *Client, 1000), 32 | Unregister: make(chan *Client, 1000), 33 | Broadcast: make(chan *WResponse, 1000), 34 | TagBroadcast: make(chan *TagWResponse, 1000), 35 | UserBroadcast: make(chan *UserWResponse, 1000), 36 | } 37 | return 38 | } 39 | 40 | // GetUserKey 获取用户key 41 | func GetUserKey(userId uint64) (key string) { 42 | key = fmt.Sprintf("%s_%d", "ws", userId) 43 | return 44 | } 45 | 46 | // InClient 客户端是否存在 47 | func (manager *ClientManager) InClient(client *Client) (ok bool) { 48 | manager.ClientsLock.RLock() 49 | defer manager.ClientsLock.RUnlock() 50 | _, ok = manager.Clients[client] 51 | return 52 | } 53 | 54 | // GetClients 获取所有客户端 55 | func (manager *ClientManager) GetClients() (clients map[*Client]bool) { 56 | clients = make(map[*Client]bool) 57 | manager.ClientsRange(func(client *Client, value bool) (result bool) { 58 | clients[client] = value 59 | return true 60 | }) 61 | return 62 | } 63 | 64 | // ClientsRange 遍历 65 | func (manager *ClientManager) ClientsRange(f func(client *Client, value bool) (result bool)) { 66 | manager.ClientsLock.RLock() 67 | defer manager.ClientsLock.RUnlock() 68 | for key, value := range manager.Clients { 69 | result := f(key, value) 70 | if result == false { 71 | return 72 | } 73 | } 74 | return 75 | } 76 | 77 | // GetClientsLen 获取客户端总数 78 | func (manager *ClientManager) GetClientsLen() (clientsLen int) { 79 | clientsLen = len(manager.Clients) 80 | return 81 | } 82 | 83 | // AddClients 添加客户端 84 | func (manager *ClientManager) AddClients(client *Client) { 85 | manager.ClientsLock.Lock() 86 | defer manager.ClientsLock.Unlock() 87 | manager.Clients[client] = true 88 | } 89 | 90 | // DelClients 删除客户端 91 | func (manager *ClientManager) DelClients(client *Client) { 92 | manager.ClientsLock.Lock() 93 | defer manager.ClientsLock.Unlock() 94 | if _, ok := manager.Clients[client]; ok { 95 | delete(manager.Clients, client) 96 | } 97 | } 98 | 99 | // GetUserClient 获取用户的连接 100 | func (manager *ClientManager) GetUserClient(userId uint64) (client *Client) { 101 | manager.UserLock.RLock() 102 | defer manager.UserLock.RUnlock() 103 | userKey := GetUserKey(userId) 104 | if value, ok := manager.Users[userKey]; ok { 105 | client = value 106 | } 107 | return 108 | } 109 | 110 | // AddUsers 添加用户 111 | func (manager *ClientManager) AddUsers(key string, client *Client) { 112 | manager.UserLock.Lock() 113 | defer manager.UserLock.Unlock() 114 | manager.Users[key] = client 115 | } 116 | 117 | // DelUsers 删除用户 118 | func (manager *ClientManager) DelUsers(client *Client) (result bool) { 119 | manager.UserLock.Lock() 120 | defer manager.UserLock.Unlock() 121 | key := GetUserKey(client.UserId) 122 | if value, ok := manager.Users[key]; ok { 123 | // 判断是否为相同的用户 124 | if value.Addr != client.Addr { 125 | return 126 | } 127 | delete(manager.Users, key) 128 | result = true 129 | } 130 | return 131 | } 132 | 133 | // GetUsersLen 已登录用户数 134 | func (manager *ClientManager) GetUsersLen() (userLen int) { 135 | userLen = len(manager.Users) 136 | return 137 | } 138 | 139 | // EventRegister 用户建立连接事件 140 | func (manager *ClientManager) EventRegister(client *Client) { 141 | manager.AddClients(client) 142 | //发送当前客户端标识 143 | client.SendMsg(&WResponse{Event: "connected", Data: g.Map{ 144 | "ID": client.ID, 145 | }}) 146 | } 147 | 148 | // EventLogin 用户登录事件 149 | func (manager *ClientManager) EventLogin(login *login) { 150 | client := login.Client 151 | if manager.InClient(client) { 152 | userKey := login.GetKey() 153 | manager.AddUsers(userKey, login.Client) 154 | } 155 | } 156 | 157 | // EventUnregister 用户断开连接事件 158 | func (manager *ClientManager) EventUnregister(client *Client) { 159 | manager.DelClients(client) 160 | // 删除用户连接 161 | deleteResult := manager.DelUsers(client) 162 | if deleteResult == false { 163 | // 不是当前连接的客户端 164 | return 165 | } 166 | // 关闭 chan 167 | // close(client.Send) 168 | } 169 | 170 | // ClearTimeoutConnections 定时清理超时连接 171 | func (manager *ClientManager) clearTimeoutConnections() { 172 | currentTime := uint64(gtime.Now().Unix()) 173 | clients := clientManager.GetClients() 174 | for client := range clients { 175 | if client.IsHeartbeatTimeout(currentTime) { 176 | //fmt.Println("心跳时间超时 关闭连接", client.Addr, client.UserId, client.LoginTime, client.HeartbeatTime) 177 | _ = client.Socket.Close() 178 | } 179 | } 180 | } 181 | 182 | // WebsocketPing 心跳处理 183 | func (manager *ClientManager) ping(ctx context.Context) { 184 | //定时任务,发送心跳包 185 | _, _ = gcron.Add(ctx, "0 */1 * * * *", func(ctx context.Context) { 186 | res := &WResponse{ 187 | Event: Ping, 188 | Data: g.Map{}, 189 | } 190 | SendToAll(res) 191 | }) 192 | // 定时任务,清理超时连接 193 | _, _ = gcron.Add(ctx, "*/30 * * * * *", func(ctx context.Context) { 194 | manager.clearTimeoutConnections() 195 | }) 196 | 197 | } 198 | 199 | // 管道处理程序 200 | func (manager *ClientManager) start() { 201 | for { 202 | select { 203 | case conn := <-manager.Register: 204 | // 建立连接事件 205 | manager.EventRegister(conn) 206 | 207 | case login := <-manager.Login: 208 | // 用户登录 209 | manager.EventLogin(login) 210 | 211 | case conn := <-manager.Unregister: 212 | // 断开连接事件 213 | manager.EventUnregister(conn) 214 | 215 | case message := <-manager.Broadcast: 216 | // 全部客户端广播事件 217 | clients := manager.GetClients() 218 | for conn := range clients { 219 | conn.SendMsg(message) 220 | } 221 | case message := <-manager.TagBroadcast: 222 | // 标签广播事件 223 | clients := manager.GetClients() 224 | for conn := range clients { 225 | if conn.tags.Contains(message.Tag) { 226 | conn.SendMsg(message.WResponse) 227 | } 228 | } 229 | case message := <-manager.UserBroadcast: 230 | // 用户广播事件 231 | clients := manager.GetClients() 232 | for conn := range clients { 233 | if conn.UserId == message.UserID { 234 | conn.SendMsg(message.WResponse) 235 | } 236 | } 237 | case message := <-manager.ClientBroadcast: 238 | // 单个客户端广播事件 239 | clients := manager.GetClients() 240 | for conn := range clients { 241 | if conn.ID == message.ID { 242 | conn.SendMsg(message.WResponse) 243 | } 244 | } 245 | } 246 | 247 | } 248 | } 249 | 250 | // SendToAll 发送全部客户端 251 | func SendToAll(response *WResponse) { 252 | clientManager.Broadcast <- response 253 | } 254 | 255 | // SendToClientID 发送单个客户端 256 | func SendToClientID(id string, response *WResponse) { 257 | clientRes := &ClientWResponse{ 258 | ID: id, 259 | WResponse: response, 260 | } 261 | clientManager.ClientBroadcast <- clientRes 262 | } 263 | 264 | // SendToUser 发送单个用户 265 | func SendToUser(userID uint64, response *WResponse) { 266 | userRes := &UserWResponse{ 267 | UserID: userID, 268 | WResponse: response, 269 | } 270 | clientManager.UserBroadcast <- userRes 271 | } 272 | 273 | // SendToTag 发送某个标签 274 | func SendToTag(tag string, response *WResponse) { 275 | tagRes := &TagWResponse{ 276 | Tag: tag, 277 | WResponse: response, 278 | } 279 | clientManager.TagBroadcast <- tagRes 280 | } 281 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.4.1 h1:GaI7EiDXDRfa8VshkTj7Fym7ha+y8/XxIgD2okUIjLw= 2 | github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= 3 | github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= 4 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/clbanning/mxj/v2 v2.5.5 h1:oT81vUeEiQQ/DcHbzSytRngP6Ky9O+L+0Bw0zSJag9E= 6 | github.com/clbanning/mxj/v2 v2.5.5/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 11 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 12 | github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= 13 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 14 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 15 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 16 | github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= 17 | github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= 18 | github.com/go-redis/redis/v8 v8.11.4 h1:kHoYkfZP6+pe04aFTnhDH6GDROa5yJdHJVNxV3F46Tg= 19 | github.com/go-redis/redis/v8 v8.11.4/go.mod h1:2Z2wHZXdQpCDXEGzqMockDpNyYvi2l4Pxt6RJr792+w= 20 | github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= 21 | github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 22 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 23 | github.com/gogf/gf/v2 v2.0.4 h1:m5F/f2lX+etXhI6rAQCR6szQoHE1ulAYlIvH0UXa2wM= 24 | github.com/gogf/gf/v2 v2.0.4/go.mod h1:apktt6TleWtCIwpz63vBqUnw8MX8gWKoZyxgDpXFtgM= 25 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 26 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 27 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 28 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 29 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 30 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 31 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 32 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 33 | github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= 34 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 35 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 36 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 37 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 38 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 39 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= 40 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 41 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 42 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 43 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 44 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 45 | github.com/grokify/html-strip-tags-go v0.0.1 h1:0fThFwLbW7P/kOiTBs03FsJSV9RM2M/Q/MOnCQxKMo0= 46 | github.com/grokify/html-strip-tags-go v0.0.1/go.mod h1:2Su6romC5/1VXOQMaWL2yb618ARB8iVo6/DR99A6d78= 47 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 48 | github.com/mattn/go-colorable v0.1.9 h1:sqDoxXbdeALODt0DAeJCVp38ps9ZogZEAXjus69YV3U= 49 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 50 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 51 | github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= 52 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 53 | github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= 54 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 55 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 56 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 57 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 58 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= 59 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= 60 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 61 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 62 | github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= 63 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 64 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 65 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 66 | github.com/onsi/gomega v1.16.0 h1:6gjqkI8iiRHMvdccRJM8rVKjCWk6ZIm6FTm3ddIe4/c= 67 | github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 68 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 69 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 70 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 71 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 72 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 73 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 74 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 75 | github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 76 | go.opentelemetry.io/otel v1.0.0 h1:qTTn6x71GVBvoafHK/yaRUmFzI4LcONZD0/kXxl5PHI= 77 | go.opentelemetry.io/otel v1.0.0/go.mod h1:AjRVh9A5/5DE7S+mZtTR6t8vpKKryam+0lREnfmS4cg= 78 | go.opentelemetry.io/otel/sdk v1.0.0 h1:BNPMYUONPNbLneMttKSjQhOTlFLOD9U22HNG1KrIN2Y= 79 | go.opentelemetry.io/otel/sdk v1.0.0/go.mod h1:PCrDHlSy5x1kjezSdL37PhbFUMjrsLRshJ2zCzeXwbM= 80 | go.opentelemetry.io/otel/trace v1.0.0 h1:TSBr8GTEtKevYMG/2d21M989r5WJYVimhTHBKVEZuh4= 81 | go.opentelemetry.io/otel/trace v1.0.0/go.mod h1:PXTWqayeFUlJV1YDNhsJYB184+IvAH814St6o6ajzIs= 82 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 83 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 84 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 85 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 86 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 87 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 88 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 89 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 90 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 91 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 92 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 93 | golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 94 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= 95 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 96 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 97 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 98 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 99 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 100 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 101 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 102 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 103 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 104 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 105 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 106 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 107 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 110 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 111 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 112 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 115 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e h1:WUoyKPm6nCo1BnNUvPGnFG3T5DUVem42yDJZZ4CNxMA= 116 | golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 117 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 118 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 119 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 120 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 121 | golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2 h1:GLw7MR8AfAG2GmGcmVgObFOHXYypgGjnGno25RDwn3Y= 122 | golang.org/x/text v0.3.8-0.20211105212822-18b340fc7af2/go.mod h1:EFNZuWvGYxIRUEX+K8UmCFwYmZjqcrnq15ZuVldZkZ0= 123 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 124 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 125 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 126 | golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= 127 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 128 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 129 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 130 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 131 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 132 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 133 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 134 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 135 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 136 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 137 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 138 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 139 | google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= 140 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 141 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 142 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 143 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 144 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 145 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 146 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 147 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 148 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 149 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 150 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 151 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 152 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 153 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 154 | --------------------------------------------------------------------------------