原生模式下
完整示例代码 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
<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.0modelVersion>
<groupId>com.rockbb.testgroupId>
<artifactId>ws-demo01artifactId>
<packaging>jarpackaging>
<version>1.0-SNAPSHOTversion>
<name>WS: Demo 01name>
<properties>
<project.jdk.version>17project.jdk.version>
<project.source.encoding>UTF-8project.source.encoding>
<spring-boot.version>2.7.11spring-boot.version>
properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-websocketartifactId>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-messagingartifactId>
dependency>
<dependency>
<groupId>org.java-websocketgroupId>
<artifactId>Java-WebSocketartifactId>
<version>1.5.3version>
dependency>
dependencies>
<build>
<finalName>ws-demo01finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-compiler-pluginartifactId>
<version>3.10.1version>
<configuration>
<source>${project.jdk.version}source>
<target>${project.jdk.version}target>
<encoding>${project.source.encoding}encoding>
configuration>
plugin>
<plugin>
<groupId>org.apache.maven.pluginsgroupId>
<artifactId>maven-resources-pluginartifactId>
<version>3.3.0version>
<configuration>
<encoding>${project.source.encoding}encoding>
configuration>
plugin>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<version>${spring-boot.version}version>
<executions>
<execution>
<goals>
<goal>repackagegoal>
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 路径, 观察通信的变化情况.