net
node:net
,提供了一个异步网络API模块,用于创建基于TCP流(父子socket之间的通讯)以及IPC流(父子管道之间的通讯) 首先,从net模块导出入手,如 所示: 并结合官方的学习文档来配合学习的话,我们可以很清楚该模块中关键的几个模块以及对应的重要方法,:point_down: 将一一道来!!
源头的开始(createConnection)
在开始分析关于这个
createConnection
方法入口之前,首先来看 的一段代码
const net = require('node:net'); const server = net.createServer(c => { console.info(c); }); server.listen('3000', res => { console.info(res); });
这里我们创建了一个server对象,并使得其对3000端口进行了一个监听,同时对官方的源码的一个解读,我们可以晓得,
net.createServer
本质上是调用了一个new Server()
的过程
通过对 上述过程的一个分析,我们可以晓得,关于这个tcp/ipc之间的通信,主要是通过其内部的_handle
对象来与libuv
底层之间
的通讯,来实现的! 那么这里创建的server
对象是一个怎样的对象的呢?它是如何实现通讯的呢?
Server
用来创建TCP/IPC服务的对象,一般通过
new Server()
或者net.createServer()
的方式来创建,其调用语法如下:const server = new Server([options][, connectListener]);
关于这里的参数options
的解析如下:
参数 | 取值/默认值 | 描述 |
---|---|---|
allowHalfOpen | boolean/false | 表示套接字将在可读端结束时自动结束可写端表示套接字将在可读端结束时自动结束可写端 |
pauseOnConnect | boolean/false | 指示套接字是否在开始连接是,自动暂停 |
onDelay | boolean/false | 连接时禁用nagle算法 |
keepAlive | boolean/false | 是否连接时保持长连接 |
keepAliveInitialDelay | number/0 | 延迟多长时间保持长连接状态 |
而关于另外一个参数connectListener
连接监听器,这个与后续的connect
监听方法是同一个对象来的,主要是用于在创建一个server之后,
当有其他的socket连接上来的时候,触发这个connectionListener连接监听器的回调,:confused: 那么关于 最开始所编写的代码它的一个运行过程是怎样的呢?
关于这个Server
对象,其提供了一个比较关键的API: listen
,用于对一个连接来设置监听,可以是tcp也可以是ipc的方式,这取决于所传递的参数,
也就是这个listen
方法将会有多种重载方法,该方法一旦调用,而且是仅在第一次调用触发listening
回调,:warning: 在平时的使用过程中,经常性地会遇到关于这个端口号或者
路径正在被使用中,较好的使用方式是使用一个异步回调不断地进行一个尝试,比如延迟1秒后重复执行这个listen方法,
关于这个listen方法有 种变体方法:
- server.listen(handle[, backlog][, callback])
- server.listen(options[, callback]),IPC或者TCP,取决于path与port端口传递了哪个
- server.listen(path[, backlog][, callback]),IPC专用
- server.listen([port[, host[, backlog]]][, callback]),TCP专用
将最开始的demo给拆分为两个流程:new一个Server对象的流程 + 让Server对象对端口设置监听服务 + 监听响应动作
- 创建一个Server对象的过程
从 可以看出关于在创建一个Server对象的时候,调用了EventEmitter构造方法,将这个Server对象直接继承于EventEmitter对象, 也就是这个Server拥有了父类EventEmitter的所有API方法;而且在创建一个Server的时候,也同时发起了一个connection事件!
- Server对象设置监听的过程
从 可以看出在Server对象中,关键在于_handle
对象中的rval
对象,该对象可以是一个TCP
,也可以是一个Pipe
,
而关于这里的rval
对象(以TCP为例),创建出来的rval的一个继承关系如下:
我们可以看出所有的server
对象其底层都是用c++层的libuv库来提供服务的,而关于底层libuv其实也是类似于node.js一样,提供了一个基于
事件循环机制,将事件进行消费的框架!!
- 响应监听动作
在响应事件后,并执行connection方法的时候,创建一个专门的socket与所有的其他socket进行通讯,实现点对点之间的一个直接通讯!
并将这个socket对象通过connection
事件参数调用出去,所以,也就 createServer与on('connection')方法中的回调参数是同一个对象的情况,
如下代码所示:
const net = require('node:net');
let tempC = null;
const server = net.createServer(c=>{
tempC = c;
});
server.on('connection', socket => {
console.info('这里的socket与createServer中的socket是否同一个-->', socket === tempC);
});
server.listen(8888);
这里对于createServer的回调进行了一个监听,然后也在其connection
回调中进行了一个监听,两者在触发回调的时候,响应的都是统一的一个
socket对象,而且就算是有多个client与这个server进行通讯的话,也是公用的统一一个socket对象的!
Socket
TCP/IPC流式套接字的抽象,它也是一个
EventEmitter
的实例,因而也会有对应的on/emit相关的操作,而且是作为 的Server
对象中的一个 自动创建的connectListener中的一个回调参数,同样地,要区分它是一个TCP流还是IPC流的话,关键在于这个connect
方法所传递的参数(这个我们后面说明一下)。 这里Socket对象,它为毛是一个EventEmitter呢?它的源码里面也没有明确说它调用了EventEmitter
对象实现继承关系呀? 在它的构造方法中,:u6709: 两行代码:ObjectSetPrototypeOf(Socket.prototype, stream.Duplex.prototype);
ObjectSetPrototypeOf(Socket, stream.Duplex);
这里stream.Duplex
是steam对象中的全双工流对象,可读可写,而且继承于Readable,然后继承于Stream,而Stream也从EventEmitter那继承, 因此这个Socket对象也有了对应的on/emit相关的操作!!
关于Socket对象的使用,它的一个使用语法如下:
const socket = new Socket([options]);
而options对象的相关属性描述如下:
参数 | 取值/默认值 | 描述 |
---|---|---|
fd | number/-1 | 文件套接字具柄,可最为当前socket流的数据源 |
allowHalfOpen | boolean/false | 在可读端(比如client端)结束时,自动结束可写端(比如server) |
readable | boolean/false | 当传递fd时,允许读取socket中的数据 |
writable | boolean/false | 当传递fd时,允许往socket中写入数据 |
signal | AbortSignal | 用于终止socket的信号 |
为什么说Socket
对象是TCP或者IPC取决于connect()
方法呢?
首先,该方法的一个定义如下:Socket.prototype.connect = function(...args)
这里的path
字段,将决定了它是一个TCP或者是一个IPC的套接字了。
然后一旦完成一个套接字的创建,如果在构造方法中也传递了这个callback
回调函数的话则立马触发一个connect
事件,并将这个callback抛出
只要理解了Socket
对象它是一个全双工流的话,就能够清楚的理解到它相关的监听事件以及一些API方法,比如可读可写流的相关的
重要监听事件
- data: 用于接收往该socket对象传递的数据,一般用来从socket流中获取数据;
- drain: 当可写入缓冲区不够时触发该方法;
- end: 当套接字的另一段发出传输结束信号时,触发该方法;
- lookup: 在解析主机名之后发出连接之前触发该方法,表示即将建立连接;
- timeout: 在socket不再处于活动时触发该方法,告知该socket套接字已经空闲,需要手动关闭;
相关的api方法
- connect: 连接一个socket套接字;
- destroy: 确认该socket不再进行I/O操作,销毁该套接字;
- end: 告知server端已经停止I/O操作,有可能这时server还在传输;
- pause: 暂停从socket中读取数据;
- resume: 在调用了
pause
方法之后,通过该方法恢复数据的读取; - write: 往socket对象中写入数据
关于net的一个完整使用例子
服务端
// server端
const net = require('node:net');
const process = require('node:process');
const port = 8888;
let tempC = null;
const server = net.createServer(c=>{
tempC = c;
console.info('我是连接器监听');
});
server.listen(8888);
server.on('connection', socket => {
debugger
console.info('这里的socket与createServer中的socket是否同一个-->', socket === tempC);
// 远程客户端地址
console.log(`connected:${socket.remoteAddress}:${socket.remotePort}`);
// 本地服务端地址
console.log(`local:${socket.localAddress}:${socket.localPort}`);
// 向客户端发送数据
socket.write("服务端:你好客户端");
// 收到客户端数据时
socket.on("data", (data) => {
console.log(`${data}`);
});
// 客户端主动断连,触发end事件
socket.on("end", (data) => {
console.log(`客户端${socket.remoteAddress}:${socket.remotePort}已断连`);
});
});
console.log(`server listen on ${port}`);
客户端
const net = require('node:net');
const port = 8888;
const client = new net.Socket();
client.connect(port, () => {
console.log(`connected to: ${port}`);
// 向服务端发送数据
client.write("客户端:你好服务端");
// 收到服务端数据时
client.on("data", (data) => {
console.log(data.toString());
client.write("客户端:已收到");
});
// 客户端主动断连,触发自己的end事件
client.on("end", () => {
console.log("链接已主动断开");
});
});
// 收到客户端的连接是
client.on('connect', () => {
console.info('客户端连接上了');
});
client.on('close', () => {
console.info('客户端我关闭了要')
})