1. 程式人生 > >又論Node.js的HTTP模組之深入理解

又論Node.js的HTTP模組之深入理解

問題1:HTTP服務繼承了TCP服務模型,是從connection為單位的服務到以request為單位的服務的封裝,那麼request事件何時觸發?

注意:在開啟keepalive後,一個TCP會話可以用於多次請求和響應,在請求產生的過程中,http模組拿到傳遞過來的資料,呼叫二進位制模組http_parser模組進行解析,在解析完請求報文的報文頭以後,觸發request事件,呼叫使用者的業務邏輯。客戶端物件的reponse事件也是一樣的,只要解析完了響應頭就會觸發,同時傳入一個響應物件以供操作響應,後續報文以只讀流的方式提供。同時,不許知道這時候的response物件是一個http.IncomingMessage例項!

問題2:http請求中的req物件的內部資料是怎麼樣的?

//很顯然req物件是一個IncomingMessage例項
IncomingMessage {
  httpVersionMajor: 1,
  httpVersionMinor: 1,
  httpVersion: '1.1',
  complete: false,
  //req.headers屬性是請求的頭
  headers:
   { host: 'localhost:1337',
     connection: 'keep-alive',
     accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*
/*;q=0.8',
     'upgrade-insecure-requests': '1',
     'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like
 Gecko) Chrome/49.0.2623.110 Safari/537.36',
     'accept-encoding': 'gzip, deflate, sdch',
     'accept-language': 'zh-CN,zh;q=0.8',
     cookie: 'qinliang=s%3ABDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8
oMziad4Ig7jUT1REzGcYcdg; blog=s%3A-ZkSm8urr8KsXAKsZbSTCp8EWOu7zq2o.Axjo6YmD2dLPG
QK9aD1mR8FcpOzyHaGG6cfGUWUVK00' },
  //req.rawHeaders屬性是沒有格式化請求的頭
  rawHeaders:
   [ 'Host',
     'localhost:1337',
     'Connection',
     'keep-alive',
     'Accept',
     'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
',
     'Upgrade-Insecure-Requests',
     '1',
     'User-Agent',
     'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome
/49.0.2623.110 Safari/537.36',
     'Accept-Encoding',
     'gzip, deflate, sdch',
     'Accept-Language',
     'zh-CN,zh;q=0.8',
     'Cookie',
     'qinliang=s%3ABDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8oMziad4I
g7jUT1REzGcYcdg; blog=s%3A-ZkSm8urr8KsXAKsZbSTCp8EWOu7zq2o.Axjo6YmD2dLPGQK9aD1mR
8FcpOzyHaGG6cfGUWUVK00' ],
  trailers: {},
  rawTrailers: [],
  upgrade: false,
  url: '/',
  method: 'GET',
  statusCode: null,
  statusMessage: null,
  httpVersionMajor: 1,
  httpVersionMinor: 1,
  httpVersion: '1.1',
  complete: false,
  headers:
   { host: 'localhost:1337',
     connection: 'keep-alive',
     accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*
/*;q=0.8',
     'upgrade-insecure-requests': '1',
     'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like
 Gecko) Chrome/49.0.2623.110 Safari/537.36',
     'accept-encoding': 'gzip, deflate, sdch',
     'accept-language': 'zh-CN,zh;q=0.8',
     cookie: 'qinliang=s%3ABDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8
oMziad4Ig7jUT1REzGcYcdg; blog=s%3A-ZkSm8urr8KsXAKsZbSTCp8EWOu7zq2o.Axjo6YmD2dLPG
QK9aD1mR8FcpOzyHaGG6cfGUWUVK00' },
  rawHeaders:
   [ 'Host',
     'localhost:1337',
     'Connection',
     'keep-alive',
     'Accept',
     'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
',
     'Upgrade-Insecure-Requests',
     '1',
     'User-Agent',
     'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome
/49.0.2623.110 Safari/537.36',
     'Accept-Encoding',
     'gzip, deflate, sdch',
     'Accept-Language',
     'zh-CN,zh;q=0.8',
     'Cookie',
     'qinliang=s%3ABDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8oMziad4I
g7jUT1REzGcYcdg; blog=s%3A-ZkSm8urr8KsXAKsZbSTCp8EWOu7zq2o.Axjo6YmD2dLPGQK9aD1mR
8FcpOzyHaGG6cfGUWUVK00' ]

注意:對於報文體部分被抽象為一個只讀流,如果業務邏輯需要讀取報文體中的資料必須要在資料流結束後才能進行操作。如資料讀取如下:

function(req,res){
    var buffers=[];
    req.on('data',function(trunk){
        buffers.push(trunk);
    }).on('end',function(){
        var buffer=Buffer.concat(buffers);
        //獲取到所有的資料了,而不僅僅是資料頭
        res.end(buffer);
        //原樣返回給客戶端
    })
}
下面兩種情況會返回IncomingMessage:

第一種情況是伺服器端的request物件

var http=require('http');
http.createServer(function(req,res){
	res.end("IncomingMessage="+(req instanceof http.IncomingMessage));
	//列印true
}).listen(1337);
注意:伺服器的request事件回撥函式第一個引數為http.IncomingMessage物件

第二種情況就是客戶端的res物件

var http=require('http');
var options={
     hostname:'localhost',
     port:1337,
     path:'/',
     method:'GET'
};
var req=http.request(options,function(res){
    //獲取到伺服器的返回資料
    res.on('data',function(chunk){
        //這裡的chunk是Buffer物件,這一點一定要注意是編碼的資料
        console.log(res instanceof http.IncomingMessage);
        //這裡列印true
    })
})
req.end();
//必須呼叫,否則客戶端不會發送請求到伺服器

問題3:http.ClientRequest 物件如何獲取,含有那些方法?

http.request(options[, callback])
        Node.js在每一個伺服器上保持多個連線,這個方法可以讓客戶端傳送一個請求,options如果是string那麼就會用url.parse方法自動解析,物件包含的屬性如下:
 protocal:預設為"http:"
 host:是主機名
 hostname:是host的別名,hostname的優先順序高於host
 family:表示解析host/hostname時候的Ip型別,可以是4/6,如果沒有指定那麼會同時用IPV4/IPV6
 port:預設為80
 localAddress:傳送請求的本地網絡卡
 socketPath:Unix域套接字,要麼使用host:port/socketPath
 method:預設為get
 path:預設為/。路徑中不能使用空格
 headers:客戶端傳送的HTTP頭
 auth:使用基本認證。如"user:pass"去計算認證頭Authorization

伺服器端的程式碼:

var http=require('http');
http.createServer(function(req,res){
	var auth=req.headers['authorization']||'';
    //這裡獲取到的是一個數組Basic cWlubGlhbmc6MTIz,其中第一個部分是一定的,第二部分為new Buffer("qinliang:123").toString('base64')
	var parts=auth.split(' ');
	var method=parts[0];//Basic
	var encoded=parts[1];//祕鑰
	var decoder=new Buffer(encoded,'base64').toString('utf-8').split(":");
    //解密後獲取原始值
    var user=decoder[0];
    //使用者名稱
    var pass=decoder[1];
    //密碼,到了這一步就獲取到了客戶端的使用者名稱和密碼了
    if(!user=='qinliang'&&!pass=='123'){
    	res.setHeader('WWW-Authenticate','Basic realm="Secure Area"');
    	res.writeHead(401);
    	//未授權
    	res.end();
    }else{
    	res.end('You are forbidden to enter!');
    }

}).listen(8888,'localhost');
客戶端的程式碼:
var http=require('http');
var encode=function(username,password){
    return new Buffer(username+":"+password).toString('base64');
}
var options={
     hostname:'localhost',
     port:8888,
     path:'/',
     method:'GET',
     //auth:encode('qinliang',123)
     auth:"qinliang:123"
     //使用者名稱為qinliang,密碼為123
};
var req=http.request(options,function(res){
    //獲取到伺服器的返回資料
    res.on('data',function(chunk){
        //這裡的chunk是Buffer物件,這一點一定要注意是編碼的資料
        console.log(res instanceof http.IncomingMessage);
        //這裡列印true
    })
})
req.end();
//必須呼叫,否則客戶端不會發送請求到伺服器
agent:用於控制Agent行為。如果指定了agent那麼請求就變成Connect:keep-alive了。可以指定他的值為undefined(這時候這個值就是http.globalAgent);也可以指定為Agent物件;第三個值為false,從連線池中拿到一個Agent,預設請求為Connection:close。其中回撥函式callback是作為response事件的預設事件處理函式。這個方法返回的是一個http.ClientRequest類,這個類是一個可寫的流,如果需要用Post請求來上傳檔案,這時候就需要寫ClientRequest物件。下面是用這個物件進行上傳檔案的操作:
var http=require('http');
var querystring=require('querystring');
var postData = querystring.stringify({
  'msg' : 'Hello World!'
});
var options = {
  hostname: 'www.google.com',
  port: 80,
  path: '/upload',
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-Length': postData.length
  }
};
//這裡的req物件是一個Class: http.ClientRequest
var req = http.request(options, (res) => {
  console.log(`STATUS: ${res.statusCode}`);
  console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
  res.setEncoding('utf8');
  res.on('data', (chunk) => {
    console.log(`BODY: ${chunk}`);
  });
  res.on('end', () => {
    console.log('No more data in response.')
  })
});
//如果DNS解析錯誤,TCP錯誤,HTTP解析錯誤就會觸發error事件
req.on('error', (e) => {
  console.log(`problem with request: ${e.message}`);
});
// write data to request body
//把資料寫入到請求體中
req.write(postData);
req.end();
//使用http.request方法時候必須呼叫re.end方法來表示沒有請求體傳送了,也就是請求完成了。
注意:傳送給一個Connection:keep-alive就會通知Node.js當前的這個連線應該儲存,並用於下一次請求;如果設定了Content-length時候,那麼就會取消chunked編碼;Expect請求頭如果被髮送,那麼所有的請求頭都會馬上傳送,如果你設定了Expect: 100-continue,這時候你必須指定timeout屬性,同時監聽continue事件;傳送Authorization頭時候,上面指定的用auth計算的Authorization值就會被覆蓋。
Class: http.ClientRequest:
這個物件是通過http.request方法返回的。他代表一個正在處理的請求,但是這個請求的頭已經在佇列中了。這時候這個頭還是可以通過setHeader(name, value), getHeader(name), removeHeader(name)改變的。實際的頭部發送需要等到和資料一起傳送,或者連線關閉的時候也會發送。如果你需要獲取伺服器端的響應,這時候需要監聽response事件,這個事件當收到伺服器端的響應頭的時候就會觸發<響應頭>,其回撥函式只有一個引數是一個IncomingMessage物件例項,在response事件中,我們可以為response物件新增事件,一般監聽data事件。

如果沒有指定任何response事件處理函式,那麼所有的response響應都會被丟棄。如果你指定了resposne事件那麼你必須自己從response物件上消費資料,可以通過呼叫response.read方法或者新增一個"data"事件處理函式,或者通過呼叫"resume"方法。當data被消費了以後,'end'事件就會觸發。當然,如果讀取了資料,那麼這時候就會消耗記憶體,也可能導致最後的記憶體溢位錯誤。注意:Node.js不會監測Content-Length是否和資料體的長度一致。這個request物件實現了Writtable Stream,也是一個EventEmitter物件。我們先來看看這個http.ClientRequest類的簽名:

ClientRequest {
  domain: null,
  _events:
   { response: { [Function: g] listener: [Function] },
     socket: { [Function: g] listener: [Function] }
    },
  _eventsCount: 2,
  _maxListeners: undefined,
  output: [],
  outputEncodings: [],
  outputCallbacks: [],
  outputSize: 0,
  writable: true,
  _last: true,
  chunkedEncoding: false,
  shouldKeepAlive: false,
  useChunkedEncodingByDefault: false,
  sendDate: false,
  _removedHeader: {},
  _contentLength: null,
  _hasBody: true,
  _trailer: '',
  finished: false,
  _headerSent: false,
  socket: null,
  connection: null,
  _header: null,
  _headers:
   { host: 'localhost:8888',
     authorization: 'Basic cWlubGlhbmc6MTIz' },
  _headerNames: { host: 'Host', authorization: 'Authorization' },
  _onPendingData: null,
  agent:
   Agent {
     domain: null,
     _events: { free: [Function] },
     _eventsCount: 1,
     _maxListeners: undefined,
     defaultPort: 80,
     protocol: 'http:',
     options: { path: null },
     requests: {},
     sockets: { 'localhost:8888:': [Object] },
     freeSockets: {},
     keepAliveMsecs: 1000,
     keepAlive: false,
     maxSockets: Infinity,
     maxFreeSockets: 256 },
  socketPath: undefined,
  method: 'GET',
  path: '/' }
這個物件有下面這些方法:
request.abort()
//取消客戶端的請求,這個方法會導致響應物件中的資料被丟掉同時socket被銷燬
request.end([data][, encoding][, callback])
//結束髮送請求。如果指定了data,那麼相當於呼叫了response.write(data, encoding)和request.end(callback),回撥函式當請求流結束時候觸發
request.flushHeaders()
//Node.js通常會快取請求頭直到request.end方法被呼叫或者在傳送資料的時候才會傳送頭部。進而把頭部和資料在一個TCP包中傳送到伺服器。這會節省TCP來回時間。這個方法相當於繞開NOde.js這種優化,從而立即開始請求
request.setNoDelay([noDelay])
//如果這個請求獲取到了socket,同時已經連線了,那麼就會呼叫socket.setNoDelay方法
request.setSocketKeepAlive([enable][, initialDelay])
//如果這個請求獲取到了socket,同時已經連線了,那麼就會呼叫socket.setKeepAlive
request.setTimeout(timeout[, callback])
//如果這個請求獲取到了socket,同時已經連線了,那麼socket.setTimeout就會被呼叫
request.write(chunk[, encoding][, callback])
//傳送資料塊。如果多次呼叫這個方法,那麼使用者就會把這個資料傳送到伺服器,這時候建議使用['Transfer-Encoding', 'chunked']請求頭。第一個引數可以是BUffer/string
這個物件有一下事件:
abort事件
//如果客戶端取消了請求的時候就會觸發,而且只會第一次取消的時候觸發
connect事件
//函式簽名為:function (response, socket, head) { },每次伺服器通過CONNECT方法來回應客戶端的時候觸發。如果客戶端沒有監聽這個事件那麼當接收到伺服器的CONNECT方法的時候就會關閉連線。
continue事件
//當伺服器傳送一個100 Continue的HTTP響應的時候觸發,一般是因為客戶端傳送了Expect: 100-continue',這時候客戶端應該傳送訊息體
response事件
//獲取到請求的時候觸發,而且只會觸發一次,response物件是一個http.IncomingMessage.
socket事件
//如果這個request物件獲取到一個socket的時候就會觸發
upgrade事件
//如果伺服器端通過傳送一個upgrade響應客戶端的請求的時候觸發。如果客戶端沒有監聽這個事件那麼當客戶端收到一個upgrade請求頭的時候那麼連線就會關閉

下面展示如何通過http.ClientRequest類和http.Server類實現協議升級:(具體程式碼見github地址)

const http = require('http');
// Create an HTTP server
var srv = http.createServer( (req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('okay');
});
//伺服器監聽upgrade事件
srv.on('upgrade', (req, socket, head) => {
  socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
               'Upgrade: WebSocket\r\n' +
               'Connection: Upgrade\r\n' +
               '\r\n');
  //傳送訊息到伺服器端
  socket.pipe(socket); // echo back
});

// now that server is running
srv.listen(7777, '127.0.0.1', () => {
  // make a request
  var options = {
    port: 7777,
    hostname: '127.0.0.1',
    headers: {
      'Connection': 'Upgrade',
      'Upgrade': 'websocket'
    }
  };
  //如果客戶端有一個請求到來了,那麼馬上向伺服器傳送一個upgrade請求
  var req = http.request(options);
  req.end();
  //客戶端接收到upgrade事件,要清楚的知道升級協議不代表上面的http.createServer回撥會執行,這個回撥函式只有當瀏覽器訪問的時候才會觸發
  req.on('upgrade', (res, socket, upgradeHead) => {
    console.log('got upgraded!');
    socket.end();
    process.exit(0);
  });
});

如何從http.ClientReqeust物件傳送一個CONNECT請求:

const http = require('http');
const net = require('net');
const url = require('url');
//如果在connect事件和reques事件中都列印log,那麼就會發現request在connect之前
// Create an HTTP tunneling proxy
var proxy = http.createServer( (req, res) => {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('okay');
});
//http.Server的connect事件,Emitted each time a client requests a http CONNECT method
proxy.on('connect', (req, cltSocket, head) => {
  // connect to an origin server
  var srvUrl = url.parse(`http://${req.url}`);
  //獲取到請求的URL地址,也就是req.url請求地址
  //console.log(srvUrl);
  //這個物件具有如下的函式簽名方法:
  /*
  Url {
    protocol: 'http:',
    slashes: true,
    auth: null,
    host: 'www.google.com:80',
    port: '80',
    hostname: 'www.google.com',
    hash: null,
    search: null,
    query: null,
    pathname: '/',
    path: '/',
    href: 'http://www.google.com:80/' }
  */
  //net.connect方法的返回型別是一個net.Socket型別,同時連線到特定的url
  var srvSocket = net.connect(srvUrl.port, srvUrl.hostname, () => {
    //在http.Server物件的connect事件中第二個引數為一個socket,連線客戶端和伺服器端
    cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +
                    'Proxy-agent: Node.js-Proxy\r\n' +
                    '\r\n');
    //利用雙方的socket用於傳送訊息
    srvSocket.write(head);
    //這個也是一個net.Socket型別
    srvSocket.pipe(cltSocket);
    //readable.pipe(destination[, options])
    cltSocket.pipe(srvSocket);
  });
});

