SpringBoot对WebSocket集成十分完美,直接上步骤。
引入Maven依赖
<!--WebSocket-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
启用WebSocket以及注意事项(关于启动类的操作)
- 在启动类上添加注解
@EnableWebSocket
- 使用内置Tomcat需要添加Bean
ServerEndpointExporter
- 如果同时使用了定时任务则需要添加Bean
TaskScheduler
- 代码如下(包含详细注释):
@EnableWebSocket
@MapperScan("com.chase.repository")
@SpringBootApplication
public class AccesslogApplication extends SpringBootServletInitializer {
//打包时注册启动类
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(AccesslogApplication.class);
}
/**
* 使用 websockt注解的时候,使用@EnableScheduling注解后,即开启定时任务
* 启动的时候一直报错,增加这个bean 则报错解决。
*/
@Bean
public TaskScheduler taskScheduler(){
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
taskScheduler.setPoolSize(10);
taskScheduler.initialize();
return taskScheduler;
}
public static void main(String[] args) {
SpringApplication.run(AccesslogApplication.class, args);
}
/**
* 如果直接使用springboot的内置容器,而不是使用独立的servlet容器,就要注入ServerEndpointExporter,外部容器则不需要。
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
ServerEndpoint(相当于服务器端)
- 类添加上两个注解
@Component
和@ServerEndpoint("/accesslog/ws/{username}")
/accesslog/ws/{username}
路径可以自定义,username用于区别每个不同的用户- WebSocket四个事件,分别对应四个注解
@OnOpen
(建立连接)、@OnMessage
(收到客户端消息)、@OnClose
(连接关闭)、@OnError
(发生异常) - WebSocket推送采用Session,username用于区分不同用户的Session。在建立连接的时候会将该用户的Session和username信息存入ConcurrentHashMap(保证多线程安全同时方便利用map.get(username)进行推送到指定用户),推送时只需要根据相应的username即可实现推送。推送方法和存储的map由一个工具类来实现(当然你也可以有更简单的实现方式),推送工具类见下一点。
- 小熙踩到过一个坑,SpringBoot项目设置了
server.servlet.context-path=/accesslog
后会直接导致WebSocket连接失败,猜测是由于路径的问题,但百思不得其解,尝试修改亦无果,望路过大神解惑一二。
@Component
@ServerEndpoint("/accesslog/ws/{username}")
public class ChatRoomServerEndpoint {
@OnOpen
public void openSession(@PathParam("username") String username, Session session) {
ONLINE_USER_SESSIONS.put(username, session);
String message = "[" + username + "] 客户端信息!";
sendMessageAll("服务器连接成功!");
sendMessage(session,"");
System.out.println("连接成功"+message);
}
@OnMessage
public void onMessage(@PathParam("username") String username, String message) {
System.out.println("服务器收到:"+"[" + username + "] : " + message);
sendMessageAll("[" + username + "] : " + message);
}
@OnClose
public void onClose(@PathParam("username") String username, Session session) {
//当前的Session 移除
ONLINE_USER_SESSIONS.remove(username);
//并且通知其他人当前用户已经断开连接了
sendMessageAll("[" + username + "] 断开连接!");
try {
session.close();
} catch (IOException e) {
}
}
@OnError
public void onError(Session session, Throwable throwable) {
try {
session.close();
} catch (IOException e) {
}
}
}
推送工具类
- 推送工具类定义了两个静态方法,单用户推送和全用户推送(全用户推送就是对 ConcurrentHashMap中的所有用户进行推送)
- websocket session发送文本消息使用了
RemoteEndpoint.Basic basic = session.getBasicRemote(); basic.sendText(message);
同步发送的方式。 - 在进行推送的时候直接调用该工具类即可。见推送示例。
- WebSocket Session发送消息的两个方法getAsyncRemote()和getBasicRemote()的区别,见文章目录。
public class WebSocketUtils {
public static final Map<String, Session> ONLINE_USER_SESSIONS = new ConcurrentHashMap<>();
// 单用户推送
public static void sendMessage(Session session, String message) {
if (session == null) {
return;
}
final RemoteEndpoint.Basic basic = session.getBasicRemote();
if (basic == null)
{
return;
}
try {
basic.sendText(message);
} catch (IOException e) {
System.out.println("sendMessage IOException "+ e);
}
}
// 全用户推送
public static void sendMessageAll(String message) {
ONLINE_USER_SESSIONS.forEach((sessionId, session) -> sendMessage(session, message));
}
}
客户端JS
<script type="text/javascript">
$(document).ready(function(){
var urlPrefix ='ws://localhost:8080/accesslog/ws/';
var ws = null;
var joinfun = function(){
var username = "小熙";
var url = urlPrefix + username;
ws = new WebSocket(url);
ws.onopen = function () {
console.log("建立 websocket 连接...");
};
ws.onmessage = function(event){
//服务端发送的消息
$('#message_content').append(event.data+'\n');
// 接到消息之后 任君处置
};
ws.onclose = function(){
$('#message_content').append('用户['+username+'] 断开连接!');
console.log("关闭 websocket 连接...");
}
};
joinfun();//自动连接
// 重新连接
$('#user_add').click(function(){
joinfun();
});
//客户端发送消息到服务器
$('#user_send_all').click(function(){
var msg = $('#in_room_msg').val();
if(ws){
ws.send(msg);
}
});
// 断开连接
$('#user_back').click(function(){
if(ws){
ws.close();
}
});
});
</script>
推送示例
private void configureTasks() {
//全用户发送
sendMessageAll("全体通知!关注熙乎博客!”);
//单用户发送消息
for (Map.Entry<String, Session> ma: ONLINE_USER_SESSIONS.entrySet()
) {
if ("熙乎".equals(ma.getKey())) {
sendMessage(ma.getValue(), "小熙你好!我是马云!");
}
}
}
文章目录
打包注意事项
Maven导入的WebSocket的jar包会与SpringBoot内置tomcat中的WebSocket的jar包冲突,打包时把SpringBoot内置tomcat的jar包给忽略掉即可。
报错如下:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'serverEndpointExporter' defined in class path resource [xxxxxx.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: javax.websocket.server.ServerContainer not available
- 方法一:
在打包时使用 mvn clean package -DskipTests 就可以完美打包(跳过测试)
- 方法二:
在pom文件的的中加入如下配置,即可直接打包成功
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
WebSocket Session发送消息的两个方法getAsyncRemote()和getBasicRemote()的区别
websocket session发送文本消息有两个方法:getAsyncRemote()
非阻塞式的(异步)和getBasicRemote()
阻塞式的(同步)。
直接上结论:
- 如果一次性发送全部消息,两者基本没有差异,只是单纯同步和异步的区别。
- 如果需要一次发送部分消息,则避免使用
getBasicRemote()
。 - 推荐使用
getAsyncRemote()
两者区别如果觉得小熙描述不够,深入了解可以参考以下博客:
Is WebSocket Session really thread safe?