使用Socket.IO类库实现WebSocket通信

Socket.IO是什么? Socket.IO 是一个类库。 可以做跨平台(浏览器或移动设备),支持多种连接方式自动切换,在做即时通讯方面的开发也很方便,而且能和expressjs提供的传统请求方式很好的结合。这个类库Node.js中net模块的功能。包括可以WebSocket通信,XHR轮询,JSONP轮询,它会自动根据浏览器选择适合的通讯方式,从而让开发者可以聚焦到功能的实现而不是平台的兼容性,同时Socket.IO具有不错的稳定性和性能。这篇文章,主要以实例的形式,展示如何使用socket.io类库,如何利用socket.io实现客户端和服务器端的通信,如何与express结合,如何使用不同命名空间等等。学习它的目的,是我想做一个node聊天室。


搭建测试服务端环境

npm init

利用npm init命令生成package.json文件,填入相应的信息。大概是会生成下面的一些样子。

1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "node-chat",
"version": "1.0.0",
"description": "chatroom based on WebSocket",
"main": "index.js",
"keywords": [
"chatroom",
"node"
],
"author": "Seven",
"license": "ISC",
//.......

安装相应的包
1
2
npm install --save express
npm install --save socket.io

看到文件夹已经生成相应的node_modules目录了。

测试

先测试下,创建index.js测试服务器是否搭建好:

1
2
3
4
5
6
7
8
9
10
11
var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);

app.get('/',function(req,res){
res.send('<h1>Welcome</h1>');
});

http.listen(3000,function(){
console.log('listen on *:3000');
})

这时候可以在localhost上访问3000端口,这样就可以在页面上看到 welcome字样,这样简单的测试就完成了。


socket.io类库的使用方法

注意:Socket.IO类库的使用方法比较简单,创建一个Socket.IO服务器即可,但是该服务器依赖于一个已经创建好的HTTP服务器。所以在HTTP服务器运行后,使用listen方法为该HTTP服务器加一个Socket.IO服务器。

1
2
3
//先创建一个server
var sio = require('socket.io');
var socket = sio.listen(server);

listen 方法返回的就是一个Socket.IO服务器。

当Socket.IO服务器创建完成后,客户端和服务器端建立联系时,就会触发Socket.IO服务器的connection事件。

然后通过监听该事件回调函数的方法指定当客户端与服务器端建立连接时所需要执行的处理。

1
2
3
4
5
6
7
//参数socket是服务器用于与客户端建立连接的socket端口对象
socket.on('connection',function(socket){
//回调函数,这里面可以通过传入的soket参数监听事件
socket.on('message',function(){})
socket.send(msg)
socket.on('disconnect',function(){})
})


一个简单的完整实例

下面的代码展示了如何为一个HTTP服务器附加一个Socket.IO服务器。当HTTP服务器接收到客户端的请求时,读取index.html文件中的内容并返回给客户端。当客户端与Socket.IO服务器端建立连接后,向客户端发送‘你好’消息字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var http = require('http');
var sio = require('socket.io');
var fs = require('fs');

//先createSever
var server = http.createServer(function(req,res){
res.writeHead('200',{'Content-Type':'text/html'});
res.end(fs.readFileSync('./index.html'));
});

server.listen(3000,function(){
console.log("listen on 3000 port");
});

//listen上面的server创建一个socket.io服务器
var socket = sio.listen(server);

socket.on('connection',function(socket){
console.log('客户端已建立');
socket.send("hello");

socket.on('message',function(msg){
console.log('接收到一个消息:'+msg);
});

socket.on('disconnect',function(){
console.log('客户端连接断开');
})
});

index.html文件如下:

1
2
3
4
5
6
7
8
9
10
11
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect();
socket.on('message',function(data){
console.log(data);
socket.send("messages are received");
});
socket.on('disconnect',function(){
console.log("服务器端断开连接");
});
</script>

现在,你肯定会有疑问,这个前面引入的文件是什么???/socket.io/socket.io.js由服务器端Socket.IO类库自动提供,所以咱们不需要再客户端实际放置一个socket.io.js文件

在脚本代码中,首先我们利用/socket.io/socket.io.js类库中的io.connect方法连接到服务器端socket.IO服务器。var socket = io.connect();io.connect方法返回了一个socket对象。代表一个用于服务器建立连接的客户端socket端口对象。

