├── LICENSE ├── README.md ├── clients └── client.go ├── message-chat ├── Different room.html ├── First room.html ├── README.md ├── Same room.html ├── config │ └── config.go ├── docs │ ├── pic.png │ └── test.toml ├── main.go ├── middlewares │ ├── config.go │ └── redis.go └── server │ ├── client.go │ └── hub.go └── server └── server.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 hcf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # websocket-gin-demo 2 | gin websocket 服务端与客户端长连接 服务端进行消息推送 3 | 4 | `cd server` and `go run main.go` 5 | 6 | `cd client` and `go run main.go` 7 | 8 | you can send message to client.. 9 | 10 | 11 | 添加多人多个聊天室demo在**message-chat** 12 | Add multiple people to multiple chat room demos in **message-chat** 13 | ![](./message-chat/docs/pic.png) -------------------------------------------------------------------------------- /clients/client.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/url" 6 | "time" 7 | 8 | "github.com/gorilla/websocket" 9 | ) 10 | 11 | func main() { 12 | u := url.URL{Scheme: "ws", Host: "localhost:8009", Path: "/ws"} 13 | log.Printf("connecting to %s", u.String()) 14 | 15 | c, _, err := websocket.DefaultDialer.Dial(u.String(), nil) 16 | if err != nil { 17 | log.Fatal("dial:", err) 18 | } 19 | defer c.Close() 20 | 21 | go func() { 22 | for { 23 | _, message, err := c.ReadMessage() 24 | if err != nil { 25 | log.Println("read:", err) 26 | return 27 | } 28 | log.Printf("recv: %s", message) 29 | } 30 | }() 31 | time.Sleep(100 * time.Second) 32 | } 33 | -------------------------------------------------------------------------------- /message-chat/Different room.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chat Example 5 | 50 | 83 | 84 | 85 |
86 |
87 | 88 | 89 |
90 | 91 | -------------------------------------------------------------------------------- /message-chat/First room.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chat Example 5 | 50 | 83 | 84 | 85 |
86 |
87 | 88 | 89 |
90 | 91 | -------------------------------------------------------------------------------- /message-chat/README.md: -------------------------------------------------------------------------------- 1 | #GIN+Websocket Multiplayer, multiple chat rooms - demo(GIN+Websocket 多人,多个聊天室-demo) 2 | 3 | 4 | ##中文 5 | 改变程序内的import包,为自己的目录结构。 6 | 运行 7 | ``` 8 | go run main.go 9 | ``` 10 | 启动后台websocket服务 11 | 依次打开 12 | **First room.html** 13 | **Same room.html** 14 | **Different room.html** 15 | 16 | 模拟3个用户,进行聊天室聊天 17 | 如图 18 | ![](./docs/pic.png) 19 | 20 | ##English 21 | 22 | 23 | Change the import package inside the program to its own directory structure. 24 | Run 25 | ``` 26 | go run main.go 27 | ``` 28 | 29 | Start the background websocket service 30 | Open sequentially 31 | **First room.html** 32 | **Same room.html** 33 | **Different room.html** 34 | 35 | Simulate 3 users and chat in a chat room 36 | -------------------------------------------------------------------------------- /message-chat/Same room.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chat Example 5 | 50 | 83 | 84 | 85 |
86 |
87 | 88 | 89 |
90 | 91 | -------------------------------------------------------------------------------- /message-chat/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "github.com/BurntSushi/toml" 5 | ) 6 | 7 | // Config 对应配置文件结构 8 | type Config struct { 9 | Listen string `toml:"listen"` 10 | RedisServers map[string]RedisServer `toml:"redisservers"` 11 | } 12 | 13 | // UnmarshalConfig 解析toml配置 14 | func UnmarshalConfig(tomlfile string) (*Config, error) { 15 | c := &Config{} 16 | if _, err := toml.DecodeFile(tomlfile, c); err != nil { 17 | return c, err 18 | } 19 | return c, nil 20 | } 21 | 22 | // RedisServerConf 获取数据库配置 23 | func (c Config) RedisServerConf(key string) (RedisServer, bool) { 24 | s, ok := c.RedisServers[key] 25 | return s, ok 26 | } 27 | 28 | // GetListenAddr 监听地址 29 | func (c Config) GetListenAddr() string { 30 | return c.Listen 31 | } 32 | 33 | // RedisServer 表示 redis 服务器配置 34 | type RedisServer struct { 35 | Addr string `toml:"addr"` 36 | Password string `toml:"password"` 37 | DB int `toml:"db"` 38 | } 39 | -------------------------------------------------------------------------------- /message-chat/docs/pic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XanCafe/websocket-gin-demo/ff2426d42408e11e87d886c26dcf4f7a4faff92d/message-chat/docs/pic.png -------------------------------------------------------------------------------- /message-chat/docs/test.toml: -------------------------------------------------------------------------------- 1 | listen = ":8009" 2 | # Redis数据库配置 3 | [redisservers] 4 | # token口令数据库 5 | [redisservers.test] 6 | addr = "127.0.0.1:6379" 7 | password = "" 8 | db = 0 -------------------------------------------------------------------------------- /message-chat/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "runtime" 6 | "test/websocket-gin-demo/message-chat/config" 7 | "test/websocket-gin-demo/message-chat/middlewares" 8 | "test/websocket-gin-demo/message-chat/server" 9 | "time" 10 | 11 | "github.com/gin-contrib/cors" 12 | "github.com/gin-gonic/gin" 13 | "github.com/go-xweb/log" 14 | ) 15 | 16 | var ( 17 | tomlFile = flag.String("config", "docs/test.toml", "config file") 18 | ) 19 | 20 | // init 初始化配置 21 | func init() { 22 | runtime.GOMAXPROCS(runtime.NumCPU()) 23 | gin.SetMode(gin.DebugMode) 24 | } 25 | func main() { 26 | flag.Parse() 27 | // 解析配置文件 28 | tomlConfig, err := config.UnmarshalConfig(*tomlFile) 29 | if err != nil { 30 | log.Printf("UnmarshalConfig: err:%v\n", err) 31 | return 32 | } 33 | // 绑定路由,及公共的tomlConfig 34 | //router := gin.Default() 35 | router := gin.New() 36 | router.Use(gin.Recovery()) 37 | //设置跨域 38 | router.Use(cors.New(cors.Config{ 39 | AllowAllOrigins: true, 40 | AllowMethods: []string{"GET", "PUT", "POST", "DELETE", "OPTIONS"}, 41 | AllowHeaders: []string{"x-xq5-jwt", "Content-Type", "Origin", "Content-Length"}, 42 | ExposeHeaders: []string{"x-xq5-jwt"}, 43 | AllowCredentials: true, 44 | MaxAge: 12 * time.Hour, 45 | })) 46 | router.Use(middlewares.Config(tomlConfig)) 47 | // router.Use(middlewares.Redis("test", tomlConfig)) 48 | hub := server.NewHub() 49 | go hub.Run() 50 | router.GET("/ws", func(c *gin.Context) { server.ServeWs(hub, c) }) 51 | log.Println("run websocket at ", tomlConfig.GetListenAddr()) 52 | router.Run(tomlConfig.GetListenAddr()) 53 | } 54 | -------------------------------------------------------------------------------- /message-chat/middlewares/config.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "test/websocket-gin-demo/message-chat/config" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | // Config . 10 | func Config(tomlConfig *config.Config) gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | c.Set("config", tomlConfig) 13 | c.Next() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /message-chat/middlewares/redis.go: -------------------------------------------------------------------------------- 1 | package middlewares 2 | 3 | import ( 4 | "fmt" 5 | "test/websocket-gin-demo/message-chat/config" 6 | "time" 7 | 8 | "github.com/garyburd/redigo/redis" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // Redis . 13 | func Redis(cacheName string, tomlConfig *config.Config) gin.HandlerFunc { 14 | cacheConfig, ok := tomlConfig.RedisServerConf(cacheName) 15 | if !ok { 16 | panic(fmt.Sprintf("%v not set.", cacheName)) 17 | } 18 | 19 | // 链接数据库 20 | pool := newPool(cacheConfig.Addr, cacheConfig.Password, cacheConfig.DB) 21 | 22 | return func(c *gin.Context) { 23 | c.Set(cacheName, pool) 24 | c.Next() 25 | } 26 | } 27 | 28 | // newPool New redis pool. 29 | func newPool(server, password string, db int) *redis.Pool { 30 | return &redis.Pool{ 31 | MaxIdle: 3, 32 | IdleTimeout: 240 * time.Second, 33 | Dial: func() (redis.Conn, error) { 34 | c, err := redis.Dial("tcp", server, 35 | redis.DialPassword(password), 36 | redis.DialDatabase(db), 37 | redis.DialConnectTimeout(500*time.Millisecond), 38 | redis.DialReadTimeout(500*time.Millisecond), 39 | redis.DialWriteTimeout(500*time.Millisecond)) 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | return c, err 45 | }, 46 | 47 | TestOnBorrow: func(c redis.Conn, t time.Time) error { 48 | if time.Since(t) < 5*time.Second { 49 | return nil 50 | } 51 | _, err := c.Do("PING") 52 | return err 53 | }, 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /message-chat/server/client.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | 8 | "net/http" 9 | "time" 10 | 11 | "github.com/gin-gonic/gin" 12 | 13 | "github.com/gorilla/websocket" 14 | ) 15 | 16 | const ( 17 | // Time allowed to write a message to the peer. 18 | writeWait = 10 * time.Second 19 | 20 | // Time allowed to read the next pong message from the peer. 21 | pongWait = 60 * time.Second 22 | 23 | // Send pings to peer with this period. Must be less than pongWait. 24 | pingPeriod = (pongWait * 9) / 10 25 | 26 | // Maximum message size allowed from peer. 27 | maxMessageSize = 512 28 | ) 29 | 30 | var ( 31 | newline = []byte{'\n'} 32 | space = []byte{' '} 33 | ) 34 | 35 | var upgrader = websocket.Upgrader{ 36 | ReadBufferSize: 1024, 37 | WriteBufferSize: 1024, 38 | } 39 | 40 | // Client is a middleman between the websocket connection and the hub. 41 | type Client struct { 42 | hub *Hub 43 | 44 | // The websocket connection. 45 | conn *websocket.Conn 46 | 47 | // Buffered channel of outbound messages. 48 | send chan []byte 49 | 50 | // 用户名称 51 | username []byte 52 | // 房间号 53 | roomID []byte 54 | } 55 | 56 | // readPump pumps messages from the websocket connection to the hub. 57 | // 58 | // The application runs readPump in a per-connection goroutine. The application 59 | // ensures that there is at most one reader on a connection by executing all 60 | // reads from this goroutine. 61 | func (c *Client) readPump() { 62 | defer func() { 63 | c.hub.unregister <- c 64 | c.conn.Close() 65 | }() 66 | c.conn.SetReadLimit(maxMessageSize) 67 | c.conn.SetReadDeadline(time.Now().Add(pongWait)) 68 | c.conn.SetPongHandler(func(string) error { c.conn.SetReadDeadline(time.Now().Add(pongWait)); return nil }) 69 | for { 70 | _, message, err := c.conn.ReadMessage() 71 | if err != nil { 72 | if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) { 73 | log.Printf("error: %v", err) 74 | } 75 | break 76 | } 77 | message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1)) 78 | message = []byte(string(c.roomID) + "&" + string(c.username) + ":" + string(message)) 79 | fmt.Println(string(message)) 80 | c.hub.broadcast <- []byte(message) 81 | } 82 | } 83 | 84 | // writePump pumps messages from the hub to the websocket connection. 85 | // 86 | // A goroutine running writePump is started for each connection. The 87 | // application ensures that there is at most one writer to a connection by 88 | // executing all writes from this goroutine. 89 | func (c *Client) writePump() { 90 | ticker := time.NewTicker(pingPeriod) 91 | defer func() { 92 | ticker.Stop() 93 | c.conn.Close() 94 | }() 95 | for { 96 | select { 97 | case message, ok := <-c.send: 98 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 99 | if !ok { 100 | // The hub closed the channel. 101 | c.conn.WriteMessage(websocket.CloseMessage, []byte{}) 102 | return 103 | } 104 | 105 | w, err := c.conn.NextWriter(websocket.TextMessage) 106 | if err != nil { 107 | return 108 | } 109 | // 使用“&”分割获取房间号 110 | // 聊天内容不得包含&字符 111 | // msg[0]为房间号 msg[1]为打印内容 112 | // msg := strings.Split(string(message), "&") 113 | // if msg[0] == string(c.hub.roomID[c]) { 114 | // w.Write([]byte(msg[1])) 115 | // } 116 | w.Write(message) 117 | // Add queued chat messages to the current websocket message. 118 | // n := len(c.send) 119 | // for i := 0; i < n; i++ { 120 | // if msg[0] == string(c.hub.roomID[c]) { 121 | // w.Write(newline) 122 | // w.Write(<-c.send) 123 | // } 124 | // } 125 | if err := w.Close(); err != nil { 126 | log.Printf("error: %v", err) 127 | return 128 | } 129 | case <-ticker.C: 130 | c.conn.SetWriteDeadline(time.Now().Add(writeWait)) 131 | if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil { 132 | return 133 | } 134 | } 135 | } 136 | } 137 | 138 | // ChatRequest 聊天室请求 139 | type ChatRequest struct { 140 | RoomID string `json:"room_id" form:"room_id"` 141 | UserID int `json:"user_id" form:"user_id"` 142 | UserName string `json:"user_name" form:"user_name"` 143 | } 144 | 145 | // ServeWs handles websocket requests from the peer. 146 | func ServeWs(hub *Hub, c *gin.Context) { 147 | // 获取前端数据 148 | var req ChatRequest 149 | if err := c.ShouldBind(&req); err != nil { 150 | log.Printf("ServeWs err:%v\n", err) 151 | c.JSON(http.StatusOK, gin.H{"errno": "-1", "errmsg": "参数不匹配,请重试"}) 152 | return 153 | } 154 | userName := req.UserName 155 | roomID := req.RoomID 156 | // 获取redis连接(暂未使用) 157 | // pool := c.MustGet("test").(*redis.Pool) 158 | // redisConn := pool.Get() 159 | // defer redisConn.Close() 160 | // 将网络请求变为websocket 161 | var upgrader = websocket.Upgrader{ 162 | // 解决跨域问题 163 | CheckOrigin: func(r *http.Request) bool { 164 | return true 165 | }, 166 | } 167 | conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) 168 | if err != nil { 169 | log.Println(err) 170 | return 171 | } 172 | fmt.Println(userName) 173 | client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256), username: []byte(userName), roomID: []byte(roomID)} 174 | client.hub.register <- client 175 | 176 | // Allow collection of memory referenced by the caller by doing all work in 177 | // new goroutines. 178 | go client.writePump() 179 | go client.readPump() 180 | } 181 | -------------------------------------------------------------------------------- /message-chat/server/hub.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import "strings" 4 | 5 | // Hub maintains the set of active clients and broadcasts messages to the 6 | // clients. 7 | type Hub struct { 8 | // Registered clients. 9 | clients map[*Client]bool 10 | 11 | // Inbound messages from the clients. 12 | broadcast chan []byte 13 | 14 | // Register requests from the clients. 15 | register chan *Client 16 | 17 | // Unregister requests from clients. 18 | unregister chan *Client 19 | 20 | // 房间号 key:client value:房间号 21 | roomID map[*Client]string 22 | } 23 | 24 | // NewHub . 25 | func NewHub() *Hub { 26 | return &Hub{ 27 | broadcast: make(chan []byte), 28 | register: make(chan *Client), 29 | unregister: make(chan *Client), 30 | clients: make(map[*Client]bool), 31 | roomID: make(map[*Client]string), 32 | } 33 | } 34 | 35 | // Run . 36 | func (h *Hub) Run() { 37 | for { 38 | select { 39 | case client := <-h.register: 40 | h.clients[client] = true // 注册client端 41 | h.roomID[client] = string(client.roomID) // 给client端添加房间号 42 | case client := <-h.unregister: 43 | if _, ok := h.clients[client]; ok { 44 | delete(h.clients, client) 45 | delete(h.roomID, client) 46 | close(client.send) 47 | } 48 | case message := <-h.broadcast: 49 | for client := range h.clients { 50 | // 使用“&”对message进行message切割 获取房间号 51 | // 向信息所属的房间内的所有client 内添加send 52 | // msg[0]为房间号 msg[1]为打印内容 53 | msg := strings.Split(string(message), "&") 54 | if string(client.roomID) == msg[0] { 55 | select { 56 | case client.send <- []byte(msg[1]): 57 | default: 58 | close(client.send) 59 | delete(h.clients, client) 60 | delete(h.roomID, client) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/gorilla/websocket" 10 | ) 11 | 12 | var upgrader = websocket.Upgrader{} // use default options 13 | var messageChan = make(chan string) 14 | 15 | //此处为服务端接收 需要推送的信息 16 | func updateMsg() { 17 | for { 18 | var messageBuf string 19 | fmt.Scanln(&messageBuf) 20 | messageChan <- messageBuf 21 | } 22 | } 23 | 24 | //此处为websocket 执行的长连接 函数 25 | func echo(w http.ResponseWriter, r *http.Request) { 26 | var upgrader = websocket.Upgrader{ 27 | // 解决跨域问题 28 | CheckOrigin: func(r *http.Request) bool { 29 | return true 30 | }, 31 | } 32 | c, err := upgrader.Upgrade(w, r, nil) 33 | if err != nil { 34 | log.Print("upgrade:", err) 35 | return 36 | } 37 | defer c.Close() 38 | for { 39 | //信息推送 WriteMessage 40 | //updateMsg() 41 | c.WriteMessage(websocket.TextMessage, []byte(<-messageChan)) 42 | } 43 | } 44 | 45 | func main() { 46 | go updateMsg() 47 | router := gin.Default() 48 | router.GET("/ws", func(c *gin.Context) { 49 | echo(c.Writer, c.Request) 50 | }) 51 | router.Run(":8999") 52 | } 53 | --------------------------------------------------------------------------------