用Socket开发的一枚小型实时通信App

摘要:
varport=进程.env.PORT | |8081;varo=sio.listent(app.listent)(端口));3.是导入数据库还是继续在app.js.app中写入。use(function(req,require('./config')(app,将app和io作为参数传递给模块以供进一步使用。varexpress=require('express');

Socket 英文原意是插座。

在网络世界里,

当一台主机温柔而体贴的同时提供多个服务时,

每个服务被绑定在一个端口上,

而每个端口就好像一个小插座。

用户们连接对应的插座去获取相应的服务。

在Node.js中,使用的是socket.io来实现Realtime的通信。

当程序两端实现数据通信时,

每一端便化身为一枚可爱的Socket了。

本示例使用Express做框架,

数据库使用Mongo(后面附带常用查询语句),

渲染引擎使用Jade。

1. 安装 dependencies:

在package.json文件里,指定所需依赖。

除了body-parser,cookie-parser,debug,serve-favicon之类常用依赖之外,

需安装的还有但不限于:

express,jade,mongo-client,mongodb,monk,

morgan,mpromise,mongoose,mongoskin……

By the way,由于实时通信是面向多用户,

如果你想给每个用户设置唯一的头像和Email,

可以考虑使用gravatar,本例为方便叙述,不讲gravatar了。

当然还有本篇的主角大人:socket.io

然后就可以妥妥的sudo npm install了。

2. 配置主程序 app.js

你也可以叫它index.js或者server.js,

总之它是程序启动的入口程序。

首先仍然是require所需的模块们:

var express = require('express');

var app = express();

var sio = require('socket.io');

指定端口:

var port = process.env.PORT || 8081;

然后是socket的监听:

var io = sio.listen( app.listen(port) );

3. 引入数据库

还是在app.js里继续写。

要使用数据库,必须先引入数据库模块。

var mongo = require('mongodb');

var monk = require('monk');

看到monk总想到和尚……

这里就把它理解为连接Mongo的一个小中间件。

在此不深究。

有了monk模块,就可以连接数据库了:

var db = monk('127.0.0.1:27017/myRealtimeApp');

其中27017是Monk默认指定的端口,后面是数据库名。

使用数据库之前,还需要将数据库传递给app:

app.use(function( req, res, next ){

  req.db = db;

  next();

});

其中的next即是执行此段代码后面紧跟着的程序。

为了方便维护,我把config的内容和routes规则写在另外的JS文件里。

因此在next后面要引入这两个模块:

require('./config')(app, io);

require('./routes')(app, io);

appio当作参数传递到模块里供继续使用。

至此,app.js基本完成了。

4. 配置config文件模块

在config里基本就是bodyParser、cookieParser、Path、favicon以及视图引擎的配置了。

不解释了,代码如下:

var express = require('express');

var cookieParser = require('cookie-parser');

var bodyParser = require('body-parser');

var path = require('path');

var favicon = require('serve-favicon');

module.exports = function(app, io){

  app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));

  app.use(cookieParser());

  app.use(bodyParser.json());

  app.use(bodyParser.urlencoded({ extended: false }));

  app.set('views', __dirname + '/views');

  app.set('view engine', 'jade');

  app.use(express.static(__dirname + '/public'));

};

5. 基本的路由规则

由于App很小,所以路由规则和服务端的业务逻辑我都写在了routes.js里。

如果程序更为复杂,最好能够分开写之。

routes.js里,先配置基本的几个路由:

app.get('/', function(req, res){

  res.render('login', { // 对应的在views目录里要有login.jade页,并在页面里有变量title和desc

    title: 'Realtime App with socket.io',

    desc: 'Welcome, this is V2.0'

  });

});

补充说明,本例的逻辑是在首页直接显示登录,

你也可以不这样做,比如设计一个单独的首页,

而登录页采用'/login'。说明毕。

有登录页,就有注册页,GET '/reg'与之类似,不赘述了。

在聊天页面,可以在URL上设置room的id,确保所有登录用户进入的是同一个room:

app.get("/chat/:id", function(req, res){

  res.render("chat", { // 在views目录里对应要有chat.jade页面,并有变量title

    title: 'Whatever……'

  });

});

6. 注册登录与数据库的通信

注册和登录的POST方法,

是接收客户端的用户信息并需要与数据库产生数据交换的。

因此有必要详细说明一下:

首先是注册:

app.post( '/register', function(req, res){

  var nickname = req.body.nickname;

  var phone = req.body.phone;

  var psw = req.body.password; // 从客户端获取用户名、电话、密码

  var db = req.db;

  var collection = db.get('usercollection'); // Mongo里的collection可以理解为table

  collection.insert({ //向表里插入一条用户数据

    nickname: nickname,

    phone: phone,

    password: psw

  }, function( err, doc ){ //写数据库后的回调,err有值表示读写失败

    if(err) {

      res.send('error'); 

    }else{ // 没有err表示读写成功,即用户注册成功

      var userInfo = {};

      var _sessionID = req.sessionID;

      userInfo['nickname'] = doc.nickname;

      userInfo['userphone'] = doc.phone;

      res.cookie( 'webchat_nickname', doc.nickname );

      res.cookie( 'webchat_token', _sessionID );

      res.cookie( 'webchat_roomid', staticRoomId ); // 用Cookie存储用户信息不是唯一的方法

      res.send( userInfo ); // 用户信息返回给客户端

    }

  });

});

需要注意的是:

接收POST请求后的返回,必须res.end或res.send(包含end),

否则在浏览器里的这个请求会一直pending。

再来就是登录:

app.post( '/login', function(req, res){

  var phone = req.body.username;

  var psw = req.body.password;

  var db = req.db;

  var collection = db.get('usercollection');

  var result = collection.findOne( {phone: phone, password: psw}, function(err, doc){

    if(!doc){

      res.send("error");

    }else{

      var nickname = doc.nickname;

      var userPhone = doc.phone;

      var _sessionID = req.sessionID;

      var userInfo = {};

      userInfo['nickname'] = nickname;

      userInfo['userphone'] = userPhone;

      userInfo['roomId'] = staticRoomId;

      res.cookie( 'webchat_nickname', nickname );

      res.cookie( 'webchat_token', _sessionID );

      res.cookie( 'webchat_roomid', staticRoomId );

      res.send( userInfo );

    }

  });

});

有登录,就有登出,具体实现此不赘述。

只是登出时要记得清空Cookie。

7. 附加福利:Mongo的常用查询语句

先在本地安装Mongo,然后使用命令“mongo”进入Mongo。

进入后会先显示:

MongoDB shell version: 2.0.4

connecting to: test // 默认连接到了 test 数据库

常用语句如下:

a:查看所有数据库:show dbs

b:进入某一个数据库:use myRealtimeApp

c:查看当前数据库的所有表:show collections // Mongo里叫collection,我就理解为表……

d:查看各表状态:db.printCollectionStats() // 会列出所有表的状态

e:查询某个表的数据:db.usercollection.find() //这样输出的结果没有格式化

f: 查询某个表的数据:db.usercollection.find().pretty() //输出结果格式化了

g:用某个特定条件查询某个表的数据:db.usercollection.find({'nickname': 'Alex'})

h:存储某条数据:db.foo.save({'name': 'aaa'})

i:删除某条数据:db.foo.remove({'name': 'aaa'}) 或 db.foo.remove()

j:删除某个表:db.mycollection.drop()

8. Socket的连接和事件的注册与触发

到这里才是真正重要的内容:

在服务端连接Socket,并注册load、login等事件的回调,供客户端触发时调用。

同时,也要在客户端连接Socket,并注册startChat、receive等事件的回调,供服务端触发时调用。

这有点像一张网,服务端和客户端分别注册了若干事件,供对方调用。

同时它们也在调用对方所注册的事件。

具体实现如下:

先在服务端连接Socket:

var chat = io.of("/socket").on("connection", function(socket){

  // 在这个回调里去注册socket的 'load', 'login', 'disconnect', 'msg' 等事件:

  socket.on('load',function(data){

    if( chat.clients(data).length === 0 ){ // 获取用户数量,但这个clients不能缓存,每次都必须用chat获取

      socket.emit('peopleinchat', {number: 0}); // "peopleinchat"在客户端注册,这里触发之,并将人数传到客户端

    } else if ( chat.clients(data).length === 1 ) { // 用户数量1个人

      socket.emit('peopleinchat', {

        number: 1,

        user: chat.clients(data)[0].username, // 用户姓名传到客户端

        id: data // room的id传到客户端

      });

    } else if ( chat.clients(data).length === 2 ) { // 用户数量2个人

      socket.emit('peopleinchat', {

        number: 2,

        user: [chat.clients(data)[0].username, chat.clients(data)[1].username], // 两个人及以上,user是数组

        id: data

      });

    } ...... else { // 可以设置一个最多人数的限制,超过时提示人数满员

      socket.emit('peopleinchat', { state: 'too many people here!' });

    }

  });

  socket.on('login',function(data){

    var maxPeopleLength = 10; // 自定义最大容纳人数

    if( chat.clients(data.id).length < maxPeopleLength ){ //必须在人数限制内

      socket.username = data.userName;

      socket.room = data.id; // 这个必须有

      socket.join(data.id); // join方法会使clients.length+1

      if( chat.clients(data.id).length >= 2 ) { // 只有 >= 2个人时才会触发startChat

        dealWithClients(data); // 每 login(增加)一个用户,要触发一次startChat并将用户数组传给客户端

      }

      // if length == 3 ... 不赘述了

    }

  });

  // dealWithClients 方法定义如下:

  function dealWithClients(data) {

    var usernames = []; // 包含当前登录用户的一个数组

    chat.clients(data.id).forEach(function(item, index){

      usernames.push( item.username ); // 拿到所有用户,push到这个数组里

    }); 

    chat.in(data.id).emit('startChat', { // 只有 >= 2个人时才会触发startChat

      check: true,

      user: usernames, // 所有用户

      number: usernames.length, // 人数

      id: data.id // room 的 id

    }); 

  };

  socket.on('msg',function(data){ // 这里注册发消息的事件(当客户端点击发送按钮时,触发之)

    socket.broadcast.to(socket.room).emit('receive', { // 接收消息的注册是在客户端,这里触发之

      msg: data.msg, // 消息内容

      user: data.user, // 发消息的用户

      time: new Date().getTime() // 发消息的时间,用服务器时间,返回给客户端供渲染

    });

  });

  socket.on('disconnect',function(data){

    // 失联后的回调,没写……

  });

}); // End of connection

9. Socket在客户端的事件注册与接收

刚才说的都是在服务端的事件注册和触发。

对应的在客户端也会有相应的事件触发和注册。

下面的代码就是写在Browser客户端里的JS了:

首先要在页面里引用socket的包包:socket.io.js

然后在JS里获取socket对象并缓存:

var socket = io.connect('/socket');

从URL里获取room的id,如果用户手动修改URL,则需响应错误:

var RoomId = Number(window.location.pathname.match(//chat/(d+)$/)[1]);

从Cookie里获取登录后的自己的用户名:// 用于在页面中渲染时区分于其他用户

var me = $.cookie("_nickname"); // 这不是必需的

下面才是重头戏:

socket.on("connect", function(){

  socket.emit("load", RoomId); // Connect成功后触发load事件

  // 这里就会执行注册在服务端的 load 事件回调里的代码了。

});

socket.on('startChat', function( data ){ // 这里注册的就是startChat

  renderUsers( data.number, data.user ); // 渲染当前登录的用户,渲染逻辑这里不贴了

});

socket.on("peopleinchat", function(data){ // 这里注册peopleinchat

  var howManyPeople = data.number;

  if( howManyPeople == 0 ){

    makePopAlert('No one here...', 0, 'Ok'); // 调用自己写的一个带样式的弹窗组件

  }else if( howManyPeople == 1 ){

    renderUsers( 1, data.user ); // 渲染用户

  }else if( howManyPeople == 2 ){

    renderUsers( 2, data.user );

  }else if( howManyPeople == 3 ){

    renderUsers( 3, data.user );

  }else{ ....

    // Too many people

  } 

});

socket.on("receive", function(data){ // 监听收到消息后的处理

  renderMsg(data); // 渲染消息的方法,自己定义,这里不贴了

});

至于发送消息,在页面里的[发送]按钮上绑定Click事件,

并触发 socket.emit('msg', msgData); // msgData根据服务端所需的各个属性自己组装

10. 小结

以上是使用Socket实现的最简单的实时通信流程。

基本思路就是在服务端和客户端分别引入Socket,

并按业务逻辑在不同方向监听事件,在对应方向触发之。

要完善实时通信的功能,还有很多可以继续写的。

这里不写了。。。

免责声明:文章转载自《用Socket开发的一枚小型实时通信App》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇C语言刷 堆(优先队列)jquery设置控件位置的方法下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

Slurm任务调度系统部署和测试(源码)(1)

1. 概述1.1 节点信息2. 节点准备3. 部署NTP服务器4. 部署LDAP服务器5. 部署Munge认证服务6. 部署Mysql数据库服务7. 部署slurm7.1 创建slurm用户7.2 挂载全局文件系统7.3 slurm下载7.4 编译安装8. slurm配置8.1 配置slurm.conf8.2 配置slurmdbd.conf8.3 配置c...

MongoDB常用操作整理

Mongodb:是一种NoSQL数据库,NoSQL:Not Only SQLSQL: 数据表->JDBC读取->POJO(VO、PO)->控制层转化为JSON数据->客户端 这种转换太麻烦了,如果有直接数据库存放要显示的内容,就能够省略所有需要进行转换的过程。 所以在实际开发中,往往除了关系型数据库之外还要提供一个NoSql数据库,...

oracle Database Link

1 Database Link 的创建: 有两个数据库服务器A/B, 其中A的IP地址为172.20.36.245, 服务器B为本机。服务器B上的数据库实例名为ORCL,在本机上的服务监听配置上有服务器A上实例配置: BIWG_TEST = (DESCRIPTION = (ADDRESS = (PROTOCOL = TCP)(HOST = 172.20...

JDBC 基础入门

由于我也是初学参考的是网上的或者是培训机构的资料所以可能会有错误的信息,仅供参考 一、什么是JDBC(Java Data Base Connectivity)? java程序连接数据库,JDBC是由SUN公司提出的一组规范,这组规范主要由一组接口构成,主要作用就是访问数据库。 二、JDBC核心思想【思想重要】     三、核心API【重点】      ...

EFCore数据库迁移命令整理

因为现在用.net core 开发新项目,过程中需要经常涉及到数据命令的迁移,今天分别整EFCore 的两种迁移数据库的方式  1 程序包管理器控制台 , Package Manager Console(PMC)        -如果你用visual studio 开发建议使用PMC迁移方式,该方式是同时支持efcore和原先的ef 迁移的 2 命令行工具...

数据库增删改查

一. 请写出数据库分离和附加的步骤   选中数据库右键→任务→分离   选中数据库右键→附加 二. 请写出数据库导出SQL脚本的步骤   选中数据库右键→生成脚本 三. 请写出SQL Server的四种完整性约束   实体完整性束  域完整性约束  引用完整性约束  自定义完整性约束 四. 如何设置表的主键和标识列   右键 △ 设置主键   标识列在标识...