websocket 实战——好友聊天
JS websocket 实战——好友聊天
原文地址:websocket 实战——好友聊天
还不了解 websocket 的同鞋,请先学习阮一峰老师的 WebSocket 教程
websocket
- websocket 在实际项目中有着很广的应用,如好友聊天,异步请求,react-hot-loader 的热更新等等
- 本文前端采用原生
WebSocket
,后端采用express-ws 库
实现聊天通信 - 后端 mongodb 数据存储采用 mongoose 操作,不了解的可以先看看 文档 哦
- 聊天原理很简单,如下图:
简单版本
先撸个简单版本,能够实现用户与服务器之间的通信
- 前端:WsRequest 封装类
class WsRequest { ws: WebSocket constructor(url: string) { this.ws = new WebSocket(url) this.initListeners() } initListeners() { this.ws.onopen = _event => console.log('client connect') this.ws.onmessage = event => console.log(`来自服务器的信息:${event.data}`) this.ws.onclose = _event => console.log('client disconnect') } send(content: string) { this.ws.send(content) } close() { this.ws.close() } } // 使用 const ws = new WsRequest('your_websocket_url') // 如: ws://localhost:4000/ws ws.send('hello from user')
- 服务端:WsRouter 封装类,使用单例模式
import expressWs, { Application, Options } from 'express-ws'; import ws, { Data } from 'ws'; import { Server as hServer } from 'http'; import { Server as hsServer } from 'https'; class WsRouter { static instance: WsRouter; wsServer: expressWs.Instance; clientMap: Map<string, ws>; // 保存所有连接的用户 id constructor( private path: string, private app: Application, private server?: hServer | hsServer, private options?: Options ) { this.wsServer = expressWs(this.app, this.server, this.options); this.app.ws(this.path, this.wsMiddleWare); this.clientMap = new Map(); } static getInstance(path: string, app: Application, server?: hServer | hsServer, options: Options = {}) { if (!this.instance) { this.instance = new WsRouter(path, app, server, options); } return this.instance; } wsMiddleWare = (wServer: any, _req: any) => { this.clientMap.set(id, wServer); this.broadcast('hello from server'); // send data to users wServer.on('message', async (data: Data) => console.log(`来自用户的信息:${data.toString()}`)); wServer.on('close', (closeCode: number) => console.log(`a client has disconnected: ${closeCode}`)); } broadcast(data: Data) { // 全体广播 this.clientMap.forEach((client: any) => { if (client.readyState === ws.OPEN) { client.send(data); } }); } } export default WsRouter.getInstance; // 使用:bootstrap.ts const server = new InversifyExpressServer(container); // 注:本项目后端使用的是 [Inversify](https://github.com/inversify) 框架 // 具体传的 private server?: hServer | hsServer 参数值,请类比改变 server.setConfig((app: any) => WsRouter('/ws/:id', app)) server.build().listen(4000);
升级版本
要实现好友通信,在前后端的 send
方法中,当然要指定 from
和 to
的用户
再者,后台要记录发送的消息,也必须要有好友表的主键 friendId,表示为这两个人之间的消息
- mongoose 数据存储
// user.ts const userSchema = new Schema( { name: { type: String, required: true, unique: true } } ); export default model('User', userSchema); // friend.ts 两个用户之间的好友关系 import { Schema, model, Types } from 'mongoose'; const FriendSchema = new Schema( { user1: { type: Types.ObjectId, ref: 'User', required: true }, // user1Id < user2Id user2: { type: Types.ObjectId, ref: 'User', required: true } } ); export default model('Friend', FriendSchema); // message.ts const MessageSchema = new Schema( { friend: { type: Types.ObjectId, ref: 'Friend', required: true }, // 关联 Friend 表 from: String, to: String, content: String, type: { type: String, default: 'text' }, } ); export default model('Message', MessageSchema);
- 接口说明
type msgType = 'text' | 'emoji' | 'file' interface IMessage { from: string to: string content: string type: msgType }
- 前端:WsRequest 封装类
import { IMessage, msgType } from './interface' export default class WsRequest { ws: WebSocket constructor(url: string, private userId: string) { this.ws = new WebSocket(`${url}/${this.userId}`) this.initListeners() } initListeners() { this.ws.onopen = _event => console.log('client connect') this.ws.onmessage = event => { const msg: IMessage = JSON.parse(event.data) console.log(msg.content) } this.ws.onclose = _event => console.log('client disconnect') } // friendId 指 Friend Model 的 _id async send(friendId: string, content: string, receiverId: string, type: msgType = 'text') { const message: IMessage = { from: this.userId, to: receiverId, content, type } await this.ws.send(JSON.stringify({ friend: friendId, ...message })) } close() { this.ws.close() } } // 使用 const ws = new WsRequest('your_websocket_url', 'your_user_id') // example: ws://localhost:4000/ws await wsRequest.send('Friend_model_id', '你好啊,jeffery', 'jeffery_id')
- 服务端:WsRouter 封装类,使用单例模式
import expressWs, { Application, Options } from 'express-ws'; import ws, { Data } from 'ws'; import { Server as hServer } from 'http'; import { Server as hsServer } from 'https'; import Message, { IMessage } from 'models/message'; import Friend from 'models/friend'; class WsRouter { static instance: WsRouter; wsServer: expressWs.Instance; clientMap: Map<string, ws>; // 保存所有连接的用户 id constructor( private path: string, private app: Application, private server?: hServer | hsServer, private options?: Options ) { this.wsServer = expressWs(this.app, this.server, this.options); this.app.ws(this.path, this.wsMiddleWare); this.clientMap = new Map(); } static getInstance(path: string, app: Application, server?: hServer | hsServer, options: Options = {}) { if (!this.instance) { this.instance = new WsRouter(path, app, server, options); } return this.instance; } wsMiddleWare = (wServer: any, req: any) => { const { id } = req.params; // 解析用户 id wServer.id = id; this.clientMap.set(id, wServer); wServer.on('message', async (data: Data) => { const message: IMessage = JSON.parse(data.toString()); const { _id } = await new Message(message).save(); // 更新数据库 this.sendMsgToClientById(message); }); wServer.on('close', (closeCode: number) => console.log(`a client has disconnected, closeCode: ${closeCode}`)); }; sendMsgToClientById(message: IMessage) { const client: any = this.clientMap.get(message.to); if (client) { client!.send(JSON.stringify(message)); } } broadcast(data: Data) { this.clientMap.forEach((client: any) => { if (client.readyState === ws.OPEN) { client.send(data); } }); } } export default WsRouter.getInstance; // 使用:bootstrap.ts const server = new InversifyExpressServer(container); // 注:本项目后端使用的是 [Inversify](https://github.com/inversify) 框架 // 具体传的 private server?: hServer | hsServer 参数值,请类比改变 server.setConfig((app: any) => WsRouter('/ws/:id', app)) server.build().listen(4000);
- 心跳检测
参考:
// 服务端 wsMiddleWare = (wServer: any, req: any) => { const { id } = req.params; wServer.id = id; wServer.isAlive = true; this.clientMap.set(id, wServer); wServer.on('message', async (data: Data) => {...}); wServer.on('pong', () => (wServer.isAlive = true)); } initHeartbeat(during: number = 10000) { return setInterval(() => { this.clientMap.forEach((client: any) => { if (!client.isAlive) { this.clientMap.delete(client.id); return client.terminate(); } client.isAlive = false; client.ping(() => {...}); }); }, during); }
在线测试
一、准备
- 为方便大家测试,可以访问线上的服务端接口:
wss://qaapi.omyleon.com/ws
,具体使用要附上用户 id,如:wss://qaapi.omyleon.com/ws/asdf...
,参见 升级版本的 websocket - 客户端:可以使用谷歌插件:Simple WebSocket Client;也可以访问在线项目,使用项目提供的客户端,具体参见:qa-app
- 使用线上的一对好友信息
- friend: jeffery 与 testuser => _id: 5d1328295793f14020a979d5
- jeffery => _id: 5d1327c65793f14020a979ca
- testuser => _id: 5d1328065793f14020a979cf
二、实际演示
- 打开 WebSocket Client 插件,输入测试链接,如下图:
wss://qaapi.omyleon.com/ws/5d1327c65793f14020a979ca
- 点击
Open
,下方Status
显示Opened
表示连接成功
- 发送消息,请根据 IMessage 接口来发送,当然还要附上
friendId
,否则不能对应到相应的好友关系上
{ "friend": "5d1328295793f14020a979d5", "from": "5d1327c65793f14020a979ca", "to": "5d1328065793f14020a979cf", "content": "你好呀,testuser,这是通过 simple websocket client 发送的消息", "type": "text" }
- 同时用 simple websocket client 连接另一个用户,会收到发来的消息
- 同理,在另一个 client 中改变 from 和 to,就能回复消息给对方
wss://qaapi.omyleon.com/ws/5d1328065793f14020a979cf { "friend": "5d1328295793f14020a979d5", "from": "5d1328065793f14020a979cf", "to": "5d1327c65793f14020a979ca", "content": "嗯嗯,收到了 jeffery,这也是通过 simple websocket client 发送的", "type": "text" }
- 补充,在线上项目 qa-app 中,也是用的是个原理,只不过在显示时候做了解析,只显示了 content 字段,而在这个简易的测试客户端中没有做其他处理
源码获取
相关推荐
firejq 2020-06-14
woniyu 2020-06-02
取个好名字真难 2020-06-01
woniyu 2020-05-20
darylove 2020-05-14
shufen0 2020-04-18
shufen0 2020-04-14
guozewei0 2020-03-06
取个好名字真难 2020-02-13
柳木木的IT 2020-11-04
joynet00 2020-09-23
wenf00 2020-09-14
蓝色深海 2020-08-16
wuychn 2020-08-16
取个好名字真难 2020-08-06
darylove 2020-06-26
shufen0 2020-06-20
Lovexinyang 2020-06-14