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

JavaScript实现Node非HTTP网络通信的要点

2024-07-045.9k 阅读

理解 Node.js 网络通信基础

Node.js 网络模块概述

Node.js 提供了丰富的网络模块来支持不同类型的网络通信,其中包括 netdgram 等,这些模块用于非 HTTP 网络通信场景。net 模块主要用于创建 TCP 服务器和客户端,而 dgram 模块则专注于 UDP 协议的通信。

在 TCP 通信中,连接是可靠的,数据按顺序传输,适合对数据完整性和顺序要求较高的场景,如文件传输、数据库连接等。而 UDP 通信则是无连接的,数据传输速度快但不保证可靠性,适用于实时性要求高但对数据丢失不太敏感的场景,像视频流、音频流传输等。

TCP 通信原理与 Node.js 实现要点

  1. TCP 服务器创建
    • 在 Node.js 中,使用 net 模块创建 TCP 服务器非常直观。以下是一个简单的 TCP 服务器示例:
const net = require('net');
const server = net.createServer((socket) => {
    console.log('A client has connected.');
    socket.write('Welcome to the TCP server!\r\n');
    socket.on('data', (data) => {
        console.log('Received data: ', data.toString());
        socket.write('Message received: '.concat(data.toString()));
    });
    socket.on('end', () => {
        console.log('Client has disconnected.');
    });
});
server.listen(8080, '127.0.0.1', () => {
    console.log('Server listening on port 8080');
});
  • 首先,通过 net.createServer 创建一个服务器实例,传入一个回调函数,该回调函数在每次有新客户端连接时被调用,socket 参数代表与客户端的连接。
  • socket.write 方法用于向客户端发送数据,\r\n 是 TCP 通信中常用的换行符,用于分隔消息。
  • socket.on('data') 事件监听客户端发送的数据,data 是一个 Buffer 对象,需要通过 toString() 方法转换为字符串。
  • socket.on('end') 事件在客户端断开连接时触发。
  • server.listen 方法用于启动服务器,指定监听的端口和地址,回调函数在服务器成功监听后执行。
  1. TCP 客户端连接
    • 下面是一个与上述服务器对应的 TCP 客户端示例:
const net = require('net');
const client = net.connect({ port: 8080, host: '127.0.0.1' }, () => {
    console.log('Connected to the server');
    client.write('Hello, server!');
});
client.on('data', (data) => {
    console.log('Received from server: ', data.toString());
});
client.on('end', () => {
    console.log('Connection to server ended');
});
  • net.connect 方法用于创建与服务器的连接,传入连接的端口和主机地址,回调函数在成功连接到服务器后执行。
  • client.write 方法用于向服务器发送数据。
  • client.on('data') 事件监听服务器返回的数据,client.on('end') 事件在服务器关闭连接时触发。

UDP 通信原理与 Node.js 实现要点

  1. UDP 服务器创建
    • 使用 dgram 模块创建 UDP 服务器的示例如下:
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
server.on('message', (msg, rinfo) => {
    console.log('Received message: ', msg.toString());
    console.log('From address: ', rinfo.address);
    console.log('From port: ', rinfo.port);
    const response = 'Message received: '.concat(msg.toString());
    server.send(response, rinfo.port, rinfo.address, (err) => {
        if (err) {
            console.error('Error sending response: ', err);
        }
    });
});
server.bind(8081, '127.0.0.1', () => {
    console.log('UDP server listening on port 8081');
});
  • dgram.createSocket('udp4') 创建一个 UDP v4 套接字实例。
  • server.on('message') 事件监听接收到的 UDP 消息,msg 是接收到的数据(Buffer 对象),rinfo 包含发送方的地址和端口信息。
  • server.send 方法用于向发送方回复消息,需要指定回复的数据、目标端口、目标地址,以及一个可选的回调函数来处理发送错误。
  • server.bind 方法用于启动 UDP 服务器,指定监听的端口和地址,回调函数在服务器成功绑定后执行。
  1. UDP 客户端发送数据
    • UDP 客户端示例如下:
const dgram = require('dgram');
const client = dgram.createSocket('udp4');
const message = Buffer.from('Hello, UDP server!');
client.send(message, 0, message.length, 8081, '127.0.0.1', (err) => {
    if (err) {
        console.error('Error sending message: ', err);
    }
});
client.on('message', (msg, rinfo) => {
    console.log('Received response: ', msg.toString());
    console.log('From address: ', rinfo.address);
    console.log('From port: ', rinfo.port);
    client.close();
});
  • dgram.createSocket('udp4') 创建 UDP 客户端套接字。
  • client.send 方法用于向 UDP 服务器发送数据,需要指定要发送的数据、数据的偏移量、数据长度、目标端口和目标地址,以及一个可选的回调函数来处理发送错误。
  • client.on('message') 事件监听服务器返回的消息,接收到消息后打印相关信息并关闭客户端套接字。

处理连接管理与并发

TCP 连接的持久化与复用

在 TCP 通信中,连接的持久化和复用可以减少连接建立和关闭的开销,提高性能。在 Node.js 的 TCP 实现中,可以通过保持 socket 对象的引用并重复使用来实现连接的持久化。

例如,在一个需要频繁与服务器交互的客户端应用中,可以将 net.connect 创建的 socket 对象保存为全局变量或类的属性:

const net = require('net');
class MyClient {
    constructor() {
        this.socket = net.connect({ port: 8080, host: '127.0.0.1' });
        this.socket.on('connect', () => {
            console.log('Connected to the server');
        });
        this.socket.on('data', (data) => {
            console.log('Received from server: ', data.toString());
        });
    }
    sendMessage(message) {
        this.socket.write(message);
    }
}
const client = new MyClient();
client.sendMessage('Hello, server!');

这样,每次调用 client.sendMessage 方法时,都复用了已建立的 TCP 连接,而不是重新创建连接。

处理 TCP 并发连接

Node.js 的事件驱动架构使得处理并发 TCP 连接变得相对容易。当有多个客户端连接到 TCP 服务器时,服务器会为每个连接创建一个独立的 socket 对象,并且通过事件机制来处理不同 socket 上的事件。

例如,在一个聊天服务器场景中,服务器需要处理多个客户端的连接和消息发送:

const net = require('net');
const server = net.createServer((socket) => {
    socket.name = 'Guest'.concat(Math.random().toString(36).substring(7));
    console.log(`${socket.name} has connected.`);
    socket.on('data', (data) => {
        const message = `${socket.name}: ${data.toString()}`;
        server.clients.forEach((client) => {
            if (client!== socket) {
                client.write(message);
            }
        });
    });
    socket.on('end', () => {
        console.log(`${socket.name} has disconnected.`);
        const index = server.clients.indexOf(socket);
        if (index!== -1) {
            server.clients.splice(index, 1);
        }
    });
});
server.clients = [];
server.on('connection', (socket) => {
    server.clients.push(socket);
});
server.listen(8080, '127.0.0.1', () => {
    console.log('Server listening on port 8080');
});

在这个示例中,server.clients 数组用于存储所有连接的客户端 socket 对象。当有新客户端连接时,将其添加到数组中;当客户端发送消息时,服务器将消息广播给除发送者之外的所有其他客户端;当客户端断开连接时,从数组中移除该客户端。

UDP 无连接特性下的并发处理

UDP 的无连接特性意味着它不需要像 TCP 那样管理连接状态。在处理并发 UDP 通信时,Node.js 的 dgram 模块同样基于事件驱动。多个 UDP 客户端可以同时向 UDP 服务器发送数据,服务器通过 server.on('message') 事件来处理接收到的消息。

例如,在一个简单的 UDP 广播场景中,多个客户端可以同时向服务器发送消息:

// UDP 服务器
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
server.on('message', (msg, rinfo) => {
    console.log('Received from ', rinfo.address, ':', rinfo.port, '- ', msg.toString());
});
server.bind(8081, '127.0.0.1', () => {
    console.log('UDP server listening on port 8081');
});

// UDP 客户端 1
const client1 = dgram.createSocket('udp4');
const message1 = Buffer.from('Message from client 1');
client1.send(message1, 0, message1.length, 8081, '127.0.0.1', (err) => {
    if (err) {
        console.error('Error sending message from client 1: ', err);
    }
});

// UDP 客户端 2
const client2 = dgram.createSocket('udp4');
const message2 = Buffer.from('Message from client 2');
client2.send(message2, 0, message2.length, 8081, '127.0.0.1', (err) => {
    if (err) {
        console.error('Error sending message from client 2: ', err);
    }
});

在这个示例中,两个 UDP 客户端同时向服务器发送消息,服务器通过 server.on('message') 事件分别处理接收到的来自不同客户端的消息。

数据处理与协议设计

TCP 数据处理与粘包拆包问题

在 TCP 通信中,由于 TCP 是基于流的协议,数据可能会出现粘包和拆包问题。粘包是指多个数据包被合并成一个包发送,拆包则是指一个数据包被分割成多个部分发送。

为了解决这些问题,常见的方法有以下几种:

  1. 定长包:每个数据包都固定长度。发送方在发送数据前,将数据填充到固定长度,接收方每次按固定长度读取数据。例如,假设每个数据包固定长度为 1024 字节:
// 发送方
const net = require('net');
const client = net.connect({ port: 8080, host: '127.0.0.1' });
const message = 'Hello, server!';
const paddedMessage = message.padEnd(1024, '\0');
client.write(paddedMessage);

// 接收方
const server = net.createServer((socket) => {
    let buffer = '';
    socket.on('data', (data) => {
        buffer += data.toString();
        while (buffer.length >= 1024) {
            const packet = buffer.substring(0, 1024);
            console.log('Received packet: ', packet.trim());
            buffer = buffer.substring(1024);
        }
    });
});
server.listen(8080, '127.0.0.1');
  1. 包头 + 包体:在每个数据包前添加一个包头,包头中包含包体的长度等信息。接收方先读取包头获取包体长度,再按长度读取包体。例如:
// 发送方
const net = require('net');
const client = net.connect({ port: 8080, host: '127.0.0.1' });
const message = 'Hello, server!';
const length = Buffer.from(message.length.toString().padStart(4, '0'));
const body = Buffer.from(message);
const packet = Buffer.concat([length, body]);
client.write(packet);

// 接收方
const server = net.createServer((socket) => {
    let buffer = Buffer.alloc(0);
    let length = 0;
    socket.on('data', (data) => {
        buffer = Buffer.concat([buffer, data]);
        if (length === 0 && buffer.length >= 4) {
            length = parseInt(buffer.toString('utf8', 0, 4));
            buffer = buffer.slice(4);
        }
        while (buffer.length >= length && length > 0) {
            const packet = buffer.slice(0, length);
            console.log('Received packet: ', packet.toString());
            buffer = buffer.slice(length);
            length = 0;
            if (buffer.length >= 4) {
                length = parseInt(buffer.toString('utf8', 0, 4));
                buffer = buffer.slice(4);
            }
        }
    });
});
server.listen(8080, '127.0.0.1');

UDP 数据处理与可靠性增强

UDP 本身是不可靠的协议,为了增强 UDP 数据传输的可靠性,可以采用以下方法:

  1. 校验和:在 UDP 数据包中添加校验和字段,发送方计算数据的校验和并填充到校验和字段,接收方接收到数据后重新计算校验和并与接收到的校验和字段比较,如果不一致则丢弃数据包。虽然 Node.js 的 dgram 模块在底层已经实现了一定的校验和机制,但在应用层也可以进一步实现自定义的校验和逻辑。
  2. 重传机制:在应用层实现重传机制。发送方发送数据后,启动一个定时器,如果在一定时间内没有收到接收方的确认消息,则重新发送数据。例如:
const dgram = require('dgram');
const client = dgram.createSocket('udp4');
const message = Buffer.from('Hello, UDP server!');
const serverAddress = '127.0.0.1';
const serverPort = 8081;
let attempts = 0;
const sendMessage = () => {
    client.send(message, 0, message.length, serverPort, serverAddress, (err) => {
        if (err) {
            console.error('Error sending message: ', err);
        }
    });
    const timeout = setTimeout(() => {
        if (attempts < 3) {
            attempts++;
            console.log('Retrying... attempt ', attempts);
            sendMessage();
        } else {
            console.log('Max retry attempts reached. Giving up.');
            client.close();
        }
    }, 1000);
};
sendMessage();

在这个示例中,客户端发送消息后启动一个 1 秒的定时器,如果 1 秒内没有收到确认(这里未实现确认机制,仅为示例),则重新发送消息,最多重试 3 次。

自定义网络协议设计

在实际应用中,常常需要设计自定义的网络协议来满足特定的业务需求。无论是 TCP 还是 UDP 通信,自定义协议设计都需要考虑以下几个方面:

  1. 协议头设计:协议头包含一些元信息,如协议版本、消息类型、数据长度等。例如,设计一个简单的自定义协议头:
// 协议头结构
// 2 字节:协议版本
// 1 字节:消息类型
// 4 字节:数据长度
const PROTOCOL_VERSION = 1;
const MESSAGE_TYPE_DATA = 1;
function createPacket(message) {
    const messageBuffer = Buffer.from(message);
    const versionBuffer = Buffer.alloc(2);
    versionBuffer.writeUInt16BE(PROTOCOL_VERSION);
    const typeBuffer = Buffer.alloc(1);
    typeBuffer.writeUInt8(MESSAGE_TYPE_DATA);
    const lengthBuffer = Buffer.alloc(4);
    lengthBuffer.writeUInt32BE(messageBuffer.length);
    return Buffer.concat([versionBuffer, typeBuffer, lengthBuffer, messageBuffer]);
}
  1. 协议解析:在接收端,需要根据协议头的设计来解析接收到的数据:
function parsePacket(buffer) {
    if (buffer.length < 7) {
        return null;
    }
    const version = buffer.readUInt16BE(0);
    const type = buffer.readUInt8(2);
    const length = buffer.readUInt32BE(3);
    if (buffer.length < 7 + length) {
        return null;
    }
    const data = buffer.slice(7, 7 + length);
    return { version, type, data };
}

通过这样的自定义协议设计,可以更好地组织和管理网络通信中的数据,满足不同业务场景的需求。

安全性与优化

TCP 通信的安全增强

  1. SSL/TLS 加密:在 Node.js 中,可以使用 tls 模块为 TCP 通信添加 SSL/TLS 加密。以下是一个简单的使用 tls 模块创建加密 TCP 服务器和客户端的示例:
// 服务器端
const tls = require('tls');
const fs = require('fs');
const options = {
    key: fs.readFileSync('server-key.pem'),
    cert: fs.readFileSync('server-cert.pem')
};
const server = tls.createServer(options, (socket) => {
    console.log('A client has connected.');
    socket.write('Welcome to the secure TCP server!\r\n');
    socket.on('data', (data) => {
        console.log('Received data: ', data.toString());
        socket.write('Message received: '.concat(data.toString()));
    });
    socket.on('end', () => {
        console.log('Client has disconnected.');
    });
});
server.listen(8080, '127.0.0.1', () => {
    console.log('Secure server listening on port 8080');
});

