【0】README
1)本文旨在 介绍如何 利用 WebSocket 和 STOMP 实现消息功能;
2)要知道, WebSocket 是发送和接收消息的 底层API,而SockJS 是在 WebSocket 之上的 API;最后 STOMP(面向消息的简单文本协议)是基于 SockJS 的高级API
(干货——简而言之,WebSocket 是底层协议,SockJS 是WebSocket 的备选方案,也是 底层协议,而 STOMP 是基于 WebSocket(SockJS) 的上层协议)
3)broker==经纪人,代理;
4)当然,你可以直接跳转到 STOMP 知识(章节【3】);

【1】WebSocket
1)intro:WebSocket 协议提供了 通过一个套接字实现全双工通信的功能。也能够实现 web  浏览器 和 server 间的 异步通信, 全双工意味着 server 与 浏览器间 可以发送和接收消息。

【1.1】使用 spring 的低层级 WebSocket API
1)intro:为了在 spring 中 使用较低层级的 API 来处理消息。有如下方案:
scheme1)我们必须编写一个实现 WebSocketHandler:
public interface WebSocketHandler {
void afterConnectionEstablished(WebSocketSession session) throws Exception;
void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;
void handleTransportError(WebSocketSession session,Throwable exception) throws Exception;
void afterConnectionClosed(WebSocketSession session,CloseStatus closeStatus) throws Exception;
boolean supportsPartialMessages();
}

scheme2)当然,我们也可以扩展 AbstractWebSocketHandler(更加简单一点);
// you can also extends TextWebSocketHandler
public class ChatTextHandler extends AbstractWebSocketHandler {// handle text msg.@Overrideprotected void handleTextMessage(WebSocketSession session,TextMessage message) throws Exception {session.sendMessage(new TextMessage("hello world."));}
}

对以上代码的分析(Analysis): 当然了,我们还可以重载其他三个方法:
handleBinaryMessage()
handlePongMessage()
handleTextMessage()

scheme3)也可以扩展 TextWebSocketHandler(文本 WebSocket 处理器), 不在扩展AbstractWebSocketHandler, TextWebSocketHandler 继承 AbstractWebSocketHandler ;

2)你可能会关系建立和关闭连接感兴趣。可以重载 afterConnectionEstablished() and afterConnectionClosed():
// 当新连接建立的时候,被调用;
public void afterConnectionEstablished(WebSocketSession session)
throws Exception {
logger.info("Connection established");
}
// 当连接关闭时被调用;
@Override
public void afterConnectionClosed(
WebSocketSession session, CloseStatus status) throws Exception {
logger.info("Connection closed. Status: " + status);
}
3)现在已经有了 message handler 类了,下面对其进行配置,配置到 springmvc 的运行环境中。
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer{@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(getTextHandler(), "/websocket/p2ptext");} // 将 ChatTextHandler 处理器 映射到  /websocket/p2ptext 路径下.@Beanpublic ChatTextHandler getTextHandler() {return new ChatTextHandler();}
}
对上述代码的分析(Analysis):registerWebSocketHandlers方法 是注册消息处理器的关键: 通过 调用WebSocketHandlerRegistry .addHandler() 方法 来注册信息处理器;
Attention)server 端的 WebSocket 配置完毕,下面配置客户端;

4)WebSocket 客户端配置
4.1)client 发送 一个文本到 server,他监听来自 server 的文本消息。下面代码 展示了 利用 js 开启一个原始的 WebSocket 并使用它来发送消息给server;
4.2)代码如下:
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%><html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! -->
<title>web socket</title><link href="<c:url value="/"/>bootstrap/css/bootstrap.min.css"
rel="stylesheet"><!-- jQuery (necessary for Bootstrap's JavaScript plugins) -->
<script src="<c:url value="/"/>bootstrap/jquery/jquery.min.js"></script>
<!-- Include all compiled plugins (below), or include individual files as needed -->
<script src="<c:url value="/"/>bootstrap/js/bootstrap.min.js"></script><script type="text/javascript">
$(document).ready(function() {
websocket_client();
});function websocket_client() {
var hostaddr = window.location.host + "<c:url value='/websocket/p2ptext' />";
var url = 'ws://' + hostaddr;
var sock = new WebSocket(url);// 以下的 open(), onmessage(), onclose()
// 对应到 ChatTextHandler 的
// afterConnectionEstablished(), handleTextMessage(), afterConnectionClosed();sock.open = function() {
alert("open successfully.");
sayMarco();
};sock.onmessage = function(e) {
alert("onmessage");
alert(e);
};sock.onclose = function() {
alert("close");
};function sayMarco() {
sock.send("this is the websocket client.");
}
}
</script>
</head><body>
<div id="websocket">
websocket div.
</div>
</body>
</html>

error)这样配置后, WebSocket 无法正常运行;

【2】应对不支持 WebSocket 的场景(引入 SockJS)
1)problem+solutions:
1.1)problem:许多浏览器不支持 WebSocket 协议;
1.2)solutions: SockJS 是 WebSocket 技术的一种模拟。SockJS 会 尽可能对应 WebSocket API,但如果 WebSocket 技术 不可用的话,就会选择另外的 通信方式协议;

2)SockJS 会优先选择 WebSocket 协议,但是如果 WebSocket协议不可用的话,他就会从如下 方案中挑选最优可行方案:
XHR streaming
XDR streaming
iFrame event source
iFrame HTML file
XHR polling
XDR polling
iFrame XHR polling
JSONP polling
3)如何在 server 端配置 SockJS :添加withSockJS() 方法;
// 将 ChatTextHandler 映射到 /chat/text 路径下.@Overridepublic void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {registry.addHandler(getTextHandler(), "/websocket/p2ptext").withSockJS();
// withSockJS() 方法声明我们想要使用 SockJS 功能,如果WebSocket不可用的话,会使用 SockJS;}

4)客户端配置 SockJS, 想要确保 加载了 SockJS 客户端;

4.1)具体做法是 依赖于 JavaScript 模块加载器(如 require.js or curl.js) 还是简单使用 <script> 标签加载 JavaScript 库。最简单的方法是 使用 <script> 标签从 SockJS CDN 中进行加载,如下所示:
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
Attention)用 WebJars 解析 Web资源(可选,有兴趣的童鞋可以尝试下)
A1)在springmvc 配置中搭建一个 资源处理器,让它负责解析路径以 "webjars/**" 开头的请求,这也是 WebJars 的标准路径:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}

A2)在这个资源处理器 准备就绪后,我们可以在 web 页面中使用 如下的 <script> 标签加载 SockJS 库;
<script src="sockjs.min.js}"> </script>

5)处理加载 SockJS 客户端库以外,还要修改 两行代码:
var url = 'p2ptext';
var sock = new SockJS(url);
对以上代码的分析(Analysis): 
A1)SockJS 所处理的URL 是 "http://" 或 "https://" 模式,而不是 "ws://" or  "wss://" ;
A2)其他的函数如 onopen, onmessage, and  onclose ,SockJS 客户端与 WebSocket 一样;
6)SockJS 为 WebSocket 提供了 备选方案。但无论哪种场景,对于实际应用来说,这种通信形式层级过低。下面看一下如何 在 WebSocket 之上使用 STOMP协议,来为浏览器 和 server间的 通信增加适当的消息语义;(干货——引入 STOMP—— Simple Text Oriented Message Protocol——面向消息的简单文本协议)

【3】使用 STOMP消息
1)intro: 如何理解 STOMP 与 WebSocket 的关系:
1.1)假设 HTTP 协议 并不存在,只能使用 TCP 套接字来 编写 web 应用,你可能认为这是一件疯狂的 事情;
1.2)不过 幸好,我们有 HTTP协议,它解决了 web 浏览器发起请求以及 web 服务器响应请求的细节;
1.3)直接使用 WebSocket(SockJS) 就很类似于 使用 TCP 套接字来编写 web 应用;因为没有高层协议,因此就需要我们定义应用间所发送消息的语义,还需要确保 连接的两端都能遵循这些语义;
1.4)同 HTTP 在 TCP 套接字上添加 请求-响应 模型层一样,STOMP 在 WebSocket 之上提供了一个基于 帧的线路格式层,用来定义消息语义;(干货——STOMP 在 WebSocket 之上提供了一个基于 帧的线路格式层,用来定义消息语义)
2)STOMP 帧:该帧由命令,一个或多个 头信息 以及 负载所组成。如下就是发送 数据的一个 STOMP帧:(干货——引入了 STOMP帧格式)
SEND
destination:/app/marco
content-length:20{\"message\":\"Marco!\"}
对以上代码的分析(Analysis):
A1)SEND:STOMP命令,表明会发送一些内容;
A2)destination:头信息,用来表示消息发送到哪里;
A3)content-length:头信息,用来表示 负载内容的 大小;
A4)空行:
A5)帧内容(负载)内容

3)STOMP帧 信息 最有意思的是 destination头信息了: 它表明 STOMP 是一个消息协议,类似于 JMS 或 AMQP。消息会发送到 某个 目的地,这个 目的地实际上可能真的 有消息代理作为 支撑。另一方面,消息处理器 也可以监听这些目的地,接收所发送过来的消息;

【3.1】启用STOMP 消息功能
1)intro:spring 的消息功能是基于消息代理构建的,因此我们必须要配置一个 消息代理 和 其他的一些消息目的地;(干货——spring 的消息功能是基于消息代理构建的)
2)如下代码展现了 如何通过 java配置 启用基于代理的的web 消息功能;
(干货——@EnableWebSocketMessageBroker 注解的作用: 能够在 WebSocket 上启用 STOMP)

