WebSocket 協定在 Java Web 開發中扮演著即時通訊的關鍵角色。本文將詳細介紹如何使用 Java WebSocket API 實作一個簡單的點對點聊天功能,包含伺服器端與前端的完整程式碼,適合初學者快速上手。
本範例採用「Servlet 初始化儲存結構 + WebSocket Servlet 建立連線 + MessageInbound 處理訊息」的三層架構:
負責初始化一個靜態 HashMap,以使用者 ID 為鍵,MessageInbound 實例為值,作為線上使用者的查找表。
package socket;
import java.util.HashMap;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import org.apache.catalina.websocket.MessageInbound;
public class InitServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private static HashMap<String, MessageInbound> socketList;
@Override
public void init(ServletConfig config) throws ServletException {
socketList = new HashMap<>();
super.init(config);
System.out.println("伺服器啟動,WebSocket 連線池已初始化");
}
public static HashMap<String, MessageInbound> getSocketList() {
return socketList;
}
}
繼承 WebSocketServlet,在建立連線時從 Session 取得使用者 ID,並建立對應的 MyMessageInbound 實例。
package socket;
import javax.servlet.http.HttpServletRequest;
import org.apache.catalina.websocket.StreamInbound;
import org.apache.catalina.websocket.WebSocketServlet;
public class MyWebSocketServlet extends WebSocketServlet {
private String getUser(HttpServletRequest request) {
return (String) request.getSession().getAttribute("user");
}
@Override
protected StreamInbound createWebSocketInbound(String subProtocol, HttpServletRequest request) {
return new MyMessageInbound(getUser(request));
}
}
處理連線開啟、文字訊息接收與發送、連線關閉等生命週期事件。關鍵邏輯在 onTextMessage 中解析訊息格式「發送者,接收者,內容」,並透過 InitServlet 的 socketList 精準推送給目標使用者。
package socket;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.util.HashMap;
import org.apache.catalina.websocket.MessageInbound;
import org.apache.catalina.websocket.WsOutbound;
import util.MessageUtil;
public class MyMessageInbound extends MessageInbound {
private String userId;
public MyMessageInbound(String userId) {
this.userId = userId;
}
@Override
protected void onOpen(WsOutbound outbound) {
super.onOpen(outbound);
if (userId != null) {
InitServlet.getSocketList().put(userId, this);
System.out.println("使用者上線:" + userId);
}
}
@Override
protected void onTextMessage(CharBuffer msg) throws IOException {
HashMap<String, String> messageMap = MessageUtil.getMessage(msg);
String fromName = messageMap.get("fromName");
String toName = messageMap.get("toName");
String content = messageMap.get("content");
MessageInbound target = InitServlet.getSocketList().get(toName);
if (target != null) {
WsOutbound outbound = target.getWsOutbound();
String formattedMsg = fromName + ":" + content;
outbound.writeTextMessage(CharBuffer.wrap(formattedMsg.toCharArray()));
}
}
@Override
protected void onClose(int status) {
if (userId != null) {
InitServlet.getSocketList().remove(userId);
System.out.println("使用者下線:" + userId);
}
super.onClose(status);
}
@Override
protected void onBinaryMessage(ByteBuffer arg) throws IOException {
// 二進位訊息處理(本範例不使用)
}
}
將前端傳來的字串依逗號分割為結構化 Map。
package util;
import java.nio.CharBuffer;
import java.util.HashMap;
public class MessageUtil {
public static HashMap<String, String> getMessage(CharBuffer msg) {
HashMap<String, String> map = new HashMap<>();
String[] parts = msg.toString().split(",", 3);
map.put("fromName", parts[0]);
map.put("toName", parts.length > 1 ? parts[1] : "");
map.put("content", parts.length > 2 ? parts[2] : "");
return map;
}
}
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" xmlns="https://java.sun.com/xml/ns/javaee"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://java.sun.com/xml/ns/javaee https://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<servlet>
<servlet-name>mywebsocket</servlet-name>
<servlet-class>socket.MyWebSocketServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>mywebsocket</servlet-name>
<url-pattern>/mywebsocket.do</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>initServlet</servlet-name>
<servlet-class>socket.InitServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
提供兩個 JSP 頁面模擬不同使用者登入(小華、小明),透過 JavaScript WebSocket API 進行雙向通訊。
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<% session.setAttribute("user", "小華"); %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket 聊天室 - 小華</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
var ws;
function connect() {
var url = "ws://" + location.host + "/webSocket/mywebsocket.do";
ws = new WebSocket(url);
ws.onopen = function() {
$('#status').text("線上").css("color", "green");
$('#userName').text("小華");
};
ws.onmessage = function(evt) {
$('#msgArea').val(function(i, v) { return v + evt.data + "\n"; });
};
ws.onclose = function() {
$('#status').text("離線").css("color", "red");
};
ws.onerror = function(err) {
console.error("WebSocket 錯誤:", err);
};
}
function sendMsg() {
var to = $('#targetUser').val();
var content = $('#inputMsg').val();
if (content.trim()) {
ws.send("小華," + to + "," + content);
$('#inputMsg').val('');
}
}
$(function() { connect(); });
</script>
</head>
<body onload="connect()">
<h3>WebSocket 點對點聊天測試</h3>
狀態:<span id="status">連線中...</span><br>
登入身分:<span id="userName"></span><br><br>
傳送給:<input type="text" id="targetUser" value="小明"><br>
訊息內容:<input type="text" id="inputMsg" style="width:300px">
<button onclick="sendMsg()">發送</button><br><br>
聊天記錄:<br>
<textarea id="msgArea" rows="15" cols="60" readonly></textarea>
</body>
</html>
同上,僅將 session user 設為「小明」,預設傳送目標改為「小華」。
chat_xiaohua.jsp 與 chat_xiaoming.jsp此為基礎版本,實務應用可考慮加入:心跳機制維持長連線、訊息持久化儲存、群組聊天支援、使用者上下線通知、訊息已讀狀態、SSL/TLS 加密傳輸等功能。