socket.io

作者: ofelia_why | 来源:发表于2016-10-16 19:48 被阅读1817次

    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);
        }
    })
    

    相关文章

      网友评论

        本文标题:socket.io

        本文链接:https://www.haomeiwen.com/subject/egynyttx.html