package com.spring.spittr.web;import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {config.enableSimpleBroker("/topic", "/queue");config.setApplicationDestinationPrefixes("/app");// 应用程序以 /app 为前缀,而 代理目的地以 /topic 为前缀.// js.url = "/spring13/app/hello" -> @MessageMapping("/hello") 注释的方法.}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/hello").withSockJS();// 在网页上我们就可以通过这个链接 /server/hello ==<c:url value='/hello'></span> 来和服务器的WebSocket连接}
}
对以上代码的分析(Analysis):
A1)EnableWebSocketMessageBroker注解表明: 这个配置类不仅配置了 WebSocket,还配置了 基于代理的 STOMP 消息;
A2)它重载了 registerStompEndpoints() 方法:将 "/hello" 路径 注册为 STOMP 端点。这个路径与之前发送和接收消息的目的路径有所不同, 这是一个端点,客户端在订阅或发布消息 到目的地址前,要连接该端点,即 用户发送请求 url='/server/hello' 与 STOMP server 进行连接,之后再转发到 订阅url;(server== name of your springmvc project(干货——端点的作用——客户端在订阅或发布消息 到目的地址前,要连接该端点)
A3)它重载了 configureMessageBroker() 方法:配置了一个 简单的消息代理。如果不重载,默认case下,会自动配置一个简单的 内存消息代理,用来处理 "/topic" 为前缀的消息。但经过重载后,消息代理将会处理前缀为 "/topic" and "/queue" 消息。
A4)之外:发送应用程序的消息将会带有 "/app" 前缀,下图展现了 这个配置中的 消息流;
对上述处理step的分析(Analysis):
A1)应用程序的目的地 以 "/app" 为前缀,而代理的目的地以 "/topic" 和 "/queue" 作为前缀;
A2)以应用程序为目的地的消息将会直接路由到 带有 @MessageMapping 注解的控制器方法中;(干货——@MessageMapping的作用)
A3)而发送到 代理上的消息,包括 @MessageMapping注解方法的返回值所形成的消息,将会路由到 代理上,并最终发送到 订阅这些目的地客户端;
(干货——client 连接地址和 发送地址是不同的,以本例为例,前者是/server/hello, 后者是/server/app/XX,先连接后发送)

【3.1.1】启用 STOMP 代理中继
1)intro:在生成环境下,可能会希望使用 真正支持 STOMP 的代理来支持 WebSocket 消息,如RabbitMQ 或 ActiveMQ。这样的代理提供了可扩展性和健壮性更好的消息功能,当然,他们也支持 STOMP 命令;
2)如何 使用 STOMP 代理来替换内存代理,代码如下:
@Overridepublic void configureMessageBroker(MessageBrokerRegistry registry) {// 启用了 STOMP 代理中继功能,并将其代理目的地前缀设置为 /topic and /queue .registry.enableStompBrokerRelay("/queue", "/topic").setRelayPort(62623);registry.setApplicationDestinationPrefixes("/app"); // 应用程序目的地.}
对以上代码的分析(Analysis):(干货——STOMP代理前缀和 应用程序前缀的意义)
A1)方法第一行启用了 STOMP 代理中继功能: 并将其目的地前缀设置为 "/topic" or "/queue" ;spring就能知道 所有目的地前缀为 "/topic" or "/queue" 的消息都会发送到 STOMP 代理中;
A2)方法第二行设置了 应用的前缀为 "app":所有目的地以 "/app" 打头的消息(发送消息url not 连接url)都会路由到 带有 @MessageMapping 注解的方法中,而不会发布到 代理队列或主题中;
3)下图阐述了 代理中继如何 应用于 spring 的 STOMP 消息处理之中。与 上图的 关键区别在于: 这里不再模拟STOMP 代理的功能,而是由 代理中继将消息传送到一个 真正的消息代理来进行处理;
Attention)
A1)enableStompBrokerRelay() and setApplicationDestinationPrefixes() 方法都可以接收变长 参数;
A2)默认情况下: STOMP 代理中继会假设 代理监听 localhost 的61613 端口,并且 client 的 username 和password 均为 guest。当然你也可以自行定义;
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableStompBrokerRelay("/topic", "/queue")
.setRelayHost("rabbit.someotherserver")
.setRelayPort(62623)
.setClientLogin("marcopolo")
.setClientPasscode("letmein01");
registry.setApplicationDestinationPrefixes("/app", "/foo");
} // setXXX()方法 是可选的

【3.2】 处理来自客户端的 STOMP 消息
1)借助 @MessageMapping 注解能够 在 控制器中处理 STOMP 消息

