zoukankan      html  css  js  c++  java
  • (转)WebSocket学习

    石墨文档:https://shimo.im/docs/3UkyOPJvmj4f9EAP/

    (二期)17、即时通讯技术websocket

    【课程17】java We...实现.xmind0.1MB

    【课程17】spirngbo...cket.xmind0.1MB

    【课程17】webso...简介.xmind0.2MB

    【课程17】websoc...过程.xmind0.2MB

    【课程17预习】即...cket.xmind0.1MB

     
    百度百科:websocket

    WebSocket协议是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工(full-duplex)通信——允许服务器主动发送信息给客户端。

    http的不足,websocket的出现
    websocket背景

    了解计算机网络协议的人,应该都知道:HTTP 协议是一种无状态的、无连接的、单向的应用层协议。它采用了请求/响应模型。通信请求只能由客户端发起,服务端对请求做出应答处理。

    这种通信模型有一个弊端:HTTP 协议无法实现服务器主动向客户端发起消息。

    这种单向请求的特点,注定了如果服务器有连续的状态变化,客户端要获知就非常麻烦。大多数 Web 应用程序将通过频繁的异步JavaScript和XML(AJAX)请求实现长轮询。轮询的效率低,非常浪费资源(因为必须不停连接,或者 HTTP 连接始终打开)。

    因此,工程师们一直在思考,有没有更好的方法。WebSocket 就是这样发明的。WebSocket 连接允许客户端和服务器之间进行全双工通信,以便任一方都可以通过建立的连接将数据推送到另一端。WebSocket 只需要建立一次连接,就可以一直保持连接状态。这相比于轮询方式的不停建立连接显然效率要大大提高。

    http解决双工常用方法:

    长久以来, 创建实现客户端和用户端之间双工通讯的web app都会造成HTTP轮询的滥用: 客户端向主机不断发送不同的HTTP呼叫来进行询问。

    桥梁技术 -- ajax轮询

    ajax轮询 的原理非常简单,让浏览器隔个几秒就发送一次请求,询问服务器是否有新信息。

    场景再现:

    客户端:啦啦啦,有没有新信息(Request)

    服务端:没有(Response)

    客户端:啦啦啦,有没有新信息(Request)

    服务端:没有。。(Response)

    客户端:啦啦啦,有没有新信息(Request)

    服务端:你好烦啊,没有啊。。(Response)

    客户端:啦啦啦,有没有新消息(Request)

    服务端:好啦好啦,有啦给你。(Response)

    客户端:啦啦啦,有没有新消息(Request)

    服务端:。。。。。没。。。。没。。。没有(Response) ---- loop

    桥梁技术 -- 长轮询

    long poll 其实原理跟 ajax轮询 差不多,都是采用轮询的方式,不过采取的是阻塞模型(一直打电话,没收到就不挂电话),也就是说,客户端发起连接后,如果没消息,就一直不返回Response给客户端。直到有消息才返回,返回完之后,客户端再次建立连接,周而复始。

    场景再现

    客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request)

    服务端:额。。 等待到有消息的时候。。来 给你(Response)

    客户端:啦啦啦,有没有新信息,没有的话就等有了才返回给我吧(Request) -loop

    websocket特点

    1. 服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。
    1. 建立在 TCP 协议之上,服务器端的实现比较容易。
    1. 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
    1. 数据格式比较轻量,性能开销小,通信高效。
    1. 可以发送文本,也可以发送二进制数据。
    1. 没有同源限制,客户端可以与任意服务器通信。
    1. 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。
    ws://example.com:80/some/path
    websocket实现原理

    在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” 。在 WebSocket API,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。在此WebSocket 协议中,为我们实现即时服务带来了两大好处:

    • 1. Header

    互相沟通的Header是很小的-大概只有 2 Bytes

    • 2. Server Push

    服务器的推送,服务器不再被动的接收到浏览器的请求之后才返回数据,而是在有新数据时就主动推送给浏览器。

    websocket与http的关系

    首先Websocket是基于HTTP协议的,或者说借用了HTTP的协议来完成一部分握手。

    我们来看个典型的 Websocket 握手(借用Wikipedia的。。)

    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13
    Origin: http://example.com

    熟悉HTTP的童鞋可能发现了,这段类似HTTP协议的握手请求中,多了几个东西。我会顺便讲解下作用。

    Upgrade: websocket
    Connection: Upgrade

    这个就是Websocket的核心了,告诉 Apache 、 Nginx 等服务器:注意啦,我发起的是Websocket协议,快点帮我找到对应的助理处理~不是那个老土的HTTP。

    Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
    Sec-WebSocket-Protocol: chat, superchat
    Sec-WebSocket-Version: 13

    首先, Sec-WebSocket-Key 是一个 Base64 encode 的值,这个是浏览器随机生成的,告诉服务器:泥煤,不要忽悠窝,我要验证尼是不是真的是Websocket助理。与后面服务端响应首部的 Sec-WebSocket-Accept 是配套的,提供基本的防护,比如恶意的连接,或者无意的连接。

    然后, Sec_WebSocket-Protocol 是一个用户定义的字符串,用来区分同URL下,不同的服务所需要的协议。简单理解:今晚我要服务A,别搞错啦~

    最后, Sec-WebSocket-Version 是告诉服务器所使用的 Websocket Draft(协议版本),在最初的时候,Websocket协议还在 Draft 阶段,各种奇奇怪怪的协议都有,而且还有很多期奇奇怪怪不同的东西,什么Firefox和Chrome用的不是一个版本之类的,当初Websocket协议太多可是一个大难题。。不过现在还好,已经定下来啦~大家都使用的一个东西~ 脱水: 服务员,我要的是13岁的噢→_→

    然后服务器会返回下列东西,表示已经接受到请求, 成功建立Websocket啦!

    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
    Sec-WebSocket-Protocol: chat

    这里开始就是HTTP最后负责的区域了,告诉客户,我已经成功切换协议啦~

    Upgrade: websocket
    Connection: Upgrade

    依然是固定的,告诉客户端即将升级的是 Websocket 协议,而不是mozillasocket,lurnarsocket或者shitsocket。

    然后, Sec-WebSocket-Accept 这个则是经过服务器确认,并且加密过后的 Sec-WebSocket-Key 。 

    Sec-WebSocket-Accept 的计算方法:

    • 将 Sec-WebSocket-Key 跟 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 拼接;
    • 通过 SHA1 计算出摘要,并转成 base64 字符串。

    作用:

    1. 避免服务端收到非法的websocket连接(比如http客户端不小心请求连接websocket服务,此时服务端可以直接拒绝连接)
    1. 确保服务端理解websocket连接。因为ws握手阶段采用的是http协议,因此可能ws连接是被一个http服务器处理并返回的,此时客户端可以通过Sec-WebSocket-Key来确保服务端认识ws协议。(并非百分百保险,比如总是存在那么些无聊的http服务器,光处理Sec-WebSocket-Key,但并没有实现ws协议。。。)
    1. Sec-WebSocket-Key主要目的并不是确保数据的安全性,因为Sec-WebSocket-Key、Sec-WebSocket-Accept的转换计算公式是公开的,而且非常简单,最主要的作用是预防一些常见的意外情况(非故意的)。

    后面的, Sec-WebSocket-Protocol 则是表示最终使用的协议。

    至此,HTTP已经完成它所有工作了,接下来就是完全按照Websocket协议进行了。

    连接成功的状态码是101。

    java WebSocket实现

    在maven中添加websocket库的代码如下:

    <dependency> 
      <groupId>javax.websocket</groupId> 
      <artifactId>javax.websocket-api</artifactId> 
      <version>1.1</version>
      <scope>provided</scope>
    </dependency>
    注解、成员数据介绍

    @ServerEndpoint

    声明websocket地址类似Spring MVC中的@controller注解类似,websocket使用@ServerEndpoint来进行声明接口:@ServerEndpoint(value="/websocket/{paraName}") ; 其中 “ { } ”用来表示带参数的连接,如果需要获取{}中的参数在参数列表中增加:@PathParam("paraName") Integer userId 。

    1.@OnOpen

    public void onOpen(Session session) throws IOException{ }-------有连接时的触发函数。 我们可以在用户连接时记录用户的连接带的参数,只需在参数列表中增加参数:@PathParam("paraName") String paraName。

    2.@OnClose

    public void onClose(){ }------连接关闭时的调用方法。

    3.@OnMessage

    public void onMessage(String message, Session session) { }-------收到消息时调用的函数,其中Session是每个websocket特有的数据成员

    4.Session----每个Session代表了两个web socket断点的会话;当websocket握手成功后,websocket就会提供一个打开的Session,可以通过这个Session来对另一个端点发送数据;如果Session关闭后发送数据将会报错。

    5.Session.getBasicRemote().sendText("message")-------向该Session连接的用户发送字符串数据。

    6.@OnError

    public void onError(Session session, Throwable error) { }--------发生意外错误时调用的函数。

    websocket demo git:

    springboot + websocket项目实现

    Websocket 是通过一个socket来实现双工异步通讯的能力。但是直接使用WebSocket协议开发程序显得特别烦琐,我门会使用它的子协议STOMP,它是一个更高级级别的协议,STOMP协议使用一个基于帧(frame)的格式来定义信息。

    SockJS

    正如我们所知,websocket协议虽然已经被制定,当时还有很多版本的浏览器或浏览器厂商还没有支持的很好。

    所以,SockJS,可以理解为是websocket的一个备选方案。

    那它如何规定备选方案的呢?

    它大概支持这样几个方案:

    • Websockets
    • Streaming
    • Polling

    当然,开启并使用SockJS后,它会优先选用websocket协议作为传输协议,如果浏览器不支持websocket协议,则会在其他方案中,选择一个较好的协议进行通讯。

    此图来源于 github: sockjs-client

    所以,如果使用SockJS进行通讯,它将在使用上保持一致,底层由它自己去选择相应的协议。

    可以认为SockJS是websocket通讯层上的上层协议。底层对于开发者来说是透明的。

    STOMP

    STOMP 中文为: 面向消息的简单文本协议

    websocket定义了两种传输信息类型: 文本信息 和 二进制信息 ( text and binary )。类型虽然被确定,但是他们的传输体是没有规定的。

    当然你可以自己来写传输体,来规定传输内容。(当然,这样的复杂度是很高的)

    所以,需要用一种简单的文本传输类型来规定传输内容,它可以作为通讯中的文本传输协议,即交互中的高级协议来定义交互信息。

    STOMP本身可以支持流类型的网络传输协议: websocket协议和tcp协议。

    stomp是一个用于client之间进行异步消息传输的简单文本协议, 全称是Simple Text Oriented Messaging Protocol.

    对于stomp协议来说, client分为消费者client与生产者client两种. server是指broker, 也就是消息队列的管理者.

    stomp协议并不是为websocket设计的, 它是属于消息队列的一种协议, 和amqp, jms平级. 

    只不过由于它的简单性恰巧可以用于定义websocket的消息体格式. 

    stomp协议很多mq都已支持, 比如rabbitmq, activemq. 很多语言也都有stomp协议的解析client库.

    可以这么理解, websocket结合stomp相当于一个面向公网对用户比较友好的一种消息队列.

    stomp协议中的client分为两角色:

    • 生产者: 通过SEND命令给某个目的地址(destination)发送消息.
    • 消费者: 通过SUBSCRIBE命令订阅某个目的地址(destination), 当生产者发送消息到目的地址后, 订阅此目的地址的消费者会即时收到消息.

    它的格式为:

    springboot 基于子协议STOMP开发的websocket
    后端技术方案选型

    websocket服务端选型:spring websocket

    支持SockJS,开启SockJS后,可应对不同浏览器的通讯支持

    支持STOMP传输协议,可无缝对接STOMP协议下的消息代理器(如:RabbitMQ, ActiveMQ)

    前端技术方案选型

    前端选型: stomp.js,sockjs.js

    后端开启SOMP和SockJS支持后,前对应有对应的js库进行支持.

    所以选用此两个库.

    技术选型总结

    上述所用技术,是这样的逻辑:

    • 开启socktJS:

    如果有浏览器不支持websocket协议,可以在其他两种协议中进行选择,但是对于应用层来讲,使用起来是一样的。

    这是为了支持浏览器不支持websocket协议的一种备选方案

    • 使用STOMP:

    使用STOMP进行交互,前端可以使用stomp.js类库进行交互,消息一STOMP协议格式进行传输,这样就规定了消息传输格式。

    消息进入后端以后,可以将消息与实现STOMP格式的代理器进行整合。

    这是为了消息统一管理,进行机器扩容时,可进行负载均衡部署

    • 使用spring websocket:

    使用spring websocket,是因为他提供了STOMP的传输自协议的同时,还提供了StockJS的支持。

    当然,除此之外,spring websocket还提供了权限整合的功能,还有自带天生与spring家族等相关框架进行无缝整合。

    websocket信息流

    • Message在应用中的流动是这样一个流程,如上图。若destination是以/app开始则会通过request channel交给注解方法来处理,处理完毕根据默认的路径转发给SimpleBroker处理(若不使用默认路径可以用@SendTo来指定路径),处理完毕后交由response channel返回连接的客户端。 
    • 若destination是以/topic开头则直接交给SimpleBroker处理。

    第一步:导入spirngboot与websocket集成的pom坐标

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <!--添加jsp支持-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>org.apache.tomcat.embed</groupId>
        <artifactId>tomcat-embed-jasper</artifactId>
        <scope>provided</scope>
    </dependency>
    
    

    第二步:自定义websocket配置,配置内容包括开启子协议STOMP,配置服务端点,前缀,等信息。

    @Configuration
    @EnableWebSocketMessageBroker//注解表示开启使用STOMP协议来传输基于代理的消息,Broker就是代理的意思。
    public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    
    
        /***
         * 注册 Stomp的端点
         * addEndpoint:添加STOMP协议的端点。提供WebSocket或SockJS客户端访问的地址
         * withSockJS:使用SockJS协议
         * @param registry
         */
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/endpointWisely")
                    .withSockJS() ;
    
    
            registry.addEndpoint("/websocket")
                    .setAllowedOrigins("*")//添加允许跨域访问
                    .withSockJS() ;
        }
    
    
        /**
         * 配置消息代理
         * 启动Broker,消息的发送的地址符合配置的前缀来的消息才发送到这个broker
         */
        public void configureMessageBroker(MessageBrokerRegistry registry) {
            registry.enableSimpleBroker("/api/v1/socket/send","/user/", "/topic");//推送消息前缀
            registry.setApplicationDestinationPrefixes("/api/v1/socket/req");//应用请求前缀
            registry.setUserDestinationPrefix("/user");//推送用户前缀
        }
    
    
    }

    Spring框架提供基于websocket的STOMP支持,需要使用spring-messaging和spring-websocket模块。 

    下面的配置中,注册了一个前缀为/endpointWisely的stomp终端,客户端可以使用该url来建立websocket连接。 

    Message的destination如果是以/app开头,则会转发给响应的消息处理方法(如使用@MessageMapping注解的方法),

    如果是以/topic,/queue开头则会被转发给消息代理(broker),由broker广播给连接的客户端。

    第三步:上面配置完了之后,就可以开始编写内容了。这里表示服务器发送地址映射到/welcomeTopic,然后所有订阅了/topic/getResponse路径的都可以收到广播消息。

    @MessageMapping("/welcomeTopic")//浏览器发送请求通过@messageMapping 映射/welcome 这个地址。
    @SendTo("/topic/getResponse")//服务器端有消息时,会订阅@SendTo 中的路径的浏览器发送消息。
    public ResponseMessage say(RequestMessage message) throws Exception {
        System.out.println("发送信息-----------------------" + message.getMessage());
    
    
        return new ResponseMessage("Welcome, " + message.getMessage() + "!");
    }

    第四步:由于使用了sockjs,因此前端需要导入相关的js文件。

    sockjs.js0.2MB

    stomp.js16.7KB

    jquery.js0.3MB

    页面如下:

    topic.jsp2.6KB

    常用注解
    • @EnableWebSocketMessageBroker

    通过EnableWebSocketMessageBroker 开启使用STOMP协议来传输基于代理(message broker)的消息,此时浏览器支持使用@MessageMapping 就像支持@RequestMapping一样。

    • @MessageMapping

    配置中定义的config.setApplicationDestinationPrefixes("/app");表示如果链接以/app开头,则会转发给对应具有@MessageMapping对应链接的注解方法处理。如链接是/app/welcome则会找到@MessageMapping("/welcome")注解对应的方法。

    • @SendTo

    可以把消息广播到路径上去,例如上面可以把消息广播到”/topic/greetings”,如果客户端在这个路径订阅消息,则可以接收到消息。

    • @SendToUser

    消息目的地有UserDestinationMessageHandler来处理,会将消息路由到发送者对应的目的地。默认该注解前缀为/user。如:用户订阅/user/hi ,在@SendToUser('/hi')查找目的地时,会将目的地的转化为/user/{name}/hi, 这个name就是principal的name值,该操作是认为用户登录并且授权认证,使用principal的name作为目的地标识。发给消息来源的那个用户。(就是谁请求给谁,不会发给所有用户,区分就是依照principal-name来区分的)。

    spring websocket基于注解的@SendTo和@SendToUser虽然方便,但是有局限性,例如我这样子的需求,我想手动的把消息推送给某个人,或者特定一组人,怎么办,@SendTo只能推送给所有人,@SendToUser只能推送给请求消息的那个人,这时,我们可以利用SimpMessagingTemplate这个类。

    SimpMessagingTemplate有俩个推送的方法

    1. convertAndSend(destination, payload); //将消息广播到特定订阅路径中,类似@SendTo 
    1. convertAndSendToUser(user, destination, payload);//将消息推送到固定的用户订阅路径中,类似@SendToUser
    • @DestinationVariable

    这个注解用于动态监听路径,很想rest中的@PathVariable:

    @MessageMapping("/queue/chat/{uid}")
    public void chat(@Payload @Validated Message message, @DestinationVariable("uid") String uid, Principal principal) {
        String msg = "发送人: " + principal.getName() + " chat ";
        simpMessagingTemplate.convertAndSendToUser(uid,"/queue/chat",msg);
    }
    通讯层设计 – 1-1 && 1-n

    1-n topic:

    此方式,上述消息模型章节已经讲过,此处不再赘述

    1-1 queue:

    客服-用户沟通为1-1用户交互的案例

    前端:

    stompClient.subscribe('/user/queue/chat',function(greeting){
        showGreeting(greeting.body);
    });

    后端:

    @MessageMapping("/queue/chat/{uid}")
    public void chat(@Payload @Validated Message message, @DestinationVariable("uid") String uid, Principal principal) {
        String msg = "发送人: " + principal.getName() + " chat ";
        simpMessagingTemplate.convertAndSendToUser(uid,"/queue/chat",msg);
    }

    发送端:

    function chat(uid) {
        stompClient.send("/app/queue/chat/"+uid,{},JSON.stringify({'title':'hello','content':'message content'}));
    }

    上述的转化,看上去没有topic那样1-n的广播要流畅,因为代码中采用约定的方式进行开发,当然这是由spring约定的。

    项目git地址:

    https://gitee.com/java-mindmap/training-camp-project-demo/tree/master/springboot-websocket

  • 相关阅读:
    websocket
    svg vs canvas
    nw
    web sql
    web worker
    【转载】磁盘阵列详解
    【识记】开源软件系列
    【转载】从1.5K到18K 一个程序员的5年成长之路
    SQL中in和not in
    SQL Server select count(distinct *)
  • 原文地址:https://www.cnblogs.com/free-wings/p/9920754.html
Copyright © 2011-2022 走看看