Go Gin 框架搭建简易聊天服务后端
背景
前一篇我们使用 Golang 的 Gin 框架,结合流行的 Websocket 库 gorilla/websocket
,搭建了一个简易的实时后端服务。这个 Websocket 服务只是做了简单的消息接受和响应,如果我们需要实现聊天室的后端,还需要做一些工作。
接下来,我们继续使用 Golang + Gin,来搭建一个简易的聊天室后台服务。我们总体方案是 Websocket 服务接收前端传过来的全量数据,再实时全量转发出去,来实现聊天消息的多端通信,同时服务端需要做一个客户端管理器,来负责客户端的注册和卸载,以实现一个简单的聊天功能后端服务。
方案
基础前置教程请移步 Go Gin 框架搭建简易 Websocket 后端。
后端
- 新建 main.go
注意同目录下,只能有一个文件包含 main
方法(文件名随意)
package main
import (
"encoding/json"
"fmt"
"net"
"net/http"
"github.com/gorilla/websocket"
uuid "github.com/satori/go.uuid"
)
//客户端管理
type ClientManager struct {
//客户端 map 储存并管理所有的长连接client,在线的为true,不在的为false
clients map[*Client]bool
//web端发送来的的message我们用broadcast来接收,并最后分发给所有的client
broadcast chan []byte
//新创建的长连接client
register chan *Client
//新注销的长连接client
unregister chan *Client
}
//客户端 Client
type Client struct {
//用户id
id string
//连接的socket
socket *websocket.Conn
//发送的消息
send chan []byte
}
//会把Message格式化成json
type Message struct {
//消息struct
Sender string `json:"sender,omitempty"` //发送者
Recipient string `json:"recipient,omitempty"` //接收者
Content string `json:"content,omitempty"` //内容
ServerIP string `json:"serverIp,omitempty"` //实际不需要 验证k8s
SenderIP string `json:"senderIp,omitempty"` //实际不需要 验证k8s
}
//创建客户端管理者
var manager = ClientManager{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
func (manager *ClientManager) start() {
for {
select {
//如果有新的连接接入,就通过channel把连接传递给conn
case conn := <-manager.register:
//把客户端的连接设置为true
manager.clients[conn] = true
//把返回连接成功的消息json格式化
jsonMessage, _ := json.Marshal(&Message{Content: "/A new socket has connected. ", ServerIP: LocalIp(), SenderIP: conn.socket.RemoteAddr().String()})
//调用客户端的send方法,发送消息
manager.send(jsonMessage, conn)
//如果连接断开了
case conn := <-manager.unregister:
//判断连接的状态,如果是true,就关闭send,删除连接client的值
if _, ok := manager.clients[conn]; ok {
close(conn.send)
delete(manager.clients, conn)
jsonMessage, _ := json.Marshal(&Message{Content: "/A socket has disconnected. ", ServerIP: LocalIp(), SenderIP: conn.socket.RemoteAddr().String()})
manager.send(jsonMessage, conn)
}
//广播
case message := <-manager.broadcast:
//遍历已经连接的客户端,把消息发送给他们
for conn := range manager.clients {
select {
case conn.send <- message:
default:
close(conn.send)
delete(manager.clients, conn)
}
}
}
}
}
//定义客户端管理的send方法
func (manager *ClientManager) send(message []byte, ignore *Client) {
for conn := range manager.clients {
//不给屏蔽的连接发送消息
if conn != ignore {
conn.send <- message
}
}
}
//定义客户端结构体的read方法
func (c *Client) read() {
defer func() {
manager.unregister <- c
_ = c.socket.Close()
}()
for {
//读取消息
_, message, err := c.socket.ReadMessage()
//如果有错误信息,就注销这个连接然后关闭
if err != nil {
manager.unregister <- c
_ = c.socket.Close()
break
}
//如果没有错误信息就把信息放入broadcast
jsonMessage, _ := json.Marshal(&Message{Sender: c.id, Content: string(message), ServerIP: LocalIp(), SenderIP: c.socket.RemoteAddr().String()})
manager.broadcast <- jsonMessage
}
}
func (c *Client) write() {
defer func() {
_ = c.socket.Close()
}()
for {
select {
//从send里读消息
case message, ok := <-c.send:
//如果没有消息
if !ok {
_ = c.socket.WriteMessage(websocket.CloseMessage, []byte{})
return
}
//有消息就写入,发送给web端
_ = c.socket.WriteMessage(websocket.TextMessage, message)
}
}
}
func main() {
fmt.Println("Starting application...")
//开一个goroutine执行开始程序
go manager.start()
//注册默认路由为 /ws ,并使用wsHandler这个方法
http.HandleFunc("/ws", wsHandler)
http.HandleFunc("/health", healthHandler)
//监听本地的8011端口
fmt.Println("chat server start.....")
//注意这里必须是0.0.0.0才能部署在服务器使用
_ = http.ListenAndServe("0.0.0.0:8448", nil)
}
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024 * 1024 * 1024,
WriteBufferSize: 1024 * 1024 * 1024,
//解决跨域问题
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func wsHandler(res http.ResponseWriter, req *http.Request) {
//将http协议升级成websocket协议
conn, err := upgrader.Upgrade(res, req, nil)
if err != nil {
http.NotFound(res, req)
return
}
//每一次连接都会新开一个client,client.id通过uuid生成保证每次都是不同的
client := &Client{id: uuid.Must(uuid.NewV4(), nil).String(), socket: conn, send: make(chan []byte)}
//注册一个新的链接
manager.register <- client
//启动协程收web端传过来的消息
go client.read()
//启动协程把消息返回给web端
go client.write()
}
func healthHandler(res http.ResponseWriter, _ *http.Request) {
_, _ = res.Write([]byte("ok"))
}
func LocalIp() string {
address, _ := net.InterfaceAddrs()
var ip = "localhost"
for _, address := range address {
if ipAddress, ok := address.(*net.IPNet); ok && !ipAddress.IP.IsLoopback() {
if ipAddress.IP.To4() != nil {
ip = ipAddress.IP.String()
}
}
}
return ip
}
- 安装依赖
go mod tidy
前端
- 新建 HTML
当前工程或者任意目录新建 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Univer Server</title>
</head>
<body>
<script>
var ws = new WebSocket("ws://localhost:8448/ws");
//Triggered when the connection is opened
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
//Triggered when a message is received
ws.onmessage = function(evt) {
console.log("Received Message: " + evt.data);
};
//Triggered when the connection is closed
ws.onclose = function(evt) {
console.log("Connection closed.");
};
</script>
</body>
</html>
- 启动
使用 VSCode Live Server 来启动这个 HTML 文件
http://127.0.0.1:5500/index.html
多打开几个页面,就能在浏览器控制台看到多个客户端消息同步。
评论