首页
Preview

使用 Go 和 WebSockets 构建 Web 聊天应用

WebSocket

当你需要从服务器端立即接收客户端更新时,WebSocket是一种很好的通信协议。如果没有它,你就必须执行HTTP轮询,这有很多缺点。其中一个主要缺点是轮询,特别是当你在服务器上创建了巨大的负载时。使用HTTP时,你没有一种从服务器端发起请求的机制。

你可以使用WebSockets创建聊天、新闻源、游戏等。本文将描述如何使用WebSockets、Go和React创建一个简单的Web信使。

后端技术栈

我使用Go 1.19和Gin构建Web服务器。为了启用WebSocket API,我选择了Gorilla WebSocket

前端技术栈

我决定不在前端上花费太多时间,所以它看起来很丑。我使用TypeScript与ReactMaterial UI一起使用。因此,如果你熟悉这些技术,你可以完成前端部分,使其看起来更美观。另外,我使用了react-use-websocket包,以实现与React兼容的WebSocket集成。

服务器端代码

我想描述一下最有趣的部分。我想从服务器端的WebSocketClient实现开始。客户端负责简化WebSocket的工作。接口声明了以下方法:

  • Id() — 返回WebSocket连接的唯一标识符的方法
  • Launch(ctx context.Context) — 启动客户端,以便开始侦听新消息
  • Write(m model.WebSocketMessage) — 将消息 m 发送回客户端的方法
  • Close() — 关闭WebSocket连接的方法
  • Listen() — 返回具有传入消息的通道的方法
  • Done() — 返回一个通道,当工作完成时关闭(WebSocket连接关闭或应该关闭)
  • Error() — 返回具有在WebSocket侦听期间发生的错误的通道的方法

最有趣的方法是Launch。这是它的样子:

func (c *WebSocketClient) Launch(ctx context.Context) {
    ctx, cancel := context.WithCancel(ctx)
    c.cancel = cancel
    go func() {
        go c.read(ctx)
        go c.ping(ctx)
    }()
}

它启动了一个带有两个内部goroutine的goroutine:

  • read goroutine负责侦听传入消息。它将它们发布到通道中(通道由Listen方法返回)。当上下文完成或读操作返回错误时,goroutine完成:
for {
    _, message, err := c.conn.ReadMessage()
    if err != nil {
        c.errChan <- err
        return
    }
    c.inChan <- message
}
  • ping goroutine负责发送定期ping。当上下文完成时,goroutine完成:
for {
    select {
    case <-ctx.Done():
        return
    case <-time.After(pingPeriod):
        c.write(websocket.PingMessage, []byte{})
    }
}

这两个内部goroutine都由传递给Launch方法的上下文控制。该上下文被包装在另一个取消上下文中。如果read goroutine出现错误完成,它会取消取消ping goroutine的上下文(反之亦然)。在这种情况下,WebSocket连接将关闭并返回到上下文ctx,在那里你可以通过取消它来关闭WebSocket连接。

方法Writewrite用于发送消息。公共方法用于发送客户端消息。私有方法发送服务消息(ping、关闭连接)。它们都使用互斥锁锁定,以避免并发消息发送。

让我们切换到负责WebSocket连接的Gin处理程序——方法WebSocketConnect

func WebSocketConnect(pool *WebSocketPool, w http.ResponseWriter, r *http.Request) {
    conn, err := upgrader.Upgrade(w, r, nil)
    if err != nil {
        log.Println(err)
        return
    }
    client := NewWebSocketClient(conn, pool)
    pool.Add(client)
    defer client.Close()
    pool.MemberJoin(client)
    client.Launch(r.Context())
    for {
        select {
        case message := <-client.Listen():
            pool.HandleMessage(client, message)
        case err := <-client.Error():
            log.Println(err)
            return
        case <-client.Done():
            pool.MemberLeave(client)
            return
        }
    }
}

它切换到WebSocket协议,并启动了带有WebSocketClient的goroutine:

client := NewWebSocketClient(conn, pool)
pool.Add(client)
defer client.Close()
pool.MemberJoin(client)
client.Launch(r.Context())

该方法创建了一个新的WebSocketClient,并将该客户端添加到客户端池中。然后,它使用MemberJoin方法广播MEMBER_JOIN消息。在select语句中,该方法侦听传入消息、客户端错误和完成信号。

传入消息处理取决于消息类型。这段代码处理的唯一消息类型是MESSAGE。客户端发送新消息时,此消息被初始化。以下是Message处理程序代码:

case model.MESSAGE:
    message := model.Message{}
    if err := json.Unmarshal(data, &message); err != nil {
        log.Println(err)
        return
    }
    pool.Broadcast(client, message)

该消息被广播到除发送者以外的每个WebSocket客户端。

当接收到完成信号时,广播MEMBER_LEAVE消息,并从池中删除并关闭客户端(startClient方法的defer部分)。以下是代码:

pool.MemberLeave(client)

客户端代码

UI中最有趣的部分是ChatContext组件,它看起来像这样:

function ChatContextProvider({ children }: Props) {
    const { sendMessage, lastMessage } = useWebSocket(SERVER_WS_URL, {
        shouldReconnect: () => true,
        reconnectAttempts: 10,
    });

    const [chat, setChat] = useState<ChatState>({
        messages: [],
        members: [],
    });

    useEffect(() => {
        if (!lastMessage) return;
        const message = JSON.parse(lastMessage.data) as WebSocketMessage;
        switch (message.type) {
            case MESSAGE:
                setChat((chat) => {
                    return { ...chat, messages: [...chat.messages, message] };
                });
                break;
            case MEMBER_JOIN:
                setChat((chat) => {
                    return {
                        ...chat,
                        members: [...chat.members, message.data],
                    };
                });
                break;
            case MEMBER_LEAVE:
                setChat((chat) => {
                    return {
                        ...chat,
                        members: chat.members.filter(
                            (m) => m.id !== message.data.id
                        ),
                    };
                });
                break;
        }
    }, [lastMessage]);

    return (
        <ChatContext.Provider value={{ chat, sendMessage }}>
            {children}
        </ChatContext.Provider>
    );
}

这是一个React组件,它为其他组件提供上下文。使用react-use-websocket包中的useWebSocket方法打开WebSocket连接。使用两个属性时,它返回以下内容:

  • sendMessage — 用于通过WebSocket发送消息
  • lastMessage — 返回最新接收到的消息

后者在以下效果中使用。根据消息类型,将新消息添加到聊天中。它看起来像这样:

聊天

其他组件使用上下文来呈现消息(组件Messages)或发送消息(组件TextInput)。

应用程序代码

你可以在下一个存储库中找到应用程序代码:

GitHub — misikdmitriy/go-ws-api

结论

正如你所看到的,使用此Web聊天来改进客户体验需要很多工作。该应用程序仅用于演示WebSocket协议的功能以及如何在服务器端使用Go和在客户端端使用React实现它。它是一种提高整体UX并减少服务器负载的强大工具。

资源

Gin Web Framework

GitHub — gorilla/websocket: A fast, well-tested and widely used WebSocket implementation for Go.

译自:https://betterprogramming.pub/building-web-chat-with-go-and-websockets-312f459c001a

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
菜鸟一只
你就是个黄焖鸡,又黄又闷又垃圾。

评论(0)

添加评论