package com.spring.spittr.web;import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;import com.spring.pojo.Greeting;
import com.spring.pojo.HelloMessage;@Controller
public class GreetingController {@MessageMapping("/hello")@SendTo("/topic/greetings")public Greeting greeting(HelloMessage message) throws Exception {System.out.println("receiving " + message.getName());System.out.println("connecting successfully.");return new Greeting("Hello, " + message.getName() + "!");}
}

对以上代码的分析(Analysis):

A1)@MessageMapping注解:表示 handleShout()方法能够处理 指定目的地上到达的消息;
A2)这个目的地(消息发送目的地url)就是 "/server/app/hello",其中 "/app" 是 隐含的 ,"/server" 是 springmvc 项目名称;

2)因为我们现在处理的 不是 HTTP,所以无法使用 spring 的 HttpMessageConverter 实现 将负载转换为Shout 对象。Spring 4.0 提供了几个消息转换器如下:(Attention, 如果是传输json数据的话,定要添加 Jackson jar 包到你的springmvc 项目中,不然连接不会成功的)

【3.2.1】处理订阅(@SubscribeMapping注解)
1)@SubscribeMapping注解 的方法:当收到 STOMP 订阅消息的时候,带有 @SubscribeMapping 注解 的方法将会触发;其也是通过 AnnotationMethodMessageHandler 来接收消息的;
2)@SubscribeMapping注解的应用场景:实现 请求-回应模式。在请求-回应模式中,客户端订阅一个目的地,然后预期在这个目的地上 获得一个一次性的 响应;(干货——引入了@SubsribeMapping注解实现 请求-回应模式)
2.1)看个荔枝:
@SubscribeMapping({"/marco"})
public Shout handleSubscription() {
Shout outgoing = new Shout();
outgoing.setMessage("Polo!");
return outgoing;
}
对以上代码的分析(Analysis):
A1)@SubscribeMapping注解 的方法来处理 对 "/app/macro" 目的地订阅(与 @MessageMapping类似,"/app" 是隐含的 );
A2)请求-回应模式与 HTTP GET 的全球-响应模式差不多: 关键区别在于, HTTP GET 请求是同步的,而订阅的全球-回应模式是异步的,这样客户端能够在回应可用时再去处理,而不必等待;(干货——HTTP GET 请求是同步的,而订阅的请求-回应模式是异步的)

【3.2.2】编写 JavaScript 客户端
1)intro:借助 STOMP 库,通过 JavaScript发送消息

<script type="text/javascript">var stompClient = null;function setConnected(connected) {document.getElementById('connect').disabled = connected;document.getElementById('disconnect').disabled = !connected;document.getElementById('conversationDiv').style.visibility = connected ? 'visible' : 'hidden';document.getElementById('response').innerHTML = '';}function connect() {var socket = new SockJS("<c:url value='/hello'/>");stompClient = Stomp.over(socket);stompClient.connect({}, function(frame) {setConnected(true);console.log('Connected: ' + frame);stompClient.subscribe('/topic/greetings', function(greeting){showGreeting(JSON.parse(greeting.body).content);});});}function disconnect() {if (stompClient != null) {stompClient.disconnect();}setConnected(false);console.log("Disconnected");}function sendName() {var name = document.getElementById('name').value;stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));}function showGreeting(message) {var response = document.getElementById('response');var p = document.createElement('p');p.style.wordWrap = 'break-word';p.appendChild(document.createTextNode(message));response.appendChild(p);}</script>

对以上代码的 分析(Analysis): 以上代码连接“/hello” 端点并发送 ”name“;

