简介
HTTP 协议是基于请求响应模式,并且无状态的。HTTP 通信只能由客户端发起,HTTP 协议做不到服务器主动向客户端推送信息。但是 WebSocket 协议能做到主动给客户端发信息。
spring boot 集成快速入门
1. 导入坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. 编写配置类
@Configuration
public class WebSocketConfig{
// ...
// 自动注册使用的websocket注解的bean,他会扫描bean中带有@ServerEndPoint注解的类
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
// ...
}
@EnableWebSocket 这个注解放在配置类上,不知道有什么用,只知道不能代替注入这个 bean
3. 编写 websocket 处理类
注意,别导错包了,websocket注解在 javax 包下
/**
* websocket的处理类
* 作用相当于HTTP请求中的controller
*/
@Component
@Slf4j
@ServerEndpoint("请求路径")
public class WebSocketServer {
// 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的
private static int onlineCount = 0;
// concurrent包的线程安全Set,用来存放每个客户端对应的WebSocket对象
private static ConcurrentHashMap<String, WebSocketServer> webSocketMap =
new ConcurrentHashMap<>();
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
// 接收userId
private String userId = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("userId") String userId) {
this.session = session;
this.userId = userId;
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
//加入set中
webSocketMap.put(userId, this);
} else {
//加入set中
webSocketMap.put(userId, this);
//在线数加1
addOnlineCount();
}
log.info("用户连接:" + userId + ",当前在线人数为:" + getOnlineCount());
sendMessage("连接成功");
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
if (webSocketMap.containsKey(userId)) {
webSocketMap.remove(userId);
//从set中删除
subOnlineCount();
}
log.info("用户退出:" + userId + ",当前在线人数为:" + getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
* @param message 客户端发送过来的消息
**/
@OnMessage
public void onMessage(String message, Session session) {
log.info("用户消息:" + userId + ",报文:" + message);
//可以群发消息
//消息保存到数据库、redis
if (StringUtils.isNotBlank(message)) {
try {
//解析发送的报文
JSONObject jsonObject = JSON.parseObject(message);
//追加发送人(防止串改)
jsonObject.put("fromUserId", this.userId);
String toUserId = jsonObject.getString("toUserId");
//传送给对应toUserId用户的websocket
if (StringUtils.isNotBlank(toUserId) && webSocketMap.containsKey(toUserId)) {
webSocketMap.get(toUserId).sendMessage(message);
} else {
//否则不在这个服务器上,发送到mysql或者redis
log.error("请求的userId:" + toUserId + "不在该服务器上");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("用户错误:" + this.userId + ",原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) {
try {
this.session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 发送自定义消息
**/
public static void sendInfo(String message, String userId) {
log.info("发送消息到:" + userId + ",报文:" + message);
if (StringUtils.isNotBlank(userId) && webSocketMap.containsKey(userId)) {
webSocketMap.get(userId).sendMessage(message);
} else {
log.error("用户" + userId + ",不在线!");
}
}
/**
* 获得此时的在线人数
*
* @return
*/
public static synchronized int getOnlineCount() {
return onlineCount;
}
/**
* 在线人数加1
*/
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount++;
}
/**
* 在线人数减1
*/
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount--;
}
}
Endpoint 实例在 WebSocket 握手时创建,并在客户端与服务端链接过程中有效,最后在链接关闭时结束。在 Endpoint 接口中明确定义了与其生命周期相关的方法, 规范实现者确保生命周期的各个阶段调用实例的相关方法,即以下注解:
@ServerEndpoint("/api/pushMessage/{userId}")
:前端通过此 URI 和后端交互,建立连接,userId
可以在onOpen()
方法的形参中获取@Component
不用说将此类交给 spring 管理@OnOpen
:websocket 建立连接的注解,前端触发上面 URI 时会进入此注解标注的方法@OnMessage
:收到前端传来的消息后执行的方法@OnClose
:顾名思义关闭连接,销毁 session
因为 WebSocket 是类似客户端服务端的形式(采用 ws 协议),那么这里的 WebSocketServer 其实就相当于一个 ws 协议的 Controller
上面的onOpen()
、onMessage()
和onClose()
方法并不是凭空产生的,有一个EndPoint
类,里面就是这三个方法,EndPoint
类中的方法接收的参数可以直接传入自己写的处理类的对应方法中。
sendMessage()
:调用 session 中的getBasicRemote()
方法,可以获得 ⌊向前端发信息的对象⌉
- 例如:
this.session.getBasicRemote().sendText(message);
最好拿到 session 后,存在类属性中,这样方便使用
获取 HttpSession
要想获取 HttpSession,需要编写一个类来继承 ServerEndpointConfig.Configurator
这个 ServerEndpointConfig 是一个接口,接口下面有一个 Configurator 类
然后,重写形参列表带有 request 对象的方法,偷过来重写自己的业务,当然,HttpResopnse 也可以搞到手
public class GetHttpSessionConfigurator extends ServerEndpointConfig.Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
// 将httpSession对象存储到配置对象中,使用HttpSession的字节码的名称来作为键
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
}
}
然后还需要在你 websocket 处理类的onOpen()
中获取出来,还需要在注解 @ServerEndpoint 中配置配置类
@Component
@Slf4j
@ServerEndpoint(value = "/api/pushMessage/{userId}", configurator = GetHttpSessionConfigurator.class)
public class WebSocketServer {
// ...
// 与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
// 可以获取httpSession,可以干很多事,比如获取你登录时存的用户名
private HttpSession httpSession;
@OnOpen
public void onOpen(Session session, EndpointConfig config) {
this.session = session;
HttpSession httpSession =
(HttpSession) config.getUserProperties().get(HttpSession.class.getName());
this.httpSession = httpSession;
// ...
}
// ...
}
注意,这里onOpen()
传入的 Session 其实不是 HttpSession,而是当前 websocket 会话
在 websocket 处理类中注入 server
spring 维护的是单实例,websocket 为多实例,每次请求建立链接的时候,都会生成一个新的 bean
1. 将需要注入的 bean 设置成类的静态变量
@Component
@Slf4j
@ServerEndpoint("/ws/room/{roomId}")
public class RoomWebSocket {
public static DishService dishService;
//...
}
2. 在配置类自动装配且赋值
@Configuration
public class WebSocketConfig {
@Autowired
public void setWebSocketBean(DishService dishService){
RoomWebSocket.dishService = dishService;
}
}
参考
https://blog.csdn.net/qq_31960623/article/details/114131424
https://www.bilibili.com/video/BV1r54y1D72U?p=5&vd_source=187acdaeb8d8fd433241093430df9e48
http://t.zoukankan.com/guduershi-p-13731568.html
https://blog.51cto.com/dba10g/1855978