Spring集成WebSocket通信,借助ignite消息机制

需求说明:

  1. 要求server端能与客户端实时通信,有新的消息实时推送。服务器将来有多活的需求
  2. 缓存所有历史未读取消息,用户可以去主动查询历史未读取的消息

方案:

  1. websocket通信 + ignite缓存
  2. ignite缓存

Ignite消息和事件的背景知识

Ignite 消息和事件: https://blog.csdn.net/qq_31179577/article/details/74011580
该博文对ignite消息和事件机制进行了概述,同时讲述了各个API的使用。
ignite消息通知是基于主题订阅式的,向指定主题发布消息之后,所有主题监听者都能够立即获取到消息并根据消息进行后续处理。
消息是一个String类型,不适合复杂的数据结构。如果需要查询存储在缓存中的复杂数据结构时,可以定义推送消息的格式(用Json的key、value形式存储),将缓存的id包含进去,然后拿到消息时解析缓存ID,从而根据ID去缓存中取到复杂的POJO。其他方式也可以放入存储在数据库中的数据表以及表中存放的ID。

项目代码

WebSocketConfig

@Configuration
public class WebSocketConfig {
    
    
    // 只是为Controller中注入Service
    @Autowired
    public void setWebSocketServer(WebSocketService WebSocketService ) {
    
    
        WebSocketController.WebSocketService = WebSocketService;
    }
}

WebSocketController

用于控制WebSocket建立、关闭时的操作
请求的URL如下:
wss://IP:Port/domain/notice/user1

@ServerEndpoint("/notice/{username}")
@Component
public class WebSocketController {
    
    
    public static WebSocketService webSocketService;
	// 生成的WebSocket信息保存在缓存中,这里存储该用户这个websocket在缓存中存的独一无二的ID标志
    private Long id;
    private Session session;
	// 连接成功时的操作,这里是将用户的session、生成的websocket信息存入缓存中
	// 缓存中也维护了WebSocketController实例,在用户断掉连接时从缓存中注销掉该Controller,用来管理实时存活的Controller对象
    @OnOpen
    public void onOpen(Session sessionObj, @PathParam("username") String userName) {
    
    
        this.session = sessionObj;
        webSocketCommon.addWebSocket(this, session, userName);
    }

    // 移除缓存中的controller实例(移除session)
    @OnClose
    public void onClose(Session sessionObj) {
    
    
        webSocketCommon.removeWebSocket(sessionObj);
    }

    @OnMessage
    public void onMessage(String message, Session sessionObj) {
    
    
        logger.info("Received a message from the client, message {}", message);
    }
    
    @OnError
    public void onError(Throwable error) {
    
    
        logger.error("Websocket error! {}", error.getMessage());
    }
}

WebSocketService

通过Ignite存储所有的WebSocket的session,管理这些Session并通过具体session发送消息
而发送的消息也存储在Ignite中,存储在另一个缓存中,见下方Ignite配置

@Service
public class WebSocketService {
    
    
    // 获取Ignite缓存实例
    private static WebSocketRepository webSocketRepository = ...Context.getBean(WebSocketRepository.class);
	// 使用Ignite作为缓存
    private static Ignite ignite = ...Context.getBean(Ignite.class);
	// 生成独一无二的ID,从0生成
	// Ignite缓存的POJO中存在ID字段,该ID要保证唯一性,从而能够使用SQL语句根据ID去ignite缓存中取得对应的缓存信息
	// 参考博客:https://blog.csdn.net/weixin_33881140/article/details/91686339
    private static final IgniteAtomicSequence NOTICE_SEQUENCE = ignite.atomicSequence("webSocketSequence", 0, true);
    /**
     * 缓存所有的websocket连接,用于为所有的websocket连接推送消息,websocket辅助信息存入ignite中,通过id进行关联
     */
    private static ConcurrentHashMap<String, WebSocketController> websocketList = new ConcurrentHashMap<>();

    public void addWebSocket(WebSocketController webSocketController, Session session, String username) {
    
    
        WebSocketInfo webSocket = new WebSocketInfo();
        Long id = NOTICE_SEQUENCE.incrementAndGet();
        webSocket.setId(id);
        webSocket.setSessionId(session.getId());
        webSocket.setUsername(username);
        webSocketController.setId(id);
		// 管理所有的WebSocketController
        websocketList.put(session.getId(), webSocketController);
        // Websocket信息存入ignite缓存
        webSocketRepository.save(webSocket.getId(), webSocket);
    }

    public void removeWebSocket(Session session) {
    
    
        String sessionId = session.getId();
        webSocketRepository.deleteById(webSocketController.getId());
        websocketList.remove(sessionId);
    }
    
    public void sendMessageToUser(String message, String username) {
    
    
        List<WebSocketInfo> webSockets = webSocketRepository.findByUsername(username);
        if (webSockets == null || webSockets.isEmpty()) {
    
    
            return;
        }
        sendMessage(webSockets, message);
    }
    
    private void sendMessage(Iterable<WebSocketInfo> webSockets, String message) {
    
    
        try {
    
    
            for (WebSocketInfo item : webSockets) {
    
    
                if (websocketList.containsKey(item.getSessionId())) {
    
    
                    Session session = websocketList.get(item.getSessionId()).getSession();
                    if (session.isOpen()) {
    
    
                    	// 通过session发送消息
                        session.getBasicRemote().sendText(message);
                    }
                } 
            }
        } catch (IOException e) {
    
    
            logger.error("Websocket message send error. error: {}", e.getMessage());
        }
    }
}

Ignite作为消息缓存的配置

消息监听与推送配置:

