使用RabbitMQ解决WebSocket集群问题

使用RabbitMQ解决WebSocket集群问题

1、存在的问题

    当前一个项目在做在线客服的功能,用到了WebSocket技术,由于服务是分布式服务,所以涉及到了WebSocket的集群问题。
    假如我们部署了三个节点A、B、C,存在一个用户甲,甲用户通过WebSocket与节点服务器A建立了通信,并将消息推送给了服务端,A服务端接收到消息后,推送给客服系统D,客服人员会把回复的信息再通过WebSocket服务端推送给用户客户端。那么问题出来了,客服系统D将消息推送给WebSocket服务端节点时,是根据负载均衡策略推送的,并不能精确的知道是哪个服务节点给他推送的客户消息,所以就会出现客服人员给用户推送消息,推送到了B或者C节点,导致用户无法正常接收到响应信息。

2、解决方案

    问题的理想解决方案是客服系统D将回复用户的信息精确地传递到对应的A节点服务端。 所以我们打算采用RabbitMQ消息中间件来解决这个问题。原理是这样的:A节点服务器每次启动时获取其节点的IP地址,然后以其IP地址(去掉符号“.”)作为队列名创建队列。A节点服务端将信息发送到客户系统D,然后客户系统D将回复信息发送到RabbitMQ的directexchange交换机中,然后以每个节点的IP地址作为路由,实现信息精确送达。

3、代码实现

1)根据动态IP生成动态队列

/**
 * @program: myframe-springboot
 * @description: 直连交换机队列配置文件
 * @author: 
 * @create: 2020-05-18 17:16
 **/
@Configuration
//@AutoConfigureAfter(StringRedisTemplate.class)
public class DirectRabbitConfig {
    @Value("${rabbit.wsExchangeName}")
    private String WSExchangName;
    /*@Autowired
    StringRedisTemplate stringRedisTemplate;*/
    //动态获取节点的IP
    String ipaddr;
   /* {
        try {
            ipaddr = InetAddress.getLocalHost().getHostAddress().replace(".","");
            //redisTemplate.opsForValue().set(ipaddr,ipaddr);
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }*/

    /**
     * @Description: 根据IP地址动态的创建队列
     * @Param:
     * @return:
     * @Author:
     * @Date: 2020/5/18
     */
    @Bean
    public Queue createWsDirectQueue(){
        ipaddr = getInternetIp().replace(".","");
        return new Queue(ipaddr);
    }
    /**
     * @Description: 创建直连交换机
     * @Param:
     * @return:
     * @Author: 
     * @Date: 2020/5/18
     */
    @Bean
    public DirectExchange createWsDirectExchange(){
        return new DirectExchange(WSExchangName);
    }
    /**
     * @Description: 将队列与直连交换机绑定,并指定ip地址为路由
     * @Param:
     * @return:
     * @Author: 
     * @Date: 2020/5/18
     */
    @Bean
    public Binding bindingExchange(){
        return BindingBuilder.bind(createWsDirectQueue()).to(createWsDirectExchange()).with(ipaddr);
    }

   /**
   * @Description:  获取外网IP
   * @Param:
   * @return:
   * @Author: 
   * @Date: 2020/5/22
   */
    private  String getInternetIp(){
        try{
            Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces();
            InetAddress ip = null;
            Enumeration<InetAddress> addrs;
            while (networks.hasMoreElements())
            {
                addrs = networks.nextElement().getInetAddresses();
                while (addrs.hasMoreElements())
                {
                    ip = addrs.nextElement();
                    if (ip != null
                            && ip instanceof Inet4Address
                            && ip.isSiteLocalAddress()
                            )
                    {
                        return ip.getHostAddress();
                    }
                }
            }

            // 如果没有外网IP,就返回内网IP
            return "";
        } catch(Exception e){
            throw new RuntimeException(e);
        }
    }

