前言也许你在面试的时候遇到过这样一个问题:从输入URL到浏览器显示页面到底发生了什么?简单的回答是:DNS解析TCP建立连接,发送HTTP请求给服务器处理请求。如果有缓存,直接读取缓存。如果没有缓存,则返回响应内容。TCP断开连接。浏览器解析并呈现页面。.网络基础在此之前,先了解一下TCP/IP的基础知识。TCP/IP参考模型早期的TCP/IP模型是四层结构,从下到上分别是网络接口层、Internet层、传输层和应用层。后来,网络接口层又分为物理层和数据链路层。应用层(Application)提供网络与用户应用软件之间的接口服务。传输层(Transimission)提供建立、维护和取消传输连接的功能,负责数据(PC)的可靠传输。传输层有两种不同的协议:TCP(传输控制协议)和UDP(用户数据报协议)网络层(Network)处理网络间路由以保证数据及时传输(Router)数据链路层(DataLink)负责错误-自由传输数据、确认帧、发送错误和重传等(交换机)物理层(Physics)提供机械、电气、功能和过程特性(网卡、网线、双绞线、同轴电缆、中继器)常用协议在每层这里可以看到HTTP协议是建立在TCP之上的,它属于应用层协议。具体过程1、DNS解析DNS服务与HTTP协议一样,是一个位于应用层的协议,提供从域名到IP地址的解析服务。获取到IP地址后,就可以建立连接了。这里有两点要知道:持久连接持久连接(也称为HTTPkeep-alive)的特点是只要任何一个段没有提出断开连接,就会保持TCP连接状态。建立流水线持久连接后,就可以使用流水线发送,可以并发发送多个请求,而不用等待一个接一个的响应。(这里想到了stream的pipe方法。)2.TCP连接与断开2.1TCP报文格式粗略地说:计算机通过端口号识别访问哪个服务,如http;源端口号是一个随机端口,目的端口决定了哪个程序接收数据序号和确认序号为保证传输数据的完整性和顺序,需要注意的是TCP的连接、传输和断开是全部由六个控制位命令(如三向握手和四向握手)。PSH(pushurgentbit)缓冲区会满,速度TransmissionRST(reset重置位)connectionbrokenreconnectURG(urgent紧急位)紧急信号ACK(acknowledgementconfirmation)为1表示确认号SYN(同步连接建立)同步序列号位当TCP建立连接时,将该值设置为1用户数据存储在应用层产生的HTTP报文中。了解了这些之后,我们就开始关注2.2TCP三向握手和四向握手三向握手。客户端首先向服务器发送一个带有SYN标志的数据包。发送一个带有SYN/ACK标志的数据包,确认客户端收到后再发送一个带有SYN/ACK标志的数据包,表示握手结束,挥手四次。客户端向服务器发送FIN消息。服务器收到后,回复一个ACK响应,服务器也发送一个FIN报文段给客户端,然后在服务器端关闭连接。客户端收到后,向服务器回复一个ACK响应。等待是确认服务器已经正常关闭)不需要四次挥手。当服务端没有内容要发送给客户端时,会直接发送FIN段,变成三挥手。3、HTTP请求/响应3.1HTTP消息HTTP消息大致可以分为消息头和消息体两部分,两者之间用空行分隔(相当于用两个换行符rnrn)。消息正文不是强制性的。3.1.1请求消息常用请求行方法:GET获取资源POST向服务器发送数据,传递实体主体PUT传递文件HEAD获取消息头DELETE删除文件OPTIONS查询支持的方法TRACE跟踪路径3.1.2响应消息说明说到响应消息,就要说一下状态码:2XX成功200(OK)客户端发送的数据处理正常204(NotContent)正常响应,无实体206(PartialContent)范围请求,返回部分数据,响应报文中Content-Range指定实体的内容3XXRedirection301(MovedPermanently)永久重定向302(Found)临时重定向,规范要求方法名不变,但是将被改变303(SeeOther)类似于302,但必须使用GET方法304(NotModified)状态未改变合作(If-Match,If-Modified-Since,If-None_Match,If-Range,If-Unmodified-Since)(通常缓存会返回一个304状态码)4XX客户端错误400(BadRequest)请求消息语法错误401(unauthorized)Authenticationrequired403(Forbidden)服务器拒绝访问相应资源404(NotFound)resourcecouldnotbefoundontheserver5XXServer-sideerror500(InternalServerError)服务器故障503(ServiceUnavailable)服务器过载或正在关闭维护3.1.3HeaderGeneralheaderheaderfieldnamedescriptionCache-ControlControlcachebehaviorConnectionManagementDatemessagedatePragmamessageinstructionTrailer消息末尾的headerTrasfer-Encoding指定消息正文的传输编码方式Upgrade升级到其他协议ViaProxyserver信息Warning错误通知RequestheaderHeader字段名称描述接受用户代理可以处理媒体TypeAccept-CharsetPreferredCharacterSetAccept-EncodingPreferredEncodingAccept-LangulagePreferredLanguageAuthorizationWebAuthenticationInformationExpectSpecificBehaviorFromServerUser'sE-mailAddressHostServerWhereRequestedResourcesIf-MatchCompareEntityTagsIf-Modified-Since比较资源updatetimeIf-None-MatchCompareentityflagsIf-Range资源未更新时发送实体字节范围请求If-Unmodified-Since比较资源更新时间(与If-Modified-Since相反)AuthorizationProxyserver需要客户端认证RangeEntity字节范围RequestReferer请求中URI的原始获取者TE传输编码优先级User-AgentHTTP客户端程序信息ResponseheaderHeader字段名描述Accept-Ranges是否接受字节范围Age资源创建时间ETag资源匹配信息Location客户端重定向到指定URIProxy-Authenticate代理服务器对客户端的认证信息Retry-After再次发送请求的时机Server服务器信息Vary代理服务器缓存的管理信息www-Authenticate认证实体头从服务器到客户端Header字段名称DescriptionAllow资源支持的HTTP方法Content-Encoding实体的编码方式Content-Language实体的自然语言Content-Length实体的内容大小(以字节为单位)Content-Location对应资源的替换URIContent-MD5实体的消息摘要Content-Range实体的位置范围Content-Type实体的媒体类型bodyExpires实体过期时间Last-Modified资源最后修改时间)=>{//req为可读流/res为可写流//获取请求消息信息letmethod=req.method;//methodlethttpVersion=req.httpVersion;//HTTP版本leturl=req.网址;让headers=req.headers;console.log(方法,httpVersion,url,标题);//获取请求体(如果请求体中的数据大于64k,会多次触发数据事件)letbuffers=[];req.on('data',data=>{buffers.push(data);})req.on('end',()=>{console.log(Buffer.concat(buffers).toString());res.write('hello');res.end('world');})})//监听服务器事件app.on('connection',socket=>{console.log('connection');});app.on('关闭',()=>{console.log('serverdown')});app.on('error',err=>{console.log(err);});app.listen(3000,()=>{console.log('服务器在端口3000上启动');});createclientlethttp=require('http');letoptions={hostname:'localhost',port:3000,path:'/',method:'GET',//设置实体头告诉服务器是什么类型我现在想发送给你的数据headers:{'content-Type':'application/x-www-form-urlencoded','Content-Length':15}}letreq=http.request(options);req.on('response',res=>{res.on('data',chunk=>{console.log(chunk.toString());});});req.end('name=js&&age=22')然后使用节点来运行我们的客户端。说了这么多,大家可能对从输入URL到浏览器显示页面的过程有一个大致的了解。不用说了,我们再来看一下缓存4。缓存4.1缓存功能减少了冗余数据传输,节省了网络费用。减轻了服务器的负担,大大提高了网站的性能,加快了客户端加载网页的速度。4.2缓存分类强制缓存:说白了就是在第一次请求数据的时候,服务器会把数据和缓存规则一起返回。请求时,浏览器直接根据缓存规则进行判断,无需连接服务器,直接读取缓存数据库;如果没有,它会再次进入服务器。比较缓存比较缓存,顾名思义,就是要进行比较,以确定缓存是否可以使用。当浏览器第一次请求数据时,服务器会将缓存标识和数据返回给客户端,客户端将它们备份到缓存数据库中。客户端再次请求数据时,将备份的缓存标识发送给服务器,服务器根据缓存标识进行判断。判断成功后,返回一个304状态码,通知*客户端比较成功,可以使用缓存数据了。4.3请求流程第一个请求此时不缓存。从上图可以看出第二个请求。判断缓存是否可用有两种方式。ETag是entitytag的缩写,是根据实体内容生成的哈希字符串,它可以识别资源的状态。当资源发生变化时,ETag也会随之变化。ETag由Web服务器生成并发送给浏览器客户端。Last-Modified是该资源的最后修改时间。如果客户端在请求资源的实体头中发现了Last-Modified语句,客户端再次请求时,会在头中添加if-Modified-Since字段。服务器收到请求后,发现将if-Modified-Since字段与请求资源的最后修改时间进行比较。说了这么多还是直接实现缓存通过最后修改时间判断缓存是否可用比较好lethttp=require('http');leturl=require('url');letpath=require('path');letfs=require('fs');letmime=require('mime');letapp=http.createServer((req,res)=>{//获取客户端请求的文件路径根据urllet{parsename}=url.parse(req.url);letp=path.join(__dirname,'public','.'+pathname);//fs.stat()用于读取文件信息,文件的最后修改时间是stat.ctimefs.stat(p,(err,stat)=>{if(!err){letsince=req.headers['if-modified-since'];//客户端发送的文件最后修改时间if(since){if(since===stat.ctime.toUTCString()){//最后修改时间相等,读缓存res.statusCode=304;res.end();}else{sendFile(req,res,p,stat);//最后修改时间不相等,返回新内容}}else{sendError(资源);}}})})functionsendError(res){res.statusCode=404;res.end();}functionsendFile(req,res,p,stat){res.setHeader('Cache-Control','no-cache');//设置公共头字段来控制缓存行为res.setHeader('Last-Modified',stat.ctime.toUTCString());//实体头域资源的最后修改时间res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')fs.createReadStream(p).pipe(res);}app.listen(3000,()=>{console.log('服务器在端口3000上启动');});lastmodificationtime存在的问题:1.部分服务器无法准确获取文件的lastmodificationtime,因此无法判断文件到lastmodificationtime是否已经更新2.部分文件的修改非常频繁,并且修改在不到一秒钟的时间内完成。Last-Modified只能精确到秒。3、部分文件的最后修改时间发生了变化,但内容没有发生变化。我们不希望客户认为此文件已被修改。4、如果同一个文件位于多个CDN服务器上,虽然内容相同,但修改时间不同。使用ETag判断缓存是否可用。ETag基于文件的内容。说白了就是用MD5(md5不叫加密算法,是不可逆的,应该叫摘要算法)来生成信息摘要,摘要是用来做比对的。让http=require('http');让url=require('url');让path=require('path');让fs=require('fs');让mime=require('mime');//crypto是node.js中实现加解密的模块。详细解释请自行理解letcrypto=require('crypto');letapp=http.createServer((req,res)=>{//根据url获取客户端要请求的文件路径let{parsename}=url.parse(req.url);letp=path.join(__dirname,'public','.'+pathname);//fs.stat()用于读取获取文件信息,文件最后修改时间为stat.ctimefs.stat(p,(err,stat)=>{letmd5=crypto.createHash('md5');//创建md5对象letrs=fs.createReadStream(p);rs.on('data',function(data){md5.update(data);});rs.on('end',()=>{letr=md5.digest('hex');//用md5加密文件//下次将最新文件的加密值与客户端请求进行比较letifNoneMatch=req.headers['if-none-match'];if(ifNoneMatch){if(ifNoneMatch===r){res.statusCode=304;res.end();}else{sendFile(req,res,p,r);}}else{sendFile(req,res,p,r);}});})});函数sendError(res){res.状态码=404;res.end();}functionsendFile(req,res,p,stat){res.setHeader('Cache-Control','no-cache');//设置通用头域来控制缓存行为res.setHeader('Etag',r);//响应头字段资源匹配信息res.setHeader('Content-Type',mime.getType(p)+';charset=utf8')fs.createReadStream(p).pipe(资源);}app.listen(3000,()=>{console.log('服务器在3000端口启动');});最后,对于想深入了解http的同学,推荐一本书《图解HTTP》本人水平有限,有不足之处,还请大家指出指正。
