65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 Go 和 React 构建实时聊天应用程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2024 年 10 月 1 日

CPOL

6分钟阅读

viewsIcon

8142

downloadIcon

154

一个使用 Go WebSocket 后端和 React 前端构建的实时聊天应用程序,支持用户之间的即时消息传递。

引言

在本文中,我们将使用 Go 作为后端(WebSocket 服务器)和 React 作为前端,逐步介绍一个实时聊天室应用程序的开发过程。该项目展示了现代 Web 应用程序如何使用 WebSocket 实现即时、实时的通信。

项目概述

该聊天应用程序允许用户实时连接并发送消息。当用户发送消息时,消息会被广播给所有已连接的客户端,无需刷新页面。我们使用 Go 来处理服务器端的 WebSocket 连接,并使用 React 来处理客户端的用户界面渲染。

项目结构

该项目包含两部分

  1. Go 后端:一个处理实时通信的 WebSocket 服务器。
  2. React 前端:用于发送和接收消息的基于 Web 的用户界面。

以下是项目结构的概述

├── real-time-chat/    # Go WebSocket server (backend)
├── chatroom/          # React chat application (frontend)
└── README.md          # Project documentation

Go WebSocket 服务器(后端)

为什么选择 Go?

Go(或 Golang)因其并发模型和对 I/O 操作的高效处理,是构建高性能网络应用程序的优秀语言。对于此项目,Go 的 net/http 包和 gorilla/websocket 包提供了一种高效处理 WebSocket 连接的方式,确保消息的实时交换。

必备组件

1. 安装 VS Code

如果尚未安装,请从官方网站下载并安装 VS Code

2. 安装 Go

确保您的系统已安装 Go。从Go 网站下载。

安装完成后,通过运行以下命令确认 Go 已正确设置

go version

确保 $GOPATH$GOROOT 在环境变量中已正确配置。

3. 安装 VS Code 的 Go 扩展

  • 打开 VS Code。
  • 转到扩展面板(位于左侧边栏或按 Ctrl+Shift+X)。
  • 搜索 Go(由 Google 的 Go 团队开发)并安装它。

此扩展将提供代码检查、自动格式化、智能感知和其他开发工具。

4. 设置 Go 工具

安装 Go 扩展后,VS Code 会提示您安装一些额外的 Go 工具(如 goplsgofmt、用于调试的 delve 等)。这些工具可增强您的开发体验。

当提示安装工具时,点击全部安装,或者可以通过运行以下命令手动安装

go install golang.org/x/tools/gopls@latest
go install golang.org/x/lint/golint@latest
go install github.com/go-delve/delve/cmd/dlv@latest
go install golang.org/x/tools/cmd/goimports@latest

5. Go Modules 支持

如果使用 Go modules(用于依赖管理),请确保您位于 Go module 项目中。使用以下命令初始化新模块

go mod init project-name

设置后端

步骤 1:创建 Go WebSocket 服务器

我们首先创建 Go 后端,它会监听 WebSocket 连接,管理活动客户端,并在它们之间广播消息。

以下是 Go 后端关键组件的简化分解

  1. WebSocket 连接:服务器与客户端建立 WebSocket 连接。
  2. 客户端管理:服务器使用一个 map 来跟踪已连接的客户端。
  3. 广播消息:当一个客户端发送消息时,服务器会实时将其广播给所有其他客户端。

步骤 2:实现 Go WebSocket 服务器

  • 创建项目目录

首先,为聊天应用程序创建一个文件夹。

mkdir real-time-chat
cd real-time-chat
  • 初始化 Go 模块

为了使用 Go modules 进行依赖管理,请使用 go mod 初始化项目

go mod init real-time-chat
  • 组织项目结构
real-time-chat/
│
├── go.mod               # For dependency management
├── main.go              # Entry point of your app
├── handlers/
│   └── websocket.go     # WebSocket-related logic
└── public/
    └── index.html       # Frontend (optional: for testing)
  • 创建 main.go 作为应用程序入口点

main.go 文件将作为应用程序的入口点。该文件将设置服务器并处理传入的连接。

以下是 main.go 文件

package main

import (
    "log"
    "net/http"
    "real-time-chat/handlers"
)

func main() {
    fs := http.FileServer(http.Dir("./public"))
    http.Handle("/", fs)
    
    http.HandleFunc("/ws", handlers.HandleConnections)
    
    log.Println("Server started on :8080")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

将包名声明为 main,这意味着它是应用程序的入口点。每个运行的 Go 应用程序都以 main 包开始。

我们导入了

  1. log:用于记录服务器事件(例如,启动服务器、错误)。
  2. net/http:提供 HTTP 功能来创建 Web 服务器并处理请求。
  3. real-time-chat/handlers:一个自定义包,其中定义了 WebSocket 连接逻辑。这是我们之前讨论过的文件(handlers/websocket.go)。
  4. http.FileServer:用于提供静态文件(如 HTML、CSS 和 JS)。在这里,它提供了 public/ 目录中的文件。
  5. http.Handle("/", fs):将所有对根 URL(/)的请求路由到文件服务器,因此当用户访问 https://:8080 时,他们将看到 public/ 文件夹中的静态 index.html 文件。

 

  • 创建 handlers/websocket.go 文件用于 WebSocket 逻辑

将 WebSocket 逻辑分离到一个名为 handlers/ 的文件夹下的新 websocket.go 文件中。这将使代码更加模块化和有条理。

websocket.go 文件

package handlers

import (
    "log"
    "net/http"

    "github.com/gorilla/websocket"
)

var upgrader = websocket.Upgrader{
    CheckOrigin: func(r *http.Request) bool { return true },
}

var clients = make(map[*websocket.Conn]bool) // Connected clients
var broadcast = make(chan Message)           // Channel for broadcasting messages

// Message defines the structure of the messages exchanged
type Message struct {
    Username  string `json:"username"`
    Message   string `json:"message"`
    Timestamp string `json:"timestamp"`
    Typing    bool   `json:"typing"` // Indicates if the user is typing
}

// HandleConnections handles new WebSocket requests from clients
func HandleConnections(w http.ResponseWriter, r *http.Request) {
    ws, err := upgrader.Upgrade(w, r, nil) // Upgrade HTTP to WebSocket
    if err != nil {
        log.Fatal(err)
    }
    defer ws.Close() // Close the WebSocket connection when the function returns

    clients[ws] = true // Register the new client

    for {
        var msg Message
        // Read a new message as JSON and map it to a Message object
        err := ws.ReadJSON(&msg)
        if err != nil {
            log.Printf("error: %v", err)
            delete(clients, ws) // Remove the client from the list if there is an error
            break
        }
        // Send the message to the broadcast channel
        broadcast <- msg
    }
}

// HandleMessages broadcasts incoming messages to all clients
func HandleMessages() {
    log.Println("HandleMessages running")
    for {
        // Get the next message from the broadcast channel
        msg := <-broadcast
        // Send it to every connected client
        for client := range clients {
            err := client.WriteJSON(msg) // Write message to the client
            if err != nil {
                log.Printf("error: %v", err)
                client.Close()          // Close the connection if there's an error
                delete(clients, client) // Remove the client
            }
        }
    }
}
  1. http.HandleFunc:在 /ws 处注册一个新的 WebSocket 连接路由。当客户端连接到 ws://:8080/ws 时,此路由会处理它。
  2. handlers.HandleConnections:此函数(在 websocket.go 中定义)处理每个客户端的 WebSocket 连接。它将 HTTP 连接升级为 WebSocket 连接。
  3. log.Println("Server started on :8080"):记录一条消息,表明服务器已启动并正在运行。
  4. http.ListenAndServe(":8080", nil):在端口 8080 上启动 HTTP 服务器。第一个参数(:8080)指定地址(在此情况下为端口 8080),第二个参数(nil)表示它将使用默认的 ServeMux 来处理路由。

 

  • 添加一个用于测试的前端

要测试 WebSocket 功能,只需在 public/ 文件夹中添加一个 index.html 文件,其中包含基本的 HTML/JS 来连接到 WebSocket。

以下是 index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Real-Time Chat</title>
</head>
<body>
    <h2>WebSocket Chat</h2>
    <div id="messages"></div>
    <input id="username" type="text" placeholder="Username" />
    <input id="message" type="text" placeholder="Message" />
    <button onclick="sendMessage()">Send</button>

    <script>
        const socket = new WebSocket('ws://:8080/ws');

        socket.onmessage = function(event) {
            const messages = document.getElementById('messages');
            const message = document.createElement('div');
            message.textContent = event.data;
            messages.appendChild(message);
        };

        function sendMessage() {
            const username = document.getElementById('username').value;
            const message = document.getElementById('message').value;
            socket.send(JSON.stringify({username: username, message: message}));
        }
    </script>
</body>
</html>

运行 Go 后端

要运行 Go 服务器

cd real-time-chat/

go run main.go

服务器现在将监听 ws://:8080/ws 上的 WebSocket 连接。

然后访问 https://:8080

 

 

React 前端

为什么选择 React?

React 是一个流行的用于构建用户界面的 JavaScript 库。其组件化的架构允许高效的 UI 更新,非常适合需要即时显示消息的实时应用程序。

设置前端

步骤 1:创建一个新的 React 应用

我们使用 create-react-app(带有 TypeScript)创建了前端。前端连接到 WebSocket 服务器并监听消息以实时显示。

npx create-react-app chatroom --template typescript

cd chatroom

步骤 2:为聊天添加表情符号选择器

npm install emoji-mart

步骤 3:创建 ChatRoom 组件

以下是 React 组件的分解

WebSocket 连接:客户端连接到 Go WebSocket 服务器。

状态管理:应用程序使用 React 的 useState 和 useEffect hooks 来管理消息和 WebSocket 连接。

实时更新:当服务器广播消息时,前端会立即显示。

src/ 文件夹内,创建一个名为 ChatRoom.tsx 的新文件。

以下是 ChatRoom.tsx

import React, { useState, useEffect, useRef, ChangeEvent, KeyboardEvent } from "react";
import Picker from '@emoji-mart/react';
import data from '@emoji-mart/data';

interface Message {
    username: string;
    message: string;
    timestamp: string;
    typing: boolean;
}

const ChatRoom: React.FC = () => {
    const [username, setUsername] = useState<string>("");
    const [message, setMessage] = useState<string>("");
    const [chat, setChat] = useState<Message[]>([]);
    const [typingUser, setTypingUser] = useState<string | null>(null);
    const [ws, setWs] = useState<WebSocket | null>(null);
    const [showPicker, setShowPicker] = useState<boolean>(false);
    const messageRef = useRef<HTMLInputElement>(null);

    // WebSocket connection
    useEffect(() => {
        const socket = new WebSocket("ws://:8080/ws");

        socket.onmessage = (event) => {
            const messageData: Message = JSON.parse(event.data);

            if (messageData.typing && messageData.username !== username) {
                setTypingUser(messageData.username); // Show who is typing
            } else if (!messageData.typing) {
                setChat((prevChat) => [...prevChat, messageData]);
                setTypingUser(null); // Stop showing the typing indicator
            }
        };

        setWs(socket);

        // Cleanup WebSocket connection
        return () => {
            socket.close();
        };
    }, [username]);

    // Handle sending the message
    const sendMessage = () => {
        if (ws && message && username) {
            const timestamp = new Date().toLocaleTimeString();
            const msg: Message = { username, message, timestamp, typing: false };
            ws.send(JSON.stringify(msg));
            setMessage("");
        }
    };

    // Detect when Enter key is pressed
    const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
        if (e.key === "Enter") {
            sendMessage();
        }
    };

    // Handle message input change
    const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
        setMessage(e.target.value);
        if (ws && username) {
            const typingMessage: Message = { username, message: "", timestamp: "", typing: true };
            ws.send(JSON.stringify(typingMessage));
        }
    };

    // Add emoji to the message
    const addEmoji = (emoji: any) => {
        setMessage((prevMessage) => prevMessage + emoji.native);
        setShowPicker(false);
    };

    return (
        <div className="chatroom-container">
            <div className="chatbox">
                <h2>Chat Room</h2>

                <div className="chat-inputs">
                    <input
                        type="text"
                        placeholder="Enter your username"
                        value={username}
                        onChange={(e) => setUsername(e.target.value)}
                    />
                </div>

                <div className="chat-window">
                    {typingUser && <div className="typing-indicator">{typingUser} is typing...</div>}
                    {chat.map((msg, index) => (
                        <div
                            key={index}
                            className={`chat-message ${msg.username === username ? "own-message" : ""}`}
                        >
                            <div className="chat-message-info">
                                <img
                                    src={`https://avatars.dicebear.com/api/initials/${msg.username}.svg`}
                                    alt="avatar"
                                    className="chat-avatar"
                                />
                                <strong className="username">{msg.username}</strong>
                                <span className="timestamp"> at {msg.timestamp}</span>
                            </div>
                            <div>{msg.message}</div>
                        </div>
                    ))}
                </div>

                <div className="chat-inputs">
                    <input
                        ref={messageRef}
                        type="text"
                        placeholder="Enter your message"
                        value={message}
                        onChange={handleChange}
                        onKeyDown={handleKeyDown}
                    />
                    <button onClick={sendMessage}>Send</button>
                    <button onClick={() => setShowPicker(!showPicker)}>😊</button>
                    {showPicker && <Picker data={data} onEmojiSelect={addEmoji} />}
                </div>
            </div>
        </div>
    );
};

