首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java WebSocket 實作即時聊天室完整教學 (ServerEndpoint)

     編輯:Java 相關知識

技術背景

WebSocket (JSR 356) 提供全雙工長連線通訊,解決 HTTP 輪詢高延遲、高資源消耗問題。Java EE 7+ / Spring Boot 皆內建支援,適合即時聊天、推播通知、協同編輯、股票行情等場景。

專案依賴

標準 Java EE (Tomcat/Jetty/JBoss)

<dependency>
    <groupId>javax.websocket</groupId>
    <artifactId>javax.websocket-api</artifactId>
    <version>1.1</version>
    <scope>provided</scope> <!-- 容器提供實作 -->
</dependency>

Spring Boot (內建 Tomcat)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

後端核心實作

WebSocket 端點類

package com.chat.server;

import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

@ServerEndpoint("/websocket/chat")
public class ChatWebSocketEndpoint {
    
    // 線程安全集合:儲存所有連線的端點實例
    private static final CopyOnWriteArraySet<ChatWebSocketEndpoint> connections 
        = new CopyOnWriteArraySet<>();
    
    // 當前連線的 Session (用於發送訊息)
    private Session session;
    
    // 使用者識別 (可從握手階段取得)
    private String username;

    /** 連線建立成功 */
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        this.session = session;
        connections.add(this);
        
        // 可從查詢參數或 Header 取得使用者資訊
        // String query = session.getQueryString(); // 如 ?username=Alice
        // this.username = parseUsername(query);
        
        broadcastSystemMessage("新用戶加入,當前在線: " + connections.size());
        System.out.println("[WebSocket] 新連線: " + session.getId() + ", 總數: " + connections.size());
    }

    /** 連線關閉 */
    @OnClose
    public void onClose(CloseReason reason) {
        connections.remove(this);
        broadcastSystemMessage("用戶離開,當前在線: " + connections.size());
        System.out.println("[WebSocket] 連線關閉: " + session.getId() + ", 原因: " + reason);
    }

    /** 接收客戶端訊息 */
    @OnMessage
    public void onMessage(String message, Session session) {
        System.out.println("[WebSocket] 收到訊息: " + message);
        
        // 廣播給所有連線 (含發送者)
        broadcast(message);
        
        // 進階:私聊、群組、過濾敏感詞、指令解析等
        // if (message.startsWith("@")) { privateChat(...); }
    }

    /** 發生錯誤 */
    @OnError
    public void onError(Session session, Throwable throwable) {
        System.err.println("[WebSocket] 錯誤: " + throwable.getMessage());
        throwable.printStackTrace();
    }

    /** 廣播訊息給所有連線 */
    private void broadcast(String message) {
        for (ChatWebSocketEndpoint endpoint : connections) {
            try {
                endpoint.sendMessage(message);
            } catch (IOException e) {
                System.err.println("發送失敗: " + e.getMessage());
                connections.remove(endpoint); // 移除失效連線
            }
        }
    }

    /** 發送系統訊息 (可區分類型) */
    private void broadcastSystemMessage(String msg) {
        broadcast("[SYSTEM] " + msg);
    }

    /** 發送訊息給當前連線 */
    public void sendMessage(String message) throws IOException {
        // 同步發送 (阻塞直到發送完成)
        this.session.getBasicRemote().sendText(message);
        // 非同步發送 (高併發建議)
        // this.session.getAsyncRemote().sendText(message);
    }
    
    public static int getOnlineCount() {
        return connections.size();
    }
}

Spring Boot 整合配置 (可選)

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
    
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new ChatWebSocketHandler(), "/websocket/chat")
                .setAllowedOrigins("*"); // 生產環境請限制具體域名
    }
}

// Spring 方式需實作 WebSocketHandler (較底層),或使用 @ServerEndpoint + ServerEndpointExporter
@Bean
public ServerEndpointExporter serverEndpointExporter() {
    return new ServerEndpointExporter(); // 自動掃描 @ServerEndpoint
}

前端頁面實作

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket 聊天室</title>
    <style>
        body { font-family: "Microsoft JhengHei", sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
        #messages { height: 400px; border: 1px solid #ddd; overflow-y: auto; padding: 1rem; background: #fafafa; margin-bottom: 1rem; }
        .msg { margin: 0.5rem 0; padding: 0.5rem; border-radius: 4px; }
        .system { background: #e3f2fd; color: #1565c0; text-align: center; font-size: 0.9em; }
        .user { background: #e8f5e9; border-left: 4px solid #4caf50; }
        .me { background: #fff3e0; border-left: 4px solid #ff9800; margin-left: 20%; }
        .input-area { display: flex; gap: 0.5rem; }
        input[type="text"] { flex: 1; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; }
        button { padding: 0.5rem 1.5rem; background: #2196f3; color: white; border: none; border-radius: 4px; cursor: pointer; }
        button:hover { background: #1976d2; }
        .status { padding: 0.5rem; background: #f5f5f5; border-radius: 4px; margin-bottom: 1rem; }
    </style>
</head>
<body>
    <h2>💬 WebSocket 即時聊天室</h2>
    <div class="status" id="status">連線狀態: 正在連線...</div>
    <div id="messages"></div>
    <div class="input-area">
        <input type="text" id="username" placeholder="輸入暱稱" value="User" style="width: 120px;">
        <input type="text" id="messageInput" placeholder="輸入訊息...">
        <button onclick="sendMessage()">發送</button>
        <button onclick="closeConnection()" style="background: #f44336;">離開</button>
    </div>

    <script>
        const wsUrl = "ws://" + location.host + "/websocket/chat"; // 自動適配域名/端口
        let websocket = null;
        const msgBox = document.getElementById('messages');
        const statusEl = document.getElementById('status');
        
        function connect() {
            if ('WebSocket' in window) {
                websocket = new WebSocket(wsUrl);
            } else {
                alert('瀏覽器不支援 WebSocket!');
                return;
            }
            
            websocket.onopen = function(event) {
                statusEl.textContent = '連線狀態: 已連線 🟢';
                statusEl.style.background = '#e8f5e9';
                appendMessage('系統', '歡迎進入聊天室!', 'system');
            };
            
            websocket.onmessage = function(event) {
                const data = event.data;
                if (data.startsWith('[SYSTEM]')) {
                    appendMessage('系統', data.substring(8), 'system');
                } else {
                    // 簡單格式: "暱稱: 訊息內容"
                    const colonIdx = data.indexOf(': ');
                    if (colonIdx > 0) {
                        const sender = data.substring(0, colonIdx);
                        const content = data.substring(colonIdx + 2);
                        const isMe = sender === document.getElementById('username').value;
                        appendMessage(sender, content, isMe ? 'me' : 'user');
                    } else {
                        appendMessage('未知', data, 'user');
                    }
                }
            };
            
            websocket.onclose = function(event) {
                statusEl.textContent = '連線狀態: 已斷線 🔴';
                statusEl.style.background = '#ffebee';
                appendMessage('系統', '連線已關閉', 'system');
            };
            
            websocket.onerror = function(error) {
                console.error('WebSocket 錯誤:', error);
                appendMessage('系統', '連線發生錯誤', 'system');
            };
            
            // 頁面關閉前自動斷開
            window.onbeforeunload = function() {
                if (websocket) websocket.close();
            };
        }
        
        function appendMessage(sender, content, type) {
            const div = document.createElement('div');
            div.className = 'msg ' + type;
            const time = new Date().toLocaleTimeString();
            div.innerHTML = '[' + time + '] ' + sender + ': ' + escapeHtml(content);
            msgBox.appendChild(div);
            msgBox.scrollTop = msgBox.scrollHeight; // 自動滾動到底部
        }
        
        function escapeHtml(text) {
            const div = document.createElement('div');
            div.textContent = text;
            return div.innerHTML;
        }
        
        function sendMessage() {
            const input = document.getElementById('messageInput');
            const text = input.value.trim();
            if (!text) return;
            if (!websocket || websocket.readyState !== WebSocket.OPEN) {
                alert('連線未建立!');
                return;
            }
            const username = document.getElementById('username').value.trim() || '匿名';
            websocket.send(username + ': ' + text);
            input.value = '';
            input.focus();
        }
        
        function closeConnection() {
            if (websocket) websocket.close();
        }
        
        // Enter 鍵發送
        document.getElementById('messageInput').addEventListener('keypress', function(e) {
            if (e.key === 'Enter') sendMessage();
        });
        
        // 初始化連線
        connect();
    </script>
</body>
</html>

進階功能擴充

1. 心跳機制 (防止連線被防火牆/代理斷開)

// 伺服器端:定時發送 Ping
@OnOpen
public void onOpen(Session session) {
    this.session = session;
    connections.add(this);
    // 啟動心跳任務
    scheduler.scheduleAtFixedRate(() -> {
        try {
            session.getBasicRemote().sendPing(ByteBuffer.wrap(new byte[0]));
        } catch (IOException e) {
            // 連線已斷,清理
            connections.remove(this);
        }
    }, 30, 30, TimeUnit.SECONDS);
}

// 客戶端:瀏覽器自動回應 Pong,無需處理

2. 身份認證與授權

// 握手階段攔截 (ServerEndpointConfig.Configurator)
public class AuthConfigurator extends ServerEndpointConfig.Configurator {
    @Override
    public void modifyHandshake(ServerEndpointConfig config, 
                                HandshakeRequest request, 
                                HandshakeResponse response) {
        // 從 Cookie/Header/Query 取得 Token
        String token = getTokenFromRequest(request);
        if (token != null && validateToken(token)) {
            String userId = parseUserId(token);
            config.getUserProperties().put("userId", userId); // 傳遞給 @OnOpen
        } else {
            // 拒絕握手
            response.getHeaders().put("Sec-WebSocket-Protocol", Collections.singletonList("auth_failed"));
        }
    }
}

// 端點使用
@ServerEndpoint(value = "/websocket/chat", configurator = AuthConfigurator.class)
public class SecureChatEndpoint {
    @OnOpen
    public void onOpen(Session session, EndpointConfig config) {
        String userId = (String) config.getUserProperties().get("userId");
        // ...
    }
}

3. 訊息類型化 (JSON 協議)

// 定義協議
public class ChatMessage {
    public enum Type { CHAT, PRIVATE, JOIN, LEAVE, SYSTEM, ERROR }
    private Type type;
    private String sender;
    private String content;
    private String targetUser; // 私聊目標
    private long timestamp;
    // getter/setter/toString
}

// 發送端
objectMapper.writeValueAsString(new ChatMessage(Type.CHAT, "Alice", "Hello"));

// 接收端
@OnMessage
public void onMessage(String json, Session session) {
    ChatMessage msg = objectMapper.readValue(json, ChatMessage.class);
    switch (msg.getType()) {
        case CHAT: broadcast(msg); break;
        case PRIVATE: privateSend(msg); break;
        // ...
    }
}

4. 叢集部署 (多實例同步訊息)

  • 問題:單機 CopyOnWriteArraySet 只包含本機連線
  • 方案:Redis Pub/Sub、Kafka、RabbitMQ 廣播訊息至所有實例
  • Spring 整合:spring-boot-starter-websocket + spring-messaging + RedisMessageBroker

效能調優與最佳實踐

項目建議
連線數擴展NIO 非阻塞 (容器預設)、調大執行緒池、考慮 Netty 自定義
訊息廣播大量連線時分批發送、異步發送 getAsyncRemote()
記憶體洩漏及時移除關閉的連線、弱引用儲存 Session
安全性WSS (TLS)、Origin 檢查、速率限制、輸入驗證、XSS 防護
監控指標連線數、訊息吞吐量、延遲分佈、錯誤率

常見問題排查

  1. 404 Not Found:路徑不匹配,檢查 @ServerEndpoint("/path") 與前端 URL 一致
  2. 連線立即斷開:防火牆/代理超時,啟用心跳;或握手認證失敗
  3. 訊息不顯示:前端 JS 錯誤、JSON 解析失敗、CSS 選擇器錯誤
  4. 多開頁籤連線衝突:每個頁籤獨立連線,正常現象;若需單一連線需前端協調
  5. Tomcat 需額外配置嗎? Tomcat 7.0.47+ 內建支援,無需額外 JAR (scope=provided)

總結

Java WebSocket (JSR 356) 提供標準化的全雙工通訊 API,配合 @ServerEndpoint 註解可快速建立即時通訊應用。核心要點:

  1. 端點類管理連線生命週期 (@OnOpen/@OnMessage/@OnClose/@OnError)
  2. 使用線程安全集合 (CopyOnWriteArraySet) 管理連線
  3. 前端 WebSocket API 簡單易用,注意重連與心跳
  4. 生產環境需考慮:認證授權、叢集同步、安全防護、監控告警
  5. Spring Boot 整合更便利,支援 STOMP 協議、訊息代理、擴展性更強

本範例實現基本廣播聊天室,可作為即時通訊系統的起點,逐步擴充私聊、群組、檔案傳輸、語音視訊等功能。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved