WebSocket

websocket 的GitHub地址:https://github.com/gorilla/websocket

对于 Chat

https://github.com/gorilla/websocket/blob/main/examples/chat/

是一个简单的在线聊天应用的实现,旨在展示如何使用gorilla/websocket库来构建一个基于WebSocket的应用。WebSocket是一个协议,允许客户端和服务器之间建立持久的连接并进行双向的实时通信。这种特性使其非常适合用于聊天应用、实时数据推送等场景。

使用这个chat示例,你可以学习如何:

  1. 设置和使用WebSocket服务器。
  2. 处理客户端连接和断开。
  3. 接收和发送WebSocket消息。
  4. 如何在聊天应用场景中管理用户、消息等。

Server

服务器应用程序定义了两种类型, ClientHub 。服务器为每个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启动。客户端使用 registerunregisterbroadcast 通道向集线器发送请求。

集线器通过在 clients 映射中添加客户端指针作为键来注册客户端。map值始终为true。

注销代码稍微复杂一点。除了从 clients 映射中删除客户端指针之外,集线器还关闭客户端的 send 通道,以向客户端发出不再向客户端发送消息的信号。

集线器通过循环已注册的客户端并将消息发送到客户端的 send 通道来处理消息。如果客户端的 send 缓冲区已满,则集线器假定客户端已死亡或卡住。在这种情况下,集线器注销客户端并关闭WebSocket。

这个Hub充当WebSocket聊天服务的中心,负责管理所有活跃的客户端和广播消息给这些客户端。以下是对代码的逐段解释:

  1. 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: 一个通道,用于注销客户端。
  1. newHub函数
func newHub() *Hub {
	return &Hub{
		broadcast:  make(chan []byte),
		register:   make(chan *Client),
		unregister: make(chan *Client),
		clients:    make(map[*Client]bool),
	}
}

这个函数初始化并返回一个新的Hub实例。

  1. 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连接。以下是代码的逐段解释:

  1. 常量和变量定义
const (
	...
)

var (
	newline = []byte{'\n'}
	space   = []byte{' '}
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024,
	WriteBufferSize: 1024,
}

这里定义了一些全局常量和变量。其中,upgrader是用于升级HTTP请求到WebSocket的工具。

  1. 建立握手:当客户端想要建立一个 WebSocket 连接时,它首先发送一个标准的 HTTP GET 请求,但其中包含了一些特定的头信息(如:Upgrade: websocketConnection: Upgrade),这些头信息表明客户端希望升级这个连接到 WebSocket。如果服务器支持 WebSocket,并且满足了一定的条件,它就会回应一个状态码为 101 的响应,表示连接已经升级。
  2. WebSocket 不是纯 HTTP:尽管 WebSocket 的握手使用 HTTP,但一旦握手完成,连接就完全脱离了 HTTP 协议,并转为 WebSocket 协议。WebSocket 协议更为轻量,适用于长时间运行的连接,可以双向发送数据,并且没有冗长的头信息。
  3. 复用已有的端口:由于 WebSocket 连接的建立是基于 HTTP 的,因此它可以复用 HTTP 和 HTTPS 使用的标准端口(例如 80 和 443),这有助于避免防火墙和其他网络设备的限制。
  1. Client的定义
type Client struct {
	hub *Hub
	conn *websocket.Conn
	send chan []byte
}
  • hub: 客户端关联的Hub实例。
  • conn: 客户端的WebSocket连接。
  • send: 一个通道,用于发送出站消息到WebSocket。
  1. readPump方法
func (c *Client) readPump() {
	...
}

这个方法处理从WebSocket连接中读取的消息并发送给Hub。当连接关闭或发生错误时,它将注销客户端并关闭连接。

  1. writePump方法
func (c *Client) writePump() {
	...
}

这个方法处理从Hub接收的消息并发送给WebSocket连接。它还定期发送ping消息以确保连接的活跃性。

  1. serveWs函数
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
	...
}

这个函数处理WebSocket请求。它首先使用upgrader升级HTTP请求,然后为每个新的WebSocket连接创建一个Client实例并注册到Hub。接着,它为每个连接启动writePumpreadPump的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请求。让我们逐段分析这段代码:

  1. 导入库和包声明
package main

import (
	"flag"
	"log"
	"net/http"
	"time"
)

上面的代码导入了所需的库:flag用于解析命令行参数,log用于记录日志,net/http用于处理HTTP请求,time用于时间相关操作。

  1. 命令行参数解析
var addr = flag.String("addr", ":8080", "http service address")

这行代码定义了一个命令行参数addr,它指定了HTTP服务的监听地址,默认为:8080

  1. 定义首页处理函数
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文件作为响应。

  1. 主函数
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的全双工通信协议。下面是如何发送和接收消息的详细描述:

服务端发送信息给客户端:

  1. 通过HubClient广播消息
    • Hub收到broadcast通道中的消息时,它会尝试将消息发送给所有注册的Client
    • 这是通过Clientsend通道完成的,即通过将消息放入每个Clientsend通道。
  2. ClientwritePump方法监听其send通道
    • send通道收到消息时,writePump方法会将它写入WebSocket连接。
    • 如果有多个消息在队列中,它们会被连续地写入WebSocket连接。

客户端发送信息给服务端:

  1. ClientreadPump方法监听来自WebSocket的入站消息
    • 当从WebSocket收到消息时,这个方法首先对消息进行处理(如删除新行和空格)。
    • 然后,它将处理后的消息放入Hubbroadcast通道。
  2. Hub监听其broadcast通道中的消息
    • 当收到broadcast通道中的消息时,Hub会尝试将这个消息发送给所有注册的Client(包括发送这个消息的原始Client)。