发布于 2026-01-06 4 阅读
0

让我们学习如何使用 Redis、WebSocket 和 Go 构建聊天应用程序[第 1 部分]

让我们学习如何使用 Redis、WebSocket 和 Go 构建聊天应用程序[第 1 部分]

WebSocket协议提供双向(服务器和客户端均可交换消息)和全双工(服务器或客户端可同时发送消息)通信通道,使其适用于聊天应用程序等实时场景。已连接的聊天用户(客户端)可以向应用程序(WebSocket服务器)发送消息,也可以彼此交换消息——类似于点对点设置。

在本博客中,我们将探讨如何使用WebSocket和构建一个简单的聊天应用程序Go。该解决方案还将使用Redis(稍后会详细介绍)。

后续博文(第二部分)将演示如何将此应用程序部署到Azure 应用服务,该应用服务将使用虚拟网络集成与 Azure Redis 缓存通信。

你将学到:

为什么选择 Redis?

我们来看一个聊天应用。当用户首次连接时,WebSocket应用(WebSocket服务器)内部会创建一个相应的连接,并将其与特定的应用实例关联起来。WebSocket正是这个连接使得我们可以在用户之间广播聊天消息。我们可以通过运行多个实例来扩展应用(例如,为了应对庞大的用户群)。现在,如果有新用户加入,他们可能会连接到一个新的实例。因此,我们遇到了这样的情况:不同的用户(以及他们各自的WebSocket连接)与不同的实例关联。结果,他们将无法互相交换消息——即使对于我们这个简单的聊天应用来说,这也是不可接受的 😉

Redis是一款功能强大的键值存储设备,支持多种丰富的数据结构,例如List列表、消息流、Set消息队列等等。它的一项特性是允许发布者向 Redis 通道发送消息,订阅者监听这些通道上的消息——两者完全独立且互不耦合。这可以用来解决我们遇到的问题。现在,我们不再仅仅依赖于连接,而是可以使用一个 Redis 服务,每个聊天应用都可以订阅该服务。这样,发送到连接的消息就可以通过 Redis 通道进行传输,确保所有应用实例(以及关联的聊天用户)都能收到这些消息。Sorted SetHashPubSubWebSocketchannelWebSocket

下一节深入代码时会详细介绍这一点。代码可在 Github 上找到。

请注意,除了普通的推送通知之外,您还可以使用Azure SignalRWebSocket等技术,这些技术允许应用将内容更新推送给已连接的客户端,例如单页 Web 应用或移动应用。这样,客户端无需轮询服务器或提交新的 HTTP 更新请求即可获得更新。

要跟随教程并将此解决方案部署到 Azure,您需要一个Microsoft Azure帐户。如果您还没有帐户,可以免费获取一个!

聊天应用概述

现在来快速浏览一下代码。以下是应用程序的结构:

.
├── Dockerfile
├── chat
│   ├── chat-session.go
│   └── redis.go
├── go.mod
├── go.sum
├── main.go
Enter fullscreen mode Exit fullscreen mode

在 `<script>`标签内main.go,我们注册WebSocket处理程序启动 Web 服务器——这里使用的只是普通的net/http包。

    http.Handle("/chat/", http.HandlerFunc(websocketHandler))
    server := http.Server{Addr: ":" + port, Handler: nil}
    go func() {
        err := server.ListenAndServe()
        if err != nil && err != http.ErrServerClosed {
            log.Fatal("failed to start server", err)
        }
    }()
Enter fullscreen mode Exit fullscreen mode

处理WebSocket程序处理聊天用户(也就是WebSocket客户端),并启动新的聊天会话。

func websocketHandler(rw http.ResponseWriter, req *http.Request) {
    user := strings.TrimPrefix(req.URL.Path, "/chat/")

    peer, err := upgrader.Upgrade(rw, req, nil)
    if err != nil {
        log.Fatal("websocket conn failed", err)
    }
    chatSession := chat.NewChatSession(user, peer)
    chatSession.Start()
}
Enter fullscreen mode Exit fullscreen mode

