理解Http请求于TCP/IP的网络传输过程


前言

让我们先来看一下浏览器访问Web服务器这一过程的全面貌。在浏览器地址栏上输入一个url按下回车,就发起了一次请求

浏览器:请给我xxx网页的数据。
Web服务器:好的,这就是你要的数据。

在这一些列交互完成后,浏览器就会将从Web服务器接收到的数据显示在屏幕上。如果仅仅是这样,浏览器和服务器之间通过网络进行的交互十分简单;但是作为一名前端开发人员,较详细的了解网络请求传输过程还是十分有必要的。下面我以http请求为例,将较详细地介绍HTTP请求是怎么通过TCP/IP传输的。

在这里插入图片描述


一、生成HTTP请求消息

在浏览器输入url,按下回车后,浏览器要做的第一步工作就是对URL进行解析。根据Http的规格,url会被按照固定地格式拆分。对url进行解析之后,浏览器确定了Web服务器和文件名,接下来就是根据这些信息来生成HTTP请求消息了。

提示:如果你对HTTP协议还不了解,建议先去学习HTTP协议知识。
或者参考下面这篇文章:HTTP协议

二、向DNS服务器查询IP地址

生成HTTP消息之后,我们需要委托操作系统将消息发送给Web服务器。尽管浏览器能够解析网址并生成HTTP消息,但它本身并不具备将消息发送到网络中的功能,所以这一功能需要委托操作系统来实现。但是在委托操作系统发送消息时,必须要提供的不是通信对象的域名,而是它的IP地址。因此在生成HTTP消息之后,下一步骤就是根据域名查询IP地址。这时候就到DNS服务器发挥作用了。
DNS服务器
对于DNS服务器,我们的计算机上一定有相应的DNS客户端,而相当于DNS客户端的部分称为DNS解析器,或者简称解析器。通过DNS查询IP地址的操作称为域名解析,因此负责执行解析(resolution)这一操作的就叫解析器(resolver)。要了解域名解析过程我们需要先了解Socket。

Socket是什么?
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。在设计模式中,Socket其实就是一个门面模式,它把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。几乎所有的网络进程都是通过Socket来通信的。

此处不做详细介绍,有兴趣可参考:socket原理讲解

在这里插入图片描述
在这里插入图片描述

如何通过DNS服务器根据域名查询到IP地址?
Socket库中包含的程序组件可以让其他应用程序调用操作系统的网络功能,而解析器就是这个库中的其中一种程序组件。当我们希望向某个服务器发送请求时,浏览器会先自动调用

<内存地址> = gethostbyname(“要查询的服务器域名”);

运行这一程序后,服务器的IP地址就会被写入指定的内存中。
在这里插入图片描述

我们获得了IP地址。

三、委托协议栈(TCP/IP)传输数据

知道了IP地址之后,就可以委托操作系统内部的协议栈向这个目标IP地址发送消息了。和向DNS服务器查询IP地址的操作一样,这里也需要使用Socket库中的程序组件。不过,查询IP地址只需要调用一个程序组件就可以了,而这里需要按照指定的顺序调用多个程序组件来实现。
3.1创建套接字阶段

什么是套接字?
收发数据的两台计算机之间连接了一条数据通道,数据沿着这条通道流动。我们可以把数据通道想象成一条管道,数据从一端进入管道,然后到达管道的另一端时被取出。在计算机收发数据之前,双方需要先建立起这条管道,而建立管道的关键在于管道两端的数据出入口,这些出入口称为套接字。在协议栈内部有一块用于存放控制信息的内存空间,这里记录了用于控制通信操作的控制信息,例如通信对象的IP地址、端口号、通信操作的进行状态等。本来套接字就只是一个概念而已,并不存在实体,如果一定要赋予它一个实体,我们可以说这些控制信息就是套接字的是实体,或者说存放控制信息的内存空间就是套接字的实体。

首先是套接字创建阶段。客户端调用Socket库中的socket程序组件,控制流程会转移到socket内部并执行创建套接字的操作,完成之后流程又会被移交回浏览器套接字创建完成后,协议栈会返回一个描述符,浏览器将收到的描述符存放在内存中。