// now that proxy is running
//http.Server開始啟動了
proxy.listen(9999, 'localhost', () => {
  // make a request to a tunneling proxy
  var options = {
    port: 9999,
    hostname: 'localhost',
    method: 'CONNECT',//客戶端請求一個http的CONNECT方法
    path: 'www.google.com:80'
  };
  var req = http.request(options);
  req.end();
  //客戶端請求完畢伺服器的CONNECT事件,這時候伺服器的connect事件被觸發
  req.on('connect', (res, socket, head) => {
    console.log('got connected!');
    // make a request over an HTTP tunnel
    //每次伺服器通過CONNECT方法來回應客戶端的時候觸發
    socket.write('GET / HTTP/1.1\r\n' +
                 'Host: www.google.com:80\r\n' +
                 'Connection: close\r\n' +
                 '\r\n');
    socket.on('data', (chunk) => {
      console.log(chunk.toString());
    });
    socket.on('end', () => {
      proxy.close();
    });
  });
});
問題4:http.Server類有哪些事件和方法?
//這個類繼承與net.Server,同時又有以下事件
checkContinue事件:
//當客戶端傳送Expect:100-continue的時候觸發,如果伺服器沒有監聽那麼就會自動傳送一個100 Continue的頭表示接受資料上傳。這個方法處理的時候可以呼叫response.writeContinue方法表示客戶端應該繼續傳送請求體,當然也可以產生一個合理的HTTP響應,如'400 Bad Request',這時候伺服器不能再次傳送資料體了。注意:如果這個事件觸發並且被處理了,這時候'request'事件就會被忽略
clientError事件:
//如果客戶端觸發一個'error'事件,這時候這個事件就會觸發。函式簽名為function (exception, socket) { },其中socket是一個net.socket物件,也是產生錯誤的地方
close事件:
//當伺服器呼叫close方法的時候觸發,如server.close方法停止接受新的連線,當所有的連線都斷開的時候就會觸發該事件。當然可以傳入一個回撥函式
connect事件:
//函式簽名為function (request, socket, head) { },當客戶端請求一個伺服器的CONNECT事件的時候觸發,如果這個方法沒有監聽那麼通過CONNECT方法來請求就會關閉連線。request物件是一個http request物件;socket是客戶端和伺服器端的socket;head是一個Buffer物件,the first packet of the tunneling stream,可能是空物件。注意:如果這個事件被觸發了,那麼request物件就沒有'data'事件監聽函數了,因此你需要自己處理髮送過來的資料
//http.Server的connect事件,Emitted each time a client requests a http CONNECT method
proxy.on('connect', (req, cltSocket, head) => {
  // connect to an origin server
  var srvUrl = url.parse(`http://${req.url}`);
  //獲取到請求的URL地址,也就是req.url請求地址
  //console.log(srvUrl);
  //這個物件具有如下的函式簽名方法:
  //net.connect方法的返回型別是一個net.Socket型別,同時連線到特定的url
  var srvSocket = net.connect(srvUrl.port, srvUrl.hostname, () => {
    //在http.Server物件的connect事件中第二個引數為一個socket,連線客戶端和伺服器端
    cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +
                    'Proxy-agent: Node.js-Proxy\r\n' +
                    '\r\n');
    //利用雙方的socket用於傳送訊息
    srvSocket.write(head);
    //這個也是一個net.Socket型別
    srvSocket.pipe(cltSocket);
    //readable.pipe(destination[, options])
    cltSocket.pipe(srvSocket);
  });
});
connection事件:
//一個新的TCP連線被建立時候觸發,socket就是一個net.Socket型別,一般情況下使用者不需要處理這個事件,這個socket不會觸發'readable'事件,同時你可以通過request.connection來訪問這個socket。函式簽名為function (socket) { }
request事件:
//function (request, response) { }函式簽名,當請求發動到伺服器,解析出HTTP頭的時候觸發。每次獲取到請求的時候觸發。注意:一個連線可能有多個請求,如果設定了keep-alive時候就是這樣。reqest是IncomingMesage,而response是一個http.ServerResponse物件
upgrade事件:
//函式簽名為function (request, socket, head) { },head是一個Buffer物件可能為空。這時候socket不會有'data'監聽函式,因此你需要自己處理髮送過來的資料
 //伺服器監聽upgrade事件
	srv.on('upgrade', (req, socket, head) => {
	  socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +
	               'Upgrade: WebSocket\r\n' +
	               'Connection: Upgrade\r\n' +
	               '\r\n');
	  //傳送訊息到伺服器端
	  socket.pipe(socket); // echo back
	});