A ChatSession(的一部分chat/chat-session.go)代表用户及其对应的WebSocket连接(在服务器端)。

type ChatSession struct {
    user string
    peer *websocket.Conn
}
Enter fullscreen mode Exit fullscreen mode

当会话启动时,它会启动一个 goroutinegoroutine来接收来自刚刚加入聊天的用户的消息。它通过循环调用 `(from)` 来实现这一点ReadMessage()如果websocket.Conn用户断开连接(连接关闭)或应用程序关闭(例如使用 ` catch` 命令) for,则此 goroutine会停止运行。总而言之,每个用户都会生成一个单独的 goroutine 来处理其聊天消息。exitsWebSocketctrl+c

func (s *ChatSession) Start() {
...
    go func() {
        for {
            _, msg, err := s.peer.ReadMessage()
            if err != nil {
                _, ok := err.(*websocket.CloseError)
                if ok {
                    s.disconnect()
                }
                return
            }
            SendToChannel(fmt.Sprintf(chat, s.user, string(msg)))
        }
    }()
Enter fullscreen mode Exit fullscreen mode

一旦收到用户发送的消息(通过连接WebSocket),就会使用该函数将其广播给其他用户。SendToChannel该函数的作用chat/redis.go是将消息发布到 Redispubsub通道。

func SendToChannel(msg string) {
    err := client.Publish(channel, msg).Err()
    if err != nil {
        log.Println("could not publish to channel", err)
    }
}
Enter fullscreen mode Exit fullscreen mode

关键在于sub等式中的(订阅者)部分。与为每个已连接的聊天用户创建一个专用 goroutine 的情况不同,我们使用一个single(应用程序范围的)goroutine 来订阅 Redis 通道,接收消息,并通过各自的服务器端连接将其广播给所有用户WebSocket

func startSubscriber() {
    go func() {
        sub = client.Subscribe(channel)
        messages := sub.Channel()
        for message := range messages {
            from := strings.Split(message.Payload, ":")[0]
            for user, peer := range Peers {
                if from != user {
                    peer.WriteMessage(websocket.TextMessage, []byte(message.Payload))
                }
            }
        }
    }()
}
Enter fullscreen mode Exit fullscreen mode

当应用程序实例关闭时,订阅即终止——这反过来又会终止通道for-range循环,goroutine 也随之退出。

该函数由内部函数startSubscriber调用。该函数首先连接到 Redis,如果连接失败,应用程序将退出。init()redis.goinit()

好了!现在是时候搭建一个 Redis 实例,用来连接我们的聊天后端了。让我们在云端创建一个 Redis 服务器吧!

Azure Redis 缓存设置

Azure Redis 缓存提供对安全、专用的 Redis 缓存的访问,该缓存托管在 Azure 中,Azure 内外的任何应用程序均可访问该缓存。

在本博客中,我们将设置一个 Azure Redis 缓存,Basic该缓存采用单节点层,非常适合开发/测试和非关键工作负载。请注意,您还可以选择其他StandardPremium,这些层提供不同的功能,例如持久性、集群、异地复制等

我将使用Azure CLI进行安装。如果您习惯使用浏览器,也可以使用Azure Cloud Shell !

要快速设置 Azure Redis 缓存实例,我们可以使用以下az redis create命令,例如:

az redis create --location westus2 --name chat-redis --resource-group chat-app-group --sku Basic --vm-size c0
Enter fullscreen mode Exit fullscreen mode

请查看“为 Redis 创建 Azure 缓存”以获取分步指南。

完成上述步骤后,您需要获取连接到 Azure Redis 缓存实例所需的信息,例如主机、端口和访问密钥。这也可以使用 CLI 完成,例如:

//host and (SSL) port
az redis show --name chat-redis --resource-group chat-app-group --query [hostName,sslPort] --output tsv

//primary access key
az redis list-keys --name chat-redis --resource-group chat-app-group --query [primaryKey] --output tsv
Enter fullscreen mode Exit fullscreen mode

请查看“获取 Azure Redis 缓存的主机名、端口和密钥”以获取分步指南。

就是这样...

……我们聊聊吧!

为了简化操作,该应用程序以Docker 镜像的形式提供。

首先,设置一些环境变量:

//use port 6380 for SSL
export REDIS_HOST=[redis cache hostname as obtained from CLI]:6380
export REDIS_PASSWORD=[redis cache primary access key as obtained from CLI]
export EXT_PORT=9090
export NAME=chat1
Enter fullscreen mode Exit fullscreen mode

该应用程序内部使用静态端口8080(用于 Web 服务器)。我们使用指定的外部端口,并将其映射到容器内部的EXT_PORT端口(使用)。8080-p $EXT_PORT:8080

启动 Docker 容器

docker run --name $NAME -e REDIS_HOST=$REDIS_HOST -e REDIS_PASSWORD=$REDIS_PASSWORD -p $EXT_PORT:8080 abhirockzz/redis-chat-go
Enter fullscreen mode Exit fullscreen mode

现在可以加入聊天了!你可以使用任何 WebSocket 客户端。我个人比较喜欢wscat在终端中使用,或者在浏览器中使用 Chrome WebSocket 扩展程序。

wscat我将通过终端演示这一步骤。打开两个独立的终端来模拟不同的用户:

//terminal 1 (user "foo")
wscat -c ws://localhost:9090/chat/foo

//terminal 2 (user "bar")
wscat -c ws://localhost:9090/chat/bar
Enter fullscreen mode Exit fullscreen mode

foo以下是两人之间的聊天示例bar

foo第一个加入的人收到了消息,后来加入的人Welcome foo!也收到了消息。请注意,有人收到了加入的通知,并在离开前交换了几条消息也收到了离开的通知)。barfoofoobarfoobarbarfoo

