关于 Websockets

一些参考内容:

  • https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html
  • https://www.tutorialspoint.com/http/http_messages.htm
  • http://www.w3schools.com/js/default.asp JavaScript
  • http://www.w3schools.com/jquery/default.asp JQ
  • http://www.w3schools.com/bootstrap/default.asp BOOTSTRAP

1 从Ajax到 Websockets 的进化

用户的需求通常是: 希望web页面更具交互性. 而解决方案就是使用javascript. 而这一切的推动力又是Ajax.

1.1 如何从服务器获取新数据

Ajax是异步javascript和xml的缩写(Asynchronous Javascript and XML), 利用这种技术, 可以让远程服务器和客户端保持数据同步. 但在这个机制中XML并非必须的, 它只是作为数据传递的载体, 实际很多时候是使用JSON来进行传递(很多时候又叫AJAJ).

使用Ajax的最大好处是: 客户端不必刷新整个页面就可以实现同服务器的数据交换, 而这种交换对于用户来说可以是透明的.

但这样也造成一个难题: 由于客户端是主动从服务器获取新数据, 但客户端怎么获知数据的更新呢?

过去14年间(2000年开始算起), 针对上面的问题, 出现了各种各样的解决方案.

主要有四种形式的解决办法:

  • Frequent Polling:

    客户端以一定频率发送Ajax请求(不同于一般请求)查询服务器是否数据更新, 如果有新数据, 服务器就发送, 如果没有, 则服务器返回一个没有content-body(content-length==0)的响应.

    这种方式的缺点是需要频繁发送请求响应,造成巨大的性能浪费.

    但由于实现简单, 故这种方式仍然在广泛使用...

  • Long Polling:

    和第一种类似, 只是当服务器没有更新数据的时候不会发送响应.

    但这种机制也给自己挖了坑:

    1. 假如客户端在服务器响应之前有新数据要发送给服务器
    2. 连接超时机制内建在TCP和HTTP规范内, 即如果服务器不响应就会关闭连接, 此时服务器和客户端就必须周期性地关闭当前连接并建立新连接.
    3. HTTP1.1中有连接授权限制. 即浏览器不能同时保持超过两个到服务器的并行连接
  • Chunked Encoding:

    和Long Polling十分类似. 它利用的是HTTP1.1中的一个特性, 让服务器发送响应时不指定content-length, 而是单独发送个具有Content-Length:n 头的响应, 里面还包含Transfer-Encoding: chuncked头. 这个响应报文的目的是告诉浏览器响应是以分块形式过来的.

    然后包含分块的响应中有该分块长度等信息.而服务器正是通过分块来发送更新的内容. 而连接可以得到保持.

  • Applets和Adobe Flash

    后来发现, 主要就是在单一连接上模拟全双工通信.

    Applet和Adobe Flash曾经流行过一段时间. 做法是在浏览器放置一个1像素的透明Applet或Flash Movie. 它们可以建立到服务器的Socket连接, 这样就可以完全屏蔽掉HTTP协议的限制. 当服务器传送数据到浏览器, Applet或Flash 就会调用本地JavaScript的函数来处理这些数据. 而这样的办法也确实实现了单一连接下的全双工通信.并且不会有任何的HTTP协议上的副作用.

    但是缺点在于它们使用的是开销很高的附件: 一些第三方内容,不安全,缓慢,占用内存.连接不含任何安全协议.

    由于移动网络的迅速发展, 移动设备对Flash或Java的支持不好, 另外也就是上述的缺点, 人们放弃了这两种技术, 转而寻找一种理想方案: 只使用TCP连接, 而且安全, 快速, 能够为移动设备提供良好支持, 并且不需要浏览器插件来完成的方案.

2 WebSockets: 建立在HTTP Upgrade上的技术

1999年的时候, 在HTTP1.1规范内就提供了一种功能, 可以用在任何HTTP数据交流过程中的功能, 叫做HTTP Upgrade.

2.1 HTTP Upgrade功能概述及WebSockets协议的由来

作用原理如下:

任何HTTP客户端(不仅仅是浏览器)都可以在请求头(键值对)中发送Connection:Upgrade到服务器. 为了告知服务器需要更新什么内容, 客户端还需要发送Upgrade头来指定一个协议列表, 列表中的协议应是与HTTP/1.1不同的协议, 比如IRC或RTA.

服务器如果可以接受这样的请求, 就发送一个状态码是101 Switching Protocols的响应, 并且响应内附加在协议列表中服务器支持的第一个协议.

原来的时候, 这个功能通常用来把HTTP改变为HTTPS. 但这种用途经常会受到中间人攻击, 因为连接本身并非安全的. 所以后来很长一段时间, 这个功能都一直被搁置一旁. 而HTTPS也由https scheme来指定了.

