WebSocket (JSR 356) 提供全雙工長連線通訊,解決 HTTP 輪詢高延遲、高資源消耗問題。Java EE 7+ / Spring Boot 皆內建支援,適合即時聊天、推播通知、協同編輯、股票行情等場景。
<dependency>
<groupId>javax.websocket</groupId>
<artifactId>javax.websocket-api</artifactId>
<version>1.1</version>
<scope>provided</scope> <!-- 容器提供實作 -->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
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();
}
}
@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>
// 伺服器端:定時發送 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,無需處理
// 握手階段攔截 (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");
// ...
}
}
// 定義協議
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;
// ...
}
}
CopyOnWriteArraySet 只包含本機連線spring-boot-starter-websocket + spring-messaging + RedisMessageBroker| 項目 | 建議 |
|---|---|
| 連線數擴展 | NIO 非阻塞 (容器預設)、調大執行緒池、考慮 Netty 自定義 |
| 訊息廣播 | 大量連線時分批發送、異步發送 getAsyncRemote() |
| 記憶體洩漏 | 及時移除關閉的連線、弱引用儲存 Session |
| 安全性 | WSS (TLS)、Origin 檢查、速率限制、輸入驗證、XSS 防護 |
| 監控指標 | 連線數、訊息吞吐量、延遲分佈、錯誤率 |
@ServerEndpoint("/path") 與前端 URL 一致Java WebSocket (JSR 356) 提供標準化的全雙工通訊 API,配合 @ServerEndpoint 註解可快速建立即時通訊應用。核心要點:
@OnOpen/@OnMessage/@OnClose/@OnError)CopyOnWriteArraySet) 管理連線WebSocket API 簡單易用,注意重連與心跳本範例實現基本廣播聊天室,可作為即時通訊系統的起點,逐步擴充私聊、群組、檔案傳輸、語音視訊等功能。