使用nodejs、ws模块实现简单聊天室(包括简单的语音功能实现)

一门课程的作业,记录一下。

源码下载:https://download.csdn.net/download/qq_41508508/11991195

 

完成后的界面如图:

首先通过npm安装好ws模块:

npm install ws

接下来建立工程目录,拷贝npm路径下的模块目录node_modules目录和package.json文件到该工程目录下,同时将jquery、滚动条等相关js和css也放一起。

首先创建服务器端代码文件server.js,代码如下:

//导入模块
var ws = require("ws");

//创建服务器
var wsServer = new ws.Server({
	host: "127.0.0.1",
	port: 8182,
});
console.log("listening localhost:8182");

// 广播
wsServer.broadcast = function broadcast(str){	
	wsServer.clients.forEach(function each(client) {
			client.send(str);
		});	
	//console.log(wsServer.clients.length);	
};

//建立连接后
wsServer.on('connection',function connection(ws){	
	ws.on('message',function incoming(obj){	
		var o = JSON.parse(obj);
		switch (o.type) {
			case 'loginInfo':
				var str = JSON.stringify({
					"msg" : o.msg ,
					"type" : o.type
				});
				this.name = o.name;
				wsServer.broadcast(str);
				break;
			case 'chatInfo':
				var str = JSON.stringify({
					"name" : o.name,
					"msg" : o.msg,
					"type" : o.type
				});
				wsServer.broadcast(str);
				break;
			case 'systemInfo':
				var str = JSON.stringify({
					"msg" : o.msg ,
					"type" : o.type
				});
				wsServer.broadcast(str);
				break;
			case 'audioInfo':
				var str = JSON.stringify({
					"name":o.name,
					"recordedBlobs":o.recordedBlobs,
					"type":o.type
				});
				wsServer.broadcast(str);
				break;
			
			default:
				break;
		}		
	});
	
	ws.on('close',function (o){
		try{
			console.log(this.name);
			console.log('from server: close');
			
			var str = JSON.stringify({
				"msg" : this.name+"已下线......",
				"type" : "systemInfo"
			});
			wsServer.broadcast(str);
		}
		catch (e){
			console.log(e);
		}
	});
	
});

其中的广播函数,主要通过foreach来遍历所有客户端client,并向它们发送信息。

on('message')主要是 当客户端发送信息过来时的处理逻辑,将发送的json格式字符串解析,并根据不同的消息类型type做不同的处理。(这里的type设置得可能不太合理,可以再修改完善一下)

on('close')主要是 当客户端下线时,给其它客户端都发送下线消息。

其次还可以增添on('error')函数等,都有各自的功能。

 

接下来创建客户端的处理逻辑client.js,代码如下:

