案例-Node+Websocket单人聊天功能

这次我们开发一个单人聊天的案列

思路
首先整体设计上我们除了登录模块现在还需要加一个聊天模块,其次发送方需要一个发送信息的接口,接收方应该有一个新消息通知事件

实现
1、修改terminalSDK.ts(定义外部注册的新消息通知事件onEvtNewsInComing )

import tsdkClient from './tsdkClient'
import Logger from './util/logger'
export default class terminalSDK {
	public static tsdkClient;
	constructor () {
	}
	 
	public static tsdkCreateClient(initParam: any, listeners:any) {
		try{
		    terminalSDK.tsdkClient = new tsdkClient(initParam)
		}catch (err){	
			Logger.error("terminalSDK", "terminal create failed..." + err)
		}
		if(!terminalSDK.tsdkClient){
			return ""
		}

		if(listeners && listeners.onEvtLoginSuccess != 'undefined'){
			terminalSDK.tsdkClient.on("onEvtLoginSuccess", listeners.onEvtLoginSuccess)
		}
		
		if(listeners && listeners.onEvtLoginFailed != 'undefined'){
			terminalSDK.tsdkClient.on("onEvtLoginFailed", listeners.onEvtLoginFailed)
		}

		if(listeners && listeners.onEvtNewsInComing != 'undefined'){
			terminalSDK.tsdkClient.on("onEvtNewsInComing", listeners.onEvtNewsInComing)
		}

		if(listeners && listeners.onEvtWebSocketConnect != 'undefined'){
			terminalSDK.tsdkClient.on("onEvtWebSocketConnect", listeners.onEvtWebSocketConnect)
		}
		
		if(listeners && listeners.onEvtWebSocketClose != 'undefined'){
			terminalSDK.tsdkClient.on("onEvtWebSocketClose", listeners.onEvtWebSocketClose)
		}
				
		return terminalSDK.tsdkClient
	 }
}

2、修改tsdkClient.ts(新增一个对外暴露的发送消息接口以及将新消息通知事件加入到主题类中)

import tsdkManagerService from './service/tsdkManagerService'
import tsdkLoginService from './service/tsdkLoginService'
import tsdkChatService from './service/tsdkChatService'
import  Observer from './util/observer'
export default class tsdkClient {
	private static  __listeners: any = {}
	private static loginService: tsdkLoginService
	private static managerService: tsdkManagerService
	private static tsdkChatService: tsdkChatService
	
	constructor (initParam:any) {
		tsdkClient.managerService = new tsdkManagerService(initParam.svraddr, initParam.port, initParam.ssl, this.reconnectSucessCallback)
		tsdkClient.loginService = new tsdkLoginService()
		tsdkClient.tsdkChatService = new tsdkChatService()
		
		this.registerWebsocketEvent()
		this.registerLoginEvent()
		this.registerChatEvent()
	}

	reconnectSucessCallback() {
		let _this = this
		if(Observer.getReconnectStatus()){
			tsdkClient.loginService = new tsdkLoginService()
			Observer.resetReconnect()
		}
		Observer.publish("onEvtWebSocketConnect", '服务器连接成功') // 上报重连成功
	}

	// login module
	public login(loginParam: any, callback:Function) {
		tsdkClient.loginService.login(loginParam, callback)
	}

	public reconnectBindWs(userId: number, callback:Function) {
		tsdkClient.loginService.reconnectBindWs(userId, callback)
	}

	// chat module
	public singlePersonChat(chatParam: any, callback:Function) {
		tsdkClient.tsdkChatService.singlePersonChat(chatParam, callback)
	}

	public notify (event: string, data:any) {
		let _listen = tsdkClient.__listeners[event]
		if(!_listen){
			return false
		}
		for(let i = 0;i < _listen.length; i++){
			typeof _listen[i] === 'function' && _listen[i](data)
		}
	}
	
