转 node.js里面的http模块深入理解

问题1:HTTP服务继承了TCP服务模型,是从connection为单位的服务到以request为单位的服务的封装,那么request事件何时触发?

注意:在开启keepalive后,一个TCP会话可以用于多次请求和响应,在请求产生的过程中,http模块拿到传递过来的数据,调用二进制模块http_parser模块进行解析,在解析完请求报文的报文头以后,触发request事件,调用用户的业务逻辑。客户端对象的reponse事件也是一样的,只要解析完了响应头就会触发,同时传入一个响应对象以供操作响应,后续报文以只读流的方式提供。同时,不许知道这时候的response对象是一个http.IncomingMessage实例!

问题2:http请求中的req对象的内部数据是怎么样的?

 

[javascript] view plain copy
 
  1. //很显然req对象是一个IncomingMessage实例  
  2. IncomingMessage {  
  3.   httpVersionMajor: 1,  
  4.   httpVersionMinor: 1,  
  5.   httpVersion: '1.1',  
  6.   complete: false,  
  7.   //req.headers属性是请求的头  
  8.   headers:  
  9.    { host: 'localhost:1337',  
  10.      connection: 'keep-alive',  
  11.      accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*  
  12. /*;q=0.8', 
  13.      'upgrade-insecure-requests': '1', 
  14.      'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like 
  15.  Gecko) Chrome/49.0.2623.110 Safari/537.36', 
  16.      'accept-encoding': 'gzip, deflate, sdch', 
  17.      'accept-language': 'zh-CN,zh;q=0.8', 
  18.      cookie: 'qinliang=s%3ABDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8 
  19. oMziad4Ig7jUT1REzGcYcdg; blog=s%3A-ZkSm8urr8KsXAKsZbSTCp8EWOu7zq2o.Axjo6YmD2dLPG 
  20. QK9aD1mR8FcpOzyHaGG6cfGUWUVK00' }, 
  21.   //req.rawHeaders属性是没有格式化请求的头 
  22.   rawHeaders: 
  23.    [ 'Host', 
  24.      'localhost:1337', 
  25.      'Connection', 
  26.      'keep-alive', 
  27.      'Accept', 
  28.      'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8  
  29. ',  
  30.      'Upgrade-Insecure-Requests',  
  31.      '1',  
  32.      'User-Agent',  
  33.      'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome  
  34. /49.0.2623.110 Safari/537.36',  
  35.      'Accept-Encoding',  
  36.      'gzip, deflate, sdch',  
  37.      'Accept-Language',  
  38.      'zh-CN,zh;q=0.8',  
  39.      'Cookie',  
  40.      'qinliang=s%3ABDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8oMziad4I  
  41. g7jUT1REzGcYcdg; blog=s%3A-ZkSm8urr8KsXAKsZbSTCp8EWOu7zq2o.Axjo6YmD2dLPGQK9aD1mR  
  42. 8FcpOzyHaGG6cfGUWUVK00' ],  
  43.   trailers: {},  
  44.   rawTrailers: [],  
  45.   upgrade: false,  
  46.   url: '/',  
  47.   method: 'GET',  
  48.   statusCode: null,  
  49.   statusMessage: null,  
  50.   httpVersionMajor: 1,  
  51.   httpVersionMinor: 1,  
  52.   httpVersion: '1.1',  
  53.   complete: false,  
  54.   headers:  
  55.    { host: 'localhost:1337',  
  56.      connection: 'keep-alive',  
  57.      accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*  
  58. /*;q=0.8', 
  59.      'upgrade-insecure-requests': '1', 
  60.      'user-agent': 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like 
  61.  Gecko) Chrome/49.0.2623.110 Safari/537.36', 
  62.      'accept-encoding': 'gzip, deflate, sdch', 
  63.      'accept-language': 'zh-CN,zh;q=0.8', 
  64.      cookie: 'qinliang=s%3ABDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8 
  65. oMziad4Ig7jUT1REzGcYcdg; blog=s%3A-ZkSm8urr8KsXAKsZbSTCp8EWOu7zq2o.Axjo6YmD2dLPG 
  66. QK9aD1mR8FcpOzyHaGG6cfGUWUVK00' }, 
  67.   rawHeaders: 
  68.    [ 'Host', 
  69.      'localhost:1337', 
  70.      'Connection', 
  71.      'keep-alive', 
  72.      'Accept', 
  73.      'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8  
  74. ',  
  75.      'Upgrade-Insecure-Requests',  
  76.      '1',  
  77.      'User-Agent',  
  78.      'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome  
  79. /49.0.2623.110 Safari/537.36',  
  80.      'Accept-Encoding',  
  81.      'gzip, deflate, sdch',  
  82.      'Accept-Language',  
  83.      'zh-CN,zh;q=0.8',  
  84.      'Cookie',  
  85.      'qinliang=s%3ABDOjujVhV0DH9Atax_gl4DgZ4-1RGvjQ.OeUddoRalzB4iSmUHcE8oMziad4I  
  86. g7jUT1REzGcYcdg; blog=s%3A-ZkSm8urr8KsXAKsZbSTCp8EWOu7zq2o.Axjo6YmD2dLPGQK9aD1mR  
  87. 8FcpOzyHaGG6cfGUWUVK00' ]  

 

注意:对于报文体部分被抽象为一个只读流,如果业务逻辑需要读取报文体中的数据必须要在数据流结束后才能进行操作。如数据读取如下:

 

[javascript] view plain copy
 
  1. function(req,res){  
  2.     var buffers=[];  
  3.     req.on('data',function(trunk){  
  4.         buffers.push(trunk);  
  5.     }).on('end',function(){  
  6.         var buffer=Buffer.concat(buffers);  
  7.         //获取到所有的数据了,而不仅仅是数据头  
  8.         res.end(buffer);  
  9.         //原样返回给客户端  
  10.     })  
  11. }  
下面两种情况会返回IncomingMessage:

第一种情况是服务器端的request对象

 

[javascript] view plain copy
 
  1. var http=require('http');  
  2. http.createServer(function(req,res){  
  3.     res.end("IncomingMessage="+(req instanceof http.IncomingMessage));  
  4.     //打印true  
  5. }).listen(1337);  
注意:服务器的request事件回调函数第一个参数为http.IncomingMessage对象

第二种情况就是客户端的res对象

 

[javascript] view plain copy
 
  1. var http=require('http');  
  2. var options={  
  3.      hostname:'localhost',  
  4.      port:1337,  
  5.      path:'/',  
  6.      method:'GET'  
  7. };  
  8. var req=http.request(options,function(res){  
  9.     //获取到服务器的返回数据  
  10.     res.on('data',function(chunk){  
  11.         //这里的chunk是Buffer对象,这一点一定要注意是编码的数据  
  12.         console.log(res instanceof http.IncomingMessage);  
  13.         //这里打印true  
  14.     })  
  15. })  
  16. req.end();  
  17. //必须调用,否则客户端不会发送请求到服务器  

 

问题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

服务器端的代码:

 

[javascript] view plain copy
 
  1. var http=require('http');  
  2. http.createServer(function(req,res){  
  3.     var auth=req.headers['authorization']||'';  
  4.     //这里获取到的是一个数组Basic cWlubGlhbmc6MTIz,其中第一个部分是一定的,第二部分为new Buffer("qinliang:123").toString('base64')  
  5.     var parts=auth.split(' ');  
  6.     var method=parts[0];//Basic  
  7.     var encoded=parts[1];//秘钥  
  8.     var decoder=new Buffer(encoded,'base64').toString('utf-8').split(":");  
  9.     //解密后获取原始值  
  10.     var user=decoder[0];  
  11.     //用户名  
  12.     var pass=decoder[1];  
  13.     //密码,到了这一步就获取到了客户端的用户名和密码了  
  14.     if(!user=='qinliang'&&!pass=='123'){  
  15.         res.setHeader('WWW-Authenticate','Basic realm="Secure Area"');  
  16.         res.writeHead(401);  
  17.         //未授权  
  18.         res.end();  
  19.     }else{  
  20.         res.end('You are forbidden to enter!');  
  21.     }  
  22.   
  23. }).listen(8888,'localhost');  
客户端的代码:

 

 

[javascript] view plain copy
 
  1. var http=require('http');  
  2. var encode=function(username,password){  
  3.     return new Buffer(username+":"+password).toString('base64');  
  4. }  
  5. var options={  
  6.      hostname:'localhost',  
  7.      port:8888,  
  8.      path:'/',  
  9.      method:'GET',  
  10.      //auth:encode('qinliang',123)  
  11.      auth:"qinliang:123"  
  12.      //用户名为qinliang,密码为123  
  13. };  
  14. var req=http.request(options,function(res){  
  15.     //获取到服务器的返回数据  
  16.     res.on('data',function(chunk){  
  17.         //这里的chunk是Buffer对象,这一点一定要注意是编码的数据  
  18.         console.log(res instanceof http.IncomingMessage);  
  19.         //这里打印true  
  20.     })  
  21. })  
  22. req.end();  
  23. //必须调用,否则客户端不会发送请求到服务器  
agent:用于控制Agent行为。如果指定了agent那么请求就变成Connect:keep-alive了。可以指定他的值为undefined(这时候这个值就是http.globalAgent);也可以指定为Agent对象;第三个值为false,从连接池中拿到一个Agent,默认请求为Connection:close。其中回调函数callback是作为response事件的默认事件处理函数。这个方法返回的是一个http.ClientRequest类,这个类是一个可写的流,如果需要用Post请求来上传文件,这时候就需要写ClientRequest对象。下面是用这个对象进行上传文件的操作:

 

 

[javascript] view plain copy
 
  1. var http=require('http');  
  2. var querystring=require('querystring');  
  3. var postData = querystring.stringify({  
  4.   'msg' : 'Hello World!'  
  5. });  
  6. var options = {  
  7.   hostname: 'www.google.com',  
  8.   port: 80,  
  9.   path: '/upload',  
  10.   method: 'POST',  
  11.   headers: {  
  12.     'Content-Type': 'application/x-www-form-urlencoded',  
  13.     'Content-Length': postData.length  
  14.   }  
  15. };  
  16. //这里的req对象是一个Class: http.ClientRequest  
  17. var req = http.request(options, (res) => {  
  18.   console.log(`STATUS: ${res.statusCode}`);  
  19.   console.log(`HEADERS: ${JSON.stringify(res.headers)}`);  
  20.   res.setEncoding('utf8');  
  21.   res.on('data', (chunk) => {  
  22.     console.log(`BODY: ${chunk}`);  
  23.   });  
  24.   res.on('end', () => {  
  25.     console.log('No more data in response.')  
  26.   })  
  27. });  
  28. //如果DNS解析错误,TCP错误,HTTP解析错误就会触发error事件  
  29. req.on('error', (e) => {  
  30.   console.log(`problem with request: ${e.message}`);  
  31. });  
  32. // write data to request body  
  33. //把数据写入到请求体中  
  34. req.write(postData);  
  35. req.end();  
  36. //使用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类的签名:

 

[javascript] view plain copy
 
  1. ClientRequest {  
  2.   domain: null,  
  3.   _events:  
  4.    { response: { [Function: g] listener: [Function] },  
  5.      socket: { [Function: g] listener: [Function] }  
  6.     },  
  7.   _eventsCount: 2,  
  8.   _maxListeners: undefined,  
  9.   output: [],  
  10.   outputEncodings: [],  
  11.   outputCallbacks: [],  
  12.   outputSize: 0,  
  13.   writable: true,  
  14.   _last: true,  
  15.   chunkedEncoding: false,  
  16.   shouldKeepAlive: false,  
  17.   useChunkedEncodingByDefault: false,  
  18.   sendDate: false,  
  19.   _removedHeader: {},  
  20.   _contentLength: null,  
  21.   _hasBody: true,  
  22.   _trailer: '',  
  23.   finished: false,  
  24.   _headerSent: false,  
  25.   socket: null,  
  26.   connection: null,  
  27.   _header: null,  
  28.   _headers:  
  29.    { host: 'localhost:8888',  
  30.      authorization: 'Basic cWlubGlhbmc6MTIz' },  
  31.   _headerNames: { host: 'Host', authorization: 'Authorization' },  
  32.   _onPendingData: null,  
  33.   agent:  
  34.    Agent {  
  35.      domain: null,  
  36.      _events: { free: [Function] },  
  37.      _eventsCount: 1,  
  38.      _maxListeners: undefined,  
  39.      defaultPort: 80,  
  40.      protocol: 'http:',  
  41.      options: { path: null },  
  42.      requests: {},  
  43.      sockets: { 'localhost:8888:': [Object] },  
  44.      freeSockets: {},  
  45.      keepAliveMsecs: 1000,  
  46.      keepAlive: false,  
  47.      maxSockets: Infinity,  
  48.      maxFreeSockets: 256 },  
  49.   socketPath: undefined,  
  50.   method: 'GET',  
  51.   path: '/' }  
这个对象有下面这些方法:

 

 

[javascript] view plain copy
 
  1. request.abort()  
  2. //取消客户端的请求,这个方法会导致响应对象中的数据被丢掉同时socket被销毁  
  3. request.end([data][, encoding][, callback])  
  4. //结束发送请求。如果指定了data,那么相当于调用了response.write(data, encoding)和request.end(callback),回调函数当请求流结束时候触发  
  5. request.flushHeaders()  
  6. //Node.js通常会缓存请求头直到request.end方法被调用或者在发送数据的时候才会发送头部。进而把头部和数据在一个TCP包中发送到服务器。这会节省TCP来回时间。这个方法相当于绕开NOde.js这种优化,从而立即开始请求  
  7. request.setNoDelay([noDelay])  
  8. //如果这个请求获取到了socket,同时已经连接了,那么就会调用socket.setNoDelay方法  
  9. request.setSocketKeepAlive([enable][, initialDelay])  
  10. //如果这个请求获取到了socket,同时已经连接了,那么就会调用socket.setKeepAlive  
  11. request.setTimeout(timeout[, callback])  
  12. //如果这个请求获取到了socket,同时已经连接了,那么socket.setTimeout就会被调用  
  13. request.write(chunk[, encoding][, callback])  
  14. //发送数据块。如果多次调用这个方法,那么用户就会把这个数据发送到服务器,这时候建议使用['Transfer-Encoding', 'chunked']请求头。第一个参数可以是BUffer/string  
这个对象有一下事件:

 

 

[javascript] view plain copy
 
  1. abort事件  
  2. //如果客户端取消了请求的时候就会触发,而且只会第一次取消的时候触发  
  3. connect事件  
  4. //函数签名为:function (response, socket, head) { },每次服务器通过CONNECT方法来回应客户端的时候触发。如果客户端没有监听这个事件那么当接收到服务器的CONNECT方法的时候就会关闭连接。  
  5. continue事件  
  6. //当服务器发送一个100 Continue的HTTP响应的时候触发,一般是因为客户端发送了Expect: 100-continue',这时候客户端应该发送消息体  
  7. response事件  
  8. //获取到请求的时候触发,而且只会触发一次,response对象是一个http.IncomingMessage.  
  9. socket事件  
  10. //如果这个request对象获取到一个socket的时候就会触发  
  11. upgrade事件  
  12. //如果服务器端通过发送一个upgrade响应客户端的请求的时候触发。如果客户端没有监听这个事件那么当客户端收到一个upgrade请求头的时候那么连接就会关闭  

 

下面展示如何通过http.ClientRequest类和http.Server类实现协议升级:(具体代码见github地址)

 

[javascript] view plain copy
 
  1. const http = require('http');  
  2. // Create an HTTP server  
  3. var srv = http.createServer( (req, res) => {  
  4.   res.writeHead(200, {'Content-Type': 'text/plain'});  
  5.   res.end('okay');  
  6. });  
  7. //服务器监听upgrade事件  
  8. srv.on('upgrade', (req, socket, head) => {  
  9.   socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +  
  10.                'Upgrade: WebSocket\r\n' +  
  11.                'Connection: Upgrade\r\n' +  
  12.                '\r\n');  
  13.   //发送消息到服务器端  
  14.   socket.pipe(socket); // echo back  
  15. });  
  16.   
  17. // now that server is running  
  18. srv.listen(7777, '127.0.0.1', () => {  
  19.   // make a request  
  20.   var options = {  
  21.     port: 7777,  
  22.     hostname: '127.0.0.1',  
  23.     headers: {  
  24.       'Connection': 'Upgrade',  
  25.       'Upgrade': 'websocket'  
  26.     }  
  27.   };  
  28.   //如果客户端有一个请求到来了,那么马上向服务器发送一个upgrade请求  
  29.   var req = http.request(options);  
  30.   req.end();  
  31.   //客户端接收到upgrade事件,要清楚的知道升级协议不代表上面的http.createServer回调会执行,这个回调函数只有当浏览器访问的时候才会触发  
  32.   req.on('upgrade', (res, socket, upgradeHead) => {  
  33.     console.log('got upgraded!');  
  34.     socket.end();  
  35.     process.exit(0);  
  36.   });  
  37. });  

如何从http.ClientReqeust对象发送一个CONNECT请求:

 

[javascript] view plain copy
 
  1. const http = require('http');  
  2. const net = require('net');  
  3. const url = require('url');  
  4. //如果在connect事件和reques事件中都打印log,那么就会发现request在connect之前  
  5. // Create an HTTP tunneling proxy  
  6. var proxy = http.createServer( (req, res) => {  
  7.   res.writeHead(200, {'Content-Type': 'text/plain'});  
  8.   res.end('okay');  
  9. });  
  10. //http.Server的connect事件,Emitted each time a client requests a http CONNECT method  
  11. proxy.on('connect', (req, cltSocket, head) => {  
  12.   // connect to an origin server  
  13.   var srvUrl = url.parse(`http://${req.url}`);  
  14.   //获取到请求的URL地址,也就是req.url请求地址  
  15.   //console.log(srvUrl);  
  16.   //这个对象具有如下的函数签名方法:  
  17.   /* 
  18.   Url { 
  19.     protocol: 'http:', 
  20.     slashes: true, 
  21.     auth: null, 
  22.     host: 'www.google.com:80', 
  23.     port: '80', 
  24.     hostname: 'www.google.com', 
  25.     hash: null, 
  26.     search: null, 
  27.     query: null, 
  28.     pathname: '/', 
  29.     path: '/', 
  30.     href: 'http://www.google.com:80/' } 
  31.   */  
  32.   //net.connect方法的返回类型是一个net.Socket类型,同时连接到特定的url  
  33.   var srvSocket = net.connect(srvUrl.port, srvUrl.hostname, () => {  
  34.     //在http.Server对象的connect事件中第二个参数为一个socket,连接客户端和服务器端  
  35.     cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +  
  36.                     'Proxy-agent: Node.js-Proxy\r\n' +  
  37.                     '\r\n');  
  38.     //利用双方的socket用于发送消息  
  39.     srvSocket.write(head);  
  40.     //这个也是一个net.Socket类型  
  41.     srvSocket.pipe(cltSocket);  
  42.     //readable.pipe(destination[, options])  
  43.     cltSocket.pipe(srvSocket);  
  44.   });  
  45. });  
  46.   
  47. // now that proxy is running  
  48. //http.Server开始启动了  
  49. proxy.listen(9999, 'localhost', () => {  
  50.   // make a request to a tunneling proxy  
  51.   var options = {  
  52.     port: 9999,  
  53.     hostname: 'localhost',  
  54.     method: 'CONNECT',//客户端请求一个http的CONNECT方法  
  55.     path: 'www.google.com:80'  
  56.   };  
  57.   var req = http.request(options);  
  58.   req.end();  
  59.   //客户端请求完毕服务器的CONNECT事件,这时候服务器的connect事件被触发  
  60.   req.on('connect', (res, socket, head) => {  
  61.     console.log('got connected!');  
  62.     // make a request over an HTTP tunnel  
  63.     //每次服务器通过CONNECT方法来回应客户端的时候触发  
  64.     socket.write('GET / HTTP/1.1\r\n' +  
  65.                  'Host: www.google.com:80\r\n' +  
  66.                  'Connection: close\r\n' +  
  67.                  '\r\n');  
  68.     socket.on('data', (chunk) => {  
  69.       console.log(chunk.toString());  
  70.     });  
  71.     socket.on('end', () => {  
  72.       proxy.close();  
  73.     });  
  74.   });  
  75. });  