var i = 1;    
$(function(){
       
        $(".box").mCustomScrollbar({theme:"inset-2-dark"});
        var wsObj = null;
        
        var mediaRecorder = null;
        var recordedBlobs;
        var recordedAudio = document.querySelector('audio#recorded');
        var recordButton = $("#btn_record");
        recordButton.on("click",toggleRecording);
        
        //test button
//        var testButton = document.querySelector('button#test_btn');
//        testButton.onclick = testButtonPlay;
        
        var contraints = {audio:true};

        //更新滚动条
        function updateScrollbar(){
            $(".box").mCustomScrollbar("update").mCustomScrollbar('scrollTo', 'bottom',{
    			axis:"y", // 竖直滚动条
    			
            });
        }
        //生成随机数
    	function num(m,n){ 	 
    		return parseInt((Math.random()*(n-m)));
    	} 
        //发送信息
    	function sendMsg(){
            var msg = bb.value;
            var name = $("#name").val();
            if (name=="" || name==null){
                alert("请设置您的昵称!");
                return ;
            }
            if (msg==""){
                alert("消息不能为空!");
                return ;
            }
            var type = "chatInfo";
            var obj = {"name":name,"msg":msg,"type":type};
            wsObj.send(JSON.stringify(obj));
            bb.value = "";
    	}
        
        $("#setName").click(function(){
           
            name = $("#name").val();
            if (name=="" || name==null || name==" "){
                alert('昵称不合法!');
                return ;
            }
            
            var name = $("#name").val();   
            var msg = name + "已上线......"
            var type = "loginInfo";
            var obj = {"name":name,"msg":msg,"type":type};
            
            wsObj.send(JSON.stringify(obj));
            
        });
        
        wsObj = new WebSocket("ws://127.0.0.1:8182");   //建立连接
        wsObj.onopen = function(){  //发送请求
            console.log("连接状态",wsObj); 
            
        };
        wsObj.onmessage = function(e){  //获取后端响应
           var data = JSON.parse(e.data);
            
            switch (data.type) {
                case 'loginInfo':
                    var msg = data.msg;
                    var subDiv = $("<div></div>");
                    var msgDiv = $("<div></div>");

                    $(subDiv).addClass("item");
                    $(msgDiv).addClass("loginDiv");
                    $(msgDiv).html(msg);
                    
                    msgDiv.appendTo(subDiv);
                    subDiv.appendTo($(".mCSB_container"));
                    updateScrollbar();

                    break;
                case 'chatInfo':
                    var name = data.name;
                    var msg = data.msg;
                        
                    var subDiv = $("<div></div>");
                    var h5 = $("<h5></h5>");
                    var p = $("<p></p>");
                    
                    h5.html(name);
                    p.html(msg);
                    
                    $(p).addClass("response");
                    $(subDiv).addClass("item");
                    $(h5).addClass("pp");
                    
                    p.appendTo(subDiv);
                    h5.prependTo(subDiv);
                    subDiv.appendTo($(".mCSB_container"));
                    
                    updateScrollbar();
                    break;
                    
                case 'systemInfo':
                    var msg = data.msg;
                    var subDiv = $("<div></div>");
                    var msgDiv = $("<div></div>");
                    
                    $(subDiv).addClass("item");
                    $(msgDiv).addClass("systemDiv");
                    $(msgDiv).html(msg);
                    
                    msgDiv.appendTo(subDiv);
                    subDiv.appendTo($(".mCSB_container"));
                    updateScrollbar();
                    break;
                    
                case 'audioInfo':
                    
                    var tmprecordedBlobs = data.recordedBlobs;
                    var name = data.name;
                    // alert(name + "发来一条语音消息!");
                    var arrayBuffer = Object.keys(tmprecordedBlobs).map((el) =>{
                        return tmprecordedBlobs[el];
                    });
                    var superBuffer = new Blob([new Uint8Array(arrayBuffer)], { type: "audio/ogg; codecs=opus" });
                    //console.log(superBuffer);
                    
                    var subDiv = $("<div></div>");
                    var h5 = $("<h5></h5>");
                    var auDiv = $("<div></div>");
                    h5.html(name);
                    auDiv.html(" ");
                    $(auDiv).attr('s',i++);
                    var _auDiv_hidden = $("<audio 'hidden' class='recorded_auDiv'></audio>");
                    $(_auDiv_hidden).attr('s',i-1);

                    $(auDiv).attr('isplay',false);
                    $(auDiv).addClass("response").addClass("auDiv");    
                    $(subDiv).addClass("item");
                    $(h5).addClass("pp");

                    auDiv.appendTo(subDiv);
                    h5.prependTo(subDiv);
                    _auDiv_hidden.appendTo(subDiv);
                    subDiv.appendTo($(".mCSB_container"));
                    
                    $('div[s="'+(i-1)+'"]').get(0).addEventListener('click',(e)=>{
                        
                        var x = $(e.target).attr("s");
                        if($(auDiv).attr('isplay')=='false'){
                            $('audio[s="'+x+'"]').get(0).currentTime = 0;
                            // console.log(e.target);
                            $(auDiv).attr('isplay',true);
                            $(auDiv).css('color','red');
                            console.log($('audio[s="'+x+'"]').get(0));
                            $('audio[s="'+x+'"]').get(0).play();
                        }
                        else{
                            $('audio[s="'+x+'"]').get(0).pause();
                            $(auDiv).attr('isplay',false);
                            $(auDiv).css('color','green');
                            
                        }
                        
                    });

                    
                    $('audio[s="'+(i-1)+'"]').get(0).src = window.URL.createObjectURL(superBuffer);
                    
                    $('audio[s="'+(i-1)+'"]').get(0).addEventListener('canplay', function (e) {
                       
                            var audioDuration = e.target.duration;
                            if(audioDuration == Infinity){
                                $('audio[s="'+(i-1)+'"]').get(0).currentTime = 1e101;
                                $('audio[s="'+(i-1)+'"]').get(0).ontimeupdate = function(){
                                    this.ontimeupdate = () => {
                                        return ;
                                    };
                                    $('audio[s="'+(i-1)+'"]').get(0).currentTime = 1e101;
                                    $('audio[s="'+(i-1)+'"]').get(0).currentTime = 0;
                                };
                            }
                            
                            var t = i - 1;
                            $('div[s="'+t+'"]').html(parseInt($('audio[s="'+(i-1)+'"]').get(0).duration)+'s');

                    });
                    

                    updateScrollbar();
                    break;
                    
                default :
                    break;
            }
            
        };
        wsObj.onclose = function(e){
            console.log(e);
        };
        wsObj.onerror = function(e){
            console.log("error");
        };
        
    	$(".btn_send").click(function(){ 
    			//send:
                sendMsg();
            });

    	$(window).on('keydown', function(e) {
            if (e.which == 13) {
                sendMsg();
                return false;
            }
        });
                
        function handleSuccess(stream){
            console.log('getUserMedia() got stream: ',stream);
            window.stream = stream;
        }
        
        function handleError(error) {
            console.log("getUserMedia() error: ",error);
        }
        
        //获取音频权限
        navigator.mediaDevices.getUserMedia(contraints).then(handleSuccess).catch(handleError);
        
        
        
        function handleDataAvailable(event){
            
            recordedBlobs.push(event.data);
        }
        
        function handleStop(event){
            console.log('Recorder stopped: ',event);
            sendAudio();
        }
        
        function toggleRecording(){
            if (recordButton.text() == '发送语音'){
                // $("#btn_record").addClass('btn_record_ison');
                startRecording();
                
            }
            else{
                // $("#btn_record").removeClass('btn_record_ison');
                stopRecording();
                recordButton.text('发送语音');
                
            }
        }
        
                
        function startRecording() {
            recordedBlobs = [];
            var options = {mimeType: 'audio/webm;codecs=vp9'};
            if (!MediaRecorder.isTypeSupported(options.mimeType)){
                console.log(options.mimeType + 'is not Supported');
                options = {mimeType: 'audio/webm;codecs=vp8'};
                if (!MediaRecorder.isTypeSupported(options.mimeType)){
                    console.log(options.mimeType + 'is not Supported');
                    options = {mimeType: 'audio/webm'};
                    if (!MediaRecorder.isTypeSupported(options.mimeType)){
                        console.log(options.mimeType + 'is not Supported');
                        options = {mimeType: ''};
                    }
                }
            }
            console.log(options);
            try{
                mediaRecorder = new MediaRecorder(window.stream);     
            }
            catch (e){
                console.error('error! create MediaRecorder(): '+e);
                return ;
            }
            recordButton.text('停止') ;           
//            testButton.textContent = 'Stop';
            mediaRecorder.start(); 
            mediaRecorder.ondataavailable = handleDataAvailable;
            mediaRecorder.onstop = handleStop;

            console.log(mediaRecorder.state);
            console.log('MediaRecorder started', mediaRecorder);

        }
        
        function stopRecording(){
            mediaRecorder.stop();
            console.log('Recorded Blobs: ',recordedBlobs);
            console.log(mediaRecorder.state);
                    
        }
        
        function sendAudio(){
        
            var name = $("#name").val();
            if (name=="" || name==null){
                alert("请设置您的昵称!");
                return ;
            }
            var type = "audioInfo";
            
           var reader = new FileReader();
           var superBuffer = new Blob(recordedBlobs, { type: "audio/ogg; codecs=opus" });
           var res;
           console.log(recordedBlobs);
           console.log(superBuffer);
           reader.readAsArrayBuffer(superBuffer);
           reader.onload = function () {
               res = new Uint8Array(reader.result);
            //    console.log(res);
               var obj = {"name":name,"recordedBlobs":res,"type":type};
                wsObj.send(JSON.stringify(obj));
               
           }
                                
        }
        
        

    });

