nodejs源码分析之http Agent

Agent对TCP连接进行了池化管理。简单的情况下,客户端发送一个HTTP请求之前,首先建立一个TCP连接,收到响应后会立刻关闭TCP连接。但是我们知道TCP的三次握手是比较耗时的。所以如果我们能复用TCP连接,在一个TCP连接上发送多个HTTP请求和接收多个HTTP响应,那么在性能上面就会得到很大的提升。Agent的作用就是复用TCP连接。不过Agent的模式是在一个TCP连接上串行地发送请求和接收响应,不支持HTTP PipeLine模式。下面我们看一下Agent模块的具体实现。看它是如何实现TCP连接复用的。

1.	function Agent(options) {  
2.	  if (!(this instanceof Agent))  
3.	    return new Agent(options);  
4.	  EventEmitter.call(this);  
5.	  this.defaultPort = 80;  
6.	  this.protocol = 'http:';  
7.	  this.options = { ...options };  
8.	  // path字段表示是本机的进程间通信时使用的路径,比如Unix域路径  
9.	  this.options.path = null;  
10.	  // socket个数达到阈值后,等待空闲socket的请求  
11.	  this.requests = {};  
12.	  // 正在使用的socket  
13.	  this.sockets = {};  
14.	  // 空闲socket  
15.	  this.freeSockets = {};  
16.	  // 空闲socket的存活时间  
17.	  this.keepAliveMsecs = this.options.keepAliveMsecs || 1000;  
18.	  /* 
19.	    用完的socket是否放到空闲队列, 
20.	      开启keepalive才会放到空闲队列, 
21.	      不开启keepalive 
22.	        还有等待socket的请求则复用socket 
23.	        没有等待socket的请求则直接销毁socket 
24.	  */  
25.	  this.keepAlive = this.options.keepAlive || false;  
26.	  // 最大的socket个数,包括正在使用的和空闲的socket  
27.	  this.maxSockets = this.options.maxSockets 
28.	                      || Agent.defaultMaxSockets;  
29.	  // 最大的空闲socket个数  
30.	  this.maxFreeSockets = this.options.maxFreeSockets || 256;  
31.	}  

Agent维护了几个数据结构,分别是等待socket的请求、正在使用的socket、空闲socket。每一个数据结构是一个对象,对象的key是根据HTTP请求参数计算的。对象的值是一个队列。具体结构如图所示。

下面我们看一下Agent模块的具体实现。

1 key的计算

key的计算是池化管理的核心。正确地设计key的计算规则,才能更好地利用池化带来的好处。

1.	// 一个请求对应的key  
2.	Agent.prototype.getName = function getName(options) {  
3.	  let name = options.host || 'localhost'; 
4.	  name += ':';  
5.	  if (options.port)  
6.	    name += options.port;  
7.	  name += ':';  
8.	  if (options.localAddress)  
9.	    name += options.localAddress;  
10.	  if (options.family === 4 || options.family === 6)  
11.	    name += `:${options.family}`;  
12.	  if (options.socketPath)  
13.	    name += `:${options.socketPath}`; 
14.	  return name;  
15.	};  

我们看到key由host、port、本地地址、地址簇类型、unix路径计算而来。所以不同的请求只有这些因子都一样的情况下才能复用连接。另外我们看到Agent支持Unix域。

2 创建一个socket

1.	function createSocket(req, options, cb) {  
2.	  options = { ...options, ...this.options };  
3.	  // 计算key
4.	  const name = this.getName(options);  
5.	  options._agentKey = name;  
6.	  options.encoding = null;  
7.	  let called = false;  
8.	  // 创建socket完毕后执行的回调
9.	  const oncreate = (err, s) => {  
10.	    if (called)  
11.	      return;  
12.	    called = true;  
13.	    if (err)  
14.	      return cb(err);  
15.	    if (!this.sockets[name]) {  
16.	      this.sockets[name] = [];  
17.	    }  
18.	    // 插入正在使用的socket队列  
19.	    this.sockets[name].push(s); 
20.	     // 监听socket的一些事件,用于回收socket 
21.	    installListeners(this, s, options); 
22.	    // 有可用socket,通知调用方 
23.	    cb(null, s);  
24.	  };  
25.	  // 创建一个新的socket,使用net.createConnection  
26.	  const newSocket = this.createConnection(options, oncreate);  
27.	  if (newSocket)  
28.	    oncreate(null, newSocket);  
29.	}  
30.	  
31.	function installListeners(agent, s, options) {  
32.	  /*
33.	    socket触发空闲事件的处理函数,告诉agent该socket空闲了,
34.	    agent会回收该socket到空闲队列  
35.	  */
36.	  function onFree() {  
37.	    agent.emit('free', s, options);  
38.	  }  
39.	  /* 
40.	    监听socket空闲事件,调用方使用完socket后触发,
41.	    通知agent socket用完了 
42.	  */ 
43.	  s.on('free', onFree);  
44.	  
45.	  function onClose(err) {  
46.	    agent.removeSocket(s, options);  
47.	  }  
48.	  // socket关闭则agent会从socket队列中删除它  
49.	  s.on('close', onClose);  
50.	  
51.	  function onRemove() {  
52.	    agent.removeSocket(s, options);  
53.	    s.removeListener('close', onClose);  
54.	    s.removeListener('free', onFree);  
55.	    s.removeListener('agentRemove', onRemove);  
56.	  }  
57.	  // agent被移除  
58.	  s.on('agentRemove', onRemove);  
59.	  
60.	}  

