SocketIO 将 Websocket、Ajax 、轮询(Polling)和其他通讯方式全部封装成统一的通信接口,因此 SocketIO没有兼容性的问题,底层会自动选用最佳的通讯方式。
SocketIO实现了实时双向的基于事件的通讯机制,是基于 WebSocket 的封装,另外还包括对轮询(Polling)机制以及其他实时通讯方式封装成了通用的接口,并且在服务端实现了这些实时机制的相应代码,会自动根据浏览器对各种通讯方式上选取最佳的方式。
Socket.IO 支持4种协议:WebSocket、htmlfile、xhr-polling 和 jsonp-lolling,会根据浏览器选择适合的通讯方式。
从 开始
socket.io@4.1.0
,Engine.IO 服务器发出三个特殊事件:
- Engine.IO 使用 WebSocket 和 xhr-polling封装了一套自己的协议,在不支持 WebSocket 的低版本浏览器中使用长轮询的方式代替。SocketIO 在 Engine.IO 的基础上增加了 namespace,room,自动重连等特性。
- Engine.IO 服务器必须支持 polling(包括jsonp和xhr)和 WebSocket 两种传输方式。Engine.IO 使用 WebSocket 时有一套自己的 ping/pong 机制,使用的是
opcode
为0x1(Text)类型的数据帧,不是 WebSocket 协议故一定的 ping/pong 类型的帧,标准的 ping/pong 帧被 uwsgi 使用(uwsgi 里发送 ping,浏览器返回 pong;engine.IO 是浏览器客户端发送 ping,服务端返回相应 pong。)。- Engine.IO 的书编码分为 Packet 和 Payload,其中 Packet 是数据包,用于 WebSocket 格式的数据传输,而 Payload 是指一系列绑定到一起的编码后的 Packet,只用于 poll 中,WebSocket 里面使用 WebSocket 帧里面的 Payload 字段来传输数据。如客户端不支持 XHR2,则 payload 格式为
<length1>:<packet1>[<length2>:<packet2>[...]]
,其中 length 是数据包的 Packet 的长度,packet 是编码后的数据包内容(也是客户端发送给服务端的 poll 请求 payload 格式)。若只支持 XHR2,则 payload 中内容全部以字节编码,其中第一位0表示字符串,1表示二进制数据,后面接着的数字则表示 packet 的长度(服务端返回给客户端的 payload 为这种字节编码)
initial_headers
: 针对于会话的第一个 HTTP 请求相应前进行自定义响应头。
io.engine.on("initial_headers", (headers, req) => {
headers["test"] = "123";
headers["set-cookie"] = "mycookie=456";
});
headers
:针对会话的每个 HTTP 请求的响应头发送前进行自定义响应头。
io.engine.on("headers", (headers, req) => {
headers["test"] = "789";
});
connection_error
: 连接异常关闭时会发出。
io.engine.on("connection_error", (err) => {
console.log(err.req); // the request object
console.log(err.code); // the error code, for example 1
console.log(err.message); // the error message, for example "Session ID unknown"
console.log(err.context); // some additional error context
});
命名空间
所谓命名空间就是指在一个域中发送消息,只有在当前域的 socket 可以收到消息,其他域的则不能;在服务端可以使用
of
函数进行创建命名空间;客户端则使用io
的参数进行连接;
- 服务端:通过io.of创建命名空间;
- 服务端进行消息推送的时候需要并行使用命名空间;
- 客户端向服务器发起连接的时候也需要指定对应的命名空间。
多个命名空间实际上共享相同的
WebSockets
连接,从而在服务器上节省了我们的套接字端口;同时可以分配不同的端点和路径,最大限度上减少资源数量(tcp
连接);
-
客户端指定命名空间:
const newsSocket = io('/news');
-
服务端对应处理为:
const io = require('socket.io)();
//news 命名空间
const newsSocket = io.of('/news');
newsSocket.on('connection' function(socket){
...
})
newsSocket.emit('Lbxin','lbxin one')
- 在对应的命名空间里发送消息
io.of('nres').send('test');
io.of('news').to('room1').send('Lbxin')
轻松实现群聊和私聊
- 对于群聊,只需要把每个
socket
扔到同一个房间即可;- 对于私聊只需要把两个
socket
扔到同一个房间即可;- 对应的
io
方法是socket.join('room1')
和socket.leave('room2')
。
- 加入房间后进行消息推送(广播或者分发可以简单的使用
to
或者in
)
io.on('connection',function(socket){
socket.join('Lbxin room');
})
io.to('Lbxin room').emit('test lbxin')//向房间中的除发件人以外的用户进行推送;
io.in('room1').emit()//向特定房间中的每个人广播,包括发件人。
//in()和to()方法都可以进行连续使用,to().to().to().....
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
app.get('/', function (req, res) {
res.sendfile('index.html');
});
var nsp = io.of('/my-namespace');
nsp.on('connection', function (socket) {
console.log('someone connected');
nsp.emit('hi', 'Hello everyone!');
});
http.listen(3000, function () {
console.log('listening on localhost:3000');
});
<!DOCTYPE html>
<html>
<head>
<title>Hello world</title>
</head>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io('/my-namespace');
socket.on('hi', function (data) {
document.body.innerHTML = '';
document.write(data);
});
</script>
<body></body>
</html>
套接字
每个新连接都分配了一个随机的20个字符的标识符,次标识符与客户端的值同步。
// server-side
io.on("connection", (socket) => {
console.log(socket.id); // ojIckSD2jqNzOqIrAGzL
});
// client-side
socket.on("connect", () => {
console.log(socket.id); // ojIckSD2jqNzOqIrAGzL
});
加入指定标识符的房间后就可以用于私人消息传递。因此该标识符不能被覆盖。
io.on("connection", socket => {
socket.on("private message", (anotherSocketId, msg) => {
socket.to(anotherSocketId).emit("private message", socket.id, msg);
});
});
中间件
中间件是为每个传入连接执行的函数,可用于日志记录、认证/授权、权速,但该函数只会在每个连接中执行一次即时连接包含多个 HTTP 请求。
可以注册多个中间件函数,会依次执行,但前提是前一个中间件函数调用了next()
且没有在next()
中抛出错误。当前一个中间件函数没有调用next()
函数时,连接将保持挂起,直到在给定的超时时间后关闭。在next()中抛出错误后连接将被拒绝且客户端会收到一个connect_error
的事件。起err.message
就是next中抛出错误中的信息。
中间件函数在执行的过程中,socket 实例并未连接,这意味值如果最终连接失败,则不会发出任何事件。
io.use((socket, next) => {
if (isValid(socket.request)) {
next();
} else {
next(new Error("invalid"));
}
});
io.use((socket, next) => {
next(new Error("thou shall not pass"));
});
io.use((socket, next) => {
// not executed, since the previous middleware has returned an error
next();
});
socket 的取消订阅和关闭连接
- 加入 socket 房间后需要订阅房间内的所有聊天内容,假如在加入房间前没有取消订阅,结果会是别人发送一条消息,你接收到多条消息通知。
beforeDestroy() {
this.sockets.unsubscribe(eventName)
}
- 当在指定页面中才加入 socket 房间,则需要在退出页面的时候进行关闭 socket 连接,不然下次进入会重复连接 socket。
created() {
this.$socket.open()
// 查看socket是否连接成功
this.$socket.connected
}
beforeDestroy() {
this.$socket.close()
}