Spring系列文章 | SockJS长连接实现一对一和一对多通信

最近项目上要做扫码登录,所以研究了一下Spring WebSocket。网上找了很多资料 springmvc(18)使用WebSocket 和 STOMP 实现消息功能spring websocket + stomp 实现广播通信和一对一通信,要么就是不是自己想要的,要么就是只有中间一部分。所以特别写了这篇文章,一方面怕自己遗忘,另一方面是希望可以给大家一些参考。

先放代码,在文章的最后我会把项目地址给大家。这个项目是可以运行的,直接导入Idea就可以了。

pom.xml文件:(这里只有最基本的包,Spring必须得是4.0+)

[html]  view plain  copy
 print ?
  1. <properties>  
  2.         <spring.version>4.2.8.RELEASE</spring.version>  
  3.     </properties>  
  4.   
  5.     <dependencies>  
  6.         <dependency>  
  7.             <groupId>org.springframework</groupId>  
  8.             <artifactId>spring-core</artifactId>  
  9.             <version>${spring.version}</version>  
  10.         </dependency>  
  11.         <dependency>  
  12.             <groupId>org.springframework</groupId>  
  13.             <artifactId>spring-context</artifactId>  
  14.             <version>${spring.version}</version>  
  15.         </dependency>  
  16.         <dependency>  
  17.             <groupId>org.springframework</groupId>  
  18.             <artifactId>spring-messaging</artifactId>  
  19.             <version>${spring.version}</version>  
  20.         </dependency>  
  21.         <dependency>  
  22.             <groupId>org.springframework</groupId>  
  23.             <artifactId>spring-websocket</artifactId>  
  24.             <version>${spring.version}</version>  
  25.         </dependency>  
  26.         <dependency>  
  27.             <groupId>org.springframework</groupId>  
  28.             <artifactId>spring-webmvc</artifactId>  
  29.             <version>${spring.version}</version>  
  30.         </dependency>  
  31.         <dependency>  
  32.             <groupId>org.springframework</groupId>  
  33.             <artifactId>spring-web</artifactId>  
  34.             <version>${spring.version}</version>  
  35.         </dependency>  
  36.         <dependency>  
  37.             <groupId>javax.servlet</groupId>  
  38.             <artifactId>javax.servlet-api</artifactId>  
  39.             <version>3.1.0</version>  
  40.             <scope>provided</scope>  
  41.         </dependency>  
  42.         <dependency>  
  43.             <groupId>org.springframework</groupId>  
  44.             <artifactId>spring-beans</artifactId>  
  45.             <version>${spring.version}</version>  
  46.         </dependency>  
  47.         <dependency>  
  48.             <groupId>org.springframework</groupId>  
  49.             <artifactId>spring-aop</artifactId>  
  50.             <version>${spring.version}</version>  
  51.         </dependency>  
  52.         <dependency>  
  53.             <groupId>org.springframework</groupId>  
  54.             <artifactId>spring-context-support</artifactId>  
  55.             <version>${spring.version}</version>  
  56.         </dependency>  
  57.         <dependency>  
  58.             <groupId>com.fasterxml.jackson.core</groupId>  
  59.             <artifactId>jackson-databind</artifactId>  
  60.             <version>2.5.3</version>  
  61.             <scope>runtime</scope>  
  62.         </dependency>  
  63.     </dependencies>  