创建socket的主要逻辑如下
1 调用net模块创建一个socket(TCP或者Unix域),然后插入使用中的socket队列,最后通知调用方socket创建成功。
2 监听socket的close、free事件和agentRemove事件,触发时从队列中删除socket。

3 删除socket

1.	// 把socket从正在使用队列或者空闲队列中移出  
2.	function removeSocket(s, options) {  
3.	  const name = this.getName(options);  
4.	  const sets = [this.sockets];  
5.	  /*
6.	    socket不可写了,则有可能是存在空闲的队列中,
7.	    所以需要遍历空闲队列,因为removeSocket只会在
8.	    使用完socket或者socket关闭的时候被调用,前者只有在
9.	    可写状态时会调用,后者是不可写的
10.	  */
11.	  if (!s.writable)  
12.	    sets.push(this.freeSockets);  
13.	  // 从队列中删除对应的socket  
14.	  for (const sockets of sets) {  
15.	    if (sockets[name]) {  
16.	      const index = sockets[name].indexOf(s);  
17.	      if (index !== -1) {  
18.	        sockets[name].splice(index, 1);  
19.	        // Don't leak  
20.	        if (sockets[name].length === 0)  
21.	          delete sockets[name];  
22.	      }  
23.	    }  
24.	  }  
25.	  /* 
26.	    如果还有在等待socekt的请求,则创建socket去处理它, 
27.	    因为socket数已经减一了,说明socket个数还没有达到阈值
28.	    但是这里应该先判断是否还有空闲的socket,有则可以复用,
29.	    没有则创建新的socket 
30.	  */  
31.	  if (this.requests[name] && this.requests[name].length) {  
32.	    const req = this.requests[name][0];  
33.	    const socketCreationHandler = handleSocketCreation(this, 
34.	                                                            req,            
35.	                                                            false);  
36.	    this.createSocket(req, options, socketCreationHandler);  
37.	  }  
38.	};  

前面已经分析过,Agent维护了两个socket队列,删除socket就是从这两个队列中找到对应的socket,然后移除它。移除后需要判断一下是否还有等待socket的请求队列,有的话就新建一个socket去处理它。因为移除了一个socket,就说明可以新增一个socket。

4 设置socket keepalive

当socket被使用完并且被插入空闲队列后,需要重新设置socket的keepalive值。等到超时会自动关闭socket。在一个socket上调用一次setKeepAlive就可以了,这里可能会导致多次调用setKeepAlive,不过也没有影响。

1.	function keepSocketAlive(socket) {  
2.	  socket.setKeepAlive(true, this.keepAliveMsecs);  
3.	  socket.unref();  
4.	  return true;  
5.	};  

另外需要设置ref标记,防止该socket阻止事件循环的退出,因为该socket是空闲的,不应该影响事件循环的退出。

5 复用socket

1.	function reuseSocket(socket, req) {  
2.	  req.reusedSocket = true;  
3.	  socket.ref();  
4.	};  

重新使用该socket,需要修改ref标记,阻止事件循环退出,并标记请求使用的是复用socket。

6 销毁Agent

1.	function destroy() {  
2.	  for (const set of [this.freeSockets, this.sockets]) {  
3.	    for (const key of ObjectKeys(set)) {  
4.	      for (const setName of set[key]) {  
5.	        setName.destroy();  
6.	      }  
7.	    }  
8.	  }  
9.	};  

因为Agent本质上是一个socket池,销毁Agent即销毁池里维护的所有socket。

7 使用连接池

我们看一下如何使用Agent。

