在介绍 Socket.IO 之前, 我们先考虑一个问题, 如果这个时候有个需求, 类似实现人工客服
的功能该如何实现?
在线客服,需求理解起来很简单,就相当于一个 web 的聊天页面,也就是客户端能够 即时拉取到服务端的响应
当然, 作为接口工程师, 这并不是一个很难解决的问题, 我们可以提供一个获取聊天记录的接口, 通过该接口我们可以获取到对方已经发送到消息. 那么问题又来了, 如何保证能够 即时 的获取到聊天记录呢? 想必这也不是问题, 前端可以通过定时器的方式, 将间隔时间缩短到 100
毫秒, 这样子就已经实现了近实时的获取消息
setInterval(function () { // do something },100)
当我们写完以上代码上线后, 却通过监控可以发现, 上线后的服务器指标明显比之前有所提升
服务器是十分珍贵的资源, 那么为什么会发生这种情况呢? 回过头一想, 会发生这种情况也无可厚非, 每 100 毫秒就请求一 次后端, 如果有聊天记录产生, 那么这种请求就认为是有意义的, 但如果长时间未聊天, 每次请求返回都是空记录, 那么这种频繁请求就是无意义的. 频繁请求会使服务器压力增大, 并且浪费带宽流量.
那么有没有别的方式可以解决?
我们也许可以使用 SSE
方式, SSE
并不是一个什么比较新颖的概念, 它出现的时间也很早
SSE 全称 Server-Sent Events,指的是网页自动获取来自服务器的更新,也就是自动化获取服务端推送至网页的数据,这是一个 H5 的属性,除了 IE,其他标准浏览器基本都兼容
这种方式不需要客户端定时去获取,而是服务端向客户端声明要发送流信息,然后连续不断地发送过来
尽管这种方式不需要定时轮询, 但是它只能单工通信,建立连接后,只能由服务端发往客户端,且需要占用一个连接,如果需要客户端向服务端通信,那么需要额外再打开一个连接!
如果连接数过多会导致什么问题?
TCP 的连接数是有限的, SYN DDOS 洪水攻击, 就是利用 TCP 半连接的问题来攻击服务器
因此这也不是一种优雅的实现方式
其实到这里, 我们解决的思路已经很明确了, 就是在不浪费带宽的情况下如何让服务端将最新的消息以最快的速度发送给客户端. 但是明显 HTTP 协议不适用, 它是会在服务端收到请求后才会做出回应. 因此为了解决这个问题, 那么就需要就需要讲到一种通信协议, 那就是 WebSocket
WebSocket 是一种计算机通信协议,通过单个 TCP 连接提供全双工通信信道。
建立一个 WebSocket 连接,客户端会发送一个 WebSocket 握手请求,服务器为此返回一个 WebSocket 握手响应,如下图所示。
相比于传统 HTTP 的每次 请求-应答
都要客户端与服务端建立连接的模式, websocket 是一种 长连接 的模式, 一旦建立起 websocekt 连接, 除非 client 或者 server 中有一端主动断开连接, 否则每次数据传输之前都不需要 HTTP 那样请求数据
客户端请求
Upgrade: websocket Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
- Upgrade 是为了表明这是一个 websocekt 类型请求, 意在告诉 server 需要将通信协议切换到 websocekt
- Sec-WebSocket-Key是 client 发送的一个 base64 编码的密文 ,要求服务器用 Sec-WebSocket-Accept 头部中的密钥散列作为响应。这是为了防止缓存代理重新发送以前的 WebSocket 对话,并且不提供任何身份验证、隐私或完整性。
服务端响应
Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk= Sec-WebSocket-Protocol: chat
握手从 HTTP 请求/响应开始,允许服务器在同一端口处理 HTTP 连接和 WebSocket 连接。一旦连接建立起来,通信就切换到不符合 HTTP 协议的双向二进制协议。
到这里其实方案已经出来了, 但是我们这篇文章的标题确实 Socket.IO, 既然都有了 Websocket, 为什么我们讲的是 Socket.IO ?
Socket.IO
在大家往下看之前先清楚这么一个观点:
Socket.IO 不是替代, 而是升级
Socket.IO 是一个库, 说到库其实我们都不陌生, 库是对已有的功能进行封装, 没错, 它是构建在 WebSocket 协议之上, 并提供额外的保证, 既然它是构建在 websocekt 之上, 说明它同样具有客户机与服务器之间延迟通信的功能.
Socket.IO可用于实现以下几种通信方式:
- HTML 5中的WebSocket通信
- 可在Flash中使用的WebSocket通信
- XHR轮询
- JSONP轮询
- Forever Iframe
Socket.IO确保在实现这些通信方式时,客户端与服务器端可以使用相同的API。并具备以下特性:
- HTTP 长轮询回退
如果不能建立 WebSocket 连接,连接将退回到 HTTP 长轮询。
- 自动重新连接
在某些特定条件下,服务器和客户端之间的 WebSocket 连接可能会被中断,双方都不知道链接的断开状态。而 Socket.IO 包含一个 heartbeat 机制的原因,该机制定期检查连接的状态.当客户端最终断开连接时,它会自动重新连接,并且会出现指数级的回退延迟,以免压垮服务器
- 数据包缓冲
当客户端断开连接时,数据包将自动缓冲,并在重新连接时发送
既然 Socket.IO 如此的美妙, 那么它该如何使用呢? 那么接下来就让我们创建一个自己的聊天室吧 !
本案例采用 NodeJS 环境搭建, 极其简单, 有条件的可以上手一试
聊天室
准备前提:
- 确保安装了 Node.js 环境
- 准备一个空文件夹
准备步骤很简单, 接下来我们就开始创建我们自己的聊天室
1. 创建 package.json 文件
我们在空目录下创建 package.json 文件, 内容如下:
{ "name": "c-chat", "version": "0.0.1", "description": "my first chat app", "dependencies": {} }
在当前目录执行命令 npm install express
安装web应用开发框架
2. 创建 index.js & index.html
在空目录下创建 index.js 文件, 内容如下:
const app = require('express')(); const http = require('http').Server(app); app.get('/', (req, res) => { res.sendFile(__dirname + '/index.html'); }); http.listen(port, () => { console.log(`${port} 端口监听成功`); });
接着创建 index.html 文件, 内容如下
<!DOCTYPE html> <html> <head> <title>my chat</title> <style> body { margin: 0; padding-bottom: 3rem; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; } #form { background: rgba(0, 0, 0, 0.15); padding: 0.25rem; position: fixed; bottom: 0; left: 0; right: 0; display: flex; height: 3rem; box-sizing: border-box; backdrop-filter: blur(10px); } #input { border: none; padding: 0 1rem; flex-grow: 1; border-radius: 2rem; margin: 0.25rem; } #input:focus { outline: none; } #form > button { background: #333; border: none; padding: 0 1rem; margin: 0.25rem; border-radius: 3px; outline: none; color: #fff; } #messages { list-style-type: none; margin: 0; padding: 0; } #messages > li { padding: 0.5rem 1rem; } #messages > li:nth-child(odd) { background: #efefef; } </style> </head> <body> <ul id="messages"></ul> <form id="form" action=""> <input id="input" /><button>Send</button> </form> </body> </html>
为了测试 http 服务与我们的页面是否有效, 我们可以利用 node index.js
启动项目来验证
到这里, 我们就已经能够成功访问到我们的页面, 接下来就开始通过 socket.io 来实现我们的聊天功能
3. 安装 socket.io 库
npm install socket.io
首先就需要执行以上命令来安装 socket.io 库
现在离目标已经实现一大半了
我们只需要修改部分内容便可以看到我们想要的效果
服务端
const { Server } = require("socket.io"); const io = new Server(server);
以上代码是为了引入 socket.io库, 并创建 websocket 服务, 然后便可以建立 socket 监听
io.on('connection', (socket) => {console.log('连接建立成功');});
在一个Socket.IO服务器创建之后,当客户端与服务器端建立连接时,触发Socket.IO服务器的connection事件,可以通过监听该事件并指定事件回调函数的方法指定当客户端与服务器端建立连接时所需执行的处理
客户端
在 index.html 页面, 我们添加以下代码来引入 socket.io.js, 并创建 socket 对象
<script src="/socket.io/socket.io.js"></script> <script> var socket = io(); </script>
到这里为止就是加载 socket.io-client 所需的全部操作,该客户端公开了一个 io 全局(以及端点 GET/ socket.io/socket.io.js ) ,然后进行连接。我们可以重启下服务, 打开浏览器, 然后查看控制台结果
可以看到连接已经成功建立了. 接下来就是最重要的环节了, 双方需要进行消息发送了, 在 IO 中任何可以被编码为 JSON 的对象都可以发送,并且还支持二进制数据
客户端
index.html 中需要修改的代码如下:
<script> var socket = io(); var messages = document.getElementById('messages'); var form = document.getElementById('form'); var input = document.getElementById('input'); form.addEventListener('submit', function(e) { e.preventDefault(); if (input.value) { socket.emit('chat message', input.value); input.value = ''; } }); socket.on('chat message', function(msg) { var item = document.createElement('li'); item.textContent = msg; messages.appendChild(item); window.scrollTo(0, document.body.scrollHeight); }); </script>
可以通过 emit 方法往服务端发送消息, 其中 chat message
为发送的目标地址
在emit方法中,使用三个参数
socket.emit(event, data, callback)
- event参数值为一个用于指定事件名的字符串, 也就是目标主题
- data参数值代表该事件中携带的数据,该数据将被对方接收,数据可以为一个字符串,也可以为一个对象
- callback参数值为一个参数,用于指定一个当对方确认接收到数据时调用的回调函数
服务端
index.js 文件中需要修改的代码如下:
io.on('connection', (socket) => { socket.on('chat message', (msg) => { console.log('message: ' + msg); }); });
通过 socket.on()
的方式监听目标地址, 这有些类似于发布/订阅模式, 双方订阅同一个地址, 然后往这个通道中传递消息 在服务端我们同样可以使用 emit 方法往客户端发送消息, 我们可以利用 socket.emit()
进行发送
附: 完整代码
index.html
index.js
到这里就彻底结束了, 来吧, 伙计们, 现在重新启动项目, 然后打开两个浏览器访问 localhost:3000
地址, 来尝试和自己对话吧 !
命名空间
上面我们已经简单的实现了一个聊天室的功能, 主要利用到以下 api
socket.on()
监听事件
socket.emit()
消息发送
这两个是最基础的用法, 下面我们说一个扩展使用, 那就是命名空间
如果开发者想在一个特定的应用程序中完全控制消息与事件的发送,只需要使用一个默认的"/"
命名空间就足够了。但是如果开发者需要将应用程序作为第三方服务提供给其他应用程序,则需要为一个用于与客户端连接的socket端口定义一个独立的命名空间。
在Socket.IO中,使用Socket.IO服务器对象的of方法定义命名空间,代码如下所示(代码中的io代表一个Socket.IO服务器对象)。
io.of(namespace)
下面我们看下如何使用:
- 服务端
io.of("/chat").on("connection", (socket) => { // 订阅对应的主题 socket.on("chat message", (msg) => { console.log("message: " + msg); socket.emit("chat message", msg); }); });
- 客户端
var socket = io('http://localhost:3000/chats');
我们以上例子中定义了 chat
这个命名空间用于区分不同 socket 连接, 小伙伴们可以发挥想象这个可以应用到什么场景中 !
总结
SOCKET 是用来让不同电脑之间,不同进程之间互相通信的一套接口。Socket, 直译过来可以是“插座”,而在中文中往往会叫“套接字”。双方要建立连接, 首先就会申请一个 套接字 来传输消息