web.xml:(servlet和所有的filter都要加 <async-supported>true</async-supported>)
[html]  view plain  copy
 print ?
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"  
  3.          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  4.          xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"  
  5.          version="3.1">  
  6.     <welcome-file-list>  
  7.         <welcome-file>index.jsp</welcome-file>  
  8.     </welcome-file-list>  
  9.   
  10.     <context-param>  
  11.         <param-name>contextConfigLocation</param-name>  
  12.         <param-value>classpath:config/spring/*.xml</param-value>  
  13.     </context-param>  
  14.   
  15.     <listener>  
  16.         <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
  17.     </listener>  
  18.   
  19.     <servlet>  
  20.         <servlet-name>dispatcherServlet</servlet-name>  
  21.         <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>  
  22.         <init-param>  
  23.             <param-name>contextConfigLocation</param-name>  
  24.             <param-value>classpath:config/spring/*.xml</param-value>  
  25.         </init-param>  
  26.         <load-on-startup>1</load-on-startup>  
  27.         <async-supported>true</async-supported>  
  28.     </servlet>  
  29.     <servlet-mapping>  
  30.         <servlet-name>dispatcherServlet</servlet-name>  
  31.         <url-pattern>/</url-pattern>  
  32.     </servlet-mapping>  
  33.   
  34.     <filter>  
  35.         <filter-name>encodingFilter</filter-name>  
  36.         <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>  
  37.         <init-param>  
  38.             <param-name>encoding</param-name>  
  39.             <param-value>UTF-8</param-value>  
  40.         </init-param>  
  41.         <init-param>  
  42.             <param-name>forceEncoding</param-name>  
  43.             <param-value>true</param-value>  
  44.         </init-param>  
  45.         <async-supported>true</async-supported>  
  46.     </filter>  
  47.   
  48.     <filter-mapping>  
  49.         <filter-name>encodingFilter</filter-name>  
  50.         <url-pattern>/*</url-pattern>  
  51.     </filter-mapping>  
  52. </web-app>  
dispatcher.xml:(SpringMVC配置文件)
[html]  view plain  copy
 print ?
  1. <?xml version="1.0" encoding="UTF-8"?>  
  2. <beans xmlns="http://www.springframework.org/schema/beans"  
  3.        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
  4.        xmlns:context="http://www.springframework.org/schema/context"  
  5.        xmlns:mvc="http://www.springframework.org/schema/mvc"  
  6.        xsi:schemaLocation="http://www.springframework.org/schema/beans  
  7.                         http://www.springframework.org/schema/beans/spring-beans-3.1.xsd  
  8.                         http://www.springframework.org/schema/context  
  9.                         http://www.springframework.org/schema/context/spring-context-3.1.xsd  
  10.                         http://www.springframework.org/schema/mvc  
  11.                         http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd">  
  12.     <context:annotation-config />  
  13.     <mvc:annotation-driven />  
  14.     <context:component-scan base-package="com.hyy" />  
  15.   
  16.     <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">  
  17.         <property name="prefix" value="/WEB-INF/views/" />  
  18.         <property name="suffix" value=".jsp" />  
  19.     </bean>  
  20. </beans>  
WebSocketConfig类:(Spring WebSocket的配置文件,这里采用的是注解的方式)
[java]  view plain  copy
 print ?
  1. import org.springframework.context.annotation.Configuration;  
  2. import org.springframework.messaging.simp.config.MessageBrokerRegistry;  
  3. import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;  
  4. import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;  
  5. import org.springframework.web.socket.config.annotation.StompEndpointRegistry;  
  6.   
  7. /** 
  8.  * Created by haoyuyang on 2016/11/25. 
  9.  */  
  10. @Configuration  
  11. @EnableWebSocketMessageBroker  
  12. public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {  
  13.   
  14.     /** 
  15.      * 将"/hello"路径注册为STOMP端点,这个路径与发送和接收消息的目的路径有所不同,这是一个端点,客户端在订阅或发布消息到目的地址前,要连接该端点, 
  16.      * 即用户发送请求url="/applicationName/hello"与STOMP server进行连接。之后再转发到订阅url; 
  17.      * PS:端点的作用——客户端在订阅或发布消息到目的地址前,要连接该端点。 
  18.      * @param stompEndpointRegistry 
  19.      */  
  20.     public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {  
  21.         //在网页上可以通过"/applicationName/hello"来和服务器的WebSocket连接  
  22.         stompEndpointRegistry.addEndpoint("/hello").setAllowedOrigins("*").withSockJS();  
  23.     }  
  24.   
  25.     /** 
  26.      * 配置了一个简单的消息代理,如果不重载,默认情况下回自动配置一个简单的内存消息代理,用来处理以"/topic"为前缀的消息。这里重载configureMessageBroker()方法, 
  27.      * 消息代理将会处理前缀为"/topic"和"/queue"的消息。 
  28.      * @param registry 
  29.      */  
  30.     @Override  
  31.     public void configureMessageBroker(MessageBrokerRegistry registry) {  
  32.          //应用程序以/app为前缀,代理目的地以/topic、/user为前缀  
  33.         registry.enableSimpleBroker("/topic""/user");  
  34.         registry.setApplicationDestinationPrefixes("/app");  
  35.         registry.setUserDestinationPrefix("/user");  
  36.     }  
  37. }  