代码看起来有点长,但逻辑还是比较简单的。

文本消息的发送,主要是将昵称、消息封装成json对象,通过调用WebSocket对象send方法,将其发送给服务器端;

接收方面,主要首先判断消息类型,当其是文本消息时,将json字符串解析出来,得到昵称和信息内容,再通过jquary构造div什么的,写到html文档上。

语音信息的发送,相对来说会麻烦些。似乎ws模块不能直接发送blob对象,采用socket.io模块好像可以。

主要是调用了MediaRecorder来创建媒体对象mediaRecorder,并准备一个用于记录语音信息的Blob数组recordedBlobs[],当调用了mediaRecorder的start方法后,触发一个handledDataAvailable事件,在其监听器上将语音数据event.data 给push进recordedBlobs数组。录制结束后,调用sendAudio()方法,将语音信息数组recordedBlobs转化成Unit8Array格式,并封装到json对象发给服务器。

接收语音信息,则是反过来的过程。将服务器发来的Unit8Array格式转为blob数组,再通过window.URL.createObjectURL创建url链接并指给 <audio>标签的src属性。

其余的主要是昵称处理,语音的点击交互什么的,通过将<audio>标签隐藏,再用div来控制语音播放,加上css3的动画,可以实现类似微信语音的效果。

 

