【Spring集成】-Spring配置stomp

1.STOMP的意义:

http协议是无状态协议,即每次请求时都不知道前面发生的什么。并且请求只能由浏览器发起,服务器只能响应该请求,不能主动发送消息给浏览器。这种单向的协议显然在不少场景下是不适用的,好比消息推送,股票实时行情。在websocket以前,咱们一般使用Ajax轮询服务器或者使用长轮询,这两种方式都极大消耗了服务端和客户端的资源。而使用websocket,咱们只须要借用http协议进行握手,而后保持着一个websocket链接,知道客户端主动断开。相对于另外两种方式,websocket只发送了一次http请求,当服务器有数据时再向浏览器推送数据,减小了带宽的使用以及服务器CPU使用率。css

2.Websocket、Http、TCP、Socket之间关系:

HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的。 
对于 WebSocket 来讲,它必须依赖 HTTP 协议进行一次握手 ,握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。 
Socket并非一种协议,而是方便咱们使用TCP/IP的一种封装,而 WebSocket 则不一样,它是一个完整的 应用层协议,包含一套标准的 API 。html

3.STOMP协议:

STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操做的链接格式,容许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议 
许多公司都提供了基于STOMP的服务器与客户端,若spring4开始支持的spring-websocket服务端,基于浏览器的stomp.js客户端 
STOMP定义了客户端和服务器之间以Frame进行同行,Frame的格式为:java

COMMAND
header1:value1
header2:value2

Body^@

COMMAND分为CONNECT、SEND、SUBSCRIBE、UNSUBSCRIBE、BEGIN、COMMIT、ABORT、ACK、NACK、DISCONNECT这几种。 
COMMAND以后下一行紧跟着的是头部的键值对,以后加入一条空行,空行以后为body,即传递的消息实体。jquery

传统HTPP请求响应:git


websocket请求响应:github


4.Spring配置stompweb

环境配置:Spring4.3.9+tomcat8+jackson2.8.2spring

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/10536671json

若是没有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是空的。

   运行截图: