socket.io

 

socket

  • socket.io一个是基于Nodejs架构体系的,支持websocket的协议用于实时通讯的一个软件包。
  • socket.io 给跨浏览器构建实时应用提供了完整的封装,socket.io彻底由javascript实现

依赖的外部包

express、socket.iojavascript

安装

  • npm install --save-dev express
  • npm install --save-dev socket.io
  • 默认会在项目下新建一个node_module文件,引入express和socket.io的外部包

服务器server:

var express = require('express'); var app = express(); var http = require('http'); //建立一个服务器 var server = http.createServer(app); //监听端口 var port = normalizePort(process.env.PORT || '3000'); server.listen(port); app.set('views', path.join(__dirname, 'views')); //服务器端引入socket.io var io = require('socket.io').listen(server); io.on('connection', function(socket){ socket.on('message', function () { }); socket.on('disconnect', function(){...}); }); 

客户端client

//客户端引入socket var socket = io(); socket.on('connect', function () { socket.send('hi'); socket.on('message', function (msg) { // my msg }); }); 

原理

  • 服务器保存好全部的 Client->Server 的 Socket 链接,
  • Client A 发送消息给 Client B 的实质是:Client A -> Server -> Client B。
  • 即 Client A 发送相似 {from:'Client A', to:'Client B', body: 'hello'} 的数据给 Server。
  • Server 接收数据根据 to值找到 Client B 的 Socket 链接并将消息转发给 Client B

使用

  • 使用socket.io,其先后端句法是一致的。
  • 即经过socket.emit() 来激发一个事件;
  • 经过socket.on() 来监听和处理对应事件;
  • 这两个事件经过传递的参数进行通讯。

服务器信息传输基本语法

  • 全部客户端
// send to current request socket client // 发送一个请求的当前请求的socket客户端 socket.emit('message', "this is a test"); // sending to all clients except sender // 广播消息,不包括当前的发送者 socket.broadcast.emit('message', "this is a test"); // sending to all clients, include sender // 发送消息给全部客户端,包括发送者 io.sockets.emit('hi', 'everyone'); io.emit('hi', 'everyone'); // 写的简单点: 
  • 房间内发送
// sending to all clients in 'room1' room except sender // 给房间room1的全部客户端发送消息,不包括发送者 socket.broadcast.to('room1').emit('message', 'hello'); // sending to all clients in 'room1' room(channel), include sender // 给房间room1的全部客户端发送消息,包括发送者 io.sockets.in('room1').emit('message', 'hello'); 
  • 指定发送给单个用户
// sending to individual socketid // 给单个用户socketId发送消息 io.sockets.socket(socketId).emit('message', 'for your eyes only'); 

socket.set和socket.get方法分为用于设置和获取变量。

io.sockets.on('connection', function (socket) { socket.on('set nickname', function (name) { socket.set('nickname', name, function () { socket.emit('ready'); }); }); socket.on('msg', function () { socket.get('nickname', function (err, name) { console.log('Chat message by ', name); }); }); }); 

socket.join()加入房间 && socket.leave()离开房间