客户端与服务器端建立连接后,触发了我们服务器端的send事件,这时候服务器向客户端发送消息,又触发了客户端socket的message事件。这时候客户端会log出收到的data,显示在浏览器控制台上。然后客户端又给服务器端发送了一个messages are received消息,触发了服务器端的message事件,并打印出接收到一个消息:messages are received

当服务器端和服务器端断开联系时,又调用disconnect事件。

这里也可以看到,客户端和服务器端处理消息机制完全相同,因为Socket.IO确保了客户端和服务器端,共享了相同的API。


socket.io的emit方法

在使用Socket.IO类库时,服务器端和客户端之间除了可以互相发送消息之外,也可以使用socket端口对象的emit方法,互相发送事件。

1
socket.emit(event,data,[callback])

event表示:参数值为一个用于指定事件名的字符串。data参数值:代表该事件中携带的数据。这个数据就是要发送给对方的数据。数据可以是字符串,也可以是对象。callback参数:值为一个参数,用于指定一个当对方确定接收到数据时调用的回调函数。

一方使用emit发送事件后,另一方可以使用on,或者once方法,对该事件进行监听。once和on不同的地方就是,once只监听一次,会在回调函数执行完毕后,取消监听。

1
2
socket.on(event,function(data,fn){})
socket.once(event,function(data,fn){})

利用emit发送事件

再体会下emit的三个参数:首先是服务器端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var socket = sio.listen(server);

socket.on('connection',function(socket){
console.log('客户端已建立');

socket.emit('setName','Seven',function(data1,data2){
console.log('客户端传来的数据1>'+data1);
console.log('客户端传来的数据2>'+data2);
});

socket.on('disconnect',function(){
console.log('客户端连接断开');
})
});

再是客户端:

1
2
3
4
5
6
7
8
9
10
11
12
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect();
socket.on('setName',function(data,fn){
console.log(data);
//fn为当对方确认收到数据时,调用的回调函数。
fn("Jason","Jade");
});
socket.on('disconnect',function(){
console.log("服务器端断开连接");
});
</script>


在Express中使用Socket.IO

先前我们测试环境的时候,就有用到express,我们可以通过Express很容易的使用Socket.IO。
在原来的版本中,我们可以是socket对象的set和get方法来设置和获得数据。但现在已经在新版本中废除了,所以具体这两个方法就不讲解了。底下还是有老写法的示例的。改进的方法是再socket对象上直接设置属性和值。具体可以看报错解决这篇博客。

这里例子跟express的关联到不是很大,只是简单示例,我们就是利用var server = http.createServer(app);把express关联起来了。然后我们就可以去调用express的api。

服务器端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
var express = require('express');
var http = require('http');
var sio = require('socket.io');

var app = express();
var server = http.createServer(app);

app.get('/',function(req,res){
res.sendFile(__dirname+'/index.html');
});

server.listen(3000,function(){
console.log("listening on port 3000");
});

var socket = sio.listen(server);

socket.on('connection',function(socket){
socket.on('set nickName',function(name){
//socket.set在1.x版本已经被废除
// socket.set('nickName',name,function(name)){
// socket.emit('send nickName',name)
// }
socket.nickName = name;
socket.emit('send nickName',name)
});
socket.on('get nickName',function(){
//socket.get在1.x也被废除
// socket.get('nickName',function(err,name){
// if(err){
// socket.emit('err',err.message);
// }else{
// socket.emit('send nickName',name);
// }
// })
var name = socket.nickName;
if(name){
socket.emit('send nickName',name);
}
});
})

客户端代码:有一个可以设置昵称的,当设置了后,我们保存到socket对象中。

1
2
3
<input type="text" id="nickname">
<input type="button" onclick="setNickName()" value="设置昵称">
<input type="button" onclick="getNickName()" value="获取昵称">
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect();
function setNickName(){
socket.emit('set nickName',document.getElementById("nickname").value);
}
function getNickName(){
socket.emit('get nickName');
}
socket.on('send nickName',function(data){
console.log("My name is "+data);
});
socket.on('err',function(data){
console.log('服务器出现错误:'+data);
})
</script>

广播消息

