当前一个项目在做在线客服的功能,用到了WebSocket技术,由于服务是分布式服务,所以涉及到了WebSocket的集群问题。
假如我们部署了三个节点A、B、C,存在一个用户甲,甲用户通过WebSocket与节点服务器A建立了通信,并将消息推送给了服务端,A服务端接收到消息后,推送给客服系统D,客服人员会把回复的信息再通过WebSocket服务端推送给用户客户端。那么问题出来了,客服系统D将消息推送给WebSocket服务端节点时,是根据负载均衡策略推送的,并不能精确的知道是哪个服务节点给他推送的客户消息,所以就会出现客服人员给用户推送消息,推送到了B或者C节点,导致用户无法正常接收到响应信息。
问题的理想解决方案是客服系统D将回复用户的信息精确地传递到对应的A节点服务端。 所以我们打算采用RabbitMQ消息中间件来解决这个问题。原理是这样的:A节点服务器每次启动时获取其节点的IP地址,然后以其IP地址(去掉符号“.”)作为队列名创建队列。A节点服务端将信息发送到客户系统D,然后客户系统D将回复信息发送到RabbitMQ的directexchange交换机中,然后以每个节点的IP地址作为路由,实现信息精确送达。
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));
}
}
该方案最核心的技术在于根据服务器IP动态创建队列,并接收消息。
如果有可能的话,可以重写@RabbitListener注解,使其支持变量队列名。