MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript中的WebSocket实时通信实践

2022-11-277.7k 阅读

JavaScript 中的 WebSocket 实时通信实践

WebSocket 基础概念

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。与传统的 HTTP 通信不同,HTTP 是一种请求 - 响应模式,每次请求都需要客户端发起,服务器响应后连接即关闭。而 WebSocket 一旦建立连接,客户端和服务器之间就可以进行双向数据传输,这使得实时通信变得更加高效和便捷。

WebSocket 协议通过一个握手过程从 HTTP 协议升级而来。在客户端发起 WebSocket 连接请求时,会在 HTTP 请求头中包含特殊的字段,告知服务器这是一个 WebSocket 连接请求。服务器如果支持 WebSocket 协议,会返回相应的响应,完成握手过程,之后双方就可以基于 WebSocket 协议进行通信了。

WebSocket 的 URL 格式与 HTTP 类似,不过协议前缀为 ws 或者 wss(用于加密连接,类似于 HTTPS)。例如:ws://example.com/socket 或者 wss://secure.example.com/socket

在 JavaScript 中使用 WebSocket

在 JavaScript 中,操作 WebSocket 非常方便,因为浏览器原生提供了 WebSocket 对象。

创建 WebSocket 连接

要创建一个 WebSocket 连接,只需要实例化 WebSocket 对象,并传入目标 URL:

const socket = new WebSocket('ws://localhost:8080');

这里创建了一个到 ws://localhost:8080 的 WebSocket 连接。如果连接成功,WebSocket 对象的 readyState 属性会变为 1(表示 OPEN 状态)。

监听连接状态变化

可以通过监听 opencloseerror 事件来处理连接状态的变化。

  • open 事件:当 WebSocket 连接成功建立时触发。
socket.addEventListener('open', function (event) {
    console.log('WebSocket 连接已建立');
    // 可以在这里发送初始数据
    socket.send('Hello, Server!');
});
  • close 事件:当 WebSocket 连接关闭时触发。
socket.addEventListener('close', function (event) {
    if (event.wasClean) {
        console.log('WebSocket 连接已正常关闭');
    } else {
        console.log('WebSocket 连接异常关闭');
    }
    console.log(`关闭代码: ${event.code}`);
    console.log(`关闭原因: ${event.reason}`);
});
  • error 事件:当 WebSocket 连接发生错误时触发。
socket.addEventListener('error', function (event) {
    console.log('WebSocket 连接发生错误:', event);
});

发送和接收数据

  • 发送数据:使用 send() 方法向服务器发送数据。send() 方法接受一个字符串、Blob 对象或 ArrayBuffer 对象作为参数。
// 发送字符串数据
socket.send('这是一条消息');

// 发送 JSON 数据
const data = { message: '这是一个 JSON 消息', key: 'value' };
socket.send(JSON.stringify(data));
  • 接收数据:通过监听 message 事件来接收服务器发送的数据。
socket.addEventListener('message', function (event) {
    console.log('收到服务器消息:', event.data);
    // 如果发送的是 JSON 数据,需要解析
    try {
        const jsonData = JSON.parse(event.data);
        console.log('解析后的 JSON 数据:', jsonData);
    } catch (error) {
        console.log('非 JSON 数据:', event.data);
    }
});

WebSocket 实时通信示例:简单聊天应用

下面我们通过一个简单的聊天应用示例,来深入理解 WebSocket 在实时通信中的应用。

前端代码

首先,创建一个 HTML 文件 index.html

<!DOCTYPE html>
<html lang="zh - CN">

<head>
    <meta charset="UTF - 8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket 聊天应用</title>
    <style>
        #chat - messages {
            height: 300px;
            border: 1px solid #ccc;
            overflow - y: scroll;
            padding: 10px;
        }

        #message - input {
            width: 80%;
            padding: 10px;
            margin - right: 10px;
        }

        #send - button {
            padding: 10px 20px;
        }
    </style>
</head>

