原生模式下
完整示例代码 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 路径, 观察通信的变化情况.