WebSocket学习

WebSocket学习

为什么需要WebSocket

  • 以往使用的HTTP协议存在一个缺陷,通信只能由客户端发起。
  • 这种单向请求的特点,如果有一个添加好友的业务场景存在,那就注定客户端必须采用轮询的机制,去设置间隔时间,不断地去请求服务端,无疑对客户端存在延时操作,同时也是对服务端的一种极大的损耗。
  • 效率低,浪费资源(因为必须不停地连接,或者HTTP连接始终打开)

轮询(Polling) 长连接 长轮询 流技术

  1. 这是最早的一种实现实时 Web应用的方案。客户端以一定的时间间隔向服务端发出请求,以频繁请求的方式来保持客户端和服务器端的同步。这种同步方案的最大问题是,当客户端以固定频率向服务器发起请求的时候,服务器端的数据可能并没有更新,这样会带来很多无谓的网络传输,所以这是一种非常低效的实时方案。
  2. 轮询(Polling)是指不管服务器端有没有更新,客户端(通常是指浏览器)都定时的发送请求进行查询,轮询的结果可能是服务器端有新的更新过来,也可能什么也没有,只是返回个空的信息。不管结果如何,客户端处理完后到下一个定时时间点将继续下一轮的轮询。
  3. 推送或叫长连接(Long-Polling)的服务其客户端是不做轮询的,客户端在发起一次请求后立即挂起,一直到服务器端有更新的时候,服务器才会主动推送信息到客户端。 在服务器端有更新并推送信息过来之前这个周期内,客户端不会有新的多余的请求发生,服务器端对此客户端也啥都不用干,只保留最基本的连接信息,一旦服务器有更新将推送给客户端,客户端将相应的做出处理,处理完后再重新发起下一轮请求。
  4. 分为长轮询和流2种:
    长轮询:长轮询是对定时轮询的改进和提高,目地是为了降低无效的网络传输。当服务器端没有数据更新的时候,连接会保持一段时间周期直到数据或状态改变或者时间过期,通过这种机制来减少无效的客户端和服务器间的交互。当然,如果服务端的数据变更非常频繁的话,这种机制和定时轮询比较起来没有本质上的性能的提高。

WebSocket机制

借助Spring WebSockets文档的原话:WebSocket通过单个TCP连接在客户端和服务器之间建立全双工,双向通信通道。它是来自HTTP的一种不同的TCP协议,但被设计为通过HTTP工作,使用端口80和443并允许重新使用现有的防火墙规则。

WebSocket的交互其实基于HTTP的"Upgrade"头进行升级HTTP请求,或者在此情况下切换到WebSocket协议:

GET /spring-websocket-portfolio/portfolio 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

相较于其他HTTP请求成功时返回200,在使用WebSocket请求连接基于HTTP握手的时候,******服务端会返回101状态码******( 服务器已经理解了客户端的请求,并将通过Upgrade 消息头通知客户端采用不同的协议来完成这个请求。在发送完这个响应最后的空行后,服务器将会切换到在Upgrade 消息头中定义的那些协议。 只有在切换新的协议更有好处的时候才应该采取类似措施。例如,切换到新的HTTP版本比旧版本更有优势,或者切换到一个实时且同步的协议以传送利用此类特性的资源。)

下面是响应头

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:     1qVdfYHU9hPOl4JYYNXF623Gzn0=
Sec-WebSocket-Protocol: v10.stomp

这段响应码的意思是说:服务端接收到WebSocket握手请求后,同意切换协议,切换至websocket协议,并且使用V10.stomp
握手成功后,HTTP升级请求的TCP套接字将保持打开状态,以便客户端和服务器继续发送和接收消息。

服务器,如果它支持的协议,回复与同一Upgrade: WebSocket和Connection: Upgrade页眉和完成握手。握手成功完成后,数据传输开始。

HTTP vs WebSocket

尽管WebSocket被设计为与HTTP兼容并以HTTP请求开始,但了解这两种协议导致非常不同的体系结构和应用程序编程模型是很重要的。

