WebSocket Java入门
WebSocket与Http
- 一般Http请求必须由客户端发送请求到服务端,服务端给出响应才能实现两者的数据交互,没有客户端请求的话服务端无法主动发送数据给客户端。
- WebSocket可以实现客户端和服务端的全双工通信,即服务端可以直接发送数据给客户端,客户端也可以直接发送数据给服务端。
- 两者其他不同请自行查阅文档。
Java注解开发WebSocket服务
- 初始化SpringBoot项目,并添加websocket启动器依赖即可
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.10</version>
<relativePath/>
</parent>
<groupId>demo</groupId>
<artifactId>websocket</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>websocket</name>
<description>websocket</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
- 定义Endpoint实体类
- WebSocket每个客户端连接都是一个endpoint
- 使用static的ConcurrentHashMap保存项目所有的endpoint
- websocket的session用于发送消息
- endpoint不是基于Servlet的,无法直接获取httpSession,httpSession需要预先存入EndpointConfig,详情见GetHttpSessionConfig代码。
- onOpen:连接建立自动执行;onClose:连接关闭自动执行;onError:连接异常自动执行;onMessage:连接收到消息自动执行。
package demo.websocket.endpoint;
import com.fasterxml.jackson.databind.ObjectMapper;
import demo.vo.MessageVo;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@ServerEndpoint("/endpoint/demo")
public class DemoEndpoint {
private static Map<String, DemoEndpoint> endpoints = new ConcurrentHashMap<>();
private Session session;
private HttpSession httpSession;
@OnOpen
public void onOpen(Session session, EndpointConfig config){
System.out.println("连接建立");
this.session = session;
this.httpSession = (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
endpoints.put("sukiha", this);
}
@OnMessage
public void onMessage(String message, Session session){
System.out.println("收到消息:" + message);
ObjectMapper objectMapper = new ObjectMapper();
try {
MessageVo messageVo = objectMapper.readValue(message, MessageVo.class);
if(endpoints.containsKey(messageVo.getTo()))
endpoints.get(messageVo.getTo()).session.getBasicRemote().sendText(messageVo.getMsg());
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
@OnClose
public void onClose(Session session, CloseReason closeReason){
System.out.println("连接关闭");
}
@OnError
public void onError(Session session, Throwable throwable){
System.out.println("连接异常");
throwable.printStackTrace();
}
}
- GetHttpSessionConfig 配置类
- 需要预先将httpSession对象存入EndpointConfig中
package demo.config;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
public class GetHttpSessionConfig extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
}
}
package demo.vo;
public class MessageVo {
private String to;
private String Msg;
}
- 注入ServerEndpointExporter bean对象,自动注册使用了@ServerEndpoint的bean
package demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter ServerEndpointExporter(){
return new ServerEndpointExporter();
}
}
JavaScript开发WebSocket客户端
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<textarea id="sendMsg" ></textarea>
<textarea id="receiceMsg"></textarea>
<button id="sendBtn" onclick="doSendMsg()">发送</button>
</body>
</html>
<script>
var ws;
window.onload=function(){
ws = new WebSocket("ws:127.0.0.1:8080/endpoint/demo");
ws.onopen = function(){console.log("连接建立");}
ws.onclose = function(){console.log("连接关闭");}
ws.onerror = function(){console.log("连接异常");}
ws.onmessage = function(event){
console.log("收到消息:" + event.data);
document.getElementById("receiceMsg").innerHTML = event.data;
}
}
function doSendMsg(){
var data = {to:"sukiha",msg:document.getElementById("sendMsg").value};
ws.send(JSON.stringify(data));
}
</script>
Java开发WebSocket客户端
- 某些应用场景下,Java服务也要作为WebSocket的客户端。
- Maven依赖
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.4.0</version>
</dependency>
package demo.client;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Scanner;
public class DemoClient {
public static void main(String[] args) {
WebSocketClient webSocketClient;
try {
webSocketClient = new WebSocketClient(new URI("ws://127.0.0.1:8080/endpoint/demo")) {
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("建立连接");
}
@Override
public void onMessage(String s) {
System.out.println("收到消息:" + s);
}
@Override
public void onClose(int i, String s, boolean b) {
System.out.println("关闭连接");
}
@Override
public void onError(Exception e) {
System.out.println("连接异常");
}
};
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
webSocketClient.connect();
System.out.println("请输入要发送的信息:");
Scanner scan = new Scanner(System.in);
while (true){
webSocketClient.send("{\"to\":\"sukiha\",\"msg\":\"" + scan.nextLine() + "\"}");
}
}
}
Maven打包报错
- 使用Maven打包SpringBoot集成WebSocket的项目有可能报错,提示Error creating bean with name ‘ServerEndpointExporter’。
- 解决方案:修改maven打包插件
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
其他问题
WebSocket运行一段时间后停止
- WebSocket连接一段时间以后会自动停止,这个时间可能会受nginx和tomcat环境的影响,具体数值超时时间请自行测试。
- 建议每隔一分钟,客户端向服务端发送一段数据,连接会一直活跃。