一篇搞懂tcp,http,socket,socket连接池之间的关系
前言
作为一名开发人员我们经常会听到HTTP协议、TCP/IP协议、UDP协议、Socket、Socket长连接、Socket连接池等字眼,然而它们之间的关系、区别及原理并不是所有人都能理解清楚,这篇文章就从网络协议基础开始到Socket连接池,一步一步解释他们之间的关系。
七层网络模型
首先从网络通信的分层模型讲起:七层模型,亦称OSI(Open System Interconnection)模型。自下往上分为:物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。所有有关通信的都离不开它,下面这张图片介绍了各层所对应的一些协议和硬件

通过上图,我知道IP协议对应于网络层,TCP、UDP协议对应于传输层,而HTTP协议对应于应用层,OSI并没有Socket,那什么是Socket,后面我们将结合代码具体详细介绍。
TCP和UDP连接
关于传输层TCP、UDP协议可能我们平时遇见的会比较多,有人说TCP是安全的,UDP是不安全的,UDP传输比TCP快,那为什么呢,我们先从TCP的连接建立的过程开始分析,然后解释UDP和TCP的区别。
TCP的三次握手和四次分手
我们知道TCP建立连接需要经过三次握手,而断开连接需要经过四次分手,那三次握手和四次分手分别做了什么和如何进行的。

第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
第二次握手:服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
完成了三次握手,客户端和服务器端就可以开始传送数据。以上就是TCP三次握手的总体介绍。通信结束客户端和服务端就断开连接,需要经过四次分手确认。
第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。
可以看到一次tcp请求的建立及关闭至少进行7次通信,这还不包过数据的通信,而UDP不需3次握手和4次分手。
TCP和UDP的区别
1、TCP是面向链接的,虽然说网络的不安全不稳定特性决定了多少次握手都不能保证连接的可靠性,但TCP的三次握手在最低限度上(实际上也很大程度上保证了)保证了连接的可靠性;而UDP不是面向连接的,UDP传送数据前并不与对方建立连接,对接收到的数据也不发送确认信号,发送端不知道数据是否会正确接收,当然也不用重发,所以说UDP是无连接的、不可靠的一种数据传输协议。
2、也正由于1所说的特点,使得UDP的开销更小数据传输速率更高,因为不必进行收发数据的确认,所以UDP的实时性更好。知道了TCP和UDP的区别,就不难理解为何采用TCP传输协议的MSN比采用UDP的QQ传输文件慢了,但并不能说QQ的通信是不安全的,因为程序员可以手动对UDP的数据收发进行验证,比如发送方对每个数据包进行编号然后由接收方进行验证啊什么的,即使是这样,UDP因为在底层协议的封装上没有采用类似TCP的“三次握手”而实现了TCP所无法达到的传输效率。
问题
关于传输层我们会经常听到一些问题
1.TCP服务器最大并发连接数是多少?
关于TCP服务器最大并发连接数有一种误解就是“因为端口号上限为65535,所以TCP服务器理论上的可承载的最大并发连接数也是65535”。首先需要理解一条TCP连接的组成部分:客户端IP、客户端端口、服务端IP、服务端端口。所以对于TCP服务端进程来说,他可以同时连接的客户端数量并不受限于可用端口号,理论上一个服务器的一个端口能建立的连接数是全球的IP数*每台机器的端口数。实际并发连接数受限于linux可打开文件数,这个数是可以配置的,可以非常大,所以实际上受限于系统性能。通过#ulimit -n 查看服务的最大文件句柄数,通过ulimit -n xxx 修改 xxx是你想要能打开的数量。也可以通过修改系统参数:
#vi /etc/security/limits.conf
* soft nofile 65536
* hard nofile 65536
2.为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?
这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的Socket可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。
3.TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态会产生什么问题
通信双方建立TCP连接后,主动关闭连接的一方就会进入TIME_WAIT状态,TIME_WAIT状态维持时间是两个MSL时间长度,也就是在1-4分钟,Windows操作系统就是4分钟。进入TIME_WAIT状态的一般情况下是客户端,一个TIME_WAIT状态的连接就占用了一个本地端口。一台机器上端口号数量的上限是65536个,如果在同一台机器上进行压力测试模拟上万的客户请求,并且循环与服务端进行短连接通信,那么这台机器将产生4000个左右的TIME_WAIT Socket,后续的短连接就会产生address already in use : connect的异常,如果使用Nginx作为方向代理也需要考虑TIME_WAIT状态,发现系统存在大量TIME_WAIT状态的连接,通过调整内核参数解决。
vi /etc/sysctl.conf
编辑文件,加入以下内容:
net.ipv4.tcp_syncookies = 1
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_tw_recycle = 1
net.ipv4.tcp_fin_timeout = 30
然后执行 /sbin/sysctl -p 让参数生效。
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭;
net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭;
net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。
net.ipv4.tcp_fin_timeout 修改系統默认的TIMEOUT时间
相关视频推荐
10道网络八股文,每道都很经典,让你在面试中逼格满满
徒手实现网络协议栈,请准备好环境,一起来写代码
C++网络面试题:TCP/UDP应用场景分析,UDP如何实现可靠性设计
免费学习地址:C/C++Linux服务器开发/后台架构师
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享

