Golang_17: Go语言 网络编程:WebSocket 客户端与服务端

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

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

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

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

相关网站:

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

安装 Gorilla WebSocket:

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

导入后包名为 websocket

1. ConnDialerUpgrader

websocket 包中主要的三个类型:

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 参数表示不校验证书。

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Golang中的Gin框架提供了一种简单而强大的方法来构建Web应用程序。与此同时,Golang标准库中的"net/http"包提供了构建WebSocket服务器和客户端的功能。 首先,我们来看一下如何使用Gin和WebSocket构建WebSocket服务器。首先,需要导入相关的包: ```go import ( "github.com/gin-gonic/gin" "github.com/gorilla/websocket" ) ``` 接下来,在Gin中创建一个WebSocket处理函数: ```go func WebSocketHandler(c *gin.Context) { upgrader := websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, } conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } for { messageType, message, err := conn.ReadMessage() if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err = conn.WriteMessage(messageType, message) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } } } ``` 上面的代码创建了一个基本的WebSocket处理函数。它使用WebSocket标准库中的Upgrader结构来处理升级连接并创建一个WebSocket连接。 然后,我们需要在Gin中设置路由来处理WebSocket请求: ```go router := gin.Default() router.GET("/ws", WebSocketHandler) ``` 以上代码将在根路径下创建一个WebSocket处理函数。 接下来,我们来看一下如何使用Golang和Gin构建WebSocket客户端。首先,我们需要导入所需的包: ```go import ( "github.com/gorilla/websocket" "net/http" ) ``` 然后,我们可以使用以下代码来创建一个WebSocket客户端: ```go func main() { c, _, err := websocket.DefaultDialer.Dial("ws://localhost:8080/ws", nil) if err != nil { log.Fatal("dial:", err) } defer c.Close() done := make(chan struct{}) go func() { defer close(done) for { _, message, err := c.ReadMessage() if err != nil { log.Println("read:", err) return } log.Printf("recv: %s", message) } }() ticker := time.NewTicker(time.Second) defer ticker.Stop() for { select { case <-done: return case <-ticker.C: err := c.WriteMessage(websocket.TextMessage, []byte("Hello, Server!")) if err != nil { log.Println("write:", err) return } } } } ``` 上面的代码创建了一个WebSocket客户端,它使用WebSocket标准库中的`DefaultDialer`结构来建立WebSocket连接。 以上就是使用Golang Gin和WebSocket构建WebSocket客户端和服务器的简单示例。这些代码可以帮助我们使用Gin和Golang的标准库来构建强大的Web应用程序,并处理WebSocket通信。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

谢TS

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值