原文链接:https://xiets.blog.csdn.net/article/details/130866531

版权声明:原创文章禁止转载

专栏目录:Golang 专栏(总目录)

Go 内置模块没有对 WebSocket 的支持,可以使用第三方库,推荐 Gorilla WebSocket。

相关网站:

GitHub:https://github.com/gorilla/websocketAPI 文档:https://pkg.go.dev/github.com/gorilla/websocket

Gorilla WebSocket 实现了 RFC 6455 中定义的 WebSocket 协议。

安装 Gorilla WebSocket:

$ go get -u github.com/gorilla/websocket

导入后包名为 websocket。

1. Conn、Dialer、Upgrader

websocket 包中主要的三个类型:

客户端/服务端 WebSocket 连接对象:websocket.Conn客户端连接 WebSocket 拨号时使用:websocket.Dialer服务端处理 WebSocket 连接时使用:websocket.Upgrader

websocket 包中表示消息类型(Message Type)的常量:

// 文本 数据消息, 数据将被解析为 UTF-8 编码的文本

websocket.TextMessage = 1

// 二进制 数据消息

websocket.BinaryMessage = 2

// 关闭 控制消息,

// 发送此消息是可以使用 FormatCloseMessage(closeCode int, text string) []byte 函数生成有效负载

websocket.CloseMessage = 8

// PING 控制消息, 消息负载是 UTF-8 编码的文本

websocket.PingMessage = 9

// PONG 控制消息, 消息负载是 UTF-8 编码的文本

websocket.PongMessage = 10

1.1 WebSocket 连接对象: Conn

websocket.Conn 类型主要属性和方法:

type Conn struct {

// 没有导出属性

}

// 远程 地址端口信息

func (c *Conn) RemoteAddr() net.Addr

// 本地 地址端口信息

func (c *Conn) LocalAddr() net.Addr

// 返回下一条 (文本/二进制) 数据消息 的 读取器, 没有数据消息时阻塞等待, 一个连接最多只能有一个打开的读取器。

// 只支持 TextMessage 和 BinaryMessage 数据消息类型 (不能读取控制消息)。

func (c *Conn) NextReader() (messageType int, r io.Reader, err error)

// 读取下一条 (文本/二进制) 数据消息, 内部调用 NextReader(), 然后读取成 []byte 后返回。

func (c *Conn) ReadMessage() (messageType int, p []byte, err error)

// 读取下一条 JSON 编码的 (文本/二进制) 数据消息, 然后存储到 v 指向的对象, 内部调用 NextReader()

func (c *Conn) ReadJSON(v interface{}) error

// 为发送下一条消息返回一个 写入器, 一个连接最多只能有一个打开的写入器。

// 支持所有消息类型 (TextMessage、BinaryMessage、PingMessage、PongMessage 和 CloseMessage)

func (c *Conn) NextWriter(messageType int) (io.WriteCloser, error)

// NextWriter() 的辅助方法, 发送一条任意类型的消息, 内部调用 NextWriter()

func (c *Conn) WriteMessage(messageType int, data []byte) error

// 在给定时间期限内发送一条控制消息, 超时或发送失败, 返回错误。

// 允许的消息类型为 CloseMessage、PingMessage 和 PongMessage。

func (c *Conn) WriteControl(messageType int, data []byte, deadline time.Time) error

// 发送一条封装好的消息

func (c *Conn) WritePreparedMessage(pm *PreparedMessage) error

// 把对象 v 进行 JSON 编码, 然后以 TextMessage 形式发送出去。

func (c *Conn) WriteJSON(v interface{}) error

// 设置接收到 PingMessage 消息的处理函数, 默认的处理函数为接收到 PingMessage 后自动回复 PongMessage。

func (c *Conn) SetPingHandler(h func(appData string) error)

// 设置接收到 PongMessage 消息的处理函数, 默认的处理函数什么也不做(直接返回 nil)。

func (c *Conn) SetPongHandler(h func(appData string) error)

// 设置接收到 CloseMessage 消息的处理函数, 默认的处理函数将关闭消息发送回对等方。

func (c *Conn) SetCloseHandler(h func(code int, text string) error)

// 设置从对方读取的消息的最大大小 (字节), 如果超出显示, 则向对方发送 关闭控制消息, 并返回 ErrReadLimit 给应用程序。