描述符是什么?
描述符时用来识别不同套接字的,大家可以做如下理解。我们现在只关注了浏览器访问Web服务器的过程,但实际上计算机中会同时进行多个数据的通信操作,比如两个数据收发操作在同时进行,也就需要创建两个不同的套接字。同一台计算机上可能同时存在多个套接字,在这情况下,我们就需要一种方法来识别出某个特定的套接字,这种方法就是描述符。

3.2连接阶段
接下里我们需要委托协议栈将客户端创建的套接字与服务端那边的套接字连接起来,通过调用Socket库中的connect程序组件来完成这一操作。调用connect需要指定描述符,服务器IP地址和端口号这三个参数:描述符用来告诉协议栈指定哪个套接字进行连接。IP地址+端口号识别具体服务器的具体套接字。
当连接成功后,协议栈会将对方的IP地址和端口号等信息保存在套接字中,这样我们就可以开始收发数据了。
客户端创建一个包含表示开始数据收发操作的控制信息的头部。TCP头部消息
请求建立连接时客户端将同步标志SYN设置为,大家可以认为它表示连接。服务端收到SYN为1的信号,知道有客户端要请求建立连接,便将套接字状态改为正在连接,而且将SYN也设置成1(表示同意连接,拒绝的话将不设置SYN包,而是将RST比特设置为1),同时将确认标志也设置成1,将消息返回到客户端。客户端接收到SYN为1和ACK号知道服务器端同意建立连接,就将ACK号的比特也设置成1返回给服务器端。用来确认数据传输没有发生错误。这样连接就建立了。

3.3通信阶段
3.3.1write
当套接字连接起来之后,便开始传递消息。首先,应用程序需要在内存中准备好要发送的数据。根据用户输入的网址生成的HTTP请求消息就是我们要发送的数据。接下来当调用write时,需要指定描述符和发送数据,然后协议栈就会将数据发送到服务器。由于套接字中已经保存了已连接的通信对象的相关信息,所以只要通过描述符指定套接字,就可以识别出通信对象,并向其发送数据。

数据是怎么传输的?
协议栈并不是已收到数据就马上发送出去,而是会将数据存放在内部的发送缓冲区,并等待应用程序的下一段数据。因为协议栈一次收到多少数据是由应用程序自行决定的,因此,如果一收到数据就发送出去,有可能会收到大量的小包,导致网络效率下降,因此需要数据积累一定量时在发送出去。而至于要积累多少数据才能发送,不同种类和版本的操作系统会有所不同,主要根据下面两个要素来判断。
1.每个网络包能容纳的数据长度。MTU:一个网络包的最大长度,以太网中一般为1500字节。MSS:MTU是包含头部的总长度,因此需要从MTU减去头部,然后得到的长度就是一个网络包中所能容纳的最大差个都,叫MSS。当应用程序收到的数据长度超过或者接近MSS时再发送出去,就可以避免发送大量小包的问题。
2.另一个判断要素是时间。当应用程序发送数据的频率不高的时候,如果每次都等到长度接近MSS时在发送,可能会因为等待时间太长而照成发送延迟,在这种情况下,即便缓冲区中的数据长度没有达到MSS,也应该果断发送出去。为此,协议栈的内部有一个计时器,当经过一定时间后,就会把网络包发送出去。
判断要素就是这两个,但是他们其实是互相矛盾的。如果长度优先,那么网络效率会提高,但可能会因为等待填满缓冲区而产生延迟;相反地,如果时间优先,那么延迟时间会变少,但是又会降低网络的效率。因此在进行发送操作时需要综合考虑这两个要素以达到平衡。不过,TCP协议规格中并没有告诉我们怎样才能平衡,因此实际如何判断是由协议栈的开发者来决定的,也正是由于这个原因,不同种类和版本的操作系统在相关操作上存在差异。

提示:这里详细介绍数据发送的两个根据,由于TCP没有对其做出规定,我们开发者可以根据实际情况想办法来平衡MSS与时间,让效率最大化。