io.on('connection', function(socket){ //加入房间 socket.join('some room'); //用to或者in是同样的,用emit来给房间激发一个事件 io.to('some room').emit('some event'): //socket.leave('some room'); }); io.on('disconnection', function(socket){ //一旦disconneted,那么会自动离开房间 ... }); 

socket.send()和socket.recv()消息的发送和接收

  • socket.emit()和socket.send()的区别
  • socket.emit allows you to emit custom events on the server and client
  • socket.send sends messages which are received with the 'message' event

数组操做

新建一个数组

var onlineList = []; 

添加元素到数组

onlineList.push(uid); 

判断元素是否是在数组

onlineList.indexOf(uid) 
  • 返回值:
  • -1:不在数组中
  • 其余数值:对应的下标

删除数据

index = onlineList.indexOf(uid)  //找到对应的下标 onlineList.splice(index,1) //删除index到index+1的数据,也就是删除下标为index的数据 
  • 请注意,splice() 方法与 slice() 方法的做用是不一样的,splice() 方法会直接对数组进行修改。

数据库设计和学习:

用户数据结构:包含用户名,密码和图片

var userSchema = new Schema({ username: String, password: String, imgUrl: String, meta: { updateAt: {type:Date, default: Date.now()}, createAt: {type:Date, default: Date.now()} } }); 

朋友数据结构

  • 包含uid--自身的id值,fid--朋友的id值
  • mongodb数据库每次新建一个对象,都会默认给这个对象一个惟一的_id值,做为这个对象的惟一标识符
  • 将uid的类型定义为ObjectId,设置引用ref为User
  • 在查询消息的时候能够同时查询两张表,而默认的_id值也就是他查询的键
var mongoose = require('../db'); var Schema = mongoose.Schema; var ObjectId = Schema.Types.ObjectId; var friendSchema = new Schema({ uid: {type:ObjectId, ref:'User'}, fid: {type:ObjectId, ref:'User'}, meta: { updateAt: {type:Date, default: Date.now()}, createAt: {type:Date, default: Date.now()} } }); 

消息数据结构

  • 消息是两个用户之间的通讯,所以须要fromto两个对象
  • 同时也须要uid
var messageSchema = new Schema({ uid: {type:ObjectId, ref:'User'},//用户 from: {type:ObjectId, ref:'User'},//发送给谁 to: {type:ObjectId, ref:'User'},//谁接收 msg: String, type: Number,//已读1 or 未读0 meta: { updateAt: {type:Date, default: Date.now()}, createAt: {type:Date, default: Date.now()} } }); 

新建一个用户数据

  • $("body").on('click', '#registerBtn', doRegister);
  • 点击body中的id为registerBtn的按钮,执行doRegister函数
  • ajax是一种异步的请求,当用户每次输入必定的值,服务器都会把这个值传递过来css

  • $("#usr").val()--用jquery的方式获取idusr的表单的值前端

  • $("#userThumb").attr("src")--用jquery的方式获取id为userThumb的属性src的值,获取图片的路径java

  • JSON.stringify是将传递过来的数据转换为JSON格式node

  • 若是成功,那么success,执行后面的function();python

  • $.cookie('username', result.data.username, {expires:30});是利用jquery.cookie.js将数据存放到cookie里面jquery

function doRegister() { $.ajax({ type: "POST", //方式post url: "/register", //路径register contentType: "application/json", dataType: "json", //数据类型json格式 data: JSON.stringify({ 'usr': $("#usr").val(), //用户名 'pwd': $("#pwd").val(), //密码 'imgUrl': $("#userThumb").attr("src") //图片 }), success: function(result) { if (result.code == 99) { //失败弹出错误信息 console.log("注册失败") } else { //成功就将输入的数据做为cookies存入 console.log("注册成功"); console.log(result.data); $.cookie('username', result.data.username, {expires:30}); $.cookie('password', result.data.password, {expires:30}); $.cookie('imgUrl', result.data.imgUrl, {expires:30}); $.cookie('id', result.data._id, {expires:30}); location.href = "/webchat"; //跳转到聊天界面 } } }) } 

咱们能够经过拆分的方法,将上面的代码拆解为几个版块

  1. 将路由定义为一个变量
var urlRegister = "/register"; 

2.将上面的一段代码提炼出骨干web

function postData(url, data, cb) { var promise = $.ajax({ type: "post", url: url, //传递过来的post路径 dataType: "json", contentType: "application/json", data:data //传递过来的data }); promise.done(cb); //执行cb回调函数 } 

3.将数据转换为JSON格式,传递参数到postData(),执行函数ajax

var jsonData = JSON.stringify({ 'usr': $("#usr").val(), //用户名 'pwd': $("#pwd").val(), //密码 'imgUrl': $("#userThumb").attr("src") }); postData(urlRegister, jsonData, cbRegister); 

4.cbRegster()函数mongodb

function cbRegister(result) { console.log(result); if (result.code == 99) { //失败弹出错误信息 console.log("注册失败") } else { //成功就将输入的数据做为cookies存入 console.log("注册成功"); console.log(result.data); $.cookie('username', result.data.username, {expires:30}); $.cookie('password', result.data.password, {expires:30}); $.cookie('imgUrl', result.data.imgUrl, {expires:30}); $.cookie('id', result.data._id, {expires:30}); location.href = "/webchat"; //跳转到聊天界面 } } 

头像上传

$("body").on('change', '#uploadFile', preUpload); $("body").on('click', '#UploadBtn', doUpload); 
  • 表单传递的方式设置为POST
  • post路径设置为/uploadImage
  • 传递过来的数据类型为form
  • 最后若是上传成功,那么将id为userThumbsrc属性设置为传递过来的data
function doUpload() { //取出上传过来的文件 var file = $("#uploadFile")[0].files[0]; //与普通的Ajax相比,使用FormData的最大优势就是能够异步上传二进制文件。 var form = new FormData(); form.append("file", file); $.ajax({ url: "/uploadImg", //路径设置为uploadImage type: "POST", data: form, //数据格式为form async: true, processData: false, contentType: false, success: function(result) { startReq = false; if (result.code == 0) { //将id为userThumb的src属性设置为传递过来的data $("#userThumb").attr("src", result.data); } } }); } 

经过formidable这个npm包来实现图片的上传

  • 安装: npm install --save-dev formidable
  • 引入: var formidable = require('formidable');
  • 图片post/uploadImg
  • var form = new formidable.IncomingForm();新建一个form
  • form.uploadDir = "./public/thumb";设置文件存放的位置,本身事先定义好用来存放图片的文件夹
  • 这边有一个问题是上传图片以后,图片的路径是window的路径'/',而咱们在浏览器渲染要手动修改成''
router.post('/uploadImg', function(req, res, next) { var form = new formidable.IncomingForm(); var path = ""; var fields = []; form.encoding = 'utf-8'; form.uploadDir = "./public/thumb";//存放文件的位置 form.keepExtensions = true; form.maxFieldsSize = 30000 * 1024 * 1024; var uploadprogress = 0; console.log("start:upload----"+uploadprogress); //开始上传 form.parse(req); form.on('field', function(field, value) { console.log(field + ":" + value); }) .on('file', function(field, file) { path = '\\' + file.path; //获取文件的本地路径 }) .on('progress', function(bytesReceived, bytesExpected) { uploadprogress = (bytesReceived / bytesExpected * 100).toFixed(0); console.log("upload----"+ uploadprogress); //上传中 }) .on('end', function() { console.log('-> upload done\n'); //上传结束 entries.code = 0; entries.data = path; //将路径赋给data res.writeHead(200, { 'content-type': 'text/json' }); res.end(JSON.stringify(entries)); //将entries转换为JSON格式 }) .on("err",function(err){ //发生错误 var callback="<script>alert('"+err+"');</script>"; res.end(callback); }) .on("abort",function(){ //中断 var callback="<script>alert('"+ttt+"');</script>"; res.end(callback); }); }); 
  • 最后用post过来的user建立一个新的user数据对象
router.post('/register', function(req, res, next) { //添加用户 dbHelper.addUser(req.body, function (success, doc) { res.send(doc); }) }); 
exports.addUser = function(data, cb) { var user = new User({ username: data.usr, password: data.pwd, imgUrl: data.imgUrl }); user.save(function(err, doc) { if (err) { cb(false, err); } else { cb(true, entries); } }) }; 

这样整个上传的逻辑就已经写完了,接下来是添加一个朋友,和上面的作法一致。
惟一不一样的是,咱们在添加朋友的时候,通常都是相互之间都成为朋友的,因此在新建的时候要同时新建两个user

var friend_me = new Friend({ uid: data.uid,//本身的id fid: data.fid }); var friend_frd= new Friend({ uid: data.fid,//朋友的id fid: data.uid }); 

保存也须要同时保存两个新的对象
这里采用的是async的并行parallel操做,async的引入是经过var async = require('async');

async.parallel({ one: function(callback) { //保存本身 friend_me.save(function(err, doc) { callback(null, doc); }) }, two: function(callback) { //保存朋友 friend_frd.save(function(err, doc) { callback(null, doc); }) } }, function(err, results) { // results is now equals to: {one: 1, two: 2} cb(true, entries); }); 

消息的传递也须要同时建立两个消息,一个用来发给本身,另外一个是发给朋友,保存的方式和朋友一致

var message_me = new Message({ uid: data.uid, //本身 from: data.from, to: data.to, type: config.site.ONLINE,//在线 message: data.msg }); var message_friend = new Message({ uid: data.to, //朋友,data.to中保存的是朋友的fid from: data.from, to: data.to, type: data.type,//朋友须要判断是否在线 message: data.msg }); 

数据表的查询

方式一,findOne

User.findOne({username: data.usr }, function(err, doc) { ...... } }) 

方式2. find()+ exec(函数体), 其中execexecute执行下一个函数的意思

User.find()
     .exec(function(err, docs) { ...... }) 

方式3.

  • 两张表之间的查询,mongodb提供了populate方法用来查询两张表
  • 索引号也就是_id
  • populate()函数能够带两个参数,第一个参数是查询的外键对应的数据表,第二个能够规定须要查询的字段,好比'username'。
Friend.find({'uid': uid}) //找到uid对应的uid .populate('fid') //查找fid对应的user表 .exec(function(err, docs){ .... }) 

点对点聊天的实现

  • 首先用户加入到一个惟一的sessionId的房间
    socket.emit('join', sessionId);

  • 用户发送消息给socket
    socket.send(_id,fid,msg);

  • socket给uid发送消息msg
    io.to(uid).emit('msg', uid,fid,msg);

  • socket给fid发送消息msg
    io.to(fid).emit('msg', uid,fid,msg);

  • 服务器端监听消息

socket.on('message', function(uid,fid,msg){ var type;//在线仍是不在线 if(onlineList.indexOf(fid) === -1){//判断朋友是否是在线 type= config.site.OFFLINE;//用户不在线 //socket给本身发送消息不在线 io.to(uid).emit('msg', uid,fid,msg); }else { type=config.site.ONLINE;//在线 io.to(fid).emit('msg', uid,fid,msg);//socket给朋友发送消息 io.to(uid).emit('msg', uid,fid,msg);//socket给本身发送消息 } //构建一个data的json数据 var data = { "uid": uid, "from": uid,//本身 "to": fid,//朋友 "type": type, "msg": msg }; //调用dbHelper中的addMessage函数来将消息存放到数据库 dbHelper.addMessage(data, function(success,data){ ... }); }); 
  • 客户端socket.on('msg')来监听消息的发送
socket.on('msg', function(uid, fid, msg) { fromID = (_id == fid)?uid:fid; //接受到的消息的发送人id if (_id == fid) { fImg = $('#'+uid).children('img').attr('src');//获取到图片路径 message = $.format(TO_MSG, fImg, msg)//格式化为发送的消息 } else { message = $.format(FROM_MSG, _img, msg); //格式化为收到的消息 } $("#v"+fromID).append(message); //将消息append添加到前端 $("#v"+fromID).scrollTop($("#v"+fromID)[0].scrollHeight); }); 

如何使session惟一

若是用户与用户之间的聊天不是在同一个聊天室的话,那么他们的聊天消息会出错
因此咱们要为用户指定一个惟一的聊天室id

  • A先加入,A-a_id,B加入,b_id,
  • A->B: sid=a_id+b_id;
  • B->A: sid=a_id+b_id;
  • 这样session的值就惟一了
roomId = (uid>fid)?(uid+fid):(fid+uid);

历史消息的处理

存放消息

  • 首先在存放历史消息的时候,给历史消息一个属性type,表示朋友是否在线
  • 若是朋友在线,type设置为1
  • 若是朋友不在线,type设置为0
  • 把消息存放到数据库里面

取出离线消息

  • 用find()方法指定须要取出type为1的消息
  • 从form对应的表中取出响应的字段,添加到messageList数组
exports.getOfflineMsg = function (data, cb) { var uid = data.uid; Message.find({'uid':uid, 'type':'1'}) .populate('from') .exec(function(err, docs) { var messageList=new Array(); for(var i=0;i<docs.length;i++) { messageList.push(docs[i].toObject()); } cb(true, messageList); }); } 

将取出的消息渲染到前端的页面

var msg = $.format(TO_MSG, result[i].from.imgUrl, result[i].msg); ... $("#v"+fid).append(msg); 

设置离线消息为已读状态

  • var conditions = {'uid':uid, 'from':fid, 'type':'0'};
  • 按照条件查询数据库里面type为0的数据的每一条数据
  • var update = {$set :{ 'type' : '1'}};
  • 将数据库里面的数据的type类型设置为1,表示为已读状态
  • var options = { multi: true };
  • 使用multi:true`的属性将数据库里面所有的数据一次性更新
var uid = data.uid; var fid = data.fid; var conditions = {'uid':uid, 'from':fid, 'type':'0'}; var update = {$set :{ 'type' : '1'}}; var options = { multi: true }; Message.update(conditions,update,options, function(error,data){ if(error) { console.log(error); }else { data.id = fid; cb(true, data); } }) 

小礼物走一走,来简书关注我

 

赞扬支持
相关文章
相关标签/搜索