在不刷新页面的情况下发送消息并获得即时响应是我们认为理所当然的事情。但在过去,启用实时功能对开发人员来说是一个真正的挑战。开发者社区已经从 HTTP 长轮询和 AJAX 走了很长一段路,终于找到了构建真正实时应用程序的解决方案。该解决方案以 WebSockets 的形式出现,它可以在用户的浏览器和服务器之间打开交互式会话。WebSockets 允许浏览器向服务器发送消息并接收事件驱动的响应,而无需轮询服务器以获取回复。目前,WebSockets 是构建实时应用程序的首选解决方案:在线游戏、即时通讯工具、跟踪应用程序等。本指南解释了 WebSockets 的运行方式,并展示了我们如何使用 Go 编程语言构建 WebSocket 应用程序。
网络套接字与 WebSockets
网络套接字
网络套接字,或简称为套接字,用作内部端点,用于在运行在同一台计算机或同一网络上的不同计算机上的应用程序之间交换数据。套接字是基于 Unix 和 Windows 的操作系统的关键部分,它们使开发人员可以更轻松地创建支持网络的软件。应用程序开发人员可以在他们的程序中包含套接字,而不是从头开始构建网络连接。由于网络套接字用于多种网络协议(HTTP、FTP 等),因此可以同时使用多个套接字。套接字是由套接字的应用程序编程接口 ( API )定义的一组函数调用创建和使用的。有几种类型的网络套接字: 数据报套接字(SOCK_DGRAM),也称为无连接套接字,使用用户数据报协议 (UDP)。数据报套接字支持双向消息流并保留记录边界。 流套接字(SOCK_STREAM),也称为面向连接的套接字,使用传输控制协议 (TCP)、流控制传输协议 (SCTP) 或数据报拥塞控制协议 (DCCP)。这些套接字提供双向、可靠、有序和不重复的数据流,没有记录边界。 原始套接字(或原始 IP 套接字)通常在路由器和其他网络设备中可用。这些套接字通常是面向数据报的,尽管它们的确切特性取决于协议提供的接口。大多数应用程序不使用原始套接字。提供它们是为了支持新通信协议的开发,并提供对现有协议更深奥的设施的访问。
套接字通信
每个网络套接字都由地址标识,地址是传输协议、IP 地址和端口号的三元组。主机之间的通信主要有两种协议:TCP 和 UDP。
- 连接到 TCP 套接字
Go 客户端使用 net 包中的 DialTCP 函数来建立 TCP 连接。DialTCP 返回一个 TCPConn 对象。建立连接后,客户端和服务器开始交换数据:客户端通过 TCPConn 对象向服务器发送请求,服务器解析请求并发送响应,TCPConn 对象接收来自服务器的响应。
此连接一直有效,直到客户端或服务器关闭它。创建连接的函数如下:客户端:
代码语言:javascript复制
// init
tcpAddr, err := net.ResolveTCPAddr(resolver, serverAddr)
if err != nil {
// handle error
}
conn, err := net.DialTCP(network, nil, tcpAddr)
if err != nil {
// handle error
}
// send message
_, err = conn.Write({message})
if err != nil {
// handle error
}
// receive message
var buf [{buffSize}]byte
_, err := conn.Read(buf[0:])
if err != nil {
// handle error
}
服务器端:
代码语言:javascript复制
// init
tcpAddr, err := net.ResolveTCPAddr(resolver, serverAddr)
if err != nil {
// handle error
}
listener, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
// handle error
}
// listen for an incoming connection
conn, err := listener.Accept()
if err != nil {
// handle error
}
// send message
if _, err := conn.Write({message}); err != nil {
// handle error
}
// receive message
buf := make([]byte, 512)
n, err := conn.Read(buf[0:])
if err != nil {
// handle error
}
- 连接到 UDP 套接字
与 TCP 套接字相反,使用 UDP 套接字,客户端只向服务器发送数据报。没有 Accept 函数,因为服务器不需要接受连接,只需等待数据报到达。
其他 TCP 功能与 UDP 对应;只需在上面的函数中用 UDP 替换 TCP。客户端:
代码语言:javascript复制
// init
raddr, err := net.ResolveUDPAddr("udp", address)
if err != nil {
// handle error
}
conn, err := net.DialUDP("udp", nil, raddr)
if err != nil {
// handle error
}
.......
// send message
buffer := make([]byte, maxBufferSize)
n, addr, err := conn.ReadFrom(buffer)
if err != nil {
// handle error
}
.......
// receive message
buffer := make([]byte, maxBufferSize)
n, err = conn.WriteTo(buffer[:n], addr)
if err != nil {
// handle error
}
服务器端:
代码语言:javascript复制 // init
udpAddr, err := net.ResolveUDPAddr(resolver, serverAddr)
if err != nil {
// handle error
}
conn, err := net.ListenUDP("udp", udpAddr)
if err != nil {
// handle error
}
.......
// send message
buffer := make([]byte, maxBufferSize)
n, addr, err := conn.ReadFromUDP(buffer)
if err != nil {
// handle error
}
.......
// receive message
buffer := make([]byte, maxBufferSize)
n, err = conn.WriteToUDP(buffer[:n], addr)
if err != nil {
// handle error
}
什么是 WebSocket
WebSocket 通信包通过单个 TCP 连接提供全双工通信通道。这意味着客户端和服务器都可以在需要时同时发送数据而无需任何请求。 WebSockets 是需要持续数据交换的服务的一个很好的解决方案——例如,即时通讯、在线游戏和实时交易系统。可以在 Internet 工程任务组 (IETF) RFC 6455 规范 中找到有关 WebSocket 协议的完整信息。
WebSocket 连接由浏览器请求并由服务器响应,然后建立连接。这个过程通常称为握手。WebSockets 中的特殊类型的标头只需要浏览器和服务器之间的一次握手即可建立连接,该连接将在其整个生命周期内保持活动状态。WebSocket 协议使用端口 80 进行不安全连接,使用端口 443 进行安全连接。WebSocket规范 确定 ws (WebSocket) 和 wss (WebSocket Secure) 协议需要哪些统一的资源标识符方案。WebSockets 解决了开发实时 Web 应用程序的许多令人头疼的问题,并且与传统 HTTP 相比有几个好处:
- 轻量级报头减少了数据传输开销。
- 单个 Web 客户端只需要一个 TCP 连接。
- WebSocket 服务器可以将数据推送到 Web 客户端。
WebSocket 协议实现起来比较简单。它使用 HTTP 协议进行初始握手。成功握手后,连接建立,WebSocket 本质上使用原始 TCP 来读/写数据。这是客户端请求的样子:
代码语言:javascript复制 GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
这是服务器响应:
代码语言:javascript复制 HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
如何在 Go 中创建 WebSocket 应用程序
要基于 net/http 库编写一个简单的 WebSocket 回显服务器,需要:
- 发起握手
- 从客户端接收数据帧
- 向客户端发送数据帧
- 关闭握手
首先,创建一个带有 WebSocket 端点的 HTTP 处理程序:
代码语言:javascript复制
// HTTP server with WebSocket endpoint
func Server() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
ws, err := NewHandler(w, r)
if err != nil {
// handle error
}
if err = ws.Handshake(); err != nil {
// handle error
}
然后初始化 WebSocket 结构。初始握手请求始终来自客户端。一旦服务器定义了一个 WebSocket 请求,它需要用一个握手响应来回复。不能使用 http.ResponseWriter 编写响应,因为一旦开始发送响应,它将关闭底层 TCP 连接。可以使用HTTP劫持。http劫持接管底层 TCP 连接处理程序和 bufio.Writer。这可以在不关闭 TCP 连接的情况下读取和写入数据。
代码语言:javascript复制
// NewHandler initializes a new handler
func NewHandler(w http.ResponseWriter, req *http.Request) (*WS, error) {
hj, ok := w.(http.Hijacker)
if !ok {
// handle error
}
}
要完成握手,服务器必须使用适当的标头进行响应。
代码语言:javascript复制
// Handshake creates a handshake header
func (ws *WS) Handshake() error {
hash := func(key string) string {
h := sha1.New()
h.Write([]byte(key))
h.Write([]byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}(ws.header.Get("Sec-WebSocket-Key"))
.....
}
“Sec-WebSocket-key”随机生成并且是Base64编码的。服务器接受请求后需要将此键附加到固定字符串。假设你有x3JJHMbDL1EzLkh9GBhXDw== key. 可以使用 SHA-1 来计算二进制值并使用 Base64 对其进行编码。可以得到HSmrc0sMlYUkAGmm5OPpG2HaGWk=。将此用作Sec-WebSocket-Accept响应标头的值。
传输数据帧
握手成功完成后,应用程序可以从客户端读取数据和向客户端写入数据。所述WebSocket规范定义了的一个客户机和一个服务器之间使用的特定帧格式。这是帧的位模式:
使用以下代码解码客户端负载:
代码语言:javascript复制
// Recv receives data and returns a Frame
func (ws *WS) Recv() (frame Frame, _ error) {
frame = Frame{}
head, err := ws.read(2)
if err != nil {
// handle error
}
反过来,这些代码行允许对数据进行编码:
代码语言:javascript复制
// Send sends a Frame
func (ws *WS) Send(fr Frame) error {
// make a slice of bytes of length 2
data := make([]byte, 2)
// Save fragmentation & opcode information in the first byte
data[0] = 0x80 | fr.Opcode
if fr.IsFragment {
data[0] &= 0x7F
}
.....
结束握手
当一方发送具有关闭状态的关闭帧作为有效载荷时,握手关闭。发送关闭帧的一方可以在有效载荷中发送关闭原因。如果关闭是由客户端发起的,服务器应该发送一个相应的关闭帧作为响应。
代码语言:javascript复制// Close sends a close frame and closes the TCP connection
func (ws *Ws) Close() error {
f := Frame{}
f.Opcode = 8
f.Length = 2
f.Payload = make([]byte, 2)
binary.BigEndian.PutUint16(f.Payload, ws.status)
if err := ws.Send(f); err != nil {
return err
}
return ws.conn.Close()
WebSocket 库列表
有几个第三方库可以简化开发人员的工作并极大地促进 WebSocket 的使用。
STDLIB ( x/net/websocket )
这个 WebSocket 库是标准 Go 库的一部分。它为 WebSocket 协议实现了客户端和服务器,如 RFC 6455 规范中所述。它不需要安装并且有很好的官方文档。另一方面,它仍然缺少一些可以在其他 WebSocket 库中找到的功能。/x/net/websocket 包中的 Golang WebSocket 实现不允许用户以明确的方式重用连接之间的 I/O 缓冲区。首先,要安装和使用这个库,需要添加这行代码:
代码语言:javascript复制import "golang.org/x/net/websocket"
客户端:
代码语言:javascript复制
// create connection
// schema can be ws:// or wss://
// host, port – WebSocket server
conn, err := websocket.Dial("{schema}://{host}:{port}", "", op.Origin)
if err != nil {
// handle error
}
defer conn.Close()
.......
// send message
if err = websocket.JSON.Send(conn, {message}); err != nil {
// handle error
}
.......
// receive message
// messageType initializes some type of message
message := messageType{}
if err := websocket.JSON.Receive(conn, &message); err != nil {
// handle error
}
.......
服务器端:
代码语言:javascript复制
// Initialize WebSocket handler server
mux := http.NewServeMux()
mux.Handle("/", websocket.Handler(func(conn *websocket.Conn) {
func() {
for {
// do something, receive, send, etc.
}
}
.......
// receive message
// messageType initializes some type of message
message := messageType{}
if err := websocket.JSON.Receive(conn, &message); err != nil {
// handle error
}
.......
// send message
if err := websocket.JSON.Send(conn, message); err != nil {
// handle error
}
........
Gorilla
Gorilla Web 工具包中的 WebSocket 包拥有完整且经过测试的 WebSocket 协议实现以及稳定的包 API。WebSocket 包的文档齐全且易于使用。可以在 Gorilla 官方网站上查看文档。安装:
代码语言:javascript复制
go get github.com/gorilla/websocket
Examples of code
Client side:
// init
// schema – can be ws:// or wss://
// host, port – WebSocket server
u := url.URL{
Scheme: {schema},
Host: {host}:{port},
Path: "/",
}
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
// handle error
}
.......
// send message
err := c.WriteMessage(websocket.TextMessage, {message})
if err != nil {
// handle error
}
.......
// receive message
_, message, err := c.ReadMessage()
if err != nil {
// handle error
}
.......
服务器端:
代码语言:javascript复制
// init
u := websocket.Upgrader{}
c, err := u.Upgrade(w, r, nil)
if err != nil {
// handle error
}
.......
// receive message
messageType, message, err := c.ReadMessage()
if err != nil {
// handle error
}
.......
// send message
err = c.WriteMessage(messageType, {message})
if err != nil {
// handle error
}
.......
Gobwas
这个微小的 WebSocket 包具有强大的功能列表,例如零拷贝升级和允许构建自定义数据包处理逻辑的低级 API。Gobwas 在 I/O 期间不需要中间分配。它还拥有 wsutil 包中 API 的高级包装器和帮助器,允许开发人员快速启动,而无需深入研究协议的内部。查看 GoDoc 网站以获取文档。通过包含以下代码行来安装 Gobwas:
代码语言:javascript复制go get github.com/gobwas/ws
客户端:
代码语言:javascript复制
// init
// schema – can be ws or wss
// host, port – ws server
conn, _, _, err := ws.DefaultDialer.Dial(ctx, {schema}://{host}:{port})
if err != nil {
// handle error
}
.......
// send message
err = wsutil.WriteClientMessage(conn, ws.OpText, {message})
if err != nil {
// handle error
}
.......
// receive message
msg, _, err := wsutil.ReadServerData(conn)
if err != nil {
// handle error
}
.......
服务器端:
代码语言:javascript复制
// init
listener, err := net.Listen("tcp", op.Port)
if err != nil {
// handle error
}
conn, err := listener.Accept()
if err != nil {
// handle error
}
upgrader := ws.Upgrader{}
if _, err = upgrader.Upgrade(conn); err != nil {
// handle error
}
.......
// receive message
for {
reader := wsutil.NewReader(conn, ws.StateServerSide)
_, err := reader.NextFrame()
if err != nil {
// handle error
}
data, err := ioutil.ReadAll(reader)
if err != nil {
// handle error
}
.......
}
.......
// send message
msg := "new server message"
if err := wsutil.WriteServerText(conn, {message}); err != nil {
// handle error
}
.......
GOWebsockets
该工具提供了广泛的易于使用的功能。它允许并发控制、数据压缩和设置请求头。GOWebsockets 支持用于发送和接收文本和二进制数据的代理和子协议。开发人员还可以启用或禁用 SSL 验证。在GoDoc 网站和项目的GitHub 页面上可以找到有关如何使用 GOWebsockets 的文档和示例。通过添加以下代码行来安装包:
代码语言:javascript复制go get github.com/sacOO7/gowebsocket
客户端:
代码语言:javascript复制
// init
// schema – can be ws or wss
// host, port – ws server
socket := gowebsocket.New({schema}://{host}:{port})
socket.Connect()
.......
// send message
socket.SendText({message})
or
socket.SendBinary({message})
.......
// receive message
socket.OnTextMessage = func(message string, socket gowebsocket.Socket) {
// hande received message
};
or
socket.OnBinaryMessage = func(data [] byte, socket gowebsocket.Socket) {
// hande received message
};
.......
服务器端:
代码语言:javascript复制
// init
// schema – can be ws or wss
// host, port – ws server
conn, _, _, err := ws.DefaultDialer.Dial(ctx, {schema}://{host}:{port})
if err != nil {
// handle error
}
.......
// send message
err = wsutil.WriteClientMessage(conn, ws.OpText, {message})
if err != nil {
// handle error
}
.......
// receive message
msg, _, err := wsutil.ReadServerData(conn)
if err != nil {
// handle error
}
nhooyr.io/websocket
还有一个常用的websocket库是nhooyr.io/websocket,关于这个库,煎鱼大佬在自己的书(Go语言编程之旅)中介绍很多,包括与其他库的一些比较,写的是很全面的,这里可以看一下书的电子版地址https://golang2.eddycjy.com/posts/ch4/02-protocol/
比较现有的解决方案
我们已经描述了用于 Golang 的四个最广泛使用的 WebSocket 库。下表包含这些工具的详细比较。
为了更好地分析它们的性能,还进行了几个基准测试。** 结果如下:
- Gobwas 与其他库相比具有显着优势。它每个操作的分配更少,每次分配使用的内存和时间更少。此外,它的 I/O 分配为零。此外,Gobwas 拥有创建 WebSocket 客户端-服务器交互和接收消息片段所需的所有方法。还可以使用它轻松处理 TCP 套接字。
- 如果感觉Gobwas不合适,你可以使用 Gorilla。它非常简单,并且具有几乎所有相同的功能。也可以使用 STDLIB,但它在生产环境中没有那么好,因为它缺少许多必要的功能,并且正如在基准测试中看到的那样,提供的性能较弱。GOWebsocket 与 STDLIB 大致相同。但是如果你需要快速构建一个原型或者MVP,它可以是一个合理的选择。
公众号:运维开发故事
github:https://github.com/orgs/sunsharing-note/dashboard
爱生活,爱运维