server.close([callback])方法:
//阻止伺服器接收新的連線,set.Server.close方法
server.maxHeadersCount
//限制到來訊息的最大的HTTP頭數量,預設為1000,設定為0表示沒有限制
server.setTimeout(msecs, callback)
//在伺服器物件上面監聽一個timeout事件。預設情況下伺服器超時為2min,如果超時了socket就會自動銷燬。如果你自己設定了一個'timeout'事件,那麼就要自己處理socket的超時請求
server.timeout
//設定這個引數只會影響新的連線,不會影響已經存在的連線
問題5:Class: http.ServerResponse內部機制是什麼?

注意:這一部分內容比較好理解,這裡就不展開討論了
問題6:Class: http.Agent類設定的初衷是什麼?

如同伺服器端的實現一樣,http提供的ClientRequest物件也是基於TCP實現的,在keep-alive的情況下,一個底層會話連線可以用於多次請求。為了重用TCP連線,http模組包含了一個預設的客戶端代理http.globalAgent物件,他對每一個伺服器端建立的連線進行了管理,預設情況下,通過ClientRequest物件對同一個伺服器端傳送的HTTP請求最多可以建立5個連線,他的實質為一個連線池。呼叫HTTP客戶端對一個伺服器傳送10次HTTP請求時候只有5個請求處於併發狀態,後續的請求需要等待某個請求完成服務後才能真正請求。(這和瀏覽器同一個域名下有連線限制是一樣的,這裡http.ClientRequest就是客戶端)
http.globalAgent
全域性Agent例項,預設為5個連線。

