소스 검색

1. 优化 main.go,把 模板相关代码独立到 templates 文件夹
2. 添加 cursor rules

shizhongqi 11 달 전
부모
커밋
8864a4034b
4개의 변경된 파일144개의 추가작업 그리고 64개의 파일을 삭제
  1. 73 0
      .cursor/general_rules.mdc
  2. 4 25
      main.go
  3. 29 0
      templates/template_handler.go
  4. 38 39
      wsClient/room.go

+ 73 - 0
.cursor/general_rules.mdc

@@ -0,0 +1,73 @@
+---
+description: Go WebSocket 服务开发规范
+globs: ["**/*.go"]
+alwaysApply: true
+---
+# Go WebSocket 服务开发核心规范
+
+## 1. 技术选型与库使用
+
+- **首选库**: 本项目统一使用 `github.com/gorilla/websocket` 库来处理 WebSocket 连接。它是目前功能最完善、社区支持最广泛的 Go WebSocket 库。
+- **禁止使用**: 除非有特殊理由,请**避免**直接使用标准库 `golang.org/x/net/websocket`,因为它缺少一些高级功能和性能优化。
+
+## 2. WebSocket 连接处理
+
+- **Upgrader 配置**:
+  - 必须创建一个全局或可重用的 `websocket.Upgrader` 实例。
+  - `CheckOrigin` 函数**必须**实现,用于校验请求来源,防止跨站 WebSocket 劫持 (CSWSH)。在开发环境中可以暂时允许所有来源,但在生产环境中必须严格校验。
+    ```go
+    var upgrader = websocket.Upgrader{
+        ReadBufferSize:  1024,
+        WriteBufferSize: 1024,
+        CheckOrigin: func(r *http.Request) bool {
+            // TODO: 在生产环境中替换为你的域名白名单
+            // return r.Header.Get("Origin") == "[https://yourdomain.com](https://yourdomain.com)"
+            return true // 开发环境
+        },
+    }
+    ```
+- **连接升级**: 在 HTTP Handler 中,使用 `upgrader.Upgrade(w, r, nil)` 来将会话升级为 WebSocket 连接。务必妥善处理该过程中的 `error`。
+- **并发安全**: `websocket.Conn` 对象**不是**并发安全的。对同一个连接的写操作必须在单一的 goroutine 中进行。通常的设计模式是为每个连接启动两个 goroutine:一个用于读(`ReadMessage`),一个用于写。
+
+## 3. 设计模式与代码结构
+
+- **核心结构体**:
+  - **Client/Connection**: 创建一个结构体来封装 `*websocket.Conn`,并包含其他与客户端相关的状态,如用户ID、订阅的频道等。
+    ```go
+    type Client struct {
+        conn *websocket.Conn
+        send chan []byte // 用于发送消息的缓冲 channel
+        // 其他用户标识...
+        userID string
+    }
+    ```
+  - **Hub/Manager**: 创建一个中心化的管理器,用于处理客户端的注册、注销和消息广播。
+    ```go
+    type Hub struct {
+        clients    map[*Client]bool
+        broadcast  chan []byte
+        register   chan *Client
+        unregister chan *Client
+    }
+    ```
+- **读写分离**:
+  - **读 Goroutine (`readPump`)**:
+    - 在一个 `for` 循环中持续调用 `conn.ReadMessage()`。
+    - 在此 goroutine 中处理连接关闭和错误。当 `ReadMessage` 返回错误时,意味着客户端已断开,应执行清理逻辑(如调用 `hub.unregister`)。
+    - 设置 Pong 消息处理器,用于维持心跳。
+    - 设置读超时 `conn.SetReadDeadline()`。
+  - **写 Goroutine (`writePump`)**:
+    - 从 `Client` 的 `send` channel 中读取消息。
+    - 在一个 `for` 循环中使用 `conn.WriteMessage()` 发送消息。
+    - 设置 Ping 消息,定期发送以检测连接活性。
+    - 设置写超时 `conn.SetWriteDeadline()`。
+
+## 4. 消息格式
+
+- **统一格式**: 所有客户端与服务端之间的消息都应使用 JSON 格式。
+- **消息结构**: 定义一个标准的消息结构体,包含消息类型、荷载 (payload) 等字段,以便于路由和处理。
+  ```go
+  type WebSocketMessage struct {
+      Type    string      `json:"type"`
+      Payload interface{} `json:"payload"`
+  }

+ 4 - 25
main.go

@@ -3,39 +3,18 @@
 package main
 
 import (
+	"chatapp/templates"
 	"chatapp/wsClient"
 	"flag"
 	"log"
 	"net/http"
 	"os"
-	"path/filepath"
 	"strconv"
-	"sync"
-	"text/template"
 
 	"github.com/joho/godotenv"
 )
 
-// templateHandler 负责处理 HTML 模板的渲染
-// 使用 sync.Once 确保模板只被解析一次,提高性能
-type templateHandler struct {
-	once     sync.Once         // 确保模板只被解析一次
-	filename string            // 模板文件名
-	templ    *template.Template // 解析后的模板对象
-}
-
-// ServeHTTP 处理 HTTP 请求并渲染模板
-// 实现了 http.Handler 接口
-func (t *templateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
-	// 使用 sync.Once 确保模板只被解析一次,避免重复解析带来的性能开销
-	t.once.Do(func() {
-		// 解析模板文件,如果失败会 panic
-		t.templ = template.Must(template.ParseFiles(filepath.Join("templates", t.filename)))
-	})
 
-	// 执行模板渲染,将请求对象作为数据传递给模板
-	t.templ.Execute(w, r)
-}
 
 func main() {
 	// makes every randomly generated number unique
@@ -84,10 +63,10 @@ func main() {
 	http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
 	
 	// 主页路由,渲染 index.html 模板
-	http.Handle("/", &templateHandler{filename: "index.html"})
-	
+	http.Handle("/", &templates.TemplateHandler{Filename: "index.html"})
+
 	// 聊天页面路由,渲染 chat.html 模板
-	http.Handle("/chat", &templateHandler{filename: "chat.html"})
+	http.Handle("/chat", &templates.TemplateHandler{Filename: "chat.html"})
 
 	// Handle all websocket connections for chat rooms dynamically
 	// 处理 WebSocket 连接的路由

+ 29 - 0
templates/template_handler.go

@@ -0,0 +1,29 @@
+// Package templates 提供 HTML 模板的处理功能
+package templates
+
+import (
+	"net/http"
+	"path/filepath"
+	"sync"
+	"text/template"
+)
+
+// TemplateHandler 负责处理 HTML 模板的渲染
+type TemplateHandler struct {
+	once     sync.Once         // 确保模板只被解析一次
+	Filename string            // 模板文件名
+	templ    *template.Template // 解析后的模板对象
+}
+
+// ServeHTTP 处理 HTTP 请求并渲染模板
+// 实现了 http.Handler 接口
+func (t *TemplateHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	// 使用 sync.Once 确保模板只被解析一次,避免重复解析带来的性能开销
+	t.once.Do(func() {
+		// 解析模板文件,如果失败会 panic
+		t.templ = template.Must(template.ParseFiles(filepath.Join("templates", t.Filename)))
+	})
+
+	// 执行模板渲染,将请求对象作为数据传递给模板
+	t.templ.Execute(w, r)
+}

+ 38 - 39
wsClient/room.go

@@ -11,18 +11,15 @@ import (
 	"github.com/gorilla/websocket"
 )
 
-// message 包含要转发的消息内容和发送者信息
-// 用于在转发时排除发送者自己
-type message struct {
-	content   []byte  // 消息内容
-	sender *Client // 发送者
-}
+// 全局房间 map,存储所有创建的房间
+// key: 房间名称,value: 房间对象指针
+var rooms = make(map[string]*Room)
 
 // Room 表示一个聊天房间
 type Room struct {
-	// holds all current clientCons in the room
+	// holds all current clientConns in the room
 	// 保存房间中所有当前活跃的客户端连接
-	clientCons map[*Client]bool
+	clientConns map[*Client]bool
 
 	// join is a channel for all clients wishing to join the room
 	// 用于接收希望加入房间的客户端。新客户端通过这个 channel 请求加入
@@ -42,23 +39,25 @@ type Room struct {
 	name string
 }
 
-// 全局房间 map,存储所有创建的房间
-// key: 房间名称,value: 房间对象指针
-var rooms = make(map[string]*Room)
+// message 包含要转发的消息内容和发送者信息
+// 用于在转发时排除发送者自己
+type message struct {
+	content []byte  // 消息内容
+	sender  *Client // 发送者
+}
+
 // 全局互斥锁,用于保护 rooms 字典的并发访问,确保多个 goroutine 同时访问 rooms 时的线程安全
 var mu sync.Mutex
 
-
-
 // newRoom 创建并返回一个新的房间实例
 // 初始化所有必要的 channel 和 map
 func newRoom(name string) *Room {
 	return &Room{
-		clientCons: make(map[*Client]bool),
-		join:    make(chan *Client),
-		leave:   make(chan *Client),
-		forward: make(chan *message), // 现在传输 message 结构体
-		name:    name,                // 设置房间名称
+		clientConns: make(map[*Client]bool),
+		join:        make(chan *Client),
+		leave:       make(chan *Client),
+		forward:     make(chan *message), // 现在传输 message 结构体
+		name:        name,                // 设置房间名称
 	}
 }
 
@@ -67,7 +66,7 @@ func newRoom(name string) *Room {
 func GetRoomAndRun(name string) *Room {
 	mu.Lock()
 	defer mu.Unlock()
-	
+
 	if r, ok := rooms[name]; ok {
 		return r
 	}
@@ -102,15 +101,15 @@ func (r *Room) run() {
 		// 处理客户端加入事件
 		case client := <-r.join:
 			// 将新客户端添加到房间的客户端集合中
-			r.clientCons[client] = true // 使用 true 作为值,实际上只使用 key(client 指针)
-			
+			r.clientConns[client] = true // 使用 true 作为值,实际上只使用 key(client 指针)
+
 		// 处理客户端离开事件
 		case client := <-r.leave:
 			// 从房间的客户端集合中删除客户端
-			delete(r.clientCons, client)
+			delete(r.clientConns, client)
 			// 关闭客户端的接收 channel,通知客户端的 write() 方法退出
 			close(client.receive)
-			
+
 		// 处理消息转发事件
 		case msg := <-r.forward:
 			// 打印消息内容与发送者信息
@@ -118,10 +117,10 @@ func (r *Room) run() {
 
 			// 直接存储原始消息内容到Redis,使用房间名作为键
 			go StoreRawMessage(r.name, msg.content) // 异步存储,不阻塞消息转发
-			
+
 			// 将消息发送给房间中的所有客户端,但排除发送者自己
 			// 使用指针比较是最高效的方法,因为每个客户端都有唯一的内存地址
-			for client := range r.clientCons {
+			for client := range r.clientConns {
 				if client != msg.sender { // 高效的指针比较,排除发送者
 					// 将消息发送到每个客户端的接收 channel
 					client.receive <- msg.content // 只发送消息内容,不包含发送者信息
@@ -134,7 +133,7 @@ func (r *Room) run() {
 // WebSocket 相关常量定义
 const (
 	// WebSocket 连接的读写缓冲区大小(字节)
-	socketBufferSize  = 1024
+	socketBufferSize = 1024
 	// 每个客户端消息接收 channel 的缓冲区大小
 	messageBufferSize = 256
 )
@@ -142,8 +141,8 @@ const (
 // WebSocket 升级器,用于将 HTTP 连接升级为 WebSocket 连接
 // 设置读写缓冲区大小以优化性能
 var upgrader = &websocket.Upgrader{
-	ReadBufferSize:  socketBufferSize,  // 读缓冲区大小
-	WriteBufferSize: socketBufferSize,  // 写缓冲区大小
+	ReadBufferSize:  socketBufferSize, // 读缓冲区大小
+	WriteBufferSize: socketBufferSize, // 写缓冲区大小
 }
 
 // ServeHTTP 处理 WebSocket 连接请求
@@ -152,12 +151,12 @@ var upgrader = &websocket.Upgrader{
 func (r *Room) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	// -------- 使用当前房间实例(接收者 r),而不是重新获取房间 -------- 这是才正确的面向对象设计
 	// 从 URL 查询参数中获取房间名称
-    // roomName := req.URL.Query().Get("room")
-    // if roomName == "" {
-    //     http.Error(w, "Room name required", http.StatusBadRequest)
-    //     return
-    // }
-    // room := GetRoomAndRun(roomName)
+	// roomName := req.URL.Query().Get("room")
+	// if roomName == "" {
+	//     http.Error(w, "Room name required", http.StatusBadRequest)
+	//     return
+	// }
+	// room := GetRoomAndRun(roomName)
 
 	// 将 HTTP 连接升级为 WebSocket 连接
 	socket, err := upgrader.Upgrade(w, req, nil)
@@ -168,16 +167,16 @@ func (r *Room) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 
 	// 创建新的客户端对象
 	client := &Client{
-		socket:  socket,                                    // WebSocket 连接
-		receive: make(chan []byte, messageBufferSize),      // 接收消息的缓冲 channel
-		room:    r,                                         // 使用当前房间实例
-		name:    fmt.Sprintf("user_%d", rand.Intn(1000)),   // 生成随机用户名
+		socket:  socket,                                  // WebSocket 连接
+		receive: make(chan []byte, messageBufferSize),    // 接收消息的缓冲 channel
+		room:    r,                                       // 使用当前房间实例
+		name:    fmt.Sprintf("user_%d", rand.Intn(1000)), // 生成随机用户名
 	}
 
 	// 将客户端加入当前房间
 	r.join <- client
 	defer func() { r.leave <- client }()
-	
+
 	// 启动客户端的写消息 goroutine
 	go client.write()
 	// 在当前 goroutine 中处理客户端的读消息(阻塞直到连接断开)