一、背景
- WebSocket 是一种网络传输协议,可在单个TCP连接上进行全双工通信,位于OSI模型的应用层。WebSocket协议在2011年由IETF标准化为RFC 6455,后由RFC 7936补充规范。Web IDL中的WebSocket API由W3C标准化。
- WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
- Go中 WebSocket包的 Github:gorilla/websocket
二、WebSocket 与 HTTP 对比
-
WebSocket协议是一种双向实时通信协议,它在客户端和服务器之间建立一个持久连接。一旦连接建立,服务器和客户端可以在任何时间互相发送数据。而经典的HTTP协议是基于请求-响应模式的无状态协议,客户端和服务器之间的通信需要每次发起新的连接。这种短暂的连接可能导致一些安全问题,比如易受到中间人攻击、重放攻击等。
-
WebSocket相比HTTP的优势如下:
- 较少的控制开销。在连接创建后,服务器和客户端之间交换数据时,用于协议控制的数据包头部相对较小。在不包含扩展的情况下,对于服务器到客户端的内容,此头部大小只有2至10字节(和数据包长度有关);对于客户端到服务器的内容,此头部还需要加上额外的4字节的掩码。相对于HTTP请求每次都要携带完整的头部,此项开销显著减少了。
- 更强的实时性。由于协议是全双工的,所以服务器可以随时主动给客户端下发数据。相对于HTTP请求需要等待客户端发起请求服务端才能响应,延迟明显更少;即使是和Comet等类似的长轮询比较,其也能在短时间内更多次地传递数据。
- 保持连接状态。与HTTP不同的是,Websocket需要先创建连接,这就使得其成为一种有状态的协议,之后通信时可以省略部分状态信息。而HTTP请求可能需要在每个请求都携带状态信息(如身份认证等)。
- 更好的二进制支持。Websocket定义了二进制帧,相对HTTP,可以更轻松地处理二进制内容。
- 可以支持扩展。Websocket定义了扩展,用户可以扩展协议、实现部分自定义的子协议。如部分浏览器支持压缩等。
- 更好的压缩效果。相对于HTTP压缩,Websocket在适当的扩展支持下,可以沿用之前内容的上下文,在传递类似的数据时,可以显著地提高压缩率。
-
相比 HTTP,WebSocket 在以下方面提高了安全性:
- 更少的开放连接:WebSocket连接在建立后可以长时间保持,这意味着同一时刻开放的连接数量会减少,降低了被攻击的可能性。
- 跨域限制:WebSocket实现了更严格的跨域策略。尽管HTTP也有同源策略,但在WebSocket中,服务器需要在握手过程中明确允许跨域请求,降低了跨站请求伪造(CSRF)攻击的风险。
- 虽然WebSocket协议在某些方面提高了安全性,但它并不能完全防止黑产攻击。要确保网络安全,还需要其他安全措施,如限制连接速率、使用验证码、实施访问控制策略等。
三、Gin 框架下实现 WebSocket
-
服务端代码
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 } } } ``` -
客户端代码
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 } } } ```