	public on(event:string, callbacks:Function) {
		if(!tsdkClient.__listeners[event]){
			tsdkClient.__listeners[event] = []
		}
		tsdkClient.__listeners[event].push(callbacks)
	}

	public registerLoginEvent() {
		Observer.subscribe('onEvtLoginSuccess', (ret:any) => {
			this.notify("onEvtLoginSuccess", ret)
		})
		
		Observer.subscribe('onEvtLoginFailed', (ret:any) => {
			this.notify("onEvtLoginFailed", ret)
		})
	}

	public registerWebsocketEvent() {
		Observer.subscribe('onEvtWebSocketConnect', (ret:any) => {
			this.notify("onEvtWebSocketConnect", ret)
		})
		
		Observer.subscribe('onEvtWebSocketClose', (ret:any) => {
			this.notify("onEvtWebSocketClose", ret)
		})
	}

	public registerChatEvent() {
		Observer.subscribe('onEvtNewsInComing', (ret:any) => {
			this.notify("onEvtNewsInComing", ret)
		})
	}
}

3、聊天模块service层tsdkChatService.ts

import tsdkChatWrapper from '../wrapper/tsdkChatWrapper'
import Observer from '../util/observer'
import Logger from '../util/logger'
export default class tsdkChatService{
	private wrapper: tsdkChatWrapper
	
	constructor() {
		this.wrapper = tsdkChatWrapper.getInstance()
		this.wrapper.build()
		this.registerChatEvent()
	}
	
	public async singlePersonChat(chatParam: any, callback: Function){
		let retData = await this.wrapper.singlePersonChat(chatParam);
		callback(retData)
	}
	
	public registerChatEvent() {
		Logger.info("tsdkChatService","registerChatEvent")
		this.wrapper.registerChatEvent({
			onEvtNewsInComing: tsdkChatService.handleOnEvtNewsInComing
		})
	}
	
	public static handleOnEvtNewsInComing(ret){
		Observer.publish("onEvtNewsInComing", ret)
	}
}

4、聊天模块wrapper层tsdkChatWrapper.ts

import tsdkChat from '../json_adapt/tsdkChat'
import tsdkManagerWrapper from './tsdkManagerWrapper'
import Logger from '../util/logger'
export default class tsdkChatWrapper{
	private static tsdkChat: tsdkChat
	private static wrapper: tsdkChatWrapper = new tsdkChatWrapper()
	
	constructor() {
		if(tsdkChatWrapper.wrapper){
			 throw Error("tsdkChatWrapper has exist")
		}
		tsdkChatWrapper.wrapper = this
	}
	
	public build () {
		Logger.info("tsdkChatWrapper","tsdkChatWrapper has build")
		tsdkChatWrapper.tsdkChat = new tsdkChat({
			socket: tsdkManagerWrapper.socketService
		})
	}
	
	public static getInstance () {
		return tsdkChatWrapper.wrapper
	}
	
	public singlePersonChat (chatParam: any) {
		let callback = { response: {} }
		let promise = new Promise((resolve, reject) => {
			callback.response = (ret: any) => {
				resolve(ret)
			}
		})
		tsdkChatWrapper.tsdkChat.singlePersonChat(chatParam, callback)
		return promise
	}
	
	public registerChatEvent(callbacks:any) {
		tsdkChatWrapper.tsdkChat.setBasicEvent(callbacks)
	}
}

5、聊天模块数据发送层tsdkChat.ts


export default class tsdkChat{
	private serviceTunnel: any
	
	constructor(opt) {
		this.serviceTunnel = opt.socket
	}
	
	sendData(data) {
		let dataStr = JSON.stringify(data)
		if(this.serviceTunnel.socket){
			this.serviceTunnel.sendData(dataStr)
		}
	}
	
	callbackResponse(callback: any, rsp:number) {
		if(callback.response && typeof callback.response === 'function'){
			this.serviceTunnel.rspFuncs[rsp] = callback.response
		}
	}
	
