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

Node.js 构建基于 TCP 的实时聊天系统

2023-07-142.3k 阅读

一、Node.js 与 TCP 简介

1.1 Node.js 概述

Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行时环境,它让 JavaScript 能够在服务器端运行。Node.js 采用事件驱动、非阻塞 I/O 模型,这使得它非常适合构建高性能、可扩展的网络应用程序。它拥有丰富的生态系统,通过 npm(Node Package Manager)可以轻松获取和管理各种第三方模块,极大地提高了开发效率。

1.2 TCP 协议基础

TCP(Transmission Control Protocol)即传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议。在 TCP 通信中,客户端和服务器端需要先建立连接,然后才能进行数据传输。它通过序列号、确认应答、重传机制等保证数据的可靠传输,并且能够根据网络状况进行流量控制和拥塞控制。例如,在浏览器与服务器进行网页数据传输时,大部分情况下就是基于 TCP 协议。

二、构建实时聊天系统的准备工作

2.1 安装 Node.js

首先,需要在本地开发环境安装 Node.js。可以从 Node.js 官方网站(https://nodejs.org/)下载适合操作系统的安装包进行安装。安装完成后,在命令行中输入 node -v 来验证是否安装成功,如果输出版本号,则说明安装无误。

2.2 了解 Node.js 的 Net 模块

在 Node.js 中,Net 模块是用于创建 TCP 服务器和客户端的核心模块。它提供了一系列的方法和事件来处理 TCP 连接、数据传输等操作。例如,net.createServer() 方法用于创建一个 TCP 服务器实例,net.connect() 方法用于创建一个 TCP 客户端实例。通过监听服务器实例的 connection 事件,可以在有新客户端连接时进行相应处理;通过监听 data 事件,可以在接收到客户端发送的数据时进行处理。

2.3 项目初始化

在本地创建一个新的文件夹作为项目目录,例如 tcp - chat - system。进入该目录,在命令行中执行 npm init -y 命令,这会自动生成一个 package.json 文件,用于管理项目的依赖和脚本等信息。

三、构建 TCP 服务器

3.1 创建服务器实例

在项目目录中创建一个新的 JavaScript 文件,例如 server.js。在 server.js 文件中引入 Net 模块:

const net = require('net');

然后创建一个 TCP 服务器实例:

const server = net.createServer((socket) => {
    // 这里处理新连接的逻辑
});

在上述代码中,net.createServer() 方法接受一个回调函数,每当有新的客户端连接到服务器时,这个回调函数就会被调用,并且传入一个 socket 对象,通过这个 socket 对象可以与客户端进行通信。

3.2 监听连接事件

接下来,为服务器实例监听 connection 事件,代码如下:

server.on('connection', (socket) => {
    console.log('A client has connected.');
    socket.write('Welcome to the chat system!\n');
});

在这个事件处理函数中,当有新客户端连接时,会在控制台打印一条消息,并向客户端发送一条欢迎信息。socket.write() 方法用于向客户端发送数据,数据以字符串形式传入,并且可以在字符串末尾添加换行符 \n 来增强可读性。

3.3 监听数据事件

为了能够接收客户端发送的消息,需要为 socket 对象监听 data 事件:

server.on('connection', (socket) => {
    console.log('A client has connected.');
    socket.write('Welcome to the chat system!\n');

    socket.on('data', (data) => {
        const message = data.toString().trim();
        console.log(`Received from client: ${message}`);
        // 这里可以对收到的消息进行进一步处理,比如广播给其他客户端
    });
});

data 事件处理函数中,通过 data.toString() 将接收到的二进制数据转换为字符串,trim() 方法用于去除字符串两端的空白字符。然后在控制台打印接收到的消息。

3.4 监听端口

最后,让服务器监听一个指定的端口,例如 3000

const port = 3000;
server.listen(port, () => {
    console.log(`Server is listening on port ${port}`);
});

完整的 server.js 代码如下:

const net = require('net');

const server = net.createServer((socket) => {
    console.log('A client has connected.');
    socket.write('Welcome to the chat system!\n');

    socket.on('data', (data) => {
        const message = data.toString().trim();
        console.log(`Received from client: ${message}`);
    });
});

const port = 3000;
server.listen(port, () => {
    console.log(`Server is listening on port ${port}`);
});

在命令行中运行 node server.js,服务器就会开始监听 3000 端口,等待客户端连接。

四、构建 TCP 客户端

4.1 创建客户端实例

在项目目录中创建一个新的 JavaScript 文件,例如 client.js。同样引入 Net 模块:

const net = require('net');

然后创建一个 TCP 客户端实例,连接到刚才创建的服务器:

const client = net.connect({ port: 3000 }, () => {
    console.log('Connected to the server.');
});

net.connect() 方法中,通过对象指定要连接的服务器端口,当连接成功时,会执行回调函数,在控制台打印连接成功的消息。

4.2 发送数据

为了能够向服务器发送消息,在 client.js 中添加如下代码:

const readline = require('readline');

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

client.on('connect', () => {
    rl.question('Enter your message: ', (message) => {
        client.write(message + '\n');
        rl.close();
    });
});

这里引入了 readline 模块,它提供了一个接口来从可读流(如 process.stdin)中逐行读取数据。通过 readline.createInterface() 创建一个接口实例 rl,然后在客户端连接成功后,使用 rl.question() 方法在控制台提示用户输入消息。用户输入消息后,通过 client.write() 方法将消息发送给服务器,并关闭 readline 接口。

4.3 接收数据

为了能够接收服务器返回的数据,为 client 对象监听 data 事件:

client.on('data', (data) => {
    const message = data.toString().trim();
    console.log(`Received from server: ${message}`);
});

data 事件处理函数中,将接收到的数据转换为字符串并去除两端空白字符,然后在控制台打印接收到的消息。

完整的 client.js 代码如下:

const net = require('net');
const readline = require('readline');

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

const client = net.connect({ port: 3000 }, () => {
    console.log('Connected to the server.');
    rl.question('Enter your message: ', (message) => {
        client.write(message + '\n');
        rl.close();
    });
});

client.on('data', (data) => {
    const message = data.toString().trim();
    console.log(`Received from server: ${message}`);
});

在命令行中运行 node client.js,客户端会连接到服务器,提示用户输入消息,发送消息后可以接收服务器返回的数据。

五、实现多客户端实时聊天

5.1 管理客户端连接

为了实现多客户端实时聊天,需要在服务器端管理所有连接的客户端。在 server.js 中,定义一个数组来存储所有的客户端 socket 对象:

const clients = [];

const server = net.createServer((socket) => {
    clients.push(socket);
    console.log('A client has connected.');
    socket.write('Welcome to the chat system!\n');

    socket.on('data', (data) => {
        const message = data.toString().trim();
        console.log(`Received from client: ${message}`);
        // 这里可以对收到的消息进行进一步处理,比如广播给其他客户端
    });

    socket.on('end', () => {
        const index = clients.indexOf(socket);
        if (index!== -1) {
            clients.splice(index, 1);
            console.log('A client has disconnected.');
        }
    });
});

connection 事件处理函数中,将新连接的客户端 socket 对象添加到 clients 数组中。当客户端断开连接时,会触发 end 事件,在这个事件处理函数中,从 clients 数组中移除对应的 socket 对象,并在控制台打印客户端断开连接的消息。

5.2 广播消息

为了实现消息广播,在接收到客户端发送的消息时,需要遍历 clients 数组,将消息发送给除发送者之外的所有客户端。修改 server.js 中的 data 事件处理函数如下:

socket.on('data', (data) => {
    const message = data.toString().trim();
    console.log(`Received from client: ${message}`);

    clients.forEach((client) => {
        if (client!== socket) {
            client.write(`Received message: ${message}\n`);
        }
    });
});

在上述代码中,通过 clients.forEach() 方法遍历 clients 数组,对于每个客户端 client,如果它不是发送消息的客户端 socket,则向其发送包含接收到消息的内容。

5.3 客户端改进

为了能够持续输入和接收消息,改进 client.js 中的代码,使其可以循环接收用户输入并发送消息。修改后的 client.js 代码如下:

const net = require('net');
const readline = require('readline');

const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
});