    /**
    * @Description: 根据类型获取IP地址
    * @Param:
    * @return:
    * @Author: 
    * @Date: 2020/5/22
    */
    private String getInternetIp(String type) {
        try {
            Enumeration<NetworkInterface> networks = NetworkInterface.getNetworkInterfaces();
            while (networks.hasMoreElements()) {
                NetworkInterface ni = (NetworkInterface) networks.nextElement();
                if (!ni.getName().equals(type)) {
                    continue;
                } else {
                    Enumeration<?> e2 = ni.getInetAddresses();
                    while (e2.hasMoreElements()) {
                        InetAddress ia = (InetAddress) e2.nextElement();
                        if (ia instanceof Inet6Address)
                            continue;
                        return ia.getHostAddress();
                    }
                }
            }
            // 如果没有外网IP,就返回内网IP
            return "";
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

2)模拟客服系统D向队列推送消息

/**
 * @program: myframe-springboot
 * @description:
 * @author: 
 * @create: 2020-05-25 16:27
 **/
@RestController(value="/pushMsg")
public class PushMsgController {
    @Autowired
    RabbitTemplate rabbitTemplate;
    @Autowired
    Queue createWsDirectQueue;
    @Value("${rabbit.wsExchangeName}")
    private String wsDirectExchange;

    @GetMapping(value="/sendMsg/{data}")
    public String sendMsg(@PathVariable("data")String data){
        String queueName = createWsDirectQueue.getName();
        rabbitTemplate.convertAndSend(wsDirectExchange,queueName,data);
        return queueName+";"+data;
    }
}

3)websocket系统接收消息(这里是最主要的关键的实现部分)
由于队列名称不是固定的常量,所有无法使用springboot封装的监听队列接收数据的注解。

/**
 * @program: myframe-springboot
 * @description:由于@RabbitListener的队列名称只能是常量,所以@RabbitListener监听注解
 *              不能使用,只能使用rabbitmq的api接口
 * @author: 
 * @create: 2020-05-26 16:34
 **/
@Configuration
@AutoConfigureAfter(RabbitTemplate.class)
public class DirectRabbitMsgReceiveConfig {
    /**
     * 使用orderMqConsumer的orderShow方法作为listener。
     * @return
     */
    @Bean("orderShowMessageListener")
    MessageListenerAdapter orderShowMessageListener() {
        MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter(new MessageDelegate());
        //MessageDelegate实现了ChannelAwareMessageListener类则可以不用指定默认的监听方法
        messageListenerAdapter.setDefaultListenerMethod("handleMessage1");
        return messageListenerAdapter;
    }

    @Bean("orderShowMessageListenerContainer")
    SimpleMessageListenerContainer orderShowMessageListenerContainer(org.springframework.amqp.rabbit.connection.ConnectionFactory connectionFactory,
                                                                     @Qualifier("orderShowMessageListener") MessageListenerAdapter listenerAdapter,
                                                                     @Qualifier("createWsDirectQueue") Queue queue) {
        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        //只接收该节点的队列信息
        container.setQueueNames(queue.getName());
        container.setMessageListener(listenerAdapter);
        return container;
    }
}
/**
 * @program: myframe-springboot
 * @description:接收队列信息
 * @author: 
 * @create: 2020-05-07 20:10
 **/
public class MessageDelegate implements ChannelAwareMessageListener {
    //如果没有继承ChannelAwareMessageListener可以将该方法指定为适配器的接收方法,如果继承了ChannelAwareMessageListener,则优先执行onMessage方法;
    public void handleMessage1(String msg) {
        System.out.println("handleMessage默认方法,消息内容 String:" + msg);
    }

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        //接收队列消息中的信息
        byte[] body = message.getBody();
        System.out.println("onMessage方法接收到的消息:"+new String(body));
    }
}

4、总结

    该方案最核心的技术在于根据服务器IP动态创建队列,并接收消息。
    如果有可能的话,可以重写@RabbitListener注解,使其支持变量队列名。

你可能感兴趣的:(java,rabbitmq,websocket,队列)