问题4:http.Server类有哪些事件和方法?

 

 

[javascript] view plain copy
 
  1. //这个类继承与net.Server,同时又有以下事件  
  2. checkContinue事件:  
  3. //当客户端发送Expect:100-continue的时候触发,如果服务器没有监听那么就会自动发送一个100 Continue的头表示接受数据上传。这个方法处理的时候可以调用response.writeContinue方法表示客户端应该继续发送请求体,当然也可以产生一个合理的HTTP响应,如'400 Bad Request',这时候服务器不能再次发送数据体了。注意:如果这个事件触发并且被处理了,这时候'request'事件就会被忽略  
  4. clientError事件:  
  5. //如果客户端触发一个'error'事件,这时候这个事件就会触发。函数签名为function (exception, socket) { },其中socket是一个net.socket对象,也是产生错误的地方  
  6. close事件:  
  7. //当服务器调用close方法的时候触发,如server.close方法停止接受新的连接,当所有的连接都断开的时候就会触发该事件。当然可以传入一个回调函数  
  8. connect事件:  
  9. //函数签名为function (request, socket, head) { },当客户端请求一个服务器的CONNECT事件的时候触发,如果这个方法没有监听那么通过CONNECT方法来请求就会关闭连接。request对象是一个http request对象;socket是客户端和服务器端的socket;head是一个Buffer对象,the first packet of the tunneling stream,可能是空对象。注意:如果这个事件被触发了,那么request对象就没有'data'事件监听函数了,因此你需要自己处理发送过来的数据  
  10. //http.Server的connect事件,Emitted each time a client requests a http CONNECT method  
  11. proxy.on('connect', (req, cltSocket, head) => {  
  12.   // connect to an origin server  
  13.   var srvUrl = url.parse(`http://${req.url}`);  
  14.   //获取到请求的URL地址,也就是req.url请求地址  
  15.   //console.log(srvUrl);  
  16.   //这个对象具有如下的函数签名方法:  
  17.   //net.connect方法的返回类型是一个net.Socket类型,同时连接到特定的url  
  18.   var srvSocket = net.connect(srvUrl.port, srvUrl.hostname, () => {  
  19.     //在http.Server对象的connect事件中第二个参数为一个socket,连接客户端和服务器端  
  20.     cltSocket.write('HTTP/1.1 200 Connection Established\r\n' +  
  21.                     'Proxy-agent: Node.js-Proxy\r\n' +  
  22.                     '\r\n');  
  23.     //利用双方的socket用于发送消息  
  24.     srvSocket.write(head);  
  25.     //这个也是一个net.Socket类型  
  26.     srvSocket.pipe(cltSocket);  
  27.     //readable.pipe(destination[, options])  
  28.     cltSocket.pipe(srvSocket);  
  29.   });  
  30. });  
  31. connection事件:  
  32. //一个新的TCP连接被建立时候触发,socket就是一个net.Socket类型,一般情况下用户不需要处理这个事件,这个socket不会触发'readable'事件,同时你可以通过request.connection来访问这个socket。函数签名为function (socket) { }  
  33. request事件:  
  34. //function (request, response) { }函数签名,当请求发动到服务器,解析出HTTP头的时候触发。每次获取到请求的时候触发。注意:一个连接可能有多个请求,如果设置了keep-alive时候就是这样。reqest是IncomingMesage,而response是一个http.ServerResponse对象  
  35. upgrade事件:  
  36. //函数签名为function (request, socket, head) { },head是一个Buffer对象可能为空。这时候socket不会有'data'监听函数,因此你需要自己处理发送过来的数据  
  37.  //服务器监听upgrade事件  
  38.     srv.on('upgrade', (req, socket, head) => {  
  39.       socket.write('HTTP/1.1 101 Web Socket Protocol Handshake\r\n' +  
  40.                    'Upgrade: WebSocket\r\n' +  
  41.                    'Connection: Upgrade\r\n' +  
  42.                    '\r\n');  
  43.       //发送消息到服务器端  
  44.       socket.pipe(socket); // echo back  
  45.     });  
  46.   
  47. server.close([callback])方法:  
  48. //阻止服务器接收新的连接,set.Server.close方法  
  49. server.maxHeadersCount  
  50. //限制到来消息的最大的HTTP头数量,默认为1000,设置为0表示没有限制  
  51. server.setTimeout(msecs, callback)  
  52. //在服务器对象上面监听一个timeout事件。默认情况下服务器超时为2min,如果超时了socket就会自动销毁。如果你自己设置了一个'timeout'事件,那么就要自己处理socket的超时请求  
  53. server.timeout  
  54. //设置这个参数只会影响新的连接,不会影响已经存在的连接  