HTTP协议
关于TCP/IP和HTTP协议的关系,网络有一段比较容易理解的介绍:“我们在传输数据时,可以只使用(传输层)TCP/IP协议,但是那样的话,如果没有应用层,便无法识别数据内容。如果想要使传输的数据有意义,则必须使用到应用层协议。应用层协议有很多,比如HTTP、FTP、TELNET等,也可以自己定义应用层协议。
HTTP协议即超文本传送协议(Hypertext Transfer Protocol ),是Web联网的基础,也是手机联网常用的协议之一,WEB使用HTTP协议作应用层协议,以封装HTTP文本信息,然后使用TCP/IP做传输层协议将它发到网络上。
由于HTTP在每次请求结束后都会主动释放连接,因此HTTP连接是一种“短连接”,要保持客户端程序的在线状态,需要不断地向服务器发起连接请求。通常 的做法是即时不需要获得任何数据,客户端也保持每隔一段固定的时间向服务器发送一次“保持连接”的请求,服务器在收到该请求后对客户端进行回复,表明知道 客户端“在线”。若服务器长时间无法收到客户端的请求,则认为客户端“下线”,若客户端长时间无法收到服务器的回复,则认为网络已经断开。
下面是一个简单的HTTP Post application/json数据内容的请求:
POST HTTP/1.1
Host: 127.0.0.1:9017
Content-Type: application/json
Cache-Control: no-cache{"a":"a"}
关于Socket(套接字)
现在我们了解到TCP/IP只是一个协议栈,就像操作系统的运行机制一样,必须要具体实现,同时还要提供对外的操作接口。就像操作系统会提供标准的编程接口,比如Win32编程接口一样,TCP/IP也必须对外提供编程接口,这就是Socket。现在我们知道,Socket跟TCP/IP并没有必然的联系。Socket编程接口在设计的时候,就希望也能适应其他的网络协议。所以,Socket的出现只是可以更方便的使用TCP/IP协议栈而已,其对TCP/IP进行了抽象,形成了几个最基本的函数接口。比如create,listen,accept,connect,read和write等等。
不同语言都有对应的建立Socket服务端和客户端的库,下面举例Nodejs如何创建服务端和客户端:
服务端:
const net = require('net');
const server = net.createServer();
server.on('connection', (client) => {client.write('Hi!\n'); // 服务端向客户端输出信息,使用 write() 方法client.write('Bye!\n');//client.end(); // 服务端结束该次会话
});
server.listen(9000);
服务监听9000端口
下面使用命令行发送http请求和telnet
$ curl http://127.0.0.1:9000
Bye!$telnet 127.0.0.1 9000
Trying 192.168.1.21...
Connected to 192.168.1.21.
Escape character is '^]'.
Hi!
Bye!
Connection closed by foreign host.
注意到curl只处理了一次报文。
客户端
const client = new net.Socket();
client.connect(9000, '127.0.0.1', function () {
});
client.on('data', (chunk) => {console.log('data', chunk.toString())//data Hi!//Bye!
});
Socket长连接
所谓长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接(心跳包),一般需要自己做在线维持。 短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接。比如Http的,只是连接、请求、关闭,过程时间较短,服务器若是一段时间内没有收到请求即可关闭连接。其实长连接是相对于通常的短连接而说的,也就是长时间保持客户端与服务端的连接状态。
通常的短连接操作步骤是:
连接→数据传输→关闭连接;
而长连接通常就是:
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;
什么时候用长连接,短连接?
长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理 速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成Socket错误,而且频繁的Socket创建也是对资源的浪费。
什么是心跳包为什么需要:
心跳包就是在客户端和服务端间定时通知对方自己状态的一个自己定义的命令字,按照一定的时间间隔发送,类似于心跳,所以叫做心跳包。网络中的接收和发送数据都是使用Socket进行实现。但是如果此套接字已经断开(比如一方断网了),那发送数据和接收数据的时候就一定会有问题。可是如何判断这个套接字是否还可以使用呢?这个就需要在系统中创建心跳机制。其实TCP中已经为我们实现了一个叫做心跳的机制。如果你设置了心跳,那TCP就会在一定的时间(比如你设置的是3秒钟)内发送你设置的次数的心跳(比如说2次),并且此信息不会影响你自己定义的协议。也可以自己定义,所谓“心跳”就是定时发送一个自定义的结构体(心跳包或心跳帧),让对方知道自己“在线”,以确保链接的有效性。
实现:
服务端:
const net = require('net');let clientList = [];
const heartbeat = 'HEARTBEAT'; // 定义心跳包内容确保和平时发送的数据不会冲突const server = net.createServer();
server.on('connection', (client) => {console.log('客户端建立连接:', client.remoteAddress + ':' + client.remotePort);clientList.push(client);client.on('data', (chunk) => {let content = chunk.toString();if (content === heartbeat) {console.log('收到客户端发过来的一个心跳包');} else {console.log('收到客户端发过来的数据:', content);client.write('服务端的数据:' + content);}});client.on('end', () => {console.log('收到客户端end');clientList.splice(clientList.indexOf(client), 1);});client.on('error', () => {clientList.splice(clientList.indexOf(client), 1);})
});
server.listen(9000);
setInterval(broadcast, 10000); // 定时发送心跳包
function broadcast() {console.log('broadcast heartbeat', clientList.length);let cleanup = []for (let i=0;i<clientList.length;i+=1) {if (clientList[i].writable) { // 先检查 sockets 是否可写clientList[i].write(heartbeat);} else {console.log('一个无效的客户端');cleanup.push(clientList[i]); // 如果不可写,收集起来销毁。销毁之前要 Socket.destroy() 用 API 的方法销毁。clientList[i].destroy();}}//Remove dead Nodes out of write loop to avoid trashing loop indexfor (let i=0; i<cleanup.length; i+=1) {console.log('删除无效的客户端:', cleanup[i].name);clientList.splice(clientList.indexOf(cleanup[i]), 1);}
}
服务端输出结果:
客户端建立连接: ::ffff:127.0.0.1:57125
broadcast heartbeat 1
收到客户端发过来的数据: Thu, 29 Mar 2018 03:45:15 GMT
收到客户端发过来的一个心跳包
收到客户端发过来的数据: Thu, 29 Mar 2018 03:45:20 GMT
broadcast heartbeat 1
收到客户端发过来的数据: Thu, 29 Mar 2018 03:45:25 GMT
收到客户端发过来的一个心跳包
客户端建立连接: ::ffff:127.0.0.1:57129
收到客户端发过来的一个心跳包
收到客户端发过来的数据: Thu, 29 Mar 2018 03:46:00 GMT
收到客户端发过来的数据: Thu, 29 Mar 2018 03:46:04 GMT
broadcast heartbeat 2
收到客户端发过来的数据: Thu, 29 Mar 2018 03:46:05 GMT
收到客户端发过来的一个心跳包
客户端代码:
const net = require('net');const heartbeat = 'HEARTBEAT';
const client = new net.Socket();
client.connect(9000, '127.0.0.1', () => {});
client.on('data', (chunk) => {let content = chunk.toString();if (content === heartbeat) {console.log('收到心跳包:', content);} else {console.log('收到数据:', content);}
});// 定时发送数据
setInterval(() => {console.log('发送数据', new Date().toUTCString());client.write(new Date().toUTCString());
}, 5000);// 定时发送心跳包
setInterval(function () {client.write(heartbeat);
}, 10000);
客户端输出结果:
发送数据 Thu, 29 Mar 2018 03:46:04 GMT
收到数据: 服务端的数据:Thu, 29 Mar 2018 03:46:04 GMT
收到心跳包: HEARTBEAT
发送数据 Thu, 29 Mar 2018 03:46:09 GMT
收到数据: 服务端的数据:Thu, 29 Mar 2018 03:46:09 GMT
发送数据 Thu, 29 Mar 2018 03:46:14 GMT
收到数据: 服务端的数据:Thu, 29 Mar 2018 03:46:14 GMT
收到心跳包: HEARTBEAT
发送数据 Thu, 29 Mar 2018 03:46:19 GMT
收到数据: 服务端的数据:Thu, 29 Mar 2018 03:46:19 GMT
发送数据 Thu, 29 Mar 2018 03:46:24 GMT
收到数据: 服务端的数据:Thu, 29 Mar 2018 03:46:24 GMT
收到心跳包: HEARTBEAT
定义自己的协议
如果想要使传输的数据有意义,则必须使用到应用层协议比如Http、Mqtt、Dubbo等。基于TCP协议上自定义自己的应用层的协议需要解决的几个问题:
- 心跳包格式的定义及处理
- 报文头的定义,就是你发送数据的时候需要先发送报文头,报文里面能解析出你将要发送的数据长度
- 你发送数据包的格式,是json的还是其他序列化的方式
下面我们就一起来定义自己的协议,并编写服务的和客户端进行调用:
定义报文头格式: length:000000000xxxx; xxxx代表数据的长度,总长度20,举例子不严谨。
数据表的格式: Json
服务端:
const net = require('net');
const server = net.createServer();
let clientList = [];
const heartBeat = 'HeartBeat'; // 定义心跳包内容确保和平时发送的数据不会冲突
const getHeader = (num) => {return 'length:' + (Array(13).join(0) + num).slice(-13);
}
server.on('connection', (client) => {client.name = client.remoteAddress + ':' + client.remotePort// client.write('Hi ' + client.name + '!\n');console.log('客户端建立连接', client.name);clientList.push(client)let chunks = [];let length = 0;client.on('data', (chunk) => {let content = chunk.toString();console.log("content:", content, content.length);if (content === heartBeat) {console.log('收到客户端发过来的一个心跳包');} else {if (content.indexOf('length:') === 0){length = parseInt(content.substring(7,20));console.log('length', length);chunks =[chunk.slice(20, chunk.length)];} else {chunks.push(chunk);}let heap = Buffer.concat(chunks);console.log('heap.length', heap.length)if (heap.length >= length) {try {console.log('收到数据', JSON.parse(heap.toString()));let data = '服务端的数据数据:' + heap.toString();;let dataBuff = Buffer.from(JSON.stringify(data));let header = getHeader(dataBuff.length)client.write(header);client.write(dataBuff);} catch (err) {console.log('数据解析失败');}}}})client.on('end', () => {console.log('收到客户端end');clientList.splice(clientList.indexOf(client), 1);});client.on('error', () => {clientList.splice(clientList.indexOf(client), 1);})
});
server.listen(9000);
setInterval(broadcast, 10000); // 定时检查客户端 并发送心跳包
function broadcast() {console.log('broadcast heartbeat', clientList.length);let cleanup = []for(var i=0;i<clientList.length;i+=1) {if(clientList[i].writable) { // 先检查 sockets 是否可写// clientList[i].write(heartBeat); // 发送心跳数据} else {console.log('一个无效的客户端')cleanup.push(clientList[i]) // 如果不可写,收集起来销毁。销毁之前要 Socket.destroy() 用 API 的方法销毁。clientList[i].destroy();}}// 删除无效的客户端for(i=0; i<cleanup.length; i+=1) {console.log('删除无效的客户端:', cleanup[i].name);clientList.splice(clientList.indexOf(cleanup[i]), 1)}
}
日志打印:
客户端建立连接 ::ffff:127.0.0.1:50178content: length:0000000000031 20length 31heap.length 0content: "Tue, 03 Apr 2018 06:12:37 GMT" 31heap.length 31收到数据 Tue, 03 Apr 2018 06:12:37 GMTbroadcast heartbeat 1content: HeartBeat 9收到客户端发过来的一个心跳包content: length:0000000000031"Tue, 03 Apr 2018 06:12:42 GMT" 51length 31heap.length 31收到数据 Tue, 03 Apr 2018 06:12:42 GMT
客户端
const net = require('net');
const client = new net.Socket();
const heartBeat = 'HeartBeat'; // 定义心跳包内容确保和平时发送的数据不会冲突
const getHeader = (num) => {return 'length:' + (Array(13).join(0) + num).slice(-13);
}
client.connect(9000, '127.0.0.1', function () {});
let chunks = [];
let length = 0;
client.on('data', (chunk) => {let content = chunk.toString();console.log("content:", content, content.length);if (content === heartBeat) {console.log('收到服务端发过来的一个心跳包');} else {if (content.indexOf('length:') === 0){length = parseInt(content.substring(7,20));console.log('length', length);chunks =[chunk.slice(20, chunk.length)];} else {chunks.push(chunk);}let heap = Buffer.concat(chunks);console.log('heap.length', heap.length)if (heap.length >= length) {try {console.log('收到数据', JSON.parse(heap.toString()));} catch (err) {console.log('数据解析失败');}}}
});
// 定时发送数据
setInterval(function () {let data = new Date().toUTCString();let dataBuff = Buffer.from(JSON.stringify(data));let header =getHeader(dataBuff.length);client.write(header);client.write(dataBuff);
}, 5000);
// 定时发送心跳包
setInterval(function () {client.write(heartBeat);
}, 10000);
日志打印:
content: length:0000000000060 20length 60heap.length 0content: "服务端的数据数据:\"Tue, 03 Apr 2018 06:12:37 GMT\"" 44heap.length 60收到数据 服务端的数据数据:"Tue, 03 Apr 2018 06:12:37 GMT"content: length:0000000000060"服务端的数据数据:\"Tue, 03 Apr 2018 06:12:42 GMT\"" 64length 60heap.length 60收到数据 服务端的数据数据:"Tue, 03 Apr 2018 06:12:42 GMT"
客户端定时发送自定义协议数据到服务端,先发送头数据,在发送内容数据,另外一个定时器发送心跳数据,服务端判断是心跳数据,再判断是不是头数据,再是内容数据,然后解析后再发送数据给客户端。从日志的打印可以看出客户端先后writeheader和data数据,服务端可能在一个data事件里面接收到。
这里可以看到一个客户端在同一个时间内处理一个请求可以很好的工作,但是想象这么一个场景,如果同一时间内让同一个客户端去多次调用服务端请求,发送多次头数据和内容数据,服务端的data事件收到的数据就很难区别哪些数据是哪次请求的,比如两次头数据同时到达服务端,服务端就会忽略其中一次,而后面的内容数据也不一定就对应于这个头的。所以想复用长连接并能很好的高并发处理服务端请求,就需要连接池这种方式了。
Socket连接池
什么是Socket连接池,池的概念可以联想到是一种资源的集合,所以Socket连接池,就是维护着一定数量Socket长连接的集合。它能自动检测Socket长连接的有效性,剔除无效的连接,补充连接池的长连接的数量。从代码层次上其实是人为实现这种功能的类,一般一个连接池包含下面几个属性:
- 空闲可使用的长连接队列
- 正在运行的通信的长连接队列
- 等待去获取一个空闲长连接的请求的队列
- 无效长连接的剔除功能
- 长连接资源池的数量配置
- 长连接资源的新建功能
场景: 一个请求过来,首先去资源池要求获取一个长连接资源,如果空闲队列里面有长连接,就获取到这个长连接Socket,并把这个Socket移到正在运行的长连接队列。如果空闲队列里面没有,且正在运行的队列长度小于配置的连接池资源的数量,就新建一个长连接到正在运行的队列去,如果正在运行的不下于配置的资源池长度,则这个请求进入到等待队列去。当一个正在运行的Socket完成了请求,就从正在运行的队列移到空闲的队列,并触发等待请求队列去获取空闲资源,如果有等待的情况。
这里简单介绍Nodejs的Socket连接池generic-pool模块的源码。
主要文件目录结构
.
|————lib ------------------------- 代码库
| |————DefaultEvictor.js ----------
| |————Deferred.js ----------------
| |————Deque.js -------------------
| |————DequeIterator.js -----------
| |————DoublyLinkedList.js --------
| |————DoublyLinkedListIterator.js-
| |————factoryValidator.js --------
| |————Pool.js -------------------- 连接池主要代码
| |————PoolDefaults.js ------------
| |————PooledResource.js ----------
| |————Queue.js ------------------- 队列
| |————ResourceLoan.js ------------
| |————ResourceRequest.js ---------
| |————utils.js ------------------- 工具
|————test ------------------------- 测试目录
|————README.md ------------------- 项目描述文件
|————.eslintrc ------------------- eslint静态检查配置文件
|————.eslintignore --------------- eslint静态检查忽略的文件
|————package.json ----------------- npm包依赖配置
下面介绍库的使用:
初始化连接池
'use strict';
const net = require('net');
const genericPool = require('generic-pool');function createPool(conifg) {let options = Object.assign({fifo: true, // 是否优先使用老的资源priorityRange: 1, // 优先级testOnBorrow: true, // 是否开启获取验证// acquireTimeoutMillis: 10 * 1000, // 获取的超时时间autostart: true, // 自动初始化和释放调度启用min: 10, // 初始化连接池保持的长连接最小数量max: 0, // 最大连接池保持的长连接数量evictionRunIntervalMillis: 0, // 资源释放检验间隔检查 设置了下面几个参数才起效果numTestsPerEvictionRun: 3, // 每次释放资源数量softIdleTimeoutMillis: -1, // 可用的超过了最小的min 且空闲时间时间 达到释放idleTimeoutMillis: 30000 // 强制释放// maxWaitingClients: 50 // 最大等待}, conifg.options);const factory = {create: function () {return new Promise((resolve, reject) => {let socket = new net.Socket();socket.setKeepAlive(true);socket.connect(conifg.port, conifg.host);// TODO 心跳包的处理逻辑socket.on('connect', () => {console.log('socket_pool', conifg.host, conifg.port, 'connect' );resolve(socket);});socket.on('close', (err) => { // 先end 事件再close事件console.log('socket_pool', conifg.host, conifg.port, 'close', err);});socket.on('error', (err) => {console.log('socket_pool', conifg.host, conifg.port, 'error', err);reject(err);});});},//销毁连接destroy: function (socket) {return new Promise((resolve) => {socket.destroy(); // 不会触发end 事件 第一次会触发发close事件 如果有message会触发error事件resolve();});},validate: function (socket) { //获取资源池校验资源有效性return new Promise((resolve) => {// console.log('socket.destroyed:', socket.destroyed, 'socket.readable:', socket.readable, 'socket.writable:', socket.writable);if (socket.destroyed || !socket.readable || !socket.writable) {return resolve(false);} else {return resolve(true);}});}};const pool = genericPool.createPool(factory, options);pool.on('factoryCreateError', (err) => { // 监听新建长连接出错 让请求直接返回错误const clientResourceRequest = pool._waitingClientsQueue.dequeue();if (clientResourceRequest) {clientResourceRequest.reject(err);}});return pool;
};let pool = createPool({port: 9000,host: '127.0.0.1',options: {min: 0, max: 10}
});
使用连接池
下面连接池的使用,使用的协议是我们之前自定义的协议。
let pool = createPool({port: 9000,host: '127.0.0.1',options: {min: 0, max: 10}
});
const getHeader = (num) => {return 'length:' + (Array(13).join(0) + num).slice(-13);
}
const request = async (requestDataBuff) => {let client;try {client = await pool.acquire();} catch (e) {console.log('acquire socket client failed: ', e);throw e;}let timeout = 10000;return new Promise((resolve, reject) => {let chunks = [];let length = 0;client.setTimeout(timeout);client.removeAllListeners('error');client.on('error', (err) => {client.removeAllListeners('error');client.removeAllListeners('data');client.removeAllListeners('timeout');pool.destroyed(client);reject(err);});client.on('timeout', () => {client.removeAllListeners('error');client.removeAllListeners('data');client.removeAllListeners('timeout');// 应该销毁以防下一个req的data事件监听才返回数据pool.destroy(client);// pool.release(client);reject(`socket connect timeout set ${timeout}`);});let header = getHeader(requestDataBuff.length);client.write(header);client.write(requestDataBuff);client.on('data', (chunk) => {let content = chunk.toString();console.log('content', content, content.length);// TODO 过滤心跳包if (content.indexOf('length:') === 0){length = parseInt(content.substring(7,20));console.log('length', length);chunks =[chunk.slice(20, chunk.length)];} else {chunks.push(chunk);}let heap = Buffer.concat(chunks);console.log('heap.length', heap.length);if (heap.length >= length) {pool.release(client);client.removeAllListeners('error');client.removeAllListeners('data');client.removeAllListeners('timeout');try {// console.log('收到数据', JSON.parse(heap.toString()));resolve(JSON.parse(heap.toString()));} catch (err) {reject(err);console.log('数据解析失败');}}});});
}
request(Buffer.from(JSON.stringify({a: 'a'}))).then((data) => {console.log('收到服务的数据',data)}).catch(err => {console.log(err);});request(Buffer.from(JSON.stringify({b: 'b'}))).then((data) => {console.log('收到服务的数据',data)}).catch(err => {console.log(err);});setTimeout(function () { //查看是否会复用Socket 有没有建立新的连接request(Buffer.from(JSON.stringify({c: 'c'}))).then((data) => {console.log('收到服务的数据',data)}).catch(err => {console.log(err);});request(Buffer.from(JSON.stringify({d: 'd'}))).then((data) => {console.log('收到服务的数据',data)}).catch(err => {console.log(err);});
}, 1000)
日志打印:
socket_pool 127.0.0.1 9000 connectsocket_pool 127.0.0.1 9000 connectcontent length:0000000000040"服务端的数据数据:{\"a\":\"a\"}" 44length 40heap.length 40收到服务的数据 服务端的数据数据:{"a":"a"}content length:0000000000040"服务端的数据数据:{\"b\":\"b\"}" 44length 40heap.length 40收到服务的数据 服务端的数据数据:{"b":"b"}content length:0000000000040 20length 40heap.length 0content "服务端的数据数据:{\"c\":\"c\"}" 24heap.length 40收到服务的数据 服务端的数据数据:{"c":"c"}content length:0000000000040"服务端的数据数据:{\"d\":\"d\"}" 44length 40heap.length 40收到服务的数据 服务端的数据数据:{"d":"d"}
这里看到前面两个请求都建立了新的Socket连接 socket_pool 127.0.0.1 9000 connect,定时器结束后重新发起两个请求就没有建立新的Socket连接了,直接从连接池里面获取Socket连接资源。
源码分析
发现主要的代码就位于lib文件夹中的Pool.js
构造函数:
lib/Pool.js
/*** Generate an Object pool with a specified `factory` and `config`.** @param {typeof DefaultEvictor} Evictor* @param {typeof Deque} Deque* @param {typeof PriorityQueue} PriorityQueue* @param {Object} factory* Factory to be used for generating and destroying the items.* @param {Function} factory.create* Should create the item to be acquired,* and call it's first callback argument with the generated item as it's argument.* @param {Function} factory.destroy* Should gently close any resources that the item is using.* Called before the items is destroyed.* @param {Function} factory.validate* Test if a resource is still valid .Should return a promise that resolves to a boolean, true if resource is still valid and false* If it should be removed from pool.* @param {Object} options*/constructor(Evictor, Deque, PriorityQueue, factory, options) {super();factoryValidator(factory); // 检验我们定义的factory的有效性包含create destroy validatethis._config = new PoolOptions(options); // 连接池配置// TODO: fix up this ugly glue-ingthis._Promise = this._config.Promise;this._factory = factory;this._draining = false;this._started = false;/*** Holds waiting clients* @type {PriorityQueue}*/this._waitingClientsQueue = new PriorityQueue(this._config.priorityRange); // 请求的对象管管理队列queue 初始化queue的size 1 { _size: 1, _slots: [ Queue { _list: [Object] } ] }/*** Collection of promises for resource creation calls made by the pool to factory.create* @type {Set}*/this._factoryCreateOperations = new Set(); // 正在创建的长连接/*** Collection of promises for resource destruction calls made by the pool to factory.destroy* @type {Set}*/this._factoryDestroyOperations = new Set(); // 正在销毁的长连接/*** A queue/stack of pooledResources awaiting acquisition* TODO: replace with LinkedList backed array* @type {Deque}*/this._availableObjects = new Deque(); // 空闲的资源长连接/*** Collection of references for any resource that are undergoing validation before being acquired* @type {Set}*/this._testOnBorrowResources = new Set(); // 正在检验有效性的资源/*** Collection of references for any resource that are undergoing validation before being returned* @type {Set}*/this._testOnReturnResources = new Set();/*** Collection of promises for any validations currently in process* @type {Set}*/this._validationOperations = new Set();// 正在校验的中间temp/*** All objects associated with this pool in any state (except destroyed)* @type {Set}*/this._allObjects = new Set(); // 所有的链接资源 是一个 PooledResource对象/*** Loans keyed by the borrowed resource* @type {Map}*/this._resourceLoans = new Map(); // 被借用的对象的map release的时候用到/*** Infinitely looping iterator over available object* @type {DequeIterator}*/this._evictionIterator = this._availableObjects.iterator(); // 一个迭代器this._evictor = new Evictor();/*** handle for setTimeout for next eviction run* @type {(number|null)}*/this._scheduledEviction = null;// create initial resources (if factory.min > 0)if (this._config.autostart === true) { // 初始化最小的连接数量this.start();}}
可以看到包含之前说的空闲的资源队列,正在请求的资源队列,正在等待的请求队列等。
下面查看 Pool.acquire 方法
lib/Pool.js
/*** Request a new resource. The callback will be called,* when a new resource is available, passing the resource to the callback.* TODO: should we add a seperate "acquireWithPriority" function** @param {Number} [priority=0]* Optional. Integer between 0 and (priorityRange - 1). Specifies the priority* of the caller if there are no available resources. Lower numbers mean higher* priority.** @returns {Promise}*/acquire(priority) { // 空闲资源队列资源是有优先等级的 if (this._started === false && this._config.autostart === false) {this.start(); // 会在this._allObjects 添加min的连接对象数}if (this._draining) { // 如果是在资源释放阶段就不能再请求资源了return this._Promise.reject(new Error("pool is draining and cannot accept work"));}// 如果要设置了等待队列的长度且要等待 如果超过了就返回资源不可获取// TODO: should we defer this check till after this event loop incase "the situation" changes in the meantimeif (this._config.maxWaitingClients !== undefined &&this._waitingClientsQueue.length >= this._config.maxWaitingClients) {return this._Promise.reject(new Error("max waitingClients count exceeded"));}const resourceRequest = new ResourceRequest(this._config.acquireTimeoutMillis, // 对象里面的超时配置 表示等待时间 会启动一个定时 超时了就触发resourceRequest.promise 的reject触发this._Promise);// console.log(resourceRequest)this._waitingClientsQueue.enqueue(resourceRequest, priority); // 请求进入等待请求队列this._dispense(); // 进行资源分发 最终会触发resourceRequest.promise的resolve(client) return resourceRequest.promise; // 返回的是一个promise对象resolve却是在其他地方触发}
/*** Attempt to resolve an outstanding resource request using an available resource from* the pool, or creating new ones** @private*/_dispense() {/*** Local variables for ease of reading/writing* these don't (shouldn't) change across the execution of this fn*/const numWaitingClients = this._waitingClientsQueue.length; // 正在等待的请求的队列长度 各个优先级的总和console.log('numWaitingClients', numWaitingClients) // 1// If there aren't any waiting requests then there is nothing to do// so lets short-circuitif (numWaitingClients < 1) {return;}// max: 10, min: 4console.log('_potentiallyAllocableResourceCount', this._potentiallyAllocableResourceCount) // 目前潜在空闲可用的连接数量const resourceShortfall =numWaitingClients - this._potentiallyAllocableResourceCount; // 还差几个可用的 小于零表示不需要 大于0表示需要新建长连接的数量console.log('spareResourceCapacity', this.spareResourceCapacity) // 距离max数量的还有几个没有创建const actualNumberOfResourcesToCreate = Math.min(this.spareResourceCapacity, // -6resourceShortfall // 这个是 -3); // 如果resourceShortfall>0 表示需要新建但是这新建的数量不能超过spareResourceCapacity最多可创建的console.log('actualNumberOfResourcesToCreate', actualNumberOfResourcesToCreate) // 如果actualNumberOfResourcesToCreate >0 表示需要创建连接for (let i = 0; actualNumberOfResourcesToCreate > i; i++) {this._createResource(); // 新增新的长连接}// If we are doing test-on-borrow see how many more resources need to be moved into test// to help satisfy waitingClientsif (this._config.testOnBorrow === true) { // 如果开启了使用前校验资源的有效性// how many available resources do we need to shift into testconst desiredNumberOfResourcesToMoveIntoTest =numWaitingClients - this._testOnBorrowResources.size;// 1const actualNumberOfResourcesToMoveIntoTest = Math.min(this._availableObjects.length, // 3desiredNumberOfResourcesToMoveIntoTest // 1);for (let i = 0; actualNumberOfResourcesToMoveIntoTest > i; i++) { // 需要有效性校验的数量 至少满足最小的waiting clinetthis._testOnBorrow(); // 资源有效校验后再分发}}// if we aren't testing-on-borrow then lets try to allocate what we canif (this._config.testOnBorrow === false) { // 如果没有开启有效性校验 就开启有效资源的分发const actualNumberOfResourcesToDispatch = Math.min(this._availableObjects.length,numWaitingClients);for (let i = 0; actualNumberOfResourcesToDispatch > i; i++) { // 开始分发资源this._dispatchResource();}}}
/*** Attempt to move an available resource to a waiting client* @return {Boolean} [description]*/_dispatchResource() {if (this._availableObjects.length < 1) {return false;}const pooledResource = this._availableObjects.shift(); // 从可以资源池里面取出一个this._dispatchPooledResourceToNextWaitingClient(pooledResource); // 分发return false;}/*** Dispatches a pooledResource to the next waiting client (if any) else* puts the PooledResource back on the available list* @param {PooledResource} pooledResource [description]* @return {Boolean} [description]*/_dispatchPooledResourceToNextWaitingClient(pooledResource) {const clientResourceRequest = this._waitingClientsQueue.dequeue(); // 可能是undefined 取出一个等待的queneconsole.log('clientResourceRequest.state', clientResourceRequest.state);if (clientResourceRequest === undefined ||clientResourceRequest.state !== Deferred.PENDING) {console.log('没有等待的')// While we were away either all the waiting clients timed out// or were somehow fulfilled. put our pooledResource back.this._addPooledResourceToAvailableObjects(pooledResource); // 在可用的资源里面添加一个// TODO: do need to trigger anything before we leave?return false;}// TODO clientResourceRequest 的state是否需要判断 如果已经是resolve的状态 已经超时回去了 这个是否有问题const loan = new ResourceLoan(pooledResource, this._Promise); this._resourceLoans.set(pooledResource.obj, loan); // _resourceLoans 是个map k=>value pooledResource.obj 就是socket本身pooledResource.allocate(); // 标识资源的状态是正在被使用clientResourceRequest.resolve(pooledResource.obj); // acquire方法返回的promise对象的resolve在这里执行的return true;}
上面的代码就按种情况一直走下到最终获取到长连接的资源,其他更多代码大家可以自己去深入了解。
相关文章:
一篇搞懂tcp,http,socket,socket连接池之间的关系
前言 作为一名开发人员我们经常会听到HTTP协议、TCP/IP协议、UDP协议、Socket、Socket长连接、Socket连接池等字眼,然而它们之间的关系、区别及原理并不是所有人都能理解清楚,这篇文章就从网络协议基础开始到Socket连接池,一步一步解释他们之…...
【JavaSE】对象的比较
哈喽,大家好!我是保护小周ღ,本期为大家带来的是Java中自定义类型(对象)的三种比较方式,equals 方法, Comparable 泛型接口, Comparator 泛型接口 。在日常编程中,我们常常会需要比较的问题&…...
Leetcode DAY 49~50:买卖股票的最佳时机 1 2 3 4
121. 买卖股票的最佳时机 1、贪心算法 class Solution { public:int maxProfit(vector<int>& prices) {//贪心int low INT_MAX;int res 0;for(int i 0; i < prices.size(); i) {low min(low, prices[i]); //左最小价格res max(res, prices[i] - low); //当前…...
Android Handler机制(二) Handler 实现原理
一. 前言 接上一篇文章为什么设计Handler , 我们来继续讲解一下Handler的实现原理, 俗话说一个好汉三个帮, 接下来一步一步引入各个主角,并说明它们在Handler机制中扮演的角色和作用. 二. Handler实现原理 首先我们先确定一个结论: 使用 Handler 是希望它被实例化在哪个线程&a…...
Elasticsearch教程(19) 详解mapping之keyword
Elasticsearch已升级,新版Elasticsearch keyword博客参考下面这篇【Elasticsearch教程8】Mapping字段类型之keyword_elasticsearch的keyword_亚瑟弹琴的博客-CSDN博客 1 前言 本文基于ES7.6,如果是之前版本,是有区别的。 ES支持的字段类型很…...
LeetCode算法复杂度分析(时间复杂度空间复杂度)
文章目录前言时间复杂度1.概述2.大O记法3.常见类型空间复杂度1.概述2.常见类型典型算法的复杂度分析1.递归算法2.哈希表前言 我们知道,研究算法的最终目的就是如何花更少的时间,如何占用更少的内存去完成相同的需求。 时间复杂度 1.概述 我们要计算算…...
Android OpenCV(七十三):吊打高斯模糊的StackBlur Android 实践
前言 OpenCV 4.7.0 2022年12月28日Release,ChangeLog中提到 Stackblur algorithm implementation. Stackblur是一种高斯模糊的快速近似,由Mario Klingemann发明。其计算耗时不会随着kernel size的增大而增加,专为大kernel size的模糊滤波场景量身定制。 使用建议:当kerne…...
4.排序算法之一:冒泡排序
排序算法稳定性假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]r[j],且r[i]在r[j]之前,而在排序后的序列中,r[…...
python自学之《21天学通Python》(16)——第19章 用Pillow库处理图片
Pillow是Python2.X时代比较流行的Python ImagingLibrary(简称Pillow)图像处理库的分支,并修复了一些bug。Pillow提供了对Python3的支持,为Python3解释器提供了图像处理的功能。和Pillow库一样提供了广泛的文件格式支持、高效的内部…...
发布依赖到maven仓库
maven中央仓库是一个开放的仓库,所以我们也可以把自己开发的jar推送到远程仓库,这样可以直接引入pom依赖使用我们的库。 准备工作 ● 需要一个github账号(程序员必备) ● 网络代理(涉及到的网站通常没版本在国内直接访…...
Laravel-admin之自定义操作日志
laravel-admin是封装性极好的框架,自带的就有操作日志的记录,但是对于非开发人员可能看不懂这个日志,所以就想着给修改一下,以谁修改了什么,谁删除了什么,谁审核了什么,谁添加了什么类似&#x…...
用Python做了一个法律查询小工具,非常好用
用Python做了一个法律查询小工具,非常好用效果展示准备工作不会的话可以点我直达代码和视频讲解,我都准备好了主要代码哈喽兄弟,今天给大家分享一个Python tkinter制作法律查询小工具。 光爬虫大家也只能自己用用,就算打包了exe&…...
工作篇:触摸屏原理介绍
一、触摸屏概述 触摸屏作为一种新的输入设备,它是目前最简单、方便、自然的一种人机交互方式。 当接触了屏幕上的图形按钮时,屏幕上的触觉反馈系统可根据预先编程的程式驱动各种连结装置,可用以取代机械式的按钮面板,并借由液晶…...
Ep_操作系统面试题-操作系统的分类
答案 单体系统 整个操作系统是以程序集合来编写的,链接在一块形成一个二进制可执行程序,这种系统称为单体系统。 分层系统 每一层都使用下面的层来执行其功能。 微内核 微内核架构的内核只保留最基本的能力,把一些应用放到了用户空间 客户-…...
iframe或document监听滚动事件不起作用
有时候我们会遇到监听iframe或document的滚动事件不起作用的情况,在排除代码写错的情况下,我们应该考虑此时的document是否可以滑动。 1、为什么document不能监听滑动? 就很奇怪,明明页面时有滚动条的,为什么说document不可滑动…...
基频估计算法简介
基频估计算法 F0 estimate methods 估计F0的方法可以分为三类:基于时域、基于频域、或混合方法。本文详细介绍了这些方法。 所有的算法都包含如下三个主要步骤: 1.预处理:滤波,加窗分帧等 2.搜寻:可能的基频值F0(候选…...
linux修改DNS 系统版本Kylin V10桌面版
配置DNS在银河麒麟桌面操作系统V10 SP1 中修改DNS信息,直接修改/etc/resolv.conf文件中的DNS信息,不能生效。应该参考如下步骤:一、首先修改 /etc/systemd/resolved.conf文件,在其中添加DNS信息在终端中执行以下命令:s…...
如何使用 AWS Lambda 运行 selenium
借助 AWS Lambda 运行 selenium 来爬取网络数据。 简介 与手动从网站收集数据相比,爬虫可以为我们节省很多时间,对于爬虫的每次请求而言,这相当于 AWS Lambda 的每次函数的运行。 AWS Lambda 是一种将脚本部署到云的简单且价格低廉的服务&…...
认识Cesium旋转大小变量
前文代码中有如下;矩阵乘以旋转大小,还放入mat; Cesium.Matrix4.multiply(mat, rotationX, mat); 初看以为rotationX是一个数值,因为矩阵可以和数相乘; 但是看它的代码,rotationX是由一长串代码获得的&a…...
异响加持、吐槽声不断,小鹏G9难解困局
小鹏汽车的烦恼就好比红尘中的三千青丝,小鹏G9“惊魂48小时”的恐慌还未平息,车门异响等问题就已经层出不穷,再次将小鹏汽车推上风口浪尖。 可以毫不客气的说,G9承载着小鹏汽车盈利的希望,但在原本处于上升之势的G9却…...
告别系统依赖!手把手教你为Unity Linux版程序自制一个搜狗风格的中文输入框
告别系统依赖!手把手教你为Unity Linux版程序自制一个搜狗风格的中文输入框 在跨平台开发中,中文输入一直是Unity开发者的痛点之一。特别是当你的应用需要在Linux系统上运行时,系统自带的输入法往往表现不稳定,甚至完全无法使用。…...
2026届最火的十大降重复率神器实测分析
Ai论文网站排名(开题报告、文献综述、降aigc率、降重综合对比) TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 维普身为国内具有权威性的学术平台,已然正式推出了AIGC检测服务,其目…...
SeqGPT-560M企业知识图谱构建:从非结构化文本中抽取实体关系三元组
SeqGPT-560M企业知识图谱构建:从非结构化文本中抽取实体关系三元组 1. 项目概述 SeqGPT-560M是一个专门为企业知识图谱构建设计的智能信息抽取系统。与通用的聊天对话模型不同,这个系统专注于一件事:从各种非结构化文本中精准提取实体和关系…...
AI写论文别担心!4款AI论文写作利器,轻松应对论文创作挑战
你是不是也在为撰写期刊论文、毕业论文或职称论文而感到无从下手呢?在写论文时,面对浩如烟海的文献资料,仿佛在大海中寻找针,繁杂的格式要求更是让人无从着手,反复的修改不断消耗着你的耐心,写作效率低下令…...
XXL-Job适配PostgreSQL踩坑实录:版本差异、SQL改写与MyBatis Mapper的那些坑
XXL-Job适配PostgreSQL实战:从版本差异到SQL优化的完整指南 在分布式任务调度领域,XXL-Job凭借其轻量级设计和易用性赢得了众多开发者的青睐。然而当我们需要将其默认的MySQL存储切换到PostgreSQL时,会遇到一系列意料之外的挑战。本文将分享我…...
首创证券冲刺港股:年营收36亿 期内利润4.9亿 已获IPO备案
雷递网 雷建平 4月19日首创证券股份有限公司(简称:“首创证券”)日前更新招股书,准备在港交所上市。首创证券已获IPO备案,拿到了上市的钥匙。2026年4月17日,首创证券股份有限公司、深圳市星源材质科技股份有…...
202106-nuPlan:面向自动驾驶汽车的闭环机器学习规划基准
第001/5页(英文原文) nuPlan: A closed-loop ML-based planning benchmark for autonomous vehicles Holger Caesar Juraj Kabzan Kok Seang Tan Whye Kit Fong Eric Wolff Alex Lang Luke Fletcher Oscar Beijbom Sammy Omari Motional Abstract In this work, we propos…...
StructBERT语义分析平台:快速搭建中文复述识别系统
StructBERT语义分析平台:快速搭建中文复述识别系统 1. 平台概述与核心价值 中文语义相似度计算是自然语言处理中的基础任务,广泛应用于智能客服、文本查重、问答系统等场景。StructBERT作为阿里巴巴开源的预训练语言模型,在中文语义理解任务…...
《信息系统项目管理师教程(第4版)》——项目管理概述知识要点
在《信息系统项目管理师教程(第4版)》中,**项目管理概述(第六章)**是整个高项考试的“地基”。这一章的内容虽然不直接主导下午的案例分析,但它是理解所有十大知识领域的“底层逻辑”,在上午的综…...
别再手动点鼠标了!用这个Praat脚本批量提取音频时长和F1F2共振峰(附Excel作图教程)
语音数据分析自动化:用Praat脚本高效提取时长与共振峰 每次打开Praat软件,面对几十个甚至上百个音频文件时,你是否感到手指发酸?那些重复点击"Analyse"→"Formant"→"To Formant"的操作,…...
