WebSocket

330 阅读5分钟

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:用来发送消息使得生产者和消费者松耦合。

message-flow-simple-broker.png

左上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标注的方法能够处理消息方法中所抛出的异常。我们可以把错误发送给用户特定的目的地上,然后用户从该目的地上订阅消息,从而用户就能知道自己出现了什么错误了。

参考

Spring WebSocket官方文档

Spring Session官方文档