HTTP Upgrade最好的地方是: 指定的协议几乎可以是任意的. 当HTTP握手结束后就会释放掉之前的HTTP连接. 理论上讲, 使用HTTP Upgrade, 可以建立起任意的两端点间的任意TCP Socket连接(甚至可以是持久的, 全双工的TCP socket连接), 并且连接上工作的协议可以是你自己设计的.

当然, 浏览器不可能将客户端程序员推入到TCP协议栈的深渊让他们自己针对HTTP Upgrade开发自己的协议. 所以由专门机构开发了一些协议出来, 而WebSockets协议正是其中之一.

注意:

  • 若服务器的某个资源只接受HTTP Upgrade请求, 而客户端请求该资源的时候又没有指定Upgrade头中的协议列表, 则服务器会返回一个状态码为426 Upgrade Required的响应.

    而这个情况下, 服务器可以在包含一个Connection:Upgrade头在响应中, 并包含一个Upgrade头来指定服务器可以支持的协议列表.

  • 若客户端请求内Upgrade头指定的协议列表中协议服务器都不支持, 服务器返回400 Bad Request响应. 并且也可以在响应中包含Upgrade头来指明服务器支持的协议列表.

  • 若服务器不支持Upgrade请求, 则也会返回400 Bad Request状态码的响应.

2.2 WebSocket连接

WebSocket连接的建立过程:

  1. 首先是使用特殊的scheme来请求URL, scheme为wswss. wswss类似就是httphttps的区别.

    而发送的请求则是普通的HTTP请求, 其中包含Connection:Upgrade头以及Upgrade头指定协议列表(当然这里指定的协议就是websocket协议了, 即Upgrade: websocket).

    Upgrade请求头的目的是告知服务器将连接改变为WebSocket协议: 该协议是一个持久的, 全双工的通信协议. 并且在2011年标准化(RFC 6455).

  2. 握手结束后, 全双工通信的通道被建立起来, 文本或二进制信息可以在上面同时双向发送(全双工), 而无需像HTTP那样关闭再重建连接. 这种连接下, 服务器和客户端地位平等, 都是简单的peer.(p2p)

ws以及wss两种scheme类型严格地说并非HTTP协议族中的部分. 而且ws或wss的作用只是用来告诉浏览器或某个API你想使用哪种类型的连接(SSL/TLS连接wss或无加密连接ws)

使用WebSocket协议的好处很多, 主要和它的实现有关:

  • 它使用的端口和HTTP协议的端口相同, ws(80), wss(443), 基本上不会被任何防火墙阻止.
  • 由于是基于HTTP协议握手, 所以WebSocket是内建在了浏览器和HTTP服务器中.
  • 心跳信息(HeartBeat)pingpong在两端间持续传送, 保证WebSocket连接可以无限保持下去.(即一端周期性地发送很小的ping包到另外一端,另外一端返回一个相同内容的pong包,这样就可以获知两端一直在连接着的)
  • 不需要任何额外代码来构造两端间传递的消息, 故两端都可以知道消息的开始和结束.
  • 关闭WebSocket连接需要特殊的消息, 消息内可以包含关闭连接的原因代码或文本.
  • WebSocket协议可以安全地进行跨站连接, 所以淘汰了Ajax和XMLHttpRequest的缺陷.
  • HTTP协议需要浏览器限制并行连接数, 而握手结束后这个限制也就不存在了, 因为握手后连接类型会被改变(upgrade).

虽然原理很复杂, 但是已经有现成的API了, 要做的就是在上面构建你自己的应用即可...

而WebSocket的API又分为客户端API和服务端API. 不同点只是对不同工作内容的支持不同而已.

另外建议任何时候都是以wss来连接, 这样即不会存在代理问题(代理通常不会处理SSL/TLS连接, 而是让其自己做自己的事), 又安全.

2.3 WebSocket协议的用途

用途基本上没有任何限制, 比如浏览器应用, 以及任何支持平台上的客户端应用.下面是一些典型用途:

  • 聊天程序
  • 多人游戏
  • 在线股票程序
  • 新闻程序
  • 高清视频流(最快最强的实现方式)
  • 分布式应用集群中的节点通信
  • 透明数据传输(不同网络中的应用间)
  • 远程系统实时监控或软件状态与性能监控

3 WebSocket API

和HTTP协议一样, 由于是信息交流的规范或标准. 所以使用WebSocket建立的不同应用理论上说都可以相互通信, 不管是哪个平台, 什么类型的.

正是因为如此, 所以大多数WebSocket的实现中都将自己分为客户端和服务器端工具两个部分. 比如Java或.net.

而Javascript中只是有客户端工具的部分, 因为它本身就是用作客户端脚本的.