2)stompClient.send("/app/hello", {}, JSON.stringify({'name':name})): 第一个参数:json 负载消息发送的 目的地; 第二个参数:是一个头信息的Map,它会包含在 STOMP 帧中;第三个参数:负载消息;
(干货—— stomp client 连接地址 和 发送地址不一样的,连接地址为 <c:url value='/hello'/> ==localhost:8080/springmvc_project_name/hello , 而 发送地址为 '/app/hello',这里要当心)
downloading these files below fromhttps://github.com/pacosonTang/SpringInAction/tree/master/spring18
<script src="<c:url value="/resources/sockjs-1.1.1.js" />"></script><script src="<c:url value="/resources/stomp.js" />"></script>
//this line.function connect() {var socket = new SockJS("<c:url value='/hello'/>");stompClient = Stomp.over(socket);stompClient.connect({}, function(frame) {setConnected(true);console.log('Connected: ' + frame);stompClient.subscribe('/topic/greetings', function(greeting){showGreeting(JSON.parse(greeting.body).content);});stompClient.subscribe('/app/macro',function(greeting){alert(JSON.parse(greeting.body).content);showGreeting(JSON.parse(greeting.body).content);});});}function sendName() {var name = document.getElementById('name').value;stompClient.send("/app/hello", {}, JSON.stringify({ 'name': name }));}
package com.spring.spittr.web;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.SimpMessageSendingOperations;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.messaging.simp.annotation.SubscribeMapping;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;import com.spring.pojo.Greeting;
import com.spring.pojo.HelloMessage;@Controller
public class GreetingController {// @MessageMapping defines the sending addr for client.// 消息发送地址: /server/app/hello@MessageMapping("/hello")@SendTo("/topic/greetings")public Greeting greeting(HelloMessage message) throws Exception {System.out.println("receiving " + message.getName());System.out.println("connecting successfully.");return new Greeting("Hello, " + message.getName() + "!");}@SubscribeMapping("/macro")public Greeting handleSubscription() {System.out.println("this is the @SubscribeMapping('/marco')");Greeting greeting = new Greeting("i am a msg from SubscribeMapping('/macro').");return greeting;}/*@MessageMapping("/feed")@SendTo("/topic/feed")public Greeting greetingForFeed(HelloMessage message) throws Exception {System.out.println("receiving " + message.getName());System.out.println("connecting successfully.");return new Greeting("i am /topic/feed, hello " + message.getName() + "!");}*///  private SimpMessagingTemplate  template;// SimpMessagingTemplate implements SimpMessageSendingOperations. private SimpMessageSendingOperations  template;@Autowiredpublic GreetingController(SimpMessageSendingOperations  template) {this.template = template;}@RequestMapping(path="/feed", method=RequestMethod.POST)public void greet(@RequestParam String greeting) {String text = "you said just now " + greeting;this.template.convertAndSend("/topic/feed", text);}
}
package com.spring.spittr.web;import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {config.enableSimpleBroker("/topic", "/queue");config.setApplicationDestinationPrefixes("/app");// 应用程序以 /app 为前缀,而 代理目的地以 /topic 为前缀.// js.url = "/spring13/app/hello" -> @MessageMapping("/hello") 注释的方法.}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {registry.addEndpoint("/hello").withSockJS();// 在网页上我们就可以通过这个链接 /server/hello 来和服务器的WebSocket连接}
}
package com.spring.spittr.web;import java.io.IOException;import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.web.multipart.MultipartResolver;
import org.springframework.web.multipart.commons.CommonsMultipartResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import org.springframework.web.servlet.view.tiles3.TilesConfigurer;
import org.springframework.web.servlet.view.tiles3.TilesViewResolver;@Configuration
@ComponentScan(basePackages = { "com.spring.spittr.web" })
@EnableWebMvc
@Import({WebSocketConfig.class})
public class WebConfig extends WebMvcConfigurerAdapter {@Beanpublic TilesConfigurer tilesConfigurer() {TilesConfigurer tiles = new TilesConfigurer();tiles.setDefinitions(new String[] { "/WEB-INF/layout/tiles.xml" });tiles.setCheckRefresh(true);return tiles;}// config processing for static resources.@Overridepublic void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {configurer.enable();}// InternalResourceViewResolver @Beanpublic ViewResolver viewResolver1() {TilesViewResolver resolver = new TilesViewResolver();return resolver;}@Beanpublic ViewResolver viewResolver2() {InternalResourceViewResolver resolver = new InternalResourceViewResolver();resolver.setPrefix("/WEB-INF/views/");resolver.setSuffix(".jsp");resolver.setExposeContextBeansAsAttributes(true);resolver.setViewClass(org.springframework.web.servlet.view.JstlView.class);return resolver;}@Beanpublic MessageSource messageSource() {ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();messageSource.setBasename("messages"); return messageSource;}@Beanpublic MultipartResolver multipartResolver() throws IOException {CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();multipartResolver.setUploadTempDir(new FileSystemResource("/WEB-INF/tmp/spittr/uploads"));multipartResolver.setMaxUploadSize(2097152);multipartResolver.setMaxInMemorySize(0);return multipartResolver;}
}
【3.3】发送消息到客户端
1)intro:spring提供了两种 发送数据到 client 的方法:
method1)作为处理消息 或处理订阅的附带结果;
method2)使用消息模板;

【3.3.1】在处理消息后,发送消息(server 对 client 请求的 响应消息)
1)intro:如果你想要在接收消息的时候,在响应中发送一条消息,修改方法签名 不是void 类型即可, 如下:

@MessageMapping("/hello")@SendTo("/topic/greetings") //highlight line.public Greeting greeting(HelloMessage message) throws Exception {System.out.println("receiving " + message.getName());System.out.println("connecting successfully.");return new Greeting("Hello, " + message.getName() + "!");}

对以上代码的分析(Analysis):返回的对象将会进行转换(通过消息转换器) 并放到 STOMP 帧的负载中,然后发送给消息代理(消息代理分为 STOMP代理中继 和 内存消息代理)

2)默认情况下:帧所发往的目的地会与 触发 处理器方法的目的地相同。所以返回的对象 会写入到 STOMP 帧的负载中,并发布到 "/topic/stomp" 目的地。不过,可以通过 @SendTo 注解,重载目的地;(干货——注解@SendTo 注解的作用)
代码同上。
对以上代码的分析(Analysis):
消息将会发布到 /topic/hello, 所有订阅这个主题的应用都会收到这条消息;