在HTTP和REST中,应用程序被建模为尽可能多的URL。要与应用程序客户端交互访问这些URL,请求 - 响应样式。服务器根据HTTP URL,方法和标头将请求路由到适当的处理程序。

相比之下,在WebSockets中,初始连接通常只有一个URL,随后所有应用程序消息都会在同一个TCP连接上流动。这指向一个完全不同的异步,事件驱动的消息体系结构。

WebSocket也是一种低级传输协议,它不像HTTP那样规定消息内容的任何语义。这意味着除非客户端和服务器对消息语义达成一致,否则无法路由或处理消息。

WebSocket客户端和服务器可以通过"Sec-WebSocket-Protocol"HTTP握手请求中的头部来协商使用更高级别的消息传递协议(例如STOMP),或者在没有他们需要提出自己的约定的情况下进行协商。

在之后会为大家介绍STOMP协议

WebSocket的特点

  • 最大的特点是:服务端可以主动向客户端推送信息,客户端也可以主动向服务端发送信息,是一种双向通信,也属于服务器的额推送技术之一。
  • 建立在TCP协议上,服务端的实现比较容易
  • 与HTTP协议有着很好的兼容性,并且握手阶段采用HTTP协议,因此握手时不容易屏蔽,能通过各种HTTP代理服务器
  • 数据格式比较轻量,性能开销小,通信高效。
  • 可以发送文本,也可以发送二进制数据。
  • 没有同源限制,客户端可以与任意服务器通信。
  • 协议标识符是ws如果加密为wss 例如 ws://localhost:8080/GoTogether/websocket

                                
WebSocket学习
协议图

SSM项目使用Spring Websocket

今天想在SSM项目中带入websocket试着使用一下,但是一直有些问题没能解决。
也借着今天这个机会能够深入理解一下websocket和普通的http请求有什么区别,下面总结一下今天踩的坑。

在Spring中使用Websocket

首先需要添加如下依赖

<!--websocket-->
<!-- https://mvnrepository.com/artifact/org.springframework/spring-websocket -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-websocket</artifactId>
  <version>5.0.5.RELEASE</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.springframework/spring-messaging -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-messaging</artifactId>
  <version>5.0.5.RELEASE</version>
</dependency>

最好还是保证版本号一致吧,至少能少一点bug

在springContext.xml中配置

<bean id="websocket" class="com.bsb.handler.MyHandler"/>

<websocket:handlers allowed-origins="http://www.blue-zero.com">
    <websocket:mapping path="/websocket" handler="websocket"/>
    <websocket:handshake-interceptors>
        <bean class="org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor"/>
    </websocket:handshake-interceptors>
</websocket:handlers>

这里的allowed-origins一会儿在做描述

这里附上自己实现的MyHandler类

package com.bsb.handler;

import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class MyHandler extends TextWebSocketHandler {
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message)     throws Exception {
        super.handleTextMessage(session, message);
        TextMessage textMessage = new TextMessage(message.getPayload() + "   received     at server");
        session.sendMessage(textMessage);
    }
}

这里的MyHandler类很简单,继承TextWebSocketHandler类,重写handleTextMessage方法,将客户端发送至我的消息又发送给客户端进行通信。

下面和大家分享一下在项目中使用时遇到的问题

下面是得知websocket建立连接遵循HTTP连接的三次握手后,自己写的握手的拦截器,目的是为了检测握手的动态。

package com.bsb.interceptor;

import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor    ;

import java.util.Map;

public class HandShakeInterceptor extends HttpSessionHandshakeInterceptor {

@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
    System.out.println("before handshake");
    return super.beforeHandshake(request, response, wsHandler, attributes);
}

@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception ex) {
    System.out.println("after handshake");
    super.afterHandshake(request, response, wsHandler, ex);
}

}

到这里就开始出问题了,我开始测试websocket的连接 使用的是 http://www.blue-zero.com/WebSocket/ 一个别人写好的websocket的测试页面,接下来看具体情况