PS:

registry.enableSimpleBroker("/topic", "/user");这句话表示在topic和user这两个域上可以向客户端发消息。

registry.setUserDestinationPrefix("/user");这句话表示给指定用户发送一对一的主题前缀是"/user"。

registry.setApplicationDestinationPrefixes("/app");这句话表示客户单向服务器端发送时的主题上面需要加"/app"作为前缀。

stompEndpointRegistry.addEndpoint("/hello").setAllowedOrigins("*").withSokJS();这个和客户端创建连接时的url有关,其中setAllowedOrigins()方法表示允许连接的域名,withSockJS()方法表示支持以SockJS方式连接服务器。

接下来是测试类GreetingController:

[java]  view plain  copy
 print ?
  1. import com.hyy.model.Greeting;  
  2. import org.springframework.beans.factory.annotation.Autowired;  
  3. import org.springframework.messaging.handler.annotation.Header;  
  4. import org.springframework.messaging.handler.annotation.Headers;  
  5. import org.springframework.messaging.handler.annotation.MessageMapping;  
  6. import org.springframework.messaging.handler.annotation.SendTo;  
  7. import org.springframework.messaging.simp.SimpMessageSendingOperations;  
  8. import org.springframework.messaging.simp.annotation.SendToUser;  
  9. import org.springframework.web.bind.annotation.RequestMapping;  
  10. import org.springframework.web.bind.annotation.RequestMethod;  
  11. import org.springframework.web.bind.annotation.RestController;  
  12.   
  13. import java.util.Map;  
  14.   
  15. /** 
  16.  * Created by haoyuyang on 2016/11/25. 
  17.  */  
  18. @RestController  
  19. public class GreetingController {  
  20.   
  21.     @Autowired  
  22.     private SimpMessageSendingOperations simpMessageSendingOperations;  
  23.   
  24.     /** 
  25.      * 表示服务端可以接收客户端通过主题“/app/hello”发送过来的消息,客户端需要在主题"/topic/hello"上监听并接收服务端发回的消息 
  26.      * @param topic 
  27.      * @param headers 
  28.      */  
  29.     @MessageMapping("/hello"//"/hello"为WebSocketConfig类中registerStompEndpoints()方法配置的  
  30.     @SendTo("/topic/greetings")  
  31.     public void greeting(@Header("atytopic") String topic, @Headers Map<String, Object> headers) {  
  32.         System.out.println("connected successfully....");  
  33.         System.out.println(topic);  
  34.         System.out.println(headers);  
  35.     }  
  36.   
  37.     /** 
  38.      * 这里用的是@SendToUser,这就是发送给单一客户端的标志。本例中, 
  39.      * 客户端接收一对一消息的主题应该是“/user/” + 用户Id + “/message” ,这里的用户id可以是一个普通的字符串,只要每个用户端都使用自己的id并且服务端知道每个用户的id就行。 
  40.      * @return 
  41.      */  
  42.     @MessageMapping("/message")  
  43.     @SendToUser("/message")  
  44.     public Greeting handleSubscribe() {  
  45.         System.out.println("this is the @SubscribeMapping('/marco')");  
  46.         return new Greeting("I am a msg from SubscribeMapping('/macro').");  
  47.     }  
  48.   
  49.     /** 
  50.      * 测试对指定用户发送消息方法 
  51.      * @return 
  52.      */  
  53.     @RequestMapping(path = "/send", method = RequestMethod.GET)  
  54.     public Greeting send() {  
  55.         simpMessageSendingOperations.convertAndSendToUser("1""/message"new Greeting("I am a msg from SubscribeMapping('/macro')."));  
  56.         return new Greeting("I am a msg from SubscribeMapping('/macro').");  
  57.     }  
  58.   
  59. }  

PS:

这个类里面注入了SimpMessagingTemplete对象,后面动态发送消息时需要这个对象。

第一个方法,表示服务器端可以接收客户端通过主题"/app/hello"发送过来的消息,客户端需要在主题"/topic/hello"上监听并接收服务器发回的消息。
第二个方法道理相同,只是注意这里用的是@SendToUser注解,这就是发送给单一客户端的标志。本例中,客户端接收一对一消息的主题应该是"/user/"+userId+"/message",这里的用户id可以是一个普通字符串,只要每个客户端都使用自己的id并且服务器端知道每个用户的id就行了。
发送消息使用SimpMessagingTemplete类的convertAndSend("/topic/hello", greeting); //广播,和convertAndSendToUser(userId, "/message", userMessage); //一对一发送给特定用户。
Greeting类:
[java]  view plain  copy
 print ?
  1. public class Greeting {  
  2.     private String content;  
  3.   
  4.     public Greeting(String content) {  
  5.         this.content = content;  
  6.     }  
  7.   
  8.     public String getContent() {  
  9.         return content;  
  10.     }  
  11. }  
测试页面websocket.jsp:
[html]  view plain  copy
 print ?
  1. <%@ page contentType="text/html;charset=UTF-8" language="java" %>  
  2. <html lang="en">  
  3. <head>  
  4.     <title>Hello WebSocket</title>  
  5.     <script src="http://cdn.bootcss.com/sockjs-client/1.1.1/sockjs.min.js"></script>  
  6.     <script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.js"></script>  
  7.     <script src="http://cdn.bootcss.com/jquery/3.1.1/jquery.min.js"></script>  
  8.     <script type="text/javascript">  
  9.         $(document).ready(function(){  
  10.             connect();  
  11.             //checkoutUserlist();  
  12.         });  
  13.   
  14.         var stompClient = null;  
  15.   
  16.         function setConnected(connected) {  
  17.             document.getElementById('connect').disabled = connected;  
  18.             document.getElementById('disconnect').disabled = !connected;  
  19.             document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';  
  20.             document.getElementById('response').innerHTML = '';  
  21.         }  
  22.   
  23.         //this line.  
  24.         function connect() {  
  25.             var userid = document.getElementById('name').value;  
  26.             var socket = new SockJS("http://192.168.3.149:8080/springmvc/hello");  
  27.             stompClient = Stomp.over(socket);  
  28.             stompClient.connect({}, function(frame) {  
  29.                 setConnected(true);  
  30.                 console.log('Connected: ' + frame);  
  31.                 stompClient.subscribe('/topic/greetings', function(greeting){  
  32.                     showGreeting(JSON.parse(greeting.body).content);  
  33.                 });  
  34.   
  35.                 stompClient.subscribe('/user/' + userid + '/message',function(greeting){  
  36.                     alert(JSON.parse(greeting.body).content);  
  37.                     showGreeting(JSON.parse(greeting.body).content);  
  38.                 });  
  39.             });  
  40.         }  
  41.   
  42.         function sendName() {  
  43.             var name = document.getElementById('name').value;  
  44.             stompClient.send("/app/hello", {atytopic:"greetings"}, JSON.stringify({ 'name': name }));  
  45.         }  
  46.   
  47.         function connectAny() {  
  48.             var socket = new SockJS("http://localhost:8080/springmvc/hello");  
  49.             stompClient = Stomp.over(socket);  
  50.             stompClient.connect({}, function(frame) {  
  51.                 setConnected(true);  
  52.                 console.log('Connected: ' + frame);  
  53.                 stompClient.subscribe('/topic/feed', function(greeting){  
  54.                     alert(JSON.parse(greeting.body).content);  
  55.                     showGreeting(JSON.parse(greeting.body).content);  
  56.                 });  
  57.             });  
  58.         }  
  59.   
  60.         function disconnect() {  
  61.             if (stompClient != null) {  
  62.                 stompClient.disconnect();  
  63.             }  
  64.             setConnected(false);  
  65.             console.log("Disconnected");  
  66.         }  
  67.   
  68.   
  69.         function showGreeting(message) {  
  70.             var response = document.getElementById('response');  
  71.             var p = document.createElement('p');  
  72.             p.style.wordWrap = 'break-word';  
  73.             p.appendChild(document.createTextNode(message));  
  74.             response.appendChild(p);  
  75.         }  
  76.     </script>  
  77. </head>  
  78. <body>  
  79. <noscript><h2 style="color: #ff0000">Seems your browser doesn't support Javascript! Websocket relies on Javascript being enabled. Please enable  
  80.     Javascript and reload this page!</h2></noscript>  
  81. <div>  
  82.     <div>  
  83.         <button id="connect" onclick="connect();">Connect</button>  
  84.         <button id="connectAny" onclick="connectAny();">ConnectAny</button>  
  85.         <button id="disconnect" disabled="disabled" onclick="disconnect();">Disconnect</button>  
  86.     </div>  
  87.     <div id="conversationDiv">  
  88.         <label>What is your name?</label><input type="text" id="name" />  
  89.         <button id="sendName" onclick="sendName();">Send</button>  
  90.         <p id="response"></p>  
  91.     </div>  
  92. </div>  
  93. </body>  
  94. </html>  
PS:

stompClient.subscribe('/topic/greetings', function(greeting){
showGreeting(JSON.parse(greeting.body).content);

});该方法是接收广播消息。

stompClient.subscribe('/user/' + userid + '/message',function(greeting){
alert(JSON.parse(greeting.body).content);
        showGreeting(JSON.parse(greeting.body).content);
});该方法表示接收一对一消息,其主题是"/user/"+userId+"/message",不同客户端具有不同的id。如果两个或多个客户端具有相同的id,那么服务器端给该userId发送消息时,这些客户端都可以收到。

如果项目中配置了拦截器,浏览器console标签中会报如下错误:

sockjs.min.js:2 GET http://localhost:8200/s3captrue/endpoint/info?t=1480493527907
XMLHttpRequest cannot load http://localhost:8200/s3captrue/endpoint/info?t=1480493527907. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'null' is therefore not allowed access. The response had HTTP status code 500.

JAVA的控制台会输出如下错误:

java.lang.ClassCastException: org.springframework.web.socket.sockjs.support.SockJsHttpRequestHandler cannot be cast to org.springframework.web.method.HandlerMethod

则需要过滤掉对websoket的拦截:

[html]  view plain  copy
 print ?
  1. <mvc:interceptors>  
  2.         <mvc:interceptor>  
  3.             <mvc:mapping path="/**"/>  
  4.             <mvc:exclude-mapping path="/endpoint/**" />  
  5.             <bean class="com.hyy.common.interceptor.AuthInterceptor"/>  
  6.         </mvc:interceptor>  
  7.     </mvc:interceptors>  
PS:<mvc:exlude-mapping path="/endpoint/**" />标签中的/endpoint为WebConfig.java中registerStompEndpoints()方法中配置的stompEndpointRegistry.addEndpoint("/endpoint").setAllowedOrigins("*").withSockJS();

如果使用了Nginx服务器,需要在location中加入如下代码:

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值