Nodejs开发进阶N-扩展网络模块TCP

在前面的章节中,我们已经讨论过作为Web应用开发平台,nodejs中最重要的模块:HTTP/S协议。逻辑上来看,它们作为网络协议是比较高级的应用层的协议,其实是基于TCP这样一个更加底层和基础协议的。这就将是我们准备在本文中要研究的问题。除此之外,其他的一些基础和扩展的网络协议相关的内容包括UDP、DNS等,我们也会在本文内一并探讨。由于相关技术在实际开发工作中,实际接触和使用这些底层协议的机会并不多,所以这里更多是概念性和了解性的探讨。

由于篇幅的限制,上面的内容,笔者需要划分为两个部分进行阐述,本文的将重点讨论TCP,后面的内容才会涉及UDP和DNS。

net模块

nodejs中,TCP协议是使用其net模块来实现的。在其官方文档中,如此描述这个模块:

node:net module 提供了用于创建基于信息流的TCP和IPC服务器(net.createServer())和客户端(net.createConnection())软件的异步网络API。

我们可以将其作为nodejs http模块的基础和底层协议模块,它的本质就是TCP网络协议的实现。和HTTP协议实现类型,它使用同一个主模块来支持客户端和服务端的应用模型。此外,有趣的是,除了TCP,它还支持可以使用基本相同的方式,来支持IPC,这个我们后面有简单的讨论。

和几乎所有的网络协议和应用的模式一样,TCP协议也是一种典型的客户端服务器(请求/响应),就是在逻辑上将通信的双方分为客户端和服务端;服务端启动后,侦听网络端口等待连接;客户端通过服务端的地址和端口发起连接请求;连接建立后就可以传输网络数据包来进行通信了。所以这里的几个核心的要素和概念就是服务端、客户端、连接通道、数据包等等。

在实际的Web应用开发场景中,直接使用net模块的机会其实不是很多。但如果要深入基于TCP协议的开发,比如要对接一些物联网(Internet Of Things, IOT)系统,由于它们在硬件和网络方面的一些限制,可能会直接使用TCP协议,或者基于TCP协议开发上层的应用协议。还有很多应用,它们也是基于累述的考虑,需要使用自己的私有协议,或者应用层语义,一般也是在TCP协议上进行扩展的。这时,对于TCP协议的了解和开发,就可能就需要了。这种开发模式,也经常被称为"Socket编程"。

示例代码

为了方便分析和表述,笔者参照了nodejs官方文档,并进行了简单的修改和合成,将本章节相关操作的示例代码,写在一个单一文件当中,然后再对应的组成步骤中具体分析,代码内容如下:

tcp.js 复制代码
const
PORT = 8124,
net = require('node:net');

//start server 
const startNet = ()=>{
    const server = net.createServer((c) => {
        // 'connection' listener.
        console.log("Server", "Client Connected");

        c
        .on('data', (data) => {
          console.log("Server","Receive:" + data);
          c.write("ServerData-" + Date.now());
        })
        .on('end', () => console.log("Server", "Client Disconnected"));
    })
    .on('error', (err) => { throw err; });

    server.listen(PORT, () => console.log('Server bound', PORT));      
}; startNet();

/// client 
let client;
const send = ()=>{
    if (!client) {
        client = net.createConnection({ port: PORT }, () => {
            // 'connect' listener.
            console.log('Client Connected to server OK!');
        });

        client
        .on('data', (data) => console.log("Client","Receive:" + data))
        .on('end', () => console.log("Client", "Disconnect From Server")); 

    } else { // send data
        client.write("ClientData-"+ Date.now());
    }

}; setInterval(send, 3000);

简单而言,这段代码分为两个大的部分。第一个部分创建了一个TCP服务器,它可以侦听一个TCP端口,在接收数据后进行相关的处理;第二个服务其实是一个TCP客户端,它连接服务器,并发送一些数据,并在收到服务端消息后,进行相关处理。

服务端

在服务端,通常的应用场景就是使用net模块来创建一个TCP服务器。参照示例代码,我们会发现,这几乎和HTTP模块是一模一样的,这里的基本操作和业务流程是:

  • 先调用createServer方法,创建一个Server实例
  • 创建方法的参数,是一个回调方法
  • 回调方法的参数,是一个connection对象,它将在有客户端连接时,触发并回调
  • 通过设置和重写connection对象的onData和onError事件,可以进行具体业务操作,特别是使用onData接收和处理客户端发送的数据
  • 设置连接方法之后,就可以调用server.listen方法,启动TCP服务的网络侦听
  • 要向客户端发送消息,可以调用连接对象的write方法
  • 侦听启动过程中,如果网络端口被占用,会抛出冲突错误,这是最常见的一个错误情况

从这个实现,我们也能够更容易理解HTTP协议和TCP协议之间的关系。它就是在TCP协议基础上,加入了应用层处理数据的机制,以遵守HTTP协议规范和约定而已。

