一些参考内容:
- 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:
和第一种类似, 只是当服务器没有更新数据的时候不会发送响应.
但这种机制也给自己挖了坑:
- 假如客户端在服务器响应之前有新数据要发送给服务器
- 连接超时机制内建在TCP和HTTP规范内, 即如果服务器不响应就会关闭连接, 此时服务器和客户端就必须周期性地关闭当前连接并建立新连接.
- 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连接的建立过程:
-
首先是使用特殊的scheme来请求URL, scheme为
ws
或wss
.ws
和wss
类似就是http
和https
的区别.而发送的请求则是普通的HTTP请求, 其中包含
Connection:Upgrade
头以及Upgrade
头指定协议列表(当然这里指定的协议就是websocket
协议了, 即Upgrade: websocket
).Upgrade请求头的目的是告知服务器将连接改变为WebSocket协议: 该协议是一个持久的, 全双工的通信协议. 并且在2011年标准化(RFC 6455).
握手结束后, 全双工通信的通道被建立起来, 文本或二进制信息可以在上面同时双向发送(全双工), 而无需像HTTP那样关闭再重建连接. 这种连接下, 服务器和客户端地位平等, 都是简单的peer.(p2p)
ws以及wss两种scheme类型严格地说并非HTTP协议族中的部分. 而且ws或wss的作用只是用来告诉浏览器或某个API你想使用哪种类型的连接(SSL/TLS连接wss或无加密连接ws)
使用WebSocket协议的好处很多, 主要和它的实现有关:
- 它使用的端口和HTTP协议的端口相同, ws(80), wss(443), 基本上不会被任何防火墙阻止.
- 由于是基于HTTP协议握手, 所以WebSocket是内建在了浏览器和HTTP服务器中.
- 心跳信息(HeartBeat)
ping
和pong
在两端间持续传送, 保证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请求, 比起现在可以说复杂很多)
下面就来建立客户端.
-
创建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定义. 这个参数的作用是提供信息的传递机制(如果需要的话).
-
使用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有两个方法:
send
和close
.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.
-
客户端API
主要由以下几个部分组成:(API中将WebSocket看作是应用的载体, 所以用Container)
ContainerProvider
类: 包含用于获取WebSocket实现对象的静态方法-
WebSocketContainer
接口:包含用于连接到远程WebSocket端的方法当建立连接后, 相应的方法会返回一个Session对象. 可以在该对象上进行想要的操作, 比如关闭Session的作用就是关闭连接, 或send消息到远端.
方法中还接受一个表示WebSocket端的对象(EndPoint对象, 这里表示客户端), 该对象就包含有onXXXX方法, 且当onXXXX事件发生时就会自动调用这些方法.
RemoteEndpoint
接口:Session
接口:
需要注意的是: Java的WebSocket API 只是提供了接口, 而非实现. 在编程时可以只使用接口, 而在运行时则需要使用到接口的实现类. 一般的web容器都提供了WebSocket的实现(比如Tomcat).
-
服务端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/chess
或ws[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.
-
{CATALINA_HOME}/conf/server.xml
``
-
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://github.com/rstoyanchev/spring-websocket-portfolio/issues/52