当多个客户端与服务器建立连接后,Socket.IO服务器具有一个sockets属性,属性值为所有与客户端建立连接的socket对象。可以利用该对象的send方法或者emit方法向所有客户端广播消息,如下:

1
2
IO.sockets.send("user connected");
IO.sockets.emit("login",names);

然后,当我们某个客户端与服务器建立连接以后,用于与该客户端连接的socket对象,就有一个broadcast对象,代表所有与其他Socket.IO客户端建立连接的socket对象。可以利用该对象的send方法和emit方法向所有其他客户端广播消息。如下:

1
2
socket.broadcast.send('user connected');
socket.broadcase.emit('login',names);

下面我们看一个广播的小实例,可以当做聊天室的雏形了。先看下最后的结果:

广播

例子中我们看的很清楚,当有不同的用户登陆进来的时候,用户列表是不断的更新的。可以动态的显示出在线的人数。主要就是用到的我们上面的方法io.sockets.emit('login',names)

服务器端的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
//.....跟前面一样(省略)
var io = sio.listen(server);

var names = [];

io.on('connection',function(socket){
socket.emit('login',names);

socket.on('login',function(name){
names.push(name);
io.sockets.emit('login',names);
});
})

客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
用户名:<input type="text" id="username">
<input type="button" value="登陆" onclick="add()">
<p id="result"></p>
<script src="/socket.io/socket.io.js"></script>
<script>
var socket = io.connect();
socket.on('login',function(names){
var str = "";
names.forEach(function(name){
str += "用户 "+name+" 已登陆。<br />";
})
document.getElementById('result').innerHTML = str;
})

function add(){
socket.emit('login',document.getElementById('username').value);
}
</script>

主要就是这段代码:当客户端有人登陆的时候,emit发送数组人名的信息给客户端。注意这里的注释,是io.sockets.emit的另外一种替代的写法。

1
2
3
4
5
6
socket.on('login',function(name){
names.push(name);
io.sockets.emit('login',names);
//socket.emit('login',names);
//socket.broadcast.emit('login',names);
});


使用命名空间

如果开发者想要在一个特定的应用程序中完全控制消息与事件发送,只需要一个默认的/就够了,但是如果开发者需要将第三方应用程序作为第三方服务提供给其他应用程序,则需要为一个用于与客户端连接的socket端口定义一个独立的命名空间。

在Socket.IO中,可以使用Socket.IO服务器对象的of方法来定义命名空间。代码如:io.of(namespace)

命名空间

服务器端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//.....跟前面一样(省略)

var io = sio.listen(server);

var chat = io.of('/chat').on('connection',function(socket){
socket.send('welcome to namespace:chat');
socket.on('message',function(msg){
console.log('chat namespace has received msg: '+msg);
})
});

var news = io.of('/news').on('connection',function(socket){
socket.emit('send message','welcome to namespace:news');
socket.on('send message',function(data){
console.log('news namespace has received mes: '+data);
})
})

客户端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script src="/socket.io/socket.io.js"></script>
<script>
var chat = io.connect('http://localhost:3000/chat');
var news = io.connect('http://localhost:3000/news');

chat.on('connect',function(){
chat.send('您好!');
chat.on('message',function(data){
console.log('从chat命名空间收到消息:'+data);
});
});

news.on('connect',function(){
news.on('send message',function(data){
console.log('从news命名空间收到消息:'+data);
})
news.emit('send message','hello');
})
</script>

代码基本上没有什么改动,就是多了个使用命名空间的。主要要知道of的用法。


总结

主要是介绍了Socket.IO类库的基本介绍,包括语法和作用。如何安装,如何创建Socket.IO服务器。如何发送事件,如何广播,如何在Express框架中用到socket.io类库。那么,你最后肯定想知道,这个Socket.IO和WebSocket的区别和联系时什么?socket.io封装了websocket,同时还包含其他的功能,比如xhr轮询,jsonp轮询等等。因为不是所有的浏览器都支持websocket,通过socket.io的封装,我们不需要知道里面用到什么连接方式,任何浏览器都可以利用socket.io来建立异步的连接。要记住,如果浏览器中使用了socket.io的js,那么服务器端也要一样。学习内容部分来自《Node.js权威指南》,感谢。