EXT_PORT作为练习,您可以启动另一个聊天应用程序实例。使用不同的`name` 和 `container`值启动另一个 Docker 容器,例如:

//use port 6380 for SSL
export REDIS_HOST=[redis cache host name as obtained from CLI]:6380
export REDIS_PASSWORD=[redis cache primary access key as obtained from CLI]
export EXT_PORT=9091
export NAME=chat2

docker run --name $NAME -e REDIS_HOST=$REDIS_HOST -e REDIS_PASSWORD=$REDIS_PASSWORD -p $EXT_PORT:8080 abhirockzz/redis-chat-go
Enter fullscreen mode Exit fullscreen mode

现在连接到端口9091(或您选择的端口)以模拟另一个用户

//user "pi"
wscat -c ws://localhost:9091/chat/pi
Enter fullscreen mode Exit fullscreen mode

由于foo它仍然处于活动状态,因此会收到通知:pi并且foo可以互致问候。

检查 Redis

让我们通过查看 Redis 数据结构来确认一下。你可以使用 `Redis Cache`redis-cli来实现这一点。如果你使用的是 Azure Redis Cache,我建议你使用一个非常方便的基于 Web 的 Redis 控制台

我们有一个SET(名称chat-users)来存储活跃用户。

SMEMBERS chat-users
Enter fullscreen mode Exit fullscreen mode

您应该看到结果——这意味着用户foo当前bar已连接到聊天应用程序,并且具有关联的活动WebSocket连接。

1) "foo"
2) "bar"
Enter fullscreen mode Exit fullscreen mode

PubSub频道呢?

PUBUSB CHANNELS
Enter fullscreen mode Exit fullscreen mode

由于所有用户都使用同一个通道,您应该从 Redis 服务器获得以下结果:

1) "chat"
Enter fullscreen mode Exit fullscreen mode

本篇博文就到这里。敬请期待第二部分!

如果您觉得这篇文章有用,请别忘了点赞和关注哦🙌 我很想听听您的反馈:欢迎在此留言或在推特上联系我🙏🏻

文章来源:https://dev.to/azure/let-s-learn-how-to-to-build-a-chat-application-with-redis-websocket-and-go-5cck