springcloud下websocket集群的解决方案(1)

1,455 阅读3分钟

背景

我这边自己的项目目前情况是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发送过来。