二、依赖和配置
<?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.1.17.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.xxxx</groupId>
<artifactId>webrtcspringboot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>webrtcspringboot</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- fastJson-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
<!--热加载-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
<scope>true</scope>
</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>
server.port=8443
server.ssl.enabled=true
server.ssl.keyStore=tomcat.keystore
server.ssl.keyAlias=tomcat
server.ssl.keyStorePassword=123456
server.ssl.keyStoreType=JKS
三、代码
1、config
package com.xxxx.webrtcspringboot.config;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.annotation.WebListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* 根据JFR-356,Tomcat提供的WebSocket API并没有获取客户端IP地址的方法。
* 我们无法直接在WebSocket类里面获取客户端IP地址等信息。
* 但是通过监听ServletRequest并使用HttpSession和ServerEndpointConfig里面的Attributes传递信息,
* 就可以实现直接在WebSocket类中获得客户端IP地址,弥补了WebSocket的一些先天不足。
*/
@WebListener
public class ClientIpServletListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent servletRequestEvent) {
}
@Override
public void requestInitialized(ServletRequestEvent servletRequestEvent) {
HttpServletRequest request=(HttpServletRequest) servletRequestEvent.getServletRequest();
HttpSession session=request.getSession();
//把HttpServletRequest中的IP地址放入HttpSession中,关键字可任取,此处为clientIp
session.setAttribute("clientIp", servletRequestEvent.getServletRequest().getRemoteAddr());
}
}
package com.xxxx.webrtcspringboot.config;
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
/**
* 在modifyHandshake方法中,可以将储存在httpSession中的clientIp,转移到ServerEndpointConfig中
*/
public class ConfiguratorForClientIp extends ServerEndpointConfig.Configurator {
/**
* 把HttpSession中保存的ClientIP放到ServerEndpointConfig中
* @param config
* @param request
* @param response
*/
@Override
public void modifyHandshake(ServerEndpointConfig config, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
config.getUserProperties().put("clientIp", httpSession.getAttribute("clientIp"));
}
}
package com.xxxx.webrtcspringboot.config;
import org.apache.catalina.Context;
import org.apache.catalina.connector.Connector;
import org.apache.tomcat.util.descriptor.web.SecurityCollection;
import org.apache.tomcat.util.descriptor.web.SecurityConstraint;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
/**
* 将http请求,自动转换成https请求
*/
public class HttpConfig {
@Bean
public Connector connector(){
Connector connector=new Connector("org.apache.coyote.http11.Http11NioProtocol");
connector.setScheme("http");
connector.setPort(80);
connector.setSecure(false);
connector.setRedirectPort(8443);
return connector;
}
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory(Connector connector){
TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {
@Override
protected void postProcessContext(Context context) {
SecurityConstraint securityConstraint = new SecurityConstraint();
securityConstraint.setUserConstraint("CONFIDENTIAL");
SecurityCollection collection = new SecurityCollection();
collection.addPattern("/*");
securityConstraint.addCollection(collection);
context.addConstraint(securityConstraint);
}
};
tomcat.addAdditionalTomcatConnectors(connector);
return tomcat;
}
}
package com.xxxx.webrtcspringboot.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* websocket 初始化配置
*/
@Configuration
public class WebSocketConfig {
/**
* 开启WebSocket支持
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
2、controller
package com.xxxx.webrtcspringboot.controller;
import com.xxxx.webrtcspringboot.service.RoomService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
@RestController
@Slf4j
public class IndexController {
private static String ipOfInet4Address;
private static final String IP_CODE = "127.0.0.1";
//拿到本机在wifi中的局域网ip
static {
// 获得本机的所有网络接口
Enumeration<NetworkInterface> naifs;
try {
naifs = NetworkInterface.getNetworkInterfaces();
while (naifs.hasMoreElements()) {
NetworkInterface nif = naifs.nextElement();
// 获得与该网络接口绑定的 IP 地址,一般只有一个
Enumeration<InetAddress> addresses = nif.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
// 获取IPv4 地址
if (addr instanceof Inet4Address) {
ipOfInet4Address = addr.getHostAddress();
log.info("网卡接口名称:{}",nif.getName());
log.info("网卡接口地址:{}",addr.getHostAddress());
}
}
}
} catch (SocketException e) {
log.error("获取数据失败:{}",e);
}
}
@Value("${server.port}")
private Integer port;
@Autowired
private RoomService roomService;
@GetMapping("/getWebSocketUrl")
public Map<String, String> getIpAddress(HttpServletRequest request) {
Map<String, String> result = new HashMap<>(1);
if(IP_CODE.equals(request.getRemoteAddr())){
//本地访问
result.put("url", "wss:"+request.getRemoteAddr()+":"+port+ "/websocket");
}else{
//服务IP访问
result.put("url", "wss:" + ipOfInet4Address +":"+port+ "/websocket");
}
return result;
}
@GetMapping("/queryCountInRoom")
public Map<String, String> queryCountInRoom(String roomId) {
Map<String, String> result = new HashMap<>(1);
result.put("count", String.valueOf(roomService.queryCountInRoom(roomId)));
return result;
}
}
3、entity
package com.xxxx.webrtcspringboot.entity;
import lombok.Data;
@Data
public class Message {
/**
* 创建房间
*/
public static final String TYPE_COMMAND_ROOM_ENTER = "enterRoom";
/**
* 获取房间
*/
public static final String TYPE_COMMAND_ROOM_LIST = "roomList";
/**
* 对话
*/
public static final String TYPE_COMMAND_DIALOGUE = "dialogue";
/**
* 准备
*/
public static final String TYPE_COMMAND_READY = "ready";
/**
* 离开
*/
public static final String TYPE_COMMAND_OFFER = "offer";
/**
* 回答
*/
public static final String TYPE_COMMAND_ANSWER = "answer";
/**
* 申请人
*/
public static final String TYPE_COMMAND_CANDIDATE = "candidate";
private String command;
private String userId;
private String roomId;
private String message;
}
4、service
package com.xxxx.webrtcspringboot.service;
import org.springframework.stereotype.Service;
/**
* 命令处理服务
*/
@Service
public class CommandService {
}
package com.xxxx.webrtcspringboot.service;
import com.alibaba.fastjson.JSON;
import com.xxxx.webrtcspringboot.entity.Message;
import com.xxxx.webrtcspringboot.websocket.Connection;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Collection;
import java.util.Set;
/**
* 消息处理服务
*/
@Slf4j
@Service
public class MessageService {
@Autowired
private RoomService roomService;
/**
* 给房间内的所有人发送消息(包括自己)
*/
public void sendMessageForEveryInRoom(Message message) {
Set<Connection> room = roomService.queryRoomById(message.getRoomId());
room.stream().forEach(t->{
try {
t.getSession().getBasicRemote().sendText(JSON.toJSONString(message));
} catch (IOException e) {
log.error("发送消息失败: {}, {}", message.getUserId(), e);
}
});
}
/**
* 给房间除自己之外的所有人发送消息
*/
public void sendMessageForEveryExcludeSelfInRoom(Message message) {
Set<Connection> room = roomService.queryRoomById(message.getRoomId());
room.stream().forEach(t->{
try {
if (!message.getUserId().equals(t.getUserId())) {
t.getSession().getBasicRemote().sendText(JSON.toJSONString(message));
}
} catch (IOException e) {
log.error("{}->向房间:{}发送消息失败,{}",message.getUserId(), message.getRoomId(),e);
}
});
}
/**
* 给在线的所有人发送消息(包括自己)
*/
public void sendMessageForAllOnline(Message message) {
Collection<Set<Connection>> rooms = roomService.queryAllRoom();
rooms.stream().forEach(t-> t.stream().forEach(k->{
try {
k.getSession().getBasicRemote().sendText(JSON.toJSONString(message));
} catch (IOException e) {
log.error("{}用户发送广播失败:{}", message.getUserId(), e);
}
}));
}
}
package com.xxxx.webrtcspringboot.service;
import com.xxxx.webrtcspringboot.websocket.Connection;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* 房间处理服务
*/
@Service
public class RoomService {
private Map<String, Set<Connection>> rooms = new ConcurrentHashMap<>();
/**
* 加入到大厅
*/
public void enterLobby(Connection connection) {
Set<Connection> lobby = rooms.get("lobby");
if (lobby == null) {
rooms.put("lobby", new HashSet<>());
lobby = rooms.get("lobby");
lobby.add(connection);
} else {
lobby.add(connection);
}
}
/**
* 离开大厅
*/
public void leaveLobby(Connection connection) {
Set<Connection> lobby = rooms.get("lobby");
lobby.remove(connection);
}
/**
* 加入指定的房间
*/
public String enterRoom(String roomId, Connection connection) {
String operate;
Set<Connection> room = rooms.get(roomId);
if (room == null) {
rooms.put(roomId, new HashSet<>());
room = rooms.get(roomId);
room.add(connection);
operate = "created";
} else {
room.add(connection);
operate = "joined";
}
//离开大厅
leaveLobby(connection);
return operate;
}
/**
* 离开指定的房间
*/
public void leaveRoom(String roomId, Connection connection) {
if (roomId != null) {
Set<Connection> room = rooms.get(roomId);
if (room != null) {
room.remove(connection);
if (room.size() == 0) {
rooms.remove(roomId);
}
}
}
}
/**
* 查询指定房间人数(包括自己)
*/
public Integer queryCountInRoom(String roomId) {
Set<Connection> room = rooms.get(roomId);
return room == null ? 0 : room.size();
}
/**
* 将用户踢出房间
*/
public void removeUserFromRoom(String roomId, String userId) {
Set<Connection> room = rooms.get(roomId);
if (room != null) {
room.stream().forEach(e->{
if (e.getUserId().equals(userId)) {
room.remove(e);
}
});
}
}
/**
* 通过房间Id查询房间
*/
public Set<Connection> queryRoomById(String roomId) {
return rooms.get(roomId);
}
/**
* 查询所有存在的房间名称
*/
public Set<String> queryAllRoomName() {
return rooms.keySet();
}
/**
* 查询所有存在的房间
*/
public Collection<Set<Connection>> queryAllRoom() {
return rooms.values();
}
}
5、websocket
package com.xxxx.webrtcspringboot.websocket;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.xxxx.webrtcspringboot.config.ConfiguratorForClientIp;
import com.xxxx.webrtcspringboot.entity.Message;
import com.xxxx.webrtcspringboot.service.CommandService;
import com.xxxx.webrtcspringboot.service.MessageService;
import com.xxxx.webrtcspringboot.service.RoomService;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @Data 由于@Data重写了hashCode()和equals()方法,
* 会导致Set remove元素时,找不到正确的元素,
* 应用@Setter @Getter @ToString替换
* @ServerEndpoint 不是单例模式
*/
@ServerEndpoint(value = "/websocket", configurator = ConfiguratorForClientIp.class)
@Component
@Slf4j
@Getter
@Setter
@ToString
public class Connection {
/**
* 在线总人数
*/
private static volatile AtomicInteger onlineCount = new AtomicInteger(0);
private static RoomService roomService;
private static MessageService messageService;
private static CommandService commandService;
@Autowired
public void setRoomService(RoomService roomService) {
Connection.roomService = roomService;
}
@Autowired
public void setMessageService(MessageService messageService) {
Connection.messageService = messageService;
}
@Autowired
public void setCommandService(CommandService commandService) {
Connection.commandService = commandService;
}
/**
* 某个客户端的ip
*/
private String ip;
/**
* 某个客户端的userID
*/
private String userId;
/**
* 某个客户端的roomNo
*/
private String roomId;
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
ip = (String) session.getUserProperties().get("clientIp");
//未进任何房间时,将本次连接放到大厅里面
roomService.enterLobby(this);
log.info("用户: {}, 连接到服务器,当前在线人数为:{}", ip, onlineCount.incrementAndGet());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose(Session session) {
//离开大厅
roomService.leaveLobby(this);
//即使连接错误,回调了onError方法,最终也会回调onClose方法,所有退出房间写在这里比较合适
roomService.leaveRoom(roomId, this);
//在线数减1
log.info("用户: {}, 关闭连接,退出房间: {}, 当前在线人数为:{}", ip, roomId, onlineCount.addAndGet(-1));
}
/**
* 连接发生错误时调用的方法
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户主动关闭连接失败: {}",ip);
try {
session.close();
} catch (Exception e) {
log.error("关闭连接失败:{}",e);
}
}
/**
* 收到客户端消息后调用的方法
* @param stringMessage 客户端发送过来的消息
* */
@OnMessage
public void onMessage(Session session, String stringMessage) {
Message message = JSON.parseObject(stringMessage, Message.class);
log.info("收到来自: {}, 信息:{}", ip, JSON.toJSONString(message));
switch (message.getCommand()) {
case Message.TYPE_COMMAND_ROOM_ENTER:
this.userId = message.getUserId();
this.roomId = message.getRoomId();
enterRoom(message);
//服务器主动向所有在线的人推送房间列表
pushRoomList();
break;
case Message.TYPE_COMMAND_DIALOGUE:
messageService.sendMessageForEveryInRoom(message);
break;
case Message.TYPE_COMMAND_ROOM_LIST:
//前端从服务器拉取房间列表
pullRoomList(message);
break;
case Message.TYPE_COMMAND_READY:
case Message.TYPE_COMMAND_OFFER:
case Message.TYPE_COMMAND_ANSWER:
case Message.TYPE_COMMAND_CANDIDATE:
messageService.sendMessageForEveryExcludeSelfInRoom(message);
break;
default:
}
}
/**
* 返回给自己是加入房间还是创建房间
* @param message
*/
private void enterRoom(Message message) {
message.setMessage(roomService.enterRoom(roomId, this));
try {
session.getBasicRemote().sendText(JSON.toJSONString(message));
} catch (IOException e) {
log.error("加入房间还是创建房间失败: {}", e);
}
}
private void pullRoomList(Message message) {
message.setMessage(JSON.toJSONString(roomService.queryAllRoomName(), SerializerFeature.WriteNullListAsEmpty));
try {
session.getBasicRemote().sendText(JSON.toJSONString(message));
} catch (IOException e) {
log.error("获取数据失败:{}", e);
}
}
private void pushRoomList() {
//告诉每个终端更新房间列表
Message roomListMessage = new Message();
roomListMessage.setCommand(Message.TYPE_COMMAND_ROOM_LIST);
roomListMessage.setMessage(JSON.toJSONString(roomService.queryAllRoomName(),SerializerFeature.WriteNullListAsEmpty));
messageService.sendMessageForAllOnline(roomListMessage);
}
}
6、启动类
package com.xxxx.webrtcspringboot;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.alibaba.fastjson.support.config.FastJsonConfig;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.context.annotation.Bean;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.scheduling.annotation.EnableScheduling;
import java.util.ArrayList;
import java.util.List;
@SpringBootApplication
@EnableScheduling
@ServletComponentScan
public class WebrtcspringbootApplication {
public static void main(String[] args) {
SpringApplication.run(WebrtcspringbootApplication.class, args);
}
/**
* 替换json框架为fastjson
*/
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
//1.需要定义一个convert转换消息的对象;
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
//2:添加fastJson的配置信息;
FastJsonConfig fastJsonConfig = new FastJsonConfig();
//指定当属性值为null是否输出:pro:null
fastJsonConfig.setSerializerFeatures(SerializerFeature.WriteMapNullValue,SerializerFeature.WriteNullListAsEmpty);
//3处理中文乱码问题
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
//4.在convert中添加配置信息.
fastConverter.setSupportedMediaTypes(fastMediaTypes);
fastConverter.setFastJsonConfig(fastJsonConfig);
//返回构成用的组件Bean
return new HttpMessageConverters((HttpMessageConverter<?>) fastConverter);
}
}
代码下载地址:https://elasti.oss-cn-beijing.aliyuncs.com/webrtcspringboot.zip
参考:https://github.com/weicaiwei/webrtc
四、题外话:websocket和stomp兼容配置
package com.xxxx.webrtcspringboot.config;
import com.xxxx.webrtcspringboot.service.SocketService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.web.socket.config.annotation.*;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* websocket 初始化配置
*/
@EnableWebSocketMessageBroker
@EnableWebSocket
@Configuration
@Slf4j
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer, WebSocketConfigurer {
/**
* stomp 协议
* @param registry
*/
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/v1/websocket-stomp").setAllowedOrigins("*").withSockJS();
registry.addEndpoint("/v1/websocket-stomp").setAllowedOrigins("*");
}
/**
* 原生websocket协议
* @param registry
*/
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
SocketService socketService = new SocketService();
registry.addHandler(socketService, "/v1/websocket").setAllowedOrigins("*").withSockJS();
registry.addHandler(socketService, "/v1/websocket").setAllowedOrigins("*");
}
/**
* 开启WebSocket支持
* @return
*/
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
/**
* stomp 订阅服务
* @param registry
*/
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 自定义调度器,用于控制心跳线程
ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler();
// 线程池线程数,心跳连接开线程
taskScheduler.setPoolSize(1);
// 线程名前缀
taskScheduler.setThreadNamePrefix("websocket-thread-");
// 初始化
taskScheduler.initialize();
registry.enableSimpleBroker("/v1").setHeartbeatValue(new long[]{10000, 10000})
.setTaskScheduler(taskScheduler);
registry.setUserDestinationPrefix("/user/");
registry.setCacheLimit(Integer.MAX_VALUE);
}
}
package com.xxxx.webrtcspringboot.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
/**
* 原生websocket方法
*/
@Slf4j
public class SocketService extends AbstractWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) {
log.debug("创建连接");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
log.debug("业务处理");
}
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) {
log.debug("发送消息");
}
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) {
log.info("关闭通道===>closeStatus={},webSocketSession={}", closeStatus, webSocketSession);
}
}