node.js+socket.io实现聊天室

前言

在web2.0的时代,我们可以通过ajax进行异步拉请求,而不需要等待请求完成才响应,这对于web体验而言是一大福音。但随着社会的进步,人们已经不满足这种拉数据的过程,特别是对于许多实时的应用,如聊天、信息监控、通知等,服务器想要给客户端传递一些信息的时候,这个时候就不能等待客户拉数据请求再把数据传送过去,而是要主动推送数据过去,从而出现了socket,一个双向的通信连接实现数据的交换。

websocket

在websocket出现以前,主要是两种技术实现socket通信,一种是长轮询,就是客户端不断发送请求,询问服务器端是否有新的数据要 传输过来;另外一种是基于ajax的Comet技术,服务器会阻塞请求,等到有消息的时候再返回。
这两种技术都是基于http协议进行实现的,直到html5的出现,从而定义了websocket协议,在websocketAPI定义中,客户端和服务器端只要进行一次握手操作,就可以建立一条socket通道,两者之间就可以直接进行数据传递。

websocket握手协议

在实现websocket连线过程中,需要通过浏览器发出websocket连线请求,然后服务器发出回应,这个过程通常称为“握手” (handshaking)。握手完成后双方处于连接状态,两者通信传输基于类似于事件监听机制。如下图可以看到请求的详细信息:
websocket图

socket.io介绍

Socket.io是对WebSocket进行封装的一种类库,其能够在多种平台进行扩展实现。
socket.io功能
我们可以看到,通过socket.io可以简单实现实时数据分析、聊天、二进制文件传输,多人共同协助攥写文档等功能。

socket.io使用

首先是通过npm进行安装:

1
npm install --save socket.io

然后在服务器端引入socket.io,并且监听端口:

1
var io = require('socket.io')(http);  //服务端

最后在客户端引入socket.io.js文件,并且同样声明一个io的对象:

1
2
<script src="/socket.io/socket.io.js"></script>
<script>var socket = io();</script> //客户端

Socket.io核心Api以及注意点

1、socket一般都是单页创建,如果出现跨页的话会出现问题。
2、服务器连接代码:

1
2
3
4
5
6
var io = require('socket.io').listen(http);
io.on('connection',function(socket){
//客户成功连接
console.log('a user connected!');
//一系列监听代码
}

3、客户端连接代码:

1
var socket = io.connect('ws://localhost:3000');

4、客户端事件

1
2
3
4
socket.emit('login',{"name":this.name}); //发送"login"信息,后面的对象为传递传递的信息对象
socket.on('login',function(obj){
//监听处理,监听服务器端发送"login"的信息,obj为服务器端传送的信息对象
});

5、服务端事件
每当一个新的用户连接,都会产生一个socket对象,用来表示此用户的连接。

1
2
3
4
5
6
7
io.emit('login',{'onlineList':onlineList,'onlineCount':onlineCount,'user':obj.name});//通知所有人,包括当前用户
socket.emit('test',{'msg':"11"});//给当前用户发信息
socket.broadcast.emit('other');//给除了当前连接以外的所有人发信息
socket.send('hi');//message接收,对于当前用户,相当于socket.emit('message',{'msg':"11"});
socket.on('login',function(obj){
//服务器端监听客户端发送的信息,并做处理
})

对于客户端来说比较简单,一个就是通过emit进行发送信息,另外一个就是通过on来监听接收信息,但对于服务器而言,需要注意的是,服务器相当于一个中转站,将某一个人的信息传递给其他人,至于传递给谁,上面的api可以清楚看出,监听事件比较简单,都是socket.on就可以进行监听,但是对于发送信息的话就有区别了,一般来说,如果发送给所有人,就用io.emit,但如果发送给某个人,就用socket.emit(之前说过,每当一个新的用户连接都会产生一个特定的socket对象代表这个连接的标志,每个人都是不同的socket,这也为下文点对点发送信息具有很大作用)。这边有两个特殊的,就是socket.broadcast.emit(给除了当前连接以外的所有人发信息)以及socket.send(‘hi’)(客户端只能通过监听”message”才可以接收)。

socket.io的命名空间

socket.io还具有命名空间的概念,通过不同的命名空间可以开启不同的socket通道,可用作对接不同的客服:

1
2
3
4
5
6
7
8
var chat = io
.of('/chat')
.on('connection', function (socket) {
socket.emit('a message', {
});
chat.emit('a message', {
}); //相当于io.emit
});

对于客户端的话:

1
var socket  = io.connect("ws://localhost/chat");

这样的话,对于客户连接服务器而言,他们相当于进入了不同的小房间,然后每个小房间都是独立的,对于消息的传递也只能在这个小房间进行传递。

node.js实现聊天室

对于一个聊天室而言,无非就是实现下列几个功能:
1、环境搭建(express+mongodb)
2、注册登录
3、在线人数、在线列表展示
4、登录、登出消息提示
5、群发信息
6、单发信息
对于1、2点的话不是这篇文章所涉及的,下面说下剩下的4个的实现:

①在线人数、在线列表展示

我的方法是,在服务器端维护两个全局变量,一个就是在线人数,一个就是socket列表,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var onlineCount = 0;//在线人数
var socketList = [];
var onlineList = {};
io.on('connection',function(socket){
socket.on('login',function(obj){
if(!socketList[obj.name]){
socketList[obj.name] = socket;
onlineList[obj.name] = obj.name;
socket.name = obj.name;
onlineCount++;
io.emit('login',{'onlineList':onlineList,'onlineCount':onlineCount,'user':obj.name});//通知所有的人,包括当前用户
}
});
})

在这里,onlineCount记录在线人数,onlineList记录在线列表,至于socketList下面会派上用场,在这里,其实同时实现了登录、登出的消息提示,每个人在连接服务器的时候,在js都可以发出一个’login’的信息,告诉服务器:“我登录了!”,然后服务器会将新加入的成员加入在线列表,然后通知所有人(包括登录的人,告诉登录的人现在在线的有多少人,分别是谁),同理,当某个人登出的时候,同意发送一个’logout’告诉服务器,然后服务器会将这个人信息移除,并且告诉剩下的人,**退出了,当然,有些情况下,客户可能是直接关闭浏览器,无法发送’logout’信息,其实,socket本身有监听一个特殊事件disconnect,所以可以这样写监听退出事件:

1
2
3
4
5
6
7
8
9
10
11
socket.on('disconnect',function(){
console.log(socket.name+'disconnect');
if(onlineList[socket.name]){
delete onlineList[socket.name];
if(socketList[socket.name]){
delete socketList[socket.name];
}
onlineCount--;
io.emit('logout',{'onlineList':onlineList,'onlineCount':onlineCount,'user':socket.name});
}
})

处理用户列表、用户人数的同时,还需要将这个人的name一同通知剩余的人,可以免去客户端进行对比排查到底是谁退出的操作,对于socket.name的话也是在登录监听事件的时候进行创建的。

②群发信息

群发信息相对简单,之前的api有提到过,只要使用io.emit就可以实现通知所有人,或者使用socket.broadcast.emit实现通知除了发送信息的人除外的所有人信息,对于服务器端:

1
2
3
4
socket.on('msg',function(obj){
io.emit('msg',{'msg':obj.msg,'user':obj.name});
//或者socket.broadcast.emit('msg',{'msg':obj.msg,'user':obj.name});
}

对于客户端同样是一个发送事件和一个监听事件。

③单独发信息

群发信息比较简单,那么单独发信息呢,其实介绍api的时候有提到过,对于服务器端来说,io和socket的区别,io可以想象成所有连到这个服务器的群体,socket某一个特定的人,所以群发信息用io.emit,所以,要想单独发信息的话,肯定是要用socket,那么socket如何区别谁是谁呢,你是否记得登录监听事件的时候维护过socketList列表,其实这个列表就是为了区别每一个不同socket通道,然后通过socket.send就可以对某个人发送信息,然后每个人在客户端需要监听”message”,切记,这个是内置事件,不能用其他进行替换,否则监听不了:

1
2
3
socket.on('msg',function(obj){
socketList[obj.to].send({'msg':obj.msg,'user':obj.name});
})

在客户端只要在发送信息的时候将想要发信息的用户名带上就可以了,然后监听message事件,看别人是否会发信息给自己就可以了。

结语

聊天室只是socket.io之中一个小小的应用,正如前面所说,还可以实现二进制文件传输、性能监控等许多了不起的功能,更多好玩的等待你的挖掘。
聊天室全部的代码可以在https://github.com/wzl610/chat-online中找到。