export default ChatRoom;

  • WebSocket 连接:

    • 当组件挂载时(useEffect),将建立到 Go 后端的 WebSocket 连接,地址为 ws://:8080/ws
    • WebSocket 监听传入消息(socket.onmessage)并更新聊天历史记录。
    • 当组件卸载时,WebSocket 连接会关闭以清理资源。
  • 发送消息:

    • sendMessage 函数将用户消息和用户名作为 JSON 对象通过 WebSocket 发送。
    • 捕获 Enter 键按下事件,以便用户在按下 Enter 键时发送消息。
  • 显示聊天:

    • chat 状态存储整个聊天历史记录。
    • 每条新消息都会被添加到 chat 并渲染到聊天窗口中。

步骤 4:将 ChatRoom 组件添加到 App.tsx

import ChatRoom from "./ChatRoom";
import './App.css';

function App() {
  return (
    <div className="App">
      <ChatRoom />
    </div>
  );
}

export default App;

步骤 4:添加 CSS 样式

App.css 中为聊天室添加样式

/* Center the chatroom on the page */
.chatroom-container {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
  background-color: #f9f9f9;
}

.chatbox {
  width: 500px;
  background-color: #fff;
  padding: 20px;
  border-radius: 10px;
  box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1);
}

h2 {
  text-align: center;
  font-size: 1.5em;
  margin-bottom: 20px;
}

/* Adjust the input layout */
.chat-inputs {
  margin-bottom: 10px;
  display: flex;
  justify-content: space-between;
}

.chat-inputs input[type="text"] {
  flex-grow: 1;
  padding: 10px;
  border-radius: 5px;
  border: 1px solid #ccc;
  margin-right: 10px;
}

.chat-inputs button {
  padding: 10px 15px;
  border: none;
  background-color: #007bff;
  color: #fff;
  border-radius: 5px;
  cursor: pointer;
}

.chat-inputs button:hover {
  background-color: #0056b3;
}

/* Emoji button */
.chat-inputs button:nth-child(3) {
  background-color: #ffcc00;
}

.chat-inputs button:nth-child(3):hover {
  background-color: #e6b800;
}

/* Chat window styling */
.chat-window {
  height: 300px;
  border: 1px solid #ccc;
  border-radius: 5px;
  padding: 10px;
  overflow-y: auto;
  margin-bottom: 10px;
}

.chat-message {
  display: flex;
  flex-direction: column;
  padding: 10px;
  border-radius: 8px;
  margin-bottom: 10px;
}

.own-message {
  background-color: #d1ffd1;
  align-self: flex-end;
}

.chat-message-info {
  display: flex;
  align-items: center;
}

.username {
  margin-right: 5px; /* Add some space between username and timestamp */
}

.timestamp {
  margin-left: 5px; /* Ensure a small space between 'at' and the timestamp */
}

.chat-avatar {
  width: 40px;
  height: 40px;
  border-radius: 50%;
  margin-right: 10px;
}

.typing-indicator {
  font-style: italic;
  color: gray;
}

/* Adjust button styles */
button {
  cursor: pointer;
}

运行 React 前端

要启动 React 前端

cd chatroom/
npm start

前端将可通过 https://:3000 访问。

使用 WebSocket 进行实时消息传递

当消息从前端发送时,它会通过 WebSocket 传输到 Go 服务器,然后服务器将消息广播给所有已连接的客户端。WebSocket 支持全双工通信,确保消息即时接收,从而提供无缝的实时聊天体验。

示例工作流程

  1. 用户 A 在聊天框中输入消息并按“发送”。
  2. 消息通过 WebSocket 连接发送到 Go 服务器。
  3. 服务器将消息广播给所有已连接的客户端(包括用户 A)。
  4. 所有客户端实时更新其聊天窗口以显示新消息。

结论

在本文中,我们介绍了如何使用 Go 作为后端和 React 作为前端来构建一个简单的实时聊天应用程序。该应用程序利用 WebSocket 实现客户端之间的实时通信。Go 和 React 都是功能强大的技术,可用于构建可扩展、高性能的应用程序。

 

© . All rights reserved.