vue, node使用websocket和protobuf结合实现聊天

该功能使用nodejs 写后台, vue写前端, 利用websoket作为长连接, protobuf作为数据格式传输数据实现了简单的聊天, 其中node是使用了nodejs-websocket作为三方库
直接上代码
vue代码
webSocketManager.js 自定义的工具类

// 获取protobuf 的root
let protobuRoot = require("protobufjs").Root;
// 获取定义的protobuf文件的对象json
let protoJson = require("../utils/proto");
let messageRoot = protobuRoot.fromJSON(protoJson);
// 定义websocket 地址
let socketurl = "ws://192.168.0.252:8091";
// 重连锁, 防止过多重连
let reconnectLock = false;
// 定义一个消息发送中(包含发送失败的)的字典
window.messageSendingDic = {};
// 定义一个消息websocket连接状态的字段, 并且要绑定到widow上, 方便调用
// 0 未连接, 1 连接成功 2 连接中
window.webSocketState = 0;

// 定义连接服务器方法
function connectWebsocket(){
    //如果用户已登录, 进行连接websoket, 如果没有登陆, 登录后进行连接 用token判断
    // 创建一个websocket连接
    // let webSocket = new WebSocket(socketurl);
    
    // 如果想要传token, 因为ws不支持通过设置header, 所以直接在地址中加参数, 
    // 如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"]);
    // 监听webSocket的各个状态
    // 连接成功
    webSocket.onopen = function() {
        console.log("websocket连接成功")
        // 连接成功后将连接状态改变
        window.webSocketState = 1;
        // 连接成功后, 要将消息队列里面的消息重新发送出去(底层重发, 和页面无关)
        for(let session in window.messageSendingDic){
            session.forEach(message => {
                // 重发消息
                reSendMessage(message)
            });
       }
    }
    // 连接出错
    webSocket.onerror = function(error){
        console.log("websocket连接出错", error);
        console.log("websocket连接出错", error.data);
        // 进行重连
        reconnectWebsocket();
    }
    // 连接关闭
    webSocket.onclose = function(result){
        console.log("websocket连接关闭", result);
        if(result == "退出登录"){
            return
        }
        // 进行重连
        reconnectWebsocket();
    }
    // 接受到消息
    webSocket.onmessage = function(message){
        // console.log("websocket接受到消息", message);
        // 将受到的消息进行分类, 分发处理
        formatAcceptMessage(message)
    } 
    // 将webSocket绑定到window上面, 方便后续调用
    window.webSocket = webSocket;
}

// 定义重连方法 如果连接失败, 或者关闭, 
function reconnectWebsocket(){
    // 如果正在重连, 则返回
    if(reconnectLock){
        return;
    }
    // 进行加锁
    reconnectLock = true;
    // 重连时将连接状态改变
    window.webSocketState = 2;
    // 为了防止过多请求, 1s后进行重连
    setTimeout(function(){
        // 解锁
        reconnectLock = false;
        // 进行连接, 如果失败接着重连
        // connectWebsocket();
    }, 1000)

}

/**
 * 关闭websocket 退出时会用到
 *
 */
function closeWebsocket(){
    window.webSocket.onclose("退出登录")
}

// 定义发送消息的方法 message 格式为json
/**
 * 
 * @param {
 *      message: "内容",
 *      id: "xxxxxxx"
 * } message 消息内容
 * @param "1" messageType 消息类型
 * @param "QueryMsg" messageClass 附加字段吗消息类, 这里是以protobufjs的消息类为例
 */