const client = net.connect({ port: 3000 }, () => {
    console.log('Connected to the server.');
    sendMessage();
});

function sendMessage() {
    rl.question('Enter your message: ', (message) => {
        client.write(message + '\n');
        sendMessage();
    });
}

client.on('data', (data) => {
    const message = data.toString().trim();
    console.log(`Received from server: ${message}`);
});

在上述代码中,定义了一个 sendMessage() 函数,在客户端连接成功后调用该函数。函数中通过 rl.question() 提示用户输入消息,发送消息后再次调用自身,实现循环输入消息的功能。

六、处理消息格式与安全性

6.1 定义消息格式

为了更好地处理不同类型的消息,如普通聊天消息、用户加入/离开通知等,可以定义一种消息格式。例如,采用 JSON 格式来封装消息:

// 服务器端发送用户加入通知消息示例
const newUserMessage = {
    type: 'user - joined',
    username: 'newUser'
};
clients.forEach((client) => {
    client.write(JSON.stringify(newUserMessage) + '\n');
});

// 客户端处理接收到的消息示例
client.on('data', (data) => {
    const message = JSON.parse(data.toString().trim());
    if (message.type === 'user - joined') {
        console.log(`${message.username} has joined the chat.`);
    } else if (message.type === 'chat - message') {
        console.log(`${message.username}: ${message.content}`);
    }
});

