一门课程的作业,记录一下。
源码下载: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>
由于只是小作业,也是第一次做,没怎么细心想,其实许多代码结构、功能逻辑都可以完善优化。
(另外,这个在谷歌浏览器打开是没问题的,需要确认下语音权限,其它浏览器可能或多或少有点问题)