问题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方法,如下例:

 

[javascript] view plain copy
 
  1. http.get('http://www.google.com/index.html', (res) => {  
  2.   console.log(`Got response: ${res.statusCode}`);  
  3.   // consume response body  
  4.   //消费从服务器端发送过来的数据  
  5.   res.resume();  
  6. }).on('error', (e) => {  
  7.   console.log(`Got error: ${e.message}`);  
  8. });  
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池的时候可以使用下面的方法:
[javascript] view plain copy
 
  1. http.get(options, (res) => {  
  2.   // Do stuff  
  3. }).on('socket', (socket) => {  
  4.   socket.emit('agentRemove');  
  5. });  
但是,如果你需要把所有的连接都不让Agent池来管理可以把agent:false
[javascript] view plain copy
 
  1. http.get({  
  2.   hostname: 'localhost',  
  3.   port: 80,  
  4.   path: '/',  
  5.   agent: false  // create a new agent just for this one request  
  6. }, (res) => {  
  7.   // Do stuff with response  
  8. })  
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对象所有值都是默认值的
 
[javascript] view plain copy
 
  1. const http = require('http');  
  2. var keepAliveAgent = new http.Agent({ keepAlive: true });  
  3. options.agent = keepAliveAgent;  
  4. http.request(options, onResponseCallback);  
