JSR 356, Java API for WebSocket 是 java 為 WebSocket 制定的 API 規格,Tomcat 10 內建 JSR 356 的實作,以下記錄如何使用 Tomcat 支援的 WebSocket
Server Side
Server 部分,不需要調整 web.xml,只需要有一個 Java Class,加上適當的 annotation
@ServerEndpoint("/websocketendpoint")
這是將這個 Java Class 註冊為處理
ws://localhost:8080/testweb/websocketendpoint
這個 WebSocket 網址的對應處理的程式@OnOpen, @OnClose
分別寫在兩個 method 上面,對應處理 WebSocket 連線及斷線的 method
@OnMessage
收到 WebSocket 的一個 message 時,對應的處理的 method
@OnError
錯誤處理
WsServer.java
package com.maxkit.testweb.ws;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
@ServerEndpoint("/websocketendpoint")
public class WsServer {
private Logger logger = LoggerFactory.getLogger(this.getClass().getName());
private Session wsSession = null;
private String username;
@OnOpen
public void onOpen(Session session) throws IOException {
this.wsSession = session;
session.getBasicRemote().sendText("The session is opened.");
WsSeverSessions.getInstance().putSession(session.getId(), session);
// parsing url in this format
// ws://localhost:8080/testweb/websocketendpoint?username=name
// queryString: username=name
String queryString = session.getQueryString();
Map<String, String> params = parseQuerystring(queryString);
// this.username = QueryString.substring(QueryString.indexOf("=")+1);
this.username = params.get("username");
logger.info("Open Connection session id={}, username={}", session.getId(), username);
String message = (username+" is in chatroom");
broadcastMessage(message);
}
@OnClose
public void onClose(Session session) {
logger.info("Close Connection username={}, session id={}", username, session.getId());
this.wsSession = null;
WsSeverSessions.getInstance().removeSession(session.getId());
}
@OnMessage
public String onMessage(String message) throws IOException {
logger.info("Message from the client username={}, session id ={}, message={}", username, this.wsSession.getId(), message);
broadcastMessage(username + " broadcast: " + message);
return username+ " echo: " + message;
}
@OnError
public void onError(Throwable e){
e.printStackTrace();
}
public void broadcastMessage(String message) throws IOException {
for (Session session : WsSeverSessions.getInstance().getSessions()) {
if (session.isOpen()) {
session.getBasicRemote().sendText(message);
}
}
}
public static Map<String, String> parseQuerystring(String queryString) {
Map<String, String> map = new HashMap<String, String>();
if ((queryString == null) || (queryString.equals(""))) {
return map;
}
String[] params = queryString.split("&");
for (String param : params) {
try {
String[] keyValuePair = param.split("=", 2);
String name = URLDecoder.decode(keyValuePair[0], StandardCharsets.UTF_8);
if (name.equals("")) {
continue;
}
String value = keyValuePair.length > 1 ? URLDecoder.decode( keyValuePair[1], StandardCharsets.UTF_8) : "";
map.put(name, value);
} catch (IllegalArgumentException e) {
// ignore this parameter if it can't be decoded
}
}
return map;
}
}
因為 Server 是透過 session.getBasicRemote()
,以 sendText 發送訊息給 client,如果要能夠在 server side 將訊息廣播給所有連線的 clients,必須在 server 的記憶體中,紀錄所有的 jakarta.websocket.Session
以下是用 Singleton 的方式,將 jakarta.websocket.Session 記錄在 ConcurrentHashMap 裡面。上面的 @OnOpen, @OnClose 會有將 Session 儲存在 WsServerSessions 的程式,另外 broadcastMessage 會取得所有的 Session,用以發送廣播訊息。
WsServerSessions.java
package com.maxkit.testweb.ws;
import jakarta.websocket.Session;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
public class WsSeverSessions {
private final ConcurrentHashMap<String, Session> sessions = new ConcurrentHashMap<>();
private volatile static WsSeverSessions _instance = null;
private WsSeverSessions() {
}
public static WsSeverSessions getInstance() {
if (_instance == null) {
synchronized (WsSeverSessions.class) {
if (_instance == null) {
_instance = new WsSeverSessions();
}
}
}
return _instance;
}
public synchronized void putSession(String id, Session session) {
sessions.put(id, session);
}
public synchronized void removeSession(String id) {
sessions.remove(id);
}
public synchronized Collection<Session> getSessions() {
return sessions.values();
}
}
Client Side
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Tomcat WebSocket</title>
</head>
<body>
<form>
username: <input id="username" type="text">
<input onclick="wsLogin();" value="Login" type="button">
<br/>
message: <input id="message" type="text">
<input onclick="wsSendMessage();" value="Echo" type="button">
<input onclick="wsCloseConnection();" value="Disconnect" type="button">
</form>
<br>
<textarea id="echoText" rows="20" cols="100"></textarea>
<script type="text/javascript">
var webSocket = null;
var username = document.getElementById("username");
var echoText = document.getElementById("echoText");
var message = document.getElementById("message");
function wsLogin() {
if( username.value === "" ) {
username.value = getRandomInt(10000).toString();
}
webSocket = new WebSocket("ws://localhost:8080/testweb/websocketendpoint?username="+username.value );
echoText.value = "";
webSocket.onopen = function (message) {
wsOpen(message);
};
webSocket.onmessage = function (message) {
wsGetMessage(message);
};
webSocket.onclose = function (message) {
wsClose(message);
};
webSocket.onerror = function (message) {
wsError(message);
};
function wsOpen(message) {
echoText.value += "Connected ... \n";
}
function wsGetMessage(message) {
echoText.value += "Message received from to the server : " + message.data + "\n";
}
function wsClose(message) {
echoText.value += "Disconnect ... \n";
}
function wsError(message) {
echoText.value += "Error ... \n";
}
}
function wsSendMessage() {
if( webSocket == null ) {
return;
}
webSocket.send(message.value);
echoText.value += "Message sended to the server : " + message.value + "\n";
message.value = "";
}
function wsCloseConnection() {
if( webSocket == null ) {
return;
}
webSocket.close();
}
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
</script>
</body>
</html>
Tomcat Example
網頁部分
https://github.com/apache/tomcat/tree/main/webapps/examples/websocket
Server Side
https://github.com/apache/tomcat/tree/main/webapps/examples/WEB-INF/classes/websocket
可以直接把 server side 的 java package: websocket 複製到自己的測試 web project,程式裡面有用到 util package 的 classes,把 util 也複製過去。server side 的寫法比上面測試的還複雜一點點
網頁部分只需要直接複製 websocket 這個目錄的網頁,但因為範例程式的 webapp 是固定寫成 examples,稍微修改 xhtml 網頁,把 examples 替代為自己的 webapp,例如 testweb
只需要這些 code 就可以執行 tomcat 的 websocket 範例
範例有四個
chat:chat room
echo:echo message
drawboard:多人在一個繪圖板上,任意繪圖
snake:可多人在分享的 snake board 上面,用 keyboard 將 snake 轉向
要注意 project 加上官方 websocket examples後,上面自己寫的程式變成無法運作,原因是 java.websocket.ExamplesConfig 這個 class 實作 ServerApplicationConfig 介面,多了一些限制
getAnnotatedEndpointClasses
限制只會掃描 websocket 這個 package 裡面的 Endpoint annotation,所以將上面的程式移到這個 package 裡面
getEndpointConfigs
加上以下這段程式
if (scanned.contains(WsServer.class)) { result.add(ServerEndpointConfig.Builder.create( WsServer.class, "/websocketendpoint").build()); }
沒有留言:
張貼留言