使用 WebSockets 创建一个简单的聊天应用程序

您将学到什么
在本教程中,我们将创建一个简单的实时聊天应用程序。它将包含一个聊天面板(用于存储您加入后收到的消息)、一个当前连接的用户列表以及一个用于发送消息的输入框。我们将使用 WebSocket 来实现这一点,因为 WebSocket 通过单个 TCP 连接为我们提供了全双工通信通道,这意味着我们无需发出额外的 HTTP 请求即可发送和接收消息。WebSocket 连接始终保持打开状态,从而大大降低了延迟(和复杂性)。

依赖项

首先,我们需要创建一个带有一些依赖项的 Maven 项目:(→ 教程)

<dependencies>
    <dependency>
        <groupId>io.javalin</groupId>
        <artifactId>javalin-bundle</artifactId>
        <version>6.6.0</version>
    </dependency>
    <dependency>
        <groupId>com.j2html</groupId>
        <artifactId>j2html</artifactId>
        <version>1.6.0</version>
    </dependency>
</dependencies>

Javalin 应用程序

Javalin 应用程序非常简单。我们需要:

  • 用于跟踪会话/用户名对的地图。
  • 用户数量计数器(昵称自动增加)
  • 用于连接/消息/关闭的 websocket 处理程序
  • 向所有用户广播消息的方法
  • 一种以 HTML(或 JSON,如果您愿意)创建消息的方法
import io.javalin.Javalin;
import io.javalin.http.staticfiles.Location;
import io.javalin.websocket.WsContext;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static j2html.TagCreator.article;
import static j2html.TagCreator.attrs;
import static j2html.TagCreator.b;
import static j2html.TagCreator.p;
import static j2html.TagCreator.span;

public class JavalinWebsocketExampleApp {

    private static final Map<WsContext, String> userUsernameMap = new ConcurrentHashMap<>();
    private static int nextUserNumber = 1; // Assign to username for next connecting user

    public static void main(String[] args) {
        Javalin app = Javalin.create(config -> {
            config.staticFiles.add("/public", Location.CLASSPATH);
            config.router.mount(router -> {
                router.ws("/chat", ws -> {
                    ws.onConnect(ctx -> {
                        String username = "User" + nextUserNumber++;
                        userUsernameMap.put(ctx, username);
                        broadcastMessage("Server", (username + " joined the chat"));
                    });
                    ws.onClose(ctx -> {
                        String username = userUsernameMap.get(ctx);
                        userUsernameMap.remove(ctx);
                        broadcastMessage("Server", (username + " left the chat"));
                    });
                    ws.onMessage(ctx -> {
                        broadcastMessage(userUsernameMap.get(ctx), ctx.message());
                    });
                });
            });
        }).start(7070);
    }

    // Sends a message from one user to all users, along with a list of current usernames
    private static void broadcastMessage(String sender, String message) {
        userUsernameMap.keySet().stream().filter(ctx -> ctx.session.isOpen()).forEach(session -> {
            session.send(
                Map.of(
                    "userMessage", createHtmlMessageFromSender(sender, message),
                    "userlist", userUsernameMap.values()
                )
            );
        });
    }

    // Builds a HTML element with a sender-name, a message, and a timestamp
    private static String createHtmlMessageFromSender(String sender, String message) {
        return article(
            b(sender + " says:"),
            span(attrs(".timestamp"), new SimpleDateFormat("HH:mm:ss").format(new Date())),
            p(message)
        ).render();
    }

}

构建 JavaScript 客户端

为了演示我们的应用程序可以正常工作,我们可以构建一个 JavaScript 客户端。首先,我们创建 index.html:

<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>WebsSockets</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="chatControls">
        <input id="message" placeholder="Type your message">
        <button id="send">Send</button>
    </div>
    <ul id="userlist"> <!-- Built by JS --> </ul>
    <div id="chat">    <!-- Built by JS --> </div>
    <script src="websocketDemo.js"></script>
</body>
</html>

完成我们的聊天应用程序所需的最后一步是创建websocketDemo.js:

// small helper function for selecting element by id
let id = id => document.getElementById(id);

//Establish the WebSocket connection and set up event handlers
let ws = new WebSocket("ws://" + location.hostname + ":" + location.port + "/chat");
ws.onmessage = msg => updateChat(msg);
ws.onclose = () => alert("WebSocket connection closed");

// Add event listeners to button and input field
id("send").addEventListener("click", () => sendAndClear(id("message").value));
id("message").addEventListener("keypress", function (e) {
    if (e.keyCode === 13) { // Send message if enter is pressed in input field
        sendAndClear(e.target.value);
    }
});

function sendAndClear(message) {
    if (message !== "") {
        ws.send(message);
        id("message").value = "";
    }
}

function updateChat(msg) { // Update chat-panel and list of connected users
    let data = JSON.parse(msg.data);
    id("chat").insertAdjacentHTML("afterbegin", data.userMessage);
    id("userlist").innerHTML = data.userlist.map(user => "<li>" + user + "</li>").join("");
}

就这样!现在尝试打开localhost:7070 几个不同的浏览器窗口(可以同时看到),然后自言自语。

结论

嗯,很简单!我们实现了一个无需轮询的实时聊天应用,服务器和客户端代码不到 100 行。虽然实现起来非常简单,我们至少应该将用户列表和消息的发送分开(这样就不用每次有人发送消息时都重建用户列表),但由于本教程的重点是 WebSocket,所以我选择在自己觉得合适的范围内尽可能精简地实现。

作者:Jeebiz  创建时间:2025-05-04 00:22
最后编辑:Jeebiz  更新时间:2025-05-04 00:55