1.	function addRequest(req, options, port, localAddress) {  
2.	  // 参数处理  
3.	  if (typeof options === 'string') {  
4.	    options = {  
5.	      host: options,  
6.	      port,  
7.	      localAddress  
8.	    };  
9.	  }  
10.	  
11.	  options = { ...options, ...this.options };  
12.	  if (options.socketPath)  
13.	    options.path = options.socketPath;  
14.	  
15.	  if (!options.servername && options.servername !== '')  
16.	    options.servername = calculateServerName(options, req);  
17.	  // 拿到请求对应的key  
18.	  const name = this.getName(options);  
19.	  // 该key还没有在使用的socekt则初始化数据结构  
20.	  if (!this.sockets[name]) {  
21.	    this.sockets[name] = [];  
22.	  }  
23.	  // 该key对应的空闲socket列表  
24.	  const freeLen = this.freeSockets[name] ? 
25.	                    this.freeSockets[name].length : 0;  
26.	  // 该key对应的所有socket个数  
27.	  const sockLen = freeLen + this.sockets[name].length;  
28.	  // 该key有对应的空闲socekt  
29.	  if (freeLen) {    
30.	    // 获取一个该key对应的空闲socket  
31.	    const socket = this.freeSockets[name].shift();  
32.	    // 取完了删除,防止内存泄漏  
33.	    if (!this.freeSockets[name].length)  
34.	      delete this.freeSockets[name];  
35.	    // 设置ref标记,因为正在使用该socket  
36.	    this.reuseSocket(socket, req);  
37.	    // 设置请求对应的socket  
38.	    setRequestSocket(this, req, socket);  
39.	    // 插入正在使用的socket队列  
40.	    this.sockets[name].push(socket);  
41.	  } else if (sockLen < this.maxSockets) {   
42.	    /* 
43.	      如果该key没有对应的空闲socket并且使用的 
44.	      socket个数还没有得到阈值,则继续创建 
45.	    */  
46.	    this.createSocket(req,
47.	                        options, 
48.	                        handleSocketCreation(this, req, true));  
49.	  } else {  
50.	    // 等待该key下有空闲的socket  
51.	    if (!this.requests[name]) {  
52.	      this.requests[name] = [];  
53.	    }  
54.	    this.requests[name].push(req);  
55.	  }  
56.	}  

当我们需要发送一个HTTP请求的时候,我们可以通过Agent的addRequest方法把请求托管到Agent中,当有可用的socket时,Agent会通知我们。addRequest的代码很长,主要分为三种情况。
1 有空闲socket,则直接复用,并插入正在使用的socket队列中
我们主要看一下setRequestSocket函数

1.	function setRequestSocket(agent, req, socket) {  
2.	  // 通知请求socket创建成功  
3.	  req.onSocket(socket);  
4.	  const agentTimeout = agent.options.timeout || 0;  
5.	  if (req.timeout === undefined || req.timeout === agentTimeout) 
6.	  {  
7.	    return;  
8.	  }  
9.	  // 开启一个定时器,过期后触发timeout事件  
10.	  socket.setTimeout(req.timeout);  
11.	  /*
12.	    监听响应事件,响应结束后需要重新设置超时时间,
13.	    开启下一个请求的超时计算,否则会提前过期 
14.	  */ 
15.	  req.once('response', (res) => {  
16.	    res.once('end', () => {  
17.	      if (socket.timeout !== agentTimeout) {  
18.	        socket.setTimeout(agentTimeout);  
19.	      }  
20.	    });  
21.	  });  
22.	}  

setRequestSocket函数通过req.onSocket(socket)通知调用方有可用socket。然后如果请求设置了超时时间则设置socket的超时时间,即请求的超时时间。最后监听响应结束事件,重新设置超时时间。
2 没有空闲socket,但是使用的socket个数还没有达到阈值,则创建新的socket。
我们主要分析创建socket后的回调handleSocketCreation。

1.	function handleSocketCreation(agent, request, informRequest) {  
2.	  return function handleSocketCreation_Inner(err, socket) {  
3.	    if (err) {  
4.	      process.nextTick(emitErrorNT, request, err);  
5.	      return;  
6.	    }  
7.	    /* 
8.	     是否需要直接通知请求方,这时候request不是来自等待
9.	      socket的requests队列, 而是来自调用方,见addRequest 
10.	    */  
11.	    if (informRequest)  
12.	      setRequestSocket(agent, request, socket);  
13.	    else  
14.	      /*
15.	        不直接通知,先告诉agent有空闲的socket,
16.	        agent会判断是否有正在等待socket的请求,有则处理  
17.	       */
18.	      socket.emit('free');  
19.	  };  
20.	}  

3 不满足1,2,则把请求插入等待socket队列。
插入等待socket队列后,当有socket空闲时会触发free事件,我们看一下该事件的处理逻辑。