function sendMessage(message, messageType) {
    // 这里可以对message做一些格式化处理
    // let formaterMessge = message;
    // 如果没有传递messageType, 则默认为即时消息
    if(!messageType){
        messageType = 1;
    }

    // 如果发送的消息为即时消息, 要记录消息的发送状态
    if(messageType == 1){
        // 将消息添加到发送中的数组中进行记录
        // 先判断该回话有没有对应的数组, 如果没有就创建, 在添加, 如果有直接添加
        if(window.messageSendingDic[message.sessionId]) {
            window.messageSendingDic[message.sessionId].push(message);
        } else {
            window.messageSendingDic[message.sessionId] = [];
            window.messageSendingDic[message.sessionId].push(message);
        }
    }
    
    // 如果websocket连接成功, 进行发送消息
    if(window.webSocketState == 1) {
        // formaterMessge = JSON.stringify(formaterMessge)

        let bufferMessage = creatBufferMessage(message, messageType)
          // 
          console.log("要发送的消息", message, messageType)
        // 这里就可以直接用window调用了
        window.webSocket.send(bufferMessage);
    } else {
        // 如果websocket没有连接成功, 直接告诉消息发送页面消息发送失败, 模拟接受到消息, 发给对应页面
        let formaterMessge = {};
        // 将处理后的消息进行发送通知, 通知给需要的页面进行处理, 在需要的页面进行监听 
        // 注意: 使用页面添加window.addEventListener("receivedNewMessage", this.testAction)
        window.dispatchEvent(new CustomEvent("receivedNewMessage", message));
    }
}

// 定义重发送消息的方法 message 格式为json
/**
 * 
 * @param {
*      message: "内容",
*      id: "xxxxxxx"
* } message 消息内容
* @param "1" messageType 消息类型
* @param "QueryMsg" messageClass 附加字段吗消息类, 这里是以protobufjs的消息类为例
*/
function reSendMessage(message) {
   // 如果websocket连接成功, 进行发送消息
   if(window.webSocketState == 1) {
       // 这里就可以直接用window调用了
       window.webSocket.send(message);
   } 
}

// 定义收到消息进行消息解析的方法
function formatAcceptMessage(message) {
    // 处理消息. 格式化, 获取消息的blob数据
    let bufferMessage = message.data;
    // 将buffer数据解析为json消息
    getMessageFromBufferMessage(bufferMessage, (message, messageType) => {
        console.log("接受到的消息")
        console.log(message, messageType)
        // 除了是服务器发送的确认消息外, 都应该向服务器发送确认消息
        if(messageType == 2){
            // 2是确认消息, 收到服务器发送的确认消息后, 说明消息发送成功
            // 将发送成功的消息从发送中移除
            if(window.messageSendingDic[message.sessionId]) {
                let sendingArray = window.messageSendingDic[message.sessionId];
                // 过滤发送成功的
                window.messageSendingDic[message.sessionId] = sendingArray.filter(msg => {
                    return msg.id != message.id
                });
            } 
        } else {
            // 向服务器发送确认消息
            // 创建确认消息
            let ackMessage = {
                mid: message.mid, 
                uid: message.uid, 
                sessionId: message.sessionId
            }
            // 发送确认消息
            sendMessage(ackMessage, "2")

            // 将处理后的消息进行发送通知, 通知给需要的页面进行处理, 在需要的页面进行监听 
            if(messageType == 1){
                // 1是即时消息, 发送给聊天页面和聊天列表页, 去刷新页面信息
                // 注意: 使用页面添加window.addEventListener("receivedNewMessage", this.testAction)
                window.dispatchEvent(new CustomEvent("receivedNewMessage", {detail: message}));

            } else if(messageType == 3){
                // 3是同步消息, 
                // 这里面是数组, 注意发送给聊天页面
                message.msgsArray.forEach(element => {
                    // 注意: 使用页面添加window.addEventListener("receivedNewMessage", this.testAction)
                    window.dispatchEvent(new CustomEvent("receivedNewMessage", message));
                });

            } else if(messageType == 4){
                // 4是离线推送消息
                // 这里面是数组, 注意发送给聊天页面
                message.msgsArray.forEach(element => {
                    // 注意: 使用页面添加window.addEventListener("receivedNewMessage", this.testAction)
                    window.dispatchEvent(new CustomEvent("receivedNewMessage", message));
                });
            } else if(messageType == 51){
                // 好有申请
                // 注意: 使用页面添加window.addEventListener("receivedApplyFriendMessage", this.testAction)
                window.dispatchEvent(new CustomEvent("receivedApplyFriendMessage", message));
            } else if(messageType == 52){
                // 好友接受申请
                // 注意: 使用页面添加window.addEventListener("receivedAcceptFriendMessage", this.testAction)
                window.dispatchEvent(new CustomEvent("receivedAcceptFriendMessage", message));
            } else if(messageType == 53){
                // 被踢出群
                // 注意: 使用页面添加window.addEventListener("receivedKickedOutMessage", this.testAction)
                window.dispatchEvent(new CustomEvent("receivedKickedOutMessage", message));
            } else if(messageType == 54){
                // 被禁言
                // 注意: 使用页面添加window.addEventListener("receivedBannedSpeakMessage", this.testAction)
                window.dispatchEvent(new CustomEvent("receivedBannedSpeakMessage", message));
            }else if(messageType == 58){
                // 通知
                // 注意: 使用页面添加window.addEventListener("receivedNotificationMessage", this.testAction)
                window.dispatchEvent(new CustomEvent("receivedNotificationMessage", message));
            }
            // 注意: 使用页面添加window.addEventListener("acceptNewMessage", this.testAction)
            window.dispatchEvent(new CustomEvent("acceptNewMessage", message));
        }
        
        
    });
   
}

