一 基本思路
基于websocketHandler实现平台websocket的统一封装
- 客户端发起长链接时会携带token凭证,服务端根据token获得用户基本信息,在与服务端握手前,先通过UserHandshakeInterceptor拦截请求,将用户的基本信息保存到当前会话中
- 通过MapSessionHandlerDecorator在建立连接前后通过SessionKeyGenerate解析用户基本信息和会话id同步数据到内存和内存中数据的释放,用服务端想客户端推送消息
- 通过CustomTextWebSocketHandler处理客户端的推送过来的消息,首先,会更具当前消息的类型选择相应的文本解析器
- 普通文本解析器,打印当前数据,不做数据的深加工
- json文本解析器根据当前type获得相应的消息处理器,执行自定义业务逻辑
服务端消息推送
服务端会更具当前项目的性质选择不同的消息发射器:
- local:单体项目,直接推送
- redis:分布式集群,通过redis的发布订阅实现消息的转发,分布式集群中监听到消息推送实现后判断当前服务中是否包含相应的session在推送到数据的客户端
二 核心代码实现
2.1 消息处理
定义相应配置约束
@Data
@ConfigurationProperties(prefix = XlcpWebSocketProperties.PRE_FIX)
public class XlcpWebSocketProperties {
public static final String PRE_FIX = "config.websocket";
/**
* websocket请求路径
*/
private Set<String> paths = new HashSet<String>(Arrays.asList("/ws/info"));
private String allowedOrigins = "*";
/**
* 是否开启心跳
*/
private Boolean heart=true;
/**
* <b>消息推送类型</b>
* <ul>
* <li>local:单机部署时使用</li>
* <li>redis:分布式集群时使用</li>
* <li>custom:自定义消息发送器</li>
* </ul>
*/
private String distributorType = DistributorType.REDIS;
}
握手前将用户信息保存到当前会话中
@Slf4j
public class UserHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
//获得用户的基础信息 保存到当前会话的信息中去
XlcpUser xlcpUser = SecurityUtils.getUser();
attributes.put(CommonConstants.WEBSOCKET_USER_INFO,xlcpUser);
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
log.debug("握手后置处理....");
}
}
用户基础信息,会话等的处理
public class MapSessionHandlerDecorator extends WebSocketHandlerDecorator {
private final SessionKeyGenerate sessionKeyGenerate;
public MapSessionHandlerDecorator(WebSocketHandler delegate, SessionKeyGenerate sessionKeyGenerate) {
super(delegate);
this.sessionKeyGenerate=sessionKeyGenerate;
}
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String sessionKey = sessionKeyGenerate.sessionKey(session);
if (StrUtil.isNotBlank(sessionKey)){
MapSessionHolder.addSession(sessionKey,session);
}
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
String sessionKey = sessionKeyGenerate.sessionKey(session);
if (StrUtil.isNotBlank(sessionKey)){
MapSessionHolder.removeSession(sessionKey);
}
}
}
public interface SessionKeyGenerate {
/**
* 构造websocketSession的存储key值
* @param webSocketSession
* @return
*/
String sessionKey(WebSocketSession webSocketSession);
}
public class WebSocketSessionKeyGenerate implements SessionKeyGenerate {
@Override
public String sessionKey(WebSocketSession webSocketSession) {
Map<String, Object> attributes = webSocketSession.getAttributes();
Object obj = attributes.get(CommonConstants.WEBSOCKET_USER_INFO);
if (obj instanceof XlcpUser){
XlcpUser xlcpUser = (XlcpUser) obj;
return String.format("%d%s%s",xlcpUser.getId(), StrPool.COLON,webSocketSession.getId());
}
return null;
}
}
public class MapSessionHolder {
public static Map<String, WebSocketSession> MAP_SESSION = new ConcurrentHashMap<>();
public static void addSession(String sessionKey,WebSocketSession webSocketSession){
MAP_SESSION.put(sessionKey,webSocketSession);
}
public static void removeSession(String sessionKey){
MAP_SESSION.remove(sessionKey);
}
public static Set<WebSocketSession> getWebSocketSessionBySessionKey(Object sessionkey){
HashSet<WebSocketSession> webSocketSessions = new HashSet<>();
MAP_SESSION.forEach((sessionKey,websocketSession)->{
if (StrUtil.startWith(sessionKey, Convert.toStr(sessionKey))){
webSocketSessions.add(websocketSession);
}
});
return webSocketSessions;
}
}
核心handler 处理客户端消息
@Slf4j
public class CustomTextWebSocketHandler extends TextWebSocketHandler {
private final PlanTextMessageHandler planTextMessageHandler;
public CustomTextWebSocketHandler(PlanTextMessageHandler planTextMessageHandler){
this.planTextMessageHandler=planTextMessageHandler;
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
if (message.getPayloadLength()==0){
return;
}
String messageText = message.getPayload();
// 判断是否为json数据处理文本
if (JSONUtil.isTypeJSON(messageText)){
boolean checkResult = JsonCheckUtil.checkJson(messageText);
if (!checkResult){
checkErrorMessage(session);
return;
}
/**
* json文本处理器
*/
JSONObject jsonObject = JSONUtil.parseObj(messageText);
String type = (String) jsonObject.get(AbstractJsonWebSocketMessage.TYPE_FIELD);
if (StrUtil.isNotBlank(type)){
JsonWebSocketMessageHandler jsonWebSocketMessageHandler = JsonWebSocketMessageHandlerRegister.MESSAGE_HANDLERS.get(type);
if (jsonWebSocketMessageHandler==null){
String jsonWebSocketMessage = JsonWebSocketMessageBuilder.getInstance()
.msg("没有匹配到相对应的type类型")
.type(JsonMessageType.ERROR)
.build();
WebSocketMessageSender.sendMessage(session,jsonWebSocketMessage);
return;
}
Class messageClass = jsonWebSocketMessageHandler.getMessageClass();
Object jsonMessage = JSONUtil.toBean(messageText, messageClass);
jsonWebSocketMessageHandler.handler((AbstractJsonWebSocketMessage) jsonMessage,session);
}else {
checkErrorMessage(session);
}
}else {
// 普通文本处理器
planTextMessageHandler.handler(session,messageText);
}
}
private void checkErrorMessage(WebSocketSession session) {
String jsonWebSocketMessage = JsonWebSocketMessageBuilder.getInstance()
.msg("json 数据格式错误!")
.type(JsonMessageType.ERROR)
.build();
WebSocketMessageSender.sendMessage(session,jsonWebSocketMessage);
}
}
public class JsonWebSocketMessageBuilder {
@Getter
private String type;
@Getter
private String msg;
@Getter
private Object data;
public static JsonWebSocketMessageBuilder getInstance(){
return new JsonWebSocketMessageBuilder();
}
public JsonWebSocketMessageBuilder type(JsonMessageType jsonMessageType){
this.type=jsonMessageType.getType();
return this;
}
public JsonWebSocketMessageBuilder msg(String msg){
this.msg=msg;
return this;
}
public JsonWebSocketMessageBuilder data(Object data){
this.data=data;
return this;
}
public String build(){
Assert.notBlank(this.type);
return JSONUtil.toJsonStr(this);
}
}
public enum JsonMessageType {
PONG("pong","pong消息"),
ERROR("error","错误"),
PING("ping","心跳");
@Getter
private String type;
private String describe;
JsonMessageType(String type,String describe){
this.type=type;
this.describe=describe;
}
}
@Slf4j
public class CustomPlanTextMessageHandler implements PlanTextMessageHandler{
@Override
public void handler(WebSocketSession session, String message) {
log.info("messageId [{}] message [{}]",session.getId(),message);
}
}
消息处理器
public interface JsonWebSocketMessageHandler<T extends AbstractJsonWebSocketMessage> {
/**
* 处理消息
* @param t
* @param webSocketSession
*/
void handler(T t, WebSocketSession webSocketSession);
/**
* 获得当前处理器处理的消息的class
* @return
*/
Class<T> getMessageClass();
/**
* 当前消息处理器处理的类型
* @return
*/
String getType();
}
public abstract class AbstractJsonWebSocketMessage implements JsonWebSocketMessage {
public static final String TYPE_FIELD = "type";
@Getter
public String type;
public AbstractJsonWebSocketMessage(String type){
this.type=type;
}
@Override
public String getType() {
return this.type;
}
}
public interface JsonWebSocketMessage {
/**
* 当前消息的处理类型
* @return
*/
String getType();
}
心跳消息处理器
public class PingJsonWebSocketMessageHandler implements JsonWebSocketMessageHandler<PingJsonWebSocketMessage>{
@Override
public void handler(PingJsonWebSocketMessage pingJsonWebSocketMessage, WebSocketSession webSocketSession) {
// 接受到客户端的心跳 返回一个pong消息给客户端
String jsonMessage = JsonWebSocketMessageBuilder.getInstance().type(JsonMessageType.PONG).build();
WebSocketMessageSender.sendMessage(webSocketSession, jsonMessage);
}
@Override
public Class<PingJsonWebSocketMessage> getMessageClass() {
return PingJsonWebSocketMessage.class;
}
@Override
public String getType() {
return JsonMessageType.PING.getType();
}
}
public class PingJsonWebSocketMessage extends AbstractJsonWebSocketMessage{
public PingJsonWebSocketMessage(String type) {
super(JsonMessageType.PING.getType());
}
}
2.2 消息分发
定义消息分发的基础接口,及消息基础定义
public interface DistributorType {
/**
* 本地
*/
String LOCAL = "local";
/**
* redis 支持分布式
*/
String REDIS = "redis";
/**
* 自定义
*/
String CUSTOM = "custom";
}
public interface MessageDistributor {
/**
* 推送消息
* @param messageDto
*/
void distributor(MessageDto messageDto);
}
@Data
@Accessors(chain = true)
public class MessageDto {
/**
* 是否为广播消息
*/
private Boolean broadcastFlag = Boolean.FALSE;
/**
* sessionKeys
*/
private List<Object> sessionKeys;
/**
* 消息
*/
private String messageText;
/**
* 构建广播消息
* @param messageText
* @return
*/
public static MessageDto buildBroadcastMessage(String messageText){
return new MessageDto().setMessageText(messageText).setBroadcastFlag(true);
}
}
public interface MessageSender {
/**
* 发送消息
* @param messageDto
*/
default void doSend(MessageDto messageDto){
Boolean broadcastFlag = messageDto.getBroadcastFlag();
Assert.notBlank(messageDto.getMessageText());
if (broadcastFlag){
/**
* 广播模式
*/
WebSocketMessageSender.broadcastMessage(messageDto.getMessageText());
}else {
List<Object> sessionKeys = messageDto.getSessionKeys();
for (Object sessionKey : sessionKeys) {
Set<WebSocketSession> webSocketSessions = MapSessionHolder.getWebSocketSessionBySessionKey(sessionKey);
WebSocketMessageSender.sendMessageByWebSocketSessions(webSocketSessions, messageDto.getMessageText());
}
}
}
}
@UtilityClass
@Slf4j
public class WebSocketMessageSender {
public Boolean sendMessage(WebSocketSession webSocketSession,String message){
if (webSocketSession==null){
log.error("message send fail,websocketSession Can not be empty!");
return Boolean.FALSE;
}
if (!webSocketSession.isOpen()){
log.error("message send fail,websocketSession is closed");
return Boolean.FALSE;
}
try {
webSocketSession.sendMessage(new TextMessage(message));
} catch (IOException e) {
e.printStackTrace();
log.error("message send fail,the reason is {}",e.getMessage());
return Boolean.FALSE;
}
return Boolean.TRUE;
}
public void broadcastMessage(String message){
MapSessionHolder.MAP_SESSION.forEach((sessionKey,webSocketSession)->{
sendMessage(webSocketSession,message);
});
}
public void sendMessageByWebSocketSessions(Set<WebSocketSession> webSocketSessions, String message){
webSocketSessions.stream().forEach(webSocketSession -> sendMessage(webSocketSession,message));
}
}
本地消息发射器
public class LocalMessageDistributor implements MessageDistributor,MessageSender{
@Override
public void distributor(MessageDto messageDto) {
doSend(messageDto);
}
}
redis消息发射器
@RequiredArgsConstructor
public class RedisMessageDistributor implements MessageDistributor{
private final WebSocketRedisMessagePublisher publisher;
@Override
public void distributor(MessageDto messageDto) {
publisher.sendMsg(WebsocketMessageRedisReceiver.CHANNEL, JSONUtil.toJsonStr(messageDto));
}
}
@Slf4j
public class WebsocketMessageRedisReceiver implements MessageListener,MessageSender {
public static final String CHANNEL = "websocket-send";
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void onMessage(Message message, byte[] pattern) {
log.info("redis channel Listener message send {}", message);
byte[] channelBytes = message.getChannel();
RedisSerializer<String> stringSerializer = stringRedisTemplate.getStringSerializer();
String channel = stringSerializer.deserialize(channelBytes);
// 这里没有使用通配符,所以一定是true
if (CHANNEL.equals(channel)) {
byte[] bodyBytes = message.getBody();
String body = stringSerializer.deserialize(bodyBytes);
MessageDto messageDO = JSONUtil.toBean(body, MessageDto.class);
doSend(messageDO);
}
}
}
public class WebSocketRedisMessagePublisher {
@Autowired
private StringRedisTemplate stringRedisTemplate;
public void sendMsg(String channel, String msg){
stringRedisTemplate.convertAndSend(channel, msg);
}
}
2.3 核心配置
@Configuration
@EnableWebSocket
@EnableConfigurationProperties(XlcpWebSocketProperties.class)
@RequiredArgsConstructor
public class XlcpWebSocketAutoConfiguration implements InitializingBean {
private final List<JsonWebSocketMessageHandler> handlers;
@Autowired
private XlcpWebSocketProperties xlcpWebSocketProperties;
@Bean
@ConditionalOnMissingBean
public WebSocketConfigurer webSocketConfigurer(WebSocketHandler webSocketHandler, List<HandshakeInterceptor>handshakeInterceptors){
return registry -> {
Set<String> paths = xlcpWebSocketProperties.getPaths();
registry.addHandler(webSocketHandler,paths.toArray(new String[0]))
.setAllowedOrigins(xlcpWebSocketProperties.getAllowedOrigins())
.addInterceptors(handshakeInterceptors.toArray(new HandshakeInterceptor[0]));
};
}
@Override
public void afterPropertiesSet() throws Exception {
handlers.stream().forEach(handler->{
JsonWebSocketMessageHandlerRegister.register(handler.getType(),handler);
});
}
@Configuration
static class JsonMessageHandlerConfig{
/**
* 心跳处理配置
* @return
*/
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = XlcpWebSocketProperties.PRE_FIX,name = "heart",havingValue = "true",matchIfMissing = true)
public PingJsonWebSocketMessageHandler pingJsonWebSocketMessageHandler(){
return new PingJsonWebSocketMessageHandler();
}
@Bean
@ConditionalOnMissingBean
public PlanTextMessageHandler planTextMessageHandler(){
return new CustomPlanTextMessageHandler();
}
@Bean
public SessionKeyGenerate sessionKeyGenerate(){
return new WebSocketSessionKeyGenerate();
}
@Bean
@ConditionalOnMissingBean
public WebSocketHandler webSocketHandler(){
return new MapSessionHandlerDecorator(new CustomTextWebSocketHandler(planTextMessageHandler()),sessionKeyGenerate());
}
@Bean
@ConditionalOnMissingBean
public HandshakeInterceptor handshakeInterceptor(){
return new UserHandshakeInterceptor();
}
}
@Configuration
@ConditionalOnProperty(prefix = XlcpWebSocketProperties.PRE_FIX,
name = "distributor-type",
havingValue = DistributorType.LOCAL)
static class LocalMessageDistributorConfig{
@ConditionalOnMissingBean
@Bean
public MessageDistributor messageDistributor(){
return new LocalMessageDistributor();
}
}
@ConditionalOnProperty(prefix = XlcpWebSocketProperties.PRE_FIX,
name = "distributor-type",
havingValue = DistributorType.REDIS,
matchIfMissing = true)
@Configuration
static class RedisMessageDistributorConfig{
@Bean
@ConditionalOnMissingBean
public MessageDistributor messageDistributor(){
return new RedisMessageDistributor(webSocketRedisMessagePublisher());
}
@Bean
public WebsocketMessageRedisReceiver websocketMessageRedisReceiver(){
return new WebsocketMessageRedisReceiver();
}
@Bean
public WebSocketRedisMessagePublisher webSocketRedisMessagePublisher(){
return new WebSocketRedisMessagePublisher();
}
@Bean
public MessageListenerAdapter messageListenerAdapter(){
MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(websocketMessageRedisReceiver(),
"onMessage");
return messageListenerAdapter;
}
@Bean
public RedisMessageListenerContainer listenerContainer(RedisConnectionFactory factory,
MessageListenerAdapter messageListenerAdapter){
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
container.addMessageListener(messageListenerAdapter, new PatternTopic(WebsocketMessageRedisReceiver.CHANNEL));
return container;
}
}
}
3 组件的使用
3.1 pom中引入公用组件
<dependency>
<groupId>com.cncloud</groupId>
<artifactId>cncloud-common-websocket</artifactId>
</dependency>
3.2 创建handel及socket消息
-
创建消息拦截器
@Component public class InviteCallJsonMessageHandler extends AbstractJsonMessageHandler<InviteCallJsonWebSocketMessage> { @Override protected String doHandler(WebSocketSession webSocketSession, InviteCallJsonWebSocketMessage message) { // todo 业务处理 } @Override public String type() { // 提供消息类型 WebSocketMessageTypeEnum提供统一规范 return WebSocketMessageTypeEnum.INVITE_CALL.getValue(); } @Override public Class<InviteCallJsonWebSocketMessage> getMessageClass() { // 返回当前消息类型 return InviteCallJsonWebSocketMessage.class; } }
-
创建对应的消息结构
public class InviteCallJsonWebSocketMessage extends AbstractJsonWebSocketMessage { /** * 任务id */ private String taskId; protected InviteCallJsonWebSocketMessage(String type) { super(WebSocketMessageTypeEnum.INVITE_CALL.getValue()); } public InviteCallJsonWebSocketMessage(){ super(WebSocketMessageTypeEnum.INVITE_CALL.getValue()); } public String getTaskId() { return taskId; } public void setTaskId(String taskId) { this.taskId = taskId; } }
3.3 服务端向客户端推送消息
// websocket 发送消息
MessageDo messageDo = new MessageDo();
messageDo.setNeedBroadcast(Boolean.FALSE);
// 接受用户id
messageDo.setSessionKeys(CollUtil.newArrayList(sendId));
messageDo.setMessageText(JSONUtil.toJsonStr(response));
// 通过redis广播消息 订阅者推送消息
messageDistributor.distribute(messageDo);