WebSocket协议定义了两种类型的消息(文本和二进制),可是它们的内容没有定义。协议定义了一种机制,供客户端和服务器协商子协议(即更高级别的消息传递协议),以便在WebSocket上使用它来定义每一个消息能够发送哪些类型、格式是什么、每一个消息的内容等等。子协议的使用是可选的,但不管如何,客户端和服务器都须要就一些定义消息内容的协议达成一致。html
STOMP(简单的面向文本的消息传递协议)最初是为脚本语言(如Ruby、Python和Perl)建立的,用于链接到企业消息代理,它被设计用于处理经常使用消息传递模式的最小子集,STOMP能够用于任何可靠的双向流网络协议,如TCP和WebSocket,虽然STOMP是一个面向文本的协议,但消息payload能够是文本或二进制。java
STOMP是基于帧的协议,其帧是基于HTTP建模的,下面的清单显示了一个STOMP帧的结构:git
COMMAND header1:value1 header2:value2 Body^@
客户端可使用SEND
或SUBSCRIBE
命令发送或订阅消息,以及描述消息是关于什么而且谁应该接收它的destination
header。这支持一种简单的发布-订阅机制,你可使用该机制将消息经过代理发送到其余链接的客户端,或将消息发送到服务器,以请求执行某些工做。github
当你使用Spring的STOMP支持时,Spring WebSocket应用程序充当客户端的STOMP代理,消息被路由到@Controller
消息处理方法或一个跟踪订阅并向订阅用户广播消息的简单的内存代理。你还能够将Spring配置为使用专用的STOMP代理(例如RabbitMQ、ActiveMQ和其余的)来实际广播消息,在这种状况下,Spring维护到代理的TCP链接,将消息传递给代理,并将消息从代理向下传递到链接的WebSocket客户端。所以,Spring web应用程序能够依赖于统一的基于http的安全性、公共验证和熟悉的消息处理编程模型。web
下面的示例显示了订阅股票报价的客户端,服务器可能按期发出股票报价(例如,经过调度任务使用SimpMessagingTemplate
向代理发送消息)。spring
SUBSCRIBE id:sub-1 destination:/topic/price.stock.* ^@
下面的示例显示了发送交易请求的客户端,服务器能够经过@MessageMapping
方法处理该请求:编程
SEND destination:/queue/trade content-type:application/json content-length:44 {"action":"BUY","ticker":"MMM","shares",44}^@
执行以后,服务器能够向客户端广播交易确认消息和详细信息。json
destination
的含义在STOMP规范中故意保持不透明,它能够是任何字符串,彻底由STOMP服务器来定义它们支持的destination
的语义和语法。然而,destination
一般是像路径字符串,其中包含/topic/..
意味着发布-订阅(一对多)和/queue/
意味着点对点(一对一)消息交换。segmentfault
STOMP服务器可使用MESSAGE
命令向全部订阅者广播消息,下面的示例显示了服务器向订阅的客户端发送股票报价:api
MESSAGE message-id:nxahklf6-1 subscription:sub-1 destination:/topic/price.stock.MMM {"ticker":"MMM","price":129.45}^@
服务器不能发送未经请求的消息,来自服务器的全部消息都必须响应特定的客户端订阅,服务器消息的subscription-id
header必须与客户端订阅的id
header匹配。
前面的概述旨在提供对STOMP协议的最基本的理解,咱们建议全面审查协议规范。
使用STOMP做为子协议,可让Spring Framework和Spring Security提供比使用原始WebSockets更丰富的编程模型,关于HTTP与原始TCP的区别,以及它如何让Spring MVC和其余web框架提供丰富的功能,能够提出一样的观点,如下是一些好处:
@Controller
实例中,能够基于STOMP destination
header将消息路由到它们,而没必要针对给定链接使用单个WebSocketHandler
处理原始WebSocket消息。destination
和消息类型保护消息。spring-messaging
和spring-websocket
模块提供WebSocket支持的STOMP,一旦有了这些依赖项,就能够经过WebSocket使用SockJS Fallback公开STOMP端点,以下例所示:
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config) { config.setApplicationDestinationPrefixes("/app"); config.enableSimpleBroker("/topic", "/queue"); } }
/portfolio
是WebSocket(或SockJS)客户端为WebSocket握手须要链接到的端点的HTTP URL。/app
开头的destination
header的STOMP消息被路由到@Controller
类中的@MessageMapping
方法。destination
header 以/topic
或/queue
开头的消息路由到代理。下面的示例显示了与前面示例等价的XML配置:
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:websocket="http://www.springframework.org/schema/websocket" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket.xsd"> <websocket:message-broker application-destination-prefix="/app"> <websocket:stomp-endpoint path="/portfolio"> <websocket:sockjs/> </websocket:stomp-endpoint> <websocket:simple-broker prefix="/topic, /queue"/> </websocket:message-broker> </beans>
对于内置的简单代理,/topic
和/queue
前缀没有任何特殊含义,它们只是区分发布-订阅和点对点消息传递(也就是说,许多订阅者和一个消费者)的一种约定,当你使用外部代理时,请检查代理的STOMP页面,以了解它支持哪一种STOMPdestination
和前缀。
要从浏览器链接SockJS,可使用sockjs-client,对于STOMP,许多应用程序已经使用了jmesnil/stomp-websocket库(也称为stomp.js),该库功能齐全,已经在生产中使用多年,但已再也不维护。目前,JSteunou/webstomp-client是该库最积极维护和发展的继承者,下面的示例代码就是基于它编写的:
var socket = new SockJS("/spring-websocket-portfolio/portfolio"); var stompClient = webstomp.over(socket); stompClient.connect({}, function(frame) { }
或者,若是你经过WebSocket链接(没有SockJS),你可使用如下代码:
var socket = new WebSocket("/spring-websocket-portfolio/portfolio"); var stompClient = Stomp.over(socket); stompClient.connect({}, function(frame) { }
注意,前面示例中的stompClient
不须要指定login
和passcode
header,即便有,它们也会在服务器端被忽略(或者更确切地说,被覆盖),有关身份验证的更多信息,请参阅链接到代理和身份验证。
更多的示例代码:
一旦公开了STOMP端点,Spring应用程序就成为链接的客户端的STOMP代理,本节描述服务器端的消息流。
spring-messaging
模块包含对源自Spring Integration的消息传递应用程序的基本支持,而且后来被提取并合并到Spring Framework中,以便在许多Spring项目和应用程序场景中获得更普遍的使用。下面的列表简要描述了一些可用的消息传递抽象:
MessageHandler
的订阅者的MessageChannel
。Executor
来传递消息的SubscribableChannel
。Java配置(即@EnableWebSocketMessageBroker
)和XML命名空间配置(即<websocket:message-broker>
)都使用前面的组件组装消息工做流,下图显示了启用简单内置消息代理时使用的组件:
前面的图显示了三个消息通道:
clientInboundChannel
:传递从WebSocket客户端收到的消息clientOutboundChannel
:用于向WebSocket客户端发送服务器消息。brokerChannel
:用于从服务器端应用程序代码中向消息代理发送消息。下一个图显示了配置外部代理(例如RabbitMQ)来管理订阅和广播消息时使用的组件:
前两个图的主要区别是使用“代理转播”将消息经过TCP传递到外部STOMP代理,并将消息从代理向下传递到订阅客户端。
当从WebSocket链接接收到消息时,它们被解码到STOMP帧,转换为Spring Message
表示,并发送到clientInboundChannel
进行进一步处理。例如,以/app
开头的destination header的STOMP消息能够路由到带注解的控制器中的@MessageMapping
方法,而/topic
和/queue
消息能够直接路由到消息代理。
处理来自客户端的STOMP消息的@Controller
注解能够经过brokerChannel
向消息代理发送消息,而代理经过clientOutboundChannel
向匹配的订阅者广播消息。相同的控制器也能够对HTTP请求进行相同的响应,所以客户端能够执行HTTP POST,而后@PostMapping
方法能够向消息代理发送消息,以便向订阅的客户端广播消息。
咱们能够经过一个简单的示例跟踪流,考虑下面的示例,它设置了一个服务器:
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/portfolio"); } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.setApplicationDestinationPrefixes("/app"); registry.enableSimpleBroker("/topic"); } } @Controller public class GreetingController { @MessageMapping("/greeting") { public String handle(String greeting) { return "[" + getTimestamp() + ": " + greeting; } }
前面的示例支持如下流:
/topic/greeting
的destination header的SUBSCRIBE帧,接收和解码后,消息被发送到clientInboundChannel
,而后被路由到消息代理,消息代理存储客户端订阅。/app/greeting
发送一个SEND
帧,/app
前缀有助于将其路由到带注解的控制器。去掉/app
前缀后,destination剩余的/greeting
部分映射到GreetingController
中的@MessageMapping
方法。GreetingController
返回的值被转换为一个Spring Message
,其payload基于返回值和/topic/greeting
的默认destination header(由/app
替换为/topic
的输入destination派生而来)。clientOutboundChannel
向每一个订阅者发送MESSAGE
帧,消息从该通道编码为STOMP帧并在WebSocket链接上发送。下一节将详细介绍带注解的方法,包括支持的参数和返回值。
应用程序可使用带注解的@Controller
类来处理来自客户端的消息,这些类能够声明@MessageMapping
、@SubscribeMapping
和@ExceptionHandler
方法,以下面的主题所述:
@MessageMapping
你可使用@MessageMapping
来注解路由基于它们的destination消息的方法,在方法级别和类型级别都支持它。在类型级别,@MessageMapping
用于表达控制器中全部方法之间的共享映射。
默认状况下,映射值是Ant风格的路径模式(例如/thing*
、/thing/**
),包括对模板变量的支持(例如/thing/{id}
),这些值能够经过@DestinationVariable
方法参数引用,应用程序还能够为映射切换到点分隔的destination约定,点做为分隔符章节进行了解释。
下表描述了方法参数:
方法参数 | 描述 |
---|---|
Message | 用于访问完整的消息 |
若是但愿从应用程序的任何部分向链接的客户端发送消息,该怎么办?任何应用程序组件均可以向brokerChannel
发送消息,最简单的方法是注入SimpMessagingTemplate
并使用它发送消息,一般,你将按类型注入它,以下面的示例所示:
@Controller public class GreetingController { private SimpMessagingTemplate template; @Autowired public GreetingController(SimpMessagingTemplate template) { this.template = template; } @RequestMapping(path="/greetings", method=POST) public void greet(String greeting) { String text = "[" + getTimestamp() + "]:" + greeting; this.template.convertAndSend("/topic/greetings", text); } }
可是,若是存在另外一个相同类型的bean,也能够经过它的名称(brokerMessagingTemplate
)来限定它。