// 将buffer二进制数据转换为json数据
function getMessageFromBufferMessage(bufferMessage, result){
    // 创建一个文件读取器 
    let reader = new FileReader();
    // 将消息读取为arrayBuffer类型
    reader.readAsArrayBuffer(bufferMessage);
    // 读取成功的回调
    reader.onload = function() {
      // 获取消息的buffer
      let buffer = new Uint8Array(reader.result);
      // 获取消息类型, 第一个字节
      let messageType = buffer[0];
      // 获取对应消息类的名字, 默认确认空
      let messageTypeName = getMessageTypeName(messageType);
      // 获取对应protobuf消息类型
      let protobufTypeObject = getProtobufTypeObject(messageTypeName);
      // 获取消息内容buffer
      let bufferMessageContent = buffer.subarray(1);
      // 将消息内容buffer进行解码, 得到具体消息
      let message = protobufTypeObject.decode(bufferMessageContent);  
      result(message, messageType);
    }
}
// 根据messageType获取(将消息类型转换为protobuf消息的毒性)对应的消息类型对象 
// messageType 消息类型, 例如 "Ack", 在proto.js中可以找到
function getProtobufTypeObject(messageTypeName){
// 根据messageType获取(将消息类型转换为protobuf消息的对象)对应的消息类型对象 
let protobufTypeObject = messageRoot.lookupType(messageTypeName);
return protobufTypeObject;
}
// 创建protobuf消息, 将json消息转换为对应的protobuf消息
function creatBufferMessage(message, messageType){
// 获取对应消息类的名字, 默认确认空
let messageTypeName = getMessageTypeName(messageType);
// 获取对应protobuf消息类型
let protobufTypeObject = getProtobufTypeObject(messageTypeName);
// 创建消息, 最后还需要添加一个字符表示消息类型
let protobufMessageContent = protobufTypeObject.create(message);
// 将消息进行编码
let encodeProtobufMessageContent = protobufTypeObject.encode(protobufMessageContent)
// 消息转换完成
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){
    // 确认消息
    messageTypeName = "Ack"
}  else if(messageType == 3){
    // 同步消息
    messageTypeName = "ChatMsgList"
} else if(messageType == 4){
    // 离线推送消息
    messageTypeName = "ChatMsgList"
} else if(messageType == 51){
    // 好友申请的命令
    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){
    // 被踢出会议房间
    messageTypeName = "GroupKick"
} else if(messageType == 57){
    // 面对面建群,加入群聊前,进入房间时刷新列表用
    messageTypeName = "RefreshContact"
} else if(messageType == 58){
    // 通知
    messageTypeName = "Push"
}

return messageTypeName;
}
// 在bufferMessage 前面加上 一个字节, 表示消息的类型, 方便客户端取用, 辨识是哪种消息类型
function bufferMessageAddType(type, buffer){
    /**
     * Uint8Array是JavaScript中的一种类型化数组。
     * 它提供了一种用于表示8位无符号整数的固定长度的数组,
     * 可以让你更轻松,更高效地操作二进制数据
     */
    // 创建一个 1 + buffer.length长度的数组
    let array = new Uint8Array(1 + buffer.byteLength)
    // 该方法允许你通过一个子数组来填充当前数组的一部分
    array.set(new Uint8Array([type]), 0)
    array.set(new Uint8Array(buffer), 1)
    // 注意 vue中使用 arraybuffer, 而nodejs中需要使用buffer, 因为底层不完全相同
    let arrayBuffer = array.buffer;
    return arrayBuffer;
}

// 如果服务器端有消息确认, 可以根据消息确认, 添加消息是否发送成功的状态, 
// 需要单独创建一个数组, 用来存放发送中的数据(包含发送失败的数据)


module.exports = {
    connectWebsocket,
    closeWebsocket,
    sendMessage
}

chat.vue 聊天页面

<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="请输入内容"
                    @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="请输入内容"></el-input>
    </div>
    <div class="row">
        <span class="title">消息:</span>
        <el-input v-model="message" placeholder="请输入内容"></el-input>
    </div> -->
    
    <!-- <span class="button" @click="sendMessage">发送</span> -->
  </div>
</template>

