Socket.IO 简介

就像 HTTP 一样,WebSocket 是一种通信协议,但它在客户端和服务器之间提供全双工(双向)通信。与 HTTP 不同,WebSocket 连接始终保持打开状态以进行实时数据传输。因此,在 WebSocket 应用程序中,服务器可以在没有客户端发起请求的情况下向客户端发送数据。此外,与以 HTTP 或 HTTPS(超文本传输安全协议)开头的 HTTP 模式不同,WebSocket 协议模式以 ws 或 wss(安全 WebSocket)开头;例如:

ws://example.com:4000

Socket.IO 是一个 JavaScript 库,它使用 WebSocket 协议和轮询作为后备选项来创建实时 Web 应用程序。它支持任何平台、浏览器或设备,并处理服务器和客户端的所有降级,以实现实时的全双工通信。如今,大多数浏览器都支持 WebSocket 协议,包括 Google Chrome、Microsoft Edge、Firefox、Safari 和 Opera。但是,在使用 Socket.IO 时,我们必须同时使用其客户端和服务器端库。客户端库在浏览器内部运行,而服务器端库在您的服务器端 Node.js 应用程序上运行。因此,让我们让这两个库在我们的应用程序中工作起来。

如果您想了解更多关于 Socket.IO 的信息,请访问 https://socket.io/。

添加和使用 Socket.IO 服务器和客户端

我们将把 Socket.IO 服务器添加到我们在前几节中构建的 API 中,然后最终将 Socket.IO 客户端添加到 Nuxt 应用程序中。但在将其添加到 Nuxt 应用程序之前,我们会将其添加到一个简单的 HTML 页面,以便我们对 Socket.IO 服务器和 Socket.IO 客户端如何协同工作有一个大致的了解。让我们学习如何做到这一点:

  1. 通过 npm 安装 Socket.IO 服务器:

    $ npm i socket.io
  2. 如果尚未创建,请在 /configs/ 目录中创建一个 index.js 文件来存储服务器设置:

    // configs/index.js
    export default {
        server: {
            port: 4000
        },
    }

    通过这个简单的设置,我们的 API 将在 4000 端口上提供服务。

  3. 导入 socket.io 并使用 Koa 的新实例将其绑定到 Node.js HTTP 对象,以创建 Socket.IO 的新实例,如下所示:

    // backend/koa/public/index.js
    import Koa from 'koa'
    import socket from 'socket.io'
    import http from 'http'
    import config from 'Configs'
    import middlewares from '../middlewares'
    
    const app = new Koa()
    const host = process.env.HOST || '127.0.0.1'
    const port = process.env.PORT || config.server.port
    
    middlewares(app)
    
    const server = http.createServer(app.callback())
    const io = socket(server)
    
    io.sockets.on('connection', socket => {
        console.log('a user connected: ' + socket.id)
        socket.on('disconnect', () => {
            console.log('user disconnected: ' + socket.id)
        })
    })
    
    server.listen(port, host)

    创建 Socket.IO 的新实例后,我们可以开始监听来自 socket 回调的传入 socket 的 Socket.IO 连接事件。我们将传入的 socket 及其 ID 记录到控制台。当传入的 socket 断开连接时,我们还会记录其断开连接事件。最后,请注意我们使用原生的 Node.js HTTP 在 localhost:4000 上启动并提供应用程序服务,而不是像以前那样使用 Koa 内部的 HTTP:

    // 以前的做法
    // app.listen(4000)
  4. 创建一个 socket-client.html 页面,并通过 CDN 导入 Socket.IO 客户端。通过传递 localhost:4000 作为特定 URL 来创建它的新实例,如下所示:

    // frontend/html/socket-client.html
    <script src="https://cdn.jsdelivr.net/npm/socket.io-client@2/dist/socket.io.js"></script>
    <script>
        var socket = io('http://localhost:4000/')
    </script>

    现在,如果您在浏览器上浏览此 HTML 页面,或者刷新该页面时,您应该会在控制台中看到打印的带有 socket ID 的日志,如下所示:

    a user connected: abeGnarBnELo33vQAAAB

    当您关闭 HTML 页面时,您还应该在控制台中看到打印的带有 socket ID 的日志,如下所示:

    user disconnected: abeGnarBnELo33vQAAAB

    这就是连接 Socket.IO 的服务器端和客户端所需的全部操作。这非常简单容易,不是吗?但是我们在这里所做的只是连接和断开服务器和客户端。我们需要更多——我们希望同时传输数据。为此,我们只需要相互发送和接收事件,我们将在接下来的步骤中进行操作。

    如果您想使用 Socket.IO 客户端的本地版本,可以将脚本标签的 URL 源指向 /node_modules/socket.io-client/dist/socket.io.js。

  5. 通过使用 Socket.IO 服务器的 emit 方法从服务器创建一个发送事件,如下所示:

    // backend/koa/public/index.js
    io.sockets.on('connection', socket => {
        io.emit('emit.onserver', 'Hi client, what you up to?')
        console.log('Message to client: ' + socket.id)
    })

    在这里,您可以看到我们通过名为 emit.onserver 的自定义事件发送了一个简单的消息,并将活动记录到控制台。请注意,我们只能在连接建立后发送事件。然后,我们可以在客户端监听这个自定义事件并记录来自服务器的消息,如下所示:

    // frontend/html/socket-client.html
    <script>
        socket.on('emit.onserver', function (message) {
            console.log('Message from server: ' + message)
        })
    </script>
  6. 所以,现在,如果您再次刷新浏览器上的页面,您应该会在控制台中看到打印的日志,如下所示:

    Message to client: abeGnarBnELo33vQAAAB // 服务器端
    Message from server: Hi client, what you up to? // 客户端
  7. 通过使用 Socket.IO 客户端的 emit 方法从客户端创建一个发送事件,如下所示:

    // frontend/html/socket-client.html
    <script
        src="https://code.jquery.com/jquery-3.4.1.slim.min.js"
        integrity="sha256-pasqAKBDmFT4eHoN2ndd6lN370kFiGUFyTiUHWhU7k8="
        crossorigin="anonymous"></script>
    <button class="button-sent">Send</button>
    <script>
        $('.button-sent').click(function(e){
            e.preventDefault()
            var message = 'Hi server, how are you holding up?'
            socket.emit('emit.onclient', message)
            console.log('Message sent to server.')
            return false
        })
    </script>

    在这里,您可以看到,首先,我们通过 CDN 安装了 jQuery 并创建了一个带有 jQuery 点击事件的 <button>。其次,当按钮被点击时,我们使用名为 emit.onclient 的 Socket.IO 自定义事件发送了一个简单的消息。最后,我们将活动记录到控制台。

  8. 之后,我们可以在服务器端监听 Socket.IO 自定义事件并记录来自客户端的消息,如下所示:

    // backend/koa/public/index.js
    socket.on('emit.onclient', (message) => {
        console.log('Message from client, '+ socket.id + ' :' + message);
    })
  9. 如果您再次刷新浏览器上的页面,您应该会在控制台中看到打印的日志以及 socket ID,如下所示:

    Message sent to server. // 客户端
    Message from client, abeGnarBnELo33vQAAAB: Hi server,
    how are you holding up? // 服务器端

    您现在知道如何使用 Socket.IO 实时地来回传输数据——只需发送自定义事件并监听它们即可。接下来您应该了解的是如何将 Socket.IO 与 RethinkDB 中的变更提要集成,以便将来自数据库的实时数据传输到客户端。所以,请继续阅读!

