SpringBoot+RabbitMQ+WebSocket实现简单聊天功能

6,241 阅读5分钟

1、WebSocket 简介

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。

WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。

现在,很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

HTML5 定义的 WebSocket 协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

2.前提

单机的消息推送实现十分简单,通过websocket中转就可以了,但是在分布式环境下不支持session共享因为服务器不同,所以可以采用 rabbitMQ+webSocket实现分布式消息推送。

项目基于之前的项目改造,首先要集成RabbitMQ,WebSokcet,主要的实现思想是:

客户端通过websocket注册到各自的服务器上,服务器绑定到同一个同一个MQ队列上,这样每次客户端发消息,先是投放到MQ中,再由绑定到队列上的消费者去发送socket信息,进而实现跨服务器的消息推送。

3.代码实现

这里贴一下主要的代码

开启WebSocket支持:

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

WebSocketServer:

@ServerEndpoint("/websocket/{from}")
@Component
public class WebSocketServer {
    private static Logger logger = LoggerFactory.getLogger(WebSocketServer.class);
    private static final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    //静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
    private static int onlineCount = 0;

    //concurrent包的线程安全Set,用来存放每个客户端对应的MyWebSocket对象。
    private static ConcurrentHashMap<String, WebSocketServer> webSocketMap = new ConcurrentHashMap<String, WebSocketServer>();

    //与某个客户端的连接会话,需要通过它来给客户端发送数据
    private Session session;

    //from
    private static String from = "";

    /**
     * 连接建立成功调用的方法*/
    @OnOpen
    public void onOpen(Session session,@PathParam("from") String from) {
        this.session = session;
        webSocketMap.put(from, this);     //加入set中
        addOnlineCount();           //在线数加1
        logger.info("有新窗口开始监听:"+from+",当前在线人数为" + getOnlineCount());
        this.from=from;
        try {
            sendMessage("连接成功");
        } catch (IOException e) {
            logger.error("websocket IO异常");
        }
    }

    /**
     * 连接关闭调用的方法
     */
    @OnClose
    public void onClose() {
        webSocketMap.remove(this);  //从set中删除
        subOnlineCount();           //在线数减1
        logger.info("有一连接关闭!当前在线人数为" + getOnlineCount());
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息*/
    @OnMessage
    public void onMessage(String message, Session session) {
        logger.info("收到来自窗口"+from+"的信息:"+message);
        JSONObject obj = new JSONObject();
        obj.put("cmd", "heartcheck");//业务类型
        obj.put("msgTxt", "服务端心跳响应 ");//消息内容
        obj.put("msgDate", DateUtils.getCurrentDateTime());//时间
        session.getAsyncRemote().sendText(obj.toJSONString());
    }

    /**
     *
     * @param session
     * @param error
     */
    @OnError
    public void onError(Session session, Throwable error) {
        logger.error("发生错误");
        error.printStackTrace();
    }
    /**
     * 实现服务器主动推送
     */
    public void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }

    /**
     * 群发自定义消息
     * */
    public static void sendMqMessage(String message) throws IOException {
        JSONObject jsonObject = new JSONObject();
        logger.info("推送消息到窗口"+from+",推送内容:"+message);
        ChatMsg chatMsg = JSONObject.parseObject(message, ChatMsg.class);

        if(chatMsg != null && chatMsg.getTo() != null && webSocketMap.containsKey(chatMsg.getTo())){
            webSocketMap.get(chatMsg.getTo()).sendMessage(message);
        }else{
            logger.error("用户"+chatMsg.getTo()+",不在线!");
        }

    }

    /**
     * 群发自定义消息
     * */
    public static void sendInfo(String message,@PathParam("from") String from) throws IOException {
        JSONObject jsonObject = new JSONObject();
        logger.info("推送消息到窗口"+from+",推送内容:"+message);
//        for (WebSocketServer item : webSocketMap) {
//            try {
//                String date = format.format(new Date());
//                String mes = message+ " (" + date + ")";
//                jsonObject.put("mes",mes);
//                jsonObject.put("sender",from);
//                //这里可以设定只推送给这个sid的,为null则全部推送
//                if(from==null) {
//                    item.sendMessage(jsonObject.toString());
//                }else if(item.from.equals(from)){
//                    item.sendMessage(jsonObject.toString());
//                }
//            } catch (IOException e) {
//                continue;
//            }
//        }
    }

