01 Socket.io 简介
- 一个100%由 JavaScript 实现、基于Node.js的用于实时通信、跨平台的开源框架,它包括了客户端的 JavaScript 库和 服务器端的 Node.js 服务。
- 实现了对于其他语言的支持,如 Java、C++、Swift。
- 提供了一个与 WebSocket 类似的通用 API:
【主要特点】:
(1) 可靠性(Reliability):依赖 Engine.IO, 首先建立长轮询,然后试着升级到更好的传输方式,如 WebSocket。
(2)自动重连(Auto-reconnection support):除非手动设置,否则当客户端断开连接时会一直尝试重连。
(3)心跳检测(Disconnection detection):在 Engine.IO 层面实现的心跳检测机制,允许服务器和客户端知道哪一方不再响应。
Engine.IO:为 Socket.IO 实现的基于传输、跨浏览器/跨设备的双向通信层。
(4)其它特点(如下图):
02 工作流程
Socket.IO 底层是 Engine.IO,这个库实现了跨平台的双向通信,使用下面的传输方式封装了一套自己的 Socket 协议(EIO Socket)。
- polling: XHR / JSONP polling transport
- websocket: WebSocket transport
默认情况下,一个完整的 EIO Socket 包括多个 XHR 和 WebSocket 连接:
EIO Socket 会首先发起XHR长轮询,然后服务端会返回以下字段:
- 0:open标志
-
sid
:当前连接的socket id -
upgrade
:表示可以把连接方式从长轮询升级到 websocket -
pingInterval
:心跳间隔 -
pingTimeout
:心跳超时时间
前端收到握手的 upgrades 后,EIO 会检测浏览器是否支持 WebSocket,如果支持,就会启动一个 WebSocket 连接,然后通过这个 WebSocket 往服务器发一条内容为 probe, 类型为 ping 的数据。如果这时服务器返回了内容为 probe, 类型为 pong 的数据,前端就会把前面建立的 HTTP 长轮询停掉,后面只使用 WebSocket 通道进行收发数据。(socket.io 的详细工作流程是怎样的?)
心跳检测:
EIO Socket生命周期内,会间隔一段时间 ping - pong 一次,用来测试网络是否正常
绿色:发送;白色:接收。
类型—> 2:ping,3:pong,4:message。
Socket.IO 在 Engine.IO 的基础上做了一些封装,比如 Socket.IO 里面这样的代码:
io.emit('add user', 'm') ;
在 Engine.io 里面是这样:
eio.send('message', '2["add user","m"]') ; // 2 是 socket.io 定义的包类型
因此,message的类型4后面有个2。
【传输机制设置】:
Socket.io 为我们提供了选项,它的默认情况是以长轮询开始,我们也可以手动设置成只使用 websocket 方式来进行通信。
// 设置成只使用 websocket
const socket = io({
transports: ['websocket']
});
// 重连时,重设选项
// 防止 websocket 可能因为代理、防火墙、浏览器等原因连接失败socket.on('reconnect_attempt', () => {
socket.io.opts.transports = ['polling', 'websocket'];
});
03 核心方法
Socket.io 的核心函数:emit
和 on
socket.emit(eventName[, ...args][, ack]):用来发射(触发)一个事件
-
eventName
(string):事件名 -
args
:要发送的数据 -
ack
(Function):回调函数,一般省略,如需对方接受到信息后立即得到确认时需要用到 - Returns
Socket
socket.emit('ferret', 'tobi', (data) => {
console.log(data); // data will be 'woot'
});
// server:
// io.on('connection', (socket) => {
// socket.on('ferret', (name, fn) => {
// fn('woot');
// });
// });
socket.on(eventName, callback):用来监听一个 emit 发射的事件
-
eventName
(string):监听的事件名 -
callback
(Function):匿名函数,接收对方发来的数据,该匿名函数的第一个参数为接收的数据,若有第二个参数,则为要返回的函数 - Returns
Socket
socket.on('news', (data) => {
console.log(data);
});
// with multiple arguments
socket.on('news', (arg1, arg2, arg3, arg4) => {
// ...
});
// with callback
socket.on('news', (cb) => {
cb(0);
});
Socket.io 提供了三种默认的事件(客户端和服务器都有):
-
connect
:当与对方建立连接后自动触发; -
message
:当收到对方发来的数据后触发; -
disconnect
:当对方关闭链接后触发。
除了 socket.io 自身提供的事件之外,还支持自定义事件,丰富了通信:
// 如:
socket.on(‘new message’, function(data) {});
socket.emit(‘new message’, { message: message });
【服务端广播的三种情况】:
客户端:
const socket = io();
// 监听事件
socket.on(‘message’, (data) => {});
// 触发事件
socket.emit(‘message’, { message });
服务端:
io.on(‘connection’, function(socket) {
socket.on(‘message’, function(data) {
// 1.广播给自己
socket.emit(‘message’, data);
// 2. 广播给除了自己的其它客户端
socket.broadcast.emit(‘message’, data);
// 3. 广播给所有客户端
io.emit(‘message’, data); // 等同于 io.sockets.emit()
});
});
04 Rooms 和命名空间
【作用】:减少TCP连接数的同时区分不同的通信频道(在不同的路由层面能体现该作用,具体请参考 socket.io 中namespace 和 room的概念)、实现私聊
默认的命名空间:io.sockets、io
io.on('connection', function(socket){
socket.on('disconnect', function(){ });
});
自定义命名空间:
// 服务器端
var nsp = io.of('/my-namespace');
nsp.on('connection', function(socket){
socket.on('disconnect', function(){ });
});
// 客户端
var socket = io('/my-namespace');
Rooms:
// 自定义room
io.on('connection', function(socket){
socket.join('some room')); // 加入房间
socket.leave('some room'); // 离开房间
});
// 向房间里的所有客户端发送消息
io.to('some room').emit('some event');
// 默认房间(每一个id一个room)
socket.on('say to someone', function(id, msg){
socket.broadcast.to(id).emit('my message', msg);
});
获取房间信息:socket.adapter.rooms
默认情况下,每一个 id 便自成一个房间,房间名为
socket.id
(指定命名空间之后,前面会带上命名空间);
自定义房间之后,原先的默认房间仍然存在;
房间为一个对象,包含当前进入房间的 sockets 以及长度。
05 打造基础聊天室
从官网最基础的聊天小例子入门,又分析了一下 Demos 中的 Chat demo 源码之后,自己试着用 react 实现了一遍,具体的功能及原理如下:
-
最基础聊天功能
【实现原理】:服务端运用
io.emit
进行广播给每个建立连接的客户端。 -
登录(Chat Demo)
【核心操作】:用户登录时(
login
事件),服务端为当前的客户端存储 username。socket.username = username
-
显示用户进入/离开
【实现原理】:用户登录(
login
)时,触发user joind
事件;用户断开连接(disconnection
)时,触发user left
事件—> 均通过socket.broadcase.emit
广播给其它客户端。 -
显示当前聊天室人数
【实现原理】:操作 numUsers 变量—> 监听
connection
事件:++numUsers;监听disconnection
事件:--numUsers 。 -
显示各自昵称
【实现原理】:触发
chat
事件的时候将当前连接的 username 通过io.emit
广播给每个给客户端。 -
提示对方正在输入
【实现原理】:监听
typing
和stop typing
事件 —> 均通过socket.broadcast.emit
广播给其它客户端。(typing
通过监听输入框的onKeyPress
事件进行触发)注意:当发送完信息之后,需要清空“***正在输入”,客户端在监听到
chat
时可将提示置空。 实现私人聊天
【实现原理】:运用 Room,通过 socket.adapter.rooms
获取当前 room 的信息,包括每个 room 中的 id。
// 加入房间
socket.join('some room');
// 离开房间
socket.leave('some room');
// 向房间里的所有客户端发送消息
io.to('some room').emit('some event');
// 向房间中的除了自己的客户端发送消息
socket.broadcast.to ('some room')
.emit('my message', msg);
自娱自乐的实现版本:
github地址:ioChat
ps:下一次我会出一篇聊天室实现 emoji 表情发送的文章,敬请期待哟~~٩(๑>◡<๑)۶ ~~
§ 参考资料
- socket.io官网
- socketio/chat-example
- React socket.io express 开发一个聊天室
- Vue全家桶+Socket.io+Koa2打造一个智能聊天室
- Express结合Webpack的全栈自动刷新
- 在 Express 开发中使用 nodemon(使用 nodemon 自动重启服务器)
- 使用socket.io打造公共聊天室
- socket.io 的详细工作流程是怎样的?
- socket.io 中namespace 和 room的概念