消息实时推送、转发:SpringBoot结合WebSocket、RabbitMQ实现、上线获取消息(叮咚,您有新的订单,请及时处理) 附工程代码 <——> 感知客户端&安全建立连接

SpringCloud、SpringBoot结合RabbitMQ实现监听消息实时转发(webSocket)

方案设计

 

所用工具(包)

IDEA、SpringCloud、RabbitMQ、SpringData(JPA)、MySQL、SpringBoot、Maven

 

初识websocket

WebSocket 是HTML5一种新的协议。它实现了浏览器与服务器全双工通信(full-duplex)。一开始的握手需要借助HTTP请求完成。 WebSocket是真正实现了全双工通信的服务器向客户端推的互联网技术。 它是一种在单个TCP连接上进行全双工通讯协议。Websocket通信协议与2011年倍IETF定为标准RFC 6455,Websocket API被W3C定为标准。

 

为什么要用websocket?

http:短连接,请求响应完之后会断开连接,重新获取新的数据需要再次请求。

WebSocket协议是一种长链接,只需要通过一次请求来初始化链接,然后所有的请求和响应都是通过这个TCP链接进行通讯。

TCP:可靠的、面向连接的通信协议!这也是TCP协议为什么建立时需要三次握手、断开连接时需要四次挥手的原因。

实现方式有:

基于注解

  • @ServerEndpoint("/websocket/{id}") 申明这是一个websocket服务 需要指定访问该服务的地址,在地址中可以指定参数,需要通过占位符{}进行占位

  • @OnOpen 用法:public void onOpen(Session session, @PathParam("id") String id) throws IOException{} 该方法将在建立连接后执行,会传入session对象,就是客户端与服务端建立的长连接通道 通过@PathParam获取url申明中的参数

  • @OnClose 用法:public void onClose() {} 该方法是在连接关闭后执行

  • @OnMessage 用法:public void onMessage(String message, Session session) throws IOException {} 该方法用于接收客户端发来的消息 message:发来的消息数据 session:会话对象(也是通道) 发送消息到客户端 用法:session.getBasicRemote().sendText("你好"); 通过session进行发送。

@ServerEndpoint("/websocket/{id}")
public class MyWebSocket {
    // TODO 管理会话session,需要用JUC下的集合
@OnOpen
public void onOpen(Session session, @PathParam("id") String id) throws
IOException {
// 连接成功并向客户端发送消息
    session.getBasicRemote().sendText(id + ",你好,欢迎连接WebSocket!");
    }
    
@OnClose
public void onClose() {
    System.out.println(this + "关闭连接");
    }
    
 /**接收客户端发来的消息,转发消息可以用该方法**/
@OnMessage
public void onMessage(String message, Session session) throws IOException {
    System.out.println("接收到消息:" + message);
    session.getBasicRemote().sendText("消息已收到.");
    // TODO 增加逻辑处理客户端的消息
    // Addition: 如果消息本地化,客户断开期间的数据保存到DB,再次上线客户端主动发送消息,响应并发送未发生的DB数据
    }
@OnError
public void onError(Session session, Throwable error) {
    System.out.println("发生错误");
    error.printStackTrace();
    }
    
    //监听RabbitMQ的消息并转发,MQ的相关应用请查相关资料
    @Transactional
    @StreamListener(target = "监听队列的名称", condition = "headers['msgCode']=='放消息定义的消息头'")
    public void dealMessageGroup(OrderPaySuccessMessage msg) {
        log.info("[dealMessageGroup]MQ 订单成功支付通知消息{}", msg.toString());
        String orderId = msg.getOrderId();
        // TODO 消息的进一步处理:
        // I、发给指定客户端(广播)
        // II、是否本地化(保存到DB)
    }
    
}

SpringBoot实现

websocket配置

@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
​
    @Autowired
    private MyHandler myHandler;
​
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(myHandler, "/websocket/{id}")
                .setAllowedOrigins("*") ;
    }
}
// 多参数时直接拼接  "/websocket/{id}"

websocket服务端

@Slf4j
@Component
public class MyHandler extends TextWebSocketHandler {
    // 当前会话信息
    private WebSocketSession wssession;
    // websocket会话管理服务
    @Autowired
    private WSSessionService wsSessionService;
​
    /**
     * 处理未发给客户端的消息,并在客户登录时推送订单消息
     *
     * @param session
     * @param message
     * @throws IOException
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void handleTextMessage(WebSocketSession session, TextMessage message)
            throws IOException {
            log.info("收到客户端发送的消息 >> {} ", message.getPayload());
           // TODO 响应、处理客户端的消息,可在此处实现,查询数据库中未推送的消息列表并逐条推送
            for(;;){
                    session.sendMessage(new TextMessage("您有新的订单,请及时处理!" + m.getOrderId()));
            }
            
        }
    }
​
    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        this.wssession = session;
        log.info("建立连接:wssession:{}", wssession);
        /** 从session的uri中获取参数 **/
        URI uri = session.getUri();
        String path = uri.toString();
        String url[] = StringUtils.split(path, "/");
        // TODO 参数逻辑校验
        