集成 Socket.IO 服务器和 RethinkDB changefeeds

您可能还记得,您之前使用位于 localhost:8080/#dataexplorer 的管理 UI 中的数据浏览器再次摆弄过 RethinkDB 的变更提要。要订阅变更提要,您只需要将 ReQL 的 changes 命令链接到查询,如下所示:

r.db('nuxtdb').table('user').changes()

RethinkDB 的变更提要包含从 RethinkDB 数据库实时发送到我们 API 的数据,这意味着我们需要在服务器端使用 Socket.IO 服务器捕获这些提要,并将它们发送到客户端。因此,让我们学习如何通过重构我们在本章中一直在开发的 API 来捕获它们:

  1. 通过 npm 将 Socket.IO 服务器安装到您的 API 中:

    $ npm i socket.io
  2. 在 /core/ 目录中的 changefeeds.js 文件中创建一个具有以下代码的异步匿名箭头函数:

    // core/database/rethinkdb/changefeeds.js
    import rdb from 'rethinkdb'
    import rdbConnection from './connection'
    
    export default async (io, tableName, eventName) => {
        try {
            const connection = await rdbConnection()
            var cursor = await rdb.table(tableName)
                .changes()
                .run(connection)
            cursor.each(function (err, row) {
                if (err) {
                    throw err
                }
                io.emit(eventName, row)
            })
        } catch( err ) {
            console.error(err);
        }
    }

    在此函数中,我们将 rethinkdb 导入为 rdb,并将我们的 RethinkDB 数据库连接导入为 rdbConnection,然后使用以下项作为此函数的参数:

    • Socket.IO 服务器的实例

    • 您想要使用的 Socket.IO 发送自定义事件名称

    • 您想要订阅其变更提要的 RethinkDB 表名

    变更提要将以游标对象的形式返回文档作为回调,因此我们遍历游标对象并使用自定义事件名称发送文档的每一行。

  3. 在 /public/ 目录中的应用程序根目录中导入 changefeeds 函数作为 rdbChangeFeeds,并将其与 index.js 文件中的其余现有代码集成,如下所示:

    // public/index.js
    import Koa from 'koa'
    import socket from 'socket.io'
    import http from 'http'
    import config from 'Configs'
    import middlewares from '../middlewares'
    import rdbChangeFeeds from 'Core/database/rethinkdb/changefeeds'
    
    const app = new Koa()
    const host = process.env.HOST || '127.0.0.1'
    const port = process.env.PORT || config.server.port
    
    middlewares(app)
    
    const server = http.createServer(app.callback())
    const io = socket(server)
    
    io.sockets.on('connection', socket => {
        //...
    })
    
    rdbChangeFeeds(io, 'user', 'user.changefeeds')
    
    server.listen(port, host)

在上面的代码中,我们要订阅的表名是 user,我们要调用的发送事件名称是 user.changefeeds。因此,我们将它们与 socket.io 实例一起传递给 rdbChangeFeeds 函数。这就是您只需要做的一次性全局集成 Socket.IO 和 RethinkDB 的全部操作。

做得好!您已成功地在服务器端集成了 Koa、RethinkDB 和 Socket.IO,并创建了一个实时 API。但是客户端呢,我们如何监听从 API 发送的事件?我们将在下一节中找到答案。