func (c *Conn) SetReadLimit(limit int64)

// 设置底层 Socket 连接的读取期限, t 传零值表示不超时, 内部调用 net.Conn.SetReadDeadline(t) 方法。

func (c *Conn) SetReadDeadline(t time.Time) error

// 设置底层 Socket 连接的写入期限, t 传零值表示不超时。

func (c *Conn) SetWriteDeadline(t time.Time) error

// 关闭底层 Socket 连接

func (c *Conn) Close() error

// 返回底层 Socket 连接

func (c *Conn) UnderlyingConn() net.Conn

1.2 客户端: Dialer

websocket.Dialer 用于客户端连接 WebSocket 服务端的拨号器。

websocket.Dialer 类型主要属性和方法:

type Dialer struct {

// 创建 TCP 连接的拨号函数, 默认为 nil 表示使用 net.Dial()

NetDial func(network, addr string) (net.Conn, error)

// 创建 TCP 连接的拨号函数, 默认为 nil 表示使用 NetDial

NetDialContext func(ctx context.Context, network, addr string) (net.Conn, error)

// 用于创建 TLS/TCP 连接的拨号函数, 默认为 nil 表示使用 NetDialContext

NetDialTLSContext func(ctx context.Context, network, addr string) (net.Conn, error)

// 设置代理, 函数类型, 支持 "http", "https", "socks5" 代理类型。

// 可以调用 http.ProxyURL(proxyUrl *url.URL) 返回一个设置代理的函数,

// 也可以直接传 http.ProxyFromEnvironment 函数从系统环境变量(HTTP_PROXY/HTTPS_PROXY)中获取代理。

Proxy func(*http.Request) (*url.URL, error)

// TLS 相关配置

TLSClientConfig *tls.Config

// 握手超时时间

HandshakeTimeout time.Duration

// 读取和写入的缓冲区大小 (不限制可以发送或接收的消息的大小), 0 表示使用默认的大小。

ReadBufferSize, WriteBufferSize int

// 用于写操作的缓冲区池

WriteBufferPool BufferPool

// 指定客户端请求的子协议

Subprotocols []string

// 指定客户端是否应该尝试协商是否压缩每个消息

EnableCompression bool

// 指定 cookie jar, nil 表示不处理 cookie

Jar http.CookieJar

}

// WebSocket 拨号连接 (使用默认上下文), 返回 WebSocket连接对象、响应 和 错误

func (d *Dialer) Dial(urlStr string, requestHeader http.Header) (*Conn, *http.Response, error)

// WebSocket 拨号连接 (使用指定上下文), 返回 WebSocket连接对象、响应 和 错误

func (d *Dialer) DialContext(ctx context.Context, urlStr string, requestHeader http.Header) (*Conn, *http.Response, error)

// 默认的拨号器实例

var DefaultDialer = &Dialer{

Proxy: http.ProxyFromEnvironment,

HandshakeTimeout: 45 * time.Second,

}

1.3 服务端: Upgrader

Gorilla 的 WebSocket 服务端的 HTTP 服务器使用的还是内置的 http 模块的实现,websocket.Upgrader 用于将服务端接收到的 HTTP 请求升级转换为 WebSocket 连接。

websocket.Upgrader 类型主要属性和方法:

type Upgrader struct {

// 握手超时时间

HandshakeTimeout time.Duration

// 读取和写入的缓冲器大小 (不限制可以发送或接收的消息的大小), 0 表示使用 HTTP 服务器分配的缓冲器。

ReadBufferSize, WriteBufferSize int

// 用于写操作的缓冲区池

WriteBufferPool BufferPool

// 按优先顺序指定服务器支持的协议

Subprotocols []string

// 用于生成 HTTP 错误响应的函数, 如果为 nil, 则使用 http.Error() 生成 HTTP 响应。

Error func(w http.ResponseWriter, r *http.Request, status int, reason error)

// 用于防止跨站点伪造连接。如果请求头有 "Origin",

// 则用此函数校验支付支持当前 Host, 支持返回 true, 不支持返回 false。

// 如果为 nil, 则使用安全的默认值。

CheckOrigin func(r *http.Request) bool

// 指定服务器是否应尝试进行协商压缩消息

EnableCompression bool

}

