WebSocket的学习及认识

介绍

Web应用的一般信息交换过程是客户端通过浏览器发出一个请求,服务器端接收和处理收到的请求并返回,然后客户端在浏览器中呈现出内容。也即是说一个request对应一个response请求,只有客户端主动发起才会获得服务端的返回信息,而服务端无法主动发送。

可是有什么用呢?

在一些场景里,我们有时会对应用的实时性要求比较高,类似于股票信息,设备监控,聊天室等等,这些都需要服务器能够主动的推送信息到客户端。使用传统的技术来解决这些问题的方案主要有轮询和comet,

  • 轮询是使用定时器周期性的向服务端发送请求,但是同样也会带来问题,首要的是如果服务器未更新信息而频繁的发送请求会增加服务器压力并造成资源的浪费,所以这是一种很低效的方式。
  • comet也是一种轮询,只是在服务器没有信息返回的时候会保持一段时间,直到数据、状态改变或者时间过期,通过这种机制来减少无效的客户端和服务器间的交互,但在比较频繁的交互过程里,这种方式并没有本质上的改变。

HTML5 WebSocket的推出让浏览器和服务器之间可以建立无限制的通信,任何一方都可以主动发消息给对方。

WebSocket

WebSocket协议是基于TCP的一种新的协议,它实现了浏览器与服务器全双工通信。

借用维基百科的一段请求,类似于下面

1
2
3
4
5
6
7
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
Origin: http://example.com

相比较于经典的http请求这里面GET请求的地址需要以ws://或者wss://开头的地址;

而多出来的一些东西

1
2
Upgrade: websocket
Connection: Upgrade

表示要告诉服务器要处理为WebSocket连接
Sec-WebSocket-Key是一个Base64 encode的值,用于标识这个连接;
Sec-WebSocket-Version指定了WebSocket的协议版本。

服务器如果接受了请求就会返回的一段响应

1
2
3
4
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=

那么经过一段复杂的处理过程,WebSocket连接也即算是建立了,之后就可以在客户端和服务端互相发送数据。数据可以是字符串或者二进制数据,一般来说,使用json字符串跟有利于解析和处理~

WebSocket事件、属性、方法

事件

WebSocket主要事件有onopenonmessageonerror 以及 onclose,同样也可以使用addEventListener来监听事件。

  • open: 一旦服务端响应WebSocket连接请求,就会触发open事件。
  • message: 当消息被接受会触发消息事件。
  • error: 如果发生意外的失败会触发error事件,错误会导致连接关闭。
  • close: 当连接关闭的时候回触发这个事件,连接关闭之后,服务端和客户端就不能再收发消息。

属性

WebSocket对象有三个属性,readyState,bufferedAmountProtocol
readyState的取值和表示意义如下:

Constant Value Description
CONNECTING 0 连接正在进行,但还没有建立
OPEN 1 连接已经建立,可以发送消息。
CLOSING 2 连接正在进行关闭握手
CLOSED 3 连接已经关闭或不能打开

bufferedAmount属性检查已经进入队列但还未被传输的数据大小;protocol参数让服务端知道客户端使用的WebSocket协议。

方法

WebSocket 对象有两个方法:send()close()

当连接是open的时候可以使用send()方法传送数据,一般会通过检查readyState的值来确定是否发送;close()方法用来关闭Socket连接。

Echo服务器能够接收和返回发送的数据,这样我们可以在客户端测试WebSocket的连接,建立一个简单的WebSocket连接如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function() {
var ws = new WebSocket('ws://echo.websocket.org')
console.log(ws)
ws.onopen = function() {
console.log('connected')
}
ws.onmessage = function(e) {
console.log('receive:' + e.data)
}
ws.onclose = function() {
console.log('close')
}
function send() {
var msg = {
msg: Date.now()
}
ws.send(JSON.stringify(msg))
}
}())

image

服务端建立WebSocket

真正的WebSocket应用需要有服务端的支持,在GitHub上有一个nodejs的WebSocket库—— ws ,可以用来创建相关的服务。在ws的官方文档上看到了使用ExpressJS集成ws的例子,ExpressJS是一个极简、灵活的web应用开发框架,可以方便的来构建各种web应用。

  1. 首先需要安装nodejs,就不说了
  2. NPM安装express,express-generator
  3. 使用express生成器创建工程并使用npm install安装相关依赖(expres生成项目时使用-e参数可以更改为ejs模板引擎)
  4. 引入ws,npm install --save ws

经过上面几步,所需要的基本环境就搭建完成了。最后呈现的目录结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|____app.js
|____bin
| |____www
|____package.json
|____public
| |____images
| |____javascripts
| | |____index.js
| |____stylesheets
| | |____style.css
|____routes
| |____index.js
| |____users.js
|____views
| |____error.ejs
| |____index.ejs

所有的依赖文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
"name": "apiserver",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"body-parser": "~1.13.2",
"cookie-parser": "~1.3.5",
"debug": "~2.2.0",
"ejs": "~2.3.3",
"express": "~4.13.1",
"morgan": "~1.6.1",
"serve-favicon": "~2.3.0",
"ws": "^2.2.3"
}
}

接下来就是需要编写有关WebSocket服务,express启动服务的部分写在了./bin/www中,为了避免和http服务混淆,这里单独建立一个模块来实现。在根目录下创建main.js,并写入以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// main.js
module.exports = function(server) {
var WebSocket = require('ws');
var wss = new WebSocket.Server({ server })
wss.on('connection', function(ws) {
ws.on('message', function(message) {
console.log('received: %s', message)
ws.send('from server:' + message)
})
ws.on('close', function(res) {
console.log('close')
});
})
}

稍微扩展一下,在NodeJS中模块导出方式有7种

  1. 导出一个命名空间
  2. 导出一个函数
  3. 导出一个高阶函数
  4. 导出一个构造函数
  5. 导出一个单体
  6. 扩展一个全局对象
  7. 运用一个猴子补丁(Monkey Patch)

具体可以参考 http://www.tuicool.com/articles/eyeUBz

修改./bin/www,将WebSocket服务加到server中,首先头部引入main模块,并在http服务创建后建立WebSocket服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('apiserver:server');
var http = require('http');
var ws = require('../main');
/**
* Create HTTP server.
*/
var server = http.createServer(app);
ws(server);
...

经过了以上的修改,使用npm run start命令即可启动服务了。因为node服务在修改代码后总是需要重新启动,这里也有个取巧的方式,全局安装supervisor模块,修改package.json中scripts为"start": "supervisor ./bin/www",以后就可以在保存代码后服务自动重新启动了。

把客户端的请求地址改成自己本地的服务地址

var ws = new WebSocket('ws://127.0.0.1:3000')

在客户端发送一条信息后可以得到如下的返回:
image
image
这样就建立起了WebSocket的双向连接。

参考资料

https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket
https://www.ibm.com/developerworks/cn/web/1112_huangxa_websocket/
https://github.com/websockets/ws/
http://www.liaoxuefeng.com/wiki/001434446689867b27157e896e74d51a89c25cc8b43bdb3000/001472780997905c8f293615c5a42eab058b6dc29936a5c000
https://www.zhihu.com/question/20215561
http://www.cnblogs.com/stoneniqiu/p/5373993.html