<script>
import {closeWebsocket, sendMessage} from "@/manager/webSocketManager"
// const WebSocket = require("websocket");
// const ws = new WebSocket("ws://192.168.0.252:8091")
// // 长连接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("连接关闭", res)
// }
// ws.onerror = function(res){
//     console.log("连接出错", 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("点击了发送消息")
            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)
        },
        // 建立长连接
        longConnection() {
            console.log("点击了关闭长连接")
            // connectWebsocket();
            closeWebsocket()
        },
        // 获得焦点
        textFocusAction(){
            this.toolHeight = 0;
        },
        // 失去焦点
        blurAction(event){
            // console.log("dd", event)
        },
        // 点击了enter键
        enterAction(value){
            this.sendMessage();
        },

        onKeyBoardShow(event){
            console.log(event.height);
        },
        // 发消息(收消息)后自动滑动到底部
        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){
                // 自动获取输入框的焦点
                // 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{
                        // 如果不设置次代码, z-index设置无效
                        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");
//定义一个对象,用于存放正在连接中的socket, 字段名是以token命名
const conningObject = {};
// 获取protobuf 的root
let protobuRoot = require("protobufjs").Root;
// 获取定义的protobuf文件的对象json
let protoJson = require("./proto.js");
let messageRoot = protobuRoot.fromJSON(protoJson);

// lookupType根据传入的字符传, 获取对应的消息类型对象(用于创建对应的消息)
// 例如传入proto.js中的Ack 表明获取ack类型对象, 用于创建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('创建成功', connection)
//连接成功的回调

// 获取连接的token
let path = connection.path;
let pathParams = getParamsFromURL(path);
console.log(pathParams);
// 不满足服务器条件时服务器主动断开连接
if(pathParams.token) {
// 如果token正确进行继续操作
// 如果是第一次连接, 添加到对应的数组, 如果不是, 不用继续添加
if (!conningObject[pathParams.token]) {
console.log("添加connect");
//将用户发来的信息对所有用户发一遍
conningObject[pathParams.token] = connection;
// console.log(conningObject)
// console.log(conningObject.keys())
}
//监听数据,当客户端传来数据时的操作
// 监听收到的数据, 如果发送的是字符串在这个方法中响应
connection.on("text", function (data) {
console.log('接受到字符串类型消息', data);
// 解析数据

  // 发送确认消息
})
// 监听收到的数据, 如果发送的是二进制数据在这个方法中响应
connection.on("binary", function (inStream) {
  // console.log('接受到二进制类型消息', inStream)
  // console.log(result)

  // Empty buffer for collecting binary data
  // 定义一块buffer内存空间用来存放接受到的二进制文件
  var buffer = Buffer.alloc(0)
  // Read chunks of binary data and add to the buffer
  inStream.on("readable", function () {
    // 因为二进制文件时分段发送的, 不是一次性发送的, 所以这里进行拼接
    var newData = inStream.read()
    if (newData){
      // 将接受到的二进制文件拼接到buffer空间内
      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);
    // 接受二进制文件完成, 将二进制数据进行解析
    getMessageFromMessageBuffer(buffer, (message, messageType) => {
      // 如果用户发送的不是确认消息, 则立即向客户端发送确认消息
      if(messageType == 2){
        // 收到的是确认消息
      } else {
        // 收到消息后要立即向客户端发送确认消息
        let protobufMessage = creatProtobufMessage({
          mid: message.mid, 
          uid: message.uid, 
          sessionId: message.sessionId
        }, "2")
        // 
        // console.log("要发送的消息", protobufMessage)
        // 发送确认消息buffer的方法
        connection.send(protobufMessage);

        // 将处理后的消息进行发送通知, 通知给需要的页面进行处理, 在需要的页面进行监听 
        if(messageType == 1){
          // 1是即时消息, 发送给对应的聊天对象
          // 获取sessionId
          let sessionId = message.sessionId;
          // 将消息存到数据库

          // 根据sessionId获取会话中的人员信息(这个过程需要去除本人)
          let usersInfo = [{id:"1"}, {id:"2"}];
          // 根据userId获取对应人员目前的token
          let tokens = ["123456", "654321"];
          // 根据token查询当前websocket连接中有没有对应的人员,(本人除外) 
          // 如果有对应连接, 将消息发送给对应人员,
          // 如果没有对应连接, 发送推送, 并记录此消息为离线消息, 下次用户连接时, 直接发送过去
          let connectKeys = Object.keys(conningObject);
          console.log(connectKeys);
          tokens.forEach(token => {
            connectKeys.every(key => {
              if(token == key){
                //发送消息
                console.log(message)
                conningObject[key].send(buffer);
                return false;
              }
            });
          });
          

        } else if(messageType == 3){
            // 3是客户端向服务器发送了消息同步的指令, 
            

        } else if(messageType == 4){
            // 4是离线推送消息
            // 客户端不会发送此类消息, 是服务器向客户端发送的消息
        } else if(messageType == 51){
            // 好有申请
            // 发送的对应的人员

        } else if(messageType == 52){
            // 好友接受申请
            // 发送的对应的人员

        } else if(messageType == 53){
            // 踢出群
            // 发送的对应的人员
        } else if(messageType == 54){
            // 禁言
            // 发送的对应的人员
        }else if(messageType == 58){
            // 通知
            // 客户端不会发送此类消息, 是服务器向客户端发送的消息
        }
      }
      
    })
  })
  
})
connection.on('connect', function(code) {
  console.log('开启连接', code)

})
connection.on('close', function(code, reason) {
  console.log('关闭连接', code)
  console.log('关闭原因:', reason)
  // console.log(conningObject);
  // 获取连接的token
  let path = connection.path;
  let pathParams = getParamsFromURL(path);
  console.log(pathParams.token);
  // 连接关闭时要将这个连接从连接对象中移除
  delete conningObject[pathParams.token]
  // console.log(conningObject);
})
connection.on('error', function(code) {
  console.log('异常关闭', code)
})

} else {
// 如果token 不合理就进行断开
console.log('token不正确')
connection.close(1, "token不正确");
}

});
webServe.listen(8091);
webServe.on('connection', (connection) => {
console.log("客户端进行连接");
// console.log("客户端进行连接", connection);
})
function getMessageFromMessageBuffer(messageBuffer, result){
// ArrayBuffer 对象代表储存二进制数据的一段内存
// Uint8Array 对象是 ArrayBuffer 的一个数据类型(8 位不带符号整数)
// 获取消息的buffer
let buffer = new Uint8Array(messageBuffer);
// console.log("111111111")
// console.log(messageBuffer)
// console.log(buffer)
// console.log("111111111")
// 获取消息类型, 第一个字节
let messageType = buffer[0];
// 获取对应消息类的名字, 默认确认空
let messageTypeName = getMessageTypeName(messageType);
// 获取对应protobuf消息类型
let protobufTypeObject = getProtobufTypeObject(messageTypeName);
// 获取消息内容buffer
let bufferMessageContent = buffer.subarray(1);
// 将消息内容buffer进行解码, 得到具体消息
let message = protobufTypeObject.decode(bufferMessageContent);
// 消息内容为
// console.log("消息内容为")
// console.log(message)
result(message, messageType);
// 读取成功的回调
//1-实时消息 2-确认收到消息 //4-有未读消息
// if(messageType == 1 || messageType == 4){

// } else if(messageType == 2){

// } else if(messageType == 58){

// }
}
// 根据messageType获取(将消息类型转换为protobuf消息的毒性)对应的消息类型对象
// messageType 消息类型, 例如 "Ack", 在proto.js中可以找到
function getProtobufTypeObject(messageTypeName){
// 根据messageType获取(将消息类型转换为protobuf消息的对象)对应的消息类型对象
let protobufTypeObject = messageRoot.lookupType(messageTypeName);
return protobufTypeObject;
}
// 创建protobuf消息, 将json消息转换为对应的protobuf消息
function creatProtobufMessage(message, messageType){
// 获取对应消息类的名字, 默认确认空
let messageTypeName = getMessageTypeName(messageType);
// 获取对应protobuf消息类型
let protobufTypeObject = getProtobufTypeObject(messageTypeName);
// 创建消息, 最后还需要添加一个字符表示消息类型
let protobufMessageContent = protobufTypeObject.create(message);
// 将消息进行编码
let encodeProtobufMessageContent = protobufTypeObject.encode(protobufMessageContent)
// 消息转换完成
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){
// 确认消息
messageTypeName = "Ack"
} else if(messageType == 3){
// 同步消息
messageTypeName = "ChatMsgList"
} else if(messageType == 4){
// 离线推送消息
messageTypeName = "ChatMsgList"
} else if(messageType == 51){
// 好友申请的命令
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){
// 被踢出会议房间
messageTypeName = "GroupKick"
} else if(messageType == 57){
// 面对面建群,加入群聊前,进入房间时刷新列表用
messageTypeName = "RefreshContact"
} else if(messageType == 58){
// 通知
messageTypeName = "Push"
}

return messageTypeName;
}
// 在bufferMessage 前面加上 一个字节, 表示消息的类型, 方便客户端取用, 辨识是哪种消息类型
function bufferMessageAddType(type, buffer){
/**

  • Uint8Array是JavaScript中的一种类型化数组。
  • 它提供了一种用于表示8位无符号整数的固定长度的数组,
  • 可以让你更轻松,更高效地操作二进制数据
    */
    // 创建一个 1 + buffer.length长度的数组
    let array = new Uint8Array(1 + buffer.byteLength)
    // 该方法允许你通过一个子数组来填充当前数组的一部分
    array.set(new Uint8Array([type]), 0)
    array.set(new Uint8Array(buffer), 1)

let arrayBuffer = array.buffer;
// 将arraybuffer 转换为buffer
let messageBuffer = Buffer.from(arrayBuffer)
// 注意 vue中使用 arraybuffer, 而nodejs中需要使用buffer, 因为底层不完全相同
return messageBuffer;
}

// 获取url上的参数, 使用的是正则表达式
function getParamsFromURL(url) {
const regex = /?&=([^&#]*)/g;
const params = {};
let match;
while (match = regex.exec(url)) {
params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
}
return params;
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容