让我们回到HTTP传输,像浏览器这种会话型的应用程序在向服务器发送数据时,等待填满缓冲区导致延迟会产生很大影响,因此一般会使用直接发送的选项。HTTP请求消息一般不会太长,一个网络包就能装得下,但如果其中要提交表单,数据长度就可能超过一个网络包所能容纳的数据量,比如在博客或者论坛上发表一篇长文就属于这种情况。这种情况下,发送缓冲区的数据就会超过MSS的长度,这时我们当然不需要继续等待后面的数据了,发送缓冲区中的数据会被以MSN的长度为单位进行拆分,拆分出来的每块数据会被放进单独的网络保存。根据发送缓冲区中的数据拆分的情况,当判断需要发送这些数据时,就在每一块数据前面加上TCP头部,并根据套接日中记录的控制信息标记发送方和接收方的端口号,然后交给IP模块来执行发送数据的操作。
在这里插入图片描述
到这里,网络包已经装好数据并发往服务器了,但数据发送操作还没有结束,TCP具有确认对方是否成功收到数据包,以及当对方没有收到时进行重发的功能,因此在发送网络包之后,接下来还需要进行确认操作。TCP模块在拆分数据时会先算好每一块数据相当于从头开始的第几个字节,接下来发送这一块数据时,将算好的字节数写在TCP头部中,’‘序号’'字段就是派在这个时候用场上的。然后发送数据的长度也需要告知对方,不过这个并不是放在TCP头部里面。因为用整个网络包的长度减去头部的长度就可以得到数据的长度,所以接收方可以用这种方式来进行计算,有了上面两个数值,我们就可以知道发送的数据是从第几个字节开始,长度是多少。

在实际的通讯中,序号并不是从1开始的,而是需要用随机数计算出一个初始值,这是因为如果序号都是从1开始通信过程,就会非常容易预测,有人会利用这一点来发动攻击,但是如果初始值是随机的,那么对方就搞不清楚需要到底是从多少开始计算的,应取,因此,需要在开始收发数据之前将初始值告知通信对象。
TCP采用这样的方式确认对方是否收到了数据,在得到对方确认之前发送过的包都会保存在发送缓冲区中,如果对方没有返回某些包对应的ACK号,那么就重新发送这些包。这一机制非常强大,通过这一机制,我们可以确认接收方有没有收到某个包,如果没有收到就重新发送,这样一来,无论网络中发生任何错误,我们都可以发现并采取补救措施(重传网络包)。反过来说,有了这一机制,我们就不需要在其他地方对错误进行补救。因此,网卡、集线器、路由器、都没有错误补偿机制,一旦检测到错误就直接丢弃相应的包。应用程序也是一样,因为采用TCP传输,即便发生一些错误,对方最终也能够收到正确的数据,所以应用程序只管自顾自的发送数据这些数据就好。不过如果发生网络中断、服务器岩机等问题,那么无论TCP怎么样重全都不管用,这种情况下,无论任何尝试都是徒劳的,因此,TCP会在尝试几次重传无效之后强制结束通讯并向应用程序报错。
3.3.2read
这样,数据就被发送到了服务端。服务端调用Socket的read程序组件,读取收到的数据包,解析请求并作出响应,响应过程也就是向客户端返回HTTP消息,也会调用write指定描述符和响应数据,协议栈将数据发送到客户端。客户端也会调用read程序组件来获得响应消息。

服务端和浏览器端有所不同,服务端需要先调用程序
组件socket来创建套接字,接着调用bind和listen进入等待连接状态,最后调用accept来接收连接。当客户端包还没发送过来的时候,先转为等待包到达状态,并在包到达的时候执行连接操作。

3.4断开阶段
当浏览器收到数据后,收发数据的过程就结束了。根据应用种类的不同,客户端和服务器都有可能会先执行close程序组件。在浏览器中会根据content-length、Transfer-encoding、body长度是否可知等信息决定哪一端先主动断开连接。如果服务器端先调用close来断开连接,断开操作传达到客户端后,客户端的套接字也会进入断开阶段。接下来,当浏览器调用read执行接收数据操作时,read会告知浏览器收发数据操作已结束,连接已断开。浏览器的知后,也会调用close进入断开阶段。
如果服务器一方先调用close程序组件,那么服务器的协议栈会生成包含断开信息的TCP头部,具体来说就是将控制位中的FIN(结束标志)比特设为1,发送到客户端,表示要断开连接。当客户端收到服务器发来的FIN为1的包,会向服务器端发送一个ACK号来表示自己已经收到了断开操作消息,客户端会调用close来结束数据收发操作。客户端也会生成一个FIN为1的标志给服务器端,服务器收到后再返回一个ACK确认标志。到这里客户端和服务端的通信就全部结束了。


在这里插入图片描述


四、接收到HTTP响应,解析数据渲染到浏览器网页上

在这里插入图片描述

  • 7
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值