最近在准备春招项目时碰到了一个问题,在自己的项目中想实现聊天的功能,传统的http请求轮询对服务器资源太浪费,于是采用websocket。
websocket通信需要依靠javax.websocket.Session的session.getBasicRemote().sendText()方法发送消息,但websocket session 并未实现 Serializable 接口,无法进行序列化。所以想靠分布式缓存实现websocket session共享走不通了。
在一番查阅网上方案自己反复推敲后总结出了四种可用方案
1、MQ广播,每个推送服务监听一个消息队列,优点占用内存资源少,缺点浪费网络计算资源多
2、MQ中direct模式,每个用户一个队列,在websocket时监听,推送消息时根据userid推送到指定队列,缺点占用内存多,优点网络计算资源少
3、一致性hash,通过一致性hash计算用户的websocket在哪一个服务器是,在网关进行一致性hash算法路由
4、缓存状态,在缓存中保存用户连接的websocket所在的服务器IP,推送消息时找到消息接收用户所在的服务器通过restful接口推送。
MQ广播实现简单小流量使用,代码也可以跑起来,但每一条消息都要发送到每一个服务器,再由服务器判断这个消息要推送的用户在不在自己身上。
MQ的direct通过routingkey选择发送给应接收消息的队列即可,实现也较为简单,本项目中使用该方案。
一致性hash,在本项目中使用了SpringCloudGateway,如果要采用该方案要实现一致性hash算法,先根据websocket的服务地址计算hash值再对2^32取余保存下来,再来的请求中对用户请求的ID计算hash值取余确定对应的服务器。如果由服务器掉线则会有部分用户断连,在前端重新建立连接即可。发送消息时从网关获取Hash环本地对要发送到用户的用户ID计算应推送到哪个服务器IP,该方案不用MQ实现推送,由网关负载均衡时找到应推送用户所在的服务器。
缓存状态,该方案需要分布式缓存支持,用户建立websocket连接时将该服务器ip与自身id记录在缓存中,对要推送的用户现在缓存中查找其websocket所在ip通过restapi推送消息。
websocket服务器的搭建参考该文章SpringBoot2.0集成WebSocket,实现后台向前端推送信息_★【World Of Moshow 郑锴】★-CSDN博客_springboot集成websocket
MQ依赖
org.springframework.boot
spring-boot-starter-amqp
2.6.3
我的方案是消息发送时调用一个消息发送接口这个接口中先做消息的持久化,再通过MQ推送到交换机中(指定routingkey),使用RabbitTemplate发送消息。(消息的产生)
rabbitTemplate.convertAndSend("交换机名", "routingkey", JSON.toJSONString(message));
对于每一个用户登录时建立websocket连接,同时监听自己id的队列(如果没有则动态创建,创建的队列名与路由key即用户id,可以加前缀从语义上得知该队列含义比如user10),登陆后则可从监听的队列中取出通过websocket将消息推送用户(前端)。
在上述文章中添加
Channel channel; private static RabbitTemplate rabbitTemplate;
在websocket建立连接时调用该方法。
//创建监听队列
public void createQueue(Long userId){
channel = rabbitTemplate.getConnectionFactory().createConnection().createChannel(true); try{
/** * 设置队列名称,持久队列 */
String queueName = "user"+userId;
channel.queueDeclare(queueName,true,false,false,null);
/** * 关联 exchange 和 queue */
channel.queueBind(queueName, exchange, queueName);
Consumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
if(StringUtils.isEmpty(message)){
return;
}
/** * 对信息做操作 */
log.info(message);
if(webSocketMap.containsKey(String.valueOf(userId))){
//推送消息
webSocketMap.get(String.valueOf(userId)).sendMessage(message);
}else{
log.error("请求的userId:"+userId+"不在该服务器上");
}
}
};
//true 自动回复ack
channel.basicConsume(queueName, true, consumer);
}catch (Exception ex){
}
}
至此,分布式websocket的MQ direct解决方案实现。
基于该方案可以自己实现一个Java后端的QQ或者微信等通信软件,也可以在web项目中实现服务端的主动消息推送。