Agent对象有如下一系列的方法:
[javascript] view plain copy
 
  1. agent.destroy()  
  2. //销毁agent当前正在使用的socket。一般情况没有必要调用,如果你使用了Agent对象并把keepAlive设置为true的时候,当你明确知道以后不需要使用这个socket的时候就需要显示的销毁。否则,这个socket长时间等待服务器销毁  
  3. agent.freeSockets  
  4. //表示HTTP的keepAlive被使用的时候,这个选项表示当前Agent等待使用的socket,是一个socket数组  
  5. agent.getName(options)  
  6. //获取一个唯一的值,这个值是在reques对象的options中设置的,使用这个值决定这个连接是否可以重用。在http Agent中返回host:port:localAddress,在https Agent,包含CA, cert, ciphers, and other HTTPS/TLS-specific options that determine socket reusability.  
  7. agent.maxFreeSockets  
  8. //默认为256.如果这个agent支持KeepAlive,那个这个值表示处于空闲状态的最大的socket  
  9. agent.maxSockets  
  10. //默认为Infinity,用于指定agent可以在一个域下面提供的最大的并发的socket数量。origion可以是'host:port' or 'host:port:localAddress' combination.  
  11. agent.requests  
  12. //用于获取一个请求数组,这个数组中保存的是请求对象,表示还没有发送也就是在等待的请求的数量。不要修改他!  
  13. agent.sockets  
  14. //用于指定当前正在被使用的Agent中的sockets数组,不要修改这个值  
