Spring中配置WebSocket

项目中需要在浏览器中实时查看  Docker  容器内框架的运行日志, 在服务器上可以通过 docker 提供的 RESTful 接口, 获取 tail -f 的执行结果 InputStream 流. 而浏览器 HTTP 协议是一个请求 - 响应的协议, 要想获得数据, 就必须发起一次请求, 显然和  Java  的 InputStream 的概念是不能协作的, 服务器无法实时的将日志流推送到浏览器. 使用 AJAX 技术可以以一定的间隔时间异步方式发起请求. 在浏览器没有发起 AJAX 的时间间隙内, 服务器需要维持日志流的缓存, 如果有很多不同的页面查看日志, 那么就需要维持多个队列, 还有一个问题就是如果一个日志页面关闭了, 如何清除这个队列缓存?

所以考虑使用 WebSocket 来实时获取日志流.

WebSocket简介

WebSocket为浏览器提供了一个真正的浏览器和服务器之间的全双工Channel. WebSocket使用HTTP的request protocol upgrade头部来进行请求建立, 服务端返回101表示协议切换成功, 底层的TCP Channel就会一直保持打开. 一旦通道建立, 浏览器端使用send()向服务器发送数据, 通过onmessage事件handler来接收服务器数据, 且每次发送数据时, 不需要再次传输HTTP Header, 大大减少了数据传输量, 适用于浏览器和服务器间进行高频率低延迟的数据交换. 同时实现了真正的服务器推送数据到浏览器.

浏览器端: 
目前主流的浏览器都支持WebSocket.

浏览器 支持的版本
Chrome Supported in version 4+
Firefox Supported in version 4+
Internet Explorer Supported in version 10+
Opera Supported in version 10+
Safari Supported in version 5+

服务端: 
JavaEE 7 JSR356提供了对WebSocket的支持, Tomcat从7.047版本开始提供了JSR356的支持, spring从4.0版本开始支持WebSocket.

Java 实现方法

在 Spring 端可以有以下几种方法使用 WebSocket 
1. 使用 Java EE7 的方式 
2. 使用 Spring 提供的接口 
3. 使用 STOMP 协议以及 Spring 的 MVC

第三种方式见 Spring 的官方文档, 基于 webSocket, 使用 Simple Text Oriented Message Protocol(STOMP) 协议:Using WebSocket to build an interactive web application

STOM 协议工作在 Socket 之上, 类似于 HTTP 协议, 为面向于文本消息的中间件而设计, 是一种语言无关的协议, 符合该协议的 client 和 broker 之间都能通信, 无论是使用何种语言开发. 
STOM 协议介绍

这里我将着重介绍使用 Spring 提供的接口开发的方式. 
主要分为以下几个类, 第一个是 WebSocket 连接建立前的拦截类 HandshakeInterceptor, 类似于 Spring MVC 的 HandlerInteceptor, 第二个 WebSocket 的处理类, 负责对生命周期进行管理, 第三个是配置类, 将 websocket 请求与对应的 handler 类进行映射. 
**1.HandshakeInterceptor 
2.WebSocketHandler 
3.WebSocketConfigurer**

依赖

Spring WebSocket 依赖于以下包, 版本为 4.0 版本及以上, Tomcat 7.047 以上版本, Java EE 7

<dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-messaging</artifactId>
      <version>4.1.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.apache.tomcat.embed</groupId>
    <artifactId>tomcat-embed-websocket</artifactId>
    <version>8.0.23</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>javax</groupId>
    <artifactId>javaee-api</artifactId>
    <version>7.0</version>
    <scope>provided</scope>
</dependency>

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

HandshakeInterceptor

这个类类似于 Spring MVC 中用于拦截 HTTP 请求的 HandlerInteceptor. 它有两个方法,

public interface HandshakeInterceptor {

    boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
            WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception;

    void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
            WebSocketHandler wsHandler, Exception exception);

}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

如果 beforeHandshake 返回 false, 那么该 WebSocket 请求被拒绝连接. 
其中 request 可以转换成 ServletServerHttpRequest, 并获取其中包装的 HttpServletRequest, 以获得 http 请求中 parameter 等信息.

WebSocketHandler

作为 WebSocket 连接建立后的处理接口, 主要有以下方法.

public interface WebSocketHandler {

    void afterConnectionEstablished(WebSocketSession session) throws Exception;

    void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception;

    void handleTransportError(WebSocketSession session, Throwable exception) throws Exception;

    void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception;

    boolean supportsPartialMessages();

}

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

对于每个 WebSocket 连接, 传入的参数 session 都有一个唯一的 id, 通过 getId() 方法获取, 可用于标记这个 WebSocket 连接. 
常用的类有 TextWebSocketHandler 和 BinaryWebSocketHandler. 这两个类继承自 AbstractWebSocketHandler, 其接收 message 时, 对 message 类型进行了判断, 并对不同类型的消息进行了分发. 使用时我们需要分别覆盖 handleTextMessage 方法或 handleBinaryMessage 方法.

    public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
        if (message instanceof TextMessage) {
            handleTextMessage(session, (TextMessage) message);
        }
        else if (message instanceof BinaryMessage) {
            handleBinaryMessage(session, (BinaryMessage) message);
        }
        else if (message instanceof PongMessage) {
            handlePongMessage(session, (PongMessage) message);
        }
        else {
            throw new IllegalStateException("Unexpected WebSocket message type: " + message);
        }
    }

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

WebSocketConfigurer

上面我们定义了握手时需要的 HandlerShakeInteceptor 和对 WebSocket 进行业务处理的 WebSocketHandler, 我们还需要将访问连接与对应的 handler 以及拦截器关联起来.

@Configuration
@EnableWebMvc
@EnableWebSocket
public class WebsocketEndPoint implements WebSocketConfigurer {
    @Autowired
    LogWebSocketHandler handler;
    @Autowired
    HandlerShakerInceptor handlerShakerInceptor;

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(handler, "/logs.do").setAllowedOrigins("*").addInterceptors(handlerShakerInceptor);
    }
}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

@Configuration 是 Spring 基于 Java 类的配置, 可以将 Spring 的 Bean 转换为 Spring 的配置. 
其等效的 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:handlers allowed-origins="*">
        <websocket:mapping path="/log.do" handler="myHandler"/>
        <websocket:handshake-interceptors>
            <ref bean="myHandlerShakeInceptor"/>
        </websocket:handshake-interceptors>
    </websocket:handlers>
    <bean id="myHandlerShakeInceptor" class="com.haoyifen.myHandlerShakeInceptor"/>
    <bean id="myHandler" class="com.haoyifen.myHandler"/>
</beans>
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

对于 java configuration 中的 setAllowedOrigins 和 xml 中的 allowed-origins, 解释一下, 这是用于设置跨域的, 简单的说就是一般浏览器对于 AJAX 和 WebSocket 是禁止跨域请求, 需要我们在服务器端进行设置, 允许其他域 (比如主机名不同或者端口不同) 上的网页与你的 WebSocket 发起连接. “*” 表示任何域上的网页请求都可以连接, 请设置为信任的域名.

客户端

1.Chrome 的 Dark WebSocket Terminal 应用 
2.Java-WebSocket 包中的 client 类 
3.JS

   $(document).ready(function () {
        var websocket = new WebSocket('ws://localhost:8081/logs.do?path=/web/logs/hadoop/hadoop-hadoop-namenode-vm10244.log');
        websocket.onmessage = function (event) {
        ....
        };
        websocket.onclose=function (event) {
        ....
        }
        websocket.onerror=function (event) {
        ...
        };
    });

   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