原生模式下
完整範例程式碼 https://github.com/MiltonLai/websocket-demos/tree/main/ws-demo01
│ pom.xml
└───src
├───main
│ ├───java
│ │ └───com
│ │ └───rockbb
│ │ └───test
│ │ └───wsdemo
│ │ SocketServer.java
│ │ WebSocketConfig.java
│ │ WsDemo01App.java
│ └───resources
│ application.yml
└───test
└───java
└───com
└───rockbb
└───test
└───wsdemo
SocketClient.java
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.rockbb.test</groupId>
<artifactId>ws-demo01</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>WS: Demo 01</name>
<properties>
<!-- Global encoding -->
<project.jdk.version>17</project.jdk.version>
<project.source.encoding>UTF-8</project.source.encoding>
<!-- Global dependency versions -->
<spring-boot.version>2.7.11</spring-boot.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Boot Dependencies -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-messaging</artifactId>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
</dependencies>
<build>
<finalName>ws-demo01</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.10.1</version>
<configuration>
<source>${project.jdk.version}</source>
<target>${project.jdk.version}</target>
<encoding>${project.source.encoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-resources-plugin</artifactId>
<version>3.3.0</version>
<configuration>
<encoding>${project.source.encoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
設定伺服器埠為 8763
server:
port: 8763
tomcat:
uri-encoding: UTF-8
spring:
application:
name: ws-demo01
@RestController
@SpringBootApplication
public class WsDemo01App {
public static void main(String[] args) {
SpringApplication.run(WsDemo01App.class, args);
}
@RequestMapping("/msg")
public String sendMsg(String sessionId, String msg) throws IOException {
Session session = SocketServer.getSession(sessionId);
SocketServer.sendMessage(session, msg);
return "send " + sessionId + " : " + msg;
}
}
必須顯式宣告 ServerEndpointExporter 這個 Bean 才能提供 websocket 服務
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter initServerEndpointExporter(){
return new ServerEndpointExporter();
}
}
提供 websocket 服務的關鍵類. @ServerEndpoint 的作用類似於 RestController, 這裡指定 client 存取的路徑格式為 ws://host:port/websocket/server/[id],
當 client 存取使用不同的 id 時, 會對應產生不同的 SocketServer 範例
@Component
@ServerEndpoint("/websocket/server/{sessionId}")
public class SocketServer {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(SocketServer.class);
private static final Map<String, Session> sessionMap = new ConcurrentHashMap<>();
private String sessionId = "";
@OnOpen
public void onOpen(Session session, @PathParam("sessionId") String sessionId) {
this.sessionId = sessionId;
/* Old connection will be kicked by new connection */
sessionMap.put(sessionId, session);
/*
* this: instance id. New instances will be created for each sessionId
* sessionId: assigned from path variable
* session.getId(): the actual session id (start from 0)
*/
log.info("On open: this{} sessionId {}, actual {}", this, sessionId, session.getId());
}
@OnClose
public void onClose() {
sessionMap.remove(sessionId);
log.info("On close: sessionId {}", sessionId);
}
@OnMessage
public void onMessage(String message, Session session) {
log.info("On message: sessionId {}, {}", session.getId(), message);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("On error: sessionId {}, {}", session.getId(), error.getMessage());
}
public static void sendMessage(Session session, String message) throws IOException {
session.getBasicRemote().sendText(message);
}
public static Session getSession(String sessionId){
return sessionMap.get(sessionId);
}
}
OnOpen 會注入一個 Session 引數, 這個是實際的 Websocket Session, 其 ID 是全域性唯一的, 可以唯一確定一個使用者端連線. 在當前版本的實現中, 這是一個從0開始自增的整數. 如果你需要實現例如單個使用者登入多個對談, 在通訊中, 將訊息轉發給同一個使用者的多個對談, 就要小心記錄這些 Session 的 ID.
@OnOpen
public void onOpen(Session session, @PathParam("sessionId") String sessionId)
在使用者端意外停止後, 伺服器端會收到 OnError 訊息, 可以通過這個訊息管理已經關閉的對談
client 測試類, 連線後可以通過命令列向 server 傳送訊息
public class SocketClient {
private static final org.slf4j.Logger log = LoggerFactory.getLogger(SocketClient.class);
public static void main(String[] args) throws URISyntaxException {
WebSocketClient wsClient = new WebSocketClient(
new URI("ws://127.0.0.1:8763/websocket/server/10001")) {
@Override
public void onOpen(ServerHandshake serverHandshake) {
log.info("On open: {}, {}", serverHandshake.getHttpStatus(), serverHandshake.getHttpStatusMessage());
}
@Override
public void onMessage(String s) {
log.info("On message: {}", s);
}
@Override
public void onClose(int i, String s, boolean b) {
log.info("On close: {}, {}, {}", i, s, b);
}
@Override
public void onError(Exception e) {
log.info("On error: {}", e.getMessage());
}
};
wsClient.connect();
log.info("Connecting...");
while (!ReadyState.OPEN.equals(wsClient.getReadyState())) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
log.error(e.getMessage(), e);
}
}
log.info("Connected");
wsClient.send("hello");
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String line = scanner.next();
wsClient.send(line);
}
wsClient.close();
}
}
程式碼的執行過程就是新建一個 WebSocketClient 並實現其處理訊息的介面方法, 使用 10001 作為 sessionId 進行連線, 在連線成功後, 不斷讀取鍵盤輸入 (System.in), 將輸入的字串傳送給伺服器端.
範例是一個普通的 Spring Boot jar專案, 可以通過mvn clean package
進行編譯, 再通過java -jar ws-demo01.jar
執行, 啟動後工作在8763埠
然後執行 SocketClient.java, 可以觀察到伺服器端接收到的訊息.
伺服器端可以通過瀏覽器存取 http://127.0.0.1:8763/msg?sessionId=10001&msg=123 向用戶端傳送訊息.
以上說明並演示了原生的 Websocket 實現方式, 可以嘗試執行多個 SocketClient, 使用相同或不同的 server sessionId 路徑, 觀察通訊的變化情況.