WebSocket
[toc]
简介
WebSocket协议(RFC 6455)是HTML5开始提供的一种建立在TCP协议之上的全双工通信方式。WebSocket协议设计出来就是为了取代轮询和Comet技术,使客户端具备实时通信能力。
WebSocket相对HTTP的优势
HTTP协议的主要弊端如下。
(1) HTTP协议为半双工协议。半双工协议指数据可以在客户端和服务器两个方向上传输,但是不能同时传输。它意味在同一时刻,只有一个方向上的数据传送;
(2) HTTP消息冗长繁琐;
(3) 针对服务器推送的黑客攻击。例如长时间轮询。
WebSocket协议的特点:
(1) 单一的TCP连接,曹勇双全工模式通信;
(2) 对代理、防火墙和路由器透明;
(3) 无头部信息、Cookie和身份验证;
(4) 无安全开销;
(5) 通过“ping/pong”帧保持链路激活;
(6) 服务器可以主动传递消息给客户端,不需要客户端轮询。
WebSocket通信过程
连接建立
客户端和服务器通过一个HTTP握手请求建立起连接。由客户端发起一个HTTP请求,与通常的HTTP请求不同的是,这个请求中包含了一些附加头信息,其中“Upgrade: WebSocket”表明这是一个申请协议升级的HTTP请求。
客户端握手请求的HTTP报文如下:
GET /handshake HTTP/1.1
Host: localhost:8080
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: Uc9l9TMkWGbHFD2qnFHltg==
Sec-WebSocket-Protocol: v10.stomp, v11.stomp
Sec-WebSocket-Version: 13
Origin: http://localhost:8080
服务端接收到握手请求后,生成状态码为101的响应报文返回给客户端,表示握手成功。
服务端的响应如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: 1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp
数据通信
握手成功之后,客户端和服务器就可以进行全双工通信了。通信的数据类型可以是文本数据也可以是二进制数据。
连接关闭
底层的TCP连接,在正常的情况下,应该首先由服务器关闭。在异常情况下,客户端可以发起TCP Close。因此当服务器被指示关闭WebSocket连接时,它应该立即发起一个TCP Close操作;客户端应该等待服务器的TCP Close。
WebSocket的握手关闭消息带有一个状态码和可选的关闭原因,它必须按照协议要求发送一个Close控制帧,当对端接收到关闭控制帧指令时,需要主动关闭WebSocket连接。
使用
Java中使用WebSocket有以下常见方案。
-
原生注解
由Tomcat实现的WebSocket API,在javax.websocket包下。
-
Spring封装
通过继承Spring提供的HandshakeInterceptor和WebSocketHandler处理握手和WebSocket事件。
-
Spring STOMP
下面详细说明。
STOMP
概述
STOMP(Simple Text Oriented Messaging Protocol)是一种消息协议,最初是为Python/Perl/Ruby这类脚本语言连接到Message Broker而创造的,也可以用在TCP和WebSocket这种可靠的全双工通信协议中。虽然STOMP是一种文本协议,但它的消息负载可以是文本或二进制。
STOMP是一种效仿HTTP的基于帧的协议,它的帧结构如下。
COMMAND
header1:value1
header2:value2
Body^@
客户端使用SEND或SUBSCRIBE命令来发送或订阅消息,头部的destination字段描述消息的目的,通常用/topic/表示一对多,/queue/表示点对点。
这是一个简单的publish-subscribe机制,可以向服务器或通过broker向其它客户端发送消息。
订阅消息:
SUBSCRIBE
id:sub-1
destination:/topic/price.stock.*
^@
发送消息:
SEND
destination:/queue/trade
content-type:application/json
content-length:44
{"action":"BUY","ticker":"MMM","shares",44}^@
服务端向所有订阅者广播消息:
MESSAGE
message-id:nxahklf6-1
subscription:sub-1
destination:/topic/price.stock.MMM
{"ticker":"MMM","price":129.45}^@
服务端不能发送未经要求的消息。所有服务端发送的所有消息必须是对特定客户端订阅的响应。服务端消息头部的subscription-id必须和客户端订阅的头部id相匹配。
服务端消息流向
下面简要介绍spring-messing中消息相关的一个概念。
- Message:消息,包括headers和payload。
- MessageHandler:用来处理消息。
- MessageChannel:用来发送消息使得生产者和消费者松耦合。
左上client为生产型客户端,发送SEND命令到某个目的地址;左下client为消费型客户端,订阅某个目的地址,并接收此地址推送过来的消息。
应用目的地址/app:发送到这类目的地址的消息在到达broker之前,会先路由到应用的某个处理方法。相当于对进入broker的消息做一次拦截,目的是针对消息做一些业务处理。
非应用目的地址/topic:发送到这类目的地址的消息会直接转到broker,不会被应用拦截。
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// http握手端点
registry.addEndpoint("/portfolio");
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
// 客户端发送给服务器的destination前缀
registry.setApplicationDestinationPrefixes("/app");
// SimpleBroker前缀
registry.enableSimpleBroker("/topic");
}
}
注解使用
@MessageMapping
用@MessageMapping
来注解类或方法。默认情况下,映射值是ant-style路径模式,例如/thing*, /thing/**, /thing/{id}。可以通过@DestinationVariable
方法参数来引用这些值。
支持的方法参数:
- Message
- MessageHeaders
- MessageHeaderAccessor、SimpMessageHeaderAccessor、StompHeaderAccessor
- @Payload
- @Header
- @Headers
- @DestinationVariable
- java.security.Principal
@MessageMapping
方法的响应会由MessageConverter
序列化,然后转发到broker,发往的destination是添加了/topic前缀的请求的destination。也可以通过@SendTo
或@SendToUser
重新指定destination。
@SubscribeMapping
@SubscribeMapping
注解的方法返回值的会异步地响应给client。同样可以使用@SendTo
或@SendToUser
来重新指定destination。
@MessageExceptionHandler
在处理消息的时候,有可能会出错并抛出异常。因为STOMP消息异步的特点,发送者可能永远也不会知道出现了错误。@MessageExceptionHandler
标注的方法能够处理消息方法中所抛出的异常。我们可以把错误发送给用户特定的目的地上,然后用户从该目的地上订阅消息,从而用户就能知道自己出现了什么错误了。