// 把 HTTP请求 升级转换为 WebSocket连接对象, 并写出状态行和响应头 (responseHeader 表示额外写到客户端的响应头)

func (u *Upgrader) Upgrade(w http.ResponseWriter, r *http.Request, responseHeader http.Header) (*Conn, error)

2. WebSocket 代码示例

实现一个 echo WebSocket 服务端,并用 WebSocket 客户端连接定时发送消息。

2.1 WebSocket 服务端

服务端代码:server.go

package main

import (

"fmt"

"github.com/gorilla/websocket"

"net/http"

)

// Upgrader 用于将 HTTP请求升级转换为 WebSocket连接

var wsUpgrader = &websocket.Upgrader{

ReadBufferSize: 1024,

WriteBufferSize: 1024,

CheckOrigin: func(r *http.Request) bool {

// 允许跨站, 不校验 Origin 请求头

return true

},

}

// wsEchoHandle 用于处理 WebSocket 连接

func wsEchoHandle(writer http.ResponseWriter, req *http.Request) {

// 输出客户端请求信息

fmt.Printf("wsEchoHandle: Req Info: %s %s %s\n", req.Method, req.URL, req.Proto)

// 把 HTTP 请求升级转换为 WebSocket 连接, 并写出 状态行 和 响应头。

// conn 表示一个 WebSocket 连接, 调用此方法后状态行和响应头已写出, 不能再调用 writer.WriteHeader() 方法。

conn, err := wsUpgrader.Upgrade(writer, req, nil)

if err != nil {

fmt.Printf("upgrade error: %v\n", err)

return

}

defer conn.Close()

// 输出 WebSocket 连接信息

fmt.Printf("wsEchoHandle: websocket info: RemoteAddr=%v, LocalAddr=%v, Subprotocol=%v\n",

conn.RemoteAddr(), conn.LocalAddr(), conn.Subprotocol())

for {

// 读取下一条 (Text/Binary) 数据消息 (接收到 Close 消息或连接异常断开时, 此方法结束阻塞并返回错误)

msgType, msg, err := conn.ReadMessage()

if err != nil {

fmt.Printf("read error: %v\n", err)

break

}

// 打印读取到的数据消息, msgType 的值为 websocket.TextMessage 或 websocket.BinaryMessage

fmt.Printf("read client(%v) msg: msgType=%d, msg=%s\n",

conn.RemoteAddr(), msgType, string(msg))

// conn.ReadMessage() 只能读取 (Text/Binary) 数据消息, 不能读取 (Ping/Pong/Close) 控制消息。

// 控制消息通过设置对应的 handler 函数处理, 如:

// conn.SetPingHandler(), conn.SetPongHandler(), conn.SetCloseHandler()。

// 控制消息的 handler 函数均有默认值:

// PingHandler 默认为自动回复 PongMessage,

// PongHandler 默认什么也不做,

// CloseHandler 默认把 CloseMessage 发回对方。

// 把消息写回客户端

err = conn.WriteMessage(msgType, msg)

if err != nil {

fmt.Printf("write error: %v\n", err)

break

}

}

fmt.Printf("%v OVER\n", conn.RemoteAddr())

}

// httpHandler 普通的 HTTP 请求处理器

func httpHandler(writer http.ResponseWriter, req *http.Request) {

clientInfo := fmt.Sprintf("%s %s %s\n", req.Method, req.URL, req.Proto)

fmt.Printf("httpHandler: %s\n", clientInfo)

writer.WriteHeader(http.StatusOK)

_, _ = fmt.Fprintf(writer, clientInfo)

}

func main() {

// Gorilla 的 WebSocket 服务端使用的是内模块 http 包中的 HTTP Server。

// 接收到 HTTP 请求后, 再使用 Gorilla 的 websocket.Upgrader 把 HTTP 请求升级转换为 WebSocket 连接。

http.HandleFunc("/", httpHandler)

http.HandleFunc("/echo", wsEchoHandle)

err := http.ListenAndServe(":8000", nil)

fmt.Printf("error: %v\n", err)

}

运行服务端:

$ go run server.go

2.2 WebSocket 客户端

客户端代码:client.go

package main

import (

"fmt"

"github.com/gorilla/websocket"

"time"

)

func main() {

wsUrl := "ws://localhost:8000/echo"

// 连接 WebSocket

conn, resp, err := websocket.DefaultDialer.Dial(wsUrl, nil)

if err != nil {

fmt.Printf("dial error: %v\n", err)

return

}

defer func(conn *websocket.Conn) {

// 关闭底层 TCP 网络连接

err := conn.Close()

if err != nil {

fmt.Printf("close tcp conn error: %v\n", err)

}

}(conn)

// 输出响应信息

fmt.Printf("ws resp: Status=%s, StatusCode=%d, Proto=%s\n",

resp.Status, resp.StatusCode, resp.Proto)

for i := 0; i < 5; i++ {

// 发送数据消息到服务端

text := "Time: " + time.Now().Format("2006-01-02 15:04:05.999")

err := conn.WriteMessage(websocket.TextMessage, []byte(text))

if err != nil {

fmt.Printf("write error: %v\n", err)

break

}

// 读取 (Text/Binary) 数据消息 (接收到 Close 消息或连接异常断开时, 此方法结束阻塞并返回错误)。

// msgType 的值为 websocket.TextMessage 或 websocket.BinaryMessage,

// msg 是 []byte 类型, 如果死 Text 消息, 则为 UTF-8 编码的文本。

msgType, msg, err := conn.ReadMessage()

if err != nil {

fmt.Printf("read error: %v\n", err)

break

}

// 打印读取到的数据消息

fmt.Printf("read server msg: msgType=%d, msg=%s\n", msgType, string(msg))

// conn.ReadMessage() 只能读取 (Text/Binary) 数据消息, 不能读取 (Ping/Pong/Close) 控制消息。

// 控制消息通过设置对应的 handler 函数处理, 如:

// conn.SetPingHandler(), conn.SetPongHandler(), conn.SetCloseHandler()。

// 控制消息的 handler 函数均有默认值:

// PingHandler 默认为自动回复 PongMessage,

// PongHandler 默认什么也不做,

// CloseHandler 默认把 CloseMessage 发回对方。

// 延迟一段时间继续发送

time.Sleep(3 * time.Second)

}

// 发送一条 Close 消息到服务端, 通知服务端正常关闭 WebSocket 连接

closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")

err = conn.WriteControl(websocket.CloseMessage, closeMsg, time.Now().Add(3*time.Second))

if err != nil {

fmt.Printf("write control error: %v\n", err)

return

}

fmt.Printf("OVER\n")

}

运行客户端:

$ go run client.go

3. 安全的 WebSocket (WSS)

Gorilla WebSocket 服务端使用的是内置模块 http 包中 HTTP Server,只需要把启动 HTTP 服务时调用的 http.ListenAndServe() 方法改为调用 http.ListenAndServeTLS() 方法启动 HTTPS 服务即可。启动 HTTPS 服务需要传递 TLS 证书和私钥。

服务端证书一般由值得信任的第三方机构签名颁发,浏览器(HTTP 客户端)才会信任。作为测试可以使用 OpenSSL 工具生成自签名证书。

使用 openssl 工具命令生成自签名证书步骤:

# 1. 生成 私钥文件(key.cer)

$ openssl genrsa -out key.cer 2048

# 2. 根据私钥生成 证书申请文件(cert_req.cer), 需要输入国家/地区、省份/城市、域名、邮箱等信息 (密码可以不输入)

$ openssl req -new -key key.cer -out cert_req.cer

# 3. 使用 私钥 对 证书申请文件 进行签名, 从而生成 证书文件(cert.cer)

$ openssl x509 -req -in cert_req.cer -out cert.cer -signkey key.cer -days 365

使用 http.ListenAndServeTLS() 函数监听 TCP 端口并启动 HTTPS 服务,需要使用上面步骤生成的两个文件:证书文件(cert.cer) 和 私钥文件(key.cer)

WebSocket (WSS) 服务端:server.go

package main

import (

"fmt"

"github.com/gorilla/websocket"

"net/http"

)

var wsUpgrader = &websocket.Upgrader{

ReadBufferSize: 1024,

WriteBufferSize: 1024,

CheckOrigin: func(r *http.Request) bool {

// 允许跨站, 不校验 Origin 请求头

return true

},

}