    public static synchronized int getOnlineCount() {
        return onlineCount;
    }

    public static synchronized void addOnlineCount() {
        WebSocketServer.onlineCount++;
    }

    public static synchronized void subOnlineCount() {
        WebSocketServer.onlineCount--;
    }
}

创建消息队列:

@Configuration
public class FanoutRabbitConfig {

    public static final String DEFAULT_BOOK_QUEUE = "dev.book.fanout.a.queue";

    @Bean
    public Queue queueMessageA() {
        // 第一个是 QUEUE 的名字,第二个是消息是否需要持久化处理
        return new Queue(DEFAULT_BOOK_QUEUE, true);
    }



    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange("fanoutExchange");
    }

    @Bean
    Binding bindingExchangeMessage(Queue queueMessageA, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(queueMessageA).to(fanoutExchange);
    }

}

消费者:

@Component
public class BookHandler {

    private static final Logger logger = LoggerFactory.getLogger(BookHandler.class);


    @RabbitListener(queues = {FanoutRabbitConfig.DEFAULT_BOOK_QUEUE})
    public void listenerAutoAck(String text, Message message, Channel channel) {

        // TODO 如果手动ACK,消息会被监听消费,但是消息在队列中依旧存在,如果 未配置 acknowledge-mode 默认是会在消费完毕后自动ACK掉
        final long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            logger.info("[消费者一监听的消息] - [{}]", text);
            new WebSocketServer().sendMqMessage(text);

            // TODO 通知 MQ 消息已被成功消费,可以ACK了
            channel.basicAck(deliveryTag, false);
        } catch (IOException e) {
            try {
                // TODO 处理失败,重新压入MQ
                channel.basicRecover();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

}

消息类:

public class ChatMsg {
    private String from;//发送的username
    private String to;//接收者
    private String content;//内容
    private Date date;//时间
    private String fromNickname;//昵称

    public String getFromNickname() {
        return fromNickname;
    }

    public void setFromNickname(String fromNickname) {
        this.fromNickname = fromNickname;
    }

    public String getFrom() {
        return from;
    }

    public void setFrom(String from) {
        this.from = from;
    }

    public String getTo() {
        return to;
    }

    public void setTo(String to) {
        this.to = to;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    public Date getDate() {
        return date;
    }

    public void setDate(Date date) {
        this.date = date;
    }
}

发送消息controller:

@Controller
@RequestMapping("/testmq")
public class MqTestController {
    private static Logger logger = LoggerFactory.getLogger(MqTestController.class);

    @Autowired
    private  RabbitTemplate rabbitTemplate; //rabbitTemplate是springboot 提供的默认实现


    @RequestMapping(value="/send")
    @ResponseBody
    public void defaultMessage(String message, String from, String to) {
        ChatMsg chatMsg = new ChatMsg();
        chatMsg.setFrom(from);
        chatMsg.setTo(to);
        chatMsg.setContent(message);
        chatMsg.setDate(new Date());
       rabbitTemplate.convertAndSend("fanoutExchange", "", JSONObject.toJSONString(chatMsg));
    }
}

pom依赖:

        <!-- rabbitmq-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>

        <!-- Websocket-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

配置文件:

  #rabbitmq 配置
  spring:
      rabbitmq:
        host: 192.0.0.171
        port: 5672
        username: admin
        password: 123456
        #虚拟主机
        virtual-host: /
        listener:
          simple:
            #手动ACK
            acknowledge-mode: manual

4.测试

首先打两个jar包,使用两个不同端口运行起来,打开两个浏览器窗口进行测试。

经过简单测试没有问题。


欢迎关注个人公众号