vue, node使用websocket和protobuf結(jié)合實(shí)現(xiàn)聊天

該功能使用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;
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市呵晨,隨后出現(xiàn)的幾起案子摸屠,更是在濱河造成了極大的恐慌粱哼,老刑警劉巖揭措,帶你破解...
    沈念sama閱讀 218,941評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绊含,死亡現(xiàn)場(chǎng)離奇詭異躬充,居然都是意外死亡充甚,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)眉反,“玉大人寸五,你說我怎么就攤上這事耿币。” “怎么了叛溢?”我有些...
    開封第一講書人閱讀 165,345評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵楷掉,是天一觀的道長(zhǎng)烹植。 經(jīng)常有香客問我草雕,道長(zhǎng)墩虹,這世上最難降的妖魔是什么嘴纺? 我笑而不...
    開封第一講書人閱讀 58,851評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮闲擦,結(jié)果婚禮上场梆,老公的妹妹穿的比我還像新娘或油。我一直安慰自己顶岸,他們只是感情好辖佣,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,868評(píng)論 6 392
  • 文/花漫 我一把揭開白布卷谈。 她就那樣靜靜地躺著,像睡著了一般朗兵。 火紅的嫁衣襯著肌膚如雪顶滩。 梳的紋絲不亂的頭發(fā)上诲祸,一...
    開封第一講書人閱讀 51,688評(píng)論 1 305
  • 那天找田,我揣著相機(jī)與錄音着憨,去河邊找鬼甲抖。 笑死准谚,一個(gè)胖子當(dāng)著我的面吹牛柱衔,可吹牛的內(nèi)容都是我干的唆铐。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼脆炎!你這毒婦竟也來(lái)了叼耙?” 一聲冷哼從身側(cè)響起筛婉,我...
    開封第一講書人閱讀 39,319評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤入蛆,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后枫甲,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體想幻,經(jīng)...
    沈念sama閱讀 45,775評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡脏毯,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了幔崖。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片食店。...
    茶點(diǎn)故事閱讀 40,096評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖赏寇,靈堂內(nèi)的尸體忽然破棺而出吉嫩,到底是詐尸還是另有隱情,我是刑警寧澤嗅定,帶...
    沈念sama閱讀 35,789評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站渠退,受9級(jí)特大地震影響椒功,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜智什,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,437評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望丁屎。 院中可真熱鬧荠锭,春花似錦、人聲如沸晨川。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)共虑。三九已至愧怜,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間妈拌,已是汗流浹背拥坛。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工蓬蝶, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人猜惋。 一個(gè)月前我還...
    沈念sama閱讀 48,308評(píng)論 3 372
  • 正文 我出身青樓丸氛,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親著摔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子缓窜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,037評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容