html 文件 client.html :

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"></meta>
    <title>WebSocket Test</title>
</head>
<style>
    *{list-style: none;margin: 0;padding: 0;}

    .box1{
        width: 700px;
        height: 672px;
        border: 1px solid #aaa;
        margin: 0 auto;
        margin-top: 20px;
        border-radius: 10px;
        background-color: #fafafa
    }
    .box{
        width: 700px;
        height: 430px;
        overflow: auto;
        background: url("../res/bg.jpg")  no-repeat;
    }
    #bb{
        width: 672px;
        margin-top: 15px;
        border: 0px solid gray;
        outline: none;
        padding: 10px;
        resize: none;
        height: 125px;
        background-color: #fafafa

    }
    img{
        width: 700px;
    }
    hr{
        width:100%;
        	margin:0 auto;
        border: 0;
        	height: 2px;
        background-image: -webkit-linear-gradient(left, rgba(214, 214, 214, 0.3), rgba(214, 214, 214, 0.8), rgba(214, 214, 214, 0.3));
        background-image: -o-linear-gradient(left, rgba(214, 214, 214, 0.3), rgba(214, 214, 214, 0.8), rgba(214, 214, 214, 0.3));
        background-image: linear-gradient(to right, rgba(214, 214, 214, 0.3), rgba(214, 214, 214, 0.8), rgba(214, 214, 214, 0.3));

    }
    .btn{
        margin-left:450px;
    }
    .btn_send{
        margin-left: 23px;
    }
    .btn,.btn_send{
        display: inline-block;
        margin-top: 29px ;
        background: #fefefe;
        width: 120px;
        padding: 3px 0;
        border-radius: 7px;
        color: #222;
        border: 1px solid #3f3f3f;
        font-size: 14px;
        outline: none;
        text-align: center;
        line-height: 23px;
        height: 23px;
    }


    a:hover{
		background-color: #efefef;
		cursor: pointer;
	}



    .image{
        margin: 5px;
        width: 50px;
        float: left;
    }
    .pp{
        margin-left: 12px;
        margin-top: 0px;
        padding-top: 5px;
        color: #7575f5;
        margin-bottom: 1px;
        font-size: 14px;
    }

    .send{
        float: right;
        margin-left: 0;
        overflow: hidden;
    }


    .response{

        display: inline-block;
        margin-left: 8px;
        background: #eee;
        border-radius: 8px;
        padding: 7px;
        font-size: 14px;
        max-width: 400px;
        word-wrap:break-word;
        min-width: 28px;
        min-height: 19px;

    }


    .sendMsg{
        float:right;
        margin-top: 33px;
        margin-right: -16px;
    }


    .item{
        margin-bottom: 17px;

    }
    
    .noclick{
        pointer-events: none;
/*        background-color: #e5e5e5;*/
    }
    
    .zhezhao{
        position:absolute; 
        z-index:100; 
        background:#cccccc;
        filter: alpha(Opacity=50);
        -moz-opacity:0.5;
        opacity: 0.5;
        width: 700px;
        height: 672px;
    }
    
    .loginDiv,.systemDiv{
        margin-left: 20px;
        color: #5e9e8e;
        padding-top:15px;
        font-size: 14px;
        
    }
    
    .setnameDiv{
/*        display: inline-block;*/
        float: left;
        margin-top: 37px;
        margin-left: 30px;
    }
    
    #name{
        -web-kit-appearance:none;
        -moz-appearance: none;
        font-size: 0.9em;
        border: 1px solid #c8cccf;
        color: #6a6f77;
        border-radius: 4px;
        padding:2px;
    }
    #name:hover{
        border:1px solid gray;
    }
    #name:focus{
        border:1px solid gray;
        outline: 0;
    }
    
    #setName{
        min-width: 50px; 
        padding: 3px 5px; 
        background: #fff; 
        color: #5e5e5e; 
        border: 1px solid #afafaf; 
        border-radius: 3px;
        margin-top: -20px;
        
    }
    
    .auDiv{
        width: 105px;
        text-align: right;
        cursor: pointer;
        padding-right: 10px;
        color: green;
    }
    
    #btn_record{
        float: left;
        margin-top: 30px ;
        background: #fefefe;
        width: 90px;
        padding-bottom: 1px;
        border-radius: 7px;
        color: #222;
        border: 1px solid #3f3f3f;
        font-size: 14px;
        outline: none;
        text-align: center;
        line-height: 30px;
        height: 30px;
        margin-left: 175px;
    }
    
    #btn_record_ison{
        color: red;
        background-color: black;
    }
    
    #setName:hover,#btn_record:hover{
        color: #333333;
        background: #efefef;
        cursor: pointer;
    }
    #setName:focus,#btn_record:focus{
       
        outline: none;
    }
    
    
    