	//cmd 3001
	singlePersonChat(chatInfo: any, callback) {
		this.callbackResponse(callback, 3001)
		
		let data = {
			"cmd": 3001,
			"description": "tsdk_single_person_chat",
			"param": {
				"chatInfo": chatInfo
			}
		}
		
		this.sendData(data)
	}
	
	setBasicEvent(callbacks:any) {
		if(typeof callbacks.onEvtNewsInComing === 'function') {
			this.serviceTunnel.notifyFuncs[3002] = callbacks.onEvtNewsInComing
		}
	}
}

当前的目录结构
案例-Node+Websocket单人聊天功能
打包
项目根目录下执行npm run build 进行打包
案例-Node+Websocket单人聊天功能
服务端
接下来进行服务端功能的开发
1、Test\server\jsonAdapt目录下新建chat文件夹,在chat文件夹下新建tsdk_chat_cmd.js、tsdk_chat_notify.js文件
tsdk_chat_cmd.js

const { _makeMsgSetAndSend } = require('../../util/util');
const Tunnel = require('../../util/Tunnel');
const ChatService = require('./tsdk_chat_notify');
class ChatController {
	/*
	chatParam: {
	   target // 发送对象
	   message
	} */
	static async singlePersonChat(chatInfo, ws) {
		let ret 
		let targetWs = Tunnel.getUserWs(chatInfo.target)
		if(targetWs==null){
			ret = _makeMsgSetAndSend(3001, 10002, "tsdk_single_person_chat")
			ws.send(ret)
			return false
		}
		
		let originUser = Tunnel.searchUserByWs(ws)
		let messageInfo = {
			origin: originUser,
			content: chatInfo.message
		}
		let jsonValue = ChatService.onEvtNewsInComing(messageInfo)
		ret = _makeMsgSetAndSend(3001, 0, "tsdk_single_person_chat")
		ws.send(ret)
		targetWs.send(jsonValue)
	}
	
}

module.exports = ChatController;

tsdk_chat_notify.js

const { _makeMsgImport } = require('../../util/util');
class ChatService {
	
	// 3002
	static onEvtNewsInComing (messageInfo) {
		let param = {  
			message: messageInfo
		}
		return _makeMsgImport(3002, param, "ON_EVT_NEWS_INCOMING")
	}
}

module.exports = ChatService;

2、修改Test\server\util下Tunnel.js

class Tunnel{
	static users = {}
	static online_sum = 0 // 在线总人数

	static bindWsByUserId(userId, ws) { // 将userId、用户的连接进行绑定
		if(!Tunnel.users[userId]){
			Tunnel.users[userId] = {}
			Tunnel.online_sum++
		}
		Tunnel.users[userId].ws = ws
	}
	
	static getUserWs(userId) { // 根据userId返回用户websocket连接对象
		if(!Tunnel.users[userId]){
			return null
		}
		return Tunnel.users[userId].ws
	}

	static searchUserByWs(ws) { // 通过socket连接查找对应userId
		let allUsers = Object.keys(Tunnel.users)
		for(let i =0; i< allUsers.length; i++) {
			let userId = allUsers[i]
			if(Tunnel.users[userId].ws == ws) {
				return userId
			}
		}
		return -1;
	}
}
module.exports = Tunnel;

3、修改Test\server\util目录下util.js

const ErrorCode = {
	10001: '参数错误',
	10002: '消息发送失败'
}

// 接口调用的返回格式
function _makeMsgSetAndSend (rsp, code, description) {
	let JsonParam = {
		 description: description,
		 result: code == 0? code : {errorCode: code, reason: ErrorCode[code]},
		 rsp: rsp,
	}
	return JSON.stringify(JsonParam)
}

// 接口回调或通知事件的返回格式
function _makeMsgImport(notify, result, description){
	let JsonParam = {
		 description: description,
		 result: result,
		 notify: notify,
	}
	return JSON.stringify(JsonParam)
}