1.	// 监听socket空闲事件  
2.	 this.on('free', (socket, options) => {  
3.	   const name = this.getName(options);
4.	   // socket还可写并且还有等待socket的请求,则复用socket  
5.	   if (socket.writable &&  
6.	       this.requests[name] && this.requests[name].length) {  
7.	     // 拿到一个等待socket的请求,然后通知它有socket可用  
8.	     const req = this.requests[name].shift();  
9.	     setRequestSocket(this, req, socket);  
10.	     // 没有等待socket的请求则删除,防止内存泄漏  
11.	     if (this.requests[name].length === 0) {  
12.	       // don't leak  
13.	       delete this.requests[name];  
14.	     }  
15.	   } else {  
16.	     // socket不可用写或者没有等待socket的请求了  
17.	     const req = socket._httpMessage;  
18.	     // socket可写并且请求设置了允许使用复用的socket  
19.	     if (req &&  
20.	         req.shouldKeepAlive &&  
21.	         socket.writable &&  
22.	         this.keepAlive) {  
23.	       let freeSockets = this.freeSockets[name];  
24.	       // 该key下当前的空闲socket个数  
25.	       const freeLen = freeSockets ? freeSockets.length : 0;  
26.	       let count = freeLen;  
27.	       // 正在使用的socket个数  
28.	       if (this.sockets[name])  
29.	         count += this.sockets[name].length;  
30.	       /*
31.	           该key使用的socket个数达到阈值或者空闲socket达到阈值,
32.	           则不复用socket,直接销毁socket  
33.	        */
34.	       if (count > this.maxSockets || 
             freeLen >= this.maxFreeSockets) {  
35.	         socket.destroy();  
36.	       } else if (this.keepSocketAlive(socket)) {   
37.	         /*
38.	            重新设置socket的存活时间,设置失败说明无法重新设置存活时
39.	            间,则说明可能不支持复用  
40.	          */
41.	         freeSockets = freeSockets || [];  
42.	         this.freeSockets[name] = freeSockets;  
43.	         socket[async_id_symbol] = -1;  
44.	         socket._httpMessage = null;  
45.	         // 把socket从正在使用队列中移除  
46.	         this.removeSocket(socket, options);  
47.	         // 插入socket空闲队列  
48.	         freeSockets.push(socket);  
49.	       } else {  
50.	         // 不复用则直接销毁  
51.	         socket.destroy();  
52.	       }  
53.	     } else {  
54.	       socket.destroy();  
55.	     }  
56.	   }  
57.	 });  

当有socket空闲时,分为以下几种情况
1 如果有等待socket的请求,则直接复用socket。
2 如果没有等待socket的请求,允许复用并且socket个数没有达到阈值则插入空闲队列。
3 直接销毁

8 使用例子

下面我们从_http_client.js为例子看看如何使用agent。_http_client.js是对http客户端的封装,当我们使用nodejs发送一个请求的时候,就会使用_http_client.js的ClientRequest。

let agent = options.agent;
this.agent = agent;
this.agent.addRequest(this, options);

我们看到当使用agent的时候,ClientRequest会把请求托管给agent,当agent有可用socket的时候,就会执行ClientRequest实例的onSocket方法。ClientRequest会使用拿到的socket发送数据并解析收到的响应,那么收到响应后ClientRequest又是怎么处理的呢?ClientRequest中有一句关键代码

  // 等待响应结束
  res.on('end', responseOnEnd)

responseOnEnd通过层层调用后,执行

req.socket.emit('free');

在_http_agent.js的installListener中监听了free事件

 // socket触发空闲事件的处理函数,告诉agent该socket空闲了,agent会回收该socket到空闲队列
  function onFree() {
    debug('CLIENT socket onFree');
    agent.emit('free', s, options);
  }
  // 监听socket空闲事件
  s.on('free', onFree);

socket进一步触发了agent的free,从而agent处理空闲的socket,销毁或者复用。

9 测试例子

客户端

1.	const http = require('http');  
2.	const keepAliveAgent = new http.Agent({ keepAlive: true, maxSockets: 1 });  
3.	const options = {port: 10000, method: 'GET',  host: '127.0.0.1',}  
4.	options.agent = keepAliveAgent;  
5.	http.get(options, () => {});  
6.	http.get(options, () => {});  
7.	// 等待的请求个数
8.	console.log(Object.keys(options.agent.requests).length)  

服务器

1.	let i =0;  
2.	const net = require('net');  
3.	net.createServer((socket) => {  
4.	  console.log(++i);  
5.	}).listen(10000);  

在例子中,首先创建了一个tcp服务器。然后在客户端使用agent。但是maxSocket的值为1,代表最多只能有一个socket,而这时候客户端发送两个请求,所以有一个请求就会在排队。服务器也只收到了一个连接。服务器只输出1。当我们把maxSockets改成2则会看到输出1,2。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值