func wsEchoHandle(writer http.ResponseWriter, req *http.Request) {

fmt.Printf("wsEchoHandle: Req Info: %s %s %s\n", req.Method, req.URL, req.Proto)

conn, err := wsUpgrader.Upgrade(writer, req, nil)

if err != nil {

fmt.Printf("upgrade error: %v\n", err)

return

}

defer conn.Close()

fmt.Printf("wsEchoHandle: websocket info: RemoteAddr=%v, LocalAddr=%v, Subprotocol=%v\n",

conn.RemoteAddr(), conn.LocalAddr(), conn.Subprotocol())

for {

msgType, msg, err := conn.ReadMessage()

if err != nil {

fmt.Printf("read error: %v\n", err)

break

}

fmt.Printf("read client(%v) msg: msgType=%d, msg=%s\n",

conn.RemoteAddr(), msgType, string(msg))

err = conn.WriteMessage(msgType, msg)

if err != nil {

fmt.Printf("read error: %v\n", err)

break

}

}

fmt.Printf("%v OVER\n", conn.RemoteAddr())

}

// httpHandler 普通的 HTTP 请求处理器

func httpHandler(writer http.ResponseWriter, req *http.Request) {

clientInfo := fmt.Sprintf("%s %s %s\n", req.Method, req.URL, req.Proto)

fmt.Printf("httpHandler: %s\n", clientInfo)

writer.WriteHeader(http.StatusOK)

_, _ = fmt.Fprintf(writer, clientInfo)

}

func main() {

http.HandleFunc("/", httpHandler)

http.HandleFunc("/echo", wsEchoHandle)

certFile := "cert.cer" // 证书文件路径

keyFile := "key.cer" // 私钥文件路径

err := http.ListenAndServeTLS(":8000", certFile, keyFile, nil)

fmt.Printf("error: %v\n", err)

}

WebSocket (WSS) 客户端:client.go

package main

import (

"crypto/tls"

"fmt"

"github.com/gorilla/websocket"

"net/http"

"time"

)

func main() {

// 使用 wws URL

wsUrl := "wss://localhost:8000/echo"

// 创建拨号器

dialer := &websocket.Dialer{

Proxy: http.ProxyFromEnvironment,

HandshakeTimeout: 45 * time.Second,

TLSClientConfig: &tls.Config{

// 指定不校验 SSL/TLS 证书 (由于是自签名的证书, 无法通过安全校验)

InsecureSkipVerify: true,

},

}

conn, resp, err := dialer.Dial(wsUrl, nil)

if err != nil {

fmt.Printf("dial error: %v\n", err)

return

}

defer conn.Close()

fmt.Printf("ws resp: Status=%s, StatusCode=%d, Proto=%s\n",

resp.Status, resp.StatusCode, resp.Proto)

for i := 0; i < 5; i++ {

text := "Time: " + time.Now().Format("2006-01-02 15:04:05.999")

err := conn.WriteMessage(websocket.TextMessage, []byte(text))

if err != nil {

fmt.Printf("write error: %v\n", err)

break

}

msgType, msg, err := conn.ReadMessage()

if err != nil {

fmt.Printf("read error: %v\n", err)

break

}

fmt.Printf("read server msg: msgType=%d, msg=%s\n", msgType, string(msg))

time.Sleep(3 * time.Second)

}

closeMsg := websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")

err = conn.WriteControl(websocket.CloseMessage, closeMsg, time.Now().Add(3*time.Second))

if err != nil {

fmt.Printf("write control error: %v\n", err)

return

}

fmt.Printf("OVER\n")

}

运行服务端:

$ go run server.go

运行客户端:

$ go run client.go

使用 HTTPS 访问:

浏览器访问: https://localhost:8000/CURL 命令访问: curl -k https://localhost:8000

由于是自签名的证书,没有经过权威的第三方机构签名认证,浏览器访问时会警告提示不安全并停止访问(也就是自己无法证明自己,需要值得信任的第三方机构来证明自己),可以选择信任此证书继续访问或安装到本地信任证书。CURL 命令加 -k 参数表示不校验证书。

好文推荐

评论可见,请评论后查看内容,谢谢!!!评论后请刷新页面。