該功能使用nodejs 寫后臺(tái), vue寫前端, 利用websoket作為長(zhǎng)連接, protobuf作為數(shù)據(jù)格式傳輸數(shù)據(jù)實(shí)現(xiàn)了簡(jiǎn)單的聊天, 其中node是使用了nodejs-websocket作為三方庫(kù)
直接上代碼
vue代碼
webSocketManager.js 自定義的工具類
// 獲取protobuf 的root
let protobuRoot = require("protobufjs").Root;
// 獲取定義的protobuf文件的對(duì)象json
let protoJson = require("../utils/proto");
let messageRoot = protobuRoot.fromJSON(protoJson);
// 定義websocket 地址
let socketurl = "ws://192.168.0.252:8091";
// 重連鎖, 防止過多重連
let reconnectLock = false;
// 定義一個(gè)消息發(fā)送中(包含發(fā)送失敗的)的字典
window.messageSendingDic = {};
// 定義一個(gè)消息websocket連接狀態(tài)的字段, 并且要綁定到widow上, 方便調(diào)用
// 0 未連接, 1 連接成功 2 連接中
window.webSocketState = 0;
// 定義連接服務(wù)器方法
function connectWebsocket(){
//如果用戶已登錄, 進(jìn)行連接websoket, 如果沒有登陸, 登錄后進(jìn)行連接 用token判斷
// 創(chuàng)建一個(gè)websocket連接
// let webSocket = new WebSocket(socketurl);
// 如果想要傳token, 因?yàn)閣s不支持通過設(shè)置header, 所以直接在地址中加參數(shù),
// 如ws://192.168.0.252:8091?name=lulu&token=123456
let name = "lulu";
let token = "123456"
let webSocket = new WebSocket(socketurl+`?appname=${name}&token=${token}`);
// let webSocket = new WebSocket(socketurl+`?appname=${name}&token=${token}`, ["soap"]);
// 監(jiān)聽webSocket的各個(gè)狀態(tài)
// 連接成功
webSocket.onopen = function() {
console.log("websocket連接成功")
// 連接成功后將連接狀態(tài)改變
window.webSocketState = 1;
// 連接成功后, 要將消息隊(duì)列里面的消息重新發(fā)送出去(底層重發(fā), 和頁(yè)面無(wú)關(guān))
for(let session in window.messageSendingDic){
session.forEach(message => {
// 重發(fā)消息
reSendMessage(message)
});
}
}
// 連接出錯(cuò)
webSocket.onerror = function(error){
console.log("websocket連接出錯(cuò)", error);
console.log("websocket連接出錯(cuò)", error.data);
// 進(jìn)行重連
reconnectWebsocket();
}
// 連接關(guān)閉
webSocket.onclose = function(result){
console.log("websocket連接關(guān)閉", result);
if(result == "退出登錄"){
return
}
// 進(jìn)行重連
reconnectWebsocket();
}
// 接受到消息
webSocket.onmessage = function(message){
// console.log("websocket接受到消息", message);
// 將受到的消息進(jìn)行分類, 分發(fā)處理
formatAcceptMessage(message)
}
// 將webSocket綁定到window上面, 方便后續(xù)調(diào)用
window.webSocket = webSocket;
}
// 定義重連方法 如果連接失敗, 或者關(guān)閉,
function reconnectWebsocket(){
// 如果正在重連, 則返回
if(reconnectLock){
return;
}
// 進(jìn)行加鎖
reconnectLock = true;
// 重連時(shí)將連接狀態(tài)改變
window.webSocketState = 2;
// 為了防止過多請(qǐng)求, 1s后進(jìn)行重連
setTimeout(function(){
// 解鎖
reconnectLock = false;
// 進(jìn)行連接, 如果失敗接著重連
// connectWebsocket();
}, 1000)
}
/**
* 關(guān)閉websocket 退出時(shí)會(huì)用到
*
*/
function closeWebsocket(){
window.webSocket.onclose("退出登錄")
}
// 定義發(fā)送消息的方法 message 格式為json
/**
*
* @param {
* message: "內(nèi)容",
* id: "xxxxxxx"
* } message 消息內(nèi)容
* @param "1" messageType 消息類型
* @param "QueryMsg" messageClass 附加字段嗎消息類, 這里是以protobufjs的消息類為例
*/
function sendMessage(message, messageType) {
// 這里可以對(duì)message做一些格式化處理
// let formaterMessge = message;
// 如果沒有傳遞messageType, 則默認(rèn)為即時(shí)消息
if(!messageType){
messageType = 1;
}
// 如果發(fā)送的消息為即時(shí)消息, 要記錄消息的發(fā)送狀態(tài)
if(messageType == 1){
// 將消息添加到發(fā)送中的數(shù)組中進(jìn)行記錄
// 先判斷該回話有沒有對(duì)應(yīng)的數(shù)組, 如果沒有就創(chuàng)建, 在添加, 如果有直接添加
if(window.messageSendingDic[message.sessionId]) {
window.messageSendingDic[message.sessionId].push(message);
} else {
window.messageSendingDic[message.sessionId] = [];
window.messageSendingDic[message.sessionId].push(message);
}
}
// 如果websocket連接成功, 進(jìn)行發(fā)送消息
if(window.webSocketState == 1) {
// formaterMessge = JSON.stringify(formaterMessge)
let bufferMessage = creatBufferMessage(message, messageType)
//
console.log("要發(fā)送的消息", message, messageType)
// 這里就可以直接用window調(diào)用了
window.webSocket.send(bufferMessage);
} else {
// 如果websocket沒有連接成功, 直接告訴消息發(fā)送頁(yè)面消息發(fā)送失敗, 模擬接受到消息, 發(fā)給對(duì)應(yīng)頁(yè)面
let formaterMessge = {};
// 將處理后的消息進(jìn)行發(fā)送通知, 通知給需要的頁(yè)面進(jìn)行處理, 在需要的頁(yè)面進(jìn)行監(jiān)聽
// 注意: 使用頁(yè)面添加window.addEventListener("receivedNewMessage", this.testAction)
window.dispatchEvent(new CustomEvent("receivedNewMessage", message));
}
}
// 定義重發(fā)送消息的方法 message 格式為json
/**
*
* @param {
* message: "內(nèi)容",
* id: "xxxxxxx"
* } message 消息內(nèi)容
* @param "1" messageType 消息類型
* @param "QueryMsg" messageClass 附加字段嗎消息類, 這里是以protobufjs的消息類為例
*/
function reSendMessage(message) {
// 如果websocket連接成功, 進(jìn)行發(fā)送消息
if(window.webSocketState == 1) {
// 這里就可以直接用window調(diào)用了
window.webSocket.send(message);
}
}
// 定義收到消息進(jìn)行消息解析的方法
function formatAcceptMessage(message) {
// 處理消息. 格式化, 獲取消息的blob數(shù)據(jù)
let bufferMessage = message.data;
// 將buffer數(shù)據(jù)解析為json消息
getMessageFromBufferMessage(bufferMessage, (message, messageType) => {
console.log("接受到的消息")
console.log(message, messageType)
// 除了是服務(wù)器發(fā)送的確認(rèn)消息外, 都應(yīng)該向服務(wù)器發(fā)送確認(rèn)消息
if(messageType == 2){
// 2是確認(rèn)消息, 收到服務(wù)器發(fā)送的確認(rèn)消息后, 說明消息發(fā)送成功
// 將發(fā)送成功的消息從發(fā)送中移除
if(window.messageSendingDic[message.sessionId]) {
let sendingArray = window.messageSendingDic[message.sessionId];
// 過濾發(fā)送成功的
window.messageSendingDic[message.sessionId] = sendingArray.filter(msg => {
return msg.id != message.id
});
}
} else {
// 向服務(wù)器發(fā)送確認(rèn)消息
// 創(chuàng)建確認(rèn)消息
let ackMessage = {
mid: message.mid,
uid: message.uid,
sessionId: message.sessionId
}
// 發(fā)送確認(rèn)消息
sendMessage(ackMessage, "2")
// 將處理后的消息進(jìn)行發(fā)送通知, 通知給需要的頁(yè)面進(jìn)行處理, 在需要的頁(yè)面進(jìn)行監(jiān)聽
if(messageType == 1){
// 1是即時(shí)消息, 發(fā)送給聊天頁(yè)面和聊天列表頁(yè), 去刷新頁(yè)面信息
// 注意: 使用頁(yè)面添加window.addEventListener("receivedNewMessage", this.testAction)
window.dispatchEvent(new CustomEvent("receivedNewMessage", {detail: message}));
} else if(messageType == 3){
// 3是同步消息,
// 這里面是數(shù)組, 注意發(fā)送給聊天頁(yè)面
message.msgsArray.forEach(element => {
// 注意: 使用頁(yè)面添加window.addEventListener("receivedNewMessage", this.testAction)
window.dispatchEvent(new CustomEvent("receivedNewMessage", message));
});
} else if(messageType == 4){
// 4是離線推送消息
// 這里面是數(shù)組, 注意發(fā)送給聊天頁(yè)面
message.msgsArray.forEach(element => {
// 注意: 使用頁(yè)面添加window.addEventListener("receivedNewMessage", this.testAction)
window.dispatchEvent(new CustomEvent("receivedNewMessage", message));
});
} else if(messageType == 51){
// 好有申請(qǐng)
// 注意: 使用頁(yè)面添加window.addEventListener("receivedApplyFriendMessage", this.testAction)
window.dispatchEvent(new CustomEvent("receivedApplyFriendMessage", message));
} else if(messageType == 52){
// 好友接受申請(qǐng)
// 注意: 使用頁(yè)面添加window.addEventListener("receivedAcceptFriendMessage", this.testAction)
window.dispatchEvent(new CustomEvent("receivedAcceptFriendMessage", message));
} else if(messageType == 53){
// 被踢出群
// 注意: 使用頁(yè)面添加window.addEventListener("receivedKickedOutMessage", this.testAction)
window.dispatchEvent(new CustomEvent("receivedKickedOutMessage", message));
} else if(messageType == 54){
// 被禁言
// 注意: 使用頁(yè)面添加window.addEventListener("receivedBannedSpeakMessage", this.testAction)
window.dispatchEvent(new CustomEvent("receivedBannedSpeakMessage", message));
}else if(messageType == 58){
// 通知
// 注意: 使用頁(yè)面添加window.addEventListener("receivedNotificationMessage", this.testAction)
window.dispatchEvent(new CustomEvent("receivedNotificationMessage", message));
}
// 注意: 使用頁(yè)面添加window.addEventListener("acceptNewMessage", this.testAction)
window.dispatchEvent(new CustomEvent("acceptNewMessage", message));
}
});
}
// 將buffer二進(jìn)制數(shù)據(jù)轉(zhuǎn)換為json數(shù)據(jù)
function getMessageFromBufferMessage(bufferMessage, result){
// 創(chuàng)建一個(gè)文件讀取器
let reader = new FileReader();
// 將消息讀取為arrayBuffer類型
reader.readAsArrayBuffer(bufferMessage);
// 讀取成功的回調(diào)
reader.onload = function() {
// 獲取消息的buffer
let buffer = new Uint8Array(reader.result);
// 獲取消息類型, 第一個(gè)字節(jié)
let messageType = buffer[0];
// 獲取對(duì)應(yīng)消息類的名字, 默認(rèn)確認(rèn)空
let messageTypeName = getMessageTypeName(messageType);
// 獲取對(duì)應(yīng)protobuf消息類型
let protobufTypeObject = getProtobufTypeObject(messageTypeName);
// 獲取消息內(nèi)容buffer
let bufferMessageContent = buffer.subarray(1);
// 將消息內(nèi)容buffer進(jìn)行解碼, 得到具體消息
let message = protobufTypeObject.decode(bufferMessageContent);
result(message, messageType);
}
}
// 根據(jù)messageType獲取(將消息類型轉(zhuǎn)換為protobuf消息的毒性)對(duì)應(yīng)的消息類型對(duì)象
// messageType 消息類型, 例如 "Ack", 在proto.js中可以找到
function getProtobufTypeObject(messageTypeName){
// 根據(jù)messageType獲取(將消息類型轉(zhuǎn)換為protobuf消息的對(duì)象)對(duì)應(yīng)的消息類型對(duì)象
let protobufTypeObject = messageRoot.lookupType(messageTypeName);
return protobufTypeObject;
}
// 創(chuàng)建protobuf消息, 將json消息轉(zhuǎn)換為對(duì)應(yīng)的protobuf消息
function creatBufferMessage(message, messageType){
// 獲取對(duì)應(yīng)消息類的名字, 默認(rèn)確認(rèn)空
let messageTypeName = getMessageTypeName(messageType);
// 獲取對(duì)應(yīng)protobuf消息類型
let protobufTypeObject = getProtobufTypeObject(messageTypeName);
// 創(chuàng)建消息, 最后還需要添加一個(gè)字符表示消息類型
let protobufMessageContent = protobufTypeObject.create(message);
// 將消息進(jìn)行編碼
let encodeProtobufMessageContent = protobufTypeObject.encode(protobufMessageContent)
// 消息轉(zhuǎn)換完成
let bufferMessageContent = encodeProtobufMessageContent.finish();
// console.log("11111111")
// console.log("2222222", encodeProtobufMessageContent)
// console.log("333333", bufferMessageContent)
// 完整的proto信息, 添加了頭部消息樂行
let protobufMessage = bufferMessageAddType(messageType, bufferMessageContent);
return protobufMessage;
}
function getMessageTypeName(messageType){
let messageTypeName = "";
if(messageType == 1){
// 新消息
messageTypeName = "ChatMsg"
} else if(messageType == 2){
// 確認(rèn)消息
messageTypeName = "Ack"
} else if(messageType == 3){
// 同步消息
messageTypeName = "ChatMsgList"
} else if(messageType == 4){
// 離線推送消息
messageTypeName = "ChatMsgList"
} else if(messageType == 51){
// 好友申請(qǐng)的命令
messageTypeName = "RefreshApply"
} else if(messageType == 52){
// 好友接受的命令
messageTypeName = "RefreshContact"
} else if(messageType == 53){
// 被踢出群
messageTypeName = "GroupRemove"
} else if(messageType == 54){
// 被禁言
messageTypeName = "GroupBanned"
} else if(messageType == 55){
// 被解禁
messageTypeName = "GroupBeLifted"
} else if(messageType == 56){
// 被踢出會(huì)議房間
messageTypeName = "GroupKick"
} else if(messageType == 57){
// 面對(duì)面建群,加入群聊前,進(jìn)入房間時(shí)刷新列表用
messageTypeName = "RefreshContact"
} else if(messageType == 58){
// 通知
messageTypeName = "Push"
}
return messageTypeName;
}
// 在bufferMessage 前面加上 一個(gè)字節(jié), 表示消息的類型, 方便客戶端取用, 辨識(shí)是哪種消息類型
function bufferMessageAddType(type, buffer){
/**
* Uint8Array是JavaScript中的一種類型化數(shù)組莹汤。
* 它提供了一種用于表示8位無(wú)符號(hào)整數(shù)的固定長(zhǎng)度的數(shù)組,
* 可以讓你更輕松荒勇,更高效地操作二進(jìn)制數(shù)據(jù)
*/
// 創(chuàng)建一個(gè) 1 + buffer.length長(zhǎng)度的數(shù)組
let array = new Uint8Array(1 + buffer.byteLength)
// 該方法允許你通過一個(gè)子數(shù)組來(lái)填充當(dāng)前數(shù)組的一部分
array.set(new Uint8Array([type]), 0)
array.set(new Uint8Array(buffer), 1)
// 注意 vue中使用 arraybuffer, 而nodejs中需要使用buffer, 因?yàn)榈讓硬煌耆嗤? let arrayBuffer = array.buffer;
return arrayBuffer;
}
// 如果服務(wù)器端有消息確認(rèn), 可以根據(jù)消息確認(rèn), 添加消息是否發(fā)送成功的狀態(tài),
// 需要單獨(dú)創(chuàng)建一個(gè)數(shù)組, 用來(lái)存放發(fā)送中的數(shù)據(jù)(包含發(fā)送失敗的數(shù)據(jù))
module.exports = {
connectWebsocket,
closeWebsocket,
sendMessage
}
chat.vue 聊天頁(yè)面
<template>
<div class="chat-page-box">
<div class="chat-page-header">聊天</div>
<div class="chat-page-content" id="chat-page-content" @click="clickContentPart">
<div v-for="(message, index) in messageArray" :key="index">
<div :class="index % 2 == 0 ? 'message-left-cell':'message-right-cell'">
<div class="message-cell-portrait-part">
<img class="message-cell-portrait" src="" alt="">
</div>
<div class="message-cell-content-part">
<div class="message-cell-name">
<span>name</span>
</div>
<div class="message-cell-content">
<!-- <img class="message-cell-bubble" src="@/assets/images/icon_session_bubble_right.png" alt=""> -->
<div class="me_message_content_icon"></div>
<div class="message-cell-content-text">
<div>{{message.text}}</div>
</div>
</div>
</div>
</div>
<div class="message-left-cell">
<div></div>
<!-- <div>{{message.text}}</div> -->
</div>
</div>
</div>
<div class="chat-page-bottom">
<div class="chat-bottom-part-text">
<div class="chat-bottom-part-voice">
<img class="vocice-icon" src="@/assets/images/icon_session_voice.png" alt="">
</div>
<div class="chat-bottom-part-textview">
<el-input
ref="getfocus"
class="chat-bottom-part-textfield"
v-model="message"
placeholder="請(qǐng)輸入內(nèi)容"
@blur="blurAction"
@keyup.enter.native="enterAction"
@focus="textFocusAction"
></el-input>
</div>
<div class="chat-bottom-part-add" @click="addAction">
<img class="add-icon" src="@/assets/images/icon_session_add.png" alt="">
</div>
</div>
<div class="chat-bottom-part-tool" :style="{height: toolHeight + 'px'}" v-if="toolHeight">
<div class="chat-bottom-part-tool-item">圖片</div>
</div>
</div>
<!-- <div class="row">
<span class="title">姓名:</span>
<el-input v-model="name" placeholder="請(qǐng)輸入內(nèi)容"></el-input>
</div>
<div class="row">
<span class="title">消息:</span>
<el-input v-model="message" placeholder="請(qǐng)輸入內(nèi)容"></el-input>
</div> -->
<!-- <span class="button" @click="sendMessage">發(fā)送</span> -->
</div>
</template>
<script>
import {closeWebsocket, sendMessage} from "@/manager/webSocketManager"
// const WebSocket = require("websocket");
// const ws = new WebSocket("ws://192.168.0.252:8091")
// // 長(zhǎng)連接websocket
// ws.onopen = function () {
// ws.send(JSON.stringify({
// username: '連接成功',
// mes: ''
// }))
// console.log("websocket連接成功")
// }
// ws.onmessage = function (data) {
// console.log("接收到消息", JSON.parse(data.data))
// // localChat.push(JSON.parse(data.data))
// }
// ws.onclose = function(res){
// console.log("連接關(guān)閉", res)
// }
// ws.onerror = function(res){
// console.log("連接出錯(cuò)", res)
// }
export default {
data() {
return {
name:"",
message:"",
toolHeight: 0,
keyBoardHeight: 0,
messageArray: [
{
sessionId: "1234567890",
sender: "小明",
mid: "100000",
type: 1,
text: "你在干嘛呢, 知道了么你在干嘛呢, 知道了么你在干嘛呢, 知道了么你在干嘛呢, 知道了么你在干嘛呢, 知道了么",
uid: "1234567890"
},
{
sessionId: "1234567890",
sender: "小明",
mid: "100000",
type: 1,
text: "你在干嘛呢, 知道了么",
uid: "1234567890"
}
]
}
},
created() {
window.addEventListener("receivedNewMessage", this.receviedMessage)
},
mounted(){
window.addEventListener("keyboardWillShow", this.onKeyBoardShow)
},
beforeDestroy(){
window.removeEventListener("keyboardWillShow", this.onKeyBoardShow)
},
methods: {
sendMessage(){
console.log("點(diǎn)擊了發(fā)送消息")
let message = {
sessionId: "1234567890",
sender: "小明",
mid: "100000",
type: 1,
text: this.message,
uid: "1234567890"
};
this.message = "";
sendMessage(message)
this.messageArray.push(message)
this.scrollToBottom()
},
receviedMessage(event){
let message = event.detail;
console.log("xxxxx", event.detail)
this.messageArray.push(event.detail)
this.scrollToBottom()
// console.log(this.messageArray)
},
// 建立長(zhǎng)連接
longConnection() {
console.log("點(diǎn)擊了關(guān)閉長(zhǎng)連接")
// connectWebsocket();
closeWebsocket()
},
// 獲得焦點(diǎn)
textFocusAction(){
this.toolHeight = 0;
},
// 失去焦點(diǎn)
blurAction(event){
// console.log("dd", event)
},
// 點(diǎn)擊了enter鍵
enterAction(value){
this.sendMessage();
},
onKeyBoardShow(event){
console.log(event.height);
},
// 發(fā)消息(收消息)后自動(dòng)滑動(dòng)到底部
scrollToBottom() {
// const container = document.getElementById('chat-page-content'); // 替換為你的容器元素ID
// container.scrollIntoView(false);
this.$nextTick(() => {
var container = this.$el.querySelector("#chat-page-content");
container.scrollTop = container.scrollHeight;
});
},
addAction(){
if(this.toolHeight){
// 自動(dòng)獲取輸入框的焦點(diǎn)
// this.$nextTick(() => {
// this.$refs.getfocus.focus();
// })
this.toolHeight = 0;
} else {
this.$nextTick(() => {
if(this.keyBoardHeight){
this.toolHeight = 260;
} else {
this.toolHeight = 260;
}
})
}
},
clickContentPart(){
this.toolHeight = 0;
}
}
}
</script>
<style lang="scss">
.chat-page-box{
background: #f5f5f5;
overflow: hidden;
height: 100%;
.chat-page-header{
position: absolute;
top: 0px;
left: 0px;
right: 0px;
height: 50px;
font-size: 18px;
line-height: 50px;
color: #4a4a4a;
background: #fff;
text-align: center;
// background: rgb(98, 98, 240);
}
.chat-page-content{
position: absolute;
top: 50px;
left: 0px;
right: 0px;
bottom: 50px;
background: #f5f5f5;
padding: 0 10px;
overflow: scroll;
.message-left-cell{
margin-top: 10px;
display: flex;
padding-right: 60px;
.message-cell-portrait-part{
.message-cell-portrait{
flex-shrink: 0;
width: 40px;
height: 40px;
background: #f5f5f5;
border-radius: 20px;
}
}
.message-cell-content-part{
margin-left: 10px;
display: flex;
flex-direction: column;
.message-cell-name{
margin-left: 5px;
line-height: 20px;
}
.message-cell-content{
position: relative;
display: flex;
.message-cell-bubble{
position: absolute;
z-index: 1;
height: 100%;
width: 100%;
}
.message-cell-content-text{
margin-left: 5px;
padding: 10px 7px;
background: #ffffff;
border-radius: 4px;
color: #4a4a4a;
word-wrap: break-word;
word-break: break-all;
}
.me_message_content_icon {
width: 0;
height: 0;
border-right: 6px solid #ffffff;
border-bottom: 6px solid transparent;
border-top: 6px solid transparent;
position: absolute;
// right: -5px;
left: 0px;
top: 12px;
}
}
}
}
.message-right-cell{
margin-top: 10px;
padding-left: 60px;
display: flex;
flex-direction: row-reverse;
.message-cell-portrait-part{
.message-cell-portrait{
flex-shrink: 0;
width: 40px;
height: 40px;
background: #f5f5f5;
border-radius: 20px;
}
}
.message-cell-content-part{
margin-right: 10px;
display: flex;
flex-direction: column;
.message-cell-name{
display: none;
}
.message-cell-content{
position: relative;
display: flex;
.message-cell-content-text{
// 如果不設(shè)置次代碼, z-index設(shè)置無(wú)效
position: relative;
z-index: 2;
margin-right: 5px;
padding: 10px 7px;
background: #be3468;
border-radius: 4px;
color: #ffffff;
word-wrap: break-word;
word-break: break-all;
}
.me_message_content_icon {
width: 0;
height: 0;
border-left: 6px solid #be3468;
border-bottom: 6px solid transparent;
border-top: 6px solid transparent;
position: absolute;
// right: -5px;
right: 0px;
top: 12px;
// margin-right: 10px;
}
}
}
}
}
.chat-page-bottom{
position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
// height: 260px;
background: #f5f5f5;
// background: rgb(98, 98, 240);
border-top: 1px solid #d1d1d1;
.chat-bottom-part-text{
display: flex;
align-items: center;
justify-content: space-between;
height: 50px;
background: #fafafa;
.chat-bottom-part-voice{
height: 50px;
width: 50px;
.vocice-icon{
width: 28px;
height: 28px;
margin-left: 15px;
margin-top: 11px;
}
}
.chat-bottom-part-textview{
flex: 1;
height: 34px;
border: 0.5px solid #d1d1d1;
border-radius: 4px;
.chat-bottom-part-textfield{
width: 100%;
// height: 32px;
}
.el-input__inner{
height: 34px;
line-height: 34px;
}
}
.chat-bottom-part-add{
height: 50px;
width: 50px;
.add-icon{
width: 28px;
height: 28px;
margin-left: 7px;
margin-top: 11px;
}
}
}
.chat-bottom-part-tool{
display: flex;
.chat-bottom-part-tool-item{
width: 60px;
height: 60px;
margin-top: 20px;
margin-left: 20px;
text-align: center;
border: 0.5px solid #d1d1d1;
border-radius: 8px;
line-height: 60px;
}
}
}
.button{
padding: 5px 10px;
background: #00f;
cursor: pointer;
margin-top: 20px;
width: auto;
display: inline-block;
color: white;
border-radius: 4px;
}
.row {
display: flex;
margin-top: 20px;
align-items: center;
}
.title {
flex-shrink: 0;
}
}
</style>
main.js中
import {connectWebsocket} from "@/manager/webSocketManager"
connectWebsocket();
``
nodejs代碼
const ws = require("nodejs-websocket");
//定義一個(gè)對(duì)象痢艺,用于存放正在連接中的socket, 字段名是以token命名
const conningObject = {};
// 獲取protobuf 的root
let protobuRoot = require("protobufjs").Root;
// 獲取定義的protobuf文件的對(duì)象json
let protoJson = require("./proto.js");
let messageRoot = protobuRoot.fromJSON(protoJson);
// lookupType根據(jù)傳入的字符傳, 獲取對(duì)應(yīng)的消息類型對(duì)象(用于創(chuàng)建對(duì)應(yīng)的消息)
// 例如傳入proto.js中的Ack 表明獲取ack類型對(duì)象, 用于創(chuàng)建ack類型的消息
// let messageTypeObject = messageRoot.lookupType("Ack");
// console.log("消息類型");
// console.log(messageTypeObject);
// creatProtobufMessage("Ack", {mid: 123456, uid: "qweeer", sessionId: "fddddd"})
let webServe = ws.createServer(function (connection) {
// console.log('創(chuàng)建成功', connection)
//連接成功的回調(diào)
// 獲取連接的token
let path = connection.path;
let pathParams = getParamsFromURL(path);
console.log(pathParams);
// 不滿足服務(wù)器條件時(shí)服務(wù)器主動(dòng)斷開連接
if(pathParams.token) {
// 如果token正確進(jìn)行繼續(xù)操作
// 如果是第一次連接, 添加到對(duì)應(yīng)的數(shù)組, 如果不是, 不用繼續(xù)添加
if (!conningObject[pathParams.token]) {
console.log("添加connect");
//將用戶發(fā)來(lái)的信息對(duì)所有用戶發(fā)一遍
conningObject[pathParams.token] = connection;
// console.log(conningObject)
// console.log(conningObject.keys())
}
//監(jiān)聽數(shù)據(jù)色建,當(dāng)客戶端傳來(lái)數(shù)據(jù)時(shí)的操作
// 監(jiān)聽收到的數(shù)據(jù), 如果發(fā)送的是字符串在這個(gè)方法中響應(yīng)
connection.on("text", function (data) {
console.log('接受到字符串類型消息', data);
// 解析數(shù)據(jù)
// 發(fā)送確認(rèn)消息
})
// 監(jiān)聽收到的數(shù)據(jù), 如果發(fā)送的是二進(jìn)制數(shù)據(jù)在這個(gè)方法中響應(yīng)
connection.on("binary", function (inStream) {
// console.log('接受到二進(jìn)制類型消息', inStream)
// console.log(result)
// Empty buffer for collecting binary data
// 定義一塊buffer內(nèi)存空間用來(lái)存放接受到的二進(jìn)制文件
var buffer = Buffer.alloc(0)
// Read chunks of binary data and add to the buffer
inStream.on("readable", function () {
// 因?yàn)槎M(jìn)制文件時(shí)分段發(fā)送的, 不是一次性發(fā)送的, 所以這里進(jìn)行拼接
var newData = inStream.read()
if (newData){
// 將接受到的二進(jìn)制文件拼接到buffer空間內(nèi)
buffer = Buffer.concat([buffer, newData], buffer.length+newData.length)
}
})
inStream.on("end", function () {
// console.log("Received " + buffer.length + " bytes of binary buffer")
// console.log(buffer);
// 接受二進(jìn)制文件完成, 將二進(jìn)制數(shù)據(jù)進(jìn)行解析
getMessageFromMessageBuffer(buffer, (message, messageType) => {
// 如果用戶發(fā)送的不是確認(rèn)消息, 則立即向客戶端發(fā)送確認(rèn)消息
if(messageType == 2){
// 收到的是確認(rèn)消息
} else {
// 收到消息后要立即向客戶端發(fā)送確認(rèn)消息
let protobufMessage = creatProtobufMessage({
mid: message.mid,
uid: message.uid,
sessionId: message.sessionId
}, "2")
//
// console.log("要發(fā)送的消息", protobufMessage)
// 發(fā)送確認(rèn)消息buffer的方法
connection.send(protobufMessage);
// 將處理后的消息進(jìn)行發(fā)送通知, 通知給需要的頁(yè)面進(jìn)行處理, 在需要的頁(yè)面進(jìn)行監(jiān)聽
if(messageType == 1){
// 1是即時(shí)消息, 發(fā)送給對(duì)應(yīng)的聊天對(duì)象
// 獲取sessionId
let sessionId = message.sessionId;
// 將消息存到數(shù)據(jù)庫(kù)
// 根據(jù)sessionId獲取會(huì)話中的人員信息(這個(gè)過程需要去除本人)
let usersInfo = [{id:"1"}, {id:"2"}];
// 根據(jù)userId獲取對(duì)應(yīng)人員目前的token
let tokens = ["123456", "654321"];
// 根據(jù)token查詢當(dāng)前websocket連接中有沒有對(duì)應(yīng)的人員,(本人除外)
// 如果有對(duì)應(yīng)連接, 將消息發(fā)送給對(duì)應(yīng)人員,
// 如果沒有對(duì)應(yīng)連接, 發(fā)送推送, 并記錄此消息為離線消息, 下次用戶連接時(shí), 直接發(fā)送過去
let connectKeys = Object.keys(conningObject);
console.log(connectKeys);
tokens.forEach(token => {
connectKeys.every(key => {
if(token == key){
//發(fā)送消息
console.log(message)
conningObject[key].send(buffer);
return false;
}
});
});
} else if(messageType == 3){
// 3是客戶端向服務(wù)器發(fā)送了消息同步的指令,
} else if(messageType == 4){
// 4是離線推送消息
// 客戶端不會(huì)發(fā)送此類消息, 是服務(wù)器向客戶端發(fā)送的消息
} else if(messageType == 51){
// 好有申請(qǐng)
// 發(fā)送的對(duì)應(yīng)的人員
} else if(messageType == 52){
// 好友接受申請(qǐng)
// 發(fā)送的對(duì)應(yīng)的人員
} else if(messageType == 53){
// 踢出群
// 發(fā)送的對(duì)應(yīng)的人員
} else if(messageType == 54){
// 禁言
// 發(fā)送的對(duì)應(yīng)的人員
}else if(messageType == 58){
// 通知
// 客戶端不會(huì)發(fā)送此類消息, 是服務(wù)器向客戶端發(fā)送的消息
}
}
})
})
})
connection.on('connect', function(code) {
console.log('開啟連接', code)
})
connection.on('close', function(code, reason) {
console.log('關(guān)閉連接', code)
console.log('關(guān)閉原因:', reason)
// console.log(conningObject);
// 獲取連接的token
let path = connection.path;
let pathParams = getParamsFromURL(path);
console.log(pathParams.token);
// 連接關(guān)閉時(shí)要將這個(gè)連接從連接對(duì)象中移除
delete conningObject[pathParams.token]
// console.log(conningObject);
})
connection.on('error', function(code) {
console.log('異常關(guān)閉', code)
})
} else {
// 如果token 不合理就進(jìn)行斷開
console.log('token不正確')
connection.close(1, "token不正確");
}
});
webServe.listen(8091);
webServe.on('connection', (connection) => {
console.log("客戶端進(jìn)行連接");
// console.log("客戶端進(jìn)行連接", connection);
})
function getMessageFromMessageBuffer(messageBuffer, result){
// ArrayBuffer 對(duì)象代表儲(chǔ)存二進(jìn)制數(shù)據(jù)的一段內(nèi)存
// Uint8Array 對(duì)象是 ArrayBuffer 的一個(gè)數(shù)據(jù)類型(8 位不帶符號(hào)整數(shù))
// 獲取消息的buffer
let buffer = new Uint8Array(messageBuffer);
// console.log("111111111")
// console.log(messageBuffer)
// console.log(buffer)
// console.log("111111111")
// 獲取消息類型, 第一個(gè)字節(jié)
let messageType = buffer[0];
// 獲取對(duì)應(yīng)消息類的名字, 默認(rèn)確認(rèn)空
let messageTypeName = getMessageTypeName(messageType);
// 獲取對(duì)應(yīng)protobuf消息類型
let protobufTypeObject = getProtobufTypeObject(messageTypeName);
// 獲取消息內(nèi)容buffer
let bufferMessageContent = buffer.subarray(1);
// 將消息內(nèi)容buffer進(jìn)行解碼, 得到具體消息
let message = protobufTypeObject.decode(bufferMessageContent);
// 消息內(nèi)容為
// console.log("消息內(nèi)容為")
// console.log(message)
result(message, messageType);
// 讀取成功的回調(diào)
//1-實(shí)時(shí)消息 2-確認(rèn)收到消息 //4-有未讀消息
// if(messageType == 1 || messageType == 4){
// } else if(messageType == 2){
// } else if(messageType == 58){
// }
}
// 根據(jù)messageType獲取(將消息類型轉(zhuǎn)換為protobuf消息的毒性)對(duì)應(yīng)的消息類型對(duì)象
// messageType 消息類型, 例如 "Ack", 在proto.js中可以找到
function getProtobufTypeObject(messageTypeName){
// 根據(jù)messageType獲取(將消息類型轉(zhuǎn)換為protobuf消息的對(duì)象)對(duì)應(yīng)的消息類型對(duì)象
let protobufTypeObject = messageRoot.lookupType(messageTypeName);
return protobufTypeObject;
}
// 創(chuàng)建protobuf消息, 將json消息轉(zhuǎn)換為對(duì)應(yīng)的protobuf消息
function creatProtobufMessage(message, messageType){
// 獲取對(duì)應(yīng)消息類的名字, 默認(rèn)確認(rèn)空
let messageTypeName = getMessageTypeName(messageType);
// 獲取對(duì)應(yīng)protobuf消息類型
let protobufTypeObject = getProtobufTypeObject(messageTypeName);
// 創(chuàng)建消息, 最后還需要添加一個(gè)字符表示消息類型
let protobufMessageContent = protobufTypeObject.create(message);
// 將消息進(jìn)行編碼
let encodeProtobufMessageContent = protobufTypeObject.encode(protobufMessageContent)
// 消息轉(zhuǎn)換完成
let bufferMessageContent = encodeProtobufMessageContent.finish();
// console.log("11111111")
// console.log("2222222", encodeProtobufMessageContent)
// console.log("333333", bufferMessageContent)
// 完整的proto信息, 添加了頭部消息樂行
let protobufMessage = bufferMessageAddType(messageType, bufferMessageContent);
return protobufMessage;
}
function getMessageTypeName(messageType){
let messageTypeName = "";
if(messageType == 1){
// 新消息
messageTypeName = "ChatMsg"
} else if(messageType == 2){
// 確認(rèn)消息
messageTypeName = "Ack"
} else if(messageType == 3){
// 同步消息
messageTypeName = "ChatMsgList"
} else if(messageType == 4){
// 離線推送消息
messageTypeName = "ChatMsgList"
} else if(messageType == 51){
// 好友申請(qǐng)的命令
messageTypeName = "RefreshApply"
} else if(messageType == 52){
// 好友接受的命令
messageTypeName = "RefreshContact"
} else if(messageType == 53){
// 被踢出群
messageTypeName = "GroupRemove"
} else if(messageType == 54){
// 被禁言
messageTypeName = "GroupBanned"
} else if(messageType == 55){
// 被解禁
messageTypeName = "GroupBeLifted"
} else if(messageType == 56){
// 被踢出會(huì)議房間
messageTypeName = "GroupKick"
} else if(messageType == 57){
// 面對(duì)面建群,加入群聊前,進(jìn)入房間時(shí)刷新列表用
messageTypeName = "RefreshContact"
} else if(messageType == 58){
// 通知
messageTypeName = "Push"
}
return messageTypeName;
}
// 在bufferMessage 前面加上 一個(gè)字節(jié), 表示消息的類型, 方便客戶端取用, 辨識(shí)是哪種消息類型
function bufferMessageAddType(type, buffer){
/**
- Uint8Array是JavaScript中的一種類型化數(shù)組壮虫。
- 它提供了一種用于表示8位無(wú)符號(hào)整數(shù)的固定長(zhǎng)度的數(shù)組耻瑟,
- 可以讓你更輕松,更高效地操作二進(jìn)制數(shù)據(jù)
*/
// 創(chuàng)建一個(gè) 1 + buffer.length長(zhǎng)度的數(shù)組
let array = new Uint8Array(1 + buffer.byteLength)
// 該方法允許你通過一個(gè)子數(shù)組來(lái)填充當(dāng)前數(shù)組的一部分
array.set(new Uint8Array([type]), 0)
array.set(new Uint8Array(buffer), 1)
let arrayBuffer = array.buffer;
// 將arraybuffer 轉(zhuǎn)換為buffer
let messageBuffer = Buffer.from(arrayBuffer)
// 注意 vue中使用 arraybuffer, 而nodejs中需要使用buffer, 因?yàn)榈讓硬煌耆嗤?br>
return messageBuffer;
}
// 獲取url上的參數(shù), 使用的是正則表達(dá)式
function getParamsFromURL(url) {
const regex = /?&=([^&#]*)/g;
const params = {};
let match;
while (match = regex.exec(url)) {
params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
}
return params;
}