HTTP通信机制是在一次完整的HTTP通信过程中,Web浏览器与Web服务器之间将完成下列7个步骤:
1. 建立TCP连接
在HTTP工作开始之前,Web浏览器首先要通过网络与Web服务器建立连接,该连接是通过TCP来完成的,该协议与IP协议共同构建Internet,即著名的TCP/IP协议族,因此Internet又被称作是TCP/IP网络。HTTP是比TCP更高层次的应用层协议,根据规则,只有低层协议建立之后才能进行更高层协议的连接,因此,首先要建立TCP连接,一般TCP连接的端口号是80。
2. Web浏览器向Web服务器发送请求命令
一旦建立了TCP连接,Web浏览器就会向Web服务器发送请求命令(HTTP请求报文的请求行)。例如:GET /sample/hello.jsp HTTP/1.1。
3. Web浏览器发送请求头信息
浏览器发送其请求命令之后,还要以头信息的形式向Web服务器发送一些别的信息(HTTP请求报文的请求头部),之后浏览器发送了一空白行来通知服务器,它已经结束了该头信息的发送。
4. Web服务器应答
客户机向服务器发出请求后,服务器会向客户机回送应答(HTTP响应报文的状态行), HTTP/1.1 200 OK ,应答的第一部分是协议的版本号和应答状态码。
5. Web服务器发送应答头信息
正如客户端会随同请求发送关于自身的信息一样,服务器也会随同应答向用户发送关于它自己的数据及被请求的文档(HTTP响应报文的响应头部)。
6. Web服务器向浏览器发送数据
Web服务器向浏览器发送头信息后,它会发送一个空白行来表示头信息的发送到此为结束,接着,它就以Content-Type应答头信息所描述的格式发送用户所请求的实际数据(HTTP响应报文的响应正文)。
7. Web服务器关闭TCP连接
一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码:
Connection:keep-alive
TCP连接在发送后将仍然保持打开状态,于是,浏览器可以继续通过相同的连接发送请求。保持连接节省了为每个请求建立新连接所需的时间,还节约了网络带宽。
------------------------------------------------------------------------------------------------
web页面由对象组成,一个对象只是一个文件,如一个html文件。一个JPEG图形,一个JAVA小程序,或一个视频片段。
URL由两部分组成,存放对象的服务器主机名和对象的路径名。
以在浏览器输入http://www.myApp.com:8000/mainPage/index.html为例,说明HTTP请求的过程。
1、域名解析点击打开链接
首先浏览器会解析www.myApp.com这个域名对应的ip地址
1) 搜索浏览器自身的DNS缓存(缓存时间比较短,大概只有1分钟,且只能容纳1000条缓存),看自身的DNS缓存中是否有www.myApp.com对应的条目,而且没有过期,如果有且没有过期,则解析到此结束。
2) 如果在浏览器自身的DNS缓存里没有找到,则搜索操作系统自身的DNS缓存,如果找到且没有过期,则解析到此结束。
3) 如果在操作系统自身的DNS缓存里也没有找到,则尝试读取hosts文件(位于C:\Windows\System32\drivers\etc),如果找到,则解析到此结束。
4) 如果在hosts文件里也没有找到,则会发起一个DNS的系统调用,根据本地配置网络时填写的DNS地址向首选DNS服务器(一般是电信运营商提供的,也可以使用像Google提供的DNS服务器)发起域名解析请求(通过UDP协议向DNS的53端口发起请求,这个请求是递归的请求)。
4.1) 首选DNS服务器首先查找自身的缓存,如果找到且没有过期,则解析到此结束。
4.2) 如果没有找到,则由首选DNS服务器代浏览器发起迭代的DNS解析请求,首先会找根域的DNS服务器的IP地址,找到根域的DNS服务器后,就会向其发起请求(请问www.myApp.com这个域名的IP地址是多少啊?)
4.3) 根域的DNS服务器发现这是一个顶级域com域的一个域名,于是就告诉首选DNS服务器我不知道这个域名的IP地址,但是我知道com域的DNS服务器的IP地址,于是首选DNS服务器就得到了com域的DNS服务器的IP地址,又向com域的DNS服务器发起了请求(请问www.myApp.com这个域名的IP地址是多少?)
4.4)com域的DNS服务器告诉首选DNS服务器我不知道这个域名的IP地址,但是我知道myApp.com这个域的DNS服务器的IP地址,于是首选DNS服务器又向myApp.com这个域的DNS服务器发起请求(请问www.myApp.com这个域名的IP地址是多少啊?),这时linux178.com域的DNS服务器一查,找到了www.myApp.com这个域名对应的IP地址,于是把找到的结果发送给首选DNS服务器,这时首选DNS服务器就拿到了www.myApp.com这个域名对应的IP地址,并返回给Windows系统内核,内核又把结果返回给浏览器,终于浏览器拿到了www.myApp.com对应的IP地址。
注:一般情况下是不会进行以下步骤的
如果经过以上的4个步骤,还没有解析成功,那么会进行如下步骤(以下是针对Windows操作系统):
⑤ 操作系统就会查找NetBIOS name Cache(NetBIOS名称缓存,就存在客户端电脑中的),那这个缓存有什么东西呢?凡是最近一段时间内和我成功通讯的计算机的计算机名和Ip地址,就都会存在这个缓存里面。什么情况下该步能解析成功呢?就是该名称正好是几分钟前和我成功通信过,那么这一步就可以成功解析。
⑥ 如果第⑤步也没有成功,那会查询WINS 服务器(是NETBIOS名称和IP地址对应的服务器)
⑦ 如果第⑥步也没有查询成功,那么客户端就要进行广播查找
⑧ 如果第⑦步也没有成功,那么客户端就读取LMHOSTS文件(和HOSTS文件同一个目录下,写法也一样)
如果第八步还没有解析成功,那么就宣告这次解析失败,那就无法跟目标计算机进行通信。只要这八步中有一步可以解析成功,那就可以成功和目标计算机进行通信。
2、客户端发起TCP的3次握手,通过socket与服务器建立一个TCP连接
拿到域名对应的IP地址之后,现在已经拥有了目标ip和端口号8000,这样我们就可以打开socket连接了。User-Agent即浏览器会以一个随机端口(1024 ~ 65535之间)向服务器端的Web程序发起TCP的连接请求。这个连接请求(原始的HTTP请求经过TCP/IP协议的4层模型的层层封包)到达服务器端后(这中间通过各种路由设备,局域网内除外),进入到网卡,然后是进入到内核的TCP/IP协议栈(用于识别该连接请求,解封包,一层一层的剥开),还有可能要经过Netfilter防火墙(属于内核的模块)的过滤,最终到达服务器端的Web程序,最终建立了TCP/IP的连接。
TCP的三次握手:点击打开链接
1) Client首先发送一个连接试探,ACK=0 表示确认号无效,SYN = 1 表示这是一个连接请求报文,同时表示这个数据报不能携带数据,seq = x 表示Client自己的初始序号,表示客户端等待服务器的回复。
2) Server监听到连接请求报文后,如同意建立连接,则向Client发送确认。SYN 和 ACK都置1 ,ack = x + 1表示期望收到对方下一个报文段的第一个数据字节序号是x + 1,同时表明x为止的所有数据都已正确收到,seq = y 表示Server 自己的初始序号,表示Server已经收到Client的连接请求,等待Client的确认。
3) Client收到确认后还需再次发送确认,同时携带要发送给Server的数据。ACK 置1 ,ack = y + 1表示期望收到对方下一个报文段的第一个数据字节序号是y + 1,seq= x + 1 表示Client自己的初始序号,一旦收到Client的确认之后,TCP连接就建立完毕,可以发起HTTP请求了。
3、客户端向服务器发送HTTP请求
TCP连接成功建立后,客户端开始向Web服务器发送请求,这个请求一般是GET或POST命令。
GET命令的格式为:GET 路径/文件名 HTTP/1.0,其中,路径/文件名指出所访问的文件,HTTP/1.0指出Web浏览器使用的HTTP版本。
1次完整的HTTP请求消息包括:一个请求行、若干消息头以及实体内容,而消息头和实体内容可以没有,消息头和实体内容间有一个空行。
- Get /mattmarg/ HTTP/1.0
- Accept: text/html
- Referer: http://www.myApp.com:8000/mainPage/index.html
- Accept-Lanague: zh-CN
- Content-Type: text/html
- Accept-Encoding: gzip, deflate
- User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
- Host: http://www.myApp.com
- Connection: keep-alive
- Cookie: name = value
- #空行
- <html><head></head><body>...</body></html>
第一行是HTTP请求的请求行:GET /mainPage/index.html HTTP/1.0。
接下来的几行是消息头,消息头主要是用来向服务器传达某种信息或指示。如告诉服务器自己的终端(User-Agent)(如果是浏览器则返回相应的浏览器型号),终端可以解释的类型(Accept),是从哪个页面提交的请求(Referer)以及浏览器缓存的Cookie等等。
Accept | 告诉服务器自己接受的MIME类型 |
Referer | 告诉服务器请求是从哪个页面提交的 |
Accept-Lanague | 告诉服务器自己接受的语言 |
Content-Type | 告诉服务器自己发送的MIME类型 |
Accept-Encoding | 告诉服务器自己接受的文件压缩方式 |
User-Agent | 告诉服务器自己的终端,如果是浏览器则返回相应的浏览器型号 |
Host | 用来标识请求服务器的主机 |
Connection | 告诉服务器自己是否支持Keep-Alive特性 |
Cookie | 每次请求时都会携带Cookie以方便服务器识别是否是同一个客户端 |
MIME,Multipurpose Internet Mail Extesions多用途互联网邮件扩展,是一个互联网标准,它扩展了电子邮件标准,使其能够支持非ASCII字符、二进制格式附件等多种格式的邮件消息,这个标准被定义在RFC 2045、RFC 2046、RFC 2047、RFC 2048、RFC 2049等RFC中。由RFC 822转变而来的RFC 2822,规定电子邮件标准并不允许在邮件消息中使用7位ASCII字符集以外的字符。正因如此,一些非英语字符消息和二进制文件,图像,声音等非文字消息都不能在电子邮件中传输。MIME规定了用于表示各种各样的数据类型的符号化方法。此外,在万维网中使用的HTTP协议中也使用了MIME的框架,标准被扩展为互联网媒体类型。
MIME 遵循以下格式:主类型/次类型。如:
1) image/jpg
2) image/gif
3) text/html
4) video/quicktime
5) appliation/x-httpd-php
4、服务器向客户端发送HTTP响应
Web服务器收到HTTP请求,进行处理。从它的文档空间中搜索子目录mainPage的文件index.html。如果找到该文件,Web服务器把该文件内容传送给相应的浏览器。
1次完整的HTTP响应消息包括:一个状态行、若干消息头以及实体内容,而消息头和实体内容可以没有,消息头和实体内容间有一个空行。
- HTTP 1.0 200 OK
- Accept: text/html
- Accept-Encoding: gzip, deflate
- Accept-Language: zh-CN
- Content-Type: text/html
- Cache-Control: max-age=0
- Connection: keep-alive
- Host: http://www.myApp.com
- User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36
- #空行
- <html><head></head><body>...</body></html>
HTTP 1.0 200 OK是HTTP响应的第一行,列出服务器正在运行的HTTP版本号和应答代码,代码"200 OK"表示请求完成。
那到底服务器端接收到http请求后是怎么样生成html文件?
假设服务器端使用nginx+PHP(fastcgi)架构提供服务
① nginx读取配置文件
我们在浏览器的地址栏里面输入的是 http://www.linux178.com (http://可以不用输入,浏览器会自动帮我们添加),其实完整的应该是http://www.linux178.com./ 后面还有个点(这个点代表就是根域,一般情况下我们不用输入,也不显示),后面的/也是不用添加,浏览器会自动帮我们添加(且看第3部那个图里面的URL),那么实际请求的URL是http://www.linux178.com/,那么好了Nginx在收到 浏览器 GET / 请求时,会读取http请求里面的头部信息,根据Host来匹配 自己的所有的虚拟主机的配置文件的server_name,看看有没有匹配的,有匹配那么就读取该虚拟主机的配置,发现如下配置:
1
|
root
/web/echo
|
通过这个就知道所有网页文件的就在这个目录下 这个目录就是/ 当我们http://www.linux178.com/时就是访问这个目录下面的文件,例如访问http://www.linux178.com/index.html,那么代表/web/echo下面有个文件叫index.html
1
|
index index.html index.htm index.php
|
通过这个就能得知网站的首页文件是那个文件,也就是我们在入http://www.linux178.com/ ,nginx就会自动帮我们把index.html(假设首页是index.php 当然是会尝试的去找到该文件,如果没有找到该文件就依次往下找,如果这3个文件都没有找到,那么就抛出一个404错误)加到后面,那么添加之后的URL是/index.php,然后根据后面的配置进行处理
1
2
3
4
5
6
7
|
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:寻找文件在文件系统层面是怎么操作的?
比如nginx需要得到/web/echo/index.php这个文件
每个分区(像ext3 ext3等文件系统,block块是文件存储的最小单元 默认是4096字节)都是包含元数据区和数据区,每一个文件在元数据区都有元数据条目(一般是128字节大小),每一个条目都有一个编号,我们称之为inode(index node 索引节点),这个inode里面包含 文件类型、权限、连接次数、属主和数组的ID、时间戳、这个文件占据了那些磁盘块也就是块的编号(block,每个文件可以占用多个block,并且block不一定是连续的,每个block是有编号的),如下图所示:
还有一个要点:目录其实也普通是文件,也需要占用磁盘块,目录不是一个容器。你看默认创建的目录就是4096字节,也就说只需要占用一个磁盘块,但这是不确定的。所以要找到目录也是需要到元数据区里面找到对应的条目,只有找到对应的inode就可找到目录所占用的磁盘块。
那到底目录里面存放着什么,难道不是文件或者其他目录吗?
其实目录存着这么一张表(姑且这么理解),里面放着 目录或者文件的名称和对应的inode号(暂时称之为映射表),如下图:
假设
/ 在数据区占据 1、2号block ,/其实也是一个目录 里面有3个目录 web 111
web 占据 5号block 是目录 里面有2个目录 echo data
echo 占据 11号 block 是目录 里面有1个文件 index.php
index.php 占据 15 16号 block 是文件
其在文件系统中分布如下图所示
那么内核究竟是怎么找到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的完整内容
5、服务器发起TCP的4次挥手,关闭与客户端的TCP连接
应答结束后,若Connection为close,则服务器主动关闭TCP连接,客户端被动释放TCP 连接,若Connection为keep alive,则该连接会保持一段时间,在该时间内,下次的请求可以继续使用此连接。
TCP的四次挥手:点击打开链接
1) Server向Client发送一个FIN报文,序列号为i,FIN和ACK都置1 ,用来关闭Server到Client的数据传送,也就是告诉Client,Server已经不会再发数据了(当然,在FIN包之前发送出去的数据,如果没有收到对应的ACK确认报文,Client依然会重发这些数据),但此时Server还可以接收数据。
2) Client接到FIN(i)报文后,但是如果还有数据没有发送完成,则不必急着关闭Socket,可以继续发送数据,Client发送ACK,序列号为i + 1,ACK置1,告诉Server请求已经收到了,但Client还没准备好,Server继续等待Client的FIN报文。
3) 当Client确定数据已发送完成,则向Server发送FIN报文,序列号为j,FIN和ACK都置1,告诉Server,Client数据发完了,准备好关闭连接了。
4) Server收到FIN(j)报文后,就知道可以关闭连接了,Server发送ACK,序列号为j + 1,ACK置1, Client收到ACK后,就知道可以断开连接了,至此,TCP连接就已经完全关闭了!
6、浏览器解析HTML代码,并请求HTML代码中的资源
浏览器拿到index.html文件后,就开始解析其中的HTML代码,遇到.js、.css及图片等静态资源时,就向服务器端去请求下载(会使用多线程下载,每个浏览器的线程数不一样),这个时候就用上keep-alive特性了,建立一次HTTP连接,可以请求多个资源,下载资源的顺序就是按照代码里的顺序,但是由于每个资源大小不一样,而浏览器又多线程请求请求资源,所以从下图看出,这里显示的顺序并不一定是代码里面的顺序。
浏览器在请求静态资源时(在未过期的情况下),向服务器端发起一个HTTP请求,询问从上一次修改到现在有没有对资源进行修改,如果服务器端返回304状态码(Not Modified)(告诉浏览器没有对资源进行修改),那么浏览器会直接读取本地的该资源的缓存文件。
3xx: 重定向状态码
301: 永久重定向, Location响应首部的值仍为当前URL,因此为隐藏重定向;
302: 临时重定向,显式重定向, Location响应首部的值为新的URL
304:Not Modified 未修改,比如本地缓存的资源文件和服务器上比较时,发现并没有修改,服务器返回一个304状态码,告诉浏览器,你不 用请求该资源,直接使用本地的资源即可。
7、浏览器对页面进行渲染呈现给用户
浏览器利用自己内部的工作机制,把请求到的静态资源和HTML代码进行渲染,渲染之后呈现给用户。