WebSocket是一种在单个TCP连接上进行全双工通信的协议。WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。
WebSocket在2011年被IETF标准化为RFC 6455,并由W3C定义了对应的JavaScript API。
传统的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 库:
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
。消息被分为以下几种:
TextMessage
文本消息:文本消息被解析为 UTF-8 编码的文本。需要应用程序来确保文本消息是有效的 UTF-8 编码文本。BinaryMessage
二进制消息:二进制消息的解析留给应用程序。Conn
中的 WriteControl
、WriteMessage
或 NextWriter
方法,将控制消息发送给对方。
CloseMessage
关闭连接的消息PingMessage
ping 消息PongMessage
pong 消息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)
}
}
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()
}