Go Gin 框架搭建简易聊天服务后端

背景

前一篇我们使用 Golang 的 Gin 框架,结合流行的 Websocket 库 gorilla/websocket,搭建了一个简易的实时后端服务。这个 Websocket 服务只是做了简单的消息接受和响应,如果我们需要实现聊天室的后端,还需要做一些工作。

接下来,我们继续使用 Golang + Gin,来搭建一个简易的聊天室后台服务。我们总体方案是 Websocket 服务接收前端传过来的全量数据,再实时全量转发出去,来实现聊天消息的多端通信,同时服务端需要做一个客户端管理器,来负责客户端的注册和卸载,以实现一个简单的聊天功能后端服务。

方案

基础前置教程请移步 Go Gin 框架搭建简易 Websocket 后端

后端

  1. 新建 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
}

  1. 安装依赖
go mod tidy

前端

  1. 新建 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>
  1. 启动

使用 VSCode Live Server 来启动这个 HTML 文件

http://127.0.0.1:5500/index.html

多打开几个页面,就能在浏览器控制台看到多个客户端消息同步。

参考

评论