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/querywss://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 不同点:
WebSocketHTTP通信模式双向单向握手双方协商浏览器发起服务器端推送支持不支持,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{returntrue},}
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{returntrue},}funchandler(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()}funcmain(){
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")funcWebSocketServer(){
addr :="localhost:8002"
http.HandleFunc("/wshandler", WebSocketUpgrade)
log.Println("Starting websocket server at "+ addr)gofunc(){
err := http.ListenAndServe(addr,nil)if err !=nil{
log.Fatal(err)}}()
log.Println("WebSocket 服务器正在运行。按Ctrl+C退出")select{}}funcWebSocketUpgrade(resp http.ResponseWriter, req *http.Request){// 初始化 Upgrader
upgrader := websocket.Upgrader{
ReadBufferSize:1024,
WriteBufferSize:1024,
CheckOrigin:func(r *http.Request)bool{returntrue},}// 使用默认的选项// 第三个参数是响应头,默认会初始化
conn, err := upgrader.Upgrade(resp, req,nil)if err !=nil{
log.Println(err)return}defer conn.Close()// 读取客户端的发送额消息,并返回goReadMessage(conn)select{}}// 读取客户端发送的消息,并返回funcReadMessage(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}}}funcmain(){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")funcWebSocketProxy(){
url, err := url.Parse(websocketServer)if err !=nil{
log.Println(err)}
proxy := httputil.NewSingleHostReverseProxy(url)
log.Println("WebSocket 代理启动, 按CTRL+C退出")
http.ListenAndServe(proxyServer, proxy)}funcmain(){WebSocketProxy()}
访问:
ws://localhost:8082/wshandler
5.4 WebSocket 服务端主动推送功能的实现
package main
import("fmt""log""net/http""time""github.com/gorilla/websocket")// websocket服务器每隔3秒会主动向服务器推送消息"Heart Beat"funcWebSocketServer(){
addr :="localhost:8002"
http.HandleFunc("/wshandler", WebSocketUpgrade)
log.Println("Starting websocket server at "+ addr)gofunc(){
err := http.ListenAndServe(addr,nil)if err !=nil{
log.Fatal(err)}}()
log.Println("WebSocket 服务器正在运行。按Ctrl+C退出")select{}}funcWebSocketUpgrade(resp http.ResponseWriter, req *http.Request){// 初始化 Upgrader
upgrader := websocket.Upgrader{
ReadBufferSize:1024,
WriteBufferSize:1024,
CheckOrigin:func(r *http.Request)bool{returntrue},}// 使用默认的选项// 第三个参数是响应头,默认会初始化
conn, err := upgrader.Upgrade(resp, req,nil)if err !=nil{
log.Println(err)return}defer conn.Close()// 主动向服务端推送消息goPushMessage(conn)// 读取客户端的发送额消息,并返回goReadMessage(conn)select{}}// websocket 服务器主动服务器推送消息funcPushMessage(conn *websocket.Conn){for{
err := conn.WriteMessage(websocket.TextMessage,[]byte("heart beat"))if err !=nil{
log.Println(err)return}
time.Sleep(time.Second *3)}}// 读取客户端发送的消息,并返回funcReadMessage(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}}}funcmain(){WebSocketServer()}
5.5 其他库
package main
import("log""net/http""golang.org/x/net/websocket")funcEchoWebSocket(ws *websocket.Conn){var err errorfor{var reply stringif err = websocket.Message.Receive(ws,&reply); err !=nil{break}if err = websocket.Message.Send(ws, reply); err !=nil{break}}
ws.Close()}funcmain(){
http.Handle("/echo", websocket.Handler(EchoWebSocket))if err := http.ListenAndServe(":5000",nil); err !=nil{
log.Fatal("ListenAndServe:", err)}}
版权归原作者 242030 所有, 如有侵权,请联系我们删除。