Socket.io是一个事件驱动的即时双向通信支持库,使用它可以很方便地开发出可靠的、快速的即时应用
一、起步:编写一个聊天应用
1、基本结构
以下示例,采用Koa2作为WEB服务的框架,所以首先需要安装Koa并加入依赖:
npm install koa -S
然后,需要编写用来与用户交互的界面,如下:
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>基于Socket.io的聊天室DEMO</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font: 13px Helvetica, Arial; }
form { background: #000; padding: 3px; position: fixed; bottom: 0; width: 100%; }
form input { border: 0; padding: 10px; width: 90%; margin-right: .5%; }
form button { width: 9%; background: rgb(130, 224, 255); border: none; padding: 10px; }
#messages { list-style-type: none; margin: 0; padding: 0; }
#messages li { padding: 5px 10px; }
#messages li:nth-child(odd) { background: #eee; }
</style>
</head>
<body>
<ul id="messages"></ul>
<form action="">
<input id="m" autocomplete="off" />
<button id="b">Send</button>
</form>
</body>
</html>
接下来,编写server.js
,以响应给用户,如下:
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const app = new Koa()
const template = fs.readFileSync(path.resolve(__dirname, './index.html'))
app.use(async (ctx) => {
ctx.type = 'html'
ctx.body = template
})
app.listen(3000, () => {
console.log('Application is starting on port 3000')
})
然后,运行node server.js
,即可看到如下界面:
2、引入Socket.io
为了实现聊天功能,接下来就需要引入socket.io
来实现即时通信了,通过npm
安装引入:
npm install socket.io -S
这样子,就会自动安装好必须的包,包括socket.io
和socket.io-client
接下来,需要对server.js
进行一些修改,使得Http服务和Websocket服务共用一个端口,如下:
const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const app = new Koa()
const server = require('http').Server(app.callback())
const io = require('socket.io')(server)
const template = fs.readFileSync(path.resolve(__dirname, './index.html'))
app.use(async (ctx) => {
ctx.type = 'html'
ctx.body = template
})
io.on('connection', (socket) => {
console.log('An user connected')
})
server.listen(3000, () => {
console.log('Application is starting on port 3000')
})
每次有新用户连接,都会触发connection
事件,此外,当用户断开连接,则会触发disconnect
事件,那么,我们可以处理如下:
io.on('connection', (socket) => {
console.log('An user connected')
socket.on('disconnect', () => {
console.log('User disconnected')
})
})
3、客户端向服务端发送消息
在客户端,需要引入socket.io-client
模块,所以可以在</body>
之前,加入:
<script src="/js/socket.io.js"></script>
接下来,需要加入客户端连接服务端的逻辑,如下:
<script>
const socket = io()
</script>
这里,我们并不需要对io()
指定任何参数,它默认情况下会尝试连接托管页面的服务器,当完成以上那步工作后,刷新页面,我们就可以看到终端打印了:An user connected
接下来,我们要实现客户端向服务端发送消息的功能,由于socket.io
是基于事件的,所以实现也很简单,如下:
<script>
(function() {
const text = document.querySelector('#m')
const button = document.querySelector('#b')
const messages = document.querySelector('#messages')
const socket = io()
button.addEventListener('click', (ev) => {
const msg = text.value.trim()
if (msg === '') return
messages.innerHTML += `<li>${msg}</li>`
socket.emit('newMessage', msg) // 关键代码
text.value = ''
ev.preventDefault()
})
})()
</script>
4、广播
当用户发送一条消息时,其他用户应该都能够收到这条消息,那么socket.io
中实现这一功能的机制叫做广播
,广播的消息,将会发送给除了发送者之外的用户。所以,我们可以实现消息广播如下:
// server.js
io.on('connection', (socket) => {
console.log('An user connected')
socket.on('newMessage', (msg) => {
console.log('Message from client: ', msg)
socket.broadcase.emit('serverMsg', msg)
})
socket.on('disconnect', () => {
console.log('User disconnected')
})
})
<script>
(function() {
const text = document.querySelector('#m')
const button = document.querySelector('#b')
const messages = document.querySelector('#messages')
const socket = io()
socket.on('serverMsg', (msg) => {
messages.innerHTML += `<li>${msg}</li>`
})
button.addEventListener('click', (ev) => {
const msg = text.value.trim()
if (msg === '') return
messages.innerHTML += `<li>${msg}</li>`
socket.emit('newMessage', msg) // 关键代码
text.value = ''
ev.preventDefault()
})
})()
</script>
若我们想把消息发送给包括发送者在内的用户,那么就不需要广播,修改如下:
// server.js
socket.on('newMessage', (msg) => {
console.log('Message from client: ', msg)
io.emit('serverMsg', msg)
})
如此,便完成了整个DEMO的编写
二、总览
1、安装
可以通过npm
安装socket.io
,如下:
npm install socket.io
配合Node自带的http server
,如下:
// server.js
const app = require('http').createServer(handler)
const io = require('socket.io')(app)
const fs = require('fs')
function handler(req, res) {
fs.readFile(__dirname + '/index.html', (err, data) => {
if (err) {
res.writeHead(500)
return res.end('Error loading index.html')
}
res.writeHead(200)
res.end(data)
})
}
io.on('connection', (socket) => {
socket.emit('news', { hello: 'world' })
socket.on('my other event', (data) => {
console.log(data)
})
})
app.listen(80)
配合express3/4
使用,则如下:
// server.js
const app = require('express')()
const server = require('http').createServer(app)
const io = require('socket.io')(server)
app.get('/', (req, res) => {
res.sendFile(__dirname, '/index.html')
})
io.on('connection', (socket) => {
socket.emit('news', { hello: 'world' })
socket.on('my other event', (data) => {
console.log(data)
})
})
配合Koa使用则如同起步例子所示
2、收发事件
Socket.io允许我们发送和接收自定义事件(除了connect
、message
、disconnect
外的事件),并且只需要使用on()
和emit()
,如下:
const io = require('socket.io')(80)
io.on('connection', (socket) => {
io.emit('this', { will: 'be received by everyone' })
socket.on('private message', (from, msg) => {
console.log('I received a private message by ', from, ' saying', msg)
})
socket.on('disconnect', () => {
io.emit('user disconnected')
})
})
3、命名空间约束
通常情况下,当应用规模较小时,我们能够控制好消息的收发的情况下,就不必指定命名空间(默认情况下命名空间是/
),但如果想要利用第三方库或者共享代码,那么socket.io
提供了一种对socket
进行命名空间限定的方式。这种方式还有多路复用一个连接的好处,能够让socket.io
不需要使用两个websocket
连接,使用一个就够了,如下:
// server.js
const io = require('socket.io')(80)
const namespaceA = io
.of('/namespaceA')
.on('connection', (socket) => {
socket.emit('a message', 'hello')
})
const namespaceB = io
.of('/news')
.on('connection', (socket) => {
socket.emit('another message', 'world')
})
<!-- client.html -->
<script>
const namespaceA = io.connect('http://localhost/namespaceA')
const namespaceB = io.connect('http://localhost/namespaceB')
namespaceA.on('connect', () => {
namespaceA.emit('hi')
})
namespaceB.on('someMsg', () => {
namespaceB.emit('xxx')
})
</script>
4、发送不稳定(volatile)消息
在一些情况下,消息有可能会丢失。比如:我们开发一个抓取Bieber(贾斯汀比伯
)实时推特的应用,那么当一个客户端未准备好接收消息的时候(可能因为网络问题,或者因为这个客户端是通过长轮询进行连接的,但是正处于请求-响应
周期的中间间隔时间里),这种情况下就会导致和Bieber
有关的推特没有被全部抓取,那么应用就是不够健壮的。
这种情况下,可以将这些消息认定为不稳定消息,而socket.io
里提供了支持,如下:
const io = require('socket.io')(80)
io.on('connection', (socket) => {
const tweets = setInterval(() => {
getBieberTweet((tweet) => {
socket.volatile.emit('bieber tweet', tweet)
})
}, 100)
socket.on('disconnect', () => {
clearInterval(tweets)
})
})
5、发送与获取数据(应答)
有时候,我们需要告知客户端收到了消息,并执行回调,以给客户端一个回执。
这种情况下,只要简单地在send()
方法或者emit()
方法的最后一个参数里传递一个function
就可以了。而且,其实使用emit()
方法的情况下,应答是由我们自己完成的,所以就可以自行传递数据,如下:
// server.js
const io = require('socket.io')(80)
io.on('connection', (socket) => {
socket.on('ferret', (name, fn) => {
fn('woot')
})
})
<!-- client.html -->
<script>
const socket = io() // 当io()未指定参数时,是进行自动发现处理
// 其实以下也可以不需要监听`connect`,直接监听事件也是可以的
socket.on('connect', () => {
socket.emit('ferret', 'tobi', (data) => {
console.log(data) // 当服务端收到消息后,就客户端就会打印'woot'
})
})
</script>
6、广播消息
进行消息广播,只需要在emit()
或者send()
前加入broadcast
即可。广播意味着:将消息发送给除了发送者之外的所有人,如下:
// server.js
const io = require('socket.io')(80)
io.on('connection', (socket) => {
socket.broadcast.emit('user connected')
})
7、把socket.io当做跨浏览器的WebSocket来使用
socket.io
也支持WebSocket语义:只利用message
和send
,如下:
// server.js
const io = require('socket.io')(80)
io.on('connection', (socket) => {
socket.on('message', () => {
// ...
})
socket.on('disconnect', () => {
// ...
})
})
<!-- client.html -->
<script>
const socket = io('http://localhost')
socket.on('connect', () => {
socket.send('hi')
socket.on('message', (msg) => {
// ...
})
})
</script>
三、房间与命名空间
socket.io
允许我们对socket
限定命名空间,即分配不同的端点或者路径,这种特性能够帮助减少资源数目(即TCP连接数)
1、默认命名空间
默认的命名空间是/
,这也是socket.io
客户端默认连接、服务端默认监听的命名空间
默认的命名空间,是通过io.sockets
或者io
标识的,如:
io.sockets.emit('hi', 'everyone')
io.emit('hi', 'everyone') // 其实是上面那种方式的简写形式
每个命名空间都会发送一个connection
事件,然后接收Socket
实例作为参数,如:
io.on('connection', (socket) => {
socket.on('disconnect', () => {
// ...
})
})
2、自定义命名空间
可以通过服务端的of()
方法建立自定义的命名空间,如:
const nsp = io.of('/my-namespace')
nsp.on('connection', (socket) => {
console.log('someone connected')
})
nsp.emit('hi', 'everyone')
在客户端,则需要像以下这么做来关联命名空间:
const socket = io('/my-namespace')
注意:命名空间是Socket.IO
协议的一个实现细则,它和实际底层传输的URL无关(URL默认是/socket.io/
)
3、房间
在命名空间内部,还可以定义任意的socket
可加入(join()
)和离开(leave()
)的信道,如:
// 使用join()订阅socket,指定一个信道
io.on('connection', (socket) => {
socket.join('some room')
})
// 然后,在广播或者发送的时候,使用`to()`或者`in()`(两者是相同的)
io.to('some room').emit('some event')
// 离开一个信道,则可以这么做:
socket.leave('some room')
在Socket.IO内,每个Socket
都有一个随机的、不可猜测的、独一无二的标识符Socket#id
。方便起见,每个socket都会默认自动地加入由这个ID标识的房间。所以,有了这个机制,要广播消息给其他Socket也是比较容易的,如:
io.on('connection', (socket) => {
socket.on('say to someone', (id, msg) => {
socket.broadcast.to(id).emit('my message', msg)
})
})
在关闭连接的时候,socket都会自动地离开其归属的全部信道,且不需要我们自己做额外的拆卸工作
4、从外部发送信息
在一些情况下,我们需要在Socket.IO
进程外的上下文中发送事件给Socket.IO
中的命名空间、房间。
有多种方式可以处理这一问题,如可以实现自己的信道发送消息给进程,不过为了简化这一问题,官方提供了两个模块:
socket.io-redis
socket.io-emitter
然后可以实现Redis适配器:
const io = require('socket.io')(3000)
const redis = require('socket.io-redis')
io.adapter(redis({
host: 'localhost',
port: 6397
}))
然后,就可以在其他进程里发送消息给任何信道了:
const io = require('socket.io-emitter')({
host: '127.0.0.1',
port: 6397
})
setInterval(() => {
io.emit('time', new Date())
}, 5000)