在浏览器端,大部分人都比较熟悉的协议还是HTTP协议,但是如果要开发如聊天应用之类的实时应用,HTTP协议的延迟显然就过高了。今天要介绍的这个协议是WebSocket,它是一个双向通信的协议,在处理实时应用的场合十分常见。
WebSocket的历史背景
在深入探索WebSocket协议之前,让我们来思考一下,在过去如果要让服务器和客户端进行双向通信最常用的几种技术。
第一个方法就是使用轮询机制(polling),通过每隔一段时间就去请求一次服务端来获取服务器最新的更新,这显然是比较低效的。而且要求服务器有很快的处理速度和资源,因为如果服务器不能及时返回响应数据,那么很多请求就轮空变无效了。
// 不断发起请求
setInterval(function() {
// do something with ajax
}, 100)
在这基础上又演变出了长轮询机制(long polling),也就是说客户端会开启一个连接,然后服务端保持这个连接直到数据能够被获取到或者产生超时。虽然不用频繁请求服务器,在性能上有所提高,但是也十分耗费服务器的资源。
function load() {
$.ajax({url: '/example', success: function(data) {
// do something with data
}}, complete: load, timeout: 10000)
}
由于以上方法本质上还是用HTTP协议去完成实时通信的工作,那么每一次HTTP请求都会附带上大量的HTTP请求头,这会导致资源包过大,而且由于HTTP协议半双工的工作机制,客户端和服务端都必须等待对方的数据发送完之后才能够进一步地做出响应,这就导致服务端在处理实时请求时消耗更多的资源。
为了更有利于实时应用的开发,HTML5中就引入了websocket协议。它本质上也是基于HTTP协议,不过是全双工机制的,也就是说只要建立起一次握手连接后就能源源不断地发送数据。
如何使用WebSocket
WebSocket是HTML5技术体系的一部分,和http协议类似,分为服务器端和客户端。首先服务器端负责提供提供一个WebSocket端口用于通信,客户端连接到服务器端进行数据传输。
在浏览器中使用WebSocket要先进行浏览器检测:
if(window.WebSocket) {
// do something
}
然后初始化:
const socket = new WebSocket("ws://localhost:8080")
这里连接到本地的8080端口,地址以ws://
开头。
建立了WebSocket的对象之后,我们需要采用事件监听的模式进行编程。官方暴露出来的API比较简单,总共有4个事件,它们分别是onopen(打开连接),onclose(关闭连接),onmessage(消息传递),还有onerror(错误处理)。
先来说一下建立连接的过程,当连接建立的时候,我们通常在它的回调函数里面去处理一些初始化的工作,包括获取要操作的页面元素以及打印出连接建立的信息等等。
socket.onopen = function(event) {
console.log("connection established");
}
而关闭连接onclose通常就是用来处理一些收尾工作和一些清理的工作。还可以检测一下是否是意外断开的:
socket.onclose = function(event) {
console.log("connection exited")
// 只有1000的状态码表示连接正常端口
if(event.code != 1000) {
if (!navigator.onLine) {
console.log("You are offline")
}
}
}
在通信过程当中,如果发现一些错误的话,我们就需要去主动的用onerror事件进行捕获。
socket.onerror = function(event) {
const errorMsg = document.getElementById('errorMsgBox');
errorMsg.innerHTML = event;
}
最后onmessage事件应该算是业务处理当中的主角,它是用来数据传递的一个主要事件。我们通常会在它的回调函数当中根据需要编写业务代码,包括数据转换,业务逻辑处理等的一系列工作。
socket.onmessage = function(event) {
// do something
}
Actions
浏览器端还提供了两个主要用于触发发送数据和关闭连接的事件。
当连接建立起来后,你可以显式地使用send
方法发送数据到服务器端:
const button = document.getElementById('sendBtn');
button.onclick = function() {
// 先判断连接是否处于开启状态
if (socket.readyState === WebSocket.OPEN) {
socket.send("hello world")
}
}
关闭事件同理,直接调用socket.close()
即可:
const button = document.getElementById('closeBtn');
button.onclick = function() {
// 先判断连接是否处于开启状态
if (socket.readyState === WebSocket.OPEN) {
socket.close()
}
}
WebSocket服务器
了解了客户端的一些相关知识之后我们再来看一下如何去开发一个web socket的服务端。通常根据不同的服务端语言,目前都有很好的框架和工具进行处理。这里,还是采用比较熟悉的Node.js进行开发。
const WSServer = require('ws').Server;
const wsServer = new WSServer({port: 8080});
wsServer.on('connection', function(socket) {
socket.on('message', (message) => {
socket.send(message)
});
})
这样我们就建立了一个监听在8080端口上的WebSocket服务器了。
数据传输
关于WebSocket协议支持的数据类型,比较常用的是三类:String,ArrayBuffer和Blobs。
可以显式地用代码声明要传输数据的类型:
socket.binaryType = "arraybuffer"
socket.binaryType = "blob"
socket.binaryType = "string"
当传输数据过程中也可以去判断数据类型的格式:
socket.onmessage = function(event) {
if(typeof event.data === "string") {
// do something
JSON.parse(event.data)
}
}
其中ArrayBuffer的数据类型主要用于结构化的数据,特别是用来存储图片文件。
可以用如下方法判断ArrayBuffer的数据类型:
if (event.data instanceof ArrayBuffer) {
const buffer = event.data
}
在实际使用过程中,通常用于文件数据的读取并发送:
document.ondrop = function(event) {
const file = event.dataTransfer.files[0];
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function() {
socket.send(reader.result);
}
return false;
}
上面这个例子实现了处理鼠标拖拽图片,然后将图片用WebSocket发送到服务器端。
还有另外一个比较常用的数据类型是Blobs(Binary Large Objects),它通常用来传输一些大的文件,当然也可以用来传输图片:
socket.onmessage = function (event) {
if (event.data instanceof Blob) {
const blob = event.data;
window.URL = window.URL || window.webkitURL;
const source = window.URL.createObjectURL(blob);
const image = document.createElement("img");
image.src = source;
image.alt = "Image generated from blob";
document.body.appendChild(image);
}
}
socket.io
接下来介绍一个在开发实时应用方面比较著名的框架——Socket.io,就像JQuery封装了常用了DOM操作方法一样,Socket.io底层也对WebSocket进行了封装,以便在不同的浏览器上都能正常工作。当浏览器版本不支持WebSocket的时候,Socket.io会采用轮询等方式兼容性地工作,从而保障应用的正常使用。
下面是个简单的使用例子,首先来开发服务器:
const app = require('express');
const server = require('http').Server(app);
const io = require('socket.io')(server);
const port = 3000;
server.listen(port, () => {
console.log("Server is listening on port", port);
});
app.get('/', (req, res) => {
res.sendFile(__dirname + 'public/index.html');
});
io.on('connection', (socket) => {
socket.emit('message', {msg: "hello"});
socket.on('data', (data) => {
console.log(data);
});
});
这里采用的是express + Socket.io的方式进行开发,而浏览器端可以直接进行通信:
<script>
const socket = io.connect("http://localhost:3000")
socket.on('message', (data) => {
socket.emit('data', {data: "world"}j)
})
</script>
配置一下打包的npm script脚本:
{
"scripts": {
"start": "nodemon ./index.js --exec babel-node -e js"
}
}
如果要进行测试的,建议用以下命令行进行启动,它会打印出所有的调试信息:
DEBUG=* npm run start
总结
WebSocket协议是用来开发实时应用最常用的协议方式,而其中的Socket.io也成为完成这项工作不可或缺的框架。如果你有要开发聊天应用、通知功能以及实时股票分析等需求的话,强烈建议你深入学习并熟练掌握WebSocket的开发方式。
参考资料
《Getting Started with HTML5 WebSocket Programming》