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,就断定客户端异常。
也就是定时任务开启,不断扫描最后一次发送时间和现在时间的差值,时间太长的话,那就是客户端断了。