WebSocket
websocket 的GitHub地址:https://github.com/gorilla/websocket
对于 Chat
https://github.com/gorilla/websocket/blob/main/examples/chat/
是一个简单的在线聊天应用的实现,旨在展示如何使用gorilla/websocket
库来构建一个基于WebSocket的应用。WebSocket是一个协议,允许客户端和服务器之间建立持久的连接并进行双向的实时通信。这种特性使其非常适合用于聊天应用、实时数据推送等场景。
使用这个chat
示例,你可以学习如何:
- 设置和使用WebSocket服务器。
- 处理客户端连接和断开。
- 接收和发送WebSocket消息。
- 如何在聊天应用场景中管理用户、消息等。
Server
服务器应用程序定义了两种类型, Client
和 Hub
。服务器为每个WebSocket连接创建一个 Client
类型的实例。 Client
充当WebSocket连接和 Hub
类型的单个实例之间的中介。 Hub
维护一组已注册的客户端,并向客户端广播消息。
应用程序为 Hub
运行一个goroutine,为每个 Client
运行两个goroutine。goroutine之间使用通道进行通信。 Hub
具有用于注册客户端、取消注册客户端和广播消息的通道。 Client
具有出站消息的缓冲通道。客户端的一个goroutine从这个通道读取消息并将消息写入WebSocket。另一个客户端goroutine从WebSocket读取消息并将其发送到集线器。
Hub
Hub
类型的代码在hub.go中。应用程序的 main
函数将hub的 run
方法作为一个goroutine启动。客户端使用 register
、 unregister
和 broadcast
通道向集线器发送请求。
集线器通过在 clients
映射中添加客户端指针作为键来注册客户端。map值始终为true。
注销代码稍微复杂一点。除了从 clients
映射中删除客户端指针之外,集线器还关闭客户端的 send
通道,以向客户端发出不再向客户端发送消息的信号。
集线器通过循环已注册的客户端并将消息发送到客户端的 send
通道来处理消息。如果客户端的 send
缓冲区已满,则集线器假定客户端已死亡或卡住。在这种情况下,集线器注销客户端并关闭WebSocket。
这个Hub
充当WebSocket聊天服务的中心,负责管理所有活跃的客户端和广播消息给这些客户端。以下是对代码的逐段解释:
- Hub的定义:
type Hub struct {
clients map[*Client]bool
broadcast chan []byte
register make(chan *Client)
unregister make(chan *Client)
}
clients
: 一个map,使用Client
指针作为键,用于跟踪注册的客户端。broadcast
: 一个通道,用于从客户端接收消息并将其广播给所有其他客户端。register
: 一个通道,用于注册新的客户端。unregister
: 一个通道,用于注销客户端。
- newHub函数:
func newHub() *Hub {
return &Hub{
broadcast: make(chan []byte),
register: make(chan *Client),
unregister: make(chan *Client),
clients: make(map[*Client]bool),
}
}
这个函数初始化并返回一个新的Hub
实例。
- Hub的run方法:
func (h *Hub) run() {
for {
select {
case client := <-h.register:
h.clients[client] = true
case client := <-h.unregister:
if _, ok := h.clients[client]; ok {
delete(h.clients, client)
close(client.send)
}
case message := <-h.broadcast:
for client := range h.clients {
select {
case client.send <- message:
default:
close(client.send)
delete(h.clients, client)
}
}
}
}
}
这个方法是Hub的核心,它不断地监听四个通道(register, unregister, broadcast)以及对应的操作:
- 当有客户端要注册时,它将会从
register
通道接收到这个客户端并将其添加到clients
map中。 - 当有客户端要注销时,它会从
unregister
通道接收到这个客户端并从clients
map中移除。 - 当有消息需要广播时,它会从
broadcast
通道接收到这条消息,并尝试将这条消息发送给每一个在clients
map中的客户端。如果发送失败(可能由于客户端的发送通道已满或其他原因),它将关闭那个客户端的发送通道并从clients
map中移除该客户端。
Client
Client
类型的代码在client.go中。
serveWs
函数由应用程序的 main
函数注册为HTTP处理程序。处理程序将HTTP连接升级到WebSocket协议,创建一个客户端,向集线器注册该客户端,并使用defer语句将该客户端调度为未注册。
接下来,HTTP处理程序将客户端的 writePump
方法作为一个goroutine启动。此方法将消息从客户端的发送通道传输到WebSocket连接。当hub关闭通道或写入WebSocket连接时,writer方法退出。
最后,HTTP处理程序调用客户端的 readPump
方法。此方法将入站消息从WebSocket传输到集线器。
WebSocket连接支持一个并发读取器和一个并发写入器。应用程序通过执行来自 readPump
goroutine的所有读操作和来自 writePump
goroutine的所有写操作来确保这些并发需求得到满足。
为了提高高负载下的效率, writePump
函数将 send
通道中的未决聊天消息合并为单个WebSocket消息。这减少了系统调用的数量和通过网络发送的数据量。
这段代码描述了Client
结构,它是WebSocket聊天服务的核心组件,用于管理与每个客户端的WebSocket连接。以下是代码的逐段解释:
- 常量和变量定义:
const (
...
)
var (
newline = []byte{'\n'}
space = []byte{' '}
)
var upgrader = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
}
这里定义了一些全局常量和变量。其中,upgrader
是用于升级HTTP请求到WebSocket的工具。
- 建立握手:当客户端想要建立一个 WebSocket 连接时,它首先发送一个标准的 HTTP GET 请求,但其中包含了一些特定的头信息(如:
Upgrade: websocket
和Connection: Upgrade
),这些头信息表明客户端希望升级这个连接到 WebSocket。如果服务器支持 WebSocket,并且满足了一定的条件,它就会回应一个状态码为 101 的响应,表示连接已经升级。- WebSocket 不是纯 HTTP:尽管 WebSocket 的握手使用 HTTP,但一旦握手完成,连接就完全脱离了 HTTP 协议,并转为 WebSocket 协议。WebSocket 协议更为轻量,适用于长时间运行的连接,可以双向发送数据,并且没有冗长的头信息。
- 复用已有的端口:由于 WebSocket 连接的建立是基于 HTTP 的,因此它可以复用 HTTP 和 HTTPS 使用的标准端口(例如 80 和 443),这有助于避免防火墙和其他网络设备的限制。
- Client的定义:
type Client struct {
hub *Hub
conn *websocket.Conn
send chan []byte
}
hub
: 客户端关联的Hub
实例。conn
: 客户端的WebSocket连接。send
: 一个通道,用于发送出站消息到WebSocket。
- readPump方法:
func (c *Client) readPump() {
...
}
这个方法处理从WebSocket连接中读取的消息并发送给Hub
。当连接关闭或发生错误时,它将注销客户端并关闭连接。
- writePump方法:
func (c *Client) writePump() {
...
}
这个方法处理从Hub
接收的消息并发送给WebSocket连接。它还定期发送ping消息以确保连接的活跃性。
- serveWs函数:
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
...
}
这个函数处理WebSocket请求。它首先使用upgrader
升级HTTP请求,然后为每个新的WebSocket连接创建一个Client
实例并注册到Hub
。接着,它为每个连接启动writePump
和readPump
的goroutines。
总结: 这段代码通过使用gorilla/websocket
库实现了WebSocket的客户端管理。每个Client
都有一个对应的WebSocket连接,它使用readPump
方法读取消息并使用writePump
方法写入消息。这两个方法都在它们自己的goroutine中运行,确保了高并发性和性能。
在实际应用中,使用此种模式可以轻松地实现高性能的WebSocket服务器,这得益于Go的并发模型和gorilla/websocket
库提供的功能。
Frontend
前端代码在home.html中。
在加载文档时,脚本检查浏览器中的WebSocket功能。如果WebSocket功能可用,则脚本将打开到服务器的连接,并注册一个回调来处理来自服务器的消息。
回调函数使用appendLog函数将消息附加到聊天记录中。
为了允许用户手动滚动聊天记录而不受新消息的干扰, appendLog
功能在添加新内容之前检查滚动位置。如果聊天记录滚动到底部,则该功能在添加内容后将新内容滚动到视图中。否则,滚动位置不会改变。
表单处理程序将用户输入写入WebSocket并清除输入字段。
main
这段代码是一个简单的Go语言Web服务器的主程序。它使用了net/http
标准库来处理HTTP请求。让我们逐段分析这段代码:
- 导入库和包声明:
package main
import (
"flag"
"log"
"net/http"
"time"
)
上面的代码导入了所需的库:flag
用于解析命令行参数,log
用于记录日志,net/http
用于处理HTTP请求,time
用于时间相关操作。
- 命令行参数解析:
var addr = flag.String("addr", ":8080", "http service address")
这行代码定义了一个命令行参数addr
,它指定了HTTP服务的监听地址,默认为:8080
。
- 定义首页处理函数:
func serveHome(w http.ResponseWriter, r *http.Request) {
log.Println(r.URL)
if r.URL.Path != "/" {
http.Error(w, "Not found", http.StatusNotFound)
return
}
if r.Method != http.MethodGet {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
http.ServeFile(w, r, "home.html")
}
serveHome
函数是一个HTTP处理器函数,用于处理对/
路径的请求。它先检查请求路径,只有路径为/
时才继续处理。然后,它确保请求方法为GET。最后,它发送home.html
文件作为响应。
- 主函数:
func main() {
flag.Parse()
hub := newHub()
go hub.run()
http.HandleFunc("/", serveHome)
http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
serveWs(hub, w, r)
})
server := &http.Server{
Addr: *addr,
ReadHeaderTimeout: 3 * time.Second,
}
err := server.ListenAndServe()
if err != nil {
log.Fatal("ListenAndServe: ", err)
}
}
这是程序的主函数,步骤如下:
flag.Parse()
:解析命令行参数。hub := newHub()
:创建一个新的hub
实例,这可能是用于WebSocket连接管理的结构。go hub.run()
:在一个新的goroutine中运行hub.run()
,可能是为了处理WebSocket连接和消息。http.HandleFunc()
:注册HTTP处理函数。对/
的请求交给serveHome
处理,对/ws
的请求使用匿名函数处理,它调用serveWs
来处理WebSocket连接请求。- 接下来的代码设置并启动一个HTTP服务器,监听指定的地址,并设置一个3秒的读取请求头超时。如果服务器出错,将记录错误并退出。
总的来说,这是一个简单的HTTP服务器程序,它为/
路径提供静态页面,并为/ws
路径提供WebSocket服务。
关系图
+------------+
| Web |
+------------+
|
| HTTP Request (for WebSocket connection)
v
+------------+ +-------+
| 服务端(Server) |<----| Hub |
+------------+ +-------+
|
| Creates & Registers
v
+------------+
| Client |
+------------+
|
| Uses
v
+------------+
| WebSocket |
+------------+
- 用户使用Web客户端发起HTTP请求尝试建立WebSocket连接。
- 服务端接收这些请求,并基于这些请求创建
Client
实例。 - 每个
Client
实例都与一个WebSocket连接关联。 Hub
是所有Client
实例的中心,它知道所有活跃的Client
并负责消息的广播。
信息发送和接受
在这个gorilla/websocket
的示例代码中,消息的发送和接收是通过WebSocket协议进行的,这是一个基于TCP的全双工通信协议。下面是如何发送和接收消息的详细描述:
服务端发送信息给客户端:
- 通过
Hub
向Client
广播消息:- 当
Hub
收到broadcast
通道中的消息时,它会尝试将消息发送给所有注册的Client
。 - 这是通过
Client
的send
通道完成的,即通过将消息放入每个Client
的send
通道。
- 当
Client
的writePump
方法监听其send
通道:- 当
send
通道收到消息时,writePump
方法会将它写入WebSocket连接。 - 如果有多个消息在队列中,它们会被连续地写入WebSocket连接。
- 当
客户端发送信息给服务端:
Client
的readPump
方法监听来自WebSocket的入站消息:- 当从WebSocket收到消息时,这个方法首先对消息进行处理(如删除新行和空格)。
- 然后,它将处理后的消息放入
Hub
的broadcast
通道。
Hub
监听其broadcast
通道中的消息:- 当收到
broadcast
通道中的消息时,Hub
会尝试将这个消息发送给所有注册的Client
(包括发送这个消息的原始Client
)。
- 当收到