// 客户端
const tls = require('tls');
const client = tls.connect(8080, '127.0.0.1', () => {
    console.log('Connected to the secure server');
    client.write('Hello, secure server!');
});
client.on('data', (data) => {
    console.log('Received from server: ', data.toString());
});
client.on('end', () => {
    console.log('Connection to server ended');
});

在服务器端,通过 tls.createServer 创建一个 TLS 服务器,传入包含私钥和证书的 options 对象。客户端使用 tls.connect 连接到服务器,这样通信数据在传输过程中就会被加密。 2. 身份验证:除了加密,还可以在 TCP 通信中实现身份验证。例如,可以在连接建立后,服务器要求客户端发送用户名和密码进行验证:

// 服务器端
const net = require('net');
const users = {
    'admin': 'password123'
};
const server = net.createServer((socket) => {
    socket.write('Please enter username: ');
    let username = '';
    socket.on('data', (data) => {
        if (username === '') {
            username = data.toString().trim();
            socket.write('Please enter password: ');
        } else {
            const password = data.toString().trim();
            if (users[username] === password) {
                socket.write('Authentication successful!\r\n');
            } else {
                socket.write('Authentication failed. Closing connection.\r\n');
                socket.end();
            }
        }
    });
});
server.listen(8080, '127.0.0.1');
// 客户端
const net = require('net');
const client = net.connect({ port: 8080, host: '127.0.0.1' }, () => {
    console.log('Connected to the server');
});
client.on('data', (data) => {
    const message = data.toString().trim();
    if (message === 'Please enter username: ') {
        client.write('admin\r\n');
    } else if (message === 'Please enter password: ') {
        client.write('password123\r\n');
    } else {
        console.log('Received from server: ', message);
    }
});

在这个示例中,服务器维护一个用户列表,客户端连接后按提示输入用户名和密码进行身份验证。

UDP 通信的优化策略

  1. 合理设置缓冲区大小:在 UDP 通信中,合理设置发送和接收缓冲区的大小可以提高性能。在 Node.js 中,可以通过 dgram.SocketsetSendBufferSizesetRecvBufferSize 方法来设置缓冲区大小。例如:
const dgram = require('dgram');
const server = dgram.createSocket('udp4');
server.setRecvBufferSize(65536);
server.on('message', (msg, rinfo) => {
    console.log('Received message: ', msg.toString());
});
server.bind(8081, '127.0.0.1');

const client = dgram.createSocket('udp4');
client.setSendBufferSize(65536);
const message = Buffer.from('Hello, UDP server!');
client.send(message, 0, message.length, 8081, '127.0.0.1');

在这个示例中,将服务器的接收缓冲区和客户端的发送缓冲区都设置为 65536 字节,这样可以更好地处理大数据量的 UDP 通信。 2. 避免广播风暴:在使用 UDP 广播时,要注意避免广播风暴。广播风暴是指大量的广播数据包充斥网络,导致网络性能下降甚至瘫痪。可以通过限制广播频率、设置广播范围等方式来避免广播风暴。例如,在发送 UDP 广播消息时,可以设置一个定时器,控制广播消息的发送频率:

const dgram = require('dgram');
const client = dgram.createSocket('udp4');
const message = Buffer.from('Broadcast message');
let count = 0;
const sendBroadcast = () => {
    client.send(message, 0, message.length, 8081, '255.255.255.255', (err) => {
        if (err) {
            console.error('Error sending broadcast: ', err);
        }
    });
    if (count < 5) {
        count++;
        setTimeout(sendBroadcast, 1000);
    } else {
        client.close();
    }
};
sendBroadcast();

在这个示例中,每 1 秒发送一次广播消息,最多发送 5 次,从而避免过度广播导致网络问题。

通过以上对 Node.js 中 JavaScript 实现非 HTTP 网络通信的要点介绍,包括 TCP 和 UDP 通信的基础实现、连接管理、数据处理、安全性和优化等方面,希望能够帮助开发者更好地掌握和应用 Node.js 的网络通信能力,构建高效、可靠的网络应用。