egg 框架的 socket.io 插件
$ npm i egg-socket.io --save
- Node.js >= 8.0
- Egg.js >= 2.0
1.接口地址
api/transportMessage
2.参数
{
"timestamp": 1543469988762, // 时间截
"app": "signatureApp", // APP名称,这个需要服务端参与签名校验验证合法性
"target": "-y2U2Zzjm5XLHmaGAAAA", // 客户端ID,用于寻址
"userId": "c43d5800-f46c-11e8-baad-0566a042750c", // 用户唯一标识,用于断线重连
"sign": "236e7f552a5cde6002c14c2510df1ec1", // 握手签名
"message": {"msg":"request a client id for others!"} // 消息报文
}
socket.emit('requestMessage', helper.parseMsg('requestId', 'request a client id for communication!', metaData));
// 发送消息格式 helper.parseMsg()封装:
{
"meta":{ // 元数据消息头
"timestamp":1543469988762, // 签名时间截
"app":"signatureApp", // APP名
"sign":"236e7f552a5cde6002c14c2510df1ec1", // 前端签名串
"client":"BG1dzK23yXZBBhJnAAAA", // 客户端ID
"userId":"c43d5800-f46c-11e8-baad-0566a042750c", // 用户唯一标识,用于断线重连
"target":"BG1dzK23yXZBBhJnAAAA" // 目标ID
},
"data":{ // 报文体
"action":"requestId", // 命令类型: 请求分配ID
"payload":"request a client id for communication!" // 描述内容
}
}
socket.emit('requestMessage', helper.parseMsg('sendToOthers', 'request a client id for others!', metaData));
// 发送消息格式 helper.parseMsg()封装:
{
"meta":{ // 元数据消息头
"timestamp":1543469988762, // 签名时间截
"app":"signatureApp", // APP名
"sign":"236e7f552a5cde6002c14c2510df1ec1", // 前端签名串
"client":"BG1dzK23yXZBBhJnAAAA", // 客户端ID
"userId":"c43d5800-f46c-11e8-baad-0566a042750c", // 客户端ID
"target":"ylAG54uMXduHs_tMAAAA" // 目标ID
},
"data":{ // 报文体
"action":"sendToOthers", // 命令类型: 请求分配ID
"payload":"request a client id for others!" // 描述内容
}
}
// md5(`${appName}${key}${timestamp}`)
// key由管理员统一分配,然后存储于{app_root}/config/config.default.js
exports.socketConfig = {
// app名称1
signatureApp : {
key: 'XXXXXXXXXXXXXXXXXXXXX'
},
// app名称2
centralServiceApp : {
key: 'XXXXXXXXXXXXXXXXXXXXX'
},
};
/**
* 封装数据报文 broadcast
*
* @param {*} action
* @param {*} [payload={}]
* @param {*} [metadata={}]
* @returns
*/
parseBroadcastMsg(payload = {}, metadata = {});
/**
* 封装数据报文 deny
*
* @param {*} action
* @param {*} [payload={}]
* @param {*} [metadata={}]
* @returns
*/
parseDenyMsg(payload = {}, metadata = {});
/**
* 封装数据报文 exchange
*
* @param {*} action
* @param {*} [payload={}]
* @param {*} [metadata={}]
* @returns
*/
parseExchangeMsg(payload = {}, metadata = {});
/**
* 封装数据报文
*
* @param {*} action
* @param {*} [payload={}]
* @param {*} [metadata={}]
* @returns
*/
parseMsg(action, payload = {}, metadata = {});
// 统一返回格式:
// {
// data: {
// action: 'exchange', // 'deny' || 'exchange' || 'broadcast'
// payload: {},
// },
// meta:{
// timestamp: 1512116201597,
// client: 'nNx88r1c5WuHf9XuAAAB',
// userId: 'c43d5800-f46c-11e8-baad-0566a042750c',
// target: 'nNx88r1c5WuHf9XuAAAB'
// },
// }
名称 | 描述 |
---|---|
SAVE_ID | 保存存用户ID |
REQUEST_ID | 请求用户ID |
SEND_TO_OTHERS | 发送消息给其它用户 |
RE_CONNECT_AND_UPDATES | 断线重连更新用户ID |
RE_CONNECT_UPDATED | 断线重连更新成功 |
DENY | 拒绝连接 |
EXCHANGE | 交换信息 |
BROADCAST | 广播 |
CONNECT | 连接成功 |
DISCONNECT | 断开连接 |
// 发送给其它客户端
socket.emit(SocketEngine.REQUEST_MESSAGE, helper.parseMsg(SocketEnum.SEND_TO_OTHERS, 'request a client id for others!', metaData));
// 接受重连后更新客户端消息
socket.emit(SocketEngine.REQUEST_MESSAGE, helper.parseMsg(SocketEnum.RE_CONNECT_AND_UPDATES, 'reconnect socket!', metaData));
通过 config/plugin.js
配置启动 Socket.IO 插件:
// {app_root}/config/plugin.js
exports.io = {
enable: true,
package: 'egg-socket.io',
};
在 config/config.${env}.js
配置 Socket.IO :
exports.io = {
init: { } // passed to engine.io
namespace: {
'/': {
connectionMiddleware: [],
packetMiddleware: [],
},
},
redis: {
host: '127.0.0.1',
port: 6379
}
};
Egg Socket 内部默认使用 ws
引擎,uws 因为某些原因被废止。
如坚持需要使用,请按照以下配置即可:
exports.io = {
init: { wsEngine: 'uws' },
};
- 有关更多
init
选项配置,请参考:engine.io 。 - 有关更多
Egg Socket
相关默认配置,请参考:config.default.js。
注意: 此函数作为接口预留,便于你按照自己的规则为每一个 socket 生成唯一的 ID:
exports.io = {
generateId: (request) => {
// Something like UUID.
return 'This should be a random unique ID';
}
};
由于 Socket.IO 的设计缘故,多进程的 Socket.IO 服务必须在 sticky
模式下才能工作,否则会抛出握手异常。
所以,必须开启 sticky
模式:
$ # 修改 package.json 对应的 npm scripts
$ egg-bin dev --sticky
$ egg-scripts start --sticky
这两个脚本都会使框架启动 cluster 时使用 sticky
模式
startCluster({
sticky: true,
...
});
如果你使用了 nginx 做代理转发:
location / {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:{ your node server port };
}
app
├── io
│ ├── controller
│ │ └── chat.js
│ └── middleware
│ ├── auth.js
│ ├── filter.js
├── router.js
config
├── config.default.js
└── plugin.js
middleware are functions which every connection or packet will be processed by.
- 编写连接中间件
app/io/middleware/auth.js
module.exports = app => {
return async (ctx, next) => {
ctx.socket.emit('res', 'connected!');
await next();
// execute when disconnect.
console.log('disconnection!');
};
};
- 配置使之生效
config/config.default.js
exports.io = {
namespace: {
'/': {
connectionMiddleware: ['auth'],
},
},
};
注意,必须配置在特定的命名空间下,才会生效
- 编写包中间件
app/io/middleware/filter.js
module.exports = app => {
return async (ctx, next) => {
ctx.socket.emit('res', 'packet received!');
console.log('packet:', this.packet);
await next();
};
};
- 配置使之生效
config/config.default.js
exports.io = {
namespace: {
'/': {
packetMiddleware: ['filter'],
},
},
};
注意,必须配置在特定的命名空间下,才会生效
controller is designed to handle the emit
event from the client.
example:
app/io/controller/chat.js
module.exports = app => {
class Controller extends app.Controller {
async ping() {
const message = this.ctx.args[0];
await this.ctx.socket.emit('res', `Hi! I've got your message: ${message}`);
}
}
return Controller
};
// or async functions
exports.ping = async function() {
const message = this.args[0];
await this.socket.emit('res', `Hi! I've got your message: ${message}`);
};
下一步,在 app/router.js
配置路由
module.exports = app => {
// or app.io.of('/')
app.io.route('chat', app.io.controller.chat.ping);
};
路由主要负责对特定的 socket 连接不同的事件进行分发处理到对应的控制器。路由配置写在 app/router.js
中,实例参照上一节。
除了,业务逻辑的路由之外,还有配置几个系统内置事件:
disconnecting
正在断开连接disconnect
连接已经断开error
发生了错误
示例:
app/router.js
app.io.route('disconnect', app.io.controller.chat.disconnect);
app/io/controller/chat.js
module.exports = (app) => {
class Controller extends app.Controller {
async disconnect() {
const message = this.ctx.args[0];
console.log(message);
}
}
return Controller
};
egg-socket.io
支持会话,与普通的 HTTP 会话几乎一样。
会话的创建和鉴权只发生在建立连接阶段。在包中间件和控制器中可以获取通过 ctx.session
获取会话对象,但是它只在连接创建。
会话功能由 egg-session 实现,请确保它已经开启。
如果你的 Socket.IO 服务由多台服务器提供,那么必须思考集群方案。比如,广播,房间等功能,必须依赖集群方案。
egg-socket.io 集成了 socket.io-redis ,能够非常方便的实现集群共享资源与事件分发。
配置起来也很简单:
只需要在 config/config.${env}.js
配置 :
exports.io = {
redis: {
host: { redis server host },
port: { redis server prot },
auth_pass: { redis server password },
db: 0,
}
};
egg 服务在启动时,会尝试连接 redis 服务,成功后,应用会顺利启动。
微信小程序所采用的 WebSocket 协议版本为 13 (可抓包查看),而 socket.io 支持的协议版本为 4 详见 socket.io-protocol
解决方案是可以封装一个适配小程序端的 socket.io.js,Github 上也有现成的库可以参考 wxapp-socket-io 示例代码如下:
// 小程序端示例代码
import io from 'vendor/wxapp-socket-io.js';
const socket = io('ws://127.0.0.1:7001');
socket.on('connect', function () {
socket.emit('chat', 'hello world!');
});
socket.on('res', msg => {
console.log('res from server: %s!', msg);
});
请访问 here.