用Socket开发的一枚小型实时通信App
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);
把app和io当作参数传递到模块里供继续使用。
至此,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的更多相关文章
- IOS socket开发基础
摘要 详细介绍了iOS的socket开发,说明了tcp和udp的区别,简单说明了tcp的三次握手四次挥手,用c语言分别实现了TCPsocket和UDPsocket的客户端和服务端,本文的作用是让我们了 ...
- Android Socket 开发技术
根据之前的经验,应用软件的网络通信无非就是Socket和HTTP,其中Socket又可以用TCP和UDP,HTTP的话就衍生出很多方式,基础的HTTP GET和POST请求,然后就是WebServic ...
- Socket开发
Socket开发框架之消息的回调处理 伍华聪 2016-03-31 20:16 阅读:152 评论:0 Socket开发框架之数据加密及完整性检查 伍华聪 2016-03-29 22:39 阅 ...
- 练习题|网络编程-socket开发
1.什么是C/S架构? C指的是client(客户端软件),S指的是Server(服务端软件),C/S架构的软件,实现服务端软件与客户端软件基于网络通信. 2.互联网协议是什么?分别介绍五层协议中每一 ...
- socket 开发 - 那些年用过的基础 API
---------------------------------------------------------------------------------------------------- ...
- 网络编程-socket开发
练习: 1.什么是C/S架构? 2.互联网协议是什么?分别介绍五层协议中每一层的功能? 3.基于tcp协议通信,为何建立链接需要三次握手,而断开链接却需要四次挥手 4.为何基于tcp协议的通信比基于u ...
- andriod socket开发问题小结
andriod socket开发问题小结 个人信息:就读于燕大本科软件project专业 眼下大四; 本人博客:google搜索"cqs_2012"就可以; 个人爱好:酷爱数据结构 ...
- c socket 开发测试
c语言异常 参照他人代码写一个tcp的 socket 开发测试 异常A,在mac osx系统下编译失败,缺库转到debian下. 异常B,include引用文件顺序不对,编译大遍异常 异常C,/usr ...
- 一篇看懂Socket开发
Socket[套接字]是什么,对于这个问题,初次接触的开发人员一般以为他只是一个通讯工具. Socket接口是TCP/IP网络的API,Socket接口定义了许多函数或例程,程序员可以用它们来开发 T ...
随机推荐
- python 本地变量和全局变量 locals() globals() global nonlocal 闭包 以及和 scala 闭包的区别
最近看 scala ,看到了它的作用域,特此回顾一下python的变量作用域问题. A = 10 B = 100 print A #10 print globals() #{'A': 10, 'B': ...
- GBDT 将子树结果当成lr输出
http://scikit-learn.org/stable/auto_examples/ensemble/plot_feature_transformation.html#example-ensem ...
- 如何获取某个网站的favicon.ico
http://moco.imooc.com/player/report.html 今天看到这个网站上,左侧的小图片挺好看的,想弄下来,检查源码,也没有看到 <head> <meta ...
- distinct top執行順序
select distinct top 3 from table; 先distinct后top
- python,遍历文件的方法
在做验证码识别时,识别时需要和库里的图片对比,找到最接近的那个图片,然后就行到了用与图片一致的字符命名,获取文件的名称,去将图片的名称读出来作为验证码.以下是我通过网上的资料总结的三种文件遍历的方式, ...
- django提交post请求
在做post的时候,view.py用到了下面的方法,如果是POST的method,就通过request.POTST['XX']获得html中name为XX的值,然后将值save到数据库里 models ...
- C#aspx页面前台使用<%=%>无法取到后台的值
检查是不是有拼接问题,正常public和protected修饰的字段或属性均可使用<%=%>.另外,加载(Page_Load)时有没有给它们赋初始值? 答 1)前台页面只能调用本后置代码的 ...
- tf.unstack\tf.unstack
tf.unstack 原型: unstack( value, num=None, axis=0, name='unstack' ) 官方解释:https://tensorflow.google.cn/ ...
- 几种TCP连接终止
在三次连接完成后,accept调用前,客户机发来RST. Berkeley实现将完全在内核中处理,不通知. 而SVR4实现将返回一个错误EPROTO,而POSIX指出应该是ECONNABORTED,后 ...
- 【校招面试 之 C/C++】第7题 C++构造函数不能是虚函数的原因
1.虚拟函数调用只需要“部分的”信息,即只需要知道函数接口,而不需要对象的具体类型.但是构建一个对象,却必须知道具体的类型信息.如果你调用一个虚拟构造函数,编译器怎么知道你想构建是继承树上的哪种类型呢 ...