3)@SubscriptionMapping 注解标注的方式也能发送一条消息,作为订阅的回应。
3.1)看个荔枝: 通过为 控制器添加如下的方法,当客户端订阅的时候,将会发送一条 shout 信息:
@SubscribeMapping("/macro") // defined in Controller. attention for addr '/macro' in server.public Greeting handleSubscription() {System.out.println("this is the @SubscribeMapping('/marco')");Greeting greeting = new Greeting("i am a msg from SubscribeMapping('/macro').");return greeting;}
function connect() {var socket = new SockJS("<c:url value='/hello'/>");stompClient = Stomp.over(socket);stompClient.connect({}, function(frame) {setConnected(true);console.log('Connected: ' + frame);stompClient.subscribe('/topic/greetings', function(greeting){showGreeting(JSON.parse(greeting.body).content);});// starting line.stompClient.subscribe('/app/macro',function(greeting){alert(JSON.parse(greeting.body).content);showGreeting(JSON.parse(greeting.body).content);}); // ending line. attention for addr '/app/macro' in client.});} 
对以上代码的分析(Analysis):
A0)这个SubscribeMapping annotation标记的方法,是在订阅的时候调用的,也就是说,基本是只执行一次的方法,client 调用定义在server 的 该 Annotation 标注的方法,它就会返回结果,不过经过代理。
A1)这里的 @SubscribeMapping 注解表明当 客户端订阅 "/app/macro" 主题的时候("/app"是应用目的地的前缀,注意,这里没有加springmvc 项目名称前缀), 将会调用 handleSubscription 方法。它所返回的shout 对象 将会进行转换 并发送回client;
A2)SubscribeMapping注解的区别在于: 这里的 Shout 消息将会直接发送给 client,不用经过 消息代理;但,如果为方法添加 @SendTo 注解的话,那么 消息将会发送到指定的目的地,这样就会经过代理;(干货——SubscribeMapping注解返回的消息直接发送到 client,不经过代理,而 @SendTo 注解的路径,就会经过代理,然后再发送到 目的地)

【3.3.2】 在应用的任意地方发送消息
1)intro:spring 的 SimpMessagingTemplate 能够在应用的任何地方发送消息,不必以接收一条消息为 前提;
2)看个荔枝: 让首页订阅一个 STOMP主题,在 Spittle 创建的时候,该主题能够收到 Spittle 更新时的 feed;
2.1)JavaScript 代码:
<script>
var sock = new SockJS('spittr');
var stomp = Stomp.over(sock);
stomp.connect('guest', 'guest', function(frame) {
console.log('Connected');
stomp.subscribe("/topic/spittlefeed", handleSpittle); // highlight.
});
function handleSpittle(incoming) {
var spittle = JSON.parse(incoming.body);
console.log('Received: ', spittle);
var source = $("#spittle-template").html();
var template = Handlebars.compile(source);
var spittleHtml = template(spittle);
$('.spittleList').prepend(spittleHtml);
}
</script>
对以上代码的分析(Analysis): 在连接到 STMOP 代理后,我们订阅了 "/topic/spittlefeed" 主题,并指定当消息到达的是,由 handleSpittle()函数来处理 Spittle 更新。
2.2) server 端代码:使用 SimpMessagingTemplate 将所有新创建的 Spittle 以消息的形式发布到 "/topic/feed" 主题上;
@Service
public class SpittleFeedServiceImpl implements SpittleFeedService {
private SimpMessageSendingOperations messaging;
@Autowired
public SpittleFeedServiceImpl(
SimpMessageSendingOperations messaging) { // 注入消息模板.
this.messaging = messaging;
}
public void broadcastSpittle(Spittle spittle) {
messaging.convertAndSend("/topic/spittlefeed", spittle); // 发送消息.
}
}
对以上代码的分析(Analysis):
A1)配置 spring 支持 stomp 的一个附带功能是 在spring应用上下文中已经包含了 Simple
A2)在发布消息给 STOMP 主题的时候,所有订阅该主题的客户端都会收到消息。但有的时候,我们希望将消息发送给指定用户;

【4】 为目标用户发送消息
1)intro:在使用 srping 和 STOMP 消息功能的时候,有三种方式来利用认证用户:
way1)@MessageMapping and @SubscribeMapping 注解标注的方法 能够使用 Principal 来获取认证用户;
way2)@MessageMapping, @SubscribeMapping, and @MessageException 方法返回的值能够以 消息的形式发送给 认证用户;
way3)SimpMessagingTemplate 能够发送消息给特定用户;

