1、WebSocket的基本原理
1.1、什么是WebSocket?
WebSocket协议在2008年诞生,2011年成为国际标准,目前所有主流浏览器都已经支持了。
浏览器是基于HTTP协议的,为什么还需要WebSocket呢?这要说起HTTP协议的一个缺陷:HTTP请求只能由浏览器发起,HTTP服务器无法主动发起HTTP请求。
随着前端应用的日益增长,浏览器不再仅仅完成简单的Web页面渲染,而是日益复杂的独立应用,出现AJAX、HTML5、RESTFul、React、Vue等技术或框架,其中有一个关键需求就是需要从HTTP服务主动向浏览器推送消息,这就是WebSocket产生根本动因,接下来为了从根本上理解WebSocket,我们分别与Socket和HTTP对照比较一下。
1.2、WebSocket vs. Socket
学过计算机网络的话Socket一词应该比较熟悉,Socket最早是UNIX类的操作系统定义的网络通信接口,我们最常用的就是通过Socket接口调用TCP协议实现网络通信。
WebSocket中socket大概就是沿用Socket接口的含义,并通过Web来说明WebSocket是基于Web的socket接口,换句话说就是在在浏览器中使用的socket接口。
基于TCP的Socket接口支持客户端和服务器双向主动发送数据,WebSocket和HTTP协议一样都是基于TCP协议。
(图片来源于网络)
WebSocket使用ws表示协议标识,wss和https一样表示通信加密。
1.3、WebSocket vs. HTTP
在WebSocket出现之前,HTTP服务器主动推送消息的技术已经出现,比如HTTP客户端通过轮询/心跳不断向服务器发送HTTP请求,当HTTP服务器有消息要推送给客户端的时候就将消息放到HTTP响应中返回给HTTP客户端。这是一种迂回的方式间接实现了在业务层面HTTP服务器主动推送消息。但是频繁的HTTP请求和响应造成的资源开销很大,于是就诞生了WebSocket协议。
WebSocket协议的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话。
(图片来源于网络)
WebSocket和HTTP协议简要比较如下:
都是建立在 TCP 协议之上的应用层协议
与 HTTP 协议有着良好的兼容性。默认端口也是使用80端口和443端口,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
数据格式比较轻量,性能开销小,通信高效。可以发送文本,也可以发送二进制数据。
不像AJAX有同源限制需要较为复杂的机制实现跨域访问,WebSocket客户端天生可以与任意服务器通信。
2、WebSocket的客户端接口
2.1、创建WebSocket对象
WebSocket对象提供了用于创建和管理 WebSocket 连接,以及可以通过该连接发送和接收数据的API。使用WebSocket(url[, protocols])构造函数来构造一个WebSocket对象作为返回值,基本方法范例如下:
var ws = new WebSocket("wss://echo.websocket.org");
2.1、WebSocket对象的状态和属性
WebSocket对象的状态有四种:WebSocket.CONNECTING、WebSocket.OPEN、WebSocket.CLOSING和WebSocket.CLOSED,它们的常量值分别0、1、2和3。
WebSocket对象的属性简要列举如下:
WebSocket.binaryType使用二进制的数据类型连接。
WebSocket.bufferedAmount 只读属性,未发送至服务器的字节数。
WebSocket.extensions 只读属性,服务器选择的扩展。
WebSocket.onclose用于指定连接关闭后的回调函数。
WebSocket.onerror用于指定连接失败后的回调函数。
WebSocket.onmessage用于指定当从服务器接受到信息时的回调函数。
WebSocket.onopen用于指定连接成功后的回调函数。
WebSocket.protocol 只读属性,服务器选择的下属协议。
WebSocket.readyState 只读属性,当前的链接状态。
WebSocket.url 只读属性,WebSocket 的绝对路径。
2.3、WebSocket对象的方法和事件
WebSocket对象的方法只有两个,即close和send。
WebSocket.close([code[, reason]])关闭当前链接。
WebSocket.send(data)对要传输的数据进行排队。
WebSocket对象默认支持四个事件:即open、close、message和error。监听事件方法是使用 将一个事件的回调函数赋值给对应的WebSocket对象的属性,四个事件对应的WebSocket对象的属性为:onopen、onclose、onmessage和onerror。
open事件,当一个 WebSocket 连接成功时触发。可以通过 onopen 属性来设置监听事件的回调函数。
close事件,当一个 WebSocket 连接被关闭时触发。可以通过 onclose 属性来设置监听事件的回调函数。
message事件,当通过 WebSocket 收到数据时触发。可以通过 onmessage 属性来设置监听事件的回调函数。
error事件,当一个 WebSocket 连接因错误而关闭时触发,例如无法发送数据时。可以通过 onerror 属性来设置监听事件的回调函数。
2.4、WebSocket对象的用法范例代码
如下脚本可以打开https://jsbin.com/muqamiqimu/edit?js,console 看到运行效果。
var ws = new WebSocket("wss://echo.websocket.org");
ws.onopen = function(evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};
ws.onmessage = function(evt) {
console.log( "Received Message: " + evt.data);
ws.close();
};
ws.onclose = function(evt) {
console.log("Connection closed.");
3、Socket.IO快速上手
WebSocket的服务端实现框架众多,比如uWebSockets、WebSocket-Node和Socket.IO等,我们选择MIT许可证发布的Socket.IO为例来学习WebSocket服务端接口。
3.1、配置基于Node.JS的Web框架express
下载安装Node.JS参见https://nodejs.org
创建一个项目目录,比如创建了一个名称为“socket.io”的目录
在项目目录下创建一个项目配置文件package.json,文件内容如下:
{
"name": "socket-chat-example",
"version": "0.0.1",
"description": "my first socket.io app",
"dependencies": {}
}
安装Web框架express
npm install express@4.15.2
创建一个源代码文件index.js
var app = require('express')();
var http = require('http').createServer(app);
app.get('/', (req, res) => {
res.send('<h1>Hello world</h1>');
});
http.listen(3000, () => {
console.log('listening on *:3000');
})
执行执行程序index.js
$ node index.js
listening on *:3000
打开http://localhost:3000 查看程序运行效果
3.2、服务端安装集成Socket.IO
安装Socket.IO
npm install socket.io
安装过程中会将对socket.io的依赖添加到package.json文件中。
在服务端集成Socket.IO
修改index.js文件如下:
var app = require('express')();
var http = require('http').createServer(app);
var io = require('socket.io')(http);
app.get('/', (req, res) => {
res.send('<h1>Hello world</h1>');
//res.sendFile(__dirname + '/index.html');
});
io.on('connection', (socket) => {
console.log('a user connected');
});
http.listen(3000, () => {
console.log('listening on *:3000');
});
这时打开http://localhost:3000 查看程序运行效果,与之前完全相同,但是服务器端提供的服务却有所不同,我们启用了Socket.IO能够接收客户端的WebSocket连接请求。
3.3、使用Socket.IO客户端代码连接服务器
Socket.IO不仅仅是WebSocket服务端接口框架,还支持许多种轮询机制以及其他实时通信方式,这些方式包含 Adobe Flash Socket、Ajax 长轮询、Ajax multipart streaming 、持久 Iframe、JSONP 轮询等。
当 Socket.IO 检测到当前环境不支持 WebSocket 时,能够自动地选择最佳的方式来实现网络的实时通信。不同的方式客户端的接口也是不同,Socket.IO将客户端也封装成为了通用的接口。
因此,使用WebSocket客户端代码连接服务器既可以使用在客户端创建WebSocket对象的方式,也可以使用Socket.IO客户端接口。
使用创建WebSocket对象的方式连接服务器需要遵守Socket.IO封装的协议规格,会复杂一些,而且兼容性也没有Socket.IO客户端接口好,因此我们使用Socket.IO的客户端接口连接服务器。
将如下文件内容保存为index.html文件放在项目目录下。
<!doctype html>
<html>
<head>
<title>Socket.IO example</title>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
</script>
</head>
<body>
<h1>Hello world</h1>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" /><button>Send</button>
</form>
</body>
</html>
修改index.js部分内容如下:
app.get('/', (req, res) => {
//res.send('<h1>Hello world</h1>');
res.sendFile(__dirname + '/index.html');
})
也就是在客户端启用var socket = io();与服务器建立连接。这时每打开一次http://localhost:3000 ,服务器都会与之建立一个WebSocket连接。
$ node index.js
listening on *:3000
a user connected
a user connected
a user connected
a user connected
3.4、Socket.IO建立连接的接口
Socket.IO服务端创建Socket.IO对象及监听连接请求
var app = require('express')();
var http = require('http').createServer(app);
var io = require('socket.io')(http);
//...
io.on('connection', (socket) => {
console.log('a user connected');
}
Socket.IO客户端创建Socket.IO对象并发起连接请求
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io();
</script>
3.5、Socket.IO的双向通信接口
自定义服务路径和通信方式。默认路径为/socket.io,我们自定义为myownpath
//服务端代码
var app = require('express')();
var http = require('http').createServer(app);
var io = require('socket.io')(http, {
path: '/myownpath'
});
//...
io.on('connection', (socket) => {
console.log('a user connected');
});
//...
//客户端代码
<script src="myownpath/socket.io.js"></script>
<script>
const socket = io('http://localhost:3000/', {
path: '/myownpath',
transports: ['websocket', 'polling']
});
socket.on('connect', () => {
console.log(socket.id); // 'G5p5...'
});
</script>
transports的默认值为['polling', 'websocket'],也就是Socket.IO默认首先尝试轮询polling方式,然后尝试websocket方式,为了提高效率我们在客户端配置为优先尝试websocket方式。
客户端和服务端双向通信
//客户端代码
socket.on('server', (data, fn) => {
console.log(data);
fn('callback a server function');
});
socket.emit('client', 'data to server', (data) => {
console.log(data);
});
//服务端代码
io.on('connection', (socket) => {
console.log(socket.id);
socket.on('client', (data, fn) => {
console.log(data);
fn('callback a client function');
});
socket.emit('server', 'data to client', (data) => {
console.log(data);
});
});
参考资料
http://www.ruanyifeng.com/blog/2017/05/websocket.html
https://socket.io/get-started/chat/