Spring4+WebSocket+Socket+STOMP+Jetty构建示例
Spring 4引入了WebSocket API,浏览器和Web服务器可以根据WebSocket API通过WebSocket协议进行通信。通过这种方式,我们可以创建一个高度交互的UI和在线游戏,需要从服务器快速响应。SockJS在UI上提供类似WebSocket的对象。STOMP客户端用于通过WebSocket协议进行通信。我正在使用tomcat服务器来部署项目。下图为WebSocket通信的图表。
WebSocket协议
WebSocket是一种在浏览器和Web服务器之间进行通信的协议。WebSocket通过TCP协议工作。它在浏览器和Web服务器之间打开套接字连接并开始通信,它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。WebSocket可以轻松嵌入子协议,如STOMP。
STOMP协议
stomp是一个用于client之间进行异步消息传输的简单文本协议, 全称是Simple Text Oriented Messaging Protocol。STOMP使用连接,发送,订阅,断开等不同的命令进行通信。严格意义上来说STOMP并不是WebSocket的子协议,它是属于消息队列的一种协议,和AMQP/JMS一个层级。
SockJS
SockJS是一个java脚本库,为浏览器提供类似websocket的对象。SockJS提供跨浏览器兼容性,并支持STOMP协议与任何消息代理进行通信。SockJS的工作方式是我们需要提供URL来连接消息代理,然后让stomp客户端进行通信。
工程示例概览图
所需的jar
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.rongdu.framework</groupId> <artifactId>spring-websocket-stomp-demo</artifactId> <version>1.0</version> <packaging>war</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>4.3.18.RELEASE</spring.version> <jettyVersion>9.4.9.v20180320</jettyVersion> <jackson.version>2.7.3</jackson.version> </properties> <dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <!-- WebSocket Tomcat插件 --> <!-- <dependency> <groupId>org.apache.tomcat.embed</groupId> <artifactId>tomcat-embed-websocket</artifactId> <version>7.0.53</version> </dependency> --> <!-- WebSocket Jetty插件 --> <dependency> <groupId>org.eclipse.jetty.websocket</groupId> <artifactId>websocket-api</artifactId> <version>${jettyVersion}</version> </dependency> <dependency> <groupId>org.eclipse.jetty.websocket</groupId> <artifactId>websocket-server</artifactId> <version>${jettyVersion}</version> </dependency> <dependency> <groupId>javax.websocket</groupId> <artifactId>javax.websocket-api</artifactId> <version>1.1</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-websocket</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.java-websocket</groupId> <artifactId>Java-WebSocket</artifactId> <version>1.3.0</version> </dependency> <!-- 页面 webjar --> <dependency> <groupId>org.webjars</groupId> <artifactId>webjars-locator-core</artifactId> <version>0.35</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>sockjs-client</artifactId> <version>1.0.2</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>stomp-websocket</artifactId> <version>2.3.3</version> </dependency> <dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.1.0</version> </dependency> <!-- RabbitMQ --> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>3.5.1</version> </dependency> <dependency> <groupId>org.springframework.amqp</groupId> <artifactId>spring-rabbit</artifactId> <version>1.4.5.RELEASE</version> </dependency> <!--slf4j--> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-slf4j-impl</artifactId> <version>2.7</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>utf-8</encoding> </configuration> </plugin> </plugins> </build> </project>
web.xml配置
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:/config/spring/*-beans.xml</param-value> </context-param> <!-- 编码过滤器 --> <filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>ASYNC</dispatcher> </filter-mapping> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <servlet> <servlet-name>springServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:/config/web/web-main.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <async-supported>true</async-supported> </servlet> <servlet-mapping> <servlet-name>springServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.html</welcome-file> </welcome-file-list> </web-app>
spring的mvc-servlet.xml配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:task="http://www.springframework.org/schema/task" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.3.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <!-- 自动扫包 --> <context:component-scan base-package="com.huatech.controller"/> <!-- 提供请求分发、数据绑定、数据转换等操作 --> <mvc:annotation-driven/> <!-- application-destination-prefix 逗号分隔的前缀列表,用于匹配已处理消息的目标 user-destination-prefix 用于标识用户目标的前缀。 不解析任何不以给定前缀开头的目的地。 默认值为:/user/ --> <websocket:message-broker application-destination-prefix="/app" user-destination-prefix="/queue"> <websocket:stomp-endpoint path="/websocket-address" > <websocket:sockjs/> </websocket:stomp-endpoint> <websocket:simple-broker prefix="/topic,/queue"/> </websocket:message-broker> <!-- 视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/js/"/> <property name="suffix" value=".jsp"/> <property name="contentType" value="text/html; charset=UTF-8"/> </bean> <!-- webjars 静态资源 --> <mvc:resources mapping="/webjars/**" location="classpath:/META-INF/resources/webjars/" > <mvc:resource-chain resource-cache="false" auto-registration="false"> <mvc:resolvers> <bean class="org.springframework.web.servlet.resource.WebJarsResourceResolver"/> <bean class="org.springframework.web.servlet.resource.PathResourceResolver"/> </mvc:resolvers> </mvc:resource-chain> </mvc:resources> <!-- 本地资源 --> <mvc:resources mapping="/js/**" location="/js/"/> <!-- 默认首页 --> <mvc:view-controller path="/" view-name="index"/> <!-- scheduler配置 --> <task:executor id="executor" pool-size="5" /> <task:scheduler id="scheduler" pool-size="5" /> <task:annotation-driven executor="executor" scheduler="scheduler" /> </beans>
添加控制器类
package com.huatech.controller; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.annotation.SendToUser; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Controller; @Controller public class TopicController { private static final Logger LOGGER = LoggerFactory.getLogger(TopicController.class); @Autowired private SimpMessagingTemplate template; /*收到消息后广播推送*/ @MessageMapping("/boardcast") @SendTo("/topic/boardcast") public String sendtousers(String s){ LOGGER.info(s); return "我是广播消息"; } /*收到消息后精准投送到单个用户*/ @MessageMapping("/precise") /*broadcast = false避免把消息推送到同一个帐号不同的session中*/ @SendToUser(value = "/topic/precise",broadcast = false) public String sendtouser(String s){ LOGGER.info(s); return "我是精准投送消息"; } /*下面2个是不用接收消息动态发送消息的方法*/ /*Scheduled为定时任务,演示*/ @Scheduled(fixedDelay = 1500) public void boardcast(){ this.template.convertAndSend("/topic", "来自服务器广播消息"); } @Scheduled(fixedDelay = 1500) public void precise(){ this.template.convertAndSendToUser("abc", "/message","来自服务器精准投送消息"); } }
index.jsp示例页面
<%@ page language="java" pageEncoding="UTF-8"%> <html> <head> <meta charset="utf-8"/> <title>spring-websocket-stomp</title> </head> <body> <h1>spring-websocket-stomp</h1> <button id="btn-send">广播消息</button> <button id="btn-send2">精准投送消息</button> <button id="btn-send3">动态广播</button> <button id="btn-send4">动态精准投送</button> </body> </html> <script src="webjars/jquery/jquery.min.js"></script> <script src="webjars/sockjs-client/sockjs.min.js"></script> <script src="webjars/stomp-websocket/stomp.min.js"></script> <script src="js/app.js"></script>
app.js
var stompClient = null; var subscription = null; $(function () { var ws = new SockJS("/websocket-address"); stompClient = Stomp.over(ws); stompClient.connect({'client-id': 'my-client'},function () { }); $("#btn-send").click(function () { if(subscription != null){subscription.unsubscribe();} subscription = stompClient.subscribe("/topic/boardcast", function(){}); stompClient.send("/app/boardcast",{},"请求广播"); }); $("#btn-send2").click(function () { if(subscription != null){subscription.unsubscribe();} subscription = stompClient.subscribe("/queue/topic/precise", function(){}); stompClient.send("/app/precise",{},"请求精准投送"); }); // 请求动态广播 $("#btn-send3").click(function () { if(subscription != null){subscription.unsubscribe();} subscription = stompClient.subscribe("/topic", function(){}); }); // 请求精准投送 $("#btn-send4").click(function () { if(subscription != null){subscription.unsubscribe();} subscription = stompClient.subscribe("/queue/abc/message", function(){}); }); });
遇到的问题
1、spring引入webjars页面报404问题
参考:https://blog.csdn.net/chao_1990/article/details/53449494
2、Handler dispatch failed; nested exception is java.lang.NoSuchMethodError: com.fasterxml.jackson.databind.ObjectMapper.canSerialize(Ljava/lang/Class;Ljava/util/concurrent/atomic/AtomicReference;)Z
答:jackson版本冲突
3、@MessageMapping注解配置后不起作用
答:websocket相关的配置需通过mvc进行初始化,而不是交给IOC
参考:
https://*.com/questions/42441837/messagemapping-does-not-work-with-spring-security-and-mvc
上一篇: spring boot与websocket集成,实现主动推送
下一篇: 野菜黄花菜的食疗作用