在没有websocket之前,大家都如何实现信息推送的呢?很久很久以前有个技术叫推送,使用的概念叫Webcasting。大体意思就是用户来服务端注册一个或者多个通道,然后服务端确定给某些个通道或某个通道发送消息。很久以前有个flash技术,利用flash插件与服务端进行相互推送。不久以前有个comet,比如有Polling、Long Polling(当有消息的时候才返回),至于HTTP发起是在不同的哪些地方,Ajax或者Flash或者Iframe,都有各自不同的叫法和名词。但是不管什么技术,都限制于浏览器,因为浏览器只能发起HTTP请求。除flash技术之外其他都是利用ajax模拟实现双向通信,每一次交互都是一次完整的http请求应答过程,显然http不是为双向通信长连接而生的。故而推送最好的一种方法当然是保持Socket长连接,浏览器如何能发起TCP连接呢?HTML5的Websocket就解决了这个问题。
Websocket不是一项新技术,而是由一套websocket协议及其api组成的支持socket连接的基于浏览器模块,也就是说借着html5在浏览器增加了一个socket通信程序的客户端,在安装浏览器的时候默认会安装一个客户端,可以向服务端请求并建立链接,然后像原先的socket程序一样来回互相推送消息,并且定义了一套协议以及一套支持协议的可js操作的api。相比于传统的http请求而言,websocket是http请求的升级版,在基于tcp请求以及http请求/响应的基础上建立只需要握手一次的websocket连接。并通过其发送的报文头告知服务端我这是一套新的协议你是否支持,如果支持,那么我们就要保持连接并且我给你发信息同时你也要主动给我发信息。相比于之前模拟双向通信websocket有两大好处:1.只需要握手一次即可互相发送信息(直到一方主动关闭连接);2.请求响应头相比http请求减少了许多(这样就节省了网络流量以及带宽耗费)字节;而且相比较于ajax请求websocket不存在跨域问题。
websocket握手过程如下:
客户端的握手如下:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
服务端的握手如下:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: sample
分析请求包需求如下:
必须是有效的http request 格式
HTTP request method 必须是GET,协议应不小于1.1 如: Get /chat HTTP/1.1
必须包括Upgrade 头域,并且其值为“websocket”
必须包括"Connection" 头域,并且其值为 "Upgrade"
必须包括"Sec-WebSocket-Key"头域,其值采用base64编码的随机16字节长的字符序列, 服务器端根据该域来判断client确实是websocket请求而不是冒充的,如http。响应方式是,首先要获取到请求头中的Sec-WebSocket-Key的值,再把这一段GUID"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"加到获取到的Sec-WebSocket-Key的值的后面,然后拿这个字符串做SHA-1 hash计算,然后再把得到的结果通过base64加密,就得到了返回给客户端的Sec-WebSocket-Accept的http响应头的值。
如果请求来自浏览器客户端,还必须包括Origin头域。 该头域用于防止未授权的跨域脚本攻击,服务器可以从Origin决定是否接受该WebSocket连接。
必须包括"Sec-webSocket-Version" 头域,当前值必须是13.
可能包括"Sec-WebSocket-Protocol",表示client(应用程序)支持的协议列表,server选择一个或者没有可接受的协议响应
可能包括"Sec-WebSocket-Extensions", 协议扩展,某类协议可能支持多个扩展,通过它可以实现协议增强
可能包括任意其他域,如cookie
服务端与客户端确认并支持websocket协议,并且通过 Sec-WebSocket-Key与Sec-WebSocket-Accept确认通信成功,双方就建立了一个websocket通讯链接,进而双方就可以进行数据的传输了,直至链接任何一方主动关闭为止都不会重新握手。
实现数据传输
要互相传输数据就要code了,在实际开发过程中为了使用websocket构建web应用,我们首先要构建一个实现了websocket规范的服务端,服务端不受语言及平台限制,只要遵循websocket协议即可。目前比较成熟的websocket服务端有这么几个:
Kaazing WebSocket Gateway: 一个 Java 实现的WebSocket Server
mod_pywebsocket: 一个 Python 实现的WebSocket Server
Netty:一个 Java 实现的网络框架其中包括了对WebSocket的支持
node.js:一个 Server 端的 JavaScript 框架提供了对WebSocket的支持
如果以上不能满足当前业务的话开发人员可以根据规范自己定义实现一服务器。
我们选择web端易懂并且火热的nodejs来作为服务端来构建一个局域网内的简单聊天应用,step to step:
-
官网下载nodejs并安装默认安装到c盘,下载地址:nojs官方网站;
-
在我们的defg选择一个盘作为工程目录,假设目录名称为websocket,在里面新建一个文件夹并命名为:node_modules,这个名称跟nojs安装目录下一个插件目录名称相同。然后新建一个js文件comm.js以及html文件index.html;
-
目前比较火的websoket服务端插件当属socket.io莫属,不过在此我们不用socket.io,目的就是了解原生html5的websocket实例以及其方法应用。于是我选择websoket-sever下载地址:websocket-sever下载;
-
下载解压后重命名为“websocket_sever”并放置我们新建的node_modules文件夹内;
接下来在我们的comm.js内code启用服务代码:
var http = require('http'), fs = require('fs'),ws = require('websocket-server');
//创建server
var server = http.createServer(function(req, res){
res.writeHead(200,{ 'Content-Type': 'text/html' });
res.end(fs.readFileSync(__dirname + '/index.html'));
});
server.listen(8080);
引用三个模块,http、fs、websocket-server,http模块主要用来监听客户端发起的http请求,请求监听端口为8080,响应后客户端会打开我们的index页面,fs模块作用就是操作文件。
var conns = new Array();
server.addListener('connection', function(conn){
console.log('connecting....');
conns.push(conn);
conn.addListener('message',function(msg){
for(var i=0; i<conns.length; i++){
if(conns[i]!=conn){
conns[i].send(msg);
}
}
});
});
console.log('running......');
server.listen(8088);
ws模块则是新建一个websocket连接,我们定义一个数组存放所有client端,并且监听消息,不给本身client发送信息,这样一来即减少了数据传输,又能将缓冲池中数据发送完再发送下一个。不过如果client端如果需要该数据 就必须在send时候回调使用方法。我们发现在http与websocket分别监听了两个端口,那么这两个端口有什么区别呢?第一个也就是8080端口实际就是启动一个htp服务,并通过这个请求打开一个页面。第二个端口也就是8088端口才是真正的websocket连接所用的端口。
5. 服务端构建完毕,然后在我们的index.html内书写我们的client端接受并发送数据,要新建一个websocket连接需要新建一个websocket实例,并且传入一个字符串形如“ws://localhost:8088”,其中ws或者wss为websocket的url必须(类似http以及https),并且后跟websocket监听端口;websocketApi在html5中定义了三个事件分别是open(连接建立后执行事件)、close(连接关闭后执行事件)、message(收到消息后执行方法),当socket打通后利用send方法client端发送数据。
var host = '192.168.129.41';
var port = 8088;
var url = 'ws://'+host+':'+port+'/';
var w = new WebSocket(url); //实例化一个websocket
w.onopen = function(){ //通过onopen事件句柄来监听socket的打开事件
$('xxx').innerHTML = '已连接到服务器......<br/>';
}
w.onmessage = function(e){ //用onmessage事件句柄接受服务器传过来的数据
var msg = e.data;
var qqBox= $('xxx');//消息显示区域
qqBox.innerHTML = qqBox.innerHTML+msg+'<br/>';
}
function send(){//使用send方法向服务器发送数据
var say= $('say');
var name= $('name');
w.send('<strong style="color:red">'+name.value+':</strong>'+say.value);
var qqBox= $('xxx');
qqBox.innerHTML = qqBox.innerHTML+'<strong style="color:red">'+name.value+':</strong>'+say.value+'<br/>';
//这一句如果注销掉的话相当于发送的信息自己不会看见
}
function $(id){
return document.getElementById(id);
}
6. 至此为止我们整个工程构建完毕,启动该web应用打开nodejs的prompt窗口切换到我们工作目录路径,键入:node comm.js,然后在客户端输入192.168.129.41::8088即可开始局域网内聊天了。结果如图:
说明:由于websocket协议不断升级,浏览器支持版本状况也不同,我下载的websocket-sever插件目前在safari浏览器没问题,而在谷歌浏览器会报错,原因就是因为协议版本造成通讯握手验证没有通过。
了解整个websocket工作流程后我们可以用socket.io插件来代替websocket-sever,该插件也是基于nodejs的第三方插件,安装运行方法与websocket-sever一样只是client端书写方式不同(必须引用socket.io/socket.io.js)方可,该插件相比于websocket-sever而言会检测你的浏览器是否支持websocket协议如果不支持就会逐个检测是想建立长轮询还是flash通讯来实现长连接相互推送数据(也就是传说的socket.io插件支持到了ie5.5)。