【4.1】在控制器中处理用户的 消息
1)看个荔枝: 编写一个控制器方法,根据传入的消息创建新的Spittle 对象,并发送一个回应,表明 对象创建成功;(这种 REST也可以实现,不过它是同步的,而这里是异步的);
1.1)代码如下:它会处理传入的消息并将其存储我 Spittle:
@MessageMapping("/spittle")
@SendToUser("/queue/notifications")
public Notification handleSpittle(
Principal principal, SpittleForm form) {
Spittle spittle = new Spittle(
principal.getName(), form.getText(), new Date());
spittleRepo.save(spittle);
return new Notification("Saved Spittle");
}

1.2)该方法最后返回一个 新的 Notificatino,表明对象保存成功;
1.3)该方法使用了 @MessageMapping("/spittle") 注解,所以当有发往 "/app/spittle" 目的地的消息 到达时,该方法就会触发;如果用户已经认证的话,将会根据 STOMP 帧上的头信息得到 Principal 对象;
1.4)@SendToUser注解: 指定了 Notification 要发送的 目的地 "/queue/notifications";
1.5)表明上, "/queue/notifications" 并不会与 特定用户相关联,但因为 这里使用的是 @SendToUser注解, 而不是 @SendTo,所以 就会发生更多的事情了;
2)看一下针对 控制器方法发布的 Notificatino 对象的目的地,客户端该如何进行订阅。
2.1)看个荔枝:考虑如下的 JavaScript代码,它订阅了一个 用户特定的 目的地:
stomp.subscribe("/user/queue/notifications", handleNotifications);

对以上代码的分析(Analysis):这个目的地使用了 "/user" 作为前缀,在内部,以"/user" 为前缀的消息将会通过 UserDestinationMessageHandler 进行处理,而不是 AnnotationMethodMessageHandler 或  SimpleBrokerMessageHandler or StompBrokerRelayMessageHandler,如下图所示:

Attention)UserDestinationMessageHandler 的主要任务: 是 将用户消息重新路由到 某个用户独有的目的地上。 在处理订阅的时候,它会将目标地址中的 "/user" 前缀去掉,并基于用户 的会话添加一个后缀。如,对  "/user/queue/notifications" 的订阅最后可能路由到 名为 "/queue/notifacations-user65a4sdfa" 目的地上;

【4.2】为指定用户发送消息
1)intro:SimpMessagingTemplate还提供了 convertAndSendToUser() 方法,该方法能够让 我们给特定用户发送消息;
2)我们在 web 应用上添加一个特性: 当其他用户提交的 Spittle 提到某个用户时,将会提醒该用户(干货——这难道不是 微博的 @ 功能吗)
2.1)看个荔枝:如果Spittle 文本中包含 "@tangrong",那么我们就应该发送一条消息给 使用 tangrong 用户名登录的client,代码实例如下:
@Service
public class SpittleFeedServiceImpl implements SpittleFeedService {
private SimpMessagingTemplate messaging;
// 实现用户提及功能的正则表达式
private Pattern pattern = Pattern.compile("\\@(\\S+)"); @Autowired
public SpittleFeedServiceImpl(SimpMessagingTemplate messaging) {
this.messaging = messaging;
}
public void broadcastSpittle(Spittle spittle) {
messaging.convertAndSend("/topic/spittlefeed", spittle);
Matcher matcher = pattern.matcher(spittle.getMessage());
if (matcher.find()) {
String username = matcher.group(1);
// 发送提醒给用户.
messaging.convertAndSendToUser(
username, "/queue/notifications",
new Notification("You just got mentioned!"));
}
}
}
【5】处理消息异常
1)intro:我们也可以在 控制器方法上添加 @MessageExceptionHandler 注解,让它来处理 @MessageMapping 方法所抛出的异常;
2)看个荔枝:它会处理 消息方法所抛出的异常;
@MessageExceptionHandler
public void handleExceptions(Throwable t) {
logger.error("Error handling message: " + t.getMessage());
}
3)我们也可以以 参数的形式声明它所能处理的异常;
@MessageExceptionHandler(SpittleException.class) // highlight line.
public void handleExceptions(Throwable t) {
logger.error("Error handling message: " + t.getMessage());
}
// 或者:
@MessageExceptionHandler( {SpittleException.class, DatabaseException.class})  // highlight line.
public void handleExceptions(Throwable t) {
logger.error("Error handling message: " + t.getMessage());
}
4)该方法还可以回应一个错误:
@MessageExceptionHandler(SpittleException.class)
@SendToUser("/queue/errors")
public SpittleException handleExceptions(SpittleException e) {
logger.error("Error handling message: " + e.getMessage());
return e;
}
// 如果抛出 SpittleException 的话,将会记录这个异常,并将其返回.
// 而 UserDestinationMessageHandler 会重新路由这个消息到特定用户所对应的 唯一路径;

