1.STOMP的意义:
http协议是无状态协议,即每次请求时都不知道前面发生的什么。而且请求只能由浏览器发起,服务器只能响应该请求,不能主动发送消息给浏览器。这种单向的协议显然在很多场景下是不适用的,比如消息推送,股票实时行情。在websocket之前,我们通常使用Ajax轮询服务器或者使用长轮询,这两种方式都极大消耗了服务端和客户端的资源。而使用websocket,我们只需要借用http协议进行握手,然后保持着一个websocket连接,知道客户端主动断开。相对于另外两种方式,websocket只发送了一次http请求,当服务器有数据时再向浏览器推送数据,减少了带宽的使用以及服务器CPU使用率。
2.Websocket、Http、TCP、Socket之间关系:
HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的。
对于 WebSocket 来说,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。
Socket并不是一种协议,而是方便我们使用TCP/IP的一种封装,而 WebSocket 则不同,它是一个完整的 应用层协议,包含一套标准的 API 。
3.STOMP协议:
STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议
许多公司都提供了基于STOMP的服务器与客户端,若spring4开始支持的spring-websocket服务端,基于浏览器的stomp.js客户端
STOMP定义了客户端和服务器之间以Frame进行同行,Frame的格式为:
COMMAND
header1:value1
header2:value2
Body^@
COMMAND分为CONNECT、SEND、SUBSCRIBE、UNSUBSCRIBE、BEGIN、COMMIT、ABORT、ACK、NACK、DISCONNECT这几种。
COMMAND之后下一行紧跟着的是头部的键值对,之后加入一条空行,空行之后为body,即传递的消息实体。
传统HTPP请求响应:
websocket请求响应:
4.Spring配置stomp
环境配置:Spring4.3.9+tomcat8+jackson2.8.2
tomacat要8才支持stomp,spring4.0+以后支持stomp,而跨域问题可以通过下面代码解决,但是在spring4.0.9却没有setAllowedOrigins("*")这个函数,所以我选择4.3.9版本registry.addEndpoint("/endpoint").setAllowedOrigins("*").withSockJS();
而jackson版本过低的话和spring4.3.9不兼容,jackson2.8.2版本没有问题,项目所需类库下载地址:https://download.csdn.net/download/fxkcsdn/10536671。
如果没有jackson-core,jackson-databind,jackson-annotations这三个类,则浏览器连接会出现下面情况:
ok,终于要步入正题了: Spring配置stomp
下面开始我们的编程之旅:
第一步:编写实体类:
import java.io.Serializable;
public class JinNang implements Serializable{
private static final long serialVersionUID = 1L;
private String jice;//计策
private String people;//计策实施者
public JinNang(String jice,String people){
this.jice=jice;
this.people=people;
}
public String getJice() {
return jice;
}
public void setJice(String jice) {
this.jice = jice;
}
public String getPeople() {
return people;
}
public void setPeople(String people) {
this.people = people;
}
}
People.java
public class People {
private String name;
public People(){}
public People(String name){
this.name=name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
这两个实体类要有无参构造函数,待会的消息转换器会使用java映射来将浏览器发送的json数据转换为实体类。
第二步:配置启用代理的web消息功能:
如下的程序展示了如何通过java配置启用基于代理的Web消息功能:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
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;
import org.springframework.messaging.simp.SimpMessagingTemplate;
@Configuration
@ComponentScan
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic","/queue");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/endpoint").setAllowedOrigins("*").withSockJS();
}
}
WebSocketStompConfig 使用了 @EnableWebSocketMessageBroker 注解。这表明这个配置类不仅配置了 WebSocket ,还配置了基于代理的 STOMP 消息。它重载了 registerStompEndpoints() 方法,将 “/endpoint” 注册为 STOMP 端点。这个路径与之前发送和接收消息的目的地路径有所不同。这是一个端点,客户端在订阅或发布消息到目的地路径前,要连接该端点。WebSocketStompConfig 还通过重载 configureMessageBroker() 方法配置了一个简单的消息代理。这个方法是可选的,如果不重载它的话,将会自动配置一个简单的内存消息代理,用它来处理以 “/topic” 为前缀的消息。但是在本例中,我们重载了这个方法,所以消息代理将会处理前缀为 “/topic” 和 “/queue” 的消息。除此之外,发往应用程序的消息将会带有 “/app” 前缀。书上这个截图展示了配置中的消息流:
当消息到达时,目的地的前缀将会决定消息该如何处理。在图 18.2 中,应用程序的目的地以 “/app” 作为前缀,而代理的目的地以 “/topic” 和 “/queue” 作为前缀。以应用程序为目的地的消息将会直接路由到带有 @MessageMapping 注解的控制器方法中。而发送到代理上的消息,其中也包括 @MessageMapping 注解方法的返回值所形成的消息,将会路由到代理上,并最终发送到订阅
这些目的地的客户端.
第三步:构造控制器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
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.stereotype.Controller;
@Controller
public class ShuguoController {
// public SimpMessagingTemplate template;
//
// @Autowired
// public ShuguoController(SimpMessagingTemplate template) {
// this.template = template;
// }
@MessageMapping("/jinnang")
@SendTo("/topic/jinnang")
public JinNang getJinNang(People people) throws Exception {
System.out.println("people:"+people.getName());
return new JinNang("火烧赤壁",people.getName());
}
}
我们在控制器方法添加@MessageMapping注解,使其处理STOMP消息,他与带有@RequestMapping注解的方法处理HTTP请求的方式非常类似。但是。与@RequestMapping不同的是,@MessageMapping的功能无法通过@EnableWebMvc启用。Spring的Web消息功能基于消息代理构建,因此除了告诉Spring我们想要处理的消息以外,还有其他的内容需要配置。我们必须要配置一个消息代理和其他的一些消息目的地。
但是这个处理器方法与我们之前看到的有一点区别。 getJinnang() 方法没有使用 @RequestMapping 注解,而是使用了 @MessageMapping 注解。这表示 getJinnang()方法能够处理指定目的地上到达的消息。在本例中,这个目的地也就是 “/app/jingnang” ( “/app” 前缀是隐含的,因为我们将其配置为应用的目的地前缀)。因为 getJinnang()方法接收一个 Jinnang参数,所以 Spring 的某一个消息转换器会将 STOMP 消息的负载转换为 JinNang对象。
因为我们现在处理的不是HTTP,所以无法使用Spring的HttpMessageConverter实现将负载转换为JinNang对象。Spring4.0提供了几个消息转换器,作为API的一部分。下图描述了这些消息转换器,在处理STOMP消息的时候可能会用到他们。
假设getJinnang()方法所处理消息的内容类型为“application/json”(这是一个安全的假设,因为JinNang不是byte[]和String),MappingJackson2MessageConverter会负责将JSON消息转换为JinNang对象。就像在HTTP中对应的MappingJackson2HttpMessageConverter一样。MappingJackson2MessageConverter会将其任务委托给底层的Jackson2 JSON处理器。默认情况下,Jackson会使用反射将JSON属性映射为Java对象的属性。
第四步:编写客户端代码:
<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
<meta name="format-detection" content="telephone=no"/>
<meta name="format-detection" content="email=no"/>
<meta http-equiv="Cache-Control" content="no-cache"/>
<meta http-equiv="Pragma" content="no-cache"/>
<meta http-equiv="Expires" content="0"/>
<!--必须导入的三个脚本文件 -->
<script src="http://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<script src="http://cdn.bootcss.com/sockjs-client/1.1.4/sockjs.js"></script>
<script src="http://cdn.bootcss.com/stomp.js/2.3.3/stomp.js"></script>
</head>
<body class="test">
<script>
//定义stomp连接服务器的地址:只需要一次HTTP握手就可以进行连接。整个通讯过程是建立在一次连接/状态中,也就避免了HTTP的非状态性
var url = 'http://localhost:8080/SpringTest/endpoint'
var socket = new SockJS(url, undefined, {transports: ['websocket']});
//新建stomp客户端
var stompClient = Stomp.over(socket);
//stomp请求与服务器建立连接 connet({},function(),function())第一个参数Map是请求的头信息,第二个参数是请求成功回调函数,第三个函数是请求失败回调函数
stompClient.connect({}, function(frame) {
console.log("connected------------");
stompClient.subscribe("/topic/jinnang", handleJinNang);
sendMessage();
},
function(error){
console.log(error.headers.message)
}
);
function handleJinNang(result){
var jinnang=JSON.parse(result.body);
console.log("received:",jinnang);
document.getElementById("display").value=jinnang.people+"实施"+jinnang.jice;
}
function sendMessage(){
var message=JSON.stringify({"name":"黄盖"})
stompClient.send("/app/jinnang",{},message);
}
</script>
<input type="text" id="display"/>
</body>
</html>
send()方法传递的第二个参数是一个头信息的Map,他会包含在STOMP的帧中,不过在这个例子中,我们没有提供任何参数,Map是空的。
运行截图: