Aplicaciones de mensajería instantánea, que incluyen servidor, administración y cliente
Se ha implementado y lanzado, bienvenido a experimentar el lado del cliente y la administración.
No cambie el papel predeterminado y los permisos a voluntad. Por favor, siéntete un poco amoroso y no inventes algunos nombres incivilizados.
Usando el marco de huevos, el lado del servidor del servicio IM
Desde el desarrollo de Internet móvil, los servicios de mensajería instantánea dirigidos por WeChat se han integrado en cada rincón de nuestras vidas y también juegan un papel importante en algunos de los negocios de la compañía. Nuestra empresa utilizó servicios de mensajería instantánea, pero no se pueden lograr muchas necesidades personalizadas. Por lo tanto, se decidió desarrollar un microservicio de mensajería instantánea que satisfaga las necesidades personalizadas internamente.
El marco socket.io se usó porque el backend no tenía personas en ese momento. Después de leer algunos ejemplos, pensé que era realmente conveniente usar y apoyado en toda la plataforma. Por lo tanto, este microservicio se implementó en el equipo frontal, y el efecto es actualmente bastante bueno.
La comunidad actualmente tiene contenido menos o demasiado simple en esta área (solo hay una sala de chat pública). Además, es muy incómodo ser PM durante el proceso de desarrollo empresarial, por lo que quiero separarme de algunas cosas comerciales únicas y darme cuenta de una aplicación IM simple y completa que no mezcla negocios de la compañía con funciones simples, incluidos el servidor, la administración y el cliente. El objeto de imitación del cliente es WeChat, porque estoy muy familiarizado con él y no tengo que pensar demasiado sobre el producto. Además, las personas que lo prueban están muy familiarizadas con él y no requieren demasiados costos de comunicación.
Para desarrollar un conjunto completo de servicios de mensajería instantánea, se requieren las siguientes partes:
Nacido para marcos y aplicaciones de nivel empresarial
La razón por la que elegí el marco de huevo de Alibaba como soporte es que han hecho un buen trabajo en la implementación y seguridad a gran escala dentro de él. La razón por la que no eligieron Nest es que es problemático integrar socket.io . ORM usa secuelas, y la base de datos es MySQL. Lo he usado antes, por lo que es menos difícil comenzar.
Solución frontal/de diseño listo para usar
La razón para elegir Ant Design Pro es que estoy familiarizado con Vue Family Bucket. Quiero aprovechar esta oportunidad para familiarizarme con el proceso de desarrollo de todo el ecosistema React y sentir las diferencias esenciales y los diferentes caminos de los dos principales marcos de desarrollo en China. Ant Design Pro ha sido lanzado durante varios años, y de hecho ha traído mejoras de eficiencia a las pequeñas y medianas empresas, lo cual es adecuado para mis necesidades.
Herramientas estándar desarrolladas por Vue.js
Use @Vue/CLI para construir un cliente de servicio IM, un proyecto móvil H5 y el marco de la interfaz de usuario usa Yozan Vant, que integra mi componente de código abierto Vue-Page-Page-Page y el mejor desplazamiento del Sr. Huang para realizar las funciones básicas de IM
Como ingeniero front-end, la mayoría de las tareas diarias no requieren pensar en las relaciones de entidad. Sin embargo, según mi experiencia real, comprender las relaciones de las entidades puede ayudarnos a comprender mejor los modelos de negocio. La mejora de la comprensión del producto y el negocio es de gran ayuda para nosotros. Podemos encontrar muchos aspectos ilógicos durante la revisión de la demanda (¿por qué tenemos que quejarnos nuevamente por el gerente de producto)? Si podemos proponerlo en este momento, tomaremos la iniciativa para evitar el desarrollo repetido en el proceso posterior, y al mismo tiempo, podemos formar una interacción relativamente buena con los estudiantes en el lado del producto (en lugar de confrontación). Estas son algunas de las relaciones de entidad más importantes:
De la figura anterior, podemos ver que el usuario es el núcleo de todo el diagrama de relaciones. La siguiente es la relación entre varias entidades:
La siguiente es una introducción detallada a las sesiones, roles y permisos:
Al completar una solicitud de mensajería instantánea, lo primero que debe considerar es la conversación, que es la ventana de conversación en nuestro WeChat. Se necesita mucho esfuerzo para pensar en la relación entre conversaciones y mensajes, usuarios y grupos, y finalmente formar la siguiente relación básica:
En otras palabras, el usuario no tiene una relación directa con la sesión y solo puede obtener la sesión a través del chat único y el chat grupal correspondiente del usuario. Esto puede tener los siguientes beneficios:
Para diseñar un sistema de gestión de permisos flexible, universal y conveniente, este sistema adopta el control RBAC (control de acceso basado en roles) para diseñar una plataforma general de "permisos de roles de usuario" para la conveniencia de la expansión posterior.
RBAC (control de acceso basado en roles) se refiere al usuario asociado con los permisos a través de roles. Es decir, un usuario tiene varios roles, y cada rol tiene varios permisos (por supuesto, no combinen roles y permisos conflictivos juntos). De esta manera, se construye un modelo de autorización de "permisiones de rol de usuario". En este modelo, generalmente hay una relación de muchos a muchos usuarios y roles y entre roles y permisos.
Este sistema tiene los roles predeterminados del administrador, el usuario general, el usuario prohibido y el usuario prohibido, y se asignan diferentes permisos a diferentes roles. Por lo tanto, es necesario realizar el procesamiento de autenticación unificada (a través del middleware) para el enrutamiento de la interfaz, como la administración y el habla. Los métodos y métodos específicos se explicarán en detalle en el proyecto de fondo. Este sistema utiliza temporalmente el método de roles y permisos predefinidos. Si desea expandirse en el futuro, puede editar roles y permisos.
Nunca he visto el lado de gestión de WeChat, pero puede imaginar que los administradores pueden configurar los roles y permisos de los usuarios y editar el estado del grupo:
Después de registrarse e iniciar sesión, puede agregar amigos y unirse a grupos normalmente, modificar la información básica personal y las aplicaciones de procesos.
No se puede iniciar sesión
Por ejemplo: ahora hay una nueva versión del centro personal que debe probarse en línea. Primero, cree un nuevo rol "Centro personal de prueba" y luego asigne los permisos correspondientes al rol; Luego agrupe a los usuarios comunes y seleccione a algunas personas para configurar este rol, para que pueda probarlo.
Hablemos sobre el principio de comunicación central de los servicios de mensajería instantánea. Al igual que los servicios generales de HTTP, hay un servidor y un cliente para la comunicación, pero los métodos detallados de protocolo y procesamiento son diferentes.
Debido a razones históricas, el protocolo HTTP convencional ahora es un protocolo sin estado (HTTP2 no se usa ampliamente por el momento). En general, el cliente inicia la solicitud activamente y luego responde a ella. Entonces, para darse cuenta de que el servidor empuja la información al cliente, el front-end necesita encuestar activamente el back-end. Este método es ineficiente y propenso a errores. De hecho, esto se hace en nuestra página de inicio de gestión antes (5s una vez).
Para lograr esta necesidad de que el lado del servidor impulse activamente la información, HTML5 comenzó a proporcionar un protocolo para la comunicación duplex en una sola conexión TCP, a saber, WebSocket. WebSocket facilita el intercambio de datos entre clientes y servidores, lo que permite que el servidor presione activamente los datos a los clientes. El Protocolo WebSocket nació en 2008 y se convirtió en un estándar internacional en 2011. Actualmente, la mayoría de los navegadores ya lo han apoyado.
El uso de WebSocket es bastante simple:
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.");
};
Con el protocolo WebSocket, el servidor tiene armas avanzadas para impulsar activamente información. Entonces, ¿hay alguna forma de ser compatible con los navegadores antiguos y nuevos? De hecho, muchas personas piensan en esto, y la respuesta es socket.io
socket.io socket.io encapsula aún más la interfaz WebSocket y puede cambiar automáticamente al uso de encuestas en navegadores antiguos para la comunicación (nuestros usuarios no percibirán), formando un conjunto unificado de interfaces, reduciendo en gran medida la carga del desarrollo. Principalmente tiene las siguientes ventajas:
Esta es la página de inicio de Socket.io
El motor de mensajería instantánea más rápido y confiable (con el motor en tiempo real más rápido y confiable)
Es realmente fácil de usar:
var io = require('socket.io')(80);
var cfg = require('./config.json');
var tw = require('node-tweet-stream')(cfg);
tw.track('socket.io');
tw.track('javascript');
tw.on('tweet', function(tweet){
io.emit('tweet', tweet);
});
Centrémonos en varios puntos importantes en el proyecto del lado del servidor. La mayor parte del contenido debe verse en el sitio web oficial de Egg.
Use andamios npm init egg --type=simple para inicializar el proyecto del servidor, instale mysql (mi versión 8.0), configure la contraseña del enlace de la base de datos requerida para la secuela, etc., y puede iniciarlo
// 目录结构说明
├── package.json // 项目信息
├── app.js // 启动文件,其中有一些钩子函数
├── app
| ├── router.js // 路由
│ ├── controller
│ ├── service
│ ├── middleware // 中间件
│ ├── model // 实体模型
│ └── io // socket.io 相关
│ ├── controller
│ └── middleware // io独有的中间件
├── config // 配置文件
| ├── plugin.js // 插件配置文件
| └── config.default.js // 默认的配置文件
├── logs // server运行期间产生的log文件
└── public // 静态文件和上传文件目录
El enrutador se usa principalmente para describir la correspondencia entre la URL de solicitud y el controlador que realiza específicamente la acción de ejecución, es decir, app/router
app/middleware/auth.jsapp/middleware/admin.js Debido a que este sistema tiene diferentes roles como administradores y usuarios de comunicación general, se requiere un procesamiento de autenticación unificado para el enrutamiento de la interfaz de la gestión y la comunicación.
Por ejemplo, la ruta del lado de gestión /v1/admin/... , quiero agregar autenticación del administrador a todas las rutas de esta serie. En este momento, puede usar el middleware para autenticarse. El siguiente es un ejemplo específico de usar el middleware en el enrutador de administración.
// middware
module.exports = () => {
return async function admin(ctx, next) {
let { session } = ctx;
// 判断admin权限
if (session.user && session.user.rights.some(right => right.keyName === 'admin')) {
await next();
} else {
ctx.redirect('/login');
}
};
};
// router
const admin = app.middleware.admin();
router.get('/api/v1/admin/rights', admin, controller.v1.admin.rightsIndex);
La combinación Secelize+MySQL utilizada, y Egg también tiene complementos relacionados con la secuela. SECLELIZE es un ORM utilizado en el entorno de nodo, admitiendo Postgres, MySQL, Mariadb, SQLite y Microsoft SQL Server, que es bastante conveniente de usar. Primero debe definir la relación directa entre el modelo y el modelo. Después de establecer la relación, puede usar algunos métodos preestablecidos.
La información básica del modelo es más fácil de procesar. Lo que debe prestarse la atención es el diseño de la relación entre las entidades, es decir, asociado. La siguiente es la descripción de la relación del usuario
// User.js
module.exports = app => {
const { STRING } = app.Sequelize;
const User = app.model.define('user', {
provider: {
type: STRING
},
username: {
type: STRING,
unique: 'username'
},
password: {
type: STRING
}
});
User.associate = function() {
// One-To-One associations
app.model.User.hasOne(app.model.UserInfo);
// One-To-Many associations
app.model.User.hasMany(app.model.Apply);
// Many-To-Many associations
app.model.User.belongsToMany(app.model.Group, { through: 'user_group' });
app.model.User.belongsToMany(app.model.Role, { through: 'user_role' });
};
return User;
};
Por ejemplo, la relación entre User y UserInfo es una relación uno a uno. Después de definirlo, podemos usar user.setUserInfo(userInfo) al crear un nuevo usuario. Cuando desee obtener la información básica de este usuario, también puede usar user.getUserInfo()
La relación entre el usuario y la aplicación es de uno a muchos, es decir, un usuario puede corresponder a múltiples aplicaciones, y actualmente solo se aplican amigos y aplicaciones grupales:
Al agregar una aplicación, puede usar user.addApply(apply) , y al obtenerla, puede usarla de la siguiente manera:
const result = await ctx.model.Apply.findAndCountAll({
where: {
userId: ctx.session.user.id,
hasHandled: false
}
});
La relación entre el usuario y el grupo es de muchos a muchos, es decir, un usuario puede corresponder a múltiples grupos, y un grupo puede corresponder a múltiples usuarios. De esta manera, Sequelize establecerá una tabla intermedia user_group para lograr esta relación.
Normalmente uso esto:
group.addUser(user); // 建立群组和用户的关系
user.getGroups(); // 获取用户的群组信息
El huevo proporciona el complemento de huevo. Debe abrir el complemento en config/plugin.js después de instalar huevo-socket.io. IO tiene su propio middleware y controlador.
La ruta de IO es diferente de la de las solicitudes HTTP generales. Tenga en cuenta que la ruta aquí no se puede procesar con el middleware (no tuve éxito), por lo que manejé el procesamiento de prohibición en el controlador.
// 加入群
io.of('/').route('/v1/im/join', app.io.controller.im.join);
// 发送消息
io.of('/').route('/v1/im/new-message', app.io.controller.im.newMessage);
// 查询消息
io.of('/').route('/v1/im/get-messages', app.io.controller.im.getMessages);
Nota: Considero que la relación grupo y amigo es una sala (es decir, una sesión), para que pueda enviar mensajes directamente al Romm, y todos los que están dentro pueden recibirlos.
Hay dos middleware predeterminados, uno es el middleware de conexión llamado al conectarse y desconectarse, que se utiliza para verificar el estado de inicio de sesión y la lógica comercial del proceso; El otro es el middleware de paquete llamado cada vez que se envía un mensaje, que se utiliza para imprimir el registro
Dado que el permiso de la llamada está preestablecido, se procesa en el controlador
// 对用户发言的权限进行判断
if (!ctx.session.user.rights.some(right => right.keyName === 'speak')) {
return;
}
Los chats se dividen en chats individuales y chats grupales. La información del chat incluye temporalmente texto general, imágenes, videos y mensajes de posicionamiento, que se pueden ampliar en pedidos o productos de acuerdo con el negocio.
El diseño de la estructura del mensaje se refiere al diseño de varios servicios de terceros, y también se ha ajustado en combinación con la situación de este proyecto en sí. Se puede ampliar a voluntad, y se hace la siguiente explicación:
const Message = app.model.define('message', {
/**
* 消息类型:
* 0:单聊
* 1:群聊
*/
type: {
type: STRING
},
// 消息体
body: {
type: JSON
},
fromId: { type: INTEGER },
toId: { type: INTEGER }
});
El cuerpo almacena el cuerpo del mensaje, que se utiliza para almacenar diferentes formatos de mensaje usando JSON:
// 文本消息
{
"type": "txt",
"msg":"哈哈哈" //消息内容
}
// 图片消息
{
"type": "img",
"url": "http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e",
"ext":"jpg",
"w":360, //宽
"h":480, //高
"size": 388245
}
// 视频消息
{
"type": 'video',
"url": "http://nimtest.nos.netease.com/cbc500e8-e19c-4b0f-834b-c32d4dc1075e",
"ext":"mp4",
"w":360, //宽
"h":480, //高
"size": 388245
}
// 地理位置消息
{
"type": "loc",
"title":"中国 浙江省 杭州市 网商路 599号", //地理位置title
"lng":120.1908686708565, // 经度
"lat":30.18704515647036 // 纬度
}
Solo hay uno en la actualidad, que es actualizar el token de Baidu. Aquí es bastante simple, solo consulte la documentación oficial.
Unidad de plataforma de personalización y servicio de diálogo inteligente
Esto es bastante interesante. Puede crear un nuevo robot y agregar habilidades correspondientes en https://ai.baidu.com/ . Estoy charlando aquí, y hay preguntas y respuestas inteligentes, etc.
Si no desea comenzar, puede eliminar ctx.service.baidu.getToken();
En primer lugar, debe configurarlo en el archivo de configuración. He limitado el tamaño del archivo aquí y formato de archivo de video iOS de sitio cruzado:
config.multipart = {
mode: 'file',
fileSize: '3mb',
fileExtensions: ['.mov']
};
Se utiliza una interfaz unificada para manejar las cargas de archivos. El problema principal es la escritura de archivos, y los archivos son la lista de archivos transmitida desde el extremo frontal
for (const file of ctx.request.files) {
// 生成文件路径,注意upload文件路径需要存在
const filePath = `./public/upload/${
Date.now() + Math.floor(Math.random() * 100000).toString() + '.' + file.filename.split('.').pop()
}`;
const reader = fs.createReadStream(file.filepath); // 创建可读流
const upStream = fs.createWriteStream(filePath); // 创建可写流
reader.pipe(upStream); // 可读流通过管道写入可写流
data.push({
url: filePath.slice(1)
});
}
Almacené /public/upload/ en el directorio del servidor. Este directorio requiere configuración de archivo estático:
config.static = {
prefix: '/public/',
dir: path.join(appInfo.baseDir, 'public')
};
El documento oficial del huevo de este capítulo es matarlo, no hay nada de ejemplos, debe leer el código fuente. Es tan terrible. Lo he estudiado durante mucho tiempo antes de descubrir qué está pasando.
Debido a que quiero controlar la contraseña de la cuenta inicio de sesión más libremente, no uso el pasaporte para el inicio de sesión de la contraseña de la cuenta, pero uso la autenticación y la sesión de la interfaz ordinaria.
La siguiente es una descripción detallada del proceso de iniciar sesión utilizando una plataforma de terceros (elegí GitHub):
Abra el complemento:
// config/plugin.js
module.exports.passport = {
enable: true,
package: 'egg-passport',
};
module.exports.passportGithub = {
enable: true,
package: 'egg-passport-github',
};
// config.default.js
config.passportGithub = {
key: 'your_clientID',
secret: 'your_clientSecret',
callbackURL: 'http://localhost:3000/api/v1/passport/github/callback' // 注意这里非常的关键,这里需要和你在github上面设置的Authorization callback URL一致
};
this.app.passport.verify(verify);
const github = app.passport.authenticate('github', { successRedirect: '/' }); // successRedirect就是最后校验完毕后前端会跳转的路由,我这里直接跳转到主页了
router.get('/v1/passport/github', github);
router.get('/v1/passport/github/callback', github);
/v1/passport/github en la parte delantera e inicie la autorización de GitHub para esta aplicación. Después del éxito, GitHub irá a http://localhost:3000/v1/passport/github/callback?code=12313123123 . Nuestro complemento GitHubPassport obtendrá la información del usuario en GitHub. Después de obtener la información detallada, necesitamos verificar la información del usuario en app/passport/verify.js , y asociarla con la información del usuario de nuestra propia plataforma, y también asignar un valor a la sesión // verify.js
module.exports = async (ctx, githubUser) => {
const { service } = ctx;
const { provider, name, photo, displayName } = githubUser;
ctx.logger.info('githubUser', { provider, name, photo, displayName });
let user = await ctx.model.User.findOne({
where: {
username: name
}
});
if (!user) {
user = await ctx.model.User.create({
provider,
username: name
});
const userInfo = await ctx.model.UserInfo.create({
nickname: displayName,
photo
});
const role = await ctx.model.Role.findOne({
where: {
keyName: 'user'
}
});
user.setUserInfo(userInfo);
user.addRole(role);
await user.save();
}
const { rights, roles } = await service.user.getUserAttribute(user.id);
// 权限判断
if (!rights.some(item => item.keyName === 'login')) {
ctx.body = {
statusCode: '1',
errorMessage: '不具备登录权限'
};
return;
}
ctx.session.user = {
id: user.id,
roles,
rights
};
return githubUser;
};
Presta atención al código anterior. Si es la primera autorización, se creará el usuario. Si es la segunda autorización, se ha creado el usuario.
Cuando el sistema se implementa o se ejecuta, algunos datos y tablas deben ser preestablecidos, y los códigos están en app.js y app/service/startup.js
La lógica es que después de que se inicia el proyecto, use el modelo para sincronizar la estructura de la tabla en la base de datos y luego comience a crear algunos datos básicos:
Después de completar lo anterior, los datos iniciales se han completado y pueden funcionar normalmente
Compré el servidor CentOS en Tencent Cloud, el nombre de dominio que compré en Alibaba Cloud, instaló el nodo (12.18.2), Nginx y MySQL8.0, y comencé directamente en CentOS. El front-end usa NGINX para proxy inverso. Debido a los recursos limitados del servidor, no hay herramientas de automatización Jenkins y Docker, lo que conduce a algunas operaciones manuales al actualizar.