module.exports = { _makeMsgSetAndSend, _makeMsgImport }

4、修改Test\server下的app.js

var express = require("express");
var http = require('http');
var fs = require('fs');
var WebSocket = require('ws');
var app = express();

app.get('/', function(req, res){
	res.send('Hello,myServer'); //服务器响应请求
});

const LoginController = require('./jsonAdapt/login/tsdk_login_cmd');
const ChatController = require('./jsonAdapt/chat/tsdk_chat_cmd');

var httpServer = http.createServer(credentials, app);
const PORT = 3000;
const hostname = '0.0.0.0';
httpServer.listen(PORT,hostname, function() {
    console.log('Websocket Server is running on: http://'+hostname+':%s', PORT);
});

var wss = new WebSocket.Server({server: httpServer});
wss.on('connection', function connection(ws) {
	console.log('链接成功!');
	ws.on('message', async function incoming(data) {
		let message = JSON.parse(data)
		console.log(message)
		await jsonAdapt(message, ws)
   });
});

function  jsonAdapt(message, ws) {
	switch (message.cmd) {
		case 1001:
		    LoginController.login(message.param.loginParam, ws)
			break;
		case 1002:
			LoginController.reconnectBindWs(message.param.userId, ws)
			break;
		case 3001:
			ChatController.singlePersonChat(message.param.chatInfo , ws)
			break;
	}
}

5、启动服务器 node app.js
案例-Node+Websocket单人聊天功能
前台
修改Test\html目录下的index.html,新建index2.html(与index.html页面除了登陆账号不同其余内容相同,用于创建单人聊天场景)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>websocketSDK</title>
    <script src="../../build/terminalSDK.js"></script>
    <script>
        let initParam = {
            svraddr: '127.0.0.1',
            port: 3000,
            ssl: 0 // 0:http, 1:https
        }
        let userInfo = {} // 登陆成功后保存用户信息
        let isReconnect = false // 重连状态标识
        let listeners = {
            onEvtLoginSuccess: (ret) => {
                console.log(ret)
                userInfo = ret.result.userInfo
            },
            onEvtLoginFailed: (ret) => {
                console.log(ret)
            },
            onEvtWebSocketConnect: (ret) => {
                console.log(ret)
                if(isReconnect) { // 重连成功
                    isReconnect = false
                    tsdkClient.reconnectBindWs(userInfo.userId, (ret) => {
                        console.log( ret)
                    })
                }
            },
            onEvtWebSocketClose: (ret) => {
                console.log(ret)
                isReconnect = true
            },
            onEvtNewsInComing: (ret) => {
                console.log('新消息:', ret)
            }
        }
        window.tsdkClient = terminalSDK.tsdkCreateClient(initParam, listeners)

        function login() {
            let loginParam = {
                username: '张三',
                pwd: '123456'
            }
            tsdkClient.login(loginParam, (ret) => {
                console.log( ret)
            })
        }

        function send() {
            let chatParam = {
                target: 2, // 接收方userId
                message: '你好'
            }
            tsdkClient.singlePersonChat(chatParam, (ret) => {
                console.log( ret)
            })
        }
        
    </script>
</head>
<body>
    <button onclick="login()">登录</button>
    <button onclick="send()">发送消息</button>
</body>
</html>

浏览器访问index.html、index2.html
案例-Node+Websocket单人聊天功能
案例-Node+Websocket单人聊天功能
备注:index.html页面登陆账号为发送方,index2.html页面登陆账号为接收方
案例-Node+Websocket单人聊天功能
备注:对方不在线场景,返回相应提示

思考
以上是单人聊天功能的实现,同样如果是群聊,我们通过数据库查询出群内所有成员,然后查找出这些成员的socket连接集,给这些成员发送消息,这块就不一一实现了,感兴趣的话可以自己尝试一下。

上一篇:本机GitHub多账号操作


下一篇:Python里面的any()、all()函数