chatByGin

chatByGin

此项目是将官方 WebSocket 文档下的 Chat 示例项目 用 Gin 框架 MVC 架构实现。

项目结构

chatByGin/
│  go.mod
│  go.sum
│  main.go
├─controllers
│      chat.go
├─models
│      client.go
│      hub.go
├─routers
│      chat.go
└─views
        chat.html

各模块功能说明

1. 主入口 (main.go)

package main

import (
	"chatByGin/models"
	"chatByGin/routers"
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()
	r.LoadHTMLGlob("views/*") // 加载HTML模板
	
	hub := models.NewHub() // 创建Hub中心
	go hub.Run()          // 启动Hub消息处理
	
	routers.ChatIndexInit(hub, r) // 初始化路由
	
	r.Run(":9090") // 启动服务器
}

功能:

  • 初始化Gin框架
  • 创建并启动Hub中心
  • 设置路由
  • 启动HTTP服务器

2. 控制器层 (controllers/chat.go)

package controllers

import (
	"chatByGin/models"
	"log"
	"net/http"
	"github.com/gin-gonic/gin"
)

// 渲染聊天页面
func ChatIndexInit(c *gin.Context) {
	c.HTML(http.StatusOK, "chat.html", gin.H{})
}

// 处理WebSocket连接
func ServerWs(hub *models.Hub, c *gin.Context) {
	conn, err := models.Upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		log.Println("WebSocket Upgrade error: ", err)
		return
	}
	
	client := &models.Client{
		Hub:    hub,
		Conn:   conn,
		ChSend: make(chan []byte, 256),
	}
	hub.ChRegister <- client // 注册客户端
	
	go client.WritePump() // 启动写协程
	go client.ReadPump()  // 启动读协程
}

功能:

  • ChatIndexInit: 渲染聊天页面
  • ServerWs:
    • 升级HTTP连接为WebSocket
    • 创建并注册客户端
    • 启动读写协程

3. 模型层 (models)

hub.go

package models

type Hub struct {
	ClientList   map[*Client]bool // 客户端列表
	ChBroadCast  chan []byte      // 广播消息通道
	ChRegister   chan *Client     // 注册通道
	ChUnregister chan *Client     // 注销通道
}

func NewHub() *Hub {
	return &Hub{
		ClientList:   make(map[*Client]bool),
		ChBroadCast:  make(chan []byte),
		ChRegister:   make(chan *Client),
		ChUnregister: make(chan *Client),
	}
}

func (h *Hub) Run() {
	for {
		select {
		case client := <-h.ChRegister:
			h.ClientList[client] = true // 注册客户端
		case client := <-h.ChUnregister:
			if _, ok := h.ClientList[client]; ok {
				delete(h.ClientList, client) // 注销客户端
				close(client.ChSend)         // 关闭发送通道
			}
		case message := <-h.ChBroadCast:
			for client := range h.ClientList {
				select {
				case client.ChSend <- message: // 发送消息
				default:
					close(client.ChSend)      // 发送失败则关闭连接
					delete(h.ClientList, client)
				}
			}
		}
	}
}

功能:

  • 管理所有客户端连接
  • 处理客户端注册/注销
  • 广播消息到所有客户端

client.go

package models

import (
	"bytes"
	"log"
	"time"
	"github.com/gorilla/websocket"
)

// WebSocket配置
var Upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}

const (
	writeWait      = 10 * time.Second
	pongWait       = 60 * time.Second
	pingPeriod     = (pongWait * 9) / 10
	maxMessageSize = 512
)

type Client struct {
	Hub    *Hub
	Conn   *websocket.Conn
	ChSend chan []byte
}

// 读取消息并广播
func (c *Client) ReadPump() {
	defer func() {
		c.Hub.ChUnregister <- c
		c.Conn.Close()
	}()
	
	c.Conn.SetReadLimit(maxMessageSize)
	c.Conn.SetReadDeadline(time.Now().Add(pongWait))
	c.Conn.SetPongHandler(func(string) error {
		c.Conn.SetReadDeadline(time.Now().Add(pongWait))
		return nil
	})
	
	for {
		_, message, err := c.Conn.ReadMessage()
		if err != nil {
			log.Println("ReadMessage Error", err)
			break
		}
		message = bytes.TrimSpace(bytes.Replace(message, []byte{'\n'}, []byte{' '}, -1))
		c.Hub.ChBroadCast <- message
	}
}

// 发送消息到客户端
func (c *Client) WritePump() {
	ticker := time.NewTicker(pingPeriod)
	defer func() {
		ticker.Stop()
		c.Conn.Close()
	}()
	
	for {
		select {
		case message, ok := <-c.ChSend:
			c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
			if !ok {
				c.Conn.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}
			
			w, err := c.Conn.NextWriter(websocket.TextMessage)
			if err != nil {
				return
			}
			w.Write(message)
			
			n := len(c.ChSend)
			for i := 0; i < n; i++ {
				w.Write([]byte{'\n'})
				w.Write(<-c.ChSend)
			}
			
			if err := w.Close(); err != nil {
				return
			}
		case <-ticker.C:
			c.Conn.SetWriteDeadline(time.Now().Add(writeWait))
			if err := c.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}
}

功能:

  • ReadPump: 从WebSocket读取消息并广播
  • WritePump: 从Hub接收消息并写入WebSocket
  • 实现心跳机制保持连接

4. 路由层 (routers/chat.go)

package routers

import (
	"chatByGin/controllers"
	"chatByGin/models"
	"github.com/gin-gonic/gin"
)

func ChatIndexInit(hub *models.Hub, c *gin.Engine) {
	ChatRouter := c.Group("/chat")
	{
		ChatRouter.GET("", controllers.ChatIndexInit) // 聊天页面
		ChatRouter.GET("/ws", func(c *gin.Context) {  // WebSocket连接
			controllers.ServerWs(hub, c)
		})
	}
}

功能:

  • 设置聊天相关路由
  • 将页面请求和WebSocket请求分组管理

5. 视图层 (views/chat.html)

<!DOCTYPE html>
<html>
<head>
    <title>聊天示例</title>
    <script>
        window.onload = function() {
            var conn = new WebSocket("ws://" + document.location.host + "/chat/ws");
            var msg = document.getElementById("msg");
            var log = document.getElementById("log");
            
            conn.onmessage = function(evt) {
                var item = document.createElement("div");
                item.innerText = evt.data;
                log.appendChild(item);
                log.scrollTop = log.scrollHeight;
            };
            
            document.getElementById("form").onsubmit = function() {
                conn.send(msg.value);
                msg.value = "";
                return false;
            };
        };
    </script>
    <style>
        /* 样式代码 */
    </style>
</head>
<body>
    <div id="log"></div>
    <form id="form">
        <input type="submit" value="发送" />
        <input type="text" id="msg" size="64" autofocus />
    </form>
</body>
</html>

功能:

  • 提供聊天界面
  • 建立WebSocket连接
  • 发送和显示消息

数据流分析

  1. 客户端连接流程:

    • 用户访问 /chat → 渲染HTML页面
    • 页面 JS 建立 WebSocket 连接到 /chat/ws
    • 服务端创建 Client 并注册到 Hub
    • 启动读写协程
  2. 消息广播流程:

    • 客户端发送消息 → ReadPump接收 → 广播到 Hub
    • Hub → 所有 Client 的 WritePump → 发送到各自 WebSocket 连接
  3. 心跳机制:

    • WritePump定期发送Ping
    • ReadPump设置Pong处理器保持连接
posted @ 2025-04-01 15:18  XiaoMo247  阅读(14)  评论(0)    收藏  举报