下面先来使用JavaScript的客户端工具建立客户端, 然后再转到Java的服务端(当然Java也有客户端的API,只是使用Java实现客户端还是交给android中去吧...)

3.1 HTML5(JavaScript)客户端API

W3C规定将浏览器中的WebSocket支持接口作为HTML5的扩展. 故虽然使用的是Javascript来实现WebSocket的交流, 但实际上WebSocket接口是HTML5的组成部分.任何浏览器都可以通过WebSocket接口的implementation来实现WebSocket通信.(早期的Ajax通信则是不同浏览器有不同的类和不同的方法来做Ajax请求, 比起现在可以说复杂很多)

下面就来建立客户端.

  1. 创建WebSocket对象

    创建的语法很直接:

    var connection = new WebSocket("ws://www.xxx.com/stocks/stream");
    var connection = new WebSocket("wss://www.xxx.com/games/chess");
    var connection = new WebSocket("ws://www.xxx.com/chat", "chat");
    var connection = new WebSocket("ws://www.xxx.com/chat", {"chat.v1, chat.v2"})
    

    创建时的第一个参数是想要连接的WebSocket服务器的URL. 第二个可选参数是指定协议(指定一个或多个客户端可以接受的协议), 通过字符串指定.

    指定的协议是用户自定义的, 而非由WebSocket定义. 这个参数的作用是提供信息的传递机制(如果需要的话).

  2. 使用WebSocket对象

    WebSocket接口中有若干属性, 以及若干的方法, 下面先来看一些比较常用的.

    readyState:表示当前WebSocket连接的状态.一共有4个值, CONNECTING(数字0), OPEN(1), CLOSING(2), CLOSED(3). OPEN表示的是建立起了连接.比如下面的代码:

    if (connection.readyState == WebSocket.OPEN) {
     /*表示连接成功, 做些什么*/
    }
    

    和XMLHttpRequest不同的是, WebSocket没有onreadystatechange事件(这样需要在事件发生时区判断当前连接状态), 取而代之的是在WebSocket中的四个事件, 对应四个状态的转换:

    connection.onopen = function(event){ ...}
    connection.onclose = function(event){ ...}
    connection.onerror = function(event){ ...}
    connection.onmessage = function(event){ ...}
    

    event参数就含有当前事件的相关信息. 上面的代码就表示当这个事件绑定处理函数. 比如onclose事件触发, 是当readyState从CLOSING转变为CLOSED时触发. 而当HTTP握手结束后, onopen事件触发, 即readyState从CONNECTING转变为OPEN. 而当onopen事件触发时, url, extension, protocol三个对象属性被自动设置并且不能再改变.

    onopen中的event参数就是普通的JS的Event对象, 而onclose中的event则有三个非常有用的属性: wasClean, code, reason. 这三个就是上述关闭连接时可以指定的文本和原因.

    比如可以利用这三个属性来告知用户未正常关闭连接:

    connection.onclose = function(event) {
     if (!event.wasClean) {//即非正常关闭连接时
           alert(event.code + ": " + event.reason);
     }
    }
    

    closure中合法的event.code值在RFC6455中定义的. Code 1000表示正常, 其余的都是不正常时候的code.详见https://tools.ietf.org/html/rfc6455.中的section7.4.

    onerror事件包含一个data属性, 属性值就是错误对象. 一般来说是字符串消息. 只有当客户端错误发生时才出发onerror事件. 而protocol error则会直接导致连接关闭.

    onmessage事件则需要小心处理, 它的event参数也包含一个data属性. 表示的是当消息传送过来获取消息. 如果消息是文本, 则data属性值是字符串类型的; 如果是二进制消息, 而WebSocket对象的binaryType又设置的是默认的blob或arraybuffer, 则data属性值是Blob类型.

    实际使用时一般在初始化WebSocket对象时就为它的binaryType属性进行设置, 然后让它一直保持设置的类别(在运行过程中动态改变它的值也是合法的, 如果需要改变的话):

    var connection = new WebSocket("ws://www.xxx.net/chat");
    connection.binaryType = "arraybuffer";
    

    WebSocket有两个方法:sendclose.

    close方法接受一个表示关闭连接时的code作为参数, 默认是1000, 表示正常关闭. 另外还有第二个可选参数, 用于指定连接关闭时的原因文本, 默认是空.

    send方法则只有一个参数, 用于将这个参数作为消息发送给连接的另外一端. 参数的类型可以是string, Blob, ArrayBuffer或者ArrayBufferView. 而只有在这里才可以使用WebSocket对象的bufferedAmount属性, 它表示上一次send到现在还剩余的未发送数据的数量. 当然你也可以不等上次数据发送完毕而继续发送, 但某些时候需要等待上次数据发送完毕再发送这次的数据:

    connection.onopen = function() {
    //setInterval方法用于间隔某时间就调用一次第一个参数所指定的函数
       var intervalId = window.setInterval(function() {
          if (connection.readyState != WebSocket.OPEN) {//连接未打开
             window.clearInterval(intervalId);   //清除什么
             return ;
          }
          if (connection.bufferedAmount == 0) {//上次数据已发送完毕
             connection.send(updatedModelData); //发送更新后的数据
          }, 50});//每50毫秒调用一次函数
    }
    

