一、背景介绍
WebSocket 是 HTML5 中新增的一种协议,它是一个在单个 TCP 连接上进行全双工通讯的协议。这个协议在实时性和性能方面相对传统的 HTTP 有很大的提升,在实时性要求较高的场景下被广泛应用。
Golang 作为一门高效、并发性能极佳的语言,天然支持协程和管道的特性,使得它在实现 WebSocket 协议上有着得天独厚的优势。
本文将介绍如何使用 Golang 通过 Goroutine 和 Channel 实现一个简单的 WebSocket 服务。
二、WebSocket 协议简介
WebSocket 协议建立在 HTTP 协议之上,它允许客户端和服务器之间建立持久化的连接,进行双向数据传输。在 WebSocket 连接建立之后,客户端和服务器可以在不关闭连接的情况下,任意发送和接收数据。
WebSocket 协议的握手过程如下:
1. 客户端向服务器发送一个 HTTP 请求,请求头包含了协议升级字段 Upgrade 和 Connection,以及 Sec-WebSocket-Key 字段。
2. 服务器在接收到请求之后,会返回一个 HTTP 响应,响应头包含了 Upgrade 和 Connection 字段,以及 Sec-WebSocket-Accept 字段。
3. 客户端在接收到响应之后,会校验 Sec-WebSocket-Accept 字段的值是否正确,如果正确表示握手成功,之后客户端和服务器即可进行双向数据通讯。
三、实现 WebSocket
1. 建立连接
首先,我们需要创建一个 HTTP 服务器,监听指定的端口,并且在有 WebSocket 请求时升级连接。在 Golang 中,可以通过 http.ListenAndServe() 函数实现创建 HTTP 服务器。
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// 升级连接为 WebSocket
conn, err := Upgrade(w, r)
if err != nil {
log.Println(err)
return
}
// 处理 WebSocket 连接
handler(conn)
})
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}
解释一下上述的代码,我们首先通过 http.HandleFunc() 函数注册一个处理函数,当客户端请求根路径 "/" 时,会执行该函数。在函数内部,我们通过 Upgrade() 函数将 HTTP 连接升级为 WebSocket 连接,然后将连接交给 handler() 函数处理。
Upgrade() 函数的实现如下:
func Upgrade(w http.ResponseWriter, r *http.Request) (*websocket.Conn, error) {
// 检查请求头中是否包含 Upgrade 和 Connection 字段
if !strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade") ||
strings.ToLower(r.Header.Get("Upgrade")) != "websocket" {
return nil, errors.New("不是 WebSocket 请求")
}
// 构建 WebSocket 响应头
h := http.Header{}
h.Set("Connection", "Upgrade")
h.Set("Upgrade", "websocket")
h.Set("Sec-WebSocket-Accept", getWebSocketAccept(r.Header.Get("Sec-WebSocket-Key")))
// 升级连接
wsConn, err := websocket.Upgrade(w, r, h, 1024, 1024)
if err != nil {
return nil, err
}
return wsConn, nil
}
在 Upgrade() 函数中,我们首先校验请求头中是否包含 Upgrade 和 Connection 字段,如果不包含则认为不是 WebSocket 请求。
之后,我们需要构建 WebSocket 响应头,其中 Sec-WebSocket-Accept 字段的值需要通过计算来得到。具体的计算方式可以参考 WebSocket 协议。
最后,我们使用 websocket.Upgrade() 函数将 HTTP 连接升级为 WebSocket 连接。
2. 数据传输
WebSocket 可以在任意时刻进行数据传输,因此我们需要为每个 WebSocket 连接开启一个独立的 Goroutine 来处理读写操作。在 Goroutine 中,我们可以通过管道来进行数据传输,从而实现协程间的通讯。
func handler(conn *websocket.Conn) {
// 接收数据的通道
receiveChan := make(chan []byte, 1024)
// 发送数据的通道
sendChan := make(chan []byte, 1024)
// Goroutine:读取客户端发送的数据
go read(conn, receiveChan)
// Goroutine:向客户端发送数据
go write(conn, sendChan)
for {
select {
// 接收到数据
case data := <-receiveChan:
// 处理接收到的数据
fmt.Println("收到客户端发送的数据:", string(data))
// 发送数据
case send := <-sendChan:
// 向客户端发送数据
conn.WriteMessage(websocket.TextMessage, send)
}
}
}
在 handler() 函数中,我们通过 make() 函数创建了两个管道 receiveChan 和 sendChan,分别用于接收和发送数据。然后,我们开启两个 Goroutine 分别来处理读取客户端发送的数据和向客户端发送数据的操作。在 Goroutine 中,我们通过管道的方式进行数据传输。
3. 接收数据
接收数据的 Goroutine 的代码实现如下:
func read(conn *websocket.Conn, receiveChan chan []byte) {
for {
_, data, err := conn.ReadMessage()
if err != nil {
log.Println(err)
break
}
receiveChan <- data
}
conn.Close()
close(receiveChan)
}
在 read() 函数中,我们使用 conn.ReadMessage() 函数来阻塞读取客户端发送的数据,并将数据通过通道传递给 handler() 函数。如果在读取数据的过程中出现错误,则关闭连接。
4. 发送数据
向客户端发送数据的 Goroutine 的代码实现如下:
func write(conn *websocket.Conn, sendChan chan []byte) {
for {
data, ok := <-sendChan
if !ok {
break
}
err := conn.WriteMessage(websocket.TextMessage, data)
if err != nil {
log.Println(err)
break
}
}
conn.Close()
}
在 write() 函数中,我们监听 sendChan 通道,如果通道中有数据,则将数据发送给客户端。如果在发送数据的过程中出现错误,则关闭连接。
四、测试
在实现 WebSocket 服务之后,我们需要进行测试。可以使用浏览器自带的 WebSocket 测试工具或者使用第三方的 WebSocket 测试工具,如 wscat 等。
在使用测试工具连接 WebSocket 服务时,我们需要注意指定正确的 WebSocket 地址,并且在发送和接收数据时,需要按照 WebSocket 协议进行数据的编码和解码。
五、总结
本文介绍了如何使用 Golang 通过 Goroutine 和 Channel 实现一个简单的 WebSocket 服务,实现了 WebSocket 的握手、数据传输功能。
在实际应用中,我们需要考虑更多的因素,如请求频率、数据量大小、连接数、安全性等。但是,通过 Goroutine 和 Channel 的特性,我们可以轻松地实现高效、稳定的 WebSocket 服务。