上面提到了allowed-origins属性,一开始我在没有配置它的时候使用连接是这样的:

你 0:45:10
等待服务器握手包...
服务器 0:45:10
和服务器断开连接!

//下面是idea打印的日志
[2018-05-08 12:45:10] - [ http-nio-8080-exec-1:7904 ]-[ DEBUG ]-DispatcherServlet with name 'dispatcher' processing GET request for     [/GoTogether/websocket]
[2018-05-08 12:45:10] - [ http-nio-8080-exec-1:7907 ]-[ DEBUG ]-Looking up handler     method for path /websocket
[2018-05-08 12:45:10] - [ http-nio-8080-exec-1:7910 ]-[ DEBUG ]-Did not find handler     method for [/websocket]
[2018-05-08 12:45:10] - [ http-nio-8080-exec-1:7915 ]-[ DEBUG ]-Mapping [/websocket]     to HandlerExecutionChain with handler     [org.springframework.web.socket.server.support.WebSocketHttpRequestHandler@7df5a3e]     and 1 interceptor
[2018-05-08 12:45:10] - [ http-nio-8080-exec-1:7920 ]-[ DEBUG ]-Last-Modified value     for [/GoTogether/websocket] is: -1
[2018-05-08 12:45:10] - [ http-nio-8080-exec-1:7961 ]-[ DEBUG ]-GET     /GoTogether/websocket
[2018-05-08 12:45:10] - [ http-nio-8080-exec-1:7961 ]-[ DEBUG ]-Handshake request     rejected, Origin header value http://www.blue-zero.com not allowed
[2018-05-08 12:45:10] - [ http-nio-8080-exec-1:7961 ]-[ DEBUG ]-org.springframework.web.socket.server.support.OriginHandshakeInterceptor@2474dfc0     returns false from beforeHandshake - precluding handshake
[2018-05-08 12:45:10] - [ http-nio-8080-exec-1:7961 ]-[ DEBUG ]-Null ModelAndView     returned to DispatcherServlet with name 'dispatcher': assuming HandlerAdapter     completed request handling
[2018-05-08 12:45:10] - [ http-nio-8080-exec-1:7962 ]-[ DEBUG ]-Successfully     completed request
[2018-05-08 12:45:10] - [ http-nio-8080-exec-1:7966 ]-[ DEBUG ]-Returning cached instance of singleton bean 'sqlSessionFactory'

可以看到错误出在这里:

Handshake request rejected, Origin header value http://www.blue-zero.com not allowed

大概意思是说,握手失败,建立连接失败,Origin这个请求头字段的这个值不被允许。这跟web页面返回给我们信息一致,就是建立不了连接,差了很多结果都没有用,接下来甚至还了解了一下web的跨域是怎么回事。

迷迷糊糊正准备配置ssm的跨域配置,最后看到了spring websocet的官方文档后,豁然开朗!

Spring WebSockets官方文档

可以看到4.2.5节的介绍

Allowed origins

Same in Spring WebFlux



list of origins. This check is mostly designed for browser clients. There is nothing     
As of Spring Framework 4.1.5, the default behavior for WebSocket and SockJS is to     
accept only same origin requests. It is also possible to allow all or a specified     
preventing other types of clients from modifying the Origin header value (see RFC     
6454: The Web Origin Concept for more details).



The 3 possible behaviors are:



Allow only same origin requests (default): in this mode, when SockJS is enabled, the     
Iframe HTTP response header X-Frame-Options is set to SAMEORIGIN, and JSONP     
transport is disabled since it does not allow to check the origin of a request. As a     
consequence, IE6 and IE7 are not supported when this mode is enabled.



Allow a specified list of origins: each provided allowed origin must start with     
http:// or https://. In this mode, when SockJS is enabled, both IFrame and JSONP     
based transports are disabled. As a consequence, IE6 through IE9 are not supported     
when this mode is enabled.



Allow all origins: to enable this mode, you should provide * as the allowed origin     
value. In this mode, all transports are available.



