浅谈 WebSocket
使用 WebSocket 的理由
传统的http协议有一个根本性的缺陷,那就是请求只能由客户端向服务器发起,服务器接收到请求后再进行响应,把数据返回给客户端。也就是说,服务器是没有办法主动向客户端传送消息的。
这样一来,如果服务器有状态是频繁变化的,那么,客户端想要实时获悉这些状态势必非常麻烦。如在线多人游戏,聊天室等。
一种可行的解决方案是使用轮询。轮询是指浏览器通过JavaScript启动一个定时器,然后以固定的间隔给服务器发请求,询问服务器有没有新消息。
这种机制不仅效率低下,实时性不够,而且频繁地发起请求也会给服务器带来极大的压力。
另外一种比较靠谱的技术是HTML5的EventSource。EventSource 接口用于接收服务器发送的事件。它通过HTTP连接到一个服务器,以text/event-stream 格式接收事件, 不关闭连接。
相对于WebSocket,这种技术要简单很多,但是其只是从服务器端往客户端单向传输数据,并不能实现真正意义上的全双工通信。因此,WebSocket是目前来说最佳的选择。
有兴趣了解EventSource的小伙伴可以点击这里
WebSocket 协议
WebSocket是HTML5新增的协议,其诞生于2008年。最大特点就是,服务器可以主动向客户端推送消息,客户端也可以主动向服务器发送信息,是一种不受限的全双工通信。
该协议有以下特征:
- 握手阶段利用了HTTP协议来建立连接,因此WebSocket连接必须由浏览器发起。
- 建立在 TCP 协议之上,服务器端的实现比较容易。
- 其可以接收和发送的数据有两种,一种是文本,一种是二进制数据(blob对象或Arraybuffer对象)。一般情况下,我们可以发送JSON格式的文本,这样,在浏览器处理起来就十分容易。
- 数据格式比较轻量,性能开销小,通信高效。
- 请求是以ws://为开头的地址(如果加密,则为wss://)。
- WebSocket协议本身不要求同源策略,也就是某个地址为"http://a.com"的网页可以通过WebSocket连接到"ws://b.com"。但是,浏览器会发送Origin的HTTP头给服务器,服务器可以根据Origin拒绝这个WebSocket请求。
浏览器支持情况
- Chrome
- Firefox
- IE >= 10
- Sarafi >= 6
- Android >= 4.4
- iOS >= 8
服务端实现
不同的编程语言和框架,实现方式各有不同。这里主要讲一下用node如何实现。
node常用的实现有一下几种:
具体如何使用可以查看它们各自的api。下面,我要详细介绍的是另一个WebSocket模块ws。
通过npm install ws --save之后,我们就可以可以编写一个简单WebSocket服务器程序。
// 首先导入ws模块 let WebSocket = require('ws'); // 通过ws模块的Server类实例化一个websocket服务器 let webSocketServer = new WebSocket.Server({ port: 8030 }, err => { console.log('The WebSocket Server already running on: 8030'); }); // 监听客户端请求接入的connection事件,连接建立后,回调函数中会传入这个WebSocket连接实例 webSocketServer.on('connection', ws => { console.log(`Server is connected`) // 对于每个WebSocket连接,可以绑定监听某些事件来进行不同的处理。这里,通过响应message事件,在收到客户端发来消息后再返回一个消息过去。 ws.on('message', mes => { console.log(`Message sent by client: ${mes}`); ws.send(`data responded by Server: ${mes}`, err => { if (err) { console.log(`Server error: ${err}`); } }) }) })
也可以对http服务器进行拓展,在其基础上建立WebSocket服务器。
const Koa = require('koa'); const WebSocket = require('ws'); const bodyParser = require('koa-bodyparser'); const controller = require('./controller.js'); const server = new Koa(); const webSocketServer = new WebSocket.Server({server}, () => console.log('The WebSocket Server already running on: 8030')); // 为websocket服务器添加一个broadcast()方法 webSocketServer.broadcast = data => { // 通过遍历webSocketServer.clients,找到所有与该服务器成功建立websocket连接的客户端,发送同一条消息 for (const client of webSocketServer.clients) { if (client.readyState === WebSocket.OPEN) { client.send(data, err => console.log(`Server error: ${err}`)); } } } webSocketServer.on('connection', ws => { console.log(`Server is connected`); ws.on('message', mes => { console.log(`Message sent by client: ${mes}`); // 接受到其中一个客户端发来的消息后,广播给所有同时连接过来的客户端 const data = { message: mes } webSocketServer.broadcast(JSON.stringify(data)) }) }) server.use(bodyParser()); server.use(controller()); server.listen(8030); console.log('server running on 8030...');
现在,websocket服务器与http服务器同时使用8030端口。当有一个请求发送过来,首先会判断其是否ws请求。若是,则交给WebSocketServer的回调函数处理,否则,还是走正常的http server回调的路子。
另外,我们注意到,这里还给WebSocketServer添加了一个broadcast()方法,用于将消息广播到所有与该服务器成功建立WebSocket连接的客户端上。
每当从其中一个客户端收到一条消息,就将该消息发送到所有WebSocket连接上,基于这种方式,我们就可以搭建一个聊天室应用的后台服务。
ws模块的完整使用方法请看这里
客户端实现
客户端要创建一个WebSocket连接就比较简单了。下面是一个简单的事例:
// 创建一个WebSocket连接: var ws = new WebSocket('ws://localhost:8030/ws'); ws.onopen = function(event) { console.log("Connection open"); // 给服务器发送一个消息: ws.send("Hello WebSocket!"); }; // 响应onmessage事件: ws.onmessage = function(event) { console.log(event.data); }; // 也可以指定接收的二进制数据类型为blob对象 ws.binaryType = "blob"; ws.onmessage = function(event) { console.log(event.data.size); }; // 或 // 指定接收的二进制数据类型为ArrayBuffer对象 ws.binaryType = "arraybuffer"; ws.onmessage = function(event) { console.log(event.data.byteLength); };
详细的属性和方法可以看这里
另外,使用ws模块提供的WebSocket构造函数也可以充当客户端创建连接。
let ws = new WebSocket('ws://localhost:8030/ws'); // 打开WebSocket连接后立刻发送一条消息 ws.on('open', () => { console.log(`Client open`); ws.send('Hello WebSocket!'); }); // 响应收到的消息 ws.on('message', mes => { console.log(mes); });