<body>
    <div id="chat - messages"></div>
    <input type="text" id="message - input" placeholder="输入消息">
    <button id="send - button">发送</button>
    <script>
        const socket = new WebSocket('ws://localhost:8080');

        socket.addEventListener('open', function () {
            console.log('WebSocket 连接已建立');
        });

        socket.addEventListener('close', function (event) {
            if (event.wasClean) {
                console.log('WebSocket 连接已正常关闭');
            } else {
                console.log('WebSocket 连接异常关闭');
            }
            console.log(`关闭代码: ${event.code}`);
            console.log(`关闭原因: ${event.reason}`);
        });

        socket.addEventListener('error', function (event) {
            console.log('WebSocket 连接发生错误:', event);
        });

        socket.addEventListener('message', function (event) {
            const messageDiv = document.createElement('div');
            messageDiv.textContent = event.data;
            document.getElementById('chat - messages').appendChild(messageDiv);
        });

        const messageInput = document.getElementById('message - input');
        const sendButton = document.getElementById('send - button');

        sendButton.addEventListener('click', function () {
            const message = messageInput.value;
            if (message) {
                socket.send(message);
                messageInput.value = '';
            }
        });

        messageInput.addEventListener('keydown', function (event) {
            if (event.key === 'Enter') {
                const message = messageInput.value;
                if (message) {
                    socket.send(message);
                    messageInput.value = '';
                }
            }
        });
    </script>
</body>

</html>

在这段代码中,我们创建了一个简单的聊天界面,包含一个消息显示区域、一个输入框和一个发送按钮。当用户输入消息并点击发送按钮或按下回车键时,消息会通过 WebSocket 发送到服务器。同时,监听 message 事件,将服务器返回的消息显示在聊天区域。

后端代码(使用 Node.js 和 ws 库)

为了实现完整的聊天应用,我们还需要一个服务器端来接收和转发消息。这里我们使用 Node.js 和 ws 库来搭建服务器。

首先,确保你已经安装了 ws 库:

npm install ws

然后,创建一个 server.js 文件:

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

const clients = new Set();

wss.on('connection', function connection(ws) {
    clients.add(ws);

    ws.on('message', function incoming(message) {
        console.log('收到客户端消息:', message);
        clients.forEach(function each(client) {
            if (client!== ws) {
                client.send(message);
            }
        });
    });

    ws.on('close', function () {
        clients.delete(ws);
        console.log('客户端连接已关闭');
    });

    ws.on('error', function (error) {
        console.log('客户端连接发生错误:', error);
    });
});

console.log('WebSocket 服务器已启动,监听端口 8080');

在这段代码中,我们创建了一个 WebSocket 服务器,监听在 8080 端口。当有客户端连接时,将客户端加入到 clients 集合中。当收到客户端发送的消息时,将消息转发给除发送者之外的其他所有客户端。当客户端关闭连接或发生错误时,相应地处理相关事件。

通过这个简单的聊天应用示例,我们可以看到 WebSocket 如何实现实时双向通信,使得前端和后端能够及时地交换数据。

WebSocket 的高级应用

心跳检测

在实际应用中,由于网络不稳定等原因,WebSocket 连接可能会在不知不觉中断开。为了保持连接的有效性,我们可以使用心跳检测机制。

心跳检测的原理是客户端和服务器定期互相发送一个简单的消息(称为心跳消息),如果一方在一定时间内没有收到对方的心跳消息,就认为连接已经断开,从而进行相应的处理,比如尝试重新连接。

以下是在客户端实现心跳检测的示例代码:

const socket = new WebSocket('ws://localhost:8080');

let heartbeatInterval;

socket.addEventListener('open', function () {
    console.log('WebSocket 连接已建立');
    // 启动心跳检测
    heartbeatInterval = setInterval(() => {
        socket.send('ping');
    }, 10000); // 每 10 秒发送一次心跳消息
});