WebSocket and SockJS allowed origins can be configured as shown bellow: 

并且还给了一段Config类和xml配置 java类不说了,看看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="http://mydomain.com">
    <websocket:mapping path="/myHandler" handler="myHandler" />
</websocket:handlers>

<bean id="myHandler" class="org.springframework.samples.MyHandler"/>

</beans>

大概意思就是在说allowed-origins属性是允许指定来源的列表,Origin和Referer的区别在这里也就不提了,需要了解的可以自己去搜索。并且一个请求头的Origin是没有办法被修改的。也可以设置为*作为允许的原始值,在这种情况下,所有传输都可以使用。

关于WebSocket的一些具体在项目中的应用后面会补上。

STOMP协议

STOMP即Simple (or Streaming) Text Orientated Messaging Protocol,简单(流)文本定向消息协议,它提供了一个可互操作的连接格式,允许STOMP客户端与任意STOMP消息代理(Broker)进行交互。STOMP协议由于设计简单,易于开发客户端,因此在多种语言和多种平台上得到广泛地应用。

假设HTTP协议不存在,只能使用TCP套接字来编写WEB应用,或许能完成目标,但是这需要自行设计客户端和服务端都认可的协议,从而实现有效通信。

同时也正是因为有了HTTP协议,才解决了Web浏览器发起请求以及Web服务器响应请求的细节。

直接使用WebSocket(或SockJS)就很类似于使用TCP套接字来编写Web应用。 因为没有高层的线路协议,因此这就需要我们定义应用之间所发送消息的语义,还需要确保连接两端都能遵守这些语义。

就像HTTP在TCP套接字上添加了请求-响应模型层一样,STOMP在WebSocket上提供了一个基于帧的线路格式层,用来定义消息的语义。

乍看上去STOMP的消息格式非常类似HTTP的请求结构。与HTTP请求和响应类似, STOMP帧由命令、一个或多个头信息以及负载组成。

例如如下一个STOMP帧

SEND
destination:/app/test
content-length:20

