前言
博客园的样式真心不会用啊,看着大大们的博客各种好看,心里无奈啊,只能慢慢摸索了。
最近的项目nodejs+wcf+app,app直接从wcf服务获取数据,nodejs作为单独的服务器为app提供图形服务和聊天室功能。主要架构如下
这一篇主要讲的是如何用nodejs+socketio实现一个基础的聊天室。其实这也是我第一个nodejs项目,真个知识体系还不太完整,遇到问题就度娘,有说错的地方请大家指正。
聊天室功能点概要
1.在线和离线人员管理
2.消息的发送,广播以及离线消息
3.音频文件,图片的发送
具体实现
首先整个聊天系统因为业务关系,容量是固定的基本不会超过1000人,实际情况在100人左右。如此轻量级的数据我选择用文本文件来记录所有的离线消息和人员列表。mongodb想用来着,留着下次处女作吧。
这里人员列表记录所有进入过聊天系统的人员,每次人员登录进入系统就将获得这个人员列表作为客户端的聊天对象。并且人员的上线和下线将触发广播in和out消息,让在线成员刷新人员列表。
消息的组成包括:from,to,body,type等,按照约定如果to为空则作为群消息进行广播发送,否则指定人员发送信息(如果该人员online属性为false则存入离线消息文件,待该人员进入聊天室的时候一起拉取离线消息)
文件的发送,实际上是文件上传和发送文件地址的过程。将type和body组合一下就可以了。
整个通讯直接用socketio,包括android端和ios端都直接调用socketid对应版本类库,完全没有学习成本。上手非常简单。
下面直接上代码:
/** * Created by qyz on 14-3-18. */ var fs = require('fs'); var path = require('path'); //许可的后缀名 var AllowExt=[ "amr", "jpg", "jpeg", "gif", "png", "swf"]; var ImageExt=[ "jpg", "jpeg", "gif", "png"]; var exec = require('child_process').exec; var path = require('path'); var fs = require('fs'); if(!fs.existsSync(__dirname + '/../public/chatfiles')){ fs.mkdirSync(__dirname + '/../public/chatfiles',0755); } if(!fs.existsSync(__dirname + '/../public/chatfiles/emplist.json')){ fs.writeFileSync(__dirname + '/../public/chatfiles/emplist.json',"") /*fs.open(__dirname + '/../public/chatfiles/emplist.json', 'w+', 0666, function(err, fd){ fs.close(fd); });*/ } var allempList=readAllEmpList();//人员列表 exports.connection=function(socket){ //实名注册事件 socket.on('login',function(msg){ var json=JSON.parse(msg); socket.name=json.card; isExists(json,socket); //加入连接列表 socket.broadcast.emit('in','{"data":'+msg+'}');//通知用户登入 socket.emit('emplist','{"data":'+ BroadCastPeopleList()+'}');//获取用户列表 OffLineMessage(socket,json); //拉取离线消息 }); //接收消息事件 socket.on('emplist', function (msg) { socket.emit('emplist','{"data":'+ BroadCastPeopleList()+'}');//获取用户列表 }); //接收消息事件 socket.on('message', function (msg) { OnMessage(socket,msg); }); //断开连接事件 socket.on('disconnect', function () { socket.broadcast.emit('out','{"data":'+socket.name+'}'); Exit(socket); }); } ///判断列表中是否已经存在该socket,不存在则加入 function isExists(json,socket){ var bo = false; for(var i=0;i<allempList.length;i++) { if(allempList[i].card== json.card)//如果存在则 认为在线 { //判断如果卡号更改了人员或者部门则要刷新 if(allempList[i].name!=json.name||allempList[i].dept!=json.dept) { allempList[i].name=json.name; allempList[i].dept=json.dept; var arr=[]; for(var j=0;j<allempList.length;j++) { arr.push(new EmpListEasy(allempList[i])); } if(arr!=null) { fs.writeFileSync(__dirname + '/../public/chatfiles/emplist.json',JSON.stringify(arr)); } } allempList[i].socket=socket; allempList[i].online=true; bo = true; break; } } if(!bo){ allempList.push(new EmpList( json.card, json.name, json.dept,true,socket)); var arr=[]; for(var j=0;j<allempList.length;j++) { arr.push(new EmpListEasy(allempList[i])); } if(arr!=null) { fs.writeFileSync(__dirname + '/../public/chatfiles/emplist.json',JSON.stringify(arr)); } } console.log(JSON.stringify(json) +'创建连接'); } ///断开连接 删除列表 function Exit(socket){ console.log(socket.name+'断开连接'); for(var i=0;i<allempList.length;i++) { if(allempList[i].card== socket.name) { allempList[i].online=false; allempList[i].socket=null; break; } } socket = null; } ///广播发送所有人员名单 function BroadCastPeopleList(){ var arr=[]; for(var i=0;i<allempList.length;i++) { arr.push(new EmpListEasy(allempList[i])); } return JSON.stringify(arr); } ///发送信息 function OnMessage(socket,msg){ var message = JSON.parse(msg); if(message!=null){ var info=JSON.stringify(new MessageModel(message)); console.log('接收信息 ', info); if(message.to=="")//广播发送信息 { socket.broadcast.emit('message','{"data":'+info+'}'); } else { for(var i=0;i<allempList.length;i++) { if(allempList[i].card==message.to)//找到对应的人,判断是离线还是在线,如果离线则保存,在线则发送 { if(allempList[i].online) { allempList[i].socket.emit('message','{"data":'+info+'}'); } else{ if(!fs.existsSync(__dirname + '/../public/chatfiles/'+allempList[i].card+'.json')) { fs.writeFile(__dirname + '/../public/chatfiles/'+allempList[i].card+'.json',info+'||', function(err){ if(err) { console.log(err); } }); } else{ fs.appendFile(__dirname + '/../public/chatfiles/'+allempList[i].card+'.json',info+'||', function(err){ if(err) { console.log(err); } } ); } } break; } } } } } //读取人员列表 function readAllEmpList(){ var fs = require('fs'); var path = require('path'); var arr=[]; var data= fs.readFileSync(__dirname + '/../public/chatfiles/emplist.json'); if(data!=null&&data!="" ) { var json = JSON.parse(data); if(json!=null && json.length>0) { for(var i=0;i<json.length;i++) { arr.push(new EmpList(json[i].card,json[i].name,json[i].dept,false,null)); } } } return arr; } //拉取离线消息 function OffLineMessage(socket,json){ if(fs.existsSync(__dirname + '/../public/chatfiles/'+json.card+'.json')) { var info= fs.readFileSync(__dirname + '/../public/chatfiles/'+json.card+'.json', 'utf-8'); if(info!="") { var strarr = info.split("||"); var arr=[]; for(var i=0;i<strarr.length;i++) { if(strarr[i]!=""){ arr.push(strarr[i]); } } socket.emit('offline','{"data":'+JSON.stringify(arr).replace(/\/g, "")+'}');//发送离线消息 fs.writeFile(__dirname + '/../public/chatfiles/'+json.card+'.json','',null); // fs.unlinkSync(__dirname + '/../public/chatfiles/'+json.card+'.json');//删除离线文件 } } } /***************实体类********************/ function EmpListEasy(list){ this.card =list.card; this.name =list.name; this.dept = list.dept; this.online=list.online; } function EmpList(card,name,dept,online,socket){ this.card = card; this.name = name; this.dept = dept; this.online = online; this.socket = socket; } function MessageModel(message){ this.from=message.from; this.to=message.to; this.body=message.body; this.time=new Date(); this.name=message.name; this.dept=message.dept; this.type=message.type; this.filetype=message.filetype; } /***************公共方法********************/ //删除array中项 根据索引 function removeArray(ob, index) { if (isNaN(index) || index > ob.length) { return false; } for (var i = 0, n = 0; i < ob.length; i++) { if (ob[i] != ob[index]) {ob[n++] = ob[i]; } } ob.length -= 1; } //删除array中项 根据id function removeArrayByID(ob, id) { for (var i = 0; i < ob.length; i++) { if (ob[i].id == id) {removeArray(ob, i); break; } } } exports.savefile=function(req,res){ var body = ''; var header = ''; var content_type = req.headers['content-type']; var boundary = content_type.split(';')[1].split('=')[1]; var content_length = parseInt(req.headers['content-length']); var headerFlag = true; var filename = 'dummy.bin'; var filenameRegexp = /filename="(.*)"/m; req.on('data', function(raw) { var i = 0; while (i < raw.length) if (headerFlag) { var chars = raw.slice(i, i+4).toString(); if (chars === ' ') { headerFlag = false; header = raw.slice(0, i+4).toString(); i = i + 4; // get the filename var result = filenameRegexp.exec(header); if (result[1]) { filename = result[1]; } } else { i += 1; } } else { // parsing body including footer body += raw.toString('binary', i, raw.length); i = raw.length; } }); req.on('end', function() { // removing footer ' '--boundary-- ' = (boundary.length + 8) body = body.slice(0, body.length - (boundary.length + 8)) if(!fs.existsSync(__dirname+'/../public/upload/'+CurentMonth())) { fs.mkdirSync(__dirname+'/../public/upload/'+CurentMonth()); } var timepath=CurentMonth()+'/'+CurentDay(); if(!fs.existsSync(__dirname+'/../public/upload/'+timepath)) { fs.mkdirSync(__dirname+'/../public/upload/'+timepath); } var ext = filename.split('.')[1]; if(AllowExt.indexOf(ext) == -1)//不允许的后缀名文件 { var result = new Result(); result.state = 0; result. info = "文件格式不正确"; result.data = null; res.write(JSON.stringify(result)); res.end(); } else { fs.writeFile(__dirname+'/../public/upload/'+timepath+'/' + filename, body, 'binary',function(){ var data = new Data();//返回body数据 data.localname=filename; data.url='upload/'+timepath+'/' + filename; data.surl=''; var result = new Result();//返回的数据包,包含头 result.state = 1; result.info = ""; result.data = data; if(ImageExt.indexOf(ext)!=-1)//图片文件则保存缩略图 { data.surl='upload/'+timepath+'/s_' + filename; //生成缩略图 exec(__dirname+'/../ImageMagick/convert -resize 100 '+__dirname+'/../public/'+data.url +' '+__dirname+'/../public/'+ data.surl , function(err){ if(err){ result.state = 0; result. info = "生成缩略图失败:"+err; result.data = null; res.write(JSON.stringify(result)); res.end(); } else{ res.write(JSON.stringify(result)); res.end(); } }); } else { res.write(JSON.stringify(result)); res.end(); } }); } }) } //实体类 //获取当前日期作为文件夹名称 function CurentMonth(){ var now = new Date(); var year = now.getFullYear(); //年 var month = now.getMonth() + 1; //月 var clock = year ; if(month < 10) clock += "0"; clock += month ; return(clock); } function CurentDay(){ var now = new Date(); return now.getDate(); //日 } //返回包的实体类 function Result() { this.state=1; this.info=""; this.data=null; } //返回包中的body数据 function Data(){ this.localname=""; this.url=""; this.surl=""; }
其中上传图片生成缩略图部分,直接参考我上一篇博客。