Spring集成WebSocket通信,借助ignite消息机制
需求说明:
- 要求server端能与客户端实时通信,有新的消息实时推送。服务器将来有多活的需求
- 缓存所有历史未读取消息,用户可以去主动查询历史未读取的消息
方案:
- websocket通信 + ignite缓存
- 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