{\"message\":\test!!\"}

STOMP命令是send,表明会发送一些内容。紧接着是两个头信息:一个用来表示消息要发送到哪里的目的地,另一个则包含了负载的大小,然后紧接着是一个空行,STOMP帧的最后内容是负载的内容,如上的帧中的负载就是一个JSON数据。

STOMP中最有意思的是destination头信息,它表明STOMP是一个消息协议,消息会发布到某个目的地,这个目的地实际上可能真的有消息代理作为支撑,另一方面,消息处理器也可能监听这些目的地,接收所发送的消息。

Spring为STOMP提供基于Spring MVC的编程模型

一会儿会为控制器的方法上添加@MessageMapping注解,使其处理STOMP消息,它与带有@RequestMapping注解的方法处理HTTP请求的方式类似,但是与其不同的是,@MessageMapping的功能无法通过@EnableWebMVC启用,Spring的Web消息功能基于消息代理(message broker)构建立,因此我们需要自己的配置类来配置一个消息代理和其它一些消息的目的地。

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
    /**
     * 将"/hello"路径注册为STOMP端点,这个路径与发送和接收消息的目的路径有所不同,这是一个端点
     * 客户端在订阅或者发布消息到目的地址前,要连接该端点
     * 即用户发送请求到"/GoTogether/hello"与STOMP server进行连接。之后再转发到订阅的url。
     * 端点的作用 -  客户端在订阅或发布信息之前,要握手连接端点
     * @param registry
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/hello").setAllowedOrigins("*").withSockJS();
    }


    /**
     * 配置了一个简单的消息代理,如果不重载,默认情况下回自动配置一个简单的内存消息代理,用来处理以"/topic"为   前缀的消息。
     * 这里重载configureMessageBroker()方法,消息代理将会处理前缀为"/topic"和"/queue"的消息。
     * @param registry
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic", "/user");
        registry.setApplicationDestinationPrefixes("/app");
        registry.setUserDestinationPrefix("/user");
    }
}

@EnableWebSocketMessageBroker注解表明了配置类不仅配置了WebSocket,还配置了基于代理的STOMP消息。

对配置中的SockJS说明(SockJS是一个浏览器JavaScript库,它提供了一个类似于网络的对象。SockJS提供了一个连贯的、跨浏览器的Javascript API,它在浏览器和web服务器之间创建了一个低延迟、全双工、跨域通信通道。)测试使用的客户端是JSP页面中的JS客户端。

再附上Controller的代码

@RestController
public class GreetingController {
    
    @Autowired
    private SimpMessageSendingOperations simpMessageSendingOperations;

    /**
     * 表示服务端可以接收客户端通过主题“/app/hello”发送过来的消息,客户端需要在主题"/topic/hello"上监听并接收服务端发回的消息
     * @param topic
     * @param headers
     */
    @MessageMapping("/hello")
    @SendTo("/topic/greetings")
    public void greeting(@Header("atytopic") String topic, @Headers Map<String, Object> headers) {
        System.out.println("connected successfully");
        System.out.println(topic);
        System.out.println(headers);
    }


    /**
     * 这里用的是@SendToUser,这就是发送给单一客户端的标志。本例中,
     * 客户端接收一对一消息的主题应该是“/user/” + 用户Id + “/message”
     * 这里的用户id可以是一个普通的字符串,只要每个用户端都使用自己的id并且服务端知道每个用户的id就行。
     * @return
     */
    @MessageMapping("/message")
    @SendToUser("/message")
    public Greeting handleSubscribe() {
        System.out.println("this is the @SubscribeMapping('/marco')");
        return new Greeting("I am a msg from SubscribeMapping('/macro').");
    }

    /**
     * 测试对指定用户发送消息方法
     * @return
     */
    @RequestMapping(path = "/send", method = RequestMethod.GET)
    public Greeting send() {
        simpMessageSendingOperations.convertAndSendToUser("1", "/message", new Greeting("I am a msg from SubscribeMapping('/macro')."));
        return new Greeting("I am a msg from SubscribeMapping('/macro').");
    }
    
}


//以及Pojo类Greeting
public class Greeting {

    private String content;

    public Greeting(String content) {
        this.content = content;
    }

    public String getContent() {
        return content;
    }
}

下面来看一下本地jsp页面使用js请求服务端连接websocket的HTTP请求报文

General
Request URL: ws://localhost:8080/GoTogether/hello/639/0curp41b/websocket
Request Method: GET
Status Code: 101 Switching Protocols

Response Header
Connection: upgrade
Date: Tue, 08 May 2018 12:50:47 GMT
Sec-WebSocket-Accept:     oQZ9jtefXu3s5+39vmbR94u0KgI=
Server: Apache-Coyote/1.1
Upgrade: websocket

Request Header
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9
Cache-Control: no-cache
Connection: Upgrade
Cookie: JSESSIONID=D1528D00C99C36EB1A756CE85AAAC137; Idea-7e7b0b41=9e9d5672-53b7-4270-9805-aac    cb74742e3; Idea-7cec4644=6d097fc1-24ca-47ce-8c6    e-3cf6184bb47d
Host: localhost:8080
Origin: http://localhost:8080
Pragma: no-cache
Sec-WebSocket-Extensions: permessage-deflate;     client_max_window_bits
Sec-WebSocket-Key: BYmRGPG9QctFlQuAJ49veQ==
Sec-WebSocket-Version: 13
Upgrade: websocket
User-Agent: Mozilla/5.0 (Windows NT 10.0;     WOW64) AppleWebKit/537.36 (KHTML, like Gecko)     Chrome/65.0.3325.181 Safari/537.36

通过上面的讲解,应该也就能读懂这个报文是什么意思了

那么对于WebSocket的分析以及STOMP的使用就到这里

向着明天的自己奔跑,加油!

上一篇:Activemq构建高并发、高可用的大规模消息系统


下一篇:《软件建模与设计: UML、用例、模式和软件体系结构》一一1.10 并发、分布式和实时设计方法