客户端

net模块中,客户端使用的概念,不是一个简单的客户端或者请求对象,而是一个"连接"的对象。这里的标准业务流程是:

  • 通过调用net.createConnection方法,可以创建一个连接对象,这里我们也可以将其看作tcp客户端
  • 创建连接的参数是一个回调方法,名为connectListener,在此处可以设置在连接成功后进行的一些操作
  • 设置和重新连接对象的onData和onError事件,可以进行相关的业务操作,特别是在onData中,接收和处理服务器发送的数据
  • 要使用连接,向服务器发送数据,需要调用write方法

net对象

net对象是net模块的默认和根类。它的主要方法和属性包括:

  • createServer(): 创建服务器
  • connect()、createConnection() 连接服务器并创建连接对象,可选的设置参数包括服务端地址、端口、路径(IPC)、keepAlive等等,这两个方法是等效的(connect是createConnection的别名)
  • isIP()、isIPv6()、isIPv4(): 可以用于判断一个字符串的是否是有效的IP和类型

基于模块架构设计的考虑,net模块还提供了net.Blocklist(黑名单)和net.SocketAddress(插座地址)等类,它们基本上是比较简单的数据结构,这里不再赘述。

net模块的核心其实是net.Server对象和net.Socket对象,我们可以将它们理解称为TCP服务端和客户端的抽象实现。Server我们前面已经了解到,下面我们再来了解一下Socket。

socket对象(net.Socket)

在net模块中,TCP的网络连接,被抽象成为socket对象(net.Socket)。在客户端,它就是客户端本身; 在服务端,它是在客户端连接成功后,回调的对象;在这两个场景中,它们是一致的。使用时,它的主要属性和方法包括:

  • address() 获得绑定地址
  • localAddress、localPort、localFamily: 作为客户端连接的ip地址和相关信息
  • remoteAddress、remotePort、
  • write() 写入和发送数据
  • pause()、resume() 暂停和恢复读取数据
  • end() 结束写入,被称为"半关闭"连接
  • setTimemout()、timeout: 设置超时选项和属性
  • distroy() 关闭和销毁连接
  • bytesRead、bytesWritten: 作为连接对象读写的字节数量
  • pending、connecting、distroyed等: 各种连接状态
  • readyState: 就绪状态,包括opening、open、readyOnly、writeOnly等

socket使用回调事件来控制工作状态和进行操作,其主要回调事件包括:

  • connect 连接成功
  • ready 就绪
  • close 关闭
  • data 读取和接收数据
  • end 数据传输结束
  • timeout 超时
  • error 错误

blockList 黑名单

在阅读nodejs net模块文档的时候,笔者发现了一个有趣的功能: blocklist,应该就是黑名单的功能。笔者觉得,这个功能的立意和构思很好,但好像实现有一点问题(也可能是笔者的理解有问题)。

为什么这么说?我们先来看看相关的示例代码:

js 复制代码
const blockList = new net.BlockList();
blockList.addAddress('123.123.123.123');
blockList.addRange('10.0.0.1', '10.0.0.10');
blockList.addSubnet('8592:757c:efae:4e45::', 64, 'ipv6');

console.log(blockList.rules());  // Prints: rules list
console.log(blockList.check('10.0.0.3'));  // Prints: true
console.log(blockList.check('222.111.111.222'));  // Prints: false

从代码中可以看到,blockList的用法是,先创建黑名单实例,然后可以其包含添加地址、范围和子网。然后就可以用这个黑名单检查一个网络地址是否在黑名单内了。

笔者的疑问在于,这好像其实只是一个规则定义和检查程序,并没有真正的和TCP协议的运行结合起来,比如服务器可以应用这个规则,并在客户端连接时进行检查,如果不匹配,可以抛出如"forbiden"这种错误并拒绝连接。起码,从它的示例代码中,笔者尚未看到相关的内容。所以,这个特性,给笔者的感觉,只是做了一些基础的工作,尚未完善。

还有,既然有黑名单,也应该设计类似的白名单的机制,起码在规则和检查层面上,并不难实现。根据"默认安全"的信息系统安全原则,白名单对于很多应用场景如系统间互联,其实必要性更大,是一个很重要的基础安全策略。

IPC

从文档和实现来看,nodejs的net模块,不仅能够支持标准的TCP协议,还能够支持IPC协议。这样,理论上而言,我们就可以使用标准的网络通信的模式,来处理进程之间通信的问题。

IPC协议全称是进程间通信(Inter-Process Communication)协议,它定义了在操作系统中,不同进程之间传递数据或信号的一种机制和规则。 严格说来,IPC并非像TCP一样的标准协议,而是由各个操作系统定义和实现的一种或者多种进程间通信的范式,在不同的操作系统中,常见的实现方式包括管道(Pipe)、命名管道(Named Pipe)、信号(Signal)、消息队列(Message Queue)、共享内存(Shared Memory)、信号量(Semaphore)、套接字(Socket)等等。

