Applications de messagerie instantanée, y compris le serveur, la gestion et le client
Il a été déployé et lancé, bienvenue pour découvrir le client et la gestion
Veuillez ne pas modifier le rôle par défaut et les autorisations à volonté. Sentez-vous un peu aimant et ne composez pas des noms non civilisés.
À l'aide du framework d'oeuf, le côté serveur du service IM
Depuis le développement d'Internet mobile, les services de messagerie instantanée dirigés par WeChat ont été intégrés dans tous les coins de nos vies et jouent également un rôle important dans certaines entreprises de l'entreprise. Notre entreprise a utilisé des services de messagerie instantanée, mais de nombreux besoins personnalisés ne peuvent pas être réalisés. Par conséquent, il a été décidé de développer un microservice de messagerie instantané qui répond aux besoins personnalisés en interne.
Le cadre socket.io a été utilisé parce que le backend était à court de personnes à ce moment-là. Après avoir lu quelques exemples, j'ai pensé que c'était vraiment pratique à utiliser et à prendre en charge sur toute la plate-forme. Par conséquent, ce microservice a été mis en œuvre dans l'équipe frontale, et l'effet est actuellement assez bon.
La communauté a actuellement un contenu moins ou trop simple dans ce domaine (il n'y a qu'une seule salle de discussion publique). De plus, il est très inconfortable d'être PM pendant le processus de développement commercial, donc je veux rompre avec des affaires uniques et réaliser une application de messagerie instantanée simple et complète qui ne mélange pas l'entreprise avec les fonctions simples, y compris le serveur, la gestion et le client. L'imitation du client est WeChat, car je le connais très bien et je n'ai pas à réfléchir trop au produit. De plus, les personnes qui l'essaient sont très familières et ne nécessitent pas trop de coûts de communication.
Pour développer un ensemble complet de services de messagerie instantanée, les parties suivantes sont nécessaires:
Né pour les frameworks et applications au niveau de l'entreprise
La raison pour laquelle j'ai choisi le cadre d'oeuf d'Alibaba comme soutien est qu'ils ont fait du bon travail en matière de mise en œuvre et de sécurité à grande échelle. La raison pour laquelle ils n'ont pas choisi Nest est qu'il est difficile d'intégrer socket.io . ORM utilise Sequelize et la base de données est MySQL. Je l'ai déjà utilisé, il est donc moins difficile de commencer.
Solution frontale / conception prête à l'emploi
La raison du choix de Ant Design Pro est que je connais le seau Vue Family. Je veux profiter de cette occasion pour me familiariser avec le processus de développement de l'ensemble de l'écosystème React et ressentir les différences essentielles et les différents chemins des deux principaux cadres de développement en Chine. Ant Design Pro est libéré depuis plusieurs années, et il a en effet apporté des améliorations d'efficacité aux petites et moyennes entreprises, ce qui convient à mes besoins.
Outils standard développés par Vue.js
Utilisez @ vue / CLI pour créer un client de service IM, un projet H5 mobile, et le framework d'interface utilisateur utilise Youzan Vant, qui intègre mon composant open source Vue-Page-Stack et M. Huang Better-Broll pour réaliser les fonctions de base de IM
En tant qu'ingénieur frontal, la plupart des tâches quotidiennes ne nécessitent pas de réflexion sur les relations entités. Cependant, en fonction de mon expérience réelle, la compréhension des relations d'entité peut nous aider à mieux comprendre les modèles commerciaux. L'amélioration de la compréhension des produits et des entreprises nous aide. Nous pouvons trouver de nombreux aspects illogiques lors de l'examen de la demande (pourquoi devons-nous nous plaindre à nouveau du chef de produit). Si nous pouvons le proposer pour le moment, nous prendrons l'initiative d'éviter le développement répété dans le processus ultérieur, et en même temps, nous pouvons former une interaction relativement bonne avec les étudiants du côté du produit (plutôt que de la confrontation). Voici quelques-unes des relations d'entité les plus importantes:
D'après la figure ci-dessus, nous pouvons voir que l'utilisateur est le cœur de l'ensemble du diagramme de relation. Voici la relation entre les différentes entités:
Ce qui suit est une introduction détaillée aux séances, rôles et autorisations:
Lorsque vous remplissez une application de messagerie instantanée, la première chose que vous devez considérer est la conversation, qui est la fenêtre de conversation de notre WeChat. Il faut beaucoup d'efforts pour réfléchir à la relation entre les conversations et les messages, les utilisateurs et les groupes, et enfin former la relation de base suivante:
En d'autres termes, l'utilisateur n'a pas de relation directe avec la session et ne peut obtenir la session que via le chat unique et le chat de groupe correspondant de l'utilisateur. Cela peut avoir les avantages suivants:
Afin de concevoir un système de gestion des autorisations flexible, universel et pratique, ce système adopte le contrôle RBAC (Contrôle d'accès basé sur les rôles) pour concevoir une plate-forme générale de "permis de rôle" pour la commodité de l'expansion ultérieure.
RBAC (Contrôle d'accès basé sur les rôles) fait référence à l'utilisateur associé aux autorisations à travers des rôles. Autrement dit, un utilisateur a plusieurs rôles, et chaque rôle a plusieurs autorisations (bien sûr, ne combinez pas les rôles et les autorisations contradictoires). De cette façon, un modèle d'autorisation de "utilisateur-role-permissions" est construit. Dans ce modèle, il existe généralement une relation plusieurs à plusieurs entre les utilisateurs et les rôles et entre les rôles et les autorisations.
Ce système a les rôles par défaut de l'administrateur, de l'utilisateur général, de l'utilisateur interdit et de l'utilisateur interdit, et différentes autorisations sont attribuées à différents rôles. Par conséquent, il est nécessaire d'effectuer un traitement d'authentification unifiée (via middleware) pour le routage d'interface tel que la gestion et la parole. Les méthodes et méthodes spécifiques seront expliquées en détail dans le projet back-end. Ce système utilise temporairement la méthode de rôles et d'autorisations prédéfinis. Si vous souhaitez vous développer à l'avenir, vous pouvez modifier des rôles et des autorisations.
Je n'ai jamais vu le côté gestion de WeChat, mais vous pouvez imaginer que les administrateurs peuvent configurer les rôles et autorisations des utilisateurs et modifier le statut du groupe:
Après vous être inscrit et enregistré, vous pouvez ajouter des amis et rejoindre les groupes normalement, modifier les informations de base personnelles et les applications de traitement.
Impossible de se connecter
Par exemple: il existe maintenant une nouvelle version du centre personnel qui doit être testée en ligne. Tout d'abord, créez un nouveau rôle "Test Personal Center", puis attribuez des autorisations correspondantes au rôle; Ensuite, regroupez les utilisateurs ordinaires et sélectionnez certaines personnes pour configurer ce rôle, afin que vous puissiez le tester.
Parlons du principe de communication de base des services de messagerie instantanée. Comme les services HTTP généraux, il existe un serveur et un client pour la communication, mais le protocole détaillé et les méthodes de traitement sont différents.
Pour des raisons historiques, le protocole HTTP traditionnel est désormais un protocole sans état (HTTP2 n'est pas largement utilisé pour le moment). Généralement, le client initie activement la demande puis y répond. Ainsi, afin de réaliser que le serveur pousse les informations au client, le front-end doit interroger activement le back-end. Cette méthode est inefficace et sujette aux erreurs. Cela se fait en effet sur notre page d'accueil de gestion avant (5S une fois).
Afin d'atteindre ce besoin de côté serveur pour pousser activement des informations, HTML5 a commencé à fournir un protocole pour la communication duplex complète sur une seule connexion TCP, à savoir WebSocket. WebSocket facilite l'échange de données entre les clients et les serveurs, permettant au serveur de pousser activement les données vers les clients. Le protocole WebSocket est né en 2008 et est devenu une norme internationale en 2011. Actuellement, la plupart des navigateurs l'ont déjà soutenu.
L'utilisation de WebSocket est assez 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.");
};
Avec le protocole WebSocket, le serveur a des armes avancées pour pousser activement des informations. Y a-t-il donc un moyen d'être compatible avec les anciens et nouveaux navigateurs? En fait, beaucoup de gens y pensent, et la réponse est socket.io
socket.io socket.io résume en outre l'interface WebSocket et peut automatiquement passer à l'utilisation du sondage dans les anciens navigateurs pour la communication (nos utilisateurs ne percevront pas), formant un ensemble unifié d'interfaces, réduisant considérablement le fardeau du développement. Il présente principalement les avantages suivants:
Ceci est la page d'accueil Socket.io
Le moteur de messagerie instantané le plus rapide et le plus fiable (avec le moteur en temps réel le plus rapide et le plus fiable)
C'est vraiment facile à utiliser:
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);
});
Concentrons-nous sur plusieurs points importants du projet côté serveur. La plupart du contenu doit être consulté sur le site officiel des Egg.
Utilisez l'échafaudage npm init egg --type=simple pour initialiser le projet serveur, installer mysql (ma version 8.0), configurer le mot de passe de liaison de la base de données requis pour séquelles, etc., et vous pouvez le démarrer
// 目录结构说明
├── 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 // 静态文件和上传文件目录
Le routeur est principalement utilisé pour décrire la correspondance entre l'URL de demande et le contrôleur qui entreprend spécifiquement l'action d'exécution, c'est-à-dire app/router
app/middleware/auth.jsapp/middleware/admin.js Étant donné que ce système a des rôles différents en tant qu'administrateurs et utilisateurs généraux de communication, un traitement d'authentification unifié est requis pour le routage d'interface de la gestion et de la communication.
Par exemple, la direction de gestion /v1/admin/... , je souhaite ajouter une authentification administratrice à tous les itinéraires de cette série. Pour le moment, vous pouvez utiliser Middleware pour vous authentifier. Ce qui suit est un exemple spécifique de l'utilisation du middleware dans le routeur d'administration.
// 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 combinaison séquette + MySQL utilisée et Egg a également des plug-ins liés. SEQUILISE est un ORM utilisé dans l'environnement de nœud, en prenant en charge Postgres, MySQL, MARIADB, SQLITE et Microsoft SQL Server, ce qui est tout à fait pratique à utiliser. Vous devez d'abord définir la relation directe entre le modèle et le modèle. Une fois la relation établie, vous pouvez utiliser certaines méthodes prédéfinies.
Les informations de base du modèle sont plus faciles à traiter. Ce qui doit être prêté attention, c'est la conception des relations entre les entités, c'est-à-dire associée. Voici la description de la relation de l'utilisateur
// 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;
};
Par exemple, la relation entre l'utilisateur et UserInfo est une relation un à un. Après sa définition, nous pouvons utiliser user.setUserInfo(userInfo) lors de la création d'un nouvel utilisateur. Lorsque vous souhaitez obtenir les informations de base de cet utilisateur, vous pouvez également utiliser user.getUserInfo()
La relation entre l'utilisateur et l'application est un à plusieurs, c'est-à-dire qu'un utilisateur peut correspondre à plusieurs applications, et actuellement uniquement des amis et des applications de groupe sont appliqués:
Lors de l'ajout d'une application, vous pouvez utiliser user.addApply(apply) et lors de l'obtention, vous pouvez l'utiliser comme suit:
const result = await ctx.model.Apply.findAndCountAll({
where: {
userId: ctx.session.user.id,
hasHandled: false
}
});
La relation entre l'utilisateur et le groupe est plusieurs à plusieurs, c'est-à-dire qu'un utilisateur peut correspondre à plusieurs groupes, et un groupe peut correspondre à plusieurs utilisateurs. De cette façon, Sequelize établira une table intermédiaire User_Group pour réaliser cette relation.
J'utilise généralement ceci:
group.addUser(user); // 建立群组和用户的关系
user.getGroups(); // 获取用户的群组信息
L'œuf fournit le plug-in à socle oeuf.io. Vous devez ouvrir le plug-in dans config / plugin.js après l'installation d'oeg-socket.io. IO a son propre middleware et contrôleur.
L'itinéraire d'IO est différent de celui des demandes General HTTP. Notez que l'itinéraire ici ne peut pas être traité avec du middleware (je n'ai pas réussi), j'ai donc géré le traitement d'interdiction dans le contrôleur.
// 加入群
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);
Remarque: Je considère la relation en groupe et ami comme une pièce (c'est-à-dire une session), afin que je puisse envoyer des messages directement au ROMM, et tout le monde à l'intérieur peut les recevoir.
Il y a deux middleware par défaut, l'un est le middleware de connexion appelé lors de la connexion et de la déconnexion, qui est utilisé pour vérifier l'état de connexion et traiter la logique métier; L'autre est le middleware de paquet appelé chaque fois qu'un message est envoyé, qui est utilisé pour imprimer le journal
Étant donné que l'autorisation d'appel est prédéfinie, elle est traitée dans le contrôleur
// 对用户发言的权限进行判断
if (!ctx.session.user.rights.some(right => right.keyName === 'speak')) {
return;
}
Les chats sont divisés en chats uniques et chats de groupe. Les informations de chat incluent temporairement des textes généraux, des images, des vidéos et des messages de positionnement, qui peuvent être étendus dans les commandes ou les produits selon l'entreprise.
La conception de la structure du message fait référence à la conception de plusieurs services tiers et a également été ajusté en combinaison avec la situation de ce projet lui-même. Il peut être étendu à volonté et l'explication suivante est faite:
const Message = app.model.define('message', {
/**
* 消息类型:
* 0:单聊
* 1:群聊
*/
type: {
type: STRING
},
// 消息体
body: {
type: JSON
},
fromId: { type: INTEGER },
toId: { type: INTEGER }
});
Le corps stocke le corps du message, qui est utilisé pour stocker différents formats de messages à l'aide de 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 // 纬度
}
Il n'y en a actuellement qu'un, qui est de mettre à jour le jeton de Baidu. C'est assez simple ici, référez-vous simplement à la documentation officielle.
Unité de plate-forme de personnalisation et de service de dialogue intelligent intelligent
C'est assez intéressant. Vous pouvez créer un nouveau robot et ajouter des compétences correspondantes sur https://ai.baidu.com/ . Je discute ici, et il y a des questions et réponses intelligentes, etc.
Si vous ne souhaitez pas commencer, vous pouvez supprimer ctx.service.baidu.getToken();
Tout d'abord, vous devez le configurer dans le fichier de configuration. J'ai limité la taille du fichier ici et le format de fichier vidéo iOS inter-sites:
config.multipart = {
mode: 'file',
fileSize: '3mb',
fileExtensions: ['.mov']
};
Une interface unifiée est utilisée pour gérer les téléchargements de fichiers. Le problème de base est l'écriture de fichiers et les fichiers sont la liste de fichiers transmise à partir de la frontale
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)
});
}
Je stockage /public/upload/ dans le répertoire du serveur. Ce répertoire nécessite une configuration de fichier statique:
config.static = {
prefix: '/public/',
dir: path.join(appInfo.baseDir, 'public')
};
Le document officiel de l'oeuf de ce chapitre est de vous tuer, il n'y a rien d'exemples, vous devez lire le code source. C'est tellement terrible. Je l'ai étudié depuis longtemps avant de comprendre ce qui se passe.
Parce que je souhaite contrôler librement le mot de passe du mot de passe du compte, je n'utilise pas le passeport pour la connexion du mot de passe du compte, mais j'utilise l'authentification et la session d'interface ordinaire.
Ce qui suit est une description détaillée du processus de connexion en utilisant une plate-forme tierce (j'ai choisi GitHub):
Ouvrez le plug-in:
// 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 sur l'avant et lancez l'autorisation GitHub pour cette application. Après succès, GitHub ira sur http://localhost:3000/v1/passport/github/callback?code=12313123123 . Notre plug-in GitHubPassport obtiendra les informations de l'utilisateur sur GitHub. Après avoir obtenu les informations détaillées, nous devons vérifier les informations de l'utilisateur dans app/passport/verify.js , et l'associer aux informations utilisateur de notre propre plate-forme, et également attribuer une valeur à la session // 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;
};
Faites attention au code ci-dessus. S'il s'agit de la première autorisation, l'utilisateur sera créé. S'il s'agit de la deuxième autorisation, l'utilisateur a été créé.
Lorsque le système est déployé ou exécuté, certaines données et tableaux doivent être prédéfinies et les codes sont dans app.js et app/service/startup.js
La logique est qu'après le démarrage du projet, utilisez un modèle pour synchroniser la structure du tableau dans la base de données, puis commencer à créer des données de base:
Après avoir terminé ce qui précède, les données initiales sont terminées et peuvent fonctionner normalement
J'ai acheté le serveur Centos sur Tencent Cloud, le nom de domaine que j'ai acheté sur Alibaba Cloud, installé Node (12.18.2), Nginx et MySQL8.0, et j'ai commencé directement sur CentOS. Le front-end utilise nginx pour le proxy inverse. En raison des ressources du serveur limitées, il n'y a pas d'outils d'automatisation Jenkins et Docker, ce qui conduit à certaines opérations manuelles lors de la mise à jour.