我們先來學習一下http.get方法:

http.get(options[, callback])
因為大多數的請求都是GET請求沒有資料體,Node.js提供了這個方法用於傳送GET請求。這個方法和http.request的唯一區別在於:這個方法會自動把方法設定為GET,同時自動呼叫req.end方法,如下例:

	http.get('http://www.google.com/index.html', (res) => {
	  console.log(`Got response: ${res.statusCode}`);
	  // consume response body
	  //消費從伺服器端傳送過來的資料
	  res.resume();
	}).on('error', (e) => {
	  console.log(`Got error: ${e.message}`);
	});
Class: http.Agent
這個HTTP Agent預設使得客戶端的請求使用了Connection:keep-alive,但是不需要使用KeepAlive手動關閉。如果你需要使用HTTP的KeepAlive選項,那麼你需要在建立一個Agent物件的時把flag設定為true。這時候Agent就會保持在連線池中那些沒有使用的連線處於活動狀態已被後續使用。這時候的連線就會被顯示標記從而不讓Node.js的程序一直執行。當然,當你確實不需要使用KeepAlive狀態的agent的時候顯示的呼叫destroy方法還是很好的選擇,這時候Socket就會被關閉。注意:當socket觸發了'close'或者'agentRemove'事件的時候就會從aget池中被移除,因此如果比需要讓一個HTTP請求保持長時間的開啟狀態,同時不想讓這個連線在Agent池的時候可以使用下面的方法:
	http.get(options, (res) => {
	  // Do stuff
	}).on('socket', (socket) => {
	  socket.emit('agentRemove');
	});