socket.addEventListener('message', function (event) {
    if (event.data === 'pong') {
        // 收到服务器的心跳响应,重置心跳检测
        clearInterval(heartbeatInterval);
        heartbeatInterval = setInterval(() => {
            socket.send('ping');
        }, 10000);
    } else {
        console.log('收到服务器消息:', event.data);
    }
});

socket.addEventListener('close', function () {
    clearInterval(heartbeatInterval);
    console.log('WebSocket 连接已关闭');
});

在服务器端,也需要相应地处理心跳消息:

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
    ws.on('message', function incoming(message) {
        if (message === 'ping') {
            ws.send('pong');
        } else {
            console.log('收到客户端消息:', message);
        }
    });

    ws.on('close', function () {
        console.log('客户端连接已关闭');
    });
});

console.log('WebSocket 服务器已启动,监听端口 8080');

通过这种方式,我们可以有效地检测和维护 WebSocket 连接的状态。

多路复用

在一些场景下,我们可能希望在一个 WebSocket 连接上进行多种不同类型的数据传输,这就可以使用多路复用技术。

一种常见的实现方式是在消息中添加一个类型字段,用来标识消息的类型。例如:

// 客户端发送不同类型消息
const socket = new WebSocket('ws://localhost:8080');

// 发送用户登录消息
const loginMessage = { type: 'login', username: 'user1', password: 'pass1' };
socket.send(JSON.stringify(loginMessage));

// 发送聊天消息
const chatMessage = { type: 'chat', content: 'Hello, everyone!' };
socket.send(JSON.stringify(chatMessage));

在服务器端,根据消息类型进行不同的处理:

const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', function connection(ws) {
    ws.on('message', function incoming(message) {
        const data = JSON.parse(message);
        switch (data.type) {
            case 'login':
                console.log('处理登录请求:', data.username, data.password);
                // 处理登录逻辑
                break;
            case 'chat':
                console.log('处理聊天消息:', data.content);
                // 处理聊天消息逻辑,转发给其他客户端等
                break;
            default:
                console.log('未知消息类型');
        }
    });

    ws.on('close', function () {
        console.log('客户端连接已关闭');
    });
});

console.log('WebSocket 服务器已启动,监听端口 8080');

通过这种简单的多路复用方式,我们可以在一个 WebSocket 连接上处理多种不同类型的实时通信需求。

WebSocket 与 HTTP 的关系及优势

WebSocket 与 HTTP 的关系

如前文所述,WebSocket 协议是基于 HTTP 协议进行握手的。在客户端发起 WebSocket 连接请求时,使用的是 HTTP 请求,请求头中包含特殊字段 Upgrade: websocketConnection: Upgrade,告知服务器这是一个 WebSocket 连接请求。服务器如果支持 WebSocket 协议,会返回状态码 101 Switching Protocols,并在响应头中确认协议升级,完成握手过程,之后双方就基于 WebSocket 协议进行通信了。

虽然 WebSocket 基于 HTTP 握手,但它与 HTTP 有本质的区别。HTTP 是一种无状态的、请求 - 响应模式的协议,每次请求都需要客户端发起,服务器响应后连接即关闭。而 WebSocket 一旦建立连接,客户端和服务器之间就可以进行双向、持续的数据传输。

WebSocket 的优势

  1. 实时性:WebSocket 允许服务器主动向客户端推送数据,无需客户端频繁发起请求,这使得实时通信(如实时聊天、实时数据更新等)变得非常高效。相比之下,传统的 HTTP 轮询或长轮询方式需要客户端不断地发起请求来获取最新数据,会造成不必要的网络开销。
  2. 双向通信:客户端和服务器可以在同一连接上随时双向发送数据,这种全双工的通信模式为实时应用提供了极大的便利。例如,在在线游戏中,服务器可以实时推送游戏状态变化给客户端,客户端也可以及时反馈玩家操作给服务器。
  3. 减少开销:WebSocket 在建立连接后,通信数据的头部开销较小。与 HTTP 请求相比,HTTP 每次请求都需要携带大量的头部信息,而 WebSocket 数据帧头部相对简单,这在数据传输量较大时可以显著减少网络带宽的消耗。
  4. 更好的性能:由于 WebSocket 连接是持久的,避免了每次请求都建立和关闭连接的开销,这使得在高并发场景下,WebSocket 能够提供更好的性能和响应速度。