在上述代码中,服务器端将用户加入的消息封装成 JSON 对象,通过 JSON.stringify() 方法转换为字符串发送给客户端。客户端接收到消息后,通过 JSON.parse() 方法解析字符串为 JSON 对象,根据 type 字段进行不同的处理。

6.2 安全性考虑

在实时聊天系统中,安全性至关重要。一方面,要防止恶意用户发送恶意代码,例如通过 HTML 标签注入来破坏页面。可以在服务器端对接收到的消息进行过滤,去除危险的 HTML 标签等。例如,可以使用 DOMPurify 库来进行 HTML 过滤:

const DOMPurify = require('dompurify');

socket.on('data', (data) => {
    const message = data.toString().trim();
    const safeMessage = DOMPurify.sanitize(message);
    // 处理安全后的消息
});

另一方面,要考虑用户认证和授权,确保只有合法用户能够连接到聊天系统并发送消息。可以采用用户名和密码认证的方式,在客户端连接时要求用户输入用户名和密码,服务器端验证通过后才允许其加入聊天。例如,可以在服务器端维护一个用户列表,验证用户名和密码是否匹配:

const users = {
    user1: 'password1',
    user2: 'password2'
};

const server = net.createServer((socket) => {
    socket.write('Enter username: ');
    socket.on('data', (data) => {
        const username = data.toString().trim();
        socket.write('Enter password: ');
        socket.once('data', (passwordData) => {
            const password = passwordData.toString().trim();
            if (users[username] === password) {
                socket.write('Login successful.\n');
                // 允许用户加入聊天
            } else {
                socket.write('Login failed. Disconnecting.\n');
                socket.end();
            }
        });
    });
});

在上述代码中,服务器在客户端连接后,先要求输入用户名,然后要求输入密码,验证用户名和密码匹配后允许用户加入聊天,否则断开连接。

七、优化与扩展

7.1 性能优化

在多客户端的情况下,为了提高性能,可以采用一些优化措施。例如,对于广播消息,可以采用更高效的数据结构和算法。可以使用 Set 来代替数组存储客户端连接,因为 Set 在查找元素时具有更高的效率。修改服务器端管理客户端连接的代码如下:

const clients = new Set();

const server = net.createServer((socket) => {
    clients.add(socket);
    console.log('A client has connected.');
    socket.write('Welcome to the chat system!\n');

    socket.on('data', (data) => {
        const message = data.toString().trim();
        console.log(`Received from client: ${message}`);
        clients.forEach((client) => {
            if (client!== socket) {
                client.write(`Received message: ${message}\n`);
            }
        });
    });

    socket.on('end', () => {
        clients.delete(socket);
        console.log('A client has disconnected.');
    });
});

另外,可以对消息处理进行优化,例如采用异步处理方式,避免阻塞事件循环。可以将一些耗时操作(如数据库查询、复杂计算等)放在 setImmediate()process.nextTick() 中执行。

7.2 功能扩展

可以对实时聊天系统进行功能扩展。例如,添加群聊功能,允许用户创建群聊并邀请其他用户加入。可以在服务器端维护一个群聊列表,每个群聊包含成员列表和聊天记录。当用户发送创建群聊消息时,服务器创建一个新的群聊对象,并将创建者加入群聊。当用户发送邀请消息时,服务器将被邀请用户加入对应的群聊。

又如,添加文件传输功能。可以在消息格式中定义一种文件传输消息类型,客户端发送文件时,将文件内容进行编码(如 Base64 编码),然后封装在消息中发送给服务器。服务器接收到文件消息后,将文件内容解码并保存到服务器端,同时将文件消息转发给其他客户端。

还可以添加离线消息功能,使用数据库(如 MongoDB、Redis 等)来存储离线消息。当用户离线时,服务器将发送给该用户的消息存储到数据库中。当用户重新连接时,服务器从数据库中读取离线消息并发送给用户。

通过以上步骤和优化扩展,我们可以使用 Node.js 的 Net 模块构建一个基于 TCP 的功能丰富、性能良好的实时聊天系统。