springmvc(18)使用WebSocket 和 STOMP 实现消息功能相关推荐

  1. Spring使用WebSocket、SockJS、STOMP实现消息功能

    WebSocket 概述 WebSocket协议提供了通过一个套接字实现全双工通信的功能.除了其他的功能之外,它能够实现Web浏览器和服务器之间的异步通信.全双工意味着服务器可以发送消息给浏览器,浏览 ...

  2. Spring Boot 2 中通过 WebSocket 发送 STOMP 消息

    描述 在这篇博客中,我们将了解如何设置应用程序以通过 WebSocket 连接,发送和接收 STOMP 消息.我们将以 Spring Boot 2 为基础,因为它包含对 STOMP 和 WebSock ...

  3. springboot+rabbitmq+vue实现stomp协议消息推送

    springboot+rabbitmq+vue实现stomp协议消息推送 一.rabbitmq添加stomp插件 rabbitmq 默认是没有开启Socket STOMP插件的.如需使用,例如集成sp ...

  4. 基于websocket的网页实时消息推送与在线聊天(上篇)

    文章目录 @[toc] 基于websocket的网页实时消息推送与在线聊天(上篇) "使用dwebsocket在django中实现websocket" websocket原理图 d ...

  5. 抓取WebSocket推送的消息

    介绍 很多直播或对数据及时性要求比较高的网站,使用了WebSocket.这种数据要怎么抓呢? 我们这里以socket.io为例,我们可以查看网站网页源代码看使用的H5的WebSocket还是socke ...

  6. SpringBoot +WebSocket实现简单聊天室功能实例

    SpringBoot +WebSocket实现简单聊天室功能实例) 一.代码来源 二.依赖下载 三.数据库准备(sql) 数据库建表并插入sql 四.resources文件配置 application ...

  7. WebSocket(3)---实现一对一聊天功能

    实现一对一聊天功能 功能介绍:实现A和B单独聊天功能,即A发消息给B只能B接收,同样B向A发消息只能A接收. 本篇博客是在上一遍基础上搭建,上一篇博客地址:[WebSocket]---实现游戏公告功能 ...

  8. html5利用websocket完成的推送功能(tomcat)

    html5利用websocket完成的推送功能(tomcat) 利用websocket和java完成的消息推送功能,服务器用的是tomcat7.0.42,一些东西是自己琢磨的,也不知道恰不恰当,不恰当 ...

  9. 微信公众号开发 [04] 模板消息功能的开发

    1.模板消息的概况 模板消息的定位是用户触发后的通知消息,不允许在用户没做任何操作或未经用户同意接收的前提下,主动下发消息给用户.目前在特殊情况下允许主动下发的消息只有故障类和灾害警示警告类通知,除此 ...

最新文章

  1. swagger2中UI界面接口点击无法展开问题解决
  2. linux redhat 下命令行全部乱码解决
  3. 天才王垠惊人言论炸翻网友:相对论是假说,爱因斯坦是民科!
  4. 微信开发者工具:Failed to load font ************** net::ERR_CONNECTION_RESET问题解决办法
  5. php study是什么,phpstudy有什么用
  6. javacript IO
  7. UE4中Bebavior Tree中Delay及其后面代码失效的原因
  8. 从输入 URL 到页面展示,这中间发生了什么?
  9. Java中一个逐渐被遗忘的强大功能,强到你难以置信!
  10. 敏感词过滤的php代码,ThinkPHP敏感词汇过滤
  11. 软考初级程序员常见类型题,错题个人笔记
  12. Mac下用命令行打开pdf文件
  13. 【SCOI 2005】王室联邦 树上分块?
  14. linux的ping命令含义,Linux ping命令详解
  15. 计算机学院新增电子信息!齐鲁工业大学
  16. arm方案商,三星S5P6818开发板ARM Cortex-A53架构
  17. 微信小程序 环形进度条_微信小程序实现圆形进度条实例分享
  18. edge浏览器如何设置无痕浏览 无痕浏览网页方法
  19. 美科学家试解“姆潘巴现象”
  20. 首届长三角青少年人工智能擂台赛全记录(YOLOv5+Win10+Anaconda+Pycharm+ModelArts)

热门文章

  1. 【WC2016】挑战NPC 【带花树】【建图】
  2. 【bzoj2555】Substring【后缀平衡树入门】
  3. 牛客题霸 [ 判断一棵二叉树是否为搜索二叉树和完全二叉树] C++题解/答案
  4. Poj 1284 Primitive Roots
  5. [CodeForces 1603C] Extreme Extension(贪心 + 数论分块优化dp)
  6. YBTOJ:魔法数字(数位dp)
  7. 多重背包的二进制优化(ybtoj-宝物筛选)
  8. CF917D-Stranger Trees【矩阵树定理,高斯消元】
  9. USACO2.4の其中3道水题【模拟,图论】
  10. tyvj/joyOI 1305-最大子序和【单调队列】