WebSocket 的局限性与应对策略

WebSocket 的局限性

  1. 浏览器兼容性:虽然现代浏览器大多都支持 WebSocket,但在一些老旧浏览器中可能不支持。这就需要在开发时考虑兼容性问题,可能需要使用一些 polyfill 库来提供支持,或者采用降级策略,如回退到 HTTP 轮询方式。
  2. 防火墙和代理服务器:有些防火墙和代理服务器可能会阻止 WebSocket 连接,因为它们可能不识别或不支持 WebSocket 协议。这可能导致连接失败,需要与网络管理员协调,配置防火墙和代理服务器以允许 WebSocket 通信。
  3. 服务器资源消耗:由于 WebSocket 连接是持久的,服务器需要为每个连接维护一定的资源(如内存、文件描述符等)。在高并发场景下,如果服务器处理不当,可能会导致资源耗尽,影响服务器性能。

应对策略

  1. 浏览器兼容性处理:可以使用 Modernizr 等工具来检测浏览器是否支持 WebSocket。如果不支持,可以使用 SockJS 等库,它会自动根据浏览器的支持情况选择合适的传输方式,如 WebSocket、XHR 长轮询等。
if (typeof WebSocket === 'undefined') {
    // 使用 SockJS 替代 WebSocket
    const sockjs = new SockJS('ws://localhost:8080');
    sockjs.onopen = function () {
        console.log('SockJS 连接已建立');
    };
    sockjs.onmessage = function (event) {
        console.log('收到 SockJS 消息:', event.data);
    };
    sockjs.onclose = function () {
        console.log('SockJS 连接已关闭');
    };
} else {
    const socket = new WebSocket('ws://localhost:8080');
    // 正常的 WebSocket 操作
}
  1. 处理防火墙和代理服务器问题:与网络管理员沟通,确保防火墙规则允许 WebSocket 通信的端口(通常是 80 或 443 端口,对于 wss 协议)通过。如果使用代理服务器,配置代理服务器以支持 WebSocket 协议的转发。例如,对于 Nginx 代理服务器,可以通过以下配置支持 WebSocket:
location /ws {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}
  1. 优化服务器资源管理:在服务器端,可以采用一些优化策略来减少资源消耗。例如,使用连接池技术来复用连接,减少新建连接的开销;对长时间不活动的连接进行自动关闭,释放资源;采用分布式架构,将连接负载分散到多个服务器节点上,提高整体的处理能力。

总结 WebSocket 在现代应用中的重要性

WebSocket 在现代 Web 应用和实时应用中扮演着至关重要的角色。随着互联网应用的不断发展,对实时性和交互性的要求越来越高,WebSocket 的实时双向通信能力正好满足了这些需求。

在实时监控系统中,WebSocket 可以实时推送设备状态、系统性能等数据,让管理员能够及时了解系统运行情况;在多人在线游戏中,WebSocket 实现了玩家之间的实时交互,如实时对战、聊天等;在金融领域,股票行情的实时更新、交易信息的即时推送等也离不开 WebSocket 的支持。

尽管 WebSocket 存在一些局限性,但通过合理的应对策略,可以有效地解决这些问题,充分发挥其优势。随着技术的不断进步,WebSocket 协议也在不断完善,未来有望在更多领域得到更广泛的应用,为用户带来更加流畅、实时的体验。

总之,掌握 WebSocket 在 JavaScript 中的应用,对于开发高性能、实时性强的 Web 应用至关重要,无论是前端开发者还是后端开发者,都应该深入理解和熟练运用这一强大的实时通信技术。