一次完整的HTTP请求
- 一次性完整的HTTP请求过程
1
2
3
4
5
61.域名解析 =>
2.发起TCP的3次握手 =>
3.发起HTTP请求 =>
4.服务器响应HTTP请求,浏览器得到HTML代码 =>
5.浏览器解析HTML代码,并请求HTML代码中的资源(如js,css,图片等)
6.浏览器对页面进行渲染呈现给用户
详解请求过程
域名解析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43在浏览器(Chrome)中输入域名"www.smallasa.com",敲回车键,进行域名解析为IP地址:
1.浏览器(Chrome)会首先搜索浏览器自身的DNS缓存
查看自身缓存中是否有"www.smallasa.com"对应的条目,而且没有过期.
如果有对应条目且没有过期,则解析到此结束
注意:浏览器缓存时间比较短,大概只有1分钟,且只能容纳1000条缓存
注意:浏览器(Chrome)自身缓存查看: chrome://net-internals/#dns
2.如果浏览器自身的缓存里面没有找到对应的条目
那么浏览器会搜索操作系统自身的DNS缓存
如果找到且没有过期,则停止搜索解析并到此结束.
注意:
ipconfig/displaydns windows查看dns缓存
nscg -d linux查看dns缓存(linux系统一般默认关闭,yum -y install nscg)
3.如果在系统自身的DNS缓存中也没有找到
那么尝试读取系统hosts文件,查找该域名对应的条目,如果有则解析成功
注意:
C:\Windows\System32\drivers\etc\hosts windows hosts文件位置
/etc/hosts linux hosts文件位置
4.如果在hosts文件中也没有找到对应的条目
浏览器就会发起一个DNS的系统调用,
该调用会向本地配置的首选DNS服务器发起域名解析请求(通过UDP协议向DNS的53端口发起请求,这个请求是递归的请求)
运营商的DNS服务器首先查找自身的缓存,找到对应的条目,且没有过期,则解析成功.
如果没有找到对应的条目,则由运营商的DNS代表我们的浏览器发起迭代DNS解析请求,它首先是会找根域的DNS的IP地址(这个DNS服务器都内置13台根域的DNS的IP地址),找到根域的DNS地址,就会向其发起请求(请问www.smallasa.com这个域名的IP地址是多少?),根域发现这是一个顶级域com域的一个域名,于是就告诉运营商的DNS我不知道这个域名的IP地址,但是我知道com域的IP地址,你去找它,于是运营商的DNS就得到了com域的IP地址,又向com域的IP地址发起了请求(请问www.smallasa.com这个域名的IP地址是多少?),com域这台服务器告诉运营商的DNS我不知道www.smallasa.com这个域名的IP地址,但是我知道smallasa.com这个域的DNS地址,你去找它,于是运营商的DNS又向smallasa.com这个域名的DNS地址发起请求(请问www.smallasa.com这个域名的IP地址是多少?),这个时候smallasa.com域的DNS服务器一查,果真在我这里,于是就把找到的结果发送给运营商的DNS服务器,这个时候运营商的DNS服务器就拿到了www.smallasa.com这个域名对应的IP地址,并返回给系统内核,内核又把结果返回给浏览器,终于浏览器拿到了www.smallasa.com对应的IP地址,该进行下一步TCP三次握手动作了
如果经过以上的4个步骤,还没有解析成功,那么会进行如下步骤:
(主要针对windows服务器)
5.操作系统就会查找NetBIOS name Cache(NetBIOS名称缓存,就存在客户端电脑中的).凡是最近一段时间内和我成功通讯的计算机的计算机名和Ip地址,都会存在这个缓存里面.什么情况下该步能解析成功呢?就是该名称正好是几分钟前和我成功通信过,那么这一步就可以成功解析.
6.如果NetBIOS name Cache解析不成功,那会查询WINS服务器(是NETBIOS名称和IP地址对应的服务器)
7.如果查询WINS服务器也没有查询成功,那么客户端就要进行广播查找
8.如果进行广播查找也没有成功,那么客户端就读取LMHOSTS文件(和HOSTS文件同一个目录下,写法也一样)
9.如果读取LMHOSTS文件还没有解析成功,那么就宣告这次解析失败,那就无法跟目标计算机进行通信.
只要这八步中有一步可以解析成功,那就可以成功和目标计算机进行通信发起TCP的三次握手
1
浏览器拿到域名对应的IP地址之后,浏览器User-Agent会以一个随机端口(1024< 端口 <65535)向服务器的WEB程序(httpd,nginx,IIS)80端口发起TCP的连接请求.这个连接请求(原始的http请求经过TCP/IP4层模型的层层封包)到达服务器端后(这中间通过各种路由设备,局域网内除外),进入到网卡,然后是进入到内核的TCP/IP协议栈(用于识别该连接请求,解封包,一层一层的剥开),还有可能要经过Netfilter防火墙(属于内核的模块)的过滤,最终到达WEB程序(本文就以Nginx为例),最终建立了TCP/IP的连接
1
2
3
4
51.客户端首先发送一个连接试探,ACK=0表示确认号无效,SYN=1表示这是一个连接请求或连接接受报文,同时表示这个数据报不能携带数据,seq=J表示客户端自己的初始序号(seq=0代表这是第0号包),这时候客户端进入SYN_SENT状态,表示客户端等待服务器的回复
2.服务端监听到连接请求报文后,如同意建立连接,则向Client发送确认.TCP报文首部中的SYN和ACK都置1,ack=J+1表示期望收到对方下一个报文段的第一个数据字节序号是J+1,同时表明J为止的所有数据都已正确收到(ack=1其实是ack=0+1,也就是期望客户端的第1个包),seq=K表示服务端自己的初始序号(seq=0代表这是服务器这边发出的第0号包).这时服务器进入SYN_REVD,表示服务器已经收到客户端的连接请求,等待客户端的确认
3.客户端收到确认后还需再次发送确认,同时携带要发送给服务端的数据.ACK=1表示确认号,ack=K+1代表期望收到服务器的第1个包,客户端自己的序号seq=J+1表示这就是我的第1个包,相对于第0个包来说的,一旦收到Client的确认之后,这个TCP连接就进入Established状态,就可以发起http请求了
建立TCP连接后发起HTTP请求
1
进过TCP3次握手之后,浏览器发起了http的请求,使用的http的方法GET方法,请求的URL是/,协议是HTTP/1.0
服务器端响应HTTP请求,浏览器得到HTML代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37服务器端WEB程序接收到HTTP请求以后,就开始处理该请求,处理之后就返回给浏览器html文件
假设服务器端使用nginx+PHP(fastcgi)架构提供服务:
1.nginx读取配置文件
我们在浏览器的地址栏里面输入的是"www.smallasa.com",其实完整的应该是"http://www.smallasa.com./",后面还有个点(这个点代表就是根域,一般情况下我们不用输入,也不显示),后面的"/"也是不用添加,浏览器会自动帮我们添加.那么实际请求的URL是"http://www.smallasa.com/",Nginx在收到浏览器"GET /"请求时,会读取http请求里面的头部信息,根据Host来匹配 自己的所有的虚拟主机的配置文件的server_name,看看有没有匹配的,有匹配那么就读取该虚拟主机的配置,发现如下配置:
root /web/echo
通过这个配置就知道所有网页文件的就在这个目录下,这个目录就是"/".当我们访问"http://www.smallasa.com/"时就是访问这个目录下面的文件.例如,"http://www.smallasa.com/index.html",那么代表"/web/echo"下面有个文件叫"index.html"
index index.html index.htm index.php
通过这个就能得知网站的首页文件是那个文件,也就是我们在输入"http://www.smallasa.com/",nginx就会自动帮我们把index.html加到后面,那么添加之后的URL是"/index.html",然后根据后面的配置进行处理.
注意:假设首页是index.php,当然是会尝试的去找到该文件,如果没有找到该文件就依次往下找,如果这3个文件都没有找到,那么就抛出一个404错误
location ~ .*\.php(\/.*)*$ {
root /web/echo;
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
astcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
这一段配置指明凡是请求的URL中匹配(这里是启用了正则表达式进行匹配)"*.php"后缀的,后面跟的参数的,都交给后端的fastcgi进程进行处理
把php文件交给fastcgi进程去处理:
于是nginx把"/index.php"这个URL交给了后端的fastcgi进程处理,等待fastcgi处理完成后(结合数据库查询出数据,填充模板生成html文件)返回给nginx一个index.html文档,Nginx再把这个index.html返回给浏览器,于是乎浏览器就拿到了首页的html代码,同时nginx写一条访问日志到日志文件中去
注1:nginx是怎么找index.php文件的?
当nginx发现需要"/web/echo/index.php"文件时,就会向内核发起IO系统调用(因为要跟硬件打交道,这里的硬件是指硬盘,通常需要靠内核来操作,而内核提供的这些功能是通过系统调用来实现的),告诉内核,我需要这个文件,内核从"/"开始找到web目录,再在web目录下找到echo目录,最后在echo目录下找到index.php文件,于是把这个index.php从硬盘上读取到内核自身的内存空间,然后再把这个文件复制到nginx进程所在的内存空间,于是乎nginx就得到了自己想要的文件了
注2:寻找文件在文件系统层面是怎么操作的?
每个分区(像ext3 ext3等文件系统,block块是文件存储的最小单元 默认是4096字节)都是包含元数据区和数据区,每一个文件在元数据区都有元数据条目(一般是128字节大小),每一个条目都有一个编号,我们称之为inode(index node 索引节点),这个inode里面包含:文件类型、权限、连接次数、属主和数组的ID、时间戳、这个文件占据了那些磁盘块也就是块的编号(block,每个文件可以占用多个block,并且block不一定是连续的,每个block是有编号的)还有一个要点:目录其实也普通是文件,也需要占用磁盘块,目录不是一个容器.你看默认创建的目录就是4096字节,也就说只需要占用一个磁盘块,但这是不确定的.所以要找到目录也是需要到元数据区里面找到对应的条目,只有找到对应的inode就可找到目录所占用的磁盘块
那么内核究竟是怎么找到index.php这个文件的呢?
内核拿到nginx的IO系统调用要获取/web/echo/index.php这个文件请求之后
① 内核读取元数据区 / 的inode,从inode里面读取/所对应的数据块的编号,然后在数据区找到其对应的块(1 2号块),读取1号块上的映射表找到web这个名称在元数据区对应的inode号
② 内核读取web对应的inode(3号),从中得知web在数据区对应的块是5号块,于是到数据区找到5号块,从中读取映射表,知道echo对应的inode是5号,于是到元数据区找到5号inode
③ 内核读取5号inode,得到echo在数据区对应的是11号块,于是到数据区读取11号块得到映射表,得到index.php对应的inode是9号
④ 内核到元数据区读取9号inode,得到index.php对应的是15和16号数据块,于是就到数据区域找到15 16号块,读取其中的内容,得到index.php的完整内容浏览器解析html代码,并请求html代码中的资源
1
2
3浏览器拿到index.html文件后,就开始解析其中的html代码,遇到js/css/image等静态资源时,就向服务器端去请求下载(会使用多线程下载,每个浏览器的线程数不一样),这个时候就用上keep-alive特性了,建立一次HTTP连接,可以请求多个资源,下载资源的顺序就是按照代码里的顺序,但是由于每个资源大小不一样,而浏览器又多线程请求请求资源,所以从下图看出,这里显示的顺序并不一定是代码里面的顺序
浏览器在请求静态资源时(在未过期的情况下),向服务器端发起一个http请求(询问自从上一次修改时间到现在有没有对资源进行修改),如果服务器端返回304状态码(告诉浏览器服务器端没有修改),那么浏览器会直接读取本地的该资源的缓存文件浏览器对页面进行渲染呈现给用户
1
2最后,浏览器利用自己内部的工作机制,把请求到的静态资源和HTML代码进行渲染,渲染之后呈现给用户
自此一次完整的HTTP事务宣告完成