socket.io

socket

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

依赖的外部包

express、socket.io

安装

  • 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是一种异步的请求,当用户每次输入一定的值,服务器都会把这个值传递过来

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

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

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

  • 如果成功,那么success,执行后面的function();

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

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.将上面的一段代码提炼出骨干

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(),执行函数

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

4.cbRegster()函数

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

推荐阅读更多精彩内容