</style>

<link rel="stylesheet" href="jquery.mCustomScrollbar.min.css" />

<body>
        
    <div class="box1">
        <div class="zhezhao" style="display: none;"></div>
        <div class="box">
            
        </div> 
        <hr>
        <textarea name="" id="bb" cols="30" rows="10"></textarea>
        <!-- <button class="btn"> 关闭<br>(ctrl+q)</button>  -->
        <div class="setnameDiv">
            <input type='text' id="name" placeholder="请设置你的昵称..."/>
            <button id='setName'>设置昵称</button>
        </div>
        
        <button class="raDiv" id="btn_record">发送语音</button>
        <a class="btn_send">发送信息(Enter)</a> 
        
    </div> 
    
    
    <br>
    <br>

    
    
</body>

<script type="text/javascript" src="jquery.min.js"></script>
<script type="text.javascript" src="jquery-latest.js"></script>
<script type="text/javascript" src="jquery.mCustomScrollbar.concat.min.js"></script>
<script type="text/javascript" src="client.js"></script>

<script>

    
</script>


</html>

由于只是小作业,也是第一次做,没怎么细心想,其实许多代码结构、功能逻辑都可以完善优化。

(另外,这个在谷歌浏览器打开是没问题的,需要确认下语音权限,其它浏览器可能或多或少有点问题)

 

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值