问题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(WindowsNT6.1)AppleWebKit/537.36(KHTML,like
- Gecko)Chrome/49.0.2623.110Safari/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(WindowsNT6.1)AppleWebKit/537.36(KHTML,likeGecko)Chrome
- /49.0.2623.110Safari/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(WindowsNT6.1)AppleWebKit/537.36(KHTML,like
- Gecko)Chrome/49.0.2623.110Safari/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(WindowsNT6.1)AppleWebKit/537.36(KHTML,likeGecko)Chrome
- /49.0.2623.110Safari/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){
- varbuffers=[];
- req.on(‘data‘,function(trunk){
- buffers.push(trunk);
- }).on(‘end‘,function(){
- varbuffer=Buffer.concat(buffers);
- //获取到所有的数据了,而不仅仅是数据头
- res.end(buffer);
- //原样返回给客户端
- })
- }
下面两种情况会返回IncomingMessage:
第一种情况是服务器端的request对象
- varhttp=require(‘http‘);
- http.createServer(function(req,res){
- res.end("IncomingMessage="+(reqinstanceofhttp.IncomingMessage));
- //打印true
- }).listen(1337);
注意:服务器的request事件回调函数第一个参数为http.IncomingMessage对象
第二种情况就是客户端的res对象
- varhttp=require(‘http‘);
- varoptions={
- hostname:‘localhost‘,
- port:1337,
- path:‘/‘,
- method:‘GET‘
- };
- varreq=http.request(options,function(res){
- //获取到服务器的返回数据
- res.on(‘data‘,function(chunk){
- //这里的chunk是Buffer对象,这一点一定要注意是编码的数据
- console.log(resinstanceofhttp.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
服务器端的代码:
- varhttp=require(‘http‘);
- http.createServer(function(req,res){
- varauth=req.headers[‘authorization‘]||‘‘;
- //这里获取到的是一个数组BasiccWlubGlhbmc6MTIz,其中第一个部分是一定的,第二部分为newBuffer("qinliang:123").toString(‘base64‘)
- varparts=auth.split(‘‘);
- varmethod=parts[0];//Basic
- varencoded=parts[1];//秘钥
- vardecoder=newBuffer(encoded,‘base64‘).toString(‘utf-8‘).split(":");
- //解密后获取原始值
- varuser=decoder[0];
- //用户名
- varpass=decoder[1];
- //密码,到了这一步就获取到了客户端的用户名和密码了
- if(!user==‘qinliang‘&&!pass==‘123‘){
- res.setHeader(‘WWW-Authenticate‘,‘Basicrealm="SecureArea"‘);
- res.writeHead(401);
- //未授权
- res.end();
- }else{
- res.end(‘Youareforbiddentoenter!‘);
- }
- }).listen(8888,‘localhost‘);
客户端的代码:
- varhttp=require(‘http‘);
- varencode=function(username,password){
- returnnewBuffer(username+":"+password).toString(‘base64‘);
- }
- varoptions={
- hostname:‘localhost‘,
- port:8888,
- path:‘/‘,
- method:‘GET‘,
- //auth:encode(‘qinliang‘,123)
- auth:"qinliang:123"
- //用户名为qinliang,密码为123
- };
- varreq=http.request(options,function(res){
- //获取到服务器的返回数据
- res.on(‘data‘,function(chunk){
- //这里的chunk是Buffer对象,这一点一定要注意是编码的数据
- console.log(resinstanceofhttp.IncomingMessage);
- //这里打印true
- })
- })
- req.end();
- //必须调用,否则客户端不会发送请求到服务器
agent:用于控制Agent行为。如果指定了agent那么请求就变成Connect:keep-alive了。可以指定他的值为undefined(这时候这个值就是http.globalAgent);也可以指定为Agent对象;第三个值为false,从连接池中拿到一个Agent,默认请求为Connection:close。其中回调函数callback是作为response事件的默认事件处理函数。这个方法返回的是一个http.ClientRequest类,这个类是一个可写的流,如果需要用Post请求来上传文件,这时候就需要写ClientRequest对象。下面是用这个对象进行上传文件的操作:
- varhttp=require(‘http‘);
- varquerystring=require(‘querystring‘);
- varpostData=querystring.stringify({
- ‘msg‘:‘HelloWorld!‘
- });
- varoptions={
- 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
- varreq=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(‘Nomoredatainresponse.‘)
- })
- });
- //如果DNS解析错误,TCP错误,HTTP解析错误就会触发error事件
- req.on(‘error‘,(e)=>{
- console.log(`problemwithrequest:${e.message}`);
- });
- //writedatatorequestbody
- //把数据写入到请求体中
- 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:‘BasiccWlubGlhbmc6MTIz‘},
- _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事件
- //当服务器发送一个100Continue的HTTP响应的时候触发,一般是因为客户端发送了Expect:100-continue‘,这时候客户端应该发送消息体
- response事件
- //获取到请求的时候触发,而且只会触发一次,response对象是一个http.IncomingMessage.
- socket事件
- //如果这个request对象获取到一个socket的时候就会触发
- upgrade事件
- //如果服务器端通过发送一个upgrade响应客户端的请求的时候触发。如果客户端没有监听这个事件那么当客户端收到一个upgrade请求头的时候那么连接就会关闭
下面展示如何通过http.ClientRequest类和http.Server类实现协议升级:(具体代码见github地址)
- consthttp=require(‘http‘);
- //CreateanHTTPserver
- varsrv=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.1101WebSocketProtocolHandshake\r\n‘+
- ‘Upgrade:WebSocket\r\n‘+
- ‘Connection:Upgrade\r\n‘+
- ‘\r\n‘);
- //发送消息到服务器端
- socket.pipe(socket);//echoback
- });
- //nowthatserverisrunning
- srv.listen(7777,‘127.0.0.1‘,()=>{
- //makearequest
- varoptions={
- port:7777,
- hostname:‘127.0.0.1‘,
- headers:{
- ‘Connection‘:‘Upgrade‘,
- ‘Upgrade‘:‘websocket‘
- }
- };
- //如果客户端有一个请求到来了,那么马上向服务器发送一个upgrade请求
- varreq=http.request(options);
- req.end();
- //客户端接收到upgrade事件,要清楚的知道升级协议不代表上面的http.createServer回调会执行,这个回调函数只有当浏览器访问的时候才会触发
- req.on(‘upgrade‘,(res,socket,upgradeHead)=>{
- console.log(‘gotupgraded!‘);
- socket.end();
- process.exit(0);
- });
- });
如何从http.ClientReqeust对象发送一个CONNECT请求:
- consthttp=require(‘http‘);
- constnet=require(‘net‘);
- consturl=require(‘url‘);
- //如果在connect事件和reques事件中都打印log,那么就会发现request在connect之前
- //CreateanHTTPtunnelingproxy
- varproxy=http.createServer((req,res)=>{
- res.writeHead(200,{‘Content-Type‘:‘text/plain‘});
- res.end(‘okay‘);
- });
- //http.Server的connect事件,EmittedeachtimeaclientrequestsahttpCONNECTmethod
- proxy.on(‘connect‘,(req,cltSocket,head)=>{
- //connecttoanoriginserver
- varsrvUrl=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
- varsrvSocket=net.connect(srvUrl.port,srvUrl.hostname,()=>{
- //在http.Server对象的connect事件中第二个参数为一个socket,连接客户端和服务器端
- cltSocket.write(‘HTTP/1.1200ConnectionEstablished\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);
- });
- });
- //n