WebSocket基础概念 #
WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
WebSocket在2011年被IETF标准化为RFC 6455,并由W3C定义了对应的JavaScript API。
WebSocket vs HTTP #
传统的HTTP协议有以下局限性:
- 单向通信:客户端发起请求,服务器响应,服务器不能主动推送数据
- 连接不持久:每次请求都需要建立新的TCP连接(虽然HTTP/1.1引入了Keep-Alive,但本质上仍是请求-响应模式)
- 头部开销大:HTTP请求和响应都包含大量头部信息
相比之下,WebSocket提供以下优势:
- 双向通信:服务器可以主动向客户端推送数据
- 持久连接:一次握手后保持TCP连接,减少连接建立的开销
- 较低的延迟:没有HTTP请求的额外开销,适合实时数据传输
- 更高效的数据传输:相比HTTP轮询,大大减少了数据传输量
WebSocket工作原理 #
1、握手阶段:客户端发起HTTP请求,请求升级到WebSocket协议
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
2、服务器响应:如果服务器支持WebSocket,则返回升级确认
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
3、建立连接:握手成功后,HTTP连接升级为WebSocket连接,此后的通信遵循WebSocket协议
4、数据传输:使用帧(frames)进行数据传输,帧可以是文本帧或二进制帧
5、关闭连接:任何一方都可以发送关闭帧来关闭连接
Go语言中的WebSocket实现 #
Go 语言有两个主要的 WebSocket 库:
gorilla/websocket:这是一个流行的 WebSocket 库,提供了简单易用的接口。它支持多路复用、自定义头部和心跳等功能。golang.org/x/net/websocket:这是 Go 标准库中的 WebSocket 库,提供了基本的 WebSocket 功能。它不支持多路复用、自定义头部和心跳等高级功能。
我们将主要关注 gorilla/websocket 库,因为它更加强大和易用。
安装 #
go get github.com/gorilla/websocket
基本概念 #
在 gorilla/websocket 中使用 Conn 来表示一个 WebSocket 连接,它主要有如下作用:
- 发送消息给客户端:
WriteXXX方法,如WriteJSON发送 JSON 类型消息,又或者WriteMessage可以发送普通的文本消息。 - 接收客户端发送的消息:
ReadXXX方法,如ReadJSON和ReadMessage。 - 其他功能:关闭连接、获取客户端 IP 地址等
消息被分为以下几种:
- 数据消息:
TextMessage文本消息:文本消息被解析为 UTF-8 编码的文本。需要应用程序来确保文本消息是有效的 UTF-8 编码文本。BinaryMessage二进制消息:二进制消息的解析留给应用程序。
- 控制消息:可以调用
Conn中的WriteControl、WriteMessage或NextWriter方法,将控制消息发送给对方。CloseMessage关闭连接的消息PingMessageping 消息PongMessagepong 消息
WebSocket 服务端 #
package main
import (
"github.com/gorilla/websocket"
"io"
"log"
"net/http"
)
// 配置 WebSocket 升级器
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
// 允许所有CORS请求,生产环境应当配置更严格的检查
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
// 将 HTTP 连接升级为 WebSocket连接
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Println(err)
}
defer conn.Close() // 关闭链接
log.Println("WebSocket 客户端已连接", conn.RemoteAddr())
// 无限循环,持续读取客户端消息
for {
// 读取消息
messageType, message, err := conn.ReadMessage()
if err != nil && err != io.EOF {
log.Println("服务端读取消息错误:", err)
break
}
log.Printf("收到客户端 %v 消息: %s", conn.RemoteAddr(), string(message))
// 简单地将消息回送给客户端
err = conn.WriteMessage(messageType, message)
if err != nil {
log.Println("服务端写入消息错误:", err)
break
}
}
}
func main() {
http.HandleFunc("/ws", handleWebSocket)
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatalln(err)
}
}
WebSocket 客户端 #
package main
import (
"fmt"
"github.com/gorilla/websocket"
"log"
"net/url"
"sync"
)
func main() {
url := url.URL{
Scheme: "ws",
Host: "localhost:8080",
Path: "/ws",
}
// 创建一个 WebSocket 客户端链接
conn, _, err := websocket.DefaultDialer.Dial(url.String(), nil)
if err != nil {
log.Println("无法连接 WebSocket 服务端:", url.String())
}
log.Println("连接服务器成功")
wg := sync.WaitGroup{}
// 启动一个 goroutine 接收消息
wg.Add(1)
go func() {
defer wg.Done()
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
log.Println("客户端读取消息错误:", err)
break
}
log.Println("收到服务端消息:", string(message), "type:", messageType)
}
}()
// 另一个 goroutine 监听用户输入发送消息
wg.Add(1)
go func() {
defer wg.Done()
for {
var message string
fmt.Scanln(&message)
err := conn.WriteMessage(websocket.TextMessage, []byte(message))
if err != nil {
log.Println("客户端发送消息错误:", err)
break
}
}
}()
wg.Wait()
}