Gin 框架下的 WebSocket 编程

431 阅读5分钟

一、背景

  1. WebSocket 是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。WebSocket协议在2011年由IETF标准化为RFC 6455,后由RFC 7936补充规范。Web IDL中的WebSocket API由W3C标准化。
  2. WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
  3. Go中 WebSocket包的 Github:gorilla/websocket

二、WebSocket 与 HTTP 对比

  1. WebSocket协议是一种双向实时通信协议,它在客户端和服务器之间建立一个持久连接。一旦连接建立,服务器和客户端可以在任何时间互相发送数据。而经典的HTTP协议是基于请求-响应模式的无状态协议,客户端和服务器之间的通信需要每次发起新的连接。这种短暂的连接可能导致一些安全问题,比如易受到中间人攻击、重放攻击等。

  2. WebSocket相比HTTP的优势如下:

    1. 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
    2. 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
    3. 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
    4. 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
    5. 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
    6. 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
  3. 相比 HTTP,WebSocket 在以下方面提高了安全性:

    1. 更少的开放连接:WebSocket连接在建立后可以长时间保持,这意味着同一时刻开放的连接数量会减少,降低了被攻击的可能性。
    2. 跨域限制:WebSocket实现了更严格的跨域策略。尽管HTTP也有同源策略,但在WebSocket中,服务器需要在握手过程中明确允许跨域请求,降低了跨站请求伪造(CSRF)攻击的风险。
    3.   虽然WebSocket协议在某些方面提高了安全性,但它并不能完全防止黑产攻击。要确保网络安全,还需要其他安全措施,如限制连接速率、使用验证码、实施访问控制策略等。

三、Gin 框架下实现 WebSocket

  1. 服务端代码

      package main
    
      import (
         "github.com/gin-gonic/gin"
         "github.com/gorilla/websocket"
         "log"
         "time"
      )
    
      var upgrader = websocket.Upgrader{
         ReadBufferSize:   1024,
         WriteBufferSize:  1024,
         HandshakeTimeout: 5 * time.Second,
      }
    
      func main() {
         // 创建Gin应用
         app := gin.Default()
    
         // 测试
         app.GET("/ping", func(c *gin.Context) {
            c.JSON(200, gin.H{
               "message": "pong",
            })
         })
    
         // 注册WebSocket路由
         app.GET("/socket", WebSocketHandler)
    
         // 启动应用
         err := app.Run(":8080")
         if err != nil {
            panic(err)
         }
      }
    
      func WebSocketHandler(c *gin.Context) {
         // 将HTTP升级为WebSocket协议
         // 获取WebSocket连接
         conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
         if err != nil {
            log.Print("Error during connection upgradation:", err)
            return
         }
         defer conn.Close() // 关闭WebSocket连接
    
         // 事件轮询,处理WebSocket消息
         for {
            messageType, message, err := conn.ReadMessage()
            if err != nil {
               log.Println("Error during message reading:", err)
               break
            }
    
            log.Printf("messageType: %d", messageType)
            log.Printf("Received: %s", message)
    
            // 输出WebSocket消息内容
            message = append(message, []byte("123")...)
            err = conn.WriteMessage(messageType, message)
            if err != nil {
               log.Println("Error during message writing:", err)
               break
            }
         }
      }
      ```
    
    
  2. 客户端代码

      package main
    
      import (
         "github.com/gorilla/websocket"
         "log"
         "os"
         "os/signal"
         "time"
      )
    
      var done chan interface{} // 用于通知接收处理完成的通道
      var interrupt chan os.Signal // 用于监听终止信号以进行优雅终止的通道
    
      func receiveHandler(connection *websocket.Conn) {
         defer close(done) // 延迟关闭done通道
         for {
            _, msg, err := connection.ReadMessage() // 从WebSocket服务器读取消息
            if err != nil { // 如果发生错误
               log.Println("Error in receive:", err) // 打印日志
               return
            }
            log.Printf("Received: %s\n", msg) // 打印收到的消息
         }
      }
    
      func main() {
         done = make(chan interface{}) // 用于通知接收处理完成的通道
         interrupt = make(chan os.Signal) // 用于监听终止信号以进行优雅终止的通道
    
         signal.Notify(interrupt, os.Interrupt) // 通知interrupt通道捕捉SIGINT信号
    
         socketUrl := "ws://localhost:8080" + "/socket"
         conn, _, err := websocket.DefaultDialer.Dial(socketUrl, nil) // 连接WebSocket服务器
         if err != nil { // 如果发生错误
            log.Fatal("Error connecting to Websocket Server:", err) // 打印错误信息并退出程序
         }
         defer conn.Close() // 延迟关闭WebSocket连接
         go receiveHandler(conn) // 启动一个goroutine来处理接收到的消息
    
         // 主循环,发送数据包
         for {
            select {
            case <-time.After(time.Duration(1) * time.Millisecond * 1000): // 每秒钟发送一次数据包
               err := conn.WriteMessage(websocket.TextMessage, []byte("Hello from GolangDocs!")) // 发送一个文本消息
               if err != nil { // 如果发生错误
                  log.Println("Error during writing to websocket:", err) // 打印错误信息
                  return
               }
            case <-interrupt:   // 接收到SIGINT信号
               log.Println("Received SIGINT interrupt signal. Closing all pending connections")   // 打印日志
    
               err := conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))   // 关闭WebSocket连接
               if err != nil {   // 如果发生错误
                  log.Println("Error during closing websocket:", err)   // 打印错误信息
                  return
               }
    
               select {
               case <-done:   // 如果done通道已关闭
                  log.Println("Receiver Channel Closed! Exiting....")   // 打印日志
               case <-time.After(time.Duration(1) * time.Second):   // 如果'done'通道未关闭,则在1秒钟后会有超时,因此程序将在1秒钟超时后退出
                  log.Println("Timeout in closing receiving channel. Exiting....")   // 打印日志
               }
               return
            }
         }
      }
      ```