Go实现WebSocket
在本文中,将会使用在 Go 中一个用得比较多的 WebSocket
实现 gorilla/websocket
。
1、WebSocket介绍
WebSocket
是一种应用层协议,WebSocket
协议在 2008 年诞生,2011 年成为国际标准,现在最新版本浏览器
都已经支持了。
WebSocket
是一种在单个 TCP
连接上进行全双工通信的协议,WebSocket
使得客户端和服务器之间的数据交换
变得更加简单,允许服务端主动向客户端推送数据。Websocket
主要用在B/S架构的应用程序中,在 WebSocket
API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接, 并进行双向数据传输。
它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等
对话,属于服务器推送技术的一种。
1.1 WebSocket的典型特点
-
基于 TCP 协议的应用层协议,实现相对简单
-
单个 TCP 连接上进行全双工通信
-
兼容 HTTP 协议,默认端口也是 80 和 443
ws://host:port/path/query wss://host:port/path/query
-
握手阶段采用 HTTP 协议,能通过各种 HTTP 代理服务器
-
数据格式比较轻量,性能开销小,通信高效
-
可以发送文本和二进制数据
-
没有浏览器的同源限制
1.2 WebSocket的典型场景:
-
即时通信
-
协同编辑/编辑
-
实时数据流的拉取与推送
1.3 WebSocket 推送和浏览器轮询
在 B/S 开发领域,若需要浏览器即时得到服务器的状态更新,常使用两个方案:
-
浏览器端轮询
-
服务器端推送
浏览器轮询:浏览器端,当需要获取最新数据状态时,利用脚本程序循环向服务端发送请求。
服务器推送:服务器端,当状态改变时,将数据发送到浏览器端。
HTTP/2 版本也支持服务器端推送,但实现上以推送静态资源为主,不能基于业务逻辑推送特定的消息,因此当前
的普及使用率 WebSocket 还是主流。
1.4 HTTP与WebSocket的关系
如果我们此前已经使用过 WebSocket
,比如在 nginx
配置过 WebSocket
,我们就会发现:
1、有个类似 upgrade
的关键字。这个关键字体现了 HTTP 与 WebSocket
的本质区别。
2、在 nginx
里配置,意味着 WebSocket
本质上也是通过 HTTP 协议来工作的。
我们知道,HTTP 的请求会在请求结束之后断开 TCP
连接,但 WebSocket
不一样,它在建立连接之后会一直维
持着连接状态, 这样客户端与服务端就可以一直维持通信状态了。
WebSocket 和 http 相同点:
-
应用层协议
-
B/S 架构中使用
-
基于 TCP 协议
-
端口默认都是:80 和 443
WebSocket 和 http 不同点:
WebSocket | HTTP | |
---|---|---|
通信模式 | 双向 | 单向 |
握手 | 双方协商 | 浏览器发起 |
服务器端推送 | 支持 | 不支持,H/2部分支持 |
1.5 WebSocket建立连接的过程
在 WebSocket
协议中,初始的握手阶段使用标准的 HTTP
请求和响应:
1、客户端先发送一个 HTTP 请求,请求升级到 WebSocket
协议。
2、服务器在收到这个请求后,如果同意升级到 WebSocket
,就会返回一个状态码为 101
的 HTTP 响应,指示升
级成功,然后不会断开 TCP 连接。
这个过程涉及到的 HTTP 头部字段是 Upgrade
和 Connection
,具体而言,HTTP 请求头部可能包含类似以下的
字段:
请求:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
也就是说,我们所看到的 Upgrade
实际上是把一个 HTTP
连接升级为了 WebSocket
连接,这个连接可以实现双
向的通信。
这使得它非常适合实时通信的应用,例如聊天应用、在线游戏等。
2、WebSocket握手过程
通过 HTTP 请求响应,中的头信息,完成 WebSocket 握手,如图:
在请求头中添加如下信息:
# 升级为 websocket
Upgrade: websocket
Connection: Upgrade
# 一个Base64 encode的值,有于验证服务器端是否支持websocket
Sec-WebSocket-Key: x4JJHMbDL22zLk1GBhXDw==
# 用户协议,可以视为不同业务逻辑的频道
Sec-WebSocket-Protocol: chat
# 协议版本,13是当前通用版本,几乎不需要更改
Sec-WebSocket-Version: 13
基于以上请求头,服务器端,就知道需要将协议升级为 WebSocket
协议,并提供一些验证信息。
服务端的响应头:
HTTP/1.1 101 Switching Protocols
# 协议升级
Upgrade: websocket
# 连接状态
Connection: Upgrade
# WebSocket服务端根据Sec-WebSocket-Key生成
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
# WebSocket协议用户协议
Sec-WebSocket-Protocol: chat
基于以上响应头,浏览器端就知道服务器端升级成功,并通过了验证。
至此,B/S 端可以基于该连接,完成 websocket 双向通信了。
websocket 只能发送 GET 请求。
3、WebSocket状态码和消息类型
3.1 WebSocket协议状态码解析
WebSocket协议是一种基于TCP的全双工通信协议,它允许客户端和服务器之间进行实时的双向通信。在
WebSocket通信过程中,服务器和客户端会通过状态码来表示当前通信的状态或错误信息。
WebSocket 协议状态码是一个 16 位的整数,用于表示 WebSocket 连接的状态。状态码的第一个数字表示状态的
分类,后三个数字表示具体的状态。根据 WebSocket 协议的规范,状态码可以分为以下几类:
-
1xxx
:表示信息性状态码,用于传递一些非错误信息。 -
2xxx
:表示成功状态码,用于表示连接成功或操作成功。 -
3xxx
:表示重定向状态码,用于表示需要进一步操作以完成请求。 -
4xxx
:表示客户端错误状态码,用于表示客户端发送的请求有误。 -
5xxx
:表示服务器错误状态码,用于表示服务器无法完成请求。
常见的 WebSocket 协议状态码有:
1000
:正常关闭1001
:终端离开1002
:协议错误1003
:数据类型错误1005
:无法接收1006
:连接关闭异常1011
:服务器遇到异常
WebSocket协议状态码详解:
-
WebSocket协议状态码1000:正常关闭
状态码1000表示WebSocket连接正常关闭。当服务器或客户端决定关闭连接时,会发送状态码1000给对方,
表示连接关闭的原因是正常的。
-
WebSocket协议状态码1001:终端离开
状态码1001表示客户端离开。当客户端主动关闭连接时,会发送状态码1001给服务器,表示客户端离开。
-
WebSocket协议状态码1002:协议错误
状态码1002表示协议错误。当服务器或客户端收到的数据不符合WebSocket协议的规范时,会发送状态码
1002给对方,表示协议错误。
-
WebSocket协议状态码1003:数据类型错误
状态码1003表示数据类型错误。当服务器或客户端收到的数据类型不符合预期时,会发送状态码1003给对
方,表示数据类型错误。
-
WebSocket协议状态码1005:无法接收
状态码1005表示无法接收数据。当服务器或客户端由于某些原因无法接收数据时,会发送状态码1005给对
方,表示无法接收。
-
WebSocket协议状态码1006:连接关闭异常
状态码1006表示连接关闭异常。当服务器或客户端在关闭连接时遇到异常情况时,会发送状态码1006给对
方,表示连接关闭异常。
-
WebSocket协议状态码1011:服务器遇到异常
状态码1011表示服务器遇到异常。当服务器在处理WebSocket请求时遇到异常情况时,会发送状态码1011给
客户端,表示服务器遇到异常。
3.2 消息类型
-
TextMessage
和BinaryMessage
分别表示发送文本消息和二级制消息 -
CloseMessage
关闭帧,接收方收到这个消息就关闭连接 -
PingMessage
和PongMessage
:是保持心跳的帧
发送方 -> 接收方是 PingMessage
接收方 -> 发送方是 PongMessage
由服务器发 ping
给浏览器,浏览器返回 pong
消息。
4、gorilla/websocket中的基本概念
4.1 WebSocket 连接-Conn
在 gorilla/websocket 中使用 Conn
来表示一个 WebSocket
连接,它主要有如下作用:
-
发送消息给客户端:
Write*
方法,如WriteJSON
发送 JSON 类型消息,又或者WriteMessage
可以发送普通的文本消息。
-
接收客户端发送的消息:
Read*
方法,如ReadJSON
和ReadMessage
。 -
其他功能:关闭连接、获取客户端 IP 地址等
4.2 消息
在 gorilla/websocket 中,消息被分为以下几种:
-
数据消息:
-
-
TextMessage
文本消息:文本消息被解析为 UTF-8 编码的文本。需要应用程序来确保文本消息是有效的UTF-8 编码文本。
-
BinaryMessage
二进制消息:二进制消息的解析留给应用程序。
-
-
控制消息:可以调用
Conn
中的WriteControl
、WriteMessage
或NextWriter
方法,将控制消息发送给对方。
-
CloseMessage
关闭连接的消息PingMessage
ping 消息PongMessage
pong 消息
注意:应用程序需要先读取连接中的消息才能处理从对等方发送的 close
、ping
和 pong
消息。如果应用程序
对来自对等方的消息不感兴趣, 则应用程序应启动一个 goroutine
来读取和丢弃来自对等方的消息。
4.3 并发
虽然 Golang 中有 goroutine
可以支持我们做并发操作,但是在 gorilla/websocket 中, 一个 WebSocket
连接
只支持一个并发 reader
和一个并发 writer
。
我们的应用程序应该确保不超过一个 goroutine
同时调用写入方法(WriteMessage
、WriteJSON
)或者读取方
法(ReadMessage
、ReadJSON
)。
而 Close
和 WriteControl
方法可以与其他所有方法同时调用。
4.4 安全性
我们知道,在一般的 web 应用中,经常需要处理跨域的问题,同样的,在 gorilla/websocket 中也需要做一定的
配置。
我们可以在 Upgrader
中的 CheckOrigin
字段中指定函数的 Origin
检查策略,如果 CheckOrigin
函数返回
false
,则 Upgrader
方法将拒绝建立 WebSocket
连接,如果允许所有来源的连接,我们可以直接返回 true
即可。
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
4.5 缓冲
缓冲在 io 类操作中是一个很常见的术语,在 gorilla/websocket 中我们可以通过上面那段代码的
ReadBufferSize
和 WriteBufferSize
来指定连接的缓冲大小,以减少读取或写入消息时的系统调用次数。
默认大小为 4096
,建议限制为最大预期消息的大小,大于最大消息最大大小的缓冲区不会带来任何好处。
5、WebSocket例子
5.1 Hello World
让我们通过一个简单的 Hello World
程序来结束本文:
package main
import (
"log"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func handler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Fatal(err)
}
conn.WriteMessage(websocket.TextMessage, []byte("Hello, World!"))
conn.Close()
}
func main() {
http.HandleFunc("/ws", handler)
http.ListenAndServe(":8181", nil)
}
启动 WebSocket
服务端,在 http://www.websocket-test.com/
访问:
ws://127.0.0.1:8181/ws
5.2 消息发送和接收
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/websocket"
)
func WebSocketServer() {
addr := "localhost:8002"
http.HandleFunc("/wshandler", WebSocketUpgrade)
log.Println("Starting websocket server at " + addr)
go func() {
err := http.ListenAndServe(addr, nil)
if err != nil {
log.Fatal(err)
}
}()
log.Println("WebSocket 服务器正在运行。按Ctrl+C退出")
select {}
}
func WebSocketUpgrade(resp http.ResponseWriter, req *http.Request) {
// 初始化 Upgrader
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
} // 使用默认的选项
// 第三个参数是响应头,默认会初始化
conn, err := upgrader.Upgrade(resp, req, nil)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
// 读取客户端的发送额消息,并返回
go ReadMessage(conn)
select {}
}
// 读取客户端发送的消息,并返回
func ReadMessage(conn *websocket.Conn) {
for {
// 消息类型:文本消息和二进制消息
messageType, msg, err := conn.ReadMessage()
if err != nil {
log.Println(err)
return
}
fmt.Println("receive msg:", string(msg))
err = conn.WriteMessage(messageType, msg)
if err != nil {
log.Println("write error:", err)
return
}
}
}
func main() {
WebSocketServer()
}
访问:
ws://localhost:8002/wshandler
5.3 WebSocket代理实现
package main
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
)
var (
// 代理服务器地址
proxyServer = "127.0.0.1:8082"
// 真实websocket服务器地址
websocketServer = "http://127.0.0.1:8002"
)
func WebSocketProxy() {
url, err := url.Parse(websocketServer)
if err != nil {
log.Println(err)
}
proxy := httputil.NewSingleHostReverseProxy(url)
log.Println("WebSocket 代理启动, 按CTRL+C退出")
http.ListenAndServe(proxyServer, proxy)
}
func main() {
WebSocketProxy()
}
访问:
ws://localhost:8082/wshandler
5.4 WebSocket 服务端主动推送功能的实现
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/websocket"
)
// websocket服务器每隔3秒会主动向服务器推送消息"Heart Beat"
func WebSocketServer() {
addr := "localhost:8002"
http.HandleFunc("/wshandler", WebSocketUpgrade)
log.Println("Starting websocket server at " + addr)
go func() {
err := http.ListenAndServe(addr, nil)
if err != nil {
log.Fatal(err)
}
}()
log.Println("WebSocket 服务器正在运行。按Ctrl+C退出")
select {}
}
func WebSocketUpgrade(resp http.ResponseWriter, req *http.Request) {
// 初始化 Upgrader
upgrader := websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
CheckOrigin: func(r *http.Request) bool {
return true
},
} // 使用默认的选项
// 第三个参数是响应头,默认会初始化
conn, err := upgrader.Upgrade(resp, req, nil)
if err != nil {
log.Println(err)
return
}
defer conn.Close()
// 主动向服务端推送消息
go PushMessage(conn)
// 读取客户端的发送额消息,并返回
go ReadMessage(conn)
select {}
}
// websocket 服务器主动服务器推送消息
func PushMessage(conn *websocket.Conn) {
for {
err := conn.WriteMessage(websocket.TextMessage, []byte("heart beat"))
if err != nil {
log.Println(err)
return
}
time.Sleep(time.Second * 3)
}
}
// 读取客户端发送的消息,并返回
func ReadMessage(conn *websocket.Conn) {
for {
// 消息类型:文本消息和二进制消息
messageType, msg, err := conn.ReadMessage()
if err != nil {
log.Println(err)
return
}
fmt.Println("receive msg:", string(msg))
err = conn.WriteMessage(messageType, msg)
if err != nil {
log.Println("write error:", err)
return
}
}
}
func main() {
WebSocketServer()
}
5.5 其他库
package main
import (
"log"
"net/http"
"golang.org/x/net/websocket"
)
func EchoWebSocket(ws *websocket.Conn) {
var err error
for {
var reply string
if err = websocket.Message.Receive(ws, &reply); err != nil {
break
}
if err = websocket.Message.Send(ws, reply); err != nil {
break
}
}
ws.Close()
}
func main() {
http.Handle("/echo", websocket.Handler(EchoWebSocket))
if err := http.ListenAndServe(":5000", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}