背景
我这边自己的项目目前情况是springboot2.0.3版本,默认最高支持的springcloud版本Finchley.RELEASE,最开始学习的时候路由这一块用的zuul,导致天然不适配websocket,虽然网上方法很多,但是我试了没有一个行的,包括官方github下有人提供的一个support,但是我这边连demo都运行不了,试了下也不行,就不浪费时间去做了,毕竟最后终端路由还是会去用ningx,做前后分离。因此考虑将websocket服务单独拿出来,脱离zuul的路由。
思考
网上的websocket集群大多都是面向于点对点发送的,而我希望的是广播,之前想了下,想了个方法。 1、使用rabbitmq的topic或者fanout模式,上一篇我用了direct模式。topic模式我认为就是在fanout模式基础上加了个routekey的功能,包括springcloudbus做config的refresh用的也是fanout模式,只是routekey使用了#,网上有人说topic快,那就快吧,毕竟我这边不用做特定用户的发送(下一篇讲topic模式吧).同时附上下官方的代码,看了我好久,也是学习了。(elipse找代码真的累!)
@Override
public ConsumerDestination provisionConsumerDestination(String name, String group,
ExtendedConsumerProperties<RabbitConsumerProperties> properties) {
boolean anonymous = !StringUtils.hasText(group);
String baseQueueName = anonymous ? groupedName(name, ANONYMOUS_GROUP_NAME_GENERATOR.generateName())
: properties.getExtension().isQueueNameGroupOnly() ? group : groupedName(name, group);
if (this.logger.isInfoEnabled()) {
this.logger.info("declaring queue for inbound: " + baseQueueName + ", bound to: " + name);
}
String prefix = properties.getExtension().getPrefix();
final String exchangeName = applyPrefix(prefix, name);
Exchange exchange = buildExchange(properties.getExtension(), exchangeName);
if (properties.getExtension().isDeclareExchange()) {
declareExchange(exchangeName, exchange);
}
String queueName = applyPrefix(prefix, baseQueueName);
boolean partitioned = !anonymous && properties.isPartitioned();
boolean durable = !anonymous && properties.getExtension().isDurableSubscription();
Queue queue;
if (anonymous) {
queue = new Queue(queueName, false, true, true, queueArgs(queueName, properties.getExtension(), false));
}
else {
if (partitioned) {
String partitionSuffix = "-" + properties.getInstanceIndex();
queueName += partitionSuffix;
}
if (durable) {
queue = new Queue(queueName, true, false, false,
queueArgs(queueName, properties.getExtension(), false));
}
else {
queue = new Queue(queueName, false, false, true,
queueArgs(queueName, properties.getExtension(), false));
}
}
declareQueue(queueName, queue);
Binding binding = null;
if (properties.getExtension().isBindQueue()) {
binding = declareConsumerBindings(name, properties, exchange, partitioned, queue);
}
if (durable) {
autoBindDLQ(applyPrefix(properties.getExtension().getPrefix(), baseQueueName), queueName,
properties.getExtension());
}
return new RabbitConsumerDestination(queue, binding);
}
具体代码在 org.springframework.cloud.stream.binder.rabbit.provisioning 这个包下面。
最初其实我看这个代码是看到默认的id貌似比我的uuid位数段啊,好看啊,最后看了下源码,哦,原来只是把uuid的进制改了下,无奈我的mysql默认不区分大小写,但是redis支持啊!为后续做点对点的推送埋个伏笔吧。
方案
生产者将消息通过rabbitmq,将需要推送的消息群发到所有websocket客户端,客户端再接着讲消息推送至前台用户。 这边我们需用用到exchange,帮助我们做群发。
/**
* 定义个fanout交换器
* @return
*/
@Bean
FanoutExchange fanoutExchange() {
// 定义一个名为fanoutExchange的fanout交换器
return new FanoutExchange(MqQueueNameConstant.WEBSOCKET_EXCHANGE);
}
websocket发送者,即rabbitmq消费者
@RabbitListener(
bindings = @QueueBinding(
exchange = @Exchange(value = MqQueueNameConstant.WEBSOCKET_EXCHANGE,type=ExchangeTypes.FANOUT),
value = @Queue(value ="#{webSocketQueue.name}",exclusive="true",
autoDelete ="true")
)
)
private static final AnonymousQueue.Base64UrlNamingStrategy ANONYMOUS_GROUP_NAME_GENERATOR
= new AnonymousQueue.Base64UrlNamingStrategy("anonymous.");
@Bean
public Queue webSocketQueue() {
String baseQueueName =groupedName(MqQueueNameConstant.WEBSOCKET_QUEUE, ANONYMOUS_GROUP_NAME_GENERATOR.generateName()); //大小写区分的uuid
return new Queue(baseQueueName, false, true, true);
}
private static final String GROUP_INDEX_DELIMITER = ".";
protected final String groupedName(String name, String group) {
return name + GROUP_INDEX_DELIMITER + (StringUtils.hasText(group) ? group : "default");
}
没错,上面的 new AnonymousQueue.Base64UrlNamingStrategy 就是超过来的,其实并没有卵用,为了提醒自己,我就这样写了。
这样一个随时上随时接受消息,没有消息自己滚蛋的queue就跟exchange绑定好了。其实这样基本也定下点对点推送的基调了,无非就是队列申明为topic,并将baseQueueName 作为routekey,websocket连接的时候将本机的 baseQueueName+用户唯一号存储到redis,生产者发送消息时查询到具体的客户端,通过routekey发送过来。