@Component
// Ignite监听指定topic的消息并发送给各个监听者
public class IgniteMessageReceiver implements ApplicationRunner {
    
    
// 跟随系统启动
// ApplicationRunner参见: https://blog.csdn.net/mqdxiaoxiao/article/details/108148600
    private static Ignite ignite = ...Context.getBean(Ignite.class);

    @Autowired
    private WebSocketService webSocketService;
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
    
    
        IgniteMessaging receivedMessage = ignite.message(ignite.cluster().forRemotes());
		// 监听某个主题消息并推送给所有注册的node,借用ignite的多个node,实现了分布式
		// 这里是只在本地节点进行监听,暂时不涉及多活
        receivedMessage.localListen(“ListenTopic”, (igniteNodeId, message) -> {
    
    
            try {
    
    
            	// 解析ignite中存储的message
                JSONObject json = JSONObject.parseObject(String.valueOf(message));
                if (json.containsKey("username") && json.getString("username") != null && json.containsKey("cacheId")
                    && json.getString("cacheId") != null) {
    
    
                    // 只有缓存的消息中包含用户名与cacheID时才会发送消息出去
                    sendMessage(json);
                }
            } catch (Exception e) {
    
    
            }
            return true; // Return true to continue listening.
        });
    }

    @Retryable(value = OperationException.class, backoff = @Backoff(delay = 1000L))
    // 重试机制
    private void sendMessage(JSONObject json) {
    
    
        String username = json.getString("username");
        String cacheId = json.getString("cacheId");
       webSocketService .sendMessageToUser(JSONObject.toJSON(noticeOptional.get()).toString(), username);
    }

    // 从Ignite中取数据,构造SQL取
    public Optional<PortalNotice> queryNoticeById(String cacheId) {
    
    
        List<PortalNotice> result = new ArrayList<>();
        IgniteCache<String, PortalNotice> cache = ignite.getOrCreateCache(
            new CacheConfiguration(ExternalConfig.PORTAL_NOTICE_CACHE));

        ……
    }
}

Websocket的存储Repository配置:

@RepositoryConfig(cacheName = "websocketSetCache")
public interface WebSocketRepository extends IgniteRepository<WebSocketInfo, Long> {
    
    
    List<WebSocketInfo> find(String username);
}

Notice的存储配置:

@RepositoryConfig(cacheName = "NoticeCache")
public interface MyNoticeRepository extends IgniteRepository<MyNotice, Long> {
    
    
    List<PortalNotice> findNotice(String username);
}

其他Spring
集成Ignite的配置可以参见博文:
https://blog.csdn.net/ltl112358/article/details/79399026

ignite官方参考链接:
http://ignite-service.cn/doc/java/Installation.html#_1-%E4%BD%BF%E7%94%A8zip%E5%8E%8B%E7%BC%A9%E5%8C%85%E5%AE%89%E8%A3%85

MessageController

用于通过URL访问用户个人的消息

@RestController
@RequestMapping("/message")
public class MessageController {
    
    
    @Autowired
    private MessageService messageService;

    @GetMapping("/list")
    public ResponseEntity getNotices() {
    
    
        String currentUsername =  ..Context.getCurrentUsername();
        return new ResponseEntity(messageService.getNotices(currentUsername), HttpStatus.OK);
    }
    
    @PostMapping("/remove")
    public ResponseEntity deleteNotices(String noticeIds) {
    
    
        messageService.deleteByKeys(noticeIds);
        return new ResponseEntity(Constants.RESPONSE_SUCCESS, HttpStatus.OK);
    }
}

MessageService

用于MyNotice的处理:存入缓存等

@Service
public class MessageService {
    
    
    private Ignite ignite = SpringContext.getBean("igniteInstance");

    private final IgniteAtomicSequence NOTICE_SEQ = ignite.atomicSequence("NoticeSeq", 0, true);

    @Autowired
    private  MyNoticeRepository myNoticeRepository;

	// 从ignite中取出所有的notice
    public List<PortalNotice> getNotices(String username) {
    
    
        return portalNoticeRepository.findNoticeByUsername(username);
    }

   
    public void deleteByKeys(List<Long> ids) {
    
    
        List<PortalNotice> lists = getNotices(currentUsername);
        portalNoticeRepository.deleteAllById(ids);
    }

    public Map<Long, PortalNotice> writeNotice(String username, MyNotice myNotice) {
    
    
        Map<Long, PortalNotice> map = new HashMap<>();
        map.put(myNotice.getId(), myNotice);
        portalNoticeRepository.save(map); // 这是已经实现的接口
        return map;
    }

    // 通过websocket发送消息出去
    public void sendNoticeMessage(MyNotice notice) {
    
    
        JSONObject json = new JSONObject();
        try {
    
    
            json.put("username", notice.getUsername());
            // 从中可以看出实际上发送的不是消息实体,而是消息的id,之后从缓存中取出消息实体
            json.put("cacheId", notice.getId());
            IgniteMessaging sendMessage = ignite.message();
            sendMessage.sendOrdered(“ListenTopic”, json.toString());
        } catch (Exception e) {
    
    
        }
    }
}

其他参考

SpringBoot集成WebSocket的四种方式

https://www.cnblogs.com/kiwifly/p/11729304.html
本文采用的是文中的第一种原生方式,其他三种方式可以参考

Websocket + STOMP系列文章

https://juejin.cn/post/6844903655221493774
讲述了Websocket的由来、协议、报文以及一个完整的STOMP的demo

STOMP:Spring + STOMP + RabbitMQ:使用STOMP消息,启用STOMP代理中继

https://blog.csdn.net/hefrankeleyn/article/details/89763744
一个使用RabbitMQ作为消息代理的demo

猜你喜欢

转载自blog.csdn.net/weixin_38370441/article/details/113995310