• 小结: 客户端和浏览器"实时通信"


    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

    待续

    参考
  • 相关阅读:
    【转】QPainter中坐标系变换问题
    【转】Python3 (入门6) 库的打包与安装
    【转】Python3 操作符重载方法
    【转】Python3 日期时间 相关模块(time(时间) / datatime(日期时间) / calendar(日历))
    正则表达式过滤HTML、JS、CSS
    JavaScript的类型转换
    JavaScript的数据类型,值和变量
    JavaScript介绍
    Ant学习笔记
    GeoServer端口配置
  • 原文地址:https://www.cnblogs.com/redcoatjk/p/10846100.html
Copyright © 2020-2023  润新知