简介
WebSocket是一种在单个TCP连接上进行全双工通信的协议,由IETF(Internet Engineering Task Force,互联网工程任务组)在2011年定为标准RFC 6455,并由RFC7936补充规范。WebSocket API也被W3C(万维网联盟)定为标准。WebSocket协议允许服务端主动向客户端推送数据,从而实现了客户端和服务器之间的实时数据交换。
背景
积累知识。
教程
一、基础配置
1、pom依赖
<!--跟随springboot版本,这里使用的是2.2.2.RELEASE-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!--代码简化工具-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<!--Hutool工具-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.22</version>
</dependency>
<!--阿里json工具库-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.38</version>
</dependency>
2、配置类
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
/**
* @Author ClancyLv
* @Date 2024/7/26 9:12
* @Description 配置类--WebSocket配置
*/
@RequiredArgsConstructor
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final WebSocketHandler webSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
// 注册WebSocket处理器,拦截器 此时socket链接:ws://localhost:port/
webSocketHandlerRegistry.addHandler(webSocketHandler, "/")
.addInterceptors(new WebSocketInterceptor())
.setAllowedOrigins("*");
}
/**
* web socket 缓冲区大小配置
*/
@Bean
public ServletServerContainerFactoryBean servletServerContainerFactoryBean() {
ServletServerContainerFactoryBean factory = new ServletServerContainerFactoryBean();
factory.setMaxBinaryMessageBufferSize(1024 * 1024);
factory.setMaxTextMessageBufferSize(1024 * 1024);
factory.setMaxSessionIdleTimeout(30 * 60000L);
return factory;
}
}
3、拦截器
import cn.hutool.core.util.StrUtil;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import javax.servlet.http.HttpServletRequest;
import java.util.Map;
/**
* @Author ClancyLv
* @Date 2024/7/30 9:49
* @Description 拦截器--WebSocket拦截器
*/
public class WebSocketInterceptor implements HandshakeInterceptor {
// 处理前置操作
// 这里主要是为了记录用户身份。
// 提高安全性方式:对socket链接进行鉴权
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) {
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest serverHttpRequest = (ServletServerHttpRequest) request;
HttpServletRequest httpServletRequest = serverHttpRequest.getServletRequest();
// 获取用户id
String userId = httpServletRequest.getParameter("userId");
if (StrUtil.isNotBlank(userId)) {
map.put("userId", Long.valueOf(userId));
}
// 获取任务id
String taskId = httpServletRequest.getParameter("taskId");
if (StrUtil.isNotBlank(taskId)) {
map.put("taskId", taskId);
}
return true;
}
return false;
}
// 处理后置操作
@Override
public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {
}
}
4、处理器
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONException;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketMessage;
import org.springframework.web.socket.WebSocketSession;
import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
/**
* @Author ClancyLv
* @Date 2024/7/30 9:23
* @Description 处理器--WebSocket处理器
*/
@Slf4j
@Component
public class WebSocketHandler implements org.springframework.web.socket.WebSocketHandler {
// 首次连接回调
@Override
public void afterConnectionEstablished(WebSocketSession webSocketSession) {
// 保存此次链接
WebSocketManager.addSession(webSocketSession);
log.info("与用户:{}建立连接", webSocketSession.getAttributes().get("userId"));
}
// 收到消息回调
@Override
public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage<?> webSocketMessage) throws Exception {
String userId = webSocketSession.getAttributes().get("userId").toString();
if (webSocketMessage instanceof TextMessage) {
String message = ((TextMessage) webSocketMessage).getPayload();
log.info("来自用户:{}的消息:{}", userId, webSocketMessage.getPayload());
WebSocketDto webSocketDto;
try {
webSocketDto = JSON.parseObject(message, WebSocketDto.class);
switch (webSocketDto.getType()) {
case 10000: // websocket连接
break;
case 10010: // 客户端发送心跳 PING
break;
// 其他业务类型
// ...
// 其他业务类型
default:
log.warn("用户:{}未知消息类型:{}", userId, webSocketDto.getType());
break;
}
} catch (JSONException e) {
log.warn("用户:{}JSON解析异常:{}\n{}", userId, e.getMessage(), e);
} catch (NullPointerException e) {
log.warn("用户:{}空指针异常:{}\n{}", userId, e.getMessage(), e);
} catch (Exception e) {
log.warn("用户:{}未知异常:{}\n{}", userId, e.getMessage(), e);
}
}
}
// 错误信息回调
@Override
public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) {
log.error("用户:{}的WebSocket发生错误:{}", webSocketSession.getAttributes().get("userId"), throwable.getMessage());
}
// 连接关闭回调
@Override
public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) {
// 清除链接
WebSocketManager.removeSession(webSocketSession);
log.info("用户:{}的WebSocket连接关闭:{}!", webSocketSession.getAttributes().get("userId"), closeStatus.toString());
}
@Override
public boolean supportsPartialMessages() {
return false;
}
}
5、管理器
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import java.io.IOException;
import java.util.concurrent.ConcurrentHashMap;
/**
* @Author ClancyLv
* @Date 2024/7/26 9:15
* @Description WebSocket管理器
*/
@Slf4j
public class WebSocketManager {
// key:sessionId value:WebSocketSession
private final static ConcurrentHashMap<String, WebSocketSession> sessionIdMap = new ConcurrentHashMap<>();
// 业务需要向指定用户推送消息 key:用户id value:WebSocketSession
public final static ConcurrentHashMap<Long, WebSocketSession> userIdMap = new ConcurrentHashMap<>();
// 存储WebSocketSession
public static void addSession(WebSocketSession webSocketSession){
if (webSocketSession != null){
sessionIdMap.put(webSocketSession.getId(), webSocketSession);
userIdMap.put((Long) webSocketSession.getAttributes().get("userId"), webSocketSession);
}
}
// 清除WebSocketSession
public static void removeSession(WebSocketSession webSocketSession){
try {
if (webSocketSession != null) {
String sessionId = webSocketSession.getId();
sessionIdMap.remove(sessionId);
userIdMap.remove((Long) webSocketSession.getAttributes().get("userId"));
webSocketSession.close();
}
} catch (IOException e) {
log.error("WebSocket关闭异常:{}\n{}", e.getMessage(), e);
}
}
/**
* 通过SessionId发送消息给特定用户
* @param sessionId 会话id
* @param msg 消息
*/
public static void sendMsgBySessionId(String sessionId, String msg){
WebSocketSession webSocketSession = sessionIdMap.get(sessionId);
if (webSocketSession == null){
log.error("Session不存在,无法发送消息!");
return;
}
sendMsg(webSocketSession, msg);
}
/**
* 通过用户id发送消息给特定用户
* @param userId 用户id
* @param msg 消息
*/
public static void sendMsgByUserId(Long userId, String msg){
WebSocketSession webSocketSession = userIdMap.get(userId);
if (webSocketSession == null){
log.error("Session不存在,无法发送消息!");
return;
}
sendMsg(webSocketSession, msg);
}
/**
* 通过Session发送消息给特定用户
* @param webSocketSession 会话
* @param msg 消息
*/
public static void sendMsg(WebSocketSession webSocketSession, String msg){
try {
if (webSocketSession == null) {
log.error("不存在该Session,无法发送消息!");
return;
}
if (msg == null) {
log.error("消息不能为位空");
return;
}
webSocketSession.sendMessage(new TextMessage(msg));
} catch (IOException e) {
log.error("WebSocket发送消息异常:{}\n{}", e.getMessage(), e);
}
}
/**
* 通过Session发送消息给特定用户
* @param webSocketSession 会话
* @param msg 消息
*/
public static void sendMsg(WebSocketSession webSocketSession, byte[] msg){
try {
if (webSocketSession == null) {
log.error("不存在该Session,无法发送消息!");
return;
}
if (msg == null) {
log.error("消息不能为位空");
return;
}
webSocketSession.sendMessage(new TextMessage(msg));
} catch (IOException e) {
log.error("WebSocket发送消息异常:{}\n{}", e.getMessage(), e);
}
}
}
6、数据传输类
package com.lyc.naza.common.websocket;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @Author ClancyLv
* @Date 2024/7/26 14:36
* @Description 数据传输类--WebSocket数据传输
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class WebSocketDto {
// 业务类型已定义成枚举类,可自行编写
/**
* 传输类型
* 10000:websocket连接
* 10001:websocket连接成功
* 10002:websocket断开连接
* 10010:PING
* 10011:PONG
*/
private Integer type;
/**
* 传输数据
*/
private String data;
}