        // 响应建立连接
        session.sendMessage(new TextMessage("欢迎使用***推送服务,即将为您推送客户成功下单消息!"));
    }
​
    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        
        // TODO  /** 断开连接时清除map中的客户端 **/
        log.info("客户端断开连接!!!" + status);
    }
​
    @Override
    public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
        if (webSocketSession.isOpen()) webSocketSession.close();
        log.error("transport error:", throwable.getMessage());
         // TODO  /** 断开连接时清除map中的客户端 **/
        log.info("发生错误:  {}", throwable.getMessage());
    }
​
}

转发消息逻辑

@Slf4j
@Service
public class DealMQMessage {
​
    // 会话管理服务
    @Autowired
    private WSSessionService wsSessionService;
​
    @Transactional
    @StreamListener(target = "当前监听的消息队列名", condition = "headers['msgCode']=='放消息定义的消息头'")
    public void dealMessageGroup(OrderPaySuccessMessage msg) {
        log.info("[dealMessageGroup] MQ 订单成功支付通知消息{}", msg.toString());
        String orderId = msg.getOrderId();
        // 从WSSessionService获取该获取推送的客户端
        /** TODO 消息的进一步处理:
        * I、定向发送或者广播
        * II、是否本地化(DB)
        **/
    }
}

启动类

 

@SpringBootApplication
@EnableBinding(MessageListener.class)
@EnableJpaAuditing //开启JPA监听器
public class MessagePushApplication {
    public static void main(String[] args) {
        new SpringApplicationBuilder(MessagePushApplication.class).web(true).run(args);
    }

 

消息监听接口

public interface MessageListener {
    /**
     * 监听来自订单成功支付的通知 — 订单处理
     * */
    public String QUEUE_NAME = "dealMessageGroup";
​
    @Input(QUEUE_NAME)
    SubscribableChannel dealMessageGroup();
}

 

注意: 不同的实现方式需要增加对应的依赖

<dependencies>
    
    <dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>7.0</version>
    <scope>provided</scope>
    </dependency>
</dependencies>
​
<build>
    <plugins>
    <!-- java编译插件 -->
        <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.2</version>
        <configuration>
        <source>1.8</source>
        <target>1.8</target>
        <encoding>UTF-8</encoding>
        </configuration>
    </plugin>
<!-- 配置Tomcat插件 -->
    <plugin>
        <groupId>org.apache.tomcat.maven</groupId>
        <artifactId>tomcat7-maven-plugin</artifactId>
        <version>2.2</version>
        <configuration>
        <port>8082</port>
        <path>/</path>
        </configuration>
    </plugin>
</plugins>

SpringBoot实现需要的依赖

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.0.RELEASE</version>
</parent>
​
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

SpringCloud 依赖:

            <dependency>
                <groupId>com.github.drtrang</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>${com.github.drtrang.druid}</version>
            </dependency>
​
            <dependency>
               <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-stream</artifactId>
           </dependency>
​
           <dependency>
              <groupId>org.springframework.cloud</groupId>
              <artifactId>spring-cloud-starter-stream-rabbit</artifactId>
            </dependency>

其他依赖:(DB)

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.github.drtrang</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>

resources文件下的相关配置就不贴了

 

在线测试工具:http://coolaf.com/tool/chattest

总结

如果出现本地测试连接成功,dev等环境失败时,请配置域名和IP+PORT或者nginx等代理

编译成功、运行失败问题

如果你在项目中遇到编译成功、运行失败的问题时,请你停下来看一下自己的工程是否配置OK(未配置、配置错误)?注解OK(加错注解、忘加注解)?依赖OK(冲突、缺少)?

一般情况下编译OK、运行失败不外乎这三个原因所致!!!

 工程请移步:工程代码、结合你自己的项目扩展或微调直接使用

 

进阶:

保证安全连接(取principal里的username,空的话不予握手成功)

感知客户端:

客户端每隔一个周期 T(举个例子)向服务器端发一个信息,服务端将发这个数据的信息和保存时间存入表中,随后应用用定时任务不断的检查当前时间和刚刚存入的时间的差值:这里用到了这个函数TIMESTAMPDIFF(SECOND, dataEnterTime,now()),如果这个值大于 3T,就断定客户端异常。

也就是定时任务开启,不断扫描最后一次发送时间和现在时间的差值,时间太长的话,那就是客户端断了。

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页