diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..a662e45
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,11 @@
+木兰宽松许可证
+
+Copyright (c) [2019] [link1st]
+[gowebsocket] is licensed under the Mulan PSL v1.
+You can use this software according to the terms and conditions of the Mulan PSL v1.
+You may obtain a copy of Mulan PSL v1 at:
+ http://license.coscl.org.cn/MulanPSL
+THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR
+PURPOSE.
+See the Mulan PSL v1 for more details.
\ No newline at end of file
diff --git a/README.md b/README.md
index 207b319..77190aa 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,1230 @@
-# online_learning
+# 基于websocket单台机器支持百万连接分布式聊天(IM)系统
+
+
+本文将介绍如何实现一个基于websocket分布式聊天(IM)系统。
+
+使用golang实现websocket通讯,单机可以支持百万连接,使用gin框架、nginx负载、可以水平部署、程序内部相互通讯、使用grpc通讯协议。
+
+本文内容比较长,如果直接想clone项目体验直接进入[项目体验](#12-项目体验) [goWebSocket项目下载](#4goWebSocket-项目) ,文本从介绍webSocket是什么开始,然后开始介绍这个项目,以及在Nginx中配置域名做webSocket的转发,然后介绍如何搭建一个分布式系统。
+
+
+## 目录
+- [1、项目说明](#1项目说明)
+ - [1.1 goWebSocket](#11-goWebSocket)
+ - [1.2 项目体验](#12-项目体验)
+- [2、介绍webSocket](#2介绍webSocket)
+ - [2.1 webSocket 是什么](#21-webSocket-是什么)
+ - [2.2 webSocket的兼容性](#22-webSocket的兼容性)
+ - [2.3 为什么要用webSocket](#23-为什么要用webSocket)
+ - [2.4 webSocket建立过程](#24-webSocket建立过程)
+- [3、如何实现基于webSocket的长连接系统](#3如何实现基于webSocket的长连接系统)
+ - [3.1 使用go实现webSocket服务端](#31-使用go实现webSocket服务端)
+ - [3.1.1 启动端口监听](#311-启动端口监听)
+ - [3.1.2 升级协议](#312-升级协议)
+ - [3.1.3 客户端连接的管理](#313-客户端连接的管理)
+ - [3.1.4 注册客户端的socket的写的异步处理程序](#314-注册客户端的socket的写的异步处理程序)
+ - [3.1.5 注册客户端的socket的读的异步处理程序](#315-注册客户端的socket的读的异步处理程序)
+ - [3.1.6 接收客户端数据并处理](#316-接收客户端数据并处理)
+ - [3.1.7 使用路由的方式处理客户端的请求数据](#317-使用路由的方式处理客户端的请求数据)
+ - [3.1.8 防止内存溢出和Goroutine不回收](#318-防止内存溢出和Goroutine不回收)
+ - [3.2 使用javaScript实现webSocket客户端](#32-使用javaScript实现webSocket客户端)
+ - [3.2.1 启动并注册监听程序](#321-启动并注册监听程序)
+ - [3.2.2 发送数据](#322-发送数据)
+ - [3.3 发送消息](#33-发送消息)
+ - [3.3.1 文本消息](#331-文本消息)
+ - [3.3.2 图片和语言消息](#332-图片和语言消息)
+- [4、goWebSocket 项目](#4goWebSocket-项目)
+ - [4.1 项目说明](#41-项目说明)
+ - [4.2 项目依赖](#42-项目依赖)
+ - [4.3 项目启动](#43-项目启动)
+ - [4.4 接口文档](#44-接口文档)
+ - [4.4.1 HTTP接口文档](#441-HTTP接口文档)
+ - [4.4.1.1 接口说明](#4411-接口说明)
+ - [4.4.1.2 聊天页面](#4412-聊天页面)
+ - [4.4.1.3 获取房间用户列表](#4413-获取房间用户列表)
+ - [4.4.1.4 查询用户是否在线](#4414-查询用户是否在线)
+ - [4.4.1.5 给用户发送消息](#4415-给用户发送消息)
+ - [4.4.1.6 给全员用户发送消息](#4416-给全员用户发送消息)
+ - [4.4.2 RPC接口文档](#442-RPC接口文档)
+ - [4.4.2.1 接口说明](#4421-接口说明)
+ - [4.4.2.2 查询用户是否在线](#4422-查询用户是否在线)
+ - [4.4.2.3 发送消息](#4423-发送消息)
+ - [4.4.2.4 给指定房间所有用户发送消息](#4424-给指定房间所有用户发送消息)
+ - [4.4.2.5 获取房间内全部用户](#4425-获取房间内全部用户)
+- [5、webSocket项目Nginx配置](#5webSocket项目Nginx配置)
+ - [5.1 为什么要配置Nginx](#51-为什么要配置Nginx)
+ - [5.2 nginx配置](#52-nginx配置)
+ - [5.3 问题处理](#53-问题处理)
+- [6、压测](#6压测)
+ - [6.1 Linux内核优化](#61-Linux内核优化)
+ - [6.2 压测准备](#62-压测准备)
+ - [6.3 压测数据](#63-压测数据)
+- [7、如何基于webSocket实现一个分布式Im](#7如何基于webSocket实现一个分布式Im)
+ - [7.1 说明](#71-说明)
+ - [7.2 架构](#72-架构)
+ - [7.3 分布式系统部署](#73-分布式系统部署)
+- [8、回顾和反思](#8回顾和反思)
+ - [8.1 在其它系统应用](#81-在其它系统应用)
+ - [8.2 需要完善、优化](#82-需要完善优化)
+ - [8.3 总结](#83-总结)
+- [9、参考文献](#9参考文献)
+
+
+## 1、项目说明
+#### 1.1 goWebSocket
+
+本文将介绍如何实现一个基于websocket聊天(IM)分布式系统。
+
+使用golang实现websocket通讯,单机支持百万连接,使用gin框架、nginx负载、可以水平部署、程序内部相互通讯、使用grpc通讯协议。
+
+- 一般项目中webSocket使用的架构图
+![网站架构图](https://img.mukewang.com/5d4e510000018ff510320842.png)
+
+#### 1.2 项目体验
+- [项目地址 gowebsocket](https://github.com/link1st/gowebsocket)
+- [IM-聊天首页](http://im.91vh.com/home/index) 或者在新的窗口打开 http://im.91vh.com/home/index
+- 打开连接以后进入聊天界面
+- 多人群聊可以同时打开两个窗口
+
+## 2、介绍webSocket
+### 2.1 webSocket 是什么
+WebSocket 协议在2008年诞生,2011年成为国际标准。所有浏览器都已经支持了。
+
+它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
+
+- HTTP和WebSocket在通讯过程的比较
+![HTTP协议和WebSocket比较](https://img.mukewang.com/5d4cf0750001bc4706280511.png)
+
+- HTTP和webSocket都支持配置证书,`ws://` 无证书 `wss://` 配置证书的协议标识
+![HTTP协议和WebSocket比较](https://img.mukewang.com/5d4cf1180001493404180312.jpg)
+
+### 2.2 webSocket的兼容性
+- 浏览器的兼容性,开始支持webSocket的版本
+
+![浏览器开始支持webSocket的版本](https://img.mukewang.com/5d4cf2170001859e12190325.jpg)
+
+- 服务端的支持
+
+golang、java、php、node.js、python、nginx 都有不错的支持
+
+- Android和IOS的支持
+
+Android可以使用java-webSocket对webSocket支持
+
+iOS 4.2及更高版本具有WebSockets支持
+
+### 2.3 为什么要用webSocket
+- 1. 从业务上出发,需要一个主动通达客户端的能力
+> 目前大多数的请求都是使用HTTP,都是由客户端发起一个请求,有服务端处理,然后返回结果,不可以服务端主动向某一个客户端主动发送数据
+
+![服务端处理一个请求](https://img.mukewang.com/5d4cf5650001773612800720.jpg)
+- 2. 大多数场景我们需要主动通知用户,如:聊天系统、用户完成任务主动告诉用户、一些运营活动需要通知到在线的用户
+- 3. 可以获取用户在线状态
+- 4. 在没有长连接的时候通过客户端主动轮询获取数据
+- 5. 可以通过一种方式实现,多种不同平台(H5/Android/IOS)去使用
+
+### 2.4 webSocket建立过程
+- 1. 客户端先发起升级协议的请求
+
+客户端发起升级协议的请求,采用标准的HTTP报文格式,在报文中添加头部信息
+
+`Connection: Upgrade`表明连接需要升级
+
+`Upgrade: websocket`需要升级到 websocket协议
+
+`Sec-WebSocket-Version: 13` 协议的版本为13
+
+`Sec-WebSocket-Key: I6qjdEaqYljv3+9x+GrhqA==` 这个是base64 encode 的值,是浏览器随机生成的,与服务器响应的 `Sec-WebSocket-Accept`对应
+
+```
+# Request Headers
+Connection: Upgrade
+Host: im.91vh.com
+Origin: http://im.91vh.com
+Pragma: no-cache
+Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
+Sec-WebSocket-Key: I6qjdEaqYljv3+9x+GrhqA==
+Sec-WebSocket-Version: 13
+Upgrade: websocket
+```
+
+![浏览器 Network](https://img.mukewang.com/5d4d2336000197a315881044.png)
+
+- 2. 服务器响应升级协议
+
+服务端接收到升级协议的请求,如果服务端支持升级协议会做如下响应
+
+返回:
+
+`Status Code: 101 Switching Protocols` 表示支持切换协议
+
+```
+# Response Headers
+Connection: upgrade
+Date: Fri, 09 Aug 2019 07:36:59 GMT
+Sec-WebSocket-Accept: mB5emvxi2jwTUhDdlRtADuBax9E=
+Server: nginx/1.12.1
+Upgrade: websocket
+```
+
+- 3. 升级协议完成以后,客户端和服务器就可以相互发送数据
+
+![websocket接收和发送数据](https://img.mukewang.com/5d4d23a50001fd6a15800716.png)
+
+## 3、如何实现基于webSocket的长连接系统
+
+### 3.1 使用go实现webSocket服务端
+
+#### 3.1.1 启动端口监听
+- websocket需要监听端口,所以需要在`golang` 成功的 `main` 函数中用协程的方式去启动程序
+- **main.go** 实现启动
+
+```
+go websocket.StartWebSocket()
+```
+- **init_acc.go** 启动程序
+
+```
+// 启动程序
+func StartWebSocket() {
+ http.HandleFunc("/acc", wsPage)
+ http.ListenAndServe(":8089", nil)
+}
+```
+
+#### 3.1.2 升级协议
+- 客户端是通过http请求发送到服务端,我们需要对http协议进行升级为websocket协议
+- 对http请求协议进行升级 golang 库[gorilla/websocket](https://github.com/gorilla/websocket) 已经做得很好了,我们直接使用就可以了
+- 在实际使用的时候,建议每个连接使用两个协程处理客户端请求数据和向客户端发送数据,虽然开启协程会占用一些内存,但是读取分离,减少收发数据堵塞的可能
+- **init_acc.go**
+
+```
+func wsPage(w http.ResponseWriter, req *http.Request) {
+
+ // 升级协议
+ conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
+ fmt.Println("升级协议", "ua:", r.Header["User-Agent"], "referer:", r.Header["Referer"])
+
+ return true
+ }}).Upgrade(w, req, nil)
+ if err != nil {
+ http.NotFound(w, req)
+
+ return
+ }
+
+ fmt.Println("webSocket 建立连接:", conn.RemoteAddr().String())
+
+ currentTime := uint64(time.Now().Unix())
+ client := NewClient(conn.RemoteAddr().String(), conn, currentTime)
+
+ go client.read()
+ go client.write()
+
+ // 用户连接事件
+ clientManager.Register <- client
+}
+```
+
+#### 3.1.3 客户端连接的管理
+- 当前程序有多少用户连接,还需要对用户广播的需要,这里我们就需要一个管理者(clientManager),处理这些事件:
+- 记录全部的连接、登录用户的可以通过 **appId+uuid** 查到用户连接
+- 使用map存储,就涉及到多协程并发读写的问题,所以需要加读写锁
+- 定义四个channel ,分别处理客户端建立连接、用户登录、断开连接、全员广播事件
+
+```
+// 连接管理
+type ClientManager struct {
+ Clients map[*Client]bool // 全部的连接
+ ClientsLock sync.RWMutex // 读写锁
+ Users map[string]*Client // 登录的用户 // appId+uuid
+ UserLock sync.RWMutex // 读写锁
+ Register chan *Client // 连接连接处理
+ Login chan *login // 用户登录处理
+ Unregister chan *Client // 断开连接处理程序
+ Broadcast chan []byte // 广播 向全部成员发送数据
+}
+
+// 初始化
+func NewClientManager() (clientManager *ClientManager) {
+ clientManager = &ClientManager{
+ Clients: make(map[*Client]bool),
+ Users: make(map[string]*Client),
+ Register: make(chan *Client, 1000),
+ Login: make(chan *login, 1000),
+ Unregister: make(chan *Client, 1000),
+ Broadcast: make(chan []byte, 1000),
+ }
+
+ return
+}
+```
+
+#### 3.1.4 注册客户端的socket的写的异步处理程序
+- 防止发生程序崩溃,所以需要捕获异常
+- 为了显示异常崩溃位置这里使用`string(debug.Stack())`打印调用堆栈信息
+- 如果写入数据失败了,可能连接有问题,就关闭连接
+- **client.go**
+
+```
+// 向客户端写数据
+func (c *Client) write() {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("write stop", string(debug.Stack()), r)
+
+ }
+ }()
+
+ defer func() {
+ clientManager.Unregister <- c
+ c.Socket.Close()
+ fmt.Println("Client发送数据 defer", c)
+ }()
+
+ for {
+ select {
+ case message, ok := <-c.Send:
+ if !ok {
+ // 发送数据错误 关闭连接
+ fmt.Println("Client发送数据 关闭连接", c.Addr, "ok", ok)
+
+ return
+ }
+
+ c.Socket.WriteMessage(websocket.TextMessage, message)
+ }
+ }
+}
+```
+
+#### 3.1.5 注册客户端的socket的读的异步处理程序
+- 循环读取客户端发送的数据并处理
+- 如果读取数据失败了,关闭channel
+- **client.go**
+
+```
+// 读取客户端数据
+func (c *Client) read() {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("write stop", string(debug.Stack()), r)
+ }
+ }()
+
+ defer func() {
+ fmt.Println("读取客户端数据 关闭send", c)
+ close(c.Send)
+ }()
+
+ for {
+ _, message, err := c.Socket.ReadMessage()
+ if err != nil {
+ fmt.Println("读取客户端数据 错误", c.Addr, err)
+
+ return
+ }
+
+ // 处理程序
+ fmt.Println("读取客户端数据 处理:", string(message))
+ ProcessData(c, message)
+ }
+}
+```
+
+#### 3.1.6 接收客户端数据并处理
+- 约定发送和接收请求数据格式,为了js处理方便,采用了`json`的数据格式发送和接收数据(人类可以阅读的格式在工作开发中使用是比较方便的)
+
+- 登录发送数据示例:
+```
+{"seq":"1565336219141-266129","cmd":"login","data":{"userId":"马远","appId":101}}
+```
+- 登录响应数据示例:
+```
+{"seq":"1565336219141-266129","cmd":"login","response":{"code":200,"codeMsg":"Success","data":null}}
+```
+- websocket是双向的数据通讯,可以连续发送,如果发送的数据需要服务端回复,就需要一个**seq**来确定服务端的响应是回复哪一次的请求数据
+- cmd 是用来确定动作,websocket没有类似于http的url,所以规定 cmd 是什么动作
+- 目前的动作有:login/heartbeat 用来发送登录请求和连接保活(长时间没有数据发送的长连接容易被浏览器、移动中间商、nginx、服务端程序断开)
+- 为什么需要AppId,UserId是表示用户的唯一字段,设计的时候为了做成通用性,设计AppId用来表示用户在哪个平台登录的(web、app、ios等),方便后续扩展
+
+- **request_model.go** 约定的请求数据格式
+
+```
+/************************ 请求数据 **************************/
+// 通用请求数据格式
+type Request struct {
+ Seq string `json:"seq"` // 消息的唯一Id
+ Cmd string `json:"cmd"` // 请求命令字
+ Data interface{} `json:"data,omitempty"` // 数据 json
+}
+
+// 登录请求数据
+type Login struct {
+ ServiceToken string `json:"serviceToken"` // 验证用户是否登录
+ AppId uint32 `json:"appId,omitempty"`
+ UserId string `json:"userId,omitempty"`
+}
+
+// 心跳请求数据
+type HeartBeat struct {
+ UserId string `json:"userId,omitempty"`
+}
+```
+
+- **response_model.go**
+
+```
+/************************ 响应数据 **************************/
+type Head struct {
+ Seq string `json:"seq"` // 消息的Id
+ Cmd string `json:"cmd"` // 消息的cmd 动作
+ Response *Response `json:"response"` // 消息体
+}
+
+type Response struct {
+ Code uint32 `json:"code"`
+ CodeMsg string `json:"codeMsg"`
+ Data interface{} `json:"data"` // 数据 json
+}
+
+```
+
+
+#### 3.1.7 使用路由的方式处理客户端的请求数据
+
+- 使用路由的方式处理由客户端发送过来的请求数据
+- 以后添加请求类型以后就可以用类是用http相类似的方式(router-controller)去处理
+- **acc_routers.go**
+
+```
+// Websocket 路由
+func WebsocketInit() {
+ websocket.Register("login", websocket.LoginController)
+ websocket.Register("heartbeat", websocket.HeartbeatController)
+}
+```
+
+#### 3.1.8 防止内存溢出和Goroutine不回收
+- 1. 定时任务清除超时连接
+没有登录的连接和登录的连接6分钟没有心跳则断开连接
+
+**client_manager.go**
+
+```
+// 定时清理超时连接
+func ClearTimeoutConnections() {
+ currentTime := uint64(time.Now().Unix())
+
+ for client := range clientManager.Clients {
+ if client.IsHeartbeatTimeout(currentTime) {
+ fmt.Println("心跳时间超时 关闭连接", client.Addr, client.UserId, client.LoginTime, client.HeartbeatTime)
+
+ client.Socket.Close()
+ }
+ }
+}
+```
+
+- 2. 读写的Goroutine有一个失败,则相互关闭
+`write()`Goroutine写入数据失败,关闭`c.Socket.Close()`连接,会关闭`read()`Goroutine
+`read()`Goroutine读取数据失败,关闭`close(c.Send)`连接,会关闭`write()`Goroutine
+
+- 3. 客户端主动关闭
+关闭读写的Goroutine
+从`ClientManager`删除连接
+
+- 4. 监控用户连接、Goroutine数
+十个内存溢出有九个和Goroutine有关
+添加一个http的接口,可以查看系统的状态,防止Goroutine不回收
+[查看系统状态](http://im.91vh.com/system/state?isDebug=true)
+
+- 5. Nginx 配置不活跃的连接释放时间,防止忘记关闭的连接
+
+- 6. 使用 pprof 分析性能、耗时
+
+### 3.2 使用javaScript实现webSocket客户端
+#### 3.2.1 启动并注册监听程序
+- js 建立连接,并处理连接成功、收到数据、断开连接的事件处理
+
+```
+ws = new WebSocket("ws://127.0.0.1:8089/acc");
+
+
+ws.onopen = function(evt) {
+ console.log("Connection open ...");
+};
+
+ws.onmessage = function(evt) {
+ console.log( "Received Message: " + evt.data);
+ data_array = JSON.parse(evt.data);
+ console.log( data_array);
+};
+
+ws.onclose = function(evt) {
+ console.log("Connection closed.");
+};
+
+```
+
+
+#### 3.2.2 发送数据
+- 需要注意:连接建立成功以后才可以发送数据
+- 建立连接以后由客户端向服务器发送数据示例
+
+```
+登录:
+ws.send('{"seq":"2323","cmd":"login","data":{"userId":"11","appId":101}}');
+
+心跳:
+ws.send('{"seq":"2324","cmd":"heartbeat","data":{}}');
+
+ping 查看服务是否正常:
+ws.send('{"seq":"2325","cmd":"ping","data":{}}');
+
+关闭连接:
+ws.close();
+```
+
+## 3.3 发送消息
+### 3.3.1 文本消息
+
+客户端只要知道发送用户是谁,还有内容就可以显示文本消息,这里我们重点关注一下数据部分
+
+target:定义接收的目标,目前未设置
+
+type:消息的类型,text 文本消息 img 图片消息
+
+msg:文本消息内容
+
+from:消息的发送者
+
+文本消息的结构:
+
+```json
+{
+ "seq": "1569080188418-747717",
+ "cmd": "msg",
+ "response": {
+ "code": 200,
+ "codeMsg": "Ok",
+ "data": {
+ "target": "",
+ "type": "text",
+ "msg": "hello",
+ "from": "马超"
+ }
+ }
+}
+```
+
+这样一个文本消息的结构就设计完成了,客户端在接收到消息内容就可以展现到 IM 界面上
+
+### 3.3.2 图片和语言消息
+
+发送图片消息,发送消息者的客户端需要先把图片上传到文件服务器,上传成功以后获得图片访问的 URL,然后由发送消息者的客户端需要将图片 URL 发送到 gowebsocket,gowebsocket 图片的消息格式发送给目标客户端,消息接收者客户端接收到图片的 URL 就可以显示图片消息。
+
+图片消息的结构:
+
+```
+{
+ "type": "img",
+ "from": "马超",
+ "url": "http://91vh.com/images/home_logo.png",
+ "secret": "消息鉴权 secret",
+ "size": {
+ "width": 480,
+ "height": 720
+ }
+}
+```
+
+语言消息、和视频消息和图片消息类似,都是先把文件上传服务器,然后通过 gowebsocket 传递文件的 URL,需要注意的是部分消息涉及到隐私的文件,文件访问的时候需要做好鉴权信息,不能让非接收用户也能查看到别人的消息内容。
+
+## 4、goWebSocket 项目
+### 4.1 项目说明
+- 本项目是基于webSocket实现的分布式IM系统
+- 客户端随机分配用户名,所有人进入一个聊天室,实现群聊的功能
+- 单台机器(24核128G内存)支持百万客户端连接
+- 支持水平部署,部署的机器之间可以相互通讯
+
+- 项目架构图
+![网站架构图](https://img.mukewang.com/5d4e510000018ff510320842.png)
+
+### 4.2 项目依赖
+
+- 本项目只需要使用 redis 和 golang
+- 本项目使用govendor管理依赖,克隆本项目就可以直接使用
+
+```
+# 主要使用到的包
+github.com/gin-gonic/gin@v1.4.0
+github.com/go-redis/redis
+github.com/gorilla/websocket
+github.com/spf13/viper
+google.golang.org/grpc
+github.com/golang/protobuf
+```
+
+
+### 4.3 项目启动
+- 克隆项目
+
+```
+git clone git@github.com:link1st/gowebsocket.git
+# 或
+git clone https://github.com/link1st/gowebsocket.git
+```
+- 修改项目配置
+
+```
+cd gowebsocket
+cd config
+mv app.yaml.example app.yaml
+# 修改项目监听端口,redis连接等(默认127.0.0.1:3306)
+vim app.yaml
+# 返回项目目录,为以后启动做准备
+cd ..
+```
+- 配置文件说明
+
+```
+app:
+ logFile: log/gin.log # 日志文件位置
+ httpPort: 8080 # http端口
+ webSocketPort: 8089 # webSocket端口
+ rpcPort: 9001 # 分布式部署程序内部通讯端口
+ httpUrl: 127.0.0.1:8080
+ webSocketUrl: 127.0.0.1:8089
+
+
+redis:
+ addr: "localhost:6379"
+ password: ""
+ DB: 0
+ poolSize: 30
+ minIdleConns: 30
+```
+
+- 启动项目
+
+```
+go run main.go
+```
+
+- 进入IM聊天地址
+[http://127.0.0.1:8080/home/index](http://127.0.0.1:8080/home/index)
+- 到这里,就可以体验到基于webSocket的IM系统
+
+#### 4.4 接口文档
+###### 4.4.1.1 接口说明
+##### 4.4.1 HTTP接口文档
+- 在接口开发和接口文档使用的过程中,规范开发流程,减少沟通成本,所以约定一下接口开发流程和文档说明
+- 接口地址
+
+ 线上:http://im.91vh.com
+
+ 测试:http://im.91vh.com
+
+
+###### 4.4.1.2 聊天页面
+- 地址:/home/index
+- 请求方式:GET
+- 接口说明:聊天页面
+- 请求参数:
+
+| 参数 | 必填 | 类型 | 说明 | 示例 |
+| :----: | :----: | :----: | :----: | :----: |
+| appId | 是 | uint32 | appId/房间Id | 101 |
+
+- 返回参数:
+无
+
+
+###### 4.4.1.3 获取房间用户列表
+- 地址:/user/list
+- 请求方式:GET/POST
+- 接口说明:获取房间用户列表
+- 请求参数:
+
+| 参数 | 必填 | 类型 | 说明 | 示例 |
+| :----: | :----: | :----: | :----: | :----: |
+| appId | 是 | uint32 | appId/房间Id | 101 |
+
+- 返回参数:
+
+| 参数 | 必填 | 类型 | 说明 | 示例 |
+| :----: | :----: | :----: | :----: | :----: |
+| code | 是 | int | 错误码 | 200 |
+| msg | 是 | string| 错误信息 |Success |
+| data | 是 | array | 返回数据 | |
+| userCount | 是 | int | 房间内用户总数 | 1 |
+| userList| 是 | list | 用户列表 | |
+
+- 示例:
+
+```json
+{
+ "code": 200,
+ "msg": "Success",
+ "data": {
+ "userCount": 1,
+ "userList": [
+ "黄帝"
+ ]
+ }
+}
+```
+
+###### 4.4.1.4 查询用户是否在线
+- 地址:/user/online
+- 请求方式:GET/POST
+- 接口说明:查询用户是否在线
+- 请求参数:
+
+| 参数 | 必填 | 类型 | 说明 | 示例 |
+| :----: | :----: | :----: | :----: | :----: |
+| appId | 是 | uint32 | appId/房间Id | 101 |
+| userId | 是 | string | 用户Id | 黄帝 |
+
+- 返回参数:
+
+| 参数 | 必填 | 类型 | 说明 | 示例 |
+| :----: | :----: | :----: | :----: | :----: |
+| code | 是 | int | 错误码 | 200 |
+| msg | 是 | string| 错误信息 |Success |
+| data | 是 | array | 返回数据 | |
+| online | 是 | bool | 发送结果 true:在线 false:不在线 | true |
+| userId | 是 | string | 用户Id | 黄帝 |
+
+- 示例:
+
+```json
+{
+ "code": 200,
+ "msg": "Success",
+ "data": {
+ "online": true,
+ "userId": "黄帝"
+ }
+}
+```
+
+###### 4.4.1.5 给用户发送消息
+- 地址:/user/sendMessage
+- 请求方式:GET/POST
+- 接口说明:给用户发送消息
+- 请求参数:
+
+| 参数 | 必填 | 类型 | 说明 | 示例 |
+| :----: | :----: | :----: | :----: | :----: |
+| appId | 是 | uint32 | appId/房间Id | 101 |
+| userId | 是 | string | 用户id | 黄帝 |
+| message | 是 | string | 消息内容 | hello |
+
+- 返回参数:
+
+| 参数 | 必填 | 类型 | 说明 | 示例 |
+| :----: | :----: | :----: | :----: | :----: |
+| code | 是 | int | 错误码 | 200 |
+| msg | 是 | string| 错误信息 |Success |
+| data | 是 | array | 返回数据 | |
+| sendResults | 是 | bool | 发送结果 true:成功 false:失败 | true |
+
+- 示例:
+
+```json
+{
+ "code": 200,
+ "msg": "Success",
+ "data": {
+ "sendResults": true
+ }
+}
+```
+
+###### 4.4.1.6 给全员用户发送消息
+- 地址:/user/sendMessageAll
+- 请求方式:GET/POST
+- 接口说明:给全员用户发送消息
+- 请求参数:
+
+| 参数 | 必填 | 类型 | 说明 | 示例 |
+| :----: | :----: | :----: | :----: | :----: |
+| appId | 是 | uint32 | appId/房间Id | 101 |
+| userId | 是 | string | 用户id | 黄帝 |
+| msgId | 是 | string | 消息Id | 避免重复发送 |
+| message | 是 | string | 消息内容 | hello |
+
+- 返回参数:
+
+| 参数 | 必填 | 类型 | 说明 | 示例 |
+| :----: | :----: | :----: | :----: | :----: |
+| code | 是 | int | 错误码 | 200 |
+| msg | 是 | string| 错误信息 |Success |
+| data | 是 | array | 返回数据 | |
+| sendResults | 是 | bool | 发送结果 true:成功 false:失败 | true |
+
+- 示例:
+
+```json
+{
+ "code": 200,
+ "msg": "Success",
+ "data": {
+ "sendResults": true
+ }
+}
+```
+
+##### 4.4.2 RPC接口文档
+###### 4.4.2.1 接口说明
+- 接口协议结构体
+```proto
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "io.grpc.examples.protobuf";
+option java_outer_classname = "ProtobufProto";
+
+
+package protobuf;
+
+// The AccServer service definition.
+service AccServer {
+ // 查询用户是否在线
+ rpc QueryUsersOnline (QueryUsersOnlineReq) returns (QueryUsersOnlineRsp) {
+ }
+ // 发送消息
+ rpc SendMsg (SendMsgReq) returns (SendMsgRsp) {
+ }
+ // 给这台机器的房间内所有用户发送消息
+ rpc SendMsgAll (SendMsgAllReq) returns (SendMsgAllRsp) {
+ }
+ // 获取用户列表
+ rpc GetUserList (GetUserListReq) returns (GetUserListRsp) {
+ }
+}
+
+// 查询用户是否在线
+message QueryUsersOnlineReq {
+ uint32 appId = 1; // AppID
+ string userId = 2; // 用户ID
+}
+
+message QueryUsersOnlineRsp {
+ uint32 retCode = 1;
+ string errMsg = 2;
+ bool online = 3;
+}
+
+// 发送消息
+message SendMsgReq {
+ string seq = 1; // 序列号
+ uint32 appId = 2; // appId/房间Id
+ string userId = 3; // 用户ID
+ string cms = 4; // cms 动作: msg/enter/exit
+ string type = 5; // type 消息类型,默认是 text
+ string msg = 6; // msg
+ bool isLocal = 7; // 是否查询本机 acc内部调用为:true(本机查询不到即结束)
+}
+
+message SendMsgRsp {
+ uint32 retCode = 1;
+ string errMsg = 2;
+ string sendMsgId = 3;
+}
+
+// 给这台机器的房间内所有用户发送消息
+message SendMsgAllReq {
+ string seq = 1; // 序列号
+ uint32 appId = 2; // appId/房间Id
+ string userId = 3; // 不发送的用户ID
+ string cms = 4; // cms 动作: msg/enter/exit
+ string type = 5; // type 消息类型,默认是 text
+ string msg = 6; // msg
+}
+
+message SendMsgAllRsp {
+ uint32 retCode = 1;
+ string errMsg = 2;
+ string sendMsgId = 3;
+}
+
+// 获取用户列表
+message GetUserListReq {
+ uint32 appId = 1;
+}
+
+message GetUserListRsp {
+ uint32 retCode = 1;
+ string errMsg = 2;
+ repeated string userId = 3;
+}
+```
+
+###### 4.4.2.2 查询用户是否在线
+- 参考上述协议结构体
+
+###### 4.4.2.3 发送消息
+###### 4.4.2.4 给指定房间所有用户发送消息
+###### 4.4.2.5 获取房间内全部用户
+
+## 5、webSocket项目Nginx配置
+### 5.1 为什么要配置Nginx
+- 使用nginx实现内外网分离,对外只暴露Nginx的Ip(一般的互联网企业会在nginx之前加一层LVS做负载均衡),减少入侵的可能
+- 支持配置 ssl 证书,使用 `wss` 的方式实现数据加密,减少数据被抓包和篡改的可能性
+- 使用Nginx可以利用Nginx的负载功能,前端再使用的时候只需要连接固定的域名,通过Nginx将流量分发了到不同的机器
+- 同时我们也可以使用Nginx的不同的负载策略(轮询、weight、ip_hash)
+
+### 5.2 nginx配置
+- 使用域名 **im.91vh.com** 为示例,参考配置
+- 一级目录**im.91vh.com/acc** 是给webSocket使用,是用nginx stream转发功能(nginx 1.3.31 开始支持,使用Tengine配置也是相同的),转发到golang 8089 端口处理
+- 其它目录是给HTTP使用,转发到golang 8080 端口处理
+
+```
+upstream go-im
+{
+ server 127.0.0.1:8080 weight=1 max_fails=2 fail_timeout=10s;
+ keepalive 16;
+}
+
+upstream go-acc
+{
+ server 127.0.0.1:8089 weight=1 max_fails=2 fail_timeout=10s;
+ keepalive 16;
+}
+
+
+server {
+ listen 80 ;
+ server_name im.91vh.com;
+ index index.html index.htm ;
+
+
+ location /acc {
+ proxy_set_header Host $host;
+ proxy_pass http://go-acc;
+ proxy_http_version 1.1;
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+ proxy_set_header Connection "";
+ proxy_redirect off;
+ proxy_intercept_errors on;
+ client_max_body_size 10m;
+ }
+
+ location /
+ {
+ proxy_set_header Host $host;
+ proxy_pass http://go-im;
+ proxy_http_version 1.1;
+ proxy_set_header Connection "";
+ proxy_redirect off;
+ proxy_intercept_errors on;
+ client_max_body_size 30m;
+ }
+
+ access_log /link/log/nginx/access/im.log;
+ error_log /link/log/nginx/access/im.error.log;
+}
+```
+
+### 5.3 问题处理
+- 运行nginx测试命令,查看配置文件是否正确
+
+```
+/link/server/tengine/sbin/nginx -t
+
+```
+
+- 如果出现错误
+
+```
+nginx: [emerg] unknown "connection_upgrade" variable
+configuration file /link/server/tengine/conf/nginx.conf test failed
+```
+
+- 处理方法
+- 在**nginx.com**添加
+
+```
+http{
+ fastcgi_temp_file_write_size 128k;
+..... # 需要添加的内容
+
+ #support websocket
+ map $http_upgrade $connection_upgrade {
+ default upgrade;
+ '' close;
+ }
+
+.....
+ gzip on;
+
+}
+
+```
+
+- 原因:Nginx代理webSocket的时候就会遇到Nginx的设计问题 **End-to-end and Hop-by-hop Headers**
+
+
+## 6、压测
+### 6.1 Linux内核优化
+- 设置文件打开句柄数
+
+被压测服务器需要保持100W长连接,客户和服务器端是通过socket通讯的,每个连接需要建立一个socket,程序需要保持100W长连接就需要单个程序能打开100W个文件句柄
+
+```
+# 查看系统默认的值
+ulimit -n
+# 设置最大打开文件数
+ulimit -n 1000000
+```
+
+通过修改配置文件的方式修改程序最大打开句柄数
+
+```
+root soft nofile 1040000
+root hard nofile 1040000
+
+root soft nofile 1040000
+root hard nproc 1040000
+
+root soft core unlimited
+root hard core unlimited
+
+* soft nofile 1040000
+* hard nofile 1040000
+
+* soft nofile 1040000
+* hard nproc 1040000
+
+* soft core unlimited
+* hard core unlimited
+```
+
+修改完成以后需要重启机器配置才能生效
+
+- 修改系统级别文件句柄数量
+
+file-max的值需要大于limits设置的值
+
+```
+# file-max 设置的值参考
+cat /proc/sys/fs/file-max
+12553500
+```
+
+- 设置sockets连接参数
+
+`vim /etc/sysctl.conf`
+
+```
+# 配置参考
+net.ipv4.tcp_tw_reuse = 1
+net.ipv4.tcp_tw_recycle = 0
+net.ipv4.ip_local_port_range = 1024 65000
+net.ipv4.tcp_mem = 786432 2097152 3145728
+net.ipv4.tcp_rmem = 4096 4096 16777216
+net.ipv4.tcp_wmem = 4096 4096 16777216
+```
+
+`sysctl -p` 修改配置以后使得配置生效命令
+
+### 6.2 压测准备
+- 待压测,如果大家有压测的结果欢迎补充
+- 后续会出专门的教程,从申请机器、写压测用例、内核优化、得出压测数据
+
+- **关于压测请移步**
+- [go实现的压测工具【单台机器100w连接压测实战】](https://github.com/link1st/go-stress-testing)
+- 用go语言实现一款压测工具,然后对本项目进行压测,实现单台机器100W长连接
+
+### 6.3 压测数据
+- 项目在实际使用的时候,每个连接约占 27Kb内存
+- 支持百万连接需要25G内存,单台机器实现百万长连接是可以实现的
+
+- 记录内存使用情况,分别记录了1W到100W连接数内存使用情况
+
+| 连接数 | 内存 |
+| :----: | :----:|
+| 10000 | 281M |
+| 100000 | 2.7g |
+| 200000 | 5.4g |
+| 500000 | 13.1g |
+| 1000000 | 25.8g |
+
+- [压测详细数据](https://github.com/link1st/go-stress-testing#65-%E5%8E%8B%E6%B5%8B%E6%95%B0%E6%8D%AE)
+
+## 7、如何基于webSocket实现一个分布式Im
+### 7.1 说明
+- 参考本项目源码
+- [gowebsocket v1.0.0 单机版Im系统](https://github.com/link1st/gowebsocket/tree/v1.0.0)
+- [gowebsocket v2.0.0 分布式Im系统](https://github.com/link1st/gowebsocket/tree/v2.0.0)
+
+- 为了方便演示,IM系统和webSocket(acc)系统合并在一个系统中
+- IM系统接口:
+获取全部在线的用户,查询当前服务的全部用户+集群中服务的全部用户
+发送消息,这里采用的是http接口发送(微信网页版发送消息也是http接口),这里考虑主要是两点:
+1.服务分离,让acc系统尽量的简单一点,不掺杂其它业务逻辑
+2.发送消息是走http接口,不使用webSocket连接,采用收和发送数据分离的方式,可以加快收发数据的效率
+
+### 7.2 架构
+
+- 项目启动注册和用户连接时序图
+
+![用户连接时序图](https://img.mukewang.com/5de5d6bf00011c8e08740728.png)
+
+- 其它系统(IM、任务)向webSocket(acc)系统连接的用户发送消息时序图
+
+![分布是系统随机给用户发送消息](https://img.mukewang.com/5d4e56f70001e91711730688.png)
+
+### 7.3 分布式系统部署
+- 用水平部署两个项目(gowebsocket和gowebsocket1)演示分部署
+- 项目之间如何相互通讯:项目启动以后将项目Ip、rpcPort注册到redis中,让其它项目可以发现,需要通讯的时候使用gRpc进行通讯
+- gowebsocket
+
+```
+# app.yaml 配置文件信息
+app:
+ logFile: log/gin.log
+ httpPort: 8080
+ webSocketPort: 8089
+ rpcPort: 9001
+ httpUrl: im.91vh.com
+ webSocketUrl: im.91vh.com
+
+# 在启动项目
+go run main.go
+
+```
+
+- gowebsocket1
+
+```
+# 将第一个项目拷贝一份
+cp -rf gowebsocket gowebsocket1
+# app.yaml 修改配置文件
+app:
+ logFile: log/gin.log
+ httpPort: 8081
+ webSocketPort: 8090
+ rpcPort: 9002
+ httpUrl: im.91vh.com
+ webSocketUrl: im.91vh.com
+
+# 在启动第二个项目
+go run main.go
+```
+
+- Nginx配置
+
+在之前Nginx配置项中添加第二台机器的Ip和端口
+
+```
+upstream go-im
+{
+ server 127.0.0.1:8080 weight=1 max_fails=2 fail_timeout=10s;
+ server 127.0.0.1:8081 weight=1 max_fails=2 fail_timeout=10s;
+ keepalive 16;
+}
+
+upstream go-acc
+{
+ server 127.0.0.1:8089 weight=1 max_fails=2 fail_timeout=10s;
+ server 127.0.0.1:8090 weight=1 max_fails=2 fail_timeout=10s;
+ keepalive 16;
+}
+```
+
+- 配置完成以后重启Nginx
+- 重启以后请求,验证是否符合预期:
+
+ 查看请求是否落在两个项目上
+ 实验两个用户分别连接不同的项目(gowebsocket和gowebsocket1)是否也可以相互发送消息
+
+- 关于分布式部署
+
+ 本项目只是演示了这个项目如何分布式部署,以及分布式部署以后模块如何进行相互通讯
+ 完全解决系统没有单点的故障,还需 Nginx集群、redis cluster等
+
+
+## 8、回顾和反思
+### 8.1 在其它系统应用
+- 本系统设计的初衷就是:和客户端保持一个长连接、对外部系统两个接口(查询用户是否在线、给在线的用户推送消息),实现业务的分离
+- 只有和业务分离可,才可以供多个业务使用,而不是每个业务都建立一个长连接
+
+#### 8.2 已经实现的功能
+
+- gin log日志(请求日志+debug日志)
+- 读取配置文件 完成
+- 定时脚本,清理过期未心跳连接 完成
+- http接口,获取登录、连接数量 完成
+- http接口,发送push、查询有多少人在线 完成
+- grpc 程序内部通讯,发送消息 完成
+- appIds 一个用户在多个平台登录
+- 界面,把所有在线的人拉倒一个群里面,发送消息 完成
+- ~~单聊~~、群聊 完成
+- 实现分布式,水平扩张 完成
+- 压测脚本
+- 文档整理
+- 文档目录、百万长连接的实现、为什么要实现一个IM、怎么实现一个Im
+- 架构图以及扩展
+
+IM实现细节:
+
+- 定义文本消息结构 完成
+- html发送文本消息 完成
+- 接口接收文本消息并发送给全体 完成
+- html接收到消息 显示到界面 完成
+- 界面优化 需要持续优化
+- 有人加入以后广播全体 完成
+- 定义加入聊天室的消息结构 完成
+- 引入机器人 待定
+
+### 8.2 需要完善、优化
+- 登录,使用微信登录 获取昵称、头像等
+- 有账号系统、资料系统
+- 界面优化、适配手机端
+- 消息 文本消息(支持表情)、图片、语音、视频消息
+- 微服务注册、发现、熔断等
+- 添加配置项,单台机器最大连接数量
+
+### 8.3 总结
+- 虽然实现了一个分布式在聊天的IM,但是有很多细节没有处理(登录没有鉴权、界面还待优化等),但是可以通过这个示例可以了解到:通过WebSocket解决很多业务上需求
+- 本文虽然号称单台机器能有百万长连接(内存上能满足),但是实际在场景远比这个复杂(cpu有些压力),当然了如果你有这么大的业务量可以购买更多的机器更好的去支撑你的业务,本程序只是演示如何在实际工作用使用webSocket.
+- 参考本文,你可以实现出来符合你需要的程序
+
+### 9、参考文献
+
+[维基百科 WebSocket](https://zh.wikipedia.org/wiki/WebSocket)
+
+[阮一峰 WebSocket教程](http://www.ruanyifeng.com/blog/2017/05/websocket.html)
+
+[WebSocket协议:5分钟从入门到精通](https://www.cnblogs.com/chyingp/p/websocket-deep-in.html)
+
+[go-stress-testing 单台机器100w连接压测实战](https://github.com/link1st/go-stress-testing)
+
+github 搜:link1st 查看项目 gowebsocket
+
+[https://github.com/link1st/gowebsocket](https://github.com/link1st/gowebsocket)
+
+### 意见反馈
+
+- 在项目中遇到问题可以直接在这里找找答案或者提问 [issues](https://github.com/link1st/gowebsocket/issues)
+- 也可以添加我的微信(申请信息填写:公司、姓名,我好备注下),直接反馈给我
+
+
+
+
+
+### 赞助商
+
+- 感谢[JetBrains](https://www.jetbrains.com/?from=gowebsocket)对本项目的支持!
+
+
+
+
+
+
diff --git a/common/error_code.go b/common/error_code.go
new file mode 100644
index 0000000..10e01c0
--- /dev/null
+++ b/common/error_code.go
@@ -0,0 +1,55 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-25
+* Time: 12:11
+ */
+
+package common
+
+const (
+ OK = 200 // Success
+ NotLoggedIn = 1000 // 未登录
+ ParameterIllegal = 1001 // 参数不合法
+ UnauthorizedUserId = 1002 // 非法的用户Id
+ Unauthorized = 1003 // 未授权
+ ServerError = 1004 // 系统错误
+ NotData = 1005 // 没有数据
+ ModelAddError = 1006 // 添加错误
+ ModelDeleteError = 1007 // 删除错误
+ ModelStoreError = 1008 // 存储错误
+ OperationFailure = 1009 // 操作失败
+ RoutingNotExist = 1010 // 路由不存在
+)
+
+// 根据错误码 获取错误信息
+func GetErrorMessage(code uint32, message string) string {
+ var codeMessage string
+ codeMap := map[uint32]string{
+ OK: "Success",
+ NotLoggedIn: "未登录",
+ ParameterIllegal: "参数不合法",
+ UnauthorizedUserId: "非法的用户Id",
+ Unauthorized: "未授权",
+ NotData: "没有数据",
+ ServerError: "系统错误",
+ ModelAddError: "添加错误",
+ ModelDeleteError: "删除错误",
+ ModelStoreError: "存储错误",
+ OperationFailure: "操作失败",
+ RoutingNotExist: "路由不存在",
+ }
+
+ if message == "" {
+ if value, ok := codeMap[code]; ok {
+ // 存在
+ codeMessage = value
+ } else {
+ codeMessage = "未定义错误类型!"
+ }
+ } else {
+ codeMessage = message
+ }
+
+ return codeMessage
+}
diff --git a/common/rsp_common.go b/common/rsp_common.go
new file mode 100644
index 0000000..4809ca0
--- /dev/null
+++ b/common/rsp_common.go
@@ -0,0 +1,33 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-25
+* Time: 12:11
+ */
+
+package common
+
+type JsonResult struct {
+ Code uint32 `json:"code"`
+ Msg string `json:"msg"`
+ Data interface{} `json:"data"`
+}
+
+func Response(code uint32, message string, data interface{}) JsonResult {
+
+ message = GetErrorMessage(code, message)
+ jsonMap := grantMap(code, message, data)
+
+ return jsonMap
+}
+
+// 按照接口格式生成原数据数组
+func grantMap(code uint32, message string, data interface{}) JsonResult {
+
+ jsonMap := JsonResult{
+ Code: code,
+ Msg: message,
+ Data: data,
+ }
+ return jsonMap
+}
diff --git a/config/app.yaml b/config/app.yaml
new file mode 100644
index 0000000..82d6e04
--- /dev/null
+++ b/config/app.yaml
@@ -0,0 +1,15 @@
+app:
+ logFile: log/gin.log
+ httpPort: 8080
+ webSocketPort: 8089
+ rpcPort: 9001
+ httpUrl: 127.0.0.1:8080
+ webSocketUrl: 127.0.0.1:8089
+
+
+redis:
+ addr: "localhost:6379"
+ password: ""
+ DB: 0
+ poolSize: 30
+ minIdleConns: 30
\ No newline at end of file
diff --git a/controllers/base_controller.go b/controllers/base_controller.go
new file mode 100644
index 0000000..b241d4f
--- /dev/null
+++ b/controllers/base_controller.go
@@ -0,0 +1,36 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-25
+* Time: 12:11
+ */
+
+package controllers
+
+import (
+ "github.com/gin-gonic/gin"
+ "gowebsocket/common"
+ "net/http"
+)
+
+type BaseController struct {
+ gin.Context
+}
+
+// 获取全部请求解析到map
+func Response(c *gin.Context, code uint32, msg string, data map[string]interface{}) {
+ message := common.Response(code, msg, data)
+
+ // 允许跨域
+ c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
+ c.Header("Access-Control-Allow-Origin", "*") // 这是允许访问所有域
+ c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") // 服务器支持的所有跨域请求的方法,为了避免浏览次请求的多次'预检'请求
+ c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
+ c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域关键设置 让浏览器可以解析
+ c.Header("Access-Control-Allow-Credentials", "true") // 跨域请求是否需要带cookie信息 默认设置为true
+ c.Set("content-type", "application/json") // 设置返回格式是json
+
+ c.JSON(http.StatusOK, message)
+
+ return
+}
diff --git a/controllers/home/home_controller.go b/controllers/home/home_controller.go
new file mode 100644
index 0000000..b4c8d98
--- /dev/null
+++ b/controllers/home/home_controller.go
@@ -0,0 +1,38 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-25
+* Time: 12:11
+ */
+
+package home
+
+import (
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "github.com/spf13/viper"
+ "gowebsocket/servers/websocket"
+ "net/http"
+ "strconv"
+)
+
+// 聊天页面
+func Index(c *gin.Context) {
+
+ appIdStr := c.Query("appId")
+ appIdUint64, _ := strconv.ParseInt(appIdStr, 10, 32)
+ appId := uint32(appIdUint64)
+ if !websocket.InAppIds(appId) {
+ appId = websocket.GetDefaultAppId()
+ }
+
+ fmt.Println("http_request 聊天首页", appId)
+
+ data := gin.H{
+ "title": "聊天首页",
+ "appId": appId,
+ "httpUrl": viper.GetString("app.httpUrl"),
+ "webSocketUrl": viper.GetString("app.webSocketUrl"),
+ }
+ c.HTML(http.StatusOK, "index.tpl", data)
+}
diff --git a/controllers/systems/system_controller.go b/controllers/systems/system_controller.go
new file mode 100644
index 0000000..57aa9bf
--- /dev/null
+++ b/controllers/systems/system_controller.go
@@ -0,0 +1,38 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-25
+* Time: 12:11
+ */
+
+package systems
+
+import (
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "gowebsocket/common"
+ "gowebsocket/controllers"
+ "gowebsocket/servers/websocket"
+ "runtime"
+)
+
+// 查询系统状态
+func Status(c *gin.Context) {
+
+ isDebug := c.Query("isDebug")
+ fmt.Println("http_request 查询系统状态", isDebug)
+
+ data := make(map[string]interface{})
+
+ numGoroutine := runtime.NumGoroutine()
+ numCPU := runtime.NumCPU()
+
+ // goroutine 的数量
+ data["numGoroutine"] = numGoroutine
+ data["numCPU"] = numCPU
+
+ // ClientManager 信息
+ data["managerInfo"] = websocket.GetManagerInfo(isDebug)
+
+ controllers.Response(c, common.OK, "", data)
+}
diff --git a/controllers/user/user_controller.go b/controllers/user/user_controller.go
new file mode 100644
index 0000000..ae4be57
--- /dev/null
+++ b/controllers/user/user_controller.go
@@ -0,0 +1,122 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-25
+* Time: 12:11
+ */
+
+package user
+
+import (
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "gowebsocket/common"
+ "gowebsocket/controllers"
+ "gowebsocket/lib/cache"
+ "gowebsocket/models"
+ "gowebsocket/servers/websocket"
+ "strconv"
+)
+
+// 查看全部在线用户
+func List(c *gin.Context) {
+
+ appIdStr := c.Query("appId")
+ appIdUint64, _ := strconv.ParseInt(appIdStr, 10, 32)
+ appId := uint32(appIdUint64)
+
+ fmt.Println("http_request 查看全部在线用户", appId)
+
+ data := make(map[string]interface{})
+
+ userList := websocket.UserList(appId)
+ data["userList"] = userList
+ data["userCount"] = len(userList)
+
+ controllers.Response(c, common.OK, "", data)
+}
+
+// 查看用户是否在线
+func Online(c *gin.Context) {
+
+ userId := c.Query("userId")
+ appIdStr := c.Query("appId")
+ appIdUint64, _ := strconv.ParseInt(appIdStr, 10, 32)
+ appId := uint32(appIdUint64)
+
+ fmt.Println("http_request 查看用户是否在线", userId, appIdStr)
+
+ data := make(map[string]interface{})
+
+ online := websocket.CheckUserOnline(appId, userId)
+ data["userId"] = userId
+ data["online"] = online
+
+ controllers.Response(c, common.OK, "", data)
+}
+
+// 给用户发送消息
+func SendMessage(c *gin.Context) {
+ // 获取参数
+ appIdStr := c.PostForm("appId")
+ userId := c.PostForm("userId")
+ msgId := c.PostForm("msgId")
+ message := c.PostForm("message")
+ appIdUint64, _ := strconv.ParseInt(appIdStr, 10, 32)
+ appId := uint32(appIdUint64)
+
+ fmt.Println("http_request 给用户发送消息", appIdStr, userId, msgId, message)
+
+ // TODO::进行用户权限认证,一般是客户端传入TOKEN,然后检验TOKEN是否合法,通过TOKEN解析出来用户ID
+ // 本项目只是演示,所以直接过去客户端传入的用户ID(userId)
+
+ data := make(map[string]interface{})
+
+ if cache.SeqDuplicates(msgId) {
+ fmt.Println("给用户发送消息 重复提交:", msgId)
+ controllers.Response(c, common.OK, "", data)
+
+ return
+ }
+
+ sendResults, err := websocket.SendUserMessage(appId, userId, msgId, message)
+ if err != nil {
+ data["sendResultsErr"] = err.Error()
+ }
+
+ data["sendResults"] = sendResults
+
+ controllers.Response(c, common.OK, "", data)
+}
+
+// 给全员发送消息
+func SendMessageAll(c *gin.Context) {
+ // 获取参数
+ appIdStr := c.PostForm("appId")
+ userId := c.PostForm("userId")
+ msgId := c.PostForm("msgId")
+ message := c.PostForm("message")
+ appIdUint64, _ := strconv.ParseInt(appIdStr, 10, 32)
+ appId := uint32(appIdUint64)
+
+ fmt.Println("http_request 给全体用户发送消息", appIdStr, userId, msgId, message)
+
+ data := make(map[string]interface{})
+ if cache.SeqDuplicates(msgId) {
+ fmt.Println("给用户发送消息 重复提交:", msgId)
+ controllers.Response(c, common.OK, "", data)
+
+ return
+ }
+
+ sendResults, err := websocket.SendUserMessageAll(appId, userId, msgId, models.MessageCmdMsg, message)
+ if err != nil {
+ data["sendResultsErr"] = err.Error()
+
+ }
+
+ data["sendResults"] = sendResults
+
+ controllers.Response(c, common.OK, "", data)
+
+}
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..67df196
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,28 @@
+module gowebsocket
+
+go 1.14
+
+require (
+ github.com/fsnotify/fsnotify v1.4.8-0.20190312181446-1485a34d5d57 // indirect
+ github.com/gin-gonic/gin v1.7.2
+ github.com/go-playground/validator/v10 v10.8.0 // indirect
+ github.com/go-redis/redis v0.0.0-20190719092155-6bc7daa5b1e8
+ github.com/golang/protobuf v1.5.2
+ github.com/gorilla/websocket v1.4.2
+ github.com/hashicorp/hcl v1.0.1-0.20190611123218-cf7d376da96d // indirect
+ github.com/json-iterator/go v1.1.11 // indirect
+ github.com/mattn/go-isatty v0.0.13 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.1 // indirect
+ github.com/pelletier/go-toml v1.4.1-0.20190725070617-84da2c4a25c5 // indirect
+ github.com/spf13/afero v1.2.2 // indirect
+ github.com/spf13/cast v1.3.1-0.20190531093228-c01685bb8421 // indirect
+ github.com/spf13/jwalterweatherman v1.1.0 // indirect
+ github.com/spf13/pflag v1.0.4-0.20181223182923-24fa6976df40 // indirect
+ github.com/spf13/viper v1.4.1-0.20190728125013-1b33e8258e07
+ github.com/subosito/gotenv v1.1.1 // indirect
+ github.com/ugorji/go v1.2.6 // indirect
+ google.golang.org/grpc v1.21.0
+ google.golang.org/protobuf v1.27.1 // indirect
+ gopkg.in/yaml.v2 v2.4.0 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..54e5e6d
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,239 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
+github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
+github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/fsnotify/fsnotify v1.4.8-0.20190312181446-1485a34d5d57 h1:r+AdyYQnMjCqabCiXfAES7u0tbaqXlLXuZ5FT+5OEQs=
+github.com/fsnotify/fsnotify v1.4.8-0.20190312181446-1485a34d5d57/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.7.2 h1:Tg03T9yM2xa8j6I3Z3oqLaQRSmKvxPd6g/2HJ6zICFA=
+github.com/gin-gonic/gin v1.7.2/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
+github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
+github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
+github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
+github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
+github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
+github.com/go-playground/validator/v10 v10.8.0 h1:1kAa0fCrnpv+QYdkdcRzrRM7AyYs5o8+jZdJCz9xj6k=
+github.com/go-playground/validator/v10 v10.8.0/go.mod h1:9JhgTzTaE31GZDpH/HSvHiRJrJ3iKAgqqH0Bl/Ocjdk=
+github.com/go-redis/redis v0.0.0-20190719092155-6bc7daa5b1e8 h1:+fE3DwORl28yutMsVT36NnW52u8el5F3Ye9bgXZtokc=
+github.com/go-redis/redis v0.0.0-20190719092155-6bc7daa5b1e8/go.mod h1:nuQKdm6S7SnV28NJEN2ZNbKpddAM1O76Z2LMJcIxJVM=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
+github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
+github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/hcl v1.0.1-0.20190611123218-cf7d376da96d h1:r4iSf+UX1tNxFJZ64FsUoOfysT7TePSbRNz4/mYGUIE=
+github.com/hashicorp/hcl v1.0.1-0.20190611123218-cf7d376da96d/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=
+github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
+github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
+github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
+github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA=
+github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
+github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w=
+github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
+github.com/onsi/gomega v1.5.0 h1:izbySO9zDPmjJ8rDjLvkA2zJHIo+HkYXHnf7eN7SSyo=
+github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pelletier/go-toml v1.4.1-0.20190725070617-84da2c4a25c5 h1:rW9pqjOLUVvikJFWrF53GlhmZNTFtsjjNA0LD2sYLvg=
+github.com/pelletier/go-toml v1.4.1-0.20190725070617-84da2c4a25c5/go.mod h1:PN7xzY2wHTK0K9p34ErDQMlFxa51Fk0OUruD3k1mMwo=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
+github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
+github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
+github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
+github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cast v1.3.1-0.20190531093228-c01685bb8421 h1:s+WESDalIlUupImv6znWrHX6XIRXuVTBsX633p7Ymms=
+github.com/spf13/cast v1.3.1-0.20190531093228-c01685bb8421/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/pflag v1.0.4-0.20181223182923-24fa6976df40 h1:2gwxRRQ5I+FcDbxGtkIC9kWD7EFBewHjQqD8rDQAVQA=
+github.com/spf13/pflag v1.0.4-0.20181223182923-24fa6976df40/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.4.1-0.20190728125013-1b33e8258e07 h1:Bxzp40S+I62o0BB0Jd23i3OYDtIP2Gs8NtP+Sv6rYHg=
+github.com/spf13/viper v1.4.1-0.20190728125013-1b33e8258e07/go.mod h1:LLu5zwCkRPEBY0VPcRMqh58VtcO8Lp1DgqwstU7rYlk=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/subosito/gotenv v1.1.1 h1:TWxckSF6WVKWbo2M3tMqCtWa9NFUgqM1SSynxmYONOI=
+github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
+github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
+github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
+github.com/ugorji/go v1.2.6 h1:tGiWC9HENWE2tqYycIqFTNorMmFRVhNwCpDOpWqnk8E=
+github.com/ugorji/go v1.2.6/go.mod h1:anCg0y61KIhDlPZmnH+so+RQbysYVyDko0IMgJv0Nn0=
+github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
+github.com/ugorji/go/codec v1.2.6 h1:7kbGefxLoDBuYXOms4yD7223OpNMMPNPZxXk5TvFcyQ=
+github.com/ugorji/go/codec v1.2.6/go.mod h1:V6TCNZ4PHqoHGFZuSG1W8nrCzzdgA2DozYxWFFpvxTw=
+github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
+go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
+go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
+go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97 h1:/UOmuWzQfxxo9UtlXMwuQU8CMgg1eZXqTRwkSQJWKOI=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c h1:F1jZWGFhYfh0Ci55sIpILtKKK8p3i2/krTr0H1rg74I=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0=
+google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
+google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4=
+gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
+gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
+gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/helper/order_helper.go b/helper/order_helper.go
new file mode 100644
index 0000000..c7f9953
--- /dev/null
+++ b/helper/order_helper.go
@@ -0,0 +1,21 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-08-01
+* Time: 18:13
+ */
+
+package helper
+
+import (
+ "fmt"
+ "time"
+)
+
+func GetOrderIdTime() (orderId string) {
+
+ currentTime := time.Now().Nanosecond()
+ orderId = fmt.Sprintf("%d", currentTime)
+
+ return
+}
diff --git a/helper/server_helper.go b/helper/server_helper.go
new file mode 100644
index 0000000..1ef239e
--- /dev/null
+++ b/helper/server_helper.go
@@ -0,0 +1,91 @@
+/**
+ * Created by GoLand.
+ * User: link1st
+ * Date: 2019-07-25
+ * Time: 17:27
+ */
+
+package helper
+
+import (
+ "net"
+)
+
+// 获取服务器Ip
+// func GetServerIp() (ip string) {
+
+// addrs, err := net.InterfaceAddrs()
+
+// if err != nil {
+// return ""
+// }
+
+// for _, address := range addrs {
+// // 检查ip地址判断是否回环地址
+// if ipNet, ok := address.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
+// if ipNet.IP.To4() != nil {
+// ip = ipNet.IP.String()
+// }
+// }
+// }
+
+// return
+// }
+
+/**** 问题:我在本地多网卡机器上,运行分布式场景,此函数返回的ip有误导致rpc连接失败。 遂google结果如下:
+ *** 1、https://www.jianshu.com/p/301aabc06972
+ *** 2、https://www.cnblogs.com/chaselogs/p/11301940.html
+****/
+func GetServerIp() string {
+ ip, err := externalIP()
+ if err != nil {
+ return ""
+ }
+ return ip.String()
+}
+
+func externalIP() (net.IP, error) {
+ ifaces, err := net.Interfaces()
+ if err != nil {
+ return nil, err
+ }
+ for _, iface := range ifaces {
+ if iface.Flags&net.FlagUp == 0 {
+ continue // interface down
+ }
+ if iface.Flags&net.FlagLoopback != 0 {
+ continue // loopback interface
+ }
+ addrs, err := iface.Addrs()
+ if err != nil {
+ return nil, err
+ }
+ for _, addr := range addrs {
+ ip := getIpFromAddr(addr)
+ if ip == nil {
+ continue
+ }
+ return ip, nil
+ }
+ }
+ return nil, err
+}
+
+func getIpFromAddr(addr net.Addr) net.IP {
+ var ip net.IP
+ switch v := addr.(type) {
+ case *net.IPNet:
+ ip = v.IP
+ case *net.IPAddr:
+ ip = v.IP
+ }
+ if ip == nil || ip.IsLoopback() {
+ return nil
+ }
+ ip = ip.To4()
+ if ip == nil {
+ return nil // not an ipv4 address
+ }
+
+ return ip
+}
diff --git a/lib/cache/server_cache.go b/lib/cache/server_cache.go
new file mode 100644
index 0000000..6a6256e
--- /dev/null
+++ b/lib/cache/server_cache.go
@@ -0,0 +1,118 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-08-03
+* Time: 15:23
+ */
+
+package cache
+
+import (
+ "encoding/json"
+ "fmt"
+ "gowebsocket/lib/redislib"
+ "gowebsocket/models"
+ "strconv"
+)
+
+const (
+ serversHashKey = "acc:hash:servers" // 全部的服务器
+ serversHashCacheTime = 2 * 60 * 60 // key过期时间
+ serversHashTimeout = 3 * 60 // 超时时间
+)
+
+func getServersHashKey() (key string) {
+ key = fmt.Sprintf("%s", serversHashKey)
+
+ return
+}
+
+// 设置服务器信息
+func SetServerInfo(server *models.Server, currentTime uint64) (err error) {
+ key := getServersHashKey()
+
+ value := fmt.Sprintf("%d", currentTime)
+
+ redisClient := redislib.GetClient()
+ number, err := redisClient.Do("hSet", key, server.String(), value).Int()
+ if err != nil {
+ fmt.Println("SetServerInfo", key, number, err)
+
+ return
+ }
+
+ if number != 1 {
+
+ return
+ }
+
+ redisClient.Do("Expire", key, serversHashCacheTime)
+
+ return
+}
+
+// 下线服务器信息
+func DelServerInfo(server *models.Server) (err error) {
+ key := getServersHashKey()
+ redisClient := redislib.GetClient()
+ number, err := redisClient.Do("hDel", key, server.String()).Int()
+ if err != nil {
+ fmt.Println("DelServerInfo", key, number, err)
+
+ return
+ }
+
+ if number != 1 {
+
+ return
+ }
+
+ redisClient.Do("Expire", key, serversHashCacheTime)
+
+ return
+}
+
+func GetServerAll(currentTime uint64) (servers []*models.Server, err error) {
+
+ servers = make([]*models.Server, 0)
+ key := getServersHashKey()
+
+ redisClient := redislib.GetClient()
+
+ val, err := redisClient.Do("hGetAll", key).Result()
+
+ valByte, _ := json.Marshal(val)
+ fmt.Println("GetServerAll", key, string(valByte))
+
+ serverMap, err := redisClient.HGetAll(key).Result()
+ if err != nil {
+ fmt.Println("SetServerInfo", key, err)
+
+ return
+ }
+
+ for key, value := range serverMap {
+ valueUint64, err := strconv.ParseUint(value, 10, 64)
+ if err != nil {
+ fmt.Println("GetServerAll", key, err)
+
+ return nil, err
+ }
+
+ // 超时
+ if valueUint64+serversHashTimeout <= currentTime {
+ continue
+ }
+
+ server, err := models.StringToServer(key)
+ if err != nil {
+ fmt.Println("GetServerAll", key, err)
+
+ return nil, err
+ }
+
+ servers = append(servers, server)
+ }
+
+ return
+}
diff --git a/lib/cache/submit_cache.go b/lib/cache/submit_cache.go
new file mode 100644
index 0000000..0722f4c
--- /dev/null
+++ b/lib/cache/submit_cache.go
@@ -0,0 +1,62 @@
+/**
+ * Created by GoLand.
+ * User: link1st
+ * Date: 2019-07-26
+ * Time: 09:18
+ */
+
+package cache
+
+import (
+ "fmt"
+ "gowebsocket/lib/redislib"
+)
+
+const (
+ submitAgainPrefix = "acc:submit:again:" // 数据不重复提交
+)
+
+/********************* 查询数据是否处理过 ************************/
+
+// 获取数据提交去除key
+func getSubmitAgainKey(from string, value string) (key string) {
+ key = fmt.Sprintf("%s%s:%s", submitAgainPrefix, from, value)
+
+ return
+}
+
+// 重复提交
+// return true:重复提交 false:第一次提交
+func submitAgain(from string, second int, value string) (isSubmitAgain bool) {
+
+ // 默认重复提交
+ isSubmitAgain = true
+ key := getSubmitAgainKey(from, value)
+
+ redisClient := redislib.GetClient()
+ number, err := redisClient.Do("setNx", key, "1").Int()
+ if err != nil {
+ fmt.Println("submitAgain", key, number, err)
+
+ return
+ }
+
+ if number != 1 {
+
+ return
+ }
+ // 第一次提交
+ isSubmitAgain = false
+
+ redisClient.Do("Expire", key, second)
+
+ return
+
+}
+
+// Seq 重复提交
+func SeqDuplicates(seq string) (result bool) {
+ result = submitAgain("seq", 12*60*60, seq)
+
+ return
+}
diff --git a/lib/cache/user_cache.go b/lib/cache/user_cache.go
new file mode 100644
index 0000000..af1e0d9
--- /dev/null
+++ b/lib/cache/user_cache.go
@@ -0,0 +1,82 @@
+/**
+ * Created by GoLand.
+ * User: link1st
+ * Date: 2019-07-25
+ * Time: 17:28
+ */
+
+package cache
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/go-redis/redis"
+ "gowebsocket/lib/redislib"
+ "gowebsocket/models"
+)
+
+const (
+ userOnlinePrefix = "acc:user:online:" // 用户在线状态
+ userOnlineCacheTime = 24 * 60 * 60
+)
+
+/********************* 查询用户是否在线 ************************/
+func getUserOnlineKey(userKey string) (key string) {
+ key = fmt.Sprintf("%s%s", userOnlinePrefix, userKey)
+
+ return
+}
+
+func GetUserOnlineInfo(userKey string) (userOnline *models.UserOnline, err error) {
+ redisClient := redislib.GetClient()
+
+ key := getUserOnlineKey(userKey)
+
+ data, err := redisClient.Get(key).Bytes()
+ if err != nil {
+ if err == redis.Nil {
+ fmt.Println("GetUserOnlineInfo", userKey, err)
+
+ return
+ }
+
+ fmt.Println("GetUserOnlineInfo", userKey, err)
+
+ return
+ }
+
+ userOnline = &models.UserOnline{}
+ err = json.Unmarshal(data, userOnline)
+ if err != nil {
+ fmt.Println("获取用户在线数据 json Unmarshal", userKey, err)
+
+ return
+ }
+
+ fmt.Println("获取用户在线数据", userKey, "time", userOnline.LoginTime, userOnline.HeartbeatTime, "AccIp", userOnline.AccIp, userOnline.IsLogoff)
+
+ return
+}
+
+// 设置用户在线数据
+func SetUserOnlineInfo(userKey string, userOnline *models.UserOnline) (err error) {
+
+ redisClient := redislib.GetClient()
+ key := getUserOnlineKey(userKey)
+
+ valueByte, err := json.Marshal(userOnline)
+ if err != nil {
+ fmt.Println("设置用户在线数据 json Marshal", key, err)
+
+ return
+ }
+
+ _, err = redisClient.Do("setEx", key, userOnlineCacheTime, string(valueByte)).Result()
+ if err != nil {
+ fmt.Println("设置用户在线数据 ", key, err)
+
+ return
+ }
+
+ return
+}
diff --git a/lib/redislib/redis_lib.go b/lib/redislib/redis_lib.go
new file mode 100644
index 0000000..1c46ee8
--- /dev/null
+++ b/lib/redislib/redis_lib.go
@@ -0,0 +1,38 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-25
+* Time: 14:18
+ */
+
+package redislib
+
+import (
+ "fmt"
+ "github.com/go-redis/redis"
+ "github.com/spf13/viper"
+)
+
+var (
+ client *redis.Client
+)
+
+func ExampleNewClient() {
+
+ client = redis.NewClient(&redis.Options{
+ Addr: viper.GetString("redis.addr"),
+ Password: viper.GetString("redis.password"),
+ DB: viper.GetInt("redis.DB"),
+ PoolSize: viper.GetInt("redis.poolSize"),
+ MinIdleConns: viper.GetInt("redis.minIdleConns"),
+ })
+
+ pong, err := client.Ping().Result()
+ fmt.Println("初始化redis:", pong, err)
+ // Output: PONG
+}
+
+func GetClient() (c *redis.Client) {
+
+ return client
+}
diff --git a/log/.gitignore b/log/.gitignore
new file mode 100644
index 0000000..b722e9e
--- /dev/null
+++ b/log/.gitignore
@@ -0,0 +1 @@
+!.gitignore
\ No newline at end of file
diff --git a/log/gin.log b/log/gin.log
new file mode 100644
index 0000000..9f17cb8
--- /dev/null
+++ b/log/gin.log
@@ -0,0 +1,21 @@
+[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
+
+[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
+ - using env: export GIN_MODE=release
+ - using code: gin.SetMode(gin.ReleaseMode)
+
+[GIN-debug] Loaded HTML Templates (3):
+ -
+ - index.html
+ - index.tpl
+
+[GIN-debug] GET /user/list --> gowebsocket/controllers/user.List (3 handlers)
+[GIN-debug] GET /user/online --> gowebsocket/controllers/user.Online (3 handlers)
+[GIN-debug] POST /user/sendMessage --> gowebsocket/controllers/user.SendMessage (3 handlers)
+[GIN-debug] POST /user/sendMessageAll --> gowebsocket/controllers/user.SendMessageAll (3 handlers)
+[GIN-debug] GET /system/state --> gowebsocket/controllers/systems.Status (3 handlers)
+[GIN-debug] GET /home/index --> gowebsocket/controllers/home.Index (3 handlers)
+[GIN] 2021/11/20 - 00:19:19 | 200 | 2.067409ms | 127.0.0.1 | GET "/home/index"
+[GIN] 2021/11/20 - 00:19:20 | 200 | 748.062µs | 127.0.0.1 | GET "/user/list?appId=101"
+[GIN] 2021/11/20 - 00:19:30 | 200 | 1.828355ms | 127.0.0.1 | GET "/home/index?appId=104"
+[GIN] 2021/11/20 - 00:19:31 | 200 | 490.323µs | 127.0.0.1 | GET "/user/list?appId=104"
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..862910c
--- /dev/null
+++ b/main.go
@@ -0,0 +1,95 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-25
+* Time: 09:59
+ */
+
+package main
+
+import (
+ "fmt"
+ "github.com/gin-gonic/gin"
+ "github.com/spf13/viper"
+ "gowebsocket/lib/redislib"
+ "gowebsocket/routers"
+ "gowebsocket/servers/grpcserver"
+ "gowebsocket/servers/task"
+ "gowebsocket/servers/websocket"
+ "io"
+ "net/http"
+ "os"
+ "os/exec"
+ "time"
+)
+
+func main() {
+ initConfig()
+
+ initFile()
+
+ initRedis()
+
+ router := gin.Default()
+ // 初始化路由
+ routers.Init(router)
+ routers.WebsocketInit()
+
+ // 定时任务
+ task.Init()
+
+ // 服务注册
+ task.ServerInit()
+
+ go websocket.StartWebSocket()
+ // grpc
+ go grpcserver.Init()
+
+ go open()
+
+ httpPort := viper.GetString("app.httpPort")
+ http.ListenAndServe(":"+httpPort, router)
+
+}
+
+// 初始化日志
+func initFile() {
+ // Disable Console Color, you don't need console color when writing the logs to file.
+ gin.DisableConsoleColor()
+
+ // Logging to a file.
+ logFile := viper.GetString("app.logFile")
+ f, _ := os.Create(logFile)
+ gin.DefaultWriter = io.MultiWriter(f)
+}
+
+func initConfig() {
+ viper.SetConfigName("config/app")
+ viper.AddConfigPath(".") // 添加搜索路径
+
+ err := viper.ReadInConfig()
+ if err != nil {
+ panic(fmt.Errorf("Fatal error config file: %s \n", err))
+ }
+
+ fmt.Println("config app:", viper.Get("app"))
+ fmt.Println("config redis:", viper.Get("redis"))
+
+}
+
+func initRedis() {
+ redislib.ExampleNewClient()
+}
+
+func open() {
+
+ time.Sleep(1000 * time.Millisecond)
+
+ httpUrl := viper.GetString("app.httpUrl")
+ httpUrl = "http://" + httpUrl + "/home/index"
+
+ fmt.Println("访问页面体验:", httpUrl)
+
+ cmd := exec.Command("open", httpUrl)
+ cmd.Output()
+}
diff --git a/models/msg_model.go b/models/msg_model.go
new file mode 100644
index 0000000..7188772
--- /dev/null
+++ b/models/msg_model.go
@@ -0,0 +1,68 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-08-01
+* Time: 10:40
+ */
+
+package models
+
+import "gowebsocket/common"
+
+const (
+ MessageTypeText = "text"
+
+ MessageCmdMsg = "msg"
+ MessageCmdEnter = "enter"
+ MessageCmdExit = "exit"
+)
+
+// 消息的定义
+type Message struct {
+ Target string `json:"target"` // 目标
+ Type string `json:"type"` // 消息类型 text/img/
+ Msg string `json:"msg"` // 消息内容
+ From string `json:"from"` // 发送者
+}
+
+func NewTestMsg(from string, Msg string) (message *Message) {
+
+ message = &Message{
+ Type: MessageTypeText,
+ From: from,
+ Msg: Msg,
+ }
+
+ return
+}
+
+func getTextMsgData(cmd, uuId, msgId, message string) string {
+ textMsg := NewTestMsg(uuId, message)
+ head := NewResponseHead(msgId, cmd, common.OK, "Ok", textMsg)
+
+ return head.String()
+}
+
+// 文本消息
+func GetMsgData(uuId, msgId, cmd, message string) string {
+
+ return getTextMsgData(cmd, uuId, msgId, message)
+}
+
+// 文本消息
+func GetTextMsgData(uuId, msgId, message string) string {
+
+ return getTextMsgData("msg", uuId, msgId, message)
+}
+
+// 用户进入消息
+func GetTextMsgDataEnter(uuId, msgId, message string) string {
+
+ return getTextMsgData("enter", uuId, msgId, message)
+}
+
+// 用户退出消息
+func GetTextMsgDataExit(uuId, msgId, message string) string {
+
+ return getTextMsgData("exit", uuId, msgId, message)
+}
diff --git a/models/request_model.go b/models/request_model.go
new file mode 100644
index 0000000..7c0aea4
--- /dev/null
+++ b/models/request_model.go
@@ -0,0 +1,28 @@
+/**
+ * Created by GoLand.
+ * User: link1st
+ * Date: 2019-07-27
+ * Time: 14:41
+ */
+
+package models
+
+/************************ 请求数据 **************************/
+// 通用请求数据格式
+type Request struct {
+ Seq string `json:"seq"` // 消息的唯一Id
+ Cmd string `json:"cmd"` // 请求命令字
+ Data interface{} `json:"data,omitempty"` // 数据 json
+}
+
+// 登录请求数据
+type Login struct {
+ ServiceToken string `json:"serviceToken"` // 验证用户是否登录
+ AppId uint32 `json:"appId,omitempty"`
+ UserId string `json:"userId,omitempty"`
+}
+
+// 心跳请求数据
+type HeartBeat struct {
+ UserId string `json:"userId,omitempty"`
+}
diff --git a/models/response_model.go b/models/response_model.go
new file mode 100644
index 0000000..f508b15
--- /dev/null
+++ b/models/response_model.go
@@ -0,0 +1,49 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-08-01
+* Time: 10:46
+ */
+
+package models
+
+import "encoding/json"
+
+/************************ 响应数据 **************************/
+type Head struct {
+ Seq string `json:"seq"` // 消息的Id
+ Cmd string `json:"cmd"` // 消息的cmd 动作
+ Response *Response `json:"response"` // 消息体
+}
+
+type Response struct {
+ Code uint32 `json:"code"`
+ CodeMsg string `json:"codeMsg"`
+ Data interface{} `json:"data"` // 数据 json
+}
+
+// push 数据结构体
+type PushMsg struct {
+ Seq string `json:"seq"`
+ Uuid uint64 `json:"uuid"`
+ Type string `json:"type"`
+ Msg string `json:"msg"`
+}
+
+// 设置返回消息
+func NewResponseHead(seq string, cmd string, code uint32, codeMsg string, data interface{}) *Head {
+ response := NewResponse(code, codeMsg, data)
+
+ return &Head{Seq: seq, Cmd: cmd, Response: response}
+}
+
+func (h *Head) String() (headStr string) {
+ headBytes, _ := json.Marshal(h)
+ headStr = string(headBytes)
+
+ return
+}
+
+func NewResponse(code uint32, codeMsg string, data interface{}) *Response {
+ return &Response{Code: code, CodeMsg: codeMsg, Data: data}
+}
diff --git a/models/server_model.go b/models/server_model.go
new file mode 100644
index 0000000..fa3ebea
--- /dev/null
+++ b/models/server_model.go
@@ -0,0 +1,49 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-08-03
+* Time: 15:38
+ */
+
+package models
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+)
+
+type Server struct {
+ Ip string `json:"ip"` // ip
+ Port string `json:"port"` // 端口
+}
+
+func NewServer(ip string, port string) *Server {
+
+ return &Server{Ip: ip, Port: port}
+}
+
+func (s *Server) String() (str string) {
+ if s == nil {
+ return
+ }
+
+ str = fmt.Sprintf("%s:%s", s.Ip, s.Port)
+
+ return
+}
+
+func StringToServer(str string) (server *Server, err error) {
+ list := strings.Split(str, ":")
+ if len(list) != 2 {
+
+ return nil, errors.New("err")
+ }
+
+ server = &Server{
+ Ip: list[0],
+ Port: list[1],
+ }
+
+ return
+}
diff --git a/models/user_model.go b/models/user_model.go
new file mode 100644
index 0000000..07bd441
--- /dev/null
+++ b/models/user_model.go
@@ -0,0 +1,109 @@
+/**
+ * Created by GoLand.
+ * User: link1st
+ * Date: 2019-07-25
+ * Time: 17:36
+ */
+
+package models
+
+import (
+ "fmt"
+ "time"
+)
+
+const (
+ heartbeatTimeout = 3 * 60 // 用户心跳超时时间
+)
+
+// 用户在线状态
+type UserOnline struct {
+ AccIp string `json:"accIp"` // acc Ip
+ AccPort string `json:"accPort"` // acc 端口
+ AppId uint32 `json:"appId"` // appId
+ UserId string `json:"userId"` // 用户Id
+ ClientIp string `json:"clientIp"` // 客户端Ip
+ ClientPort string `json:"clientPort"` // 客户端端口
+ LoginTime uint64 `json:"loginTime"` // 用户上次登录时间
+ HeartbeatTime uint64 `json:"heartbeatTime"` // 用户上次心跳时间
+ LogOutTime uint64 `json:"logOutTime"` // 用户退出登录的时间
+ Qua string `json:"qua"` // qua
+ DeviceInfo string `json:"deviceInfo"` // 设备信息
+ IsLogoff bool `json:"isLogoff"` // 是否下线
+}
+
+/********************** 数据处理 *********************************/
+
+// 用户登录
+func UserLogin(accIp, accPort string, appId uint32, userId string, addr string, loginTime uint64) (userOnline *UserOnline) {
+
+ userOnline = &UserOnline{
+ AccIp: accIp,
+ AccPort: accPort,
+ AppId: appId,
+ UserId: userId,
+ ClientIp: addr,
+ LoginTime: loginTime,
+ HeartbeatTime: loginTime,
+ IsLogoff: false,
+ }
+
+ return
+}
+
+// 用户心跳
+func (u *UserOnline) Heartbeat(currentTime uint64) {
+
+ u.HeartbeatTime = currentTime
+ u.IsLogoff = false
+
+ return
+}
+
+// 用户退出登录
+func (u *UserOnline) LogOut() {
+
+ currentTime := uint64(time.Now().Unix())
+ u.LogOutTime = currentTime
+ u.IsLogoff = true
+
+ return
+}
+
+/********************** 数据操作 *********************************/
+
+// 用户是否在线
+func (u *UserOnline) IsOnline() (online bool) {
+ if u.IsLogoff {
+
+ return
+ }
+
+ currentTime := uint64(time.Now().Unix())
+
+ if u.HeartbeatTime < (currentTime - heartbeatTimeout) {
+ fmt.Println("用户是否在线 心跳超时", u.AppId, u.UserId, u.HeartbeatTime)
+
+ return
+ }
+
+ if u.IsLogoff {
+ fmt.Println("用户是否在线 用户已经下线", u.AppId, u.UserId)
+
+ return
+ }
+
+ return true
+}
+
+// 用户是否在本台机器上
+func (u *UserOnline) UserIsLocal(localIp, localPort string) (result bool) {
+
+ if u.AccIp == localIp && u.AccPort == localPort {
+ result = true
+
+ return
+ }
+
+ return
+}
diff --git a/protobuf/gen.sh b/protobuf/gen.sh
new file mode 100755
index 0000000..8dae6cb
--- /dev/null
+++ b/protobuf/gen.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+
+protoc --go_out=plugins=grpc:. im_protobuf.proto
diff --git a/protobuf/im_protobuf.pb.go b/protobuf/im_protobuf.pb.go
new file mode 100644
index 0000000..6cbd911
--- /dev/null
+++ b/protobuf/im_protobuf.pb.go
@@ -0,0 +1,740 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: im_protobuf.proto
+
+package protobuf
+
+import (
+ context "context"
+ fmt "fmt"
+ proto "github.com/golang/protobuf/proto"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+ math "math"
+)
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ = proto.Marshal
+var _ = fmt.Errorf
+var _ = math.Inf
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the proto package it is being compiled against.
+// A compilation error at this line likely means your copy of the
+// proto package needs to be updated.
+const _ = proto.ProtoPackageIsVersion3 // please upgrade the proto package
+
+// 查询用户是否在线
+type QueryUsersOnlineReq struct {
+ AppId uint32 `protobuf:"varint,1,opt,name=appId,proto3" json:"appId,omitempty"`
+ UserId string `protobuf:"bytes,2,opt,name=userId,proto3" json:"userId,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *QueryUsersOnlineReq) Reset() { *m = QueryUsersOnlineReq{} }
+func (m *QueryUsersOnlineReq) String() string { return proto.CompactTextString(m) }
+func (*QueryUsersOnlineReq) ProtoMessage() {}
+func (*QueryUsersOnlineReq) Descriptor() ([]byte, []int) {
+ return fileDescriptor_62691ff0994625a6, []int{0}
+}
+
+func (m *QueryUsersOnlineReq) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_QueryUsersOnlineReq.Unmarshal(m, b)
+}
+func (m *QueryUsersOnlineReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_QueryUsersOnlineReq.Marshal(b, m, deterministic)
+}
+func (m *QueryUsersOnlineReq) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_QueryUsersOnlineReq.Merge(m, src)
+}
+func (m *QueryUsersOnlineReq) XXX_Size() int {
+ return xxx_messageInfo_QueryUsersOnlineReq.Size(m)
+}
+func (m *QueryUsersOnlineReq) XXX_DiscardUnknown() {
+ xxx_messageInfo_QueryUsersOnlineReq.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryUsersOnlineReq proto.InternalMessageInfo
+
+func (m *QueryUsersOnlineReq) GetAppId() uint32 {
+ if m != nil {
+ return m.AppId
+ }
+ return 0
+}
+
+func (m *QueryUsersOnlineReq) GetUserId() string {
+ if m != nil {
+ return m.UserId
+ }
+ return ""
+}
+
+type QueryUsersOnlineRsp struct {
+ RetCode uint32 `protobuf:"varint,1,opt,name=retCode,proto3" json:"retCode,omitempty"`
+ ErrMsg string `protobuf:"bytes,2,opt,name=errMsg,proto3" json:"errMsg,omitempty"`
+ Online bool `protobuf:"varint,3,opt,name=online,proto3" json:"online,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *QueryUsersOnlineRsp) Reset() { *m = QueryUsersOnlineRsp{} }
+func (m *QueryUsersOnlineRsp) String() string { return proto.CompactTextString(m) }
+func (*QueryUsersOnlineRsp) ProtoMessage() {}
+func (*QueryUsersOnlineRsp) Descriptor() ([]byte, []int) {
+ return fileDescriptor_62691ff0994625a6, []int{1}
+}
+
+func (m *QueryUsersOnlineRsp) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_QueryUsersOnlineRsp.Unmarshal(m, b)
+}
+func (m *QueryUsersOnlineRsp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_QueryUsersOnlineRsp.Marshal(b, m, deterministic)
+}
+func (m *QueryUsersOnlineRsp) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_QueryUsersOnlineRsp.Merge(m, src)
+}
+func (m *QueryUsersOnlineRsp) XXX_Size() int {
+ return xxx_messageInfo_QueryUsersOnlineRsp.Size(m)
+}
+func (m *QueryUsersOnlineRsp) XXX_DiscardUnknown() {
+ xxx_messageInfo_QueryUsersOnlineRsp.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_QueryUsersOnlineRsp proto.InternalMessageInfo
+
+func (m *QueryUsersOnlineRsp) GetRetCode() uint32 {
+ if m != nil {
+ return m.RetCode
+ }
+ return 0
+}
+
+func (m *QueryUsersOnlineRsp) GetErrMsg() string {
+ if m != nil {
+ return m.ErrMsg
+ }
+ return ""
+}
+
+func (m *QueryUsersOnlineRsp) GetOnline() bool {
+ if m != nil {
+ return m.Online
+ }
+ return false
+}
+
+// 发送消息
+type SendMsgReq struct {
+ Seq string `protobuf:"bytes,1,opt,name=seq,proto3" json:"seq,omitempty"`
+ AppId uint32 `protobuf:"varint,2,opt,name=appId,proto3" json:"appId,omitempty"`
+ UserId string `protobuf:"bytes,3,opt,name=userId,proto3" json:"userId,omitempty"`
+ Cms string `protobuf:"bytes,4,opt,name=cms,proto3" json:"cms,omitempty"`
+ Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
+ Msg string `protobuf:"bytes,6,opt,name=msg,proto3" json:"msg,omitempty"`
+ IsLocal bool `protobuf:"varint,7,opt,name=isLocal,proto3" json:"isLocal,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *SendMsgReq) Reset() { *m = SendMsgReq{} }
+func (m *SendMsgReq) String() string { return proto.CompactTextString(m) }
+func (*SendMsgReq) ProtoMessage() {}
+func (*SendMsgReq) Descriptor() ([]byte, []int) {
+ return fileDescriptor_62691ff0994625a6, []int{2}
+}
+
+func (m *SendMsgReq) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_SendMsgReq.Unmarshal(m, b)
+}
+func (m *SendMsgReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_SendMsgReq.Marshal(b, m, deterministic)
+}
+func (m *SendMsgReq) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_SendMsgReq.Merge(m, src)
+}
+func (m *SendMsgReq) XXX_Size() int {
+ return xxx_messageInfo_SendMsgReq.Size(m)
+}
+func (m *SendMsgReq) XXX_DiscardUnknown() {
+ xxx_messageInfo_SendMsgReq.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_SendMsgReq proto.InternalMessageInfo
+
+func (m *SendMsgReq) GetSeq() string {
+ if m != nil {
+ return m.Seq
+ }
+ return ""
+}
+
+func (m *SendMsgReq) GetAppId() uint32 {
+ if m != nil {
+ return m.AppId
+ }
+ return 0
+}
+
+func (m *SendMsgReq) GetUserId() string {
+ if m != nil {
+ return m.UserId
+ }
+ return ""
+}
+
+func (m *SendMsgReq) GetCms() string {
+ if m != nil {
+ return m.Cms
+ }
+ return ""
+}
+
+func (m *SendMsgReq) GetType() string {
+ if m != nil {
+ return m.Type
+ }
+ return ""
+}
+
+func (m *SendMsgReq) GetMsg() string {
+ if m != nil {
+ return m.Msg
+ }
+ return ""
+}
+
+func (m *SendMsgReq) GetIsLocal() bool {
+ if m != nil {
+ return m.IsLocal
+ }
+ return false
+}
+
+type SendMsgRsp struct {
+ RetCode uint32 `protobuf:"varint,1,opt,name=retCode,proto3" json:"retCode,omitempty"`
+ ErrMsg string `protobuf:"bytes,2,opt,name=errMsg,proto3" json:"errMsg,omitempty"`
+ SendMsgId string `protobuf:"bytes,3,opt,name=sendMsgId,proto3" json:"sendMsgId,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *SendMsgRsp) Reset() { *m = SendMsgRsp{} }
+func (m *SendMsgRsp) String() string { return proto.CompactTextString(m) }
+func (*SendMsgRsp) ProtoMessage() {}
+func (*SendMsgRsp) Descriptor() ([]byte, []int) {
+ return fileDescriptor_62691ff0994625a6, []int{3}
+}
+
+func (m *SendMsgRsp) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_SendMsgRsp.Unmarshal(m, b)
+}
+func (m *SendMsgRsp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_SendMsgRsp.Marshal(b, m, deterministic)
+}
+func (m *SendMsgRsp) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_SendMsgRsp.Merge(m, src)
+}
+func (m *SendMsgRsp) XXX_Size() int {
+ return xxx_messageInfo_SendMsgRsp.Size(m)
+}
+func (m *SendMsgRsp) XXX_DiscardUnknown() {
+ xxx_messageInfo_SendMsgRsp.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_SendMsgRsp proto.InternalMessageInfo
+
+func (m *SendMsgRsp) GetRetCode() uint32 {
+ if m != nil {
+ return m.RetCode
+ }
+ return 0
+}
+
+func (m *SendMsgRsp) GetErrMsg() string {
+ if m != nil {
+ return m.ErrMsg
+ }
+ return ""
+}
+
+func (m *SendMsgRsp) GetSendMsgId() string {
+ if m != nil {
+ return m.SendMsgId
+ }
+ return ""
+}
+
+// 发送消息
+type SendMsgAllReq struct {
+ Seq string `protobuf:"bytes,1,opt,name=seq,proto3" json:"seq,omitempty"`
+ AppId uint32 `protobuf:"varint,2,opt,name=appId,proto3" json:"appId,omitempty"`
+ UserId string `protobuf:"bytes,3,opt,name=userId,proto3" json:"userId,omitempty"`
+ Cms string `protobuf:"bytes,4,opt,name=cms,proto3" json:"cms,omitempty"`
+ Type string `protobuf:"bytes,5,opt,name=type,proto3" json:"type,omitempty"`
+ Msg string `protobuf:"bytes,6,opt,name=msg,proto3" json:"msg,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *SendMsgAllReq) Reset() { *m = SendMsgAllReq{} }
+func (m *SendMsgAllReq) String() string { return proto.CompactTextString(m) }
+func (*SendMsgAllReq) ProtoMessage() {}
+func (*SendMsgAllReq) Descriptor() ([]byte, []int) {
+ return fileDescriptor_62691ff0994625a6, []int{4}
+}
+
+func (m *SendMsgAllReq) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_SendMsgAllReq.Unmarshal(m, b)
+}
+func (m *SendMsgAllReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_SendMsgAllReq.Marshal(b, m, deterministic)
+}
+func (m *SendMsgAllReq) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_SendMsgAllReq.Merge(m, src)
+}
+func (m *SendMsgAllReq) XXX_Size() int {
+ return xxx_messageInfo_SendMsgAllReq.Size(m)
+}
+func (m *SendMsgAllReq) XXX_DiscardUnknown() {
+ xxx_messageInfo_SendMsgAllReq.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_SendMsgAllReq proto.InternalMessageInfo
+
+func (m *SendMsgAllReq) GetSeq() string {
+ if m != nil {
+ return m.Seq
+ }
+ return ""
+}
+
+func (m *SendMsgAllReq) GetAppId() uint32 {
+ if m != nil {
+ return m.AppId
+ }
+ return 0
+}
+
+func (m *SendMsgAllReq) GetUserId() string {
+ if m != nil {
+ return m.UserId
+ }
+ return ""
+}
+
+func (m *SendMsgAllReq) GetCms() string {
+ if m != nil {
+ return m.Cms
+ }
+ return ""
+}
+
+func (m *SendMsgAllReq) GetType() string {
+ if m != nil {
+ return m.Type
+ }
+ return ""
+}
+
+func (m *SendMsgAllReq) GetMsg() string {
+ if m != nil {
+ return m.Msg
+ }
+ return ""
+}
+
+type SendMsgAllRsp struct {
+ RetCode uint32 `protobuf:"varint,1,opt,name=retCode,proto3" json:"retCode,omitempty"`
+ ErrMsg string `protobuf:"bytes,2,opt,name=errMsg,proto3" json:"errMsg,omitempty"`
+ SendMsgId string `protobuf:"bytes,3,opt,name=sendMsgId,proto3" json:"sendMsgId,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *SendMsgAllRsp) Reset() { *m = SendMsgAllRsp{} }
+func (m *SendMsgAllRsp) String() string { return proto.CompactTextString(m) }
+func (*SendMsgAllRsp) ProtoMessage() {}
+func (*SendMsgAllRsp) Descriptor() ([]byte, []int) {
+ return fileDescriptor_62691ff0994625a6, []int{5}
+}
+
+func (m *SendMsgAllRsp) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_SendMsgAllRsp.Unmarshal(m, b)
+}
+func (m *SendMsgAllRsp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_SendMsgAllRsp.Marshal(b, m, deterministic)
+}
+func (m *SendMsgAllRsp) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_SendMsgAllRsp.Merge(m, src)
+}
+func (m *SendMsgAllRsp) XXX_Size() int {
+ return xxx_messageInfo_SendMsgAllRsp.Size(m)
+}
+func (m *SendMsgAllRsp) XXX_DiscardUnknown() {
+ xxx_messageInfo_SendMsgAllRsp.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_SendMsgAllRsp proto.InternalMessageInfo
+
+func (m *SendMsgAllRsp) GetRetCode() uint32 {
+ if m != nil {
+ return m.RetCode
+ }
+ return 0
+}
+
+func (m *SendMsgAllRsp) GetErrMsg() string {
+ if m != nil {
+ return m.ErrMsg
+ }
+ return ""
+}
+
+func (m *SendMsgAllRsp) GetSendMsgId() string {
+ if m != nil {
+ return m.SendMsgId
+ }
+ return ""
+}
+
+// 获取用户列表
+type GetUserListReq struct {
+ AppId uint32 `protobuf:"varint,1,opt,name=appId,proto3" json:"appId,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *GetUserListReq) Reset() { *m = GetUserListReq{} }
+func (m *GetUserListReq) String() string { return proto.CompactTextString(m) }
+func (*GetUserListReq) ProtoMessage() {}
+func (*GetUserListReq) Descriptor() ([]byte, []int) {
+ return fileDescriptor_62691ff0994625a6, []int{6}
+}
+
+func (m *GetUserListReq) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_GetUserListReq.Unmarshal(m, b)
+}
+func (m *GetUserListReq) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_GetUserListReq.Marshal(b, m, deterministic)
+}
+func (m *GetUserListReq) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_GetUserListReq.Merge(m, src)
+}
+func (m *GetUserListReq) XXX_Size() int {
+ return xxx_messageInfo_GetUserListReq.Size(m)
+}
+func (m *GetUserListReq) XXX_DiscardUnknown() {
+ xxx_messageInfo_GetUserListReq.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GetUserListReq proto.InternalMessageInfo
+
+func (m *GetUserListReq) GetAppId() uint32 {
+ if m != nil {
+ return m.AppId
+ }
+ return 0
+}
+
+type GetUserListRsp struct {
+ RetCode uint32 `protobuf:"varint,1,opt,name=retCode,proto3" json:"retCode,omitempty"`
+ ErrMsg string `protobuf:"bytes,2,opt,name=errMsg,proto3" json:"errMsg,omitempty"`
+ UserId []string `protobuf:"bytes,3,rep,name=userId,proto3" json:"userId,omitempty"`
+ XXX_NoUnkeyedLiteral struct{} `json:"-"`
+ XXX_unrecognized []byte `json:"-"`
+ XXX_sizecache int32 `json:"-"`
+}
+
+func (m *GetUserListRsp) Reset() { *m = GetUserListRsp{} }
+func (m *GetUserListRsp) String() string { return proto.CompactTextString(m) }
+func (*GetUserListRsp) ProtoMessage() {}
+func (*GetUserListRsp) Descriptor() ([]byte, []int) {
+ return fileDescriptor_62691ff0994625a6, []int{7}
+}
+
+func (m *GetUserListRsp) XXX_Unmarshal(b []byte) error {
+ return xxx_messageInfo_GetUserListRsp.Unmarshal(m, b)
+}
+func (m *GetUserListRsp) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
+ return xxx_messageInfo_GetUserListRsp.Marshal(b, m, deterministic)
+}
+func (m *GetUserListRsp) XXX_Merge(src proto.Message) {
+ xxx_messageInfo_GetUserListRsp.Merge(m, src)
+}
+func (m *GetUserListRsp) XXX_Size() int {
+ return xxx_messageInfo_GetUserListRsp.Size(m)
+}
+func (m *GetUserListRsp) XXX_DiscardUnknown() {
+ xxx_messageInfo_GetUserListRsp.DiscardUnknown(m)
+}
+
+var xxx_messageInfo_GetUserListRsp proto.InternalMessageInfo
+
+func (m *GetUserListRsp) GetRetCode() uint32 {
+ if m != nil {
+ return m.RetCode
+ }
+ return 0
+}
+
+func (m *GetUserListRsp) GetErrMsg() string {
+ if m != nil {
+ return m.ErrMsg
+ }
+ return ""
+}
+
+func (m *GetUserListRsp) GetUserId() []string {
+ if m != nil {
+ return m.UserId
+ }
+ return nil
+}
+
+func init() {
+ proto.RegisterType((*QueryUsersOnlineReq)(nil), "protobuf.QueryUsersOnlineReq")
+ proto.RegisterType((*QueryUsersOnlineRsp)(nil), "protobuf.QueryUsersOnlineRsp")
+ proto.RegisterType((*SendMsgReq)(nil), "protobuf.SendMsgReq")
+ proto.RegisterType((*SendMsgRsp)(nil), "protobuf.SendMsgRsp")
+ proto.RegisterType((*SendMsgAllReq)(nil), "protobuf.SendMsgAllReq")
+ proto.RegisterType((*SendMsgAllRsp)(nil), "protobuf.SendMsgAllRsp")
+ proto.RegisterType((*GetUserListReq)(nil), "protobuf.GetUserListReq")
+ proto.RegisterType((*GetUserListRsp)(nil), "protobuf.GetUserListRsp")
+}
+
+func init() { proto.RegisterFile("im_protobuf.proto", fileDescriptor_62691ff0994625a6) }
+
+var fileDescriptor_62691ff0994625a6 = []byte{
+ // 413 bytes of a gzipped FileDescriptorProto
+ 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xc4, 0x53, 0x4d, 0xab, 0xd3, 0x40,
+ 0x14, 0x35, 0xcd, 0x7b, 0xed, 0xcb, 0x95, 0xca, 0x73, 0x7c, 0xe8, 0x18, 0x14, 0xca, 0x2c, 0xa4,
+ 0x0b, 0xc9, 0x42, 0x17, 0x6e, 0x6d, 0xbb, 0x90, 0x42, 0x8b, 0x35, 0xc5, 0x8d, 0x08, 0xa5, 0x4d,
+ 0xae, 0x21, 0x90, 0x34, 0xd3, 0xb9, 0xa9, 0xd8, 0x5f, 0xe0, 0xbf, 0xd0, 0xbf, 0x2a, 0x33, 0xf9,
+ 0x6a, 0x35, 0x75, 0x51, 0x10, 0x57, 0xb9, 0xe7, 0xde, 0x73, 0x0f, 0x67, 0x2e, 0x27, 0xf0, 0x30,
+ 0x4e, 0x57, 0x52, 0x65, 0x79, 0xb6, 0xd9, 0x7f, 0xf1, 0x4c, 0xc1, 0x6e, 0x2a, 0x2c, 0x26, 0xf0,
+ 0xe8, 0xc3, 0x1e, 0xd5, 0xe1, 0x23, 0xa1, 0xa2, 0xf7, 0xdb, 0x24, 0xde, 0xa2, 0x8f, 0x3b, 0x76,
+ 0x07, 0xd7, 0x6b, 0x29, 0xa7, 0x21, 0xb7, 0x06, 0xd6, 0xb0, 0xef, 0x17, 0x80, 0x3d, 0x86, 0xee,
+ 0x9e, 0x50, 0x4d, 0x43, 0xde, 0x19, 0x58, 0x43, 0xc7, 0x2f, 0x91, 0x58, 0xb5, 0x88, 0x90, 0x64,
+ 0x1c, 0x7a, 0x0a, 0xf3, 0x49, 0x16, 0x62, 0x29, 0x53, 0x41, 0x2d, 0x84, 0x4a, 0xcd, 0x29, 0xaa,
+ 0x84, 0x0a, 0xa4, 0xfb, 0x99, 0x59, 0xe7, 0xf6, 0xc0, 0x1a, 0xde, 0xf8, 0x25, 0x12, 0x3f, 0x2c,
+ 0x80, 0x25, 0x6e, 0xc3, 0x39, 0x45, 0xda, 0xdd, 0x2d, 0xd8, 0x84, 0x3b, 0x23, 0xea, 0xf8, 0xba,
+ 0x6c, 0xfc, 0x76, 0xda, 0xfd, 0xda, 0xc7, 0x7e, 0xf5, 0x7e, 0x90, 0x12, 0xbf, 0x2a, 0xf6, 0x83,
+ 0x94, 0x18, 0x83, 0xab, 0xfc, 0x20, 0x91, 0x5f, 0x9b, 0x96, 0xa9, 0x35, 0x2b, 0xa5, 0x88, 0x77,
+ 0x0b, 0x56, 0x4a, 0x91, 0x7e, 0x50, 0x4c, 0xb3, 0x2c, 0x58, 0x27, 0xbc, 0x67, 0xfc, 0x55, 0x50,
+ 0x7c, 0x6e, 0xfc, 0x5d, 0xf4, 0xf0, 0x67, 0xe0, 0x50, 0xb1, 0x5f, 0x9b, 0x6d, 0x1a, 0xe2, 0xbb,
+ 0x05, 0xfd, 0x52, 0x7e, 0x94, 0x24, 0xff, 0xf1, 0x02, 0x62, 0x75, 0x62, 0xe4, 0x1f, 0x3c, 0xf5,
+ 0x05, 0x3c, 0x78, 0x87, 0xb9, 0x0e, 0xd2, 0x2c, 0xa6, 0xfc, 0x6c, 0x14, 0xc5, 0xa7, 0x53, 0xde,
+ 0xa5, 0x69, 0xab, 0x8f, 0x63, 0x37, 0xc7, 0x79, 0xf5, 0xb3, 0x03, 0xce, 0x28, 0x08, 0x96, 0xa8,
+ 0xbe, 0xa2, 0x62, 0x3e, 0xdc, 0xfe, 0x1e, 0x6e, 0xf6, 0xdc, 0xab, 0x7f, 0xa8, 0x96, 0xbf, 0xc7,
+ 0xfd, 0xdb, 0x98, 0xa4, 0xb8, 0xc7, 0xde, 0x40, 0xaf, 0x3c, 0x23, 0xbb, 0x6b, 0xb8, 0x4d, 0xc2,
+ 0xdd, 0x96, 0xae, 0x59, 0x7c, 0x5b, 0xe7, 0x6c, 0x94, 0x24, 0xec, 0xc9, 0x1f, 0xac, 0x22, 0x1e,
+ 0x6e, 0xfb, 0xc0, 0x28, 0x4c, 0xe0, 0xfe, 0xd1, 0xe1, 0x18, 0x6f, 0x98, 0xa7, 0x77, 0x77, 0xcf,
+ 0x4c, 0xb4, 0xc8, 0xf8, 0x25, 0x3c, 0x8d, 0x33, 0x2f, 0x52, 0x32, 0xf0, 0xf0, 0xdb, 0x3a, 0x95,
+ 0x09, 0x52, 0xcd, 0x1e, 0xf7, 0x17, 0x65, 0x65, 0xbe, 0x0b, 0x6b, 0xd3, 0x35, 0xa3, 0xd7, 0xbf,
+ 0x02, 0x00, 0x00, 0xff, 0xff, 0x53, 0x3e, 0x6a, 0x5a, 0x89, 0x04, 0x00, 0x00,
+}
+
+// Reference imports to suppress errors if they are not otherwise used.
+var _ context.Context
+var _ grpc.ClientConn
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+const _ = grpc.SupportPackageIsVersion4
+
+// AccServerClient is the client API for AccServer service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream.
+type AccServerClient interface {
+ // 查询用户是否在线
+ QueryUsersOnline(ctx context.Context, in *QueryUsersOnlineReq, opts ...grpc.CallOption) (*QueryUsersOnlineRsp, error)
+ // 发送消息
+ SendMsg(ctx context.Context, in *SendMsgReq, opts ...grpc.CallOption) (*SendMsgRsp, error)
+ // 发送消息
+ SendMsgAll(ctx context.Context, in *SendMsgAllReq, opts ...grpc.CallOption) (*SendMsgAllRsp, error)
+ // 获取用户列表
+ GetUserList(ctx context.Context, in *GetUserListReq, opts ...grpc.CallOption) (*GetUserListRsp, error)
+}
+
+type accServerClient struct {
+ cc *grpc.ClientConn
+}
+
+func NewAccServerClient(cc *grpc.ClientConn) AccServerClient {
+ return &accServerClient{cc}
+}
+
+func (c *accServerClient) QueryUsersOnline(ctx context.Context, in *QueryUsersOnlineReq, opts ...grpc.CallOption) (*QueryUsersOnlineRsp, error) {
+ out := new(QueryUsersOnlineRsp)
+ err := c.cc.Invoke(ctx, "/protobuf.AccServer/QueryUsersOnline", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *accServerClient) SendMsg(ctx context.Context, in *SendMsgReq, opts ...grpc.CallOption) (*SendMsgRsp, error) {
+ out := new(SendMsgRsp)
+ err := c.cc.Invoke(ctx, "/protobuf.AccServer/SendMsg", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *accServerClient) SendMsgAll(ctx context.Context, in *SendMsgAllReq, opts ...grpc.CallOption) (*SendMsgAllRsp, error) {
+ out := new(SendMsgAllRsp)
+ err := c.cc.Invoke(ctx, "/protobuf.AccServer/SendMsgAll", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *accServerClient) GetUserList(ctx context.Context, in *GetUserListReq, opts ...grpc.CallOption) (*GetUserListRsp, error) {
+ out := new(GetUserListRsp)
+ err := c.cc.Invoke(ctx, "/protobuf.AccServer/GetUserList", in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// AccServerServer is the server API for AccServer service.
+type AccServerServer interface {
+ // 查询用户是否在线
+ QueryUsersOnline(context.Context, *QueryUsersOnlineReq) (*QueryUsersOnlineRsp, error)
+ // 发送消息
+ SendMsg(context.Context, *SendMsgReq) (*SendMsgRsp, error)
+ // 发送消息
+ SendMsgAll(context.Context, *SendMsgAllReq) (*SendMsgAllRsp, error)
+ // 获取用户列表
+ GetUserList(context.Context, *GetUserListReq) (*GetUserListRsp, error)
+}
+
+// UnimplementedAccServerServer can be embedded to have forward compatible implementations.
+type UnimplementedAccServerServer struct {
+}
+
+func (*UnimplementedAccServerServer) QueryUsersOnline(ctx context.Context, req *QueryUsersOnlineReq) (*QueryUsersOnlineRsp, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method QueryUsersOnline not implemented")
+}
+func (*UnimplementedAccServerServer) SendMsg(ctx context.Context, req *SendMsgReq) (*SendMsgRsp, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SendMsg not implemented")
+}
+func (*UnimplementedAccServerServer) SendMsgAll(ctx context.Context, req *SendMsgAllReq) (*SendMsgAllRsp, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SendMsgAll not implemented")
+}
+func (*UnimplementedAccServerServer) GetUserList(ctx context.Context, req *GetUserListReq) (*GetUserListRsp, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetUserList not implemented")
+}
+
+func RegisterAccServerServer(s *grpc.Server, srv AccServerServer) {
+ s.RegisterService(&_AccServer_serviceDesc, srv)
+}
+
+func _AccServer_QueryUsersOnline_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(QueryUsersOnlineReq)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AccServerServer).QueryUsersOnline(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/protobuf.AccServer/QueryUsersOnline",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AccServerServer).QueryUsersOnline(ctx, req.(*QueryUsersOnlineReq))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _AccServer_SendMsg_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SendMsgReq)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AccServerServer).SendMsg(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/protobuf.AccServer/SendMsg",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AccServerServer).SendMsg(ctx, req.(*SendMsgReq))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _AccServer_SendMsgAll_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SendMsgAllReq)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AccServerServer).SendMsgAll(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/protobuf.AccServer/SendMsgAll",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AccServerServer).SendMsgAll(ctx, req.(*SendMsgAllReq))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _AccServer_GetUserList_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetUserListReq)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(AccServerServer).GetUserList(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: "/protobuf.AccServer/GetUserList",
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(AccServerServer).GetUserList(ctx, req.(*GetUserListReq))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+var _AccServer_serviceDesc = grpc.ServiceDesc{
+ ServiceName: "protobuf.AccServer",
+ HandlerType: (*AccServerServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "QueryUsersOnline",
+ Handler: _AccServer_QueryUsersOnline_Handler,
+ },
+ {
+ MethodName: "SendMsg",
+ Handler: _AccServer_SendMsg_Handler,
+ },
+ {
+ MethodName: "SendMsgAll",
+ Handler: _AccServer_SendMsgAll_Handler,
+ },
+ {
+ MethodName: "GetUserList",
+ Handler: _AccServer_GetUserList_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "im_protobuf.proto",
+}
diff --git a/protobuf/im_protobuf.proto b/protobuf/im_protobuf.proto
new file mode 100644
index 0000000..7729ff2
--- /dev/null
+++ b/protobuf/im_protobuf.proto
@@ -0,0 +1,80 @@
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "io.grpc.examples.protobuf";
+option java_outer_classname = "ProtobufProto";
+
+
+package protobuf;
+
+// The AccServer service definition.
+service AccServer {
+ // 查询用户是否在线
+ rpc QueryUsersOnline (QueryUsersOnlineReq) returns (QueryUsersOnlineRsp) {
+ }
+ // 发送消息
+ rpc SendMsg (SendMsgReq) returns (SendMsgRsp) {
+ }
+ // 给这台机器的房间内所有用户发送消息
+ rpc SendMsgAll (SendMsgAllReq) returns (SendMsgAllRsp) {
+ }
+ // 获取用户列表
+ rpc GetUserList (GetUserListReq) returns (GetUserListRsp) {
+ }
+}
+
+// 查询用户是否在线
+message QueryUsersOnlineReq {
+ uint32 appId = 1; // AppID
+ string userId = 2; // 用户ID
+}
+
+message QueryUsersOnlineRsp {
+ uint32 retCode = 1;
+ string errMsg = 2;
+ bool online = 3;
+}
+
+// 发送消息
+message SendMsgReq {
+ string seq = 1; // 序列号
+ uint32 appId = 2; // appId/房间Id
+ string userId = 3; // 用户ID
+ string cms = 4; // cms 动作: msg/enter/exit
+ string type = 5; // type 消息类型,默认是 text
+ string msg = 6; // msg
+ bool isLocal = 7; // 是否查询本机 acc内部调用为:true(本机查询不到即结束)
+}
+
+message SendMsgRsp {
+ uint32 retCode = 1;
+ string errMsg = 2;
+ string sendMsgId = 3;
+}
+
+// 给这台机器的房间内所有用户发送消息
+message SendMsgAllReq {
+ string seq = 1; // 序列号
+ uint32 appId = 2; // appId/房间Id
+ string userId = 3; // 不发送的用户ID
+ string cms = 4; // cms 动作: msg/enter/exit
+ string type = 5; // type 消息类型,默认是 text
+ string msg = 6; // msg
+}
+
+message SendMsgAllRsp {
+ uint32 retCode = 1;
+ string errMsg = 2;
+ string sendMsgId = 3;
+}
+
+// 获取用户列表
+message GetUserListReq {
+ uint32 appId = 1;
+}
+
+message GetUserListRsp {
+ uint32 retCode = 1;
+ string errMsg = 2;
+ repeated string userId = 3;
+}
\ No newline at end of file
diff --git a/routers/acc_routers.go b/routers/acc_routers.go
new file mode 100644
index 0000000..86e7f78
--- /dev/null
+++ b/routers/acc_routers.go
@@ -0,0 +1,19 @@
+/**
+ * Created by GoLand.
+ * User: link1st
+ * Date: 2019-07-25
+ * Time: 16:02
+ */
+
+package routers
+
+import (
+ "gowebsocket/servers/websocket"
+)
+
+// Websocket 路由
+func WebsocketInit() {
+ websocket.Register("login", websocket.LoginController)
+ websocket.Register("heartbeat", websocket.HeartbeatController)
+ websocket.Register("ping", websocket.PingController)
+}
diff --git a/routers/web_routers.go b/routers/web_routers.go
new file mode 100644
index 0000000..24d2a7d
--- /dev/null
+++ b/routers/web_routers.go
@@ -0,0 +1,42 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-25
+* Time: 12:20
+ */
+
+package routers
+
+import (
+ "github.com/gin-gonic/gin"
+ "gowebsocket/controllers/home"
+ "gowebsocket/controllers/systems"
+ "gowebsocket/controllers/user"
+)
+
+func Init(router *gin.Engine) {
+ router.LoadHTMLGlob("views/**/*")
+
+ // 用户组
+ userRouter := router.Group("/user")
+ {
+ userRouter.GET("/list", user.List)
+ userRouter.GET("/online", user.Online)
+ userRouter.POST("/sendMessage", user.SendMessage)
+ userRouter.POST("/sendMessageAll", user.SendMessageAll)
+ }
+
+ // 系统
+ systemRouter := router.Group("/system")
+ {
+ systemRouter.GET("/state", systems.Status)
+ }
+
+ // home
+ homeRouter := router.Group("/home")
+ {
+ homeRouter.GET("/index", home.Index)
+ }
+
+ // router.POST("/user/online", user.Online)
+}
diff --git a/servers/grpcclient/grpc_client.go b/servers/grpcclient/grpc_client.go
new file mode 100644
index 0000000..04427e8
--- /dev/null
+++ b/servers/grpcclient/grpc_client.go
@@ -0,0 +1,149 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-08-03
+* Time: 16:43
+ */
+
+package grpcclient
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "google.golang.org/grpc"
+ "gowebsocket/common"
+ "gowebsocket/models"
+ "gowebsocket/protobuf"
+ "time"
+)
+
+// rpc client
+// 给全体用户发送消息
+// link::https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_client/main.go
+func SendMsgAll(server *models.Server, seq string, appId uint32, userId string, cmd string, message string) (sendMsgId string, err error) {
+ // Set up a connection to the server.
+ conn, err := grpc.Dial(server.String(), grpc.WithInsecure())
+ if err != nil {
+ fmt.Println("连接失败", server.String())
+
+ return
+ }
+ defer conn.Close()
+
+ c := protobuf.NewAccServerClient(conn)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ req := protobuf.SendMsgAllReq{
+ Seq: seq,
+ AppId: appId,
+ UserId: userId,
+ Cms: cmd,
+ Msg: message,
+ }
+ rsp, err := c.SendMsgAll(ctx, &req)
+ if err != nil {
+ fmt.Println("给全体用户发送消息", err)
+
+ return
+ }
+
+ if rsp.GetRetCode() != common.OK {
+ fmt.Println("给全体用户发送消息", rsp.String())
+ err = errors.New(fmt.Sprintf("发送消息失败 code:%d", rsp.GetRetCode()))
+
+ return
+ }
+
+ sendMsgId = rsp.GetSendMsgId()
+ fmt.Println("给全体用户发送消息 成功:", sendMsgId)
+
+ return
+}
+
+// 获取用户列表
+// link::https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_client/main.go
+func GetUserList(server *models.Server, appId uint32) (userIds []string, err error) {
+ userIds = make([]string, 0)
+
+ conn, err := grpc.Dial(server.String(), grpc.WithInsecure())
+ if err != nil {
+ fmt.Println("连接失败", server.String())
+
+ return
+ }
+ defer conn.Close()
+
+ c := protobuf.NewAccServerClient(conn)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ req := protobuf.GetUserListReq{
+ AppId: appId,
+ }
+ rsp, err := c.GetUserList(ctx, &req)
+ if err != nil {
+ fmt.Println("获取用户列表 发送请求错误:", err)
+
+ return
+ }
+
+ if rsp.GetRetCode() != common.OK {
+ fmt.Println("获取用户列表 返回码错误:", rsp.String())
+ err = errors.New(fmt.Sprintf("发送消息失败 code:%d", rsp.GetRetCode()))
+
+ return
+ }
+
+ userIds = rsp.GetUserId()
+ fmt.Println("获取用户列表 成功:", userIds)
+
+ return
+}
+
+// rpc client
+// 发送消息
+// link::https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_client/main.go
+func SendMsg(server *models.Server, seq string, appId uint32, userId string, cmd string, msgType string, message string) (sendMsgId string, err error) {
+ // Set up a connection to the server.
+ conn, err := grpc.Dial(server.String(), grpc.WithInsecure())
+ if err != nil {
+ fmt.Println("连接失败", server.String())
+
+ return
+ }
+ defer conn.Close()
+
+ c := protobuf.NewAccServerClient(conn)
+ ctx, cancel := context.WithTimeout(context.Background(), time.Second)
+ defer cancel()
+
+ req := protobuf.SendMsgReq{
+ Seq: seq,
+ AppId: appId,
+ UserId: userId,
+ Cms: cmd,
+ Type: msgType,
+ Msg: message,
+ IsLocal: false,
+ }
+ rsp, err := c.SendMsg(ctx, &req)
+ if err != nil {
+ fmt.Println("发送消息", err)
+
+ return
+ }
+
+ if rsp.GetRetCode() != common.OK {
+ fmt.Println("发送消息", rsp.String())
+ err = errors.New(fmt.Sprintf("发送消息失败 code:%d", rsp.GetRetCode()))
+
+ return
+ }
+
+ sendMsgId = rsp.GetSendMsgId()
+ fmt.Println("发送消息 成功:", sendMsgId)
+
+ return
+}
diff --git a/servers/grpcserver/grpc_server.go b/servers/grpcserver/grpc_server.go
new file mode 100644
index 0000000..eeb7393
--- /dev/null
+++ b/servers/grpcserver/grpc_server.go
@@ -0,0 +1,153 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-08-03
+* Time: 16:43
+ */
+
+package grpcserver
+
+import (
+ "context"
+ "fmt"
+ "github.com/golang/protobuf/proto"
+ "github.com/spf13/viper"
+ "google.golang.org/grpc"
+ "gowebsocket/common"
+ "gowebsocket/models"
+ "gowebsocket/protobuf"
+ "gowebsocket/servers/websocket"
+ "log"
+ "net"
+)
+
+type server struct {
+}
+
+func setErr(rsp proto.Message, code uint32, message string) {
+
+ message = common.GetErrorMessage(code, message)
+ switch v := rsp.(type) {
+ case *protobuf.QueryUsersOnlineRsp:
+ v.RetCode = code
+ v.ErrMsg = message
+ case *protobuf.SendMsgRsp:
+ v.RetCode = code
+ v.ErrMsg = message
+ case *protobuf.SendMsgAllRsp:
+ v.RetCode = code
+ v.ErrMsg = message
+ case *protobuf.GetUserListRsp:
+ v.RetCode = code
+ v.ErrMsg = message
+ default:
+
+ }
+
+}
+
+// 查询用户是否在线
+func (s *server) QueryUsersOnline(c context.Context, req *protobuf.QueryUsersOnlineReq) (rsp *protobuf.QueryUsersOnlineRsp, err error) {
+
+ fmt.Println("grpc_request 查询用户是否在线", req.String())
+
+ rsp = &protobuf.QueryUsersOnlineRsp{}
+
+ online := websocket.CheckUserOnline(req.GetAppId(), req.GetUserId())
+
+ setErr(req, common.OK, "")
+ rsp.Online = online
+
+ return rsp, nil
+}
+
+// 给本机用户发消息
+func (s *server) SendMsg(c context.Context, req *protobuf.SendMsgReq) (rsp *protobuf.SendMsgRsp, err error) {
+
+ fmt.Println("grpc_request 给本机用户发消息", req.String())
+
+ rsp = &protobuf.SendMsgRsp{}
+
+ if req.GetIsLocal() {
+
+ // 不支持
+ setErr(rsp, common.ParameterIllegal, "")
+
+ return
+ }
+
+ data := models.GetMsgData(req.GetUserId(), req.GetSeq(), req.GetCms(), req.GetMsg())
+ sendResults, err := websocket.SendUserMessageLocal(req.GetAppId(), req.GetUserId(), data)
+ if err != nil {
+ fmt.Println("系统错误", err)
+ setErr(rsp, common.ServerError, "")
+
+ return rsp, nil
+ }
+
+ if !sendResults {
+ fmt.Println("发送失败", err)
+ setErr(rsp, common.OperationFailure, "")
+
+ return rsp, nil
+ }
+
+ setErr(rsp, common.OK, "")
+
+ fmt.Println("grpc_response 给本机用户发消息", rsp.String())
+ return
+}
+
+// 给本机全体用户发消息
+func (s *server) SendMsgAll(c context.Context, req *protobuf.SendMsgAllReq) (rsp *protobuf.SendMsgAllRsp, err error) {
+
+ fmt.Println("grpc_request 给本机全体用户发消息", req.String())
+
+ rsp = &protobuf.SendMsgAllRsp{}
+
+ data := models.GetMsgData(req.GetUserId(), req.GetSeq(), req.GetCms(), req.GetMsg())
+ websocket.AllSendMessages(req.GetAppId(), req.GetUserId(), data)
+
+ setErr(rsp, common.OK, "")
+
+ fmt.Println("grpc_response 给本机全体用户发消息:", rsp.String())
+
+ return
+}
+
+// 获取本机用户列表
+func (s *server) GetUserList(c context.Context, req *protobuf.GetUserListReq) (rsp *protobuf.GetUserListRsp, err error) {
+
+ fmt.Println("grpc_request 获取本机用户列表", req.String())
+
+ appId := req.GetAppId()
+ rsp = &protobuf.GetUserListRsp{}
+
+ // 本机
+ userList := websocket.GetUserList(appId)
+
+ setErr(rsp, common.OK, "")
+ rsp.UserId = userList
+
+ fmt.Println("grpc_response 获取本机用户列表:", rsp.String())
+
+ return
+}
+
+// rpc server
+// link::https://github.com/grpc/grpc-go/blob/master/examples/helloworld/greeter_server/main.go
+func Init() {
+
+ rpcPort := viper.GetString("app.rpcPort")
+ fmt.Println("rpc server 启动", rpcPort)
+
+ lis, err := net.Listen("tcp", ":"+rpcPort)
+ if err != nil {
+ log.Fatalf("failed to listen: %v", err)
+ }
+ s := grpc.NewServer()
+ protobuf.RegisterAccServerServer(s, &server{})
+ if err := s.Serve(lis); err != nil {
+ log.Fatalf("failed to serve: %v", err)
+ }
+}
diff --git a/servers/task/clean_connection _task.go b/servers/task/clean_connection _task.go
new file mode 100644
index 0000000..e98cfe8
--- /dev/null
+++ b/servers/task/clean_connection _task.go
@@ -0,0 +1,37 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-31
+* Time: 15:17
+ */
+
+package task
+
+import (
+ "fmt"
+ "gowebsocket/servers/websocket"
+ "runtime/debug"
+ "time"
+)
+
+func Init() {
+ Timer(3*time.Second, 30*time.Second, cleanConnection, "", nil, nil)
+
+}
+
+// 清理超时连接
+func cleanConnection(param interface{}) (result bool) {
+ result = true
+
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("ClearTimeoutConnections stop", r, string(debug.Stack()))
+ }
+ }()
+
+ fmt.Println("定时任务,清理超时连接", param)
+
+ websocket.ClearTimeoutConnections()
+
+ return
+}
diff --git a/servers/task/server_task.go b/servers/task/server_task.go
new file mode 100644
index 0000000..9b03c44
--- /dev/null
+++ b/servers/task/server_task.go
@@ -0,0 +1,55 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-08-03
+* Time: 15:44
+ */
+
+package task
+
+import (
+ "fmt"
+ "gowebsocket/lib/cache"
+ "gowebsocket/servers/websocket"
+ "runtime/debug"
+ "time"
+)
+
+func ServerInit() {
+ Timer(2*time.Second, 60*time.Second, server, "", serverDefer, "")
+}
+
+// 服务注册
+func server(param interface{}) (result bool) {
+ result = true
+
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("服务注册 stop", r, string(debug.Stack()))
+ }
+ }()
+
+ server := websocket.GetServer()
+ currentTime := uint64(time.Now().Unix())
+ fmt.Println("定时任务,服务注册", param, server, currentTime)
+
+ cache.SetServerInfo(server, currentTime)
+
+ return
+}
+
+// 服务下线
+func serverDefer(param interface{}) (result bool) {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("服务下线 stop", r, string(debug.Stack()))
+ }
+ }()
+
+ fmt.Println("服务下线", param)
+
+ server := websocket.GetServer()
+ cache.DelServerInfo(server)
+
+ return
+}
diff --git a/servers/task/task_init.go b/servers/task/task_init.go
new file mode 100644
index 0000000..9495d9d
--- /dev/null
+++ b/servers/task/task_init.go
@@ -0,0 +1,46 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-29
+* Time: 14:15
+ */
+
+package task
+
+import "time"
+
+type TimerFunc func(interface{}) bool
+
+/**
+ * 定时调用
+ * @delay 首次延时
+ * @tick 间隔
+ * @fun 定时执行function
+ * @param fun参数
+ */
+func Timer(delay, tick time.Duration, fun TimerFunc, param interface{}, funcDefer TimerFunc, paramDefer interface{}) {
+ go func() {
+ defer func() {
+ if funcDefer != nil {
+ funcDefer(paramDefer)
+ }
+ }()
+
+ if fun == nil {
+ return
+ }
+
+ t := time.NewTimer(delay)
+ defer t.Stop()
+
+ for {
+ select {
+ case <-t.C:
+ if fun(param) == false {
+ return
+ }
+ t.Reset(tick)
+ }
+ }
+ }()
+}
diff --git a/servers/websocket/acc_controller.go b/servers/websocket/acc_controller.go
new file mode 100644
index 0000000..ad2a858
--- /dev/null
+++ b/servers/websocket/acc_controller.go
@@ -0,0 +1,144 @@
+/**
+ * Created by GoLand.
+ * User: link1st
+ * Date: 2019-07-27
+ * Time: 13:12
+ */
+
+package websocket
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/go-redis/redis"
+ "gowebsocket/common"
+ "gowebsocket/lib/cache"
+ "gowebsocket/models"
+ "time"
+)
+
+// ping
+func PingController(client *Client, seq string, message []byte) (code uint32, msg string, data interface{}) {
+
+ code = common.OK
+ fmt.Println("webSocket_request ping接口", client.Addr, seq, message)
+
+ data = "pong"
+
+ return
+}
+
+// 用户登录
+func LoginController(client *Client, seq string, message []byte) (code uint32, msg string, data interface{}) {
+
+ code = common.OK
+ currentTime := uint64(time.Now().Unix())
+
+ request := &models.Login{}
+ if err := json.Unmarshal(message, request); err != nil {
+ code = common.ParameterIllegal
+ fmt.Println("用户登录 解析数据失败", seq, err)
+
+ return
+ }
+
+ fmt.Println("webSocket_request 用户登录", seq, "ServiceToken", request.ServiceToken)
+
+ // TODO::进行用户权限认证,一般是客户端传入TOKEN,然后检验TOKEN是否合法,通过TOKEN解析出来用户ID
+ // 本项目只是演示,所以直接过去客户端传入的用户ID
+ if request.UserId == "" || len(request.UserId) >= 20 {
+ code = common.UnauthorizedUserId
+ fmt.Println("用户登录 非法的用户", seq, request.UserId)
+
+ return
+ }
+
+ if !InAppIds(request.AppId) {
+ code = common.Unauthorized
+ fmt.Println("用户登录 不支持的平台", seq, request.AppId)
+
+ return
+ }
+
+ if client.IsLogin() {
+ fmt.Println("用户登录 用户已经登录", client.AppId, client.UserId, seq)
+ code = common.OperationFailure
+
+ return
+ }
+
+ client.Login(request.AppId, request.UserId, currentTime)
+
+ // 存储数据
+ userOnline := models.UserLogin(serverIp, serverPort, request.AppId, request.UserId, client.Addr, currentTime)
+ err := cache.SetUserOnlineInfo(client.GetKey(), userOnline)
+ if err != nil {
+ code = common.ServerError
+ fmt.Println("用户登录 SetUserOnlineInfo", seq, err)
+
+ return
+ }
+
+ // 用户登录
+ login := &login{
+ AppId: request.AppId,
+ UserId: request.UserId,
+ Client: client,
+ }
+ clientManager.Login <- login
+
+ fmt.Println("用户登录 成功", seq, client.Addr, request.UserId)
+
+ return
+}
+
+// 心跳接口
+func HeartbeatController(client *Client, seq string, message []byte) (code uint32, msg string, data interface{}) {
+
+ code = common.OK
+ currentTime := uint64(time.Now().Unix())
+
+ request := &models.HeartBeat{}
+ if err := json.Unmarshal(message, request); err != nil {
+ code = common.ParameterIllegal
+ fmt.Println("心跳接口 解析数据失败", seq, err)
+
+ return
+ }
+
+ fmt.Println("webSocket_request 心跳接口", client.AppId, client.UserId)
+
+ if !client.IsLogin() {
+ fmt.Println("心跳接口 用户未登录", client.AppId, client.UserId, seq)
+ code = common.NotLoggedIn
+
+ return
+ }
+
+ userOnline, err := cache.GetUserOnlineInfo(client.GetKey())
+ if err != nil {
+ if err == redis.Nil {
+ code = common.NotLoggedIn
+ fmt.Println("心跳接口 用户未登录", seq, client.AppId, client.UserId)
+
+ return
+ } else {
+ code = common.ServerError
+ fmt.Println("心跳接口 GetUserOnlineInfo", seq, client.AppId, client.UserId, err)
+
+ return
+ }
+ }
+
+ client.Heartbeat(currentTime)
+ userOnline.Heartbeat(currentTime)
+ err = cache.SetUserOnlineInfo(client.GetKey(), userOnline)
+ if err != nil {
+ code = common.ServerError
+ fmt.Println("心跳接口 SetUserOnlineInfo", seq, client.AppId, client.UserId, err)
+
+ return
+ }
+
+ return
+}
diff --git a/servers/websocket/acc_process.go b/servers/websocket/acc_process.go
new file mode 100644
index 0000000..54c00ac
--- /dev/null
+++ b/servers/websocket/acc_process.go
@@ -0,0 +1,108 @@
+/**
+ * Created by GoLand.
+ * User: link1st
+ * Date: 2019-07-27
+ * Time: 14:38
+ */
+
+package websocket
+
+import (
+ "encoding/json"
+ "fmt"
+ "gowebsocket/common"
+ "gowebsocket/models"
+ "sync"
+)
+
+type DisposeFunc func(client *Client, seq string, message []byte) (code uint32, msg string, data interface{})
+
+var (
+ handlers = make(map[string]DisposeFunc)
+ handlersRWMutex sync.RWMutex
+)
+
+// 注册
+func Register(key string, value DisposeFunc) {
+ handlersRWMutex.Lock()
+ defer handlersRWMutex.Unlock()
+ handlers[key] = value
+
+ return
+}
+
+func getHandlers(key string) (value DisposeFunc, ok bool) {
+ handlersRWMutex.RLock()
+ defer handlersRWMutex.RUnlock()
+
+ value, ok = handlers[key]
+
+ return
+}
+
+// 处理数据
+func ProcessData(client *Client, message []byte) {
+
+ fmt.Println("处理数据", client.Addr, string(message))
+
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("处理数据 stop", r)
+ }
+ }()
+
+ request := &models.Request{}
+
+ err := json.Unmarshal(message, request)
+ if err != nil {
+ fmt.Println("处理数据 json Unmarshal", err)
+ client.SendMsg([]byte("数据不合法"))
+
+ return
+ }
+
+ requestData, err := json.Marshal(request.Data)
+ if err != nil {
+ fmt.Println("处理数据 json Marshal", err)
+ client.SendMsg([]byte("处理数据失败"))
+
+ return
+ }
+
+ seq := request.Seq
+ cmd := request.Cmd
+
+ var (
+ code uint32
+ msg string
+ data interface{}
+ )
+
+ // request
+ fmt.Println("acc_request", cmd, client.Addr)
+
+ // 采用 map 注册的方式
+ if value, ok := getHandlers(cmd); ok {
+ code, msg, data = value(client, seq, requestData)
+ } else {
+ code = common.RoutingNotExist
+ fmt.Println("处理数据 路由不存在", client.Addr, "cmd", cmd)
+ }
+
+ msg = common.GetErrorMessage(code, msg)
+
+ responseHead := models.NewResponseHead(seq, cmd, code, msg, data)
+
+ headByte, err := json.Marshal(responseHead)
+ if err != nil {
+ fmt.Println("处理数据 json Marshal", err)
+
+ return
+ }
+
+ client.SendMsg(headByte)
+
+ fmt.Println("acc_response send", client.Addr, client.AppId, client.UserId, "cmd", cmd, "code", code)
+
+ return
+}
diff --git a/servers/websocket/client.go b/servers/websocket/client.go
new file mode 100644
index 0000000..6ed4349
--- /dev/null
+++ b/servers/websocket/client.go
@@ -0,0 +1,182 @@
+/**
+ * Created by GoLand.
+ * User: link1st
+ * Date: 2019-07-25
+ * Time: 16:24
+ */
+
+package websocket
+
+import (
+ "fmt"
+ "github.com/gorilla/websocket"
+ "runtime/debug"
+)
+
+const (
+ // 用户连接超时时间
+ heartbeatExpirationTime = 6 * 60
+)
+
+// 用户登录
+type login struct {
+ AppId uint32
+ UserId string
+ Client *Client
+}
+
+// 读取客户端数据
+func (l *login) GetKey() (key string) {
+ key = GetUserKey(l.AppId, l.UserId)
+
+ return
+}
+
+// 用户连接
+type Client struct {
+ Addr string // 客户端地址
+ Socket *websocket.Conn // 用户连接
+ Send chan []byte // 待发送的数据
+ AppId uint32 // 登录的平台Id app/web/ios
+ UserId string // 用户Id,用户登录以后才有
+ FirstTime uint64 // 首次连接事件
+ HeartbeatTime uint64 // 用户上次心跳时间
+ LoginTime uint64 // 登录时间 登录以后才有
+}
+
+// 初始化
+func NewClient(addr string, socket *websocket.Conn, firstTime uint64) (client *Client) {
+ client = &Client{
+ Addr: addr,
+ Socket: socket,
+ Send: make(chan []byte, 100),
+ FirstTime: firstTime,
+ HeartbeatTime: firstTime,
+ }
+
+ return
+}
+
+// 读取客户端数据
+func (c *Client) GetKey() (key string) {
+ key = GetUserKey(c.AppId, c.UserId)
+
+ return
+}
+
+// 读取客户端数据
+func (c *Client) read() {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("write stop", string(debug.Stack()), r)
+ }
+ }()
+
+ defer func() {
+ fmt.Println("读取客户端数据 关闭send", c)
+ close(c.Send)
+ }()
+
+ for {
+ _, message, err := c.Socket.ReadMessage()
+ if err != nil {
+ fmt.Println("读取客户端数据 错误", c.Addr, err)
+
+ return
+ }
+
+ // 处理程序
+ fmt.Println("读取客户端数据 处理:", string(message))
+ ProcessData(c, message)
+ }
+}
+
+// 向客户端写数据
+func (c *Client) write() {
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("write stop", string(debug.Stack()), r)
+
+ }
+ }()
+
+ defer func() {
+ clientManager.Unregister <- c
+ c.Socket.Close()
+ fmt.Println("Client发送数据 defer", c)
+ }()
+
+ for {
+ select {
+ case message, ok := <-c.Send:
+ if !ok {
+ // 发送数据错误 关闭连接
+ fmt.Println("Client发送数据 关闭连接", c.Addr, "ok", ok)
+
+ return
+ }
+
+ c.Socket.WriteMessage(websocket.TextMessage, message)
+ }
+ }
+}
+
+// 读取客户端数据
+func (c *Client) SendMsg(msg []byte) {
+
+ if c == nil {
+
+ return
+ }
+
+ defer func() {
+ if r := recover(); r != nil {
+ fmt.Println("SendMsg stop:", r, string(debug.Stack()))
+ }
+ }()
+
+ c.Send <- msg
+}
+
+// 读取客户端数据
+func (c *Client) close() {
+ close(c.Send)
+}
+
+// 用户登录
+func (c *Client) Login(appId uint32, userId string, loginTime uint64) {
+ c.AppId = appId
+ c.UserId = userId
+ c.LoginTime = loginTime
+ // 登录成功=心跳一次
+ c.Heartbeat(loginTime)
+}
+
+// 用户心跳
+func (c *Client) Heartbeat(currentTime uint64) {
+ c.HeartbeatTime = currentTime
+
+ return
+}
+
+// 心跳超时
+func (c *Client) IsHeartbeatTimeout(currentTime uint64) (timeout bool) {
+ if c.HeartbeatTime+heartbeatExpirationTime <= currentTime {
+ timeout = true
+ }
+
+ return
+}
+
+// 是否登录了
+func (c *Client) IsLogin() (isLogin bool) {
+
+ // 用户登录了
+ if c.UserId != "" {
+ isLogin = true
+
+ return
+ }
+
+ return
+}
diff --git a/servers/websocket/client_manager.go b/servers/websocket/client_manager.go
new file mode 100644
index 0000000..ba0a1f5
--- /dev/null
+++ b/servers/websocket/client_manager.go
@@ -0,0 +1,384 @@
+/**
+ * Created by GoLand.
+ * User: link1st
+ * Date: 2019-07-25
+ * Time: 16:24
+ */
+
+package websocket
+
+import (
+ "fmt"
+ "gowebsocket/helper"
+ "gowebsocket/lib/cache"
+ "gowebsocket/models"
+ "sync"
+ "time"
+)
+
+// 连接管理
+type ClientManager struct {
+ Clients map[*Client]bool // 全部的连接
+ ClientsLock sync.RWMutex // 读写锁
+ Users map[string]*Client // 登录的用户 // appId+uuid
+ UserLock sync.RWMutex // 读写锁
+ Register chan *Client // 连接连接处理
+ Login chan *login // 用户登录处理
+ Unregister chan *Client // 断开连接处理程序
+ Broadcast chan []byte // 广播 向全部成员发送数据
+}
+
+func NewClientManager() (clientManager *ClientManager) {
+ clientManager = &ClientManager{
+ Clients: make(map[*Client]bool),
+ Users: make(map[string]*Client),
+ Register: make(chan *Client, 1000),
+ Login: make(chan *login, 1000),
+ Unregister: make(chan *Client, 1000),
+ Broadcast: make(chan []byte, 1000),
+ }
+
+ return
+}
+
+// 获取用户key
+func GetUserKey(appId uint32, userId string) (key string) {
+ key = fmt.Sprintf("%d_%s", appId, userId)
+
+ return
+}
+
+/************************** manager ***************************************/
+
+func (manager *ClientManager) InClient(client *Client) (ok bool) {
+ manager.ClientsLock.RLock()
+ defer manager.ClientsLock.RUnlock()
+
+ // 连接存在,在添加
+ _, ok = manager.Clients[client]
+
+ return
+}
+
+// GetClients
+func (manager *ClientManager) GetClients() (clients map[*Client]bool) {
+
+ clients = make(map[*Client]bool)
+
+ manager.ClientsRange(func(client *Client, value bool) (result bool) {
+ clients[client] = value
+
+ return true
+ })
+
+ return
+}
+
+// 遍历
+func (manager *ClientManager) ClientsRange(f func(client *Client, value bool) (result bool)) {
+
+ manager.ClientsLock.RLock()
+ defer manager.ClientsLock.RUnlock()
+
+ for key, value := range manager.Clients {
+ result := f(key, value)
+ if result == false {
+ return
+ }
+ }
+
+ return
+}
+
+// GetClientsLen
+func (manager *ClientManager) GetClientsLen() (clientsLen int) {
+
+ clientsLen = len(manager.Clients)
+
+ return
+}
+
+// 添加客户端
+func (manager *ClientManager) AddClients(client *Client) {
+ manager.ClientsLock.Lock()
+ defer manager.ClientsLock.Unlock()
+
+ manager.Clients[client] = true
+}
+
+// 删除客户端
+func (manager *ClientManager) DelClients(client *Client) {
+ manager.ClientsLock.Lock()
+ defer manager.ClientsLock.Unlock()
+
+ if _, ok := manager.Clients[client]; ok {
+ delete(manager.Clients, client)
+ }
+}
+
+// 获取用户的连接
+func (manager *ClientManager) GetUserClient(appId uint32, userId string) (client *Client) {
+
+ manager.UserLock.RLock()
+ defer manager.UserLock.RUnlock()
+
+ userKey := GetUserKey(appId, userId)
+ if value, ok := manager.Users[userKey]; ok {
+ client = value
+ }
+
+ return
+}
+
+// GetClientsLen
+func (manager *ClientManager) GetUsersLen() (userLen int) {
+ userLen = len(manager.Users)
+
+ return
+}
+
+// 添加用户
+func (manager *ClientManager) AddUsers(key string, client *Client) {
+ manager.UserLock.Lock()
+ defer manager.UserLock.Unlock()
+
+ manager.Users[key] = client
+}
+
+// 删除用户
+func (manager *ClientManager) DelUsers(client *Client) (result bool) {
+ manager.UserLock.Lock()
+ defer manager.UserLock.Unlock()
+
+ key := GetUserKey(client.AppId, client.UserId)
+ if value, ok := manager.Users[key]; ok {
+ // 判断是否为相同的用户
+ if value.Addr != client.Addr {
+
+ return
+ }
+ delete(manager.Users, key)
+ result = true
+ }
+
+ return
+}
+
+// 获取用户的key
+func (manager *ClientManager) GetUserKeys() (userKeys []string) {
+
+ userKeys = make([]string, 0)
+ manager.UserLock.RLock()
+ defer manager.UserLock.RUnlock()
+ for key := range manager.Users {
+ userKeys = append(userKeys, key)
+ }
+
+ return
+}
+
+// 获取用户的key
+func (manager *ClientManager) GetUserList(appId uint32) (userList []string) {
+
+ userList = make([]string, 0)
+
+ manager.UserLock.RLock()
+ defer manager.UserLock.RUnlock()
+
+ for _, v := range manager.Users {
+ if v.AppId == appId {
+ userList = append(userList, v.UserId)
+ }
+ }
+
+ fmt.Println("GetUserList len:", len(manager.Users))
+
+ return
+}
+
+// 获取用户的key
+func (manager *ClientManager) GetUserClients() (clients []*Client) {
+
+ clients = make([]*Client, 0)
+ manager.UserLock.RLock()
+ defer manager.UserLock.RUnlock()
+ for _, v := range manager.Users {
+ clients = append(clients, v)
+ }
+
+ return
+}
+
+// 向全部成员(除了自己)发送数据
+func (manager *ClientManager) sendAll(message []byte, ignoreClient *Client) {
+
+ clients := manager.GetUserClients()
+ for _, conn := range clients {
+ if conn != ignoreClient {
+ conn.SendMsg(message)
+ }
+ }
+}
+
+// 向全部成员(除了自己)发送数据
+func (manager *ClientManager) sendAppIdAll(message []byte, appId uint32, ignoreClient *Client) {
+
+ clients := manager.GetUserClients()
+ for _, conn := range clients {
+ if conn != ignoreClient && conn.AppId == appId {
+ conn.SendMsg(message)
+ }
+ }
+}
+
+// 用户建立连接事件
+func (manager *ClientManager) EventRegister(client *Client) {
+ manager.AddClients(client)
+
+ fmt.Println("EventRegister 用户建立连接", client.Addr)
+
+ // client.Send <- []byte("连接成功")
+}
+
+// 用户登录
+func (manager *ClientManager) EventLogin(login *login) {
+
+ client := login.Client
+ // 连接存在,在添加
+ if manager.InClient(client) {
+ userKey := login.GetKey()
+ manager.AddUsers(userKey, login.Client)
+ }
+
+ fmt.Println("EventLogin 用户登录", client.Addr, login.AppId, login.UserId)
+
+ orderId := helper.GetOrderIdTime()
+ SendUserMessageAll(login.AppId, login.UserId, orderId, models.MessageCmdEnter, "哈喽~")
+}
+
+// 用户断开连接
+func (manager *ClientManager) EventUnregister(client *Client) {
+ manager.DelClients(client)
+
+ // 删除用户连接
+ deleteResult := manager.DelUsers(client)
+ if deleteResult == false {
+ // 不是当前连接的客户端
+
+ return
+ }
+
+ // 清除redis登录数据
+ userOnline, err := cache.GetUserOnlineInfo(client.GetKey())
+ if err == nil {
+ userOnline.LogOut()
+ cache.SetUserOnlineInfo(client.GetKey(), userOnline)
+ }
+
+ // 关闭 chan
+ // close(client.Send)
+
+ fmt.Println("EventUnregister 用户断开连接", client.Addr, client.AppId, client.UserId)
+
+ if client.UserId != "" {
+ orderId := helper.GetOrderIdTime()
+ SendUserMessageAll(client.AppId, client.UserId, orderId, models.MessageCmdExit, "用户已经离开~")
+ }
+}
+
+// 管道处理程序
+func (manager *ClientManager) start() {
+ for {
+ select {
+ case conn := <-manager.Register:
+ // 建立连接事件
+ manager.EventRegister(conn)
+
+ case login := <-manager.Login:
+ // 用户登录
+ manager.EventLogin(login)
+
+ case conn := <-manager.Unregister:
+ // 断开连接事件
+ manager.EventUnregister(conn)
+
+ case message := <-manager.Broadcast:
+ // 广播事件
+ clients := manager.GetClients()
+ for conn := range clients {
+ select {
+ case conn.Send <- message:
+ default:
+ close(conn.Send)
+ }
+ }
+ }
+ }
+}
+
+/************************** manager info ***************************************/
+// 获取管理者信息
+func GetManagerInfo(isDebug string) (managerInfo map[string]interface{}) {
+ managerInfo = make(map[string]interface{})
+
+ managerInfo["clientsLen"] = clientManager.GetClientsLen() // 客户端连接数
+ managerInfo["usersLen"] = clientManager.GetUsersLen() // 登录用户数
+ managerInfo["chanRegisterLen"] = len(clientManager.Register) // 未处理连接事件数
+ managerInfo["chanLoginLen"] = len(clientManager.Login) // 未处理登录事件数
+ managerInfo["chanUnregisterLen"] = len(clientManager.Unregister) // 未处理退出登录事件数
+ managerInfo["chanBroadcastLen"] = len(clientManager.Broadcast) // 未处理广播事件数
+
+ if isDebug == "true" {
+ addrList := make([]string, 0)
+ clientManager.ClientsRange(func(client *Client, value bool) (result bool) {
+ addrList = append(addrList, client.Addr)
+
+ return true
+ })
+
+ users := clientManager.GetUserKeys()
+
+ managerInfo["clients"] = addrList // 客户端列表
+ managerInfo["users"] = users // 登录用户列表
+ }
+
+ return
+}
+
+// 获取用户所在的连接
+func GetUserClient(appId uint32, userId string) (client *Client) {
+ client = clientManager.GetUserClient(appId, userId)
+
+ return
+}
+
+// 定时清理超时连接
+func ClearTimeoutConnections() {
+ currentTime := uint64(time.Now().Unix())
+
+ clients := clientManager.GetClients()
+ for client := range clients {
+ if client.IsHeartbeatTimeout(currentTime) {
+ fmt.Println("心跳时间超时 关闭连接", client.Addr, client.UserId, client.LoginTime, client.HeartbeatTime)
+
+ client.Socket.Close()
+ }
+ }
+}
+
+// 获取全部用户
+func GetUserList(appId uint32) (userList []string) {
+ fmt.Println("获取全部用户", appId)
+
+ userList = clientManager.GetUserList(appId)
+
+ return
+}
+
+// 全员广播
+func AllSendMessages(appId uint32, userId string, data string) {
+ fmt.Println("全员广播", appId, userId, data)
+
+ ignoreClient := clientManager.GetUserClient(appId, userId)
+ clientManager.sendAppIdAll([]byte(data), appId, ignoreClient)
+}
diff --git a/servers/websocket/init_acc.go b/servers/websocket/init_acc.go
new file mode 100644
index 0000000..2fd7d4d
--- /dev/null
+++ b/servers/websocket/init_acc.go
@@ -0,0 +1,115 @@
+/**
+ * Created by GoLand.
+ * User: link1st
+ * Date: 2019-07-25
+ * Time: 16:04
+ */
+
+package websocket
+
+import (
+ "fmt"
+ "net/http"
+ "time"
+
+ "gowebsocket/helper"
+ "gowebsocket/models"
+
+ "github.com/gorilla/websocket"
+ "github.com/spf13/viper"
+)
+
+const (
+ defaultAppId = 101 // 默认平台Id
+)
+
+var (
+ clientManager = NewClientManager() // 管理者
+ appIds = []uint32{defaultAppId, 102, 103, 104} // 全部的平台
+
+ serverIp string
+ serverPort string
+)
+
+func GetAppIds() []uint32 {
+
+ return appIds
+}
+
+func GetServer() (server *models.Server) {
+ server = models.NewServer(serverIp, serverPort)
+
+ return
+}
+
+func IsLocal(server *models.Server) (isLocal bool) {
+ if server.Ip == serverIp && server.Port == serverPort {
+ isLocal = true
+ }
+
+ return
+}
+
+func InAppIds(appId uint32) (inAppId bool) {
+
+ for _, value := range appIds {
+ if value == appId {
+ inAppId = true
+
+ return
+ }
+ }
+
+ return
+}
+
+func GetDefaultAppId() (appId uint32) {
+ appId = defaultAppId
+
+ return
+}
+
+// 启动程序
+func StartWebSocket() {
+
+ serverIp = helper.GetServerIp()
+
+ webSocketPort := viper.GetString("app.webSocketPort")
+ rpcPort := viper.GetString("app.rpcPort")
+
+ serverPort = rpcPort
+
+ http.HandleFunc("/acc", wsPage)
+
+ // 添加处理程序
+ go clientManager.start()
+ fmt.Println("WebSocket 启动程序成功", serverIp, serverPort)
+
+ http.ListenAndServe(":"+webSocketPort, nil)
+}
+
+func wsPage(w http.ResponseWriter, req *http.Request) {
+
+ // 升级协议
+ conn, err := (&websocket.Upgrader{CheckOrigin: func(r *http.Request) bool {
+ fmt.Println("升级协议", "ua:", r.Header["User-Agent"], "referer:", r.Header["Referer"])
+
+ return true
+ }}).Upgrade(w, req, nil)
+ if err != nil {
+ http.NotFound(w, req)
+
+ return
+ }
+
+ fmt.Println("webSocket 建立连接:", conn.RemoteAddr().String())
+
+ currentTime := uint64(time.Now().Unix())
+ client := NewClient(conn.RemoteAddr().String(), conn, currentTime)
+
+ go client.read()
+ go client.write()
+
+ // 用户连接事件
+ clientManager.Register <- client
+}
diff --git a/servers/websocket/user_srv.go b/servers/websocket/user_srv.go
new file mode 100644
index 0000000..de597fd
--- /dev/null
+++ b/servers/websocket/user_srv.go
@@ -0,0 +1,165 @@
+/**
+* Created by GoLand.
+* User: link1st
+* Date: 2019-07-30
+* Time: 12:27
+ */
+
+package websocket
+
+import (
+ "errors"
+ "fmt"
+ "github.com/go-redis/redis"
+ "gowebsocket/lib/cache"
+ "gowebsocket/models"
+ "gowebsocket/servers/grpcclient"
+ "time"
+)
+
+// 查询所有用户
+func UserList(appId uint32) (userList []string) {
+
+ userList = make([]string, 0)
+ currentTime := uint64(time.Now().Unix())
+ servers, err := cache.GetServerAll(currentTime)
+ if err != nil {
+ fmt.Println("给全体用户发消息", err)
+
+ return
+ }
+
+ for _, server := range servers {
+ var (
+ list []string
+ )
+ if IsLocal(server) {
+ list = GetUserList(appId)
+ } else {
+ list, _ = grpcclient.GetUserList(server, appId)
+ }
+ userList = append(userList, list...)
+ }
+
+ return
+}
+
+// 查询用户是否在线
+func CheckUserOnline(appId uint32, userId string) (online bool) {
+ // 全平台查询
+ if appId == 0 {
+ for _, appId := range GetAppIds() {
+ online, _ = checkUserOnline(appId, userId)
+ if online == true {
+ break
+ }
+ }
+ } else {
+ online, _ = checkUserOnline(appId, userId)
+ }
+
+ return
+}
+
+// 查询用户 是否在线
+func checkUserOnline(appId uint32, userId string) (online bool, err error) {
+ key := GetUserKey(appId, userId)
+ userOnline, err := cache.GetUserOnlineInfo(key)
+ if err != nil {
+ if err == redis.Nil {
+ fmt.Println("GetUserOnlineInfo", appId, userId, err)
+
+ return false, nil
+ }
+
+ fmt.Println("GetUserOnlineInfo", appId, userId, err)
+
+ return
+ }
+
+ online = userOnline.IsOnline()
+
+ return
+}
+
+// 给用户发送消息
+func SendUserMessage(appId uint32, userId string, msgId, message string) (sendResults bool, err error) {
+
+ data := models.GetTextMsgData(userId, msgId, message)
+
+ client := GetUserClient(appId, userId)
+
+ if client != nil {
+ // 在本机发送
+ sendResults, err = SendUserMessageLocal(appId, userId, data)
+ if err != nil {
+ fmt.Println("给用户发送消息", appId, userId, err)
+ }
+
+ return
+ }
+
+ key := GetUserKey(appId, userId)
+ info, err := cache.GetUserOnlineInfo(key)
+ if err != nil {
+ fmt.Println("给用户发送消息失败", key, err)
+
+ return false, nil
+ }
+ if !info.IsOnline() {
+ fmt.Println("用户不在线", key)
+ return false, nil
+ }
+ server := models.NewServer(info.AccIp, info.AccPort)
+ msg, err := grpcclient.SendMsg(server, msgId, appId, userId, models.MessageCmdMsg, models.MessageCmdMsg, message)
+ if err != nil {
+ fmt.Println("给用户发送消息失败", key, err)
+
+ return false, err
+ }
+ fmt.Println("给用户发送消息成功-rpc", msg)
+ sendResults = true
+
+ return
+}
+
+// 给本机用户发送消息
+func SendUserMessageLocal(appId uint32, userId string, data string) (sendResults bool, err error) {
+
+ client := GetUserClient(appId, userId)
+ if client == nil {
+ err = errors.New("用户不在线")
+
+ return
+ }
+
+ // 发送消息
+ client.SendMsg([]byte(data))
+ sendResults = true
+
+ return
+}
+
+// 给全体用户发消息
+func SendUserMessageAll(appId uint32, userId string, msgId, cmd, message string) (sendResults bool, err error) {
+ sendResults = true
+
+ currentTime := uint64(time.Now().Unix())
+ servers, err := cache.GetServerAll(currentTime)
+ if err != nil {
+ fmt.Println("给全体用户发消息", err)
+
+ return
+ }
+
+ for _, server := range servers {
+ if IsLocal(server) {
+ data := models.GetMsgData(userId, msgId, cmd, message)
+ AllSendMessages(appId, userId, data)
+ } else {
+ grpcclient.SendMsgAll(server, msgId, appId, userId, cmd, message)
+ }
+ }
+
+ return
+}
diff --git a/views/home/index.html b/views/home/index.html
new file mode 100644
index 0000000..2d6bc94
--- /dev/null
+++ b/views/home/index.html
@@ -0,0 +1,459 @@
+
+
+
+
+ {{ .title }}
+
+
+
+
+
+
+
+
+
+
+ 管理员
+
+
+
+ 欢迎加入聊天~
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/views/home/index.tpl b/views/home/index.tpl
new file mode 100644
index 0000000..2c87ccd
--- /dev/null
+++ b/views/home/index.tpl
@@ -0,0 +1,493 @@
+
+
+
+
+ {{ .title }}--房间Id({{ .appId }})
+
+
+
+
+
+
+
+
+
+
+ 管理员
+
+
+
+ 欢迎加入聊天~
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/目录.md b/目录.md
new file mode 100644
index 0000000..74ee960
--- /dev/null
+++ b/目录.md
@@ -0,0 +1,60 @@
+## 目录
+- 1、项目说明
+ - 1.1 goWebSocket
+ - 1.2 项目体验
+- 2、介绍webSocket
+ - 2.1 webSocket 是什么
+ - 2.2 webSocket的兼容性
+ - 2.3 为什么要用webSocket
+ - 2.4 webSocket建立过程
+- 3、如何实现基于webSocket的长连接系统
+ - 3.1 使用go实现webSocket服务端
+ - 3.1.1 启动端口监听
+ - 3.1.2 升级协议
+ - 3.1.3 客户端连接的管理
+ - 3.1.4 注册客户端的socket的写的异步处理程序
+ - 3.1.5 注册客户端的socket的读的异步处理程序
+ - 3.1.6 接收客户端数据并处理
+ - 3.1.7 使用路由的方式处理客户端的请求数据
+ - 3.1.8 防止内存溢出和Goroutine不回收
+ - 3.2 使用javaScript实现webSocket客户端
+ - 3.2.1 启动并注册监听程序
+ - 3.2.2 发送数据
+ - 3.3 发送消息
+ - 3.3.1 文本消息
+ - 3.3.2 图片和语言消息
+- 4、goWebSocket 项目
+ - 4.1 项目说明
+ - 4.2 项目依赖
+ - 4.3 项目启动
+ - 4.4 接口文档
+ - 4.4.1 HTTP接口文档
+ - 4.4.1.1 接口说明
+ - 4.4.1.2 聊天页面
+ - 4.4.1.3 获取房间用户列表
+ - 4.4.1.4 查询用户是否在线
+ - 4.4.1.5 给用户发送消息
+ - 4.4.1.6 给全员用户发送消息
+ - 4.4.2 RPC接口文档
+ - 4.4.2.1 接口说明
+ - 4.4.2.2 查询用户是否在线
+ - 4.4.2.3 发送消息
+ - 4.4.2.4 给指定房间所有用户发送消息
+ - 4.4.2.5 获取房间内全部用户
+- 5、webSocket项目Nginx配置
+ - 5.1 为什么要配置Nginx
+ - 5.2 nginx配置
+ - 5.3 问题处理
+- 6、压测
+ - 6.1 Linux内核优化
+ - 6.2 压测准备
+ - 6.3 压测数据
+- 7、如何基于webSocket实现一个分布式Im
+ - 7.1 说明
+ - 7.2 架构
+ - 7.3 分布式系统部署
+- 8、回顾和反思
+ - 8.1 在其它系统应用
+ - 8.2 需要完善、优化
+ - 8.3 总结
+- 9、参考文献
\ No newline at end of file