但是,如果你需要把所有的連線都不讓Agent池來管理可以把agent:false
http.get({
  hostname: 'localhost',
  port: 80,
  path: '/',
  agent: false  // create a new agent just for this one request
}, (res) => {
  // Do stuff with response
})
new Agent([options])
這個options可以是如下的內容:
 。keepAlive:布林值,預設為false,表示讓Agent池中的socket保持活動狀態用於未來的請求。
 。keepAliveMsecs,當使用了HTTP的KeepAlive的時候,多久為keep-alive的socket傳送一個TCP的KeepAlive資料包。預設為1000,但是隻有keepAlive設定為true時候才可以
 。maxSockets:每一個主機下面最多具有的socket數量,預設為Infinity
 。maxFreeSockets:處於活動狀態的最多的socket數量,預設為256,在keepAlive設定為true時候可用
 注意:http.globalAgent物件所有值都是預設值的
 
const http = require('http');
var keepAliveAgent = new http.Agent({ keepAlive: true });
options.agent = keepAliveAgent;
http.request(options, onResponseCallback);
Agent物件有如下一系列的方法:
agent.destroy()
//銷燬agent當前正在使用的socket。一般情況沒有必要呼叫,如果你使用了Agent物件並把keepAlive設定為true的時候,當你明確知道以後不需要使用這個socket的時候就需要顯示的銷燬。否則,這個socket長時間等待伺服器銷燬
agent.freeSockets
//表示HTTP的keepAlive被使用的時候,這個選項表示當前Agent等待使用的socket,是一個socket陣列
agent.getName(options)
//獲取一個唯一的值,這個值是在reques物件的options中設定的,使用這個值決定這個連線是否可以重用。在http Agent中返回host:port:localAddress,在https Agent,包含CA, cert, ciphers, and other HTTPS/TLS-specific options that determine socket reusability.
agent.maxFreeSockets
//預設為256.如果這個agent支援KeepAlive,那個這個值表示處於空閒狀態的最大的socket
agent.maxSockets
//預設為Infinity,用於指定agent可以在一個域下面提供的最大的併發的socket數量。origion可以是'host:port' or 'host:port:localAddress' combination.
agent.requests
//用於獲取一個請求陣列,這個陣列中儲存的是請求物件,表示還沒有傳送也就是在等待的請求的數量。不要修改他!
agent.sockets
//用於指定當前正在被使用的Agent中的sockets陣列,不要修改這個值
注意:這裡突然想到了為什麼瀏覽器需要限制同域名下併發的請求的數量。原因主要有幾個:第一個是為了保護伺服器端,因為伺服器併發處理的請求的數量也是有限制的,是客戶端和伺服器端一種默契的配合;第二個原因是TCP連線需要一個埠號,而埠號的數量最多是65536,而很多埠號都被系統的其他內建程序佔用,而建立TCP連線需要消耗系統資源,因此這種方法可以保護作業系統的 TCP\IP 協議棧資源不被迅速耗盡,因此瀏覽器不好發出太多的 TCP 連線,而是採取用完了之後再重複利用 TCP 連線或者乾脆重新建立 TCP 連線的方法;第三個原因是:建立TCP/IP需要一定的系統資源,而且作業系統在呼叫程序的時候需要切換上下文,因此也會限制瀏覽器併發的數量,這是作業系統一種自我保護措施!
注意:半開連線指的是 TCP 連線的一種狀態,當客戶端向伺服器端發出一個 TCP 連線請求,在客戶端還沒收到伺服器端的迴應併發回一個確認的資料包時,這個 TCP 連線就是一個半開連線。若伺服器到超時以後仍無響應,那麼這個 TCP 連線就等於白費了,所以作業系統會本能的保護自己,限制 TCP 半開連線的總個數,以免有限的核心態記憶體空間被維護 TCP 連線所需的記憶體所浪費。