本文中讨论的IPC,主要是指使用命名管道(Windows)和套件字(Socket)方式,就是类似网络端口通信实现的进程间通信机制。因为这种方式的工作模式和网络基本上一样的,无需在应用和业务层面进行调整和修改,通过简单的连接参数调整,就可以适应网络和IPC等不同的应用场景。

有了这样一个机制,就可以大大提升应用程序开发和架构时的灵活性。现代化的应用程序结构越来越复杂,开发者希望通过模块化,来降低开发的复杂性,并提高架构的灵活性,一个常见的方式就是将应用程序划分为后端服务(也经常被称为Core)和前端界面,它们在操作系统层面上,就是两个应用程序和进程,但业务逻辑

我们可以将前面TCP程序的案例,改造成为IPC的方式,可以使用的示例代码如下:

ipc.js 复制代码
const
net = require('node:net'),
path = require("path"),
PORT = 8124,
SCKFILE = path.join('\\\\?\\pipe', process.cwd(), 'SCK'+PORT);

// in linux could use:  '/tmp/echo.sock' as socket file 

//start server 
net.createServer((c) => {
    // 'connection' listener.
    console.log("Server", "Client Connected");

    c
    .on('data', (data) => {
        console.log("Server","Receive:" + data);
        c.write("ServerData-" + Date.now());
    })
    .on('end', () => console.log("Server", "Client Disconnected"));
})
.on('error', (err) => { throw err; })
.listen(SCKFILE, () => console.log('Server bound', SCKFILE));      

/// client 
let client;
const send = ()=>{
    if (!client) {
        client = net.createConnection({ path: SCKFILE }, () => {
            // 'connect' listener.
            console.log('Client Connected to server OK!');
        });

        client
        .on('data', (data) => console.log("Client","Receive:" + data))
        .on('end', () => console.log("Client", "Disconnect From Server")); 

    } else { // send data
        client.write("ClientData-"+ Date.now());
    }

}; setInterval(send, 3000);

这个程序的执行的效果,在本机上,是和前面TCP示例是完全一样的,只不过是使用了IPC的机制而已。但要注意,这一段代码,只能运行在Windows系统当中。其实,在标准的Unix或者Linux系统中,可以直接使用文件作为Socket,这是因为在这样的操作系统中,所有的输入输出接口都可以被抽象成为文件来进行操作(万物皆文件),文件本身也只是一个抽象的数据接口而非数据存储结构。但在Windows系统中,情况稍微有点复杂,所以它采用了一个"命名管道"的技术(使用\pipe作为开头的特殊路径),来模拟文件接口的方式。

理解了这个原理,我们将会认识到,使用IPC来实现应用程序进程模块之间的通信,比在本机使用网络堆栈,是有一定的优势的。首先只是使用一个文件描述符,不需要启用一个TCP网络协议模块,可以减少系统特别是网络相关资源的占用;另外不暴露网络端口,也不需要配置防火墙等,比较简单和安全;最后就是这个结构没有网络协议的解析和转换,理论传输性能也应该比较高。当然,这种方式会将应用程序模块绑定在同一个操作系统环境中运行,稍微牺牲了一点配置和运行的灵活性。

小结

本文探讨了在nodejs中,对于HTTP协议的底层基础协议-TCP的应用和实现方式。包括了模块定义、基本工作原理、主要类和方法、程序框架和一些典型的需求和使用场景。

相关推荐
qq_51583806 彩雷王40 分钟前
1004-05,使用workflow对象创建http任务,redis任务
redis·网络协议·http
27669582921 小时前
京东e卡滑块 分析
java·javascript·python·node.js·go·滑块·京东
PleaSure乐事1 小时前
【Node.js】内置模块FileSystem的保姆级入门讲解
javascript·node.js·es6·filesystem
GOTXX2 小时前
应用层协议HTTP
linux·网络·网络协议·计算机网络·http·fiddler
九圣残炎2 小时前
【springboot】简易模块化开发项目整合Redis
spring boot·redis·后端
.生产的驴2 小时前
Electron Vue框架环境搭建 Vue3环境搭建
java·前端·vue.js·spring boot·后端·electron·ecmascript
爱学的小涛2 小时前
【NIO基础】基于 NIO 中的组件实现对文件的操作(文件编程),FileChannel 详解
java·开发语言·笔记·后端·nio
爱学的小涛2 小时前
【NIO基础】NIO(非阻塞 I/O)和 IO(传统 I/O)的区别,以及 NIO 的三大组件详解
java·开发语言·笔记·后端·nio
北极无雪2 小时前
Spring源码学习:SpringMVC(4)DispatcherServlet请求入口分析
java·开发语言·后端·学习·spring
爱码少年3 小时前
springboot工程中使用tcp协议
spring boot·后端·tcp/ip