zoukankan      html  css  js  c++  java
  • 小结: 客户端和浏览器"实时通信"

    olddoor: 通过本文可了解客户端和浏览器"实时通信"的解决方案
    1 定时轮询(拉取)
        1.1 定时轮询, 整个页面刷新(过时)
        1.2 基于js, 定时轮询(普通轮询)
        1.3 基于js, 长轮询等Comet类风格的请求.(客户端请求后服务器端有结果反馈, 没结果hold请求, 等有结果或超时再响应. 得到响应后客户端重新发起轮询请求.  此方式对比普通轮询,减少js请求服务器端的次数(减少创建连接的开销))
    服务器端的早期实现方式可在servet中for循环等待有结果再response.
    servet3.0开始 相关开发规范支持异步处理的特性. 对应服务器端可基于sevlet3的规范(服务器一侧接收请求的线程和处理请求的线程分开, 接收请求后容器线程处理其他请求, 原请求的连接不关闭, 待处理线程处理完毕后, 通过监听器等方式通过原未关闭的连接给与客户端响应.)

    2 相互通信(推送, 如websocket为代表)


    1 请求-响应的局限性

    网络上的客户端-服务器通信在过去曾是一种请求-响应模型,要求客户端(比如 Web 浏览器)向服务器请求资源。服务器通过发送所请求的资源来响应客户端请求。如果资源不可用,或者客户端没有权限访问它,那么服务器会发送一条错误消息。在请求-响应架构中,未经请求, 服务器绝不能主动向客户端发送未经请求的消息

    2 浏览器和服务器实时通信的解决方案

    浏览器需要和服务器保持实时通信效果实现的一些想法, 思路其实无非就是两种
    1 客户端定时到服务器查询信息, 实现一种看起来是"服务器推送"的效果.
    2 服务器和客户端实时通信, 双方能互相推送信息.

    对应的技术实现上可能的方案有:
    1 基于客户端Socket的技术(过时的解决方案, 代表方案Flash XMLSocket,Java Applet套接口,Activex包装的socket ) 
    • 优点:原生socket的支持,和PC端和移动端的实现方式相似;
    • 缺点:浏览器端需要装相应的插件;
    2 传统的轮询方式: 利用js定时轮询服务器. 客户端每隔一段时间都会向服务器请求新数据. 要想取得数据,必须首先发送请求. 性能差.不推荐
    3 Comet技术 (可以理解为一种技术分类. 服务器在没有新数据的时候不再返回空响应,而是hold住连接. 
    而且把连接保持到有服务器方有更新的时候或超时(设置)才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求的这类实现技术. 称为Comet技术)
    本质是基于HTTP长连接定时"拉数据" 达到一个浏览器和服务器实时通信, 貌似"服务器推"的效果.
    Comet是一种技术思路, 代表的实现方案有 长轮询和 基于 Iframe 及流(streaming)方式.

    4 HTML5 标准的WebSocket 和Server-sent-events(SSE)
    5 自建或者使用第三方云推送(本质和上述3种已发生改变, 我方已变成推送接收方)

    本文不涉及App或者小程序之类的推送. 


    3 轮询

    不管服务端数据有无更新,客户端每隔定长时间请求拉取一次数据,可能有更新数据返回,也可能什么都没有。
    这显然这不会是"实时通信"/"服务器推"效果可能的选择方案.
    实现一般用AJAX 定时(可以使用JS的 setTimeout 函数)去服务器查询是否有新消息。这让用户感觉应用是实时的。实际上这会造成延时和性能问题,因为服务器每秒都要处理大量的连接请求,每次请求都会有 TCP 三次握手并附带 HTTP 的头信息。尽管现在很多应用仍在使用轮询,但这并不是理想的解决方案。
    • 优点:服务端逻辑简单;
    • 缺点:大多数请求是无效请求,在轮询很频繁的情况下对服务器的压力很大;
    可能的实现代码, 利用XHR,通过setInterval定时发送请求,但会造成数据同步不及时及无效的请求,增加后端处理压力。
    function ajax(data){
        var xhr = new XMLHttpRequest();
        xhr.open('get', '/cgi-bin/xxx', true);
        xhr.onreadystatechange = function(){
            if (xhr.readyState == 4) {
                if (xhr.status == 200) {
                    ......
                }
            }
        }
        xhr.send(data);
    }
    setTimeout(function(){ajax({"data":"hehe"});}, 2000);//每隔2秒请求一次

    4 comet 技术模型

    Comet是技术实现的一个分类而已. 也可理解为客户端所需要的响应信息不再需要主动地去索取,而是在服务器端以事件(Event)的形式推至客户端的技术类别.
    具体实现方式为长轮询和iframe流.

    4.1 长轮询

    长轮询是在Ajax传统轮询基础上做的一些改进,服务器在没有新数据的时候不再返回空响应,而是hold住连接. 
    而且把连接保持到有服务器方有更新的时候或超时(设置)才返回响应信息并关闭连接,客户端处理完响应信息后再次向服务器发送新的请求。 (这种实现思路,这类方案被成为Comet技术)
    长轮询的效果是让HTTP的连接保持,服务器端会阻塞请求,直到服务器端有一个事件触发或者到达超时。客户端在收到响应后再次发出请求,重新建立连接。
    ----------延伸--------------
    前面提到长轮询如果当时服务端没有需要的相关数据,此时请求会hold住,直到服务端把相关数据准备好,或者等待一定时间直到此次请求超时,这里大家是否有疑问,为什么不是一直等待到服务端数据准备好再返回,这样也不需要再次发起下一次的长轮询,节省资源?
    主要原因是网络传输层主要走的是tcp协议,tcp协议是可靠面向连接的协议,通过三次握手建立连接。但是所建立的连接是虚拟的,可能存在某段时间网络不通,或者服务端程序非正常关闭,亦或服务端机器非正常关机,面对这些情况客户端根本不知道服务端此时已经不能互通,还在傻傻的等服务端发数据过来,而这一等一般都是很长时间。当然tcp协议栈在实现上有保活计时器来保证的,但是等到保活计时器发现连接已经断开需要很长时间,如果没有专门配置过相关的tcp参数,一般需要2个小时,而且这些参数是机器操作系统层面,所以,以此方式来保活不太靠谱,故长轮询的实现上一般是需要设置超时时间的。
    -----------------------------

    如图4-1, 从浏览器的角度来看,长轮询的办法保持了有效的请求,又避免了大量无效请求,并且即时性更好,这是一种可行的方案。

    • 优点:任意浏览器都可用;实时性好,无消息的情况下不会进行频繁的请求;
    • 缺点:连接创建销毁操作还是比较频繁,服务器维持着连接比较消耗资源;

    在长轮询方式下,客户端是在 XMLHttpRequest 的 readystate 为 4(即数据传输结束)时调用回调函数,进行信息处理。当 readystate 为 4 时,数据传输结束,连接已经关闭。

    4.1.1 短轮询和长轮询的区别

    短轮询中服务器对请求立即响应,而长轮询中服务器等待新的数据到来才响应,因此实现了服务器向页面推送实时,并减少了页面的请求次数。
    普通Ajax轮询与基于Ajax的长轮询原理对比: 图4-2




    4.1.2 长轮询的编码实现

    可能的实现代码:

    JS客户端

    function longPoll(data, cbk){
        var xhr = new XMLHttpRequest();
        var url = '/cgi-bin/xxx';
        xhr.onreadystatechange = function(){
            if (xhr.readyState == 4) {//XMLHttpRequest 的状态中4: 请求已完成,且响应已就绪
                if (xhr.status == 200) { //请求完毕后重新发起新的一次连接
                    cbk(xhr.responseText);
                    xhr.open('get', url, true);
                    xhr.send(otherData);
                }
            }
        }
        xhr.open('get', url, true);
        xhr.send(data);
    }
    注意:
    无论是轮询还是Comet技术, 思路都是客户端频繁间隔的对服务器端发送请求数据达到"服务器推"的效果, 会在服务端和客户端都需要维持一个比较长时间的连接状态,这一点在客户端不算什么太大的负担,但是服务端是要同时对多个客户端服务的,按照经典 Request-Response 交互模型,每一个请求都占用一个 Web 线程不释放的话,Web 容器的线程则会很快消耗殆尽,而这些线程大部分时间处于空闲等待的状态。
    Comet对比轮询只不过是在请求服务器的频率上会大幅降低而已. 
    而服务器一方线程大部分时间处于空闲等待, 严重影响服务器性能(请求始终占用连接), 所以能够有异步处理的原因,希望 Web 线程不需要同步的、一对一的处理客户端请求,能做到一个 Web 线程处理多个客户端请求。
    服务器端能够异步处理请求的规范以及标准就是Servlet3.0规范引入的异步支持.

    ---------------延伸开始----------------------------
    Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。(Tomcat7提供了对Java EE6规范的支持。)
    新特性部分列列举如下:
    1 异步处理支持:有了该特性,Servlet 线程不再需要一直阻塞,直到业务处理完毕才能再输出响应,最后才结束该 Servlet 线程。在接收到请求之后,Servlet 线程可以将耗时的操作委派给另一个线程来完成,自己在不生成响应的情况下返回至容器。针对业务处理较耗时的情况,这将大大减少服务器资源的占用,并且提高并发处理速度。
    2 新增的注解支持:该版本新增了若干注解,用于简化 Servlet、过滤器(Filter)和监听器(Listener)的声明,这使得 web.xml 部署描述文件从该版本开始不再是必选的了。
    使用异步处理 Servlet 线程不再是一直处于阻塞状态以等待业务逻辑的处理,而是启动异步线程之后可以立即返回.
    异步处理特性可以应用于 Servlet 和过滤器两种组件. 
    1)对于使用传统的部署描述文件 (web.xml) 配置 Servlet 和过滤器的情况Servlet 3.0 为 和 标签增加了 子标签,该标签的默认取值为 false,要启用异步处理支持,则将其设为 true 即可。以 Servlet 为例
    <servlet> 
        <servlet-name>DemoServlet</servlet-name> 
        <servlet-class>footmark.servlet.Demo Servlet</servlet-class> 
        <async-supported>true</async-supported> 
    </servlet>
    2) 使用注解方式: Servlet 3.0 提供的 @WebServlet 和 @WebFilter 进行 Servlet 或过滤器配置的情况,这两个注解都提供了 asyncSupported 属性,默认该属性的取值为 false,要启用异步处理支持,只需将该属性设置为 true 即可
    @WebFilter 为例,其配置方式如下所示:
    @WebFilter(urlPatterns = “/demo”,asyncSupported = true)
    2、Servlet 3.0 还为异步处理提供了一个监听器,使用 AsyncListener 接口表示.
    异步的拦截器:
                1)、原生API的AsyncListener (使用异步servlet的时候需要注册AsyncListener)
                2)、SpringMVC:实现AsyncHandlerInterceptor
    ---------------延伸结束----------------------------


    服务器端

    (1)服务端基于servlet(同步/异步)的实现
    详见 Long Polling长轮询及例子详解 (名词解释, 例子服务端主要是基于servlet的实现)或者见(https://www.jianshu.com/p/d3f66b1eb748 和

    服务器端异步实现
    需要做的是
    保证web.xml中application的配置的版本是3.0
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                http://java.sun.com/xml/ns/javaee
                http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
        version="3.0">
    可以通过web.xml中的子元素<async-supported>true</async-supported>使得DispatcherServlet支持异步.此外的任何Filter参与异步语法处理必须配置为支持ASYNC分派器类型。这样可以确保Spring Framework提供的所有filter都能够异步分发.自从它们继承了OncePerRequestFilter之后.并且在runtime的时候会check filter是否需要被异步调用分发.
    下面是web.xml的配置示例:
    <web-app xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="
                http://java.sun.com/xml/ns/javaee
                http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
        version="3.0">
    
        <filter>
            <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
            <filter-class>org.springframework.~.OpenEntityManagerInViewFilter</filter-class>
            <async-supported>true</async-supported>
        </filter>
    
        <filter-mapping>
            <filter-name>Spring OpenEntityManagerInViewFilter</filter-name>
            <url-pattern>/*</url-pattern>
            <dispatcher>REQUEST</dispatcher>
            <dispatcher>ASYNC</dispatcher>
        </filter-mapping>
    
    </web-app>
    如果使用Sevlet3,Java配置可以通过WebApplicationInitializer,你同样需要像在web.xml中一样,设置”asyncSupported”标签为ASYNC.为了简化这个配置,考虑继承AbstractDispatcherServletInitializer或者AbstractAnnotationConfigDispatcherServletInitializer。它们会自动设置这些选项,使它很容易注册过滤器实例。

    在代码层面:
    接受处理请求的servlet需要使用Servlet 3.0为异步处理提供了一个监听器,使用AsyncListener接口表示。此接口负责管理异步事件.
    Long Polling长轮询及例子详解例子中使用异步servlet处理请求, 就用到了AsyncListener

    见代码片段
     asyncContext.addListener(new AsyncListener() { //这里为异步处理提供了一个监听器,使用AsyncListener接口表示。此接口负责管理异步事件
                @Override
                public void onComplete(AsyncEvent event) throws IOException {
    
                }
    
                //超时处理,注意asyncContext.complete();,表示请求处理完成
                @Override
                public void onTimeout(AsyncEvent event) throws IOException {
                    AsyncContext asyncContext = event.getAsyncContext();
                    asyncContext.complete();
                }
    
                @Override
                public void onError(AsyncEvent event) throws IOException {
    
                }
    
                @Override
                public void onStartAsync(AsyncEvent event) throws IOException {
    
                }
            });

    (2)服务器端基于SpringMVC实现(DeferredResult 或 Callable)
    官方文档中说DeferredResult和Callable(java.util.concurrent.Callable 涉及java多线程知识))都是为了异步生成返回值提供基本的支持。简单来说就是一个请求进来,如果你使用了DeferredResult或者Callable,在没有得到返回数据之前DispatcherServlet和所有Filter就会退出Servlet容器线程但响应保持打开状态,一旦返回数据有了,这个DispatcherServlet就会被再次调用并且处理,以异步产生的方式,向请求端返回值。 
    这么做的好处就是请求不会长时间占用服务连接池,提高服务器的吞吐量。
    这其实也就是SpringMVC对于外部请求的异步处理(基本实现Servlet 3.0的异步处理规范).核心是为了提高减少http请求的连接的占用, 接受请求后快速释放,将业务逻辑交给其他线程处理. 业务逻辑处理完毕后重新拿到http请求连接, 由http连接返回给客户端. 达到异步的效果.  

    Controller中构造Callable并将其作为返回值. 
    使用Callable大致流程说明
    客户端请求服务后;
    • SpringMVC调用Controller,Controller返回一个Callback对象
    • SpringMVC调用ruquest.startAsync并且将Callback提交到TaskExecutor使用一个隔离的线程去进行执行
    • DispatcherServlet以及Filters等从应用服务器线程中结束,但Response仍旧是打开状态,也就是说暂时还不返回给客户端
    • TaskExecutor调用Callback返回一个结果,SpringMVC将请求发送给应用服务器继续处理
    • DispatcherServlet再次被调用并且继续处理Callback返回的对象,根据Callable返回的结果。SpringMVC继续进行视图渲染流程等, 最终将其返回给客户端

    简易流程参考如图4-1-2

    这里写图片描述

    DeferredResult
    DeferredResult的处理过程与Callback类似,不一样的地方在于它的结果不是DeferredResult直接返回的,而是由其它线程通过同步的方式设置到该对象中。
    它的执行过程如下所示:

    DeferredResult的处理顺序与Callable十分相似,由应用程序多线程产生异步结果:

    1. Controller返回一个DeferredResult对象,并且把它保存在内在队列当中或者可以访问它的列表中。
    2. Spring MVC开始异步处理.
    3. DispatcherServlet与所有的Filter的Servlet容器线程退出,但Response仍然开放。
    4. application通过多线程返回DeferredResult中sets值.并且Spring MVC分发request给Servlet容器.
    5. DispatcherServlet再次被调用并且继续异步的处理产生的结果.

    为进一步在异步请求处理动机的背景,并且when或者why使用它请看this blog post series.
    异步请求的异常处理 HTTP Streaming等略. 详见https://blog.csdn.net/u012410733/article/details/52124333 (推荐)

    SpringMVC的配置

    Spring MVC提供Java Config与MVC namespace作为选择用来配置处理异步request.WebMvcConfigurer可以通过configureAsyncSupport来进行配置,而xml可以通过子元素来进行配置.

    如果你不想依赖Servlet容器(e.g. Tomcat是10)配置的值,允许你配置异步请求默认的timeout值。你可以配置AsyncTaskExecutor用来包含Callable实例作为controller方法的返回值.强烈建议配置这个属性,因为在默认情况下Spring MVC使用SimpleAsyncTaskExecutor。Spring MVC中Java配置与namespace允许你注册CallableProcessingInterceptorDeferredResultProcessingInterceptor实例.

    如果你想覆盖DeferredResult的默认过期时间,你可以选择使用合适的构造器.同样的,对于Callable,你可以通过WebAsyncTask来包装它并且使用相应的构造器来定制化过期时间.WebAsyncTask的构造器同样允许你提供一个AsyncTaskExecutor.

    原文地址:spring-framework-reference-4.2.6.RELEASE




    4.2 iframe流(永久帧)

    iframe方式是在页面中插入一个隐藏的iframe,利用其src属性在服务器和客户端之间创建一条长连接,服务器向iframe传输数据(通常是HTML,内有负责插入信息的javascript),来实时更新页面.
     function foreverFrame(url,callback){
          var iframe = body.appendChild(document.createElement("iframe"));
          iframe.style.display="none";
          iframe.src=url+"?callback=parent.foreverFrame.callback";
          this.callback = callback;
        }
    只不过这里涉及父子iframe之间的通信,要注意跨域问题。关于iframe跨域问题,隔壁团队有个不错的实现方案。 见http://www.alloyteam.com/2013/11/the-second-version-universal-solution-iframe-cross-domain-communication/ 
    然后服务器就发送一堆消息到iframe中
    <script>
    parent.foreverFrame.callback('hello world!');
    </script>
    <script>
    parent.foreverFrame.callback('hello Mars!');
    </script>

    4.3 流(xhr流)

    具体解决方案有XHR 流(xhr-multipart)、htmlfile
    xhr流(XMLHttpRequest Streaming)也是通过标准的XMLHttpRequest对象获得的,但是需要在readyState为3的时候去访问数据,这样就不必等待连接关闭之后再操作数据。
    参考代码
    function xhrStreaming(url, callback) {
        var xhr = new XMLHttpRequest();
        xhr.open('post', url, true);
        //保存上次返回的文档位置
        var lastSize;
        xhr.onreadystatechange = function() {
            var newResponseText = "";
            if (xhr.readyState > 2) { 
                newResponseText = xhr.responseText.slice(lastSize);
                lastSize = xhr.responseText.length;
                callback(newResponseText);
            }
            if (xhr.readyState == 4) {
                xhrStreaming(url, callback);
            }
        }
        xhr.send(null);
    }
    其实跟永久帧的方法也类似,只不过是把iframe获取内容的方式改成了ajax,然后在xhr内部处理增量逻辑、回调和重发。
    这里需要注意的是链接时间需要有超时限制,否则内存性能会受到影响,另外单个请求返回数据需要限定长度,不能无限增大。

    注意:
    不管是长轮询还是流,请求都需要在服务器上存在一段较长时间,因此Comet被称为"基于HTTP长连接的服务器推技术"。这打破了每个请求一个线程的模型。这个模型显然对Comet不适用。所以服务端这边Java对此提出了非阻塞IO(non-blocking IO)解决方案, Java 通过它的NIO库提供非阻塞IO处理Comet。
    Tomcat配置server.xml, 即启用异步版本的IO连接器
    protocol="org.apache.coyote.http11.Http11NioProtocol"
    后台请求处理以servlet为例, 通过 servlet 实现 CometProcessor 接口。这个接口要求实现 event() 方法,在配置的 Http11NioProtocol 调用 event() 方法来处理请求,而不是 doGet 或 doPost。
    服务器端代码实现略.

    WebSocket 



    Ajax
    WebSocket是html5规范新引入的功能是基于 TCP 的双向的、全双工的 socket 连接。(是独立的、创建在 TCP 上的协议。).
    WebSocket用于解决浏览器与后台服务器双向通讯的问题,使用WebSocket技术,后台可以随时向前端推送消息,以保证前后台状态统一,在传统的无状态HTTP协议中,这是“无法做到”的。在WebSocke推出以前,服务端向客户端推送消息的方式都以曲线救国的轮询方式( Comet 或轮询)为主。


    WebSocket不属于http无状态协议,协议名为”ws”,这意味着一个websocket连接地址会是这样的写法:ws://twaver.com:8080/webSocketServer。ws不是http,所以传统的web服务器不一定支持,需要服务器与浏览器同时支持, WebSocket才能正常运行,目前的支持还不普遍,需要特别的web服务器和现代的浏览器。

    现在我们来看一下都有哪些浏览器支持 WebSocket:
    Chrome >= 4
    Safari >= 5
    iOS >= 4.2
    Firefox >= 4*
    Opera >= 11*
    检测浏览器是否支持 WebSocket 也非常简单、直接:
    var supported = ("WebSocket" in window);
    if (supported) alert("WebSockets are supported");
    Websocket 通过 HTTP/1.1 协议的101状态码进行握手。为了创建Websocket连接,需要通过浏览器发出请求,之后服务器进行回应,这个过程通常称为“握手”(handshaking)。

    利用HTTP完成握手有几个好处。首先,让WebSockets与现有HTTP基础设施兼容:WebSocket服务器可以运行在80和443 端口上,这通常是对客户端唯一开放的端口。其次,让我们可以重用并扩展HTTP的Upgrade流,为其添加自定义的WebSocket首部,以完成协商。

    5.1 WebSocket API

    浏览器提供的WebSocket API很简单,使用时无需关心连接管理和消息处理等底层细节,只需要发起连接,绑定相应的事件回调即可。

    var connection = new WebSocket('ws://localhost:8080');
    // When the connection is open, send some data to the server connection.onopen = function () { connection.send('Ping'); // Send the message 'Ping' to the server }; // Log errors connection.onerror = function (error) { console.log('WebSocket Error ' + error); }; // Log messages from the server connection.onmessage = function (e) { console.log('Server: ' + e.data); };
    WebSocket资源URL采用了自定议模式,没有使用http是为了在非http协议场景下也能使用,wss表示使用加密信道通信(TCP + TLS),支持接收和发送文本和二进制数据。
    请求头信息
    Connection:Upgrade Sec-WebSocket-Key:eDCPPyPQZq7PiwRcx8SPog== Sec-WebSocket-Version:13 Upgrade:websocket
    响应头信息
    HTTP/1.1 101 Switching Protocols
    Upgrade:websocket
    Connection:upgrade
    Sec-WebSocket-Accept:QJsTRym36zHnArQ7FCmSdPhuK78=
    
    // Connection:upgrade 升级被服务器同意
    // Upgrade:websocket 指示客户端升级到websocket
    // Sec-WebSocket-Accept:参考上面请求的Sec-WebSocket-Key的注释
    最后,前述握手完成后,如果握手成功,该连接就可以用作双向通信信道交换WebSocket消息。到此,客户端与服务器之间不会再发生HTTP通信,一切由WebSocket 协议接管。

    使用场景
    适合于对数据的实时性要求比较强的场景,如通信、股票、Feed、直播、共享桌面,特别适合于客户端与服务频繁交互的情况下,如实时共享、多人协作等平台。

    优点
    • 真正的全双工通信
    • 支持跨域设置(Access-Control-Allow-Origin)
    缺点
    • 采用新的协议,后端需要单独实现
    • 客户端并不是所有浏览器都支持
    • 代理服务器会有不支持websocket的情况
    • 无超时处理
    • 更耗电及占用资源

    TIP 代理、很多现有的HTTP中间设备可能不理解新的WebSocket协议,而这可能导致各种问题,使用时需要注意,可以使借助TLS,通过建立一条端到端的加密信道,可以让WebSocket通信绕过所有中间代理。

    5.2 WebSocket在Java中

    JavaEE 7的JSR-356:Java API for WebSocket,已经对WebSocket做了支持。不少Web容器,如Tomcat、Jetty等都支持WebSocket。Tomcat从7.0.27开始支持WebSocket,从7.0.47开始支持JSR-356。

    待续




    SSE

    待续

    参考
  • 相关阅读:
    组装query,query汇总,query字段
    POJ 1276, Cash Machine
    POJ 1129, Channel Allocation
    POJ 2531, Network Saboteur
    POJ 1837, Balance
    POJ 3278, Catch That Cow
    POJ 2676, Sudoku
    POJ 3126, Prime Path
    POJ 3414, Pots
    POJ 1426, Find The Multiple
  • 原文地址:https://www.cnblogs.com/redcoatjk/p/10846100.html
Copyright © 2011-2022 走看看