一次完整的HTTP请求

一次完整的HTTP请求

  1. 一次性完整的HTTP请求过程
    1
    2
    3
    4
    5
    6
    1.域名解析 =>
    2.发起TCP的3次握手 =>
    3.发起HTTP请求 =>
    4.服务器响应HTTP请求,浏览器得到HTML代码 =>
    5.浏览器解析HTML代码,并请求HTML代码中的资源(如js,css,图片等)
    6.浏览器对页面进行渲染呈现给用户

详解请求过程

  1. 域名解析

    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文件还没有解析成功,那么就宣告这次解析失败,那就无法跟目标计算机进行通信.

    只要这八步中有一步可以解析成功,那就可以成功和目标计算机进行通信
  2. 发起TCP的三次握手

    1
    浏览器拿到域名对应的IP地址之后,浏览器User-Agent会以一个随机端口(1024< 端口 <65535)向服务器的WEB程序(httpd,nginx,IIS)80端口发起TCP的连接请求.这个连接请求(原始的http请求经过TCP/IP4层模型的层层封包)到达服务器端后(这中间通过各种路由设备,局域网内除外),进入到网卡,然后是进入到内核的TCP/IP协议栈(用于识别该连接请求,解封包,一层一层的剥开),还有可能要经过Netfilter防火墙(属于内核的模块)的过滤,最终到达WEB程序(本文就以Nginx为例),最终建立了TCP/IP的连接

TCP三次握手

1
2
3
4
5
1.客户端首先发送一个连接试探,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请求了

  1. 建立TCP连接后发起HTTP请求

    1
    进过TCP3次握手之后,浏览器发起了http的请求,使用的http的方法GET方法,请求的URL是/,协议是HTTP/1.0
  2. 服务器端响应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的完整内容
  3. 浏览器解析html代码,并请求html代码中的资源

    1
    2
    3
    浏览器拿到index.html文件后,就开始解析其中的html代码,遇到js/css/image等静态资源时,就向服务器端去请求下载(会使用多线程下载,每个浏览器的线程数不一样),这个时候就用上keep-alive特性了,建立一次HTTP连接,可以请求多个资源,下载资源的顺序就是按照代码里的顺序,但是由于每个资源大小不一样,而浏览器又多线程请求请求资源,所以从下图看出,这里显示的顺序并不一定是代码里面的顺序

    浏览器在请求静态资源时(在未过期的情况下),向服务器端发起一个http请求(询问自从上一次修改时间到现在有没有对资源进行修改),如果服务器端返回304状态码(告诉浏览器服务器端没有修改),那么浏览器会直接读取本地的该资源的缓存文件
  4. 浏览器对页面进行渲染呈现给用户

    1
    2
    最后,浏览器利用自己内部的工作机制,把请求到的静态资源和HTML代码进行渲染,渲染之后呈现给用户
    自此一次完整的HTTP事务宣告完成