3.2 Java的WebSocket API

WebSocket API是从Java EE7开始加入进来的, 在规范JSR 356可以查到.

它里面包含了客户端以及服务端的API.

客户端API是基础API: 包含了一个WebSocket端所必须的基本类和接口(在javax.websocket包中).

服务端API: 建立在客户端API之上并对客户端API进行了扩展(javax.websocket.server包中).

所以针对Java的WebSocket API, 有两大部分组件: 仅用于客户端的API或完整版API(服务端API).

API的详细使用需要查看Java EE7的API文档.

下面分别来看看这两个部分的API.

  1. 客户端API

    主要由以下几个部分组成:(API中将WebSocket看作是应用的载体, 所以用Container)

    • ContainerProvider 类: 包含用于获取WebSocket实现对象的静态方法

    • WebSocketContainer 接口:包含用于连接到远程WebSocket端的方法

      当建立连接后, 相应的方法会返回一个Session对象. 可以在该对象上进行想要的操作, 比如关闭Session的作用就是关闭连接, 或send消息到远端.

      方法中还接受一个表示WebSocket端的对象(EndPoint对象, 这里表示客户端), 该对象就包含有onXXXX方法, 且当onXXXX事件发生时就会自动调用这些方法.

    • RemoteEndpoint 接口:

    • Session 接口:

    需要注意的是: Java的WebSocket API 只是提供了接口, 而非实现. 在编程时可以只使用接口, 而在运行时则需要使用到接口的实现类. 一般的web容器都提供了WebSocket的实现(比如Tomcat).

  2. 服务端API:

    服务端API建立在整个客户端API之上, 提供了额外的处理服务端任务的类及接口.

    • ServerContainer: 是WebSocketContainer的子类, 提供用于注册ServerEndPointConfig对象或使用@ServerEndPoint注解的对象的方法.

      容器环境中使用ServletContext.getAttribute("javax.websocket.server.ServerContainer")来获取ServerContainer对象.

      不过实际使用时, 不需要手动获取. 只需要把作为ServerContainer的对象进行注解(@ServerEndpoint)即可.

      当使用这个类对象时, 需要指定value属性, 该属性对应应用中的一个URL, 表示需要对这个URL做出响应. 而URL开头必须是"/",表示应用中的URL, 并且在URL中可以使用模板参数:

      @ServerEndPoint("/game/{gameType}")
      

      上面的注解可以用这个例子来理解: 比如应用部署的URL是http[s]://www.xxx.org/app, 则服务端可以对诸如ws[s]://www.xxx.org/app/game/chessws[s]://www.xxx.org/app/game/checkers等做出响应. 之后, 该对象中的onClose, onOpen等等的方法就可以指定一个可选参数, 该参数以@PathParam("gameType")注解, 此时就可以在方法中使用URL中的模板参数值了.

      而服务端的event处理方法和客户端的是一样的. 唯一不同在于HTTP连接握手的时候, 而当连接握手完成后, 协议upgrade, 然后服务端和客户端成为两个对等peer, 此时二者就完全是同样的了.

4 其他参考

关于Tomcat中WebSocket连接数的限制, 实际就是TCP连接的最大数目:

TO reach the max alive websocket connection in Tomcat, following config changes need to be done.

  1. {CATALINA_HOME}/conf/server.xml

    ``

  2. Check the number of ports which are available for use in the m/c where Tomcat is deployed:

    cat /proc/sys/net/ipv4/ip_local_port_range

Change this to from 50 till 65535.

sysctl -w net.ipv4.ip_local_port_range="500   65535"

The above configuration changes allows around ~50k live connections in a 2GB Intel Core i5 machine provided the server and client are running in different machines.

下面是一些连接数限制的尝试:

https://blog.krecan.net/2010/05/02/cool-tomcat-is-able-to-handle-more-than-13000-concurrent-connections/

http://grokbase.com/t/tomcat/users/132d14c1q0/achieve-large-number-of-concurrent-websocket-connections-40000-50000

https://github.com/rstoyanchev/spring-websocket-portfolio/issues/52

https://support.appharbor.com/discussions/problems/26648-websockets-max-concurrent-connections-request-queue-limit

https://mrotaru.wordpress.com/2013/10/10/scaling-to-12-million-concurrent-connections-how-migratorydata-did-it/ 经典做法

你可能感兴趣的:(关于 Websockets)