注意:这里突然想到了为什么浏览器需要限制同域名下并发的请求的数量。原因主要有几个:第一个是为了保护服务器端,因为服务器并发处理的请求的数量也是有限制的,是客户端和服务器端一种默契的配合;第二个原因是TCP连接需要一个端口号,而端口号的数量最多是65536,而很多端口号都被系统的其他内置进程占用,而创建TCP连接需要消耗系统资源,因此这种方法可以保护操作系统的 TCP\IP 协议栈资源不被迅速耗尽,因此浏览器不好发出太多的 TCP 连接,而是采取用完了之后再重复利用 TCP 连接或者干脆重新建立 TCP 连接的方法;第三个原因是:创建TCP/IP需要一定的系统资源,而且操作系统在调用进程的时候需要切换上下文,因此也会限制浏览器并发的数量,这是操作系统一种自我保护措施!
注意:半开连接指的是 TCP 连接的一种状态,当客户端向服务器端发出一个 TCP 连接请求,在客户端还没收到服务器端的回应并发回一个确认的数据包时,这个 TCP 连接就是一个半开连接。若服务器到超时以后仍无响应,那么这个 TCP 连接就等于白费了,所以操作系统会本能的保护自己,限制 TCP 半开连接的总个数,以免有限的内核态内存空间被维护 TCP 连接所需的内存所浪费。
posted @ 2017-10-24 12:15  文学少女  阅读(866)  评论(0编辑  收藏  举报