浏览器生成消息——探索浏览器内部

注意
本文最后更新于 2024-01-29,文中内容可能已过时。

浏览器生成消息——探索浏览器内部

总结,第一章主要是讲了两个步骤:

浏览器根据 URL 和各种系统信息生成 HTTP 消息(同时解析域名),然后委托操作系统的 Socket,调用协议栈,发送消息

生成 HTTP 请求消息

探索之旅从输入网址开始

网址,准确来说应该叫 URL,URL:Uniform Resource Locator,统一资源定位符

除了"http:",网址还可以以其他一些文字开头,例如"ftp:““file:““mailto:“等。这代表着不同的协议,不同的协议代表着不同的功能

file 不是协议,file 可以看作是浏览器访问本地文件的一种方法file:///C:/Users/LiangKang/Desktop/111.txt,其中file:///实际是file://localhost/的省略

浏览器先要解析 URL

以 HTTP 为例,按照 URL 的格式将其中的各个元素拆分出来

省略文件名的情况

比如http://www.lab.glasscom.com/dir/ 或者 http://www.lab.glasscom.com/ 以”/“结尾代表域名后面应该有的文件名被省略了。根据 URL 的规则,文件名可以像前面这样省略。一般,我们会在服务器上事先设置好文件名省略时要访问的默认文件名。Tomcat 就是这样的,会默认访问/default/index.html

http://www.lab.glasscom.com(没有 ‘/‘符号),访问根目录下事先设置的默认文件。

http://www.lab.glasscom.com/whatisthis,没有文件拓展名。也没有”/",这个时候就要判断,如果 Web 服务器上存在名为whatisthis的文件,则将whatisthis作为文件名来处理;如果存在名为whatisthis的目录,则将whatisthis作为目录名来处理,毕竟不可能在同一目录下存在名字相同的文件和目录

HTTP 的基本思路

HTTP 协议定义了客户端和服务器之间交互的消息内容和步骤,其基本思路非常简单。首先,客户端会向服务器发送请求消息(图 1.4)。请求消息中包含的内容是"对什么"和"进行怎样的操作"两个部分

其中 “对什么"的部分称为 URI(Uniform Resource Identifier)。一般来说,URI 的内容是一个存放网页数据的文件名或者是一个 CGI 程序的文件名,例如"/dir1/file1.html""/dir1/program1.cgi"等,实际上,这个文件在 Web 服务器上未必是真实存在的,因为 Web 服务器可以通过重写规则对虚拟的 URI 进行映射。各种 Web 框架都是这样干的,比如 Spring,url 访问的实际上是 Controller 的地址。URI 是比 URL 更广泛的概念

接下来"进行怎样的操作"的部分称为方法。方法表示需要让 Web 服务器完成怎样的工作

POST DELETE PUT GET 刚好对应增删改查。而这,实际上就是 RESTful 的实现方式。在同一个 URL 上,通过请求方法的不同,实现不同的功能

操作 传统方式 REST 风格
查询操作 getUserById?id=1 user/1–>get请求方式
保存操作 saveUser user–>post 请求方式
删除操作 deleteUser?id=1 user/1–>delete请求方式
更新操作 updateUser user–>put 请求方式

GET 和 POST 方法属于 HTTP 的典型用法,除此之外的其他方法在互联网上几乎见不到使用的例子。

OPTIONS 类型的请求在跨域的时候查询是否有权限的时候会用到。


除了图 1.4 中的内容之外,HTTP 消息中还有一些用来表示附加信息的头字段。客户端向 Web 服务器发送数据时,会先发送头字段,然后再发送数据。不过,头字段属于可有可无的附加信息。

收到请求消息之后,Web 服务器会对其中的内容进行解析,通过 URI 和方法来判断"对什么"“进行怎样的操作”,如果是访问文件就直接返回文件,如果是访问应用程序就调用 URI 指定的应用程序,并将相关数据发送给它(Web 服务器跟 web 应用是两个概念,Web 服务器是一个容器),完成工作后,将结果存放在响应消息中。在响应消息的开头有一个状态码,它用来表示操作的执行结果是成功还是发生了错误(比如 404)。

生成 HTTP 请求消息

HTTP 消息在格式上是有严格规定的,因此浏览器会按照规定的格式来生成请求消息

HTTP 消息的格式:

消息头项目信息:

Allow 消息头让我想起,CROS 跨域请求的 preflight 请求的响应头中会有Access-Control-Allow-Methods表示接受的跨域请求的方法,Access-Control-Allow-Origin表示接受的跨域请求的来源 URL。具体请查看看MyLearnning\Java\Java EE\front-end\JavaScript的《CROS 跨域详解》

Cookie 信息是放在消息头中的,请求消息头中存放的是 Cookie 项目,响应消息头中存放的是 Set-Cookie(告知浏览器,添加或者修改某一个 Cookie 项目的值)

请求的具体数据在 GET 方法和 POST 方法中存储的位置不一样。使用 GET 方法请求的时候,参数会放到 URI 中。而使用 PSOT 提交的时候,数据(一般是表单数据)会放到消息体中。

在使用 GET 方法的情况下,仅凭方法和 URI,Web 服务器就能够判断需要进行怎样的操作,因此消息体中不需要填写任何数据

发送请求后会收到响应

响应消息的格式以及基本思路和请求消息是相同的,差别只在第一行上。

状态码和响应短语表示的内容一致,但它们的用途不同。状态码是一个数字,它主要用来向程序告知执行的结果(表 1.3);相对地,响应短语则是一段文字,用来向人们告知执行的结果。

如果服务器返回图片,那么消息体中的内容将是二进制的

向 DNS 服务器查询 Web 服务器的 IP 地址

IP 地址的基本知识(熟记)

学习此小节之前,建议看看《网络的进化》。

尽管浏览器能够解析网址并生成 HTTP 消息,但它本身并不具备将消息发送到网络中的功能,因此这一功能需要委托操作系统来实现

发送消息的功能对于所有的应用程序来说都是通用的,因此让操作系统来实现这一功能,其他应用程序委托操作系统来进行操作,这是一个比较合理的做法。

TCP/IP的基本思路:子网可以理解为用集线器连接起来的几台计算机,我们将它看作一个单位,称为子网。将子网通过路由器连接起来,就形成了一个网络。在网络中,所有的设备都会被分配一个地址。这个地址就相当于现实中某条路上的"××号××室”。其中"号"对应的号码是分配给整个子网的,而"室"对应的号码是分配给子网中的计算机的,这就是网络中的地址。“号"对应的号码称为网络号,“室"对应的号码称为主机号,这个地址的整体称为 IP 地址。发送者发出的消息首先经过子网中的集线器,转发到距离发送者最近的路由器上(图 1.8①)。接下来,路由器会根据消息的目的地判断下一个路由器的位置,然后将消息发送到下一个路由器,即消息再次经过子网内的集线器被转发到下一个路由器(图 1.8②)。前面的过程不断重复,最终消息就被传送到了目的地。

实际的 IP 地址:

实际的 IP 地址是一串 32 比特的数字,按照 8 比特(1 字节)为一组分成 4 组,分别用十进制表示然后再用圆点隔开。这就是我们平常经常见到的 IP 地址格式,但仅凭这一串数字我们无法区分哪部分是网络号,哪部分是主机号,这两部分的具体结构是不固定的。在组建网络时,用户可以自行决定它们之间的分配关系,因此我们还需要子网掩码来表示 IP 地址的内部结构。

子网掩码的格式如图 1.10②所示,是一串与 IP 地址长度相同的 32 比特数字,其左边一半都是 1,右边一半都是 0。其中,子网掩码为 1 的部分表示网络号,子网掩码为 0 的部分表示主机号。主机号部分的比特全部为 0 或者全部为 1 时代表两种特殊的含义。主机号部分全部为 0 代表整个子网而不是子网中的某台设备(图 1.9(d))。此外,主机号部分全部为 1 代表向子网上所有设备发送包,即广播(图 1.9(e)),这两个主机号本来是可以作为一台主机的 ip 使用的,但是我们现在特化这两个地址用于别的用途。


之所以将主机号全部为 1 定位向子网广播,可能跟路由器的转发规则有关,这个得看第三章

通俗解释

参考博客:关于 IP 地址、网络号、主机号、子网掩码之间的关系

IP 地址与网络号段、主机号段之间的关系,或者说IP 地址的构成,如下:

$$ IP 地址= 网络号段 + 主机号段 = 网络号段 + ( 子网号段 + 子网主机号段 ) $$

目前大仍采用的 IP 地址是 IPv4 版本,由 4 个字节组成,每个字节 8 位二进制数,也就是说目前的 IP 地址由 32 位二进制数组成。但随着全球主机和路由的增多,IPv6 版本的 IP 地址即将使用。

网络号

网络号就是我们常说的 A 类地址、B 类地址、C 类地址、D 类地址、E 类地址,由 IP 地址中的前数位构成。

以房地产开发为例。当房地产商向政府申请开发房地产时,

  1. 首先需要拿地,也就是向政府申请在某省某市某区某街道的建筑用地
  2. 然后才能在申请的建筑用地上,根据不同的标准建立不同的小区,如一期、二期、A 冻、B 栋

类似的,当含有多台主机和路由的某个机关、单位、公司、机构、组织等申请一个 IP 地址时,

  1. 首先获得(实际上)是一个网络号。根据不同网络的性质,可能是获得 ABC 类地址中的某个网络号

  2. 然后具体的各个主机号,再由本单位跟根据自身的情况自行分配。

  • A 类地址8 位网络号(0 开头)+24 位主机号,范围:1.0.0.0 - 126.255.255.255

  • B 类地址16 位网络号(10 开头)+16 位主机号,范围:128.0.0.0 - 191.255.255.255

  • C 类地址24 位网络号(110 开头)+8 位主机/号,范围:192.0.0.0 - 223.255.255.255

  • D 类:组播地址,范围:224.0.0.0 - 239.255.255.255

  • E 类:保留以后再用,范围:240.0.0.0 - 255.255.255.255

*注意

  1. 网络号不能全部为 0 或者全部为 1.
  2. IP 地址也不能全部为 0 或者全部为 1.

这五类地址加起来总共有 40 多亿地址,但实际上没有那么多,

因为其中的 D、E 类地址被分别被用作组播地址、实验地址

在以上点分十进制的 IP 地址段中没有127.0.0.0/8 这个网段,因为该网段被用作环回地址

子网掩码

子网掩码,与目的 IP 的地址一起,可以就是用来确定该网络的上确定:

  1. 子网个数
  2. 主机号
  3. 主机号所在的子网

同样以上面的房地产开发为例。

当房地产商开发住宅用地时,可根据某些需求将住宅用地划成几个小区或者几期工程开发。比如房地产经常用一期、二期等标记一个小区。

同样的,在为某个机关、单位、公司、机构、组织分配好网络号后,如果这些机关单位的主机很多而且分布在较大地理位置时,可以将本单位所属主机划分为若干个小局域网,这些局域网就是子网。子网之间用路由器互联

网络号分配好之后就不能再变动了,所以,在划分的子网号的时候,我们需要在主机号段中划出子网号。

如何才能知道子网号码呢?这就需要子网掩码

  1. 子网掩码,跟 IP 地址一样,是一个由一连串 1 和一连串 0共 32 位的二进制数组成
  2. 1对应于网络号字段和子网号字段
  3. 0对应于主机号字段

域名和 IP 地址并用的理由

为什么使用 IP 来确定网络通信对象,而不是通过域名?

IP 只有 4 个字节,域名则没有大小限制,会很长,降低查找效率,而且 IP 固定只有 4 个字节,注意,是固定的,查找起来非常简单快速,而域名则长度不定,查找效率不高。

现在我们使用的方案是让人来使用名称,让路由器来使用 IP 地址。为了填补两者之间的障碍,需要有一个机制能够通过名称来查询 IP 地址,或者通过 IP 地址来查询名称,这样就能够在人和机器双方都不做出牺牲的前提下完美地解决问题。这个机制就是 DNS。

DNS:Domain Name System,域名服务系统。将服务器名称和 IP 地址进行关联是 DNS 最常见的用法,但 DNS 的功能并不仅限于此,它还可以将邮件地址和邮件服务器进行关联,以及为各种信息关联相应的名称。

Socket 库提供查询 IP 地址的功能

向 DNS 服务器发出查询,也就是向 DNS 服务器发送查询消息,并接收服务器返回的响应消息。换句话说,对于 DNS 服务器,我们的计算机上一定有相应的 DNS 客户端,而相当于 DNS 客户端的部分称为 DNS 解析器,或者简称解析器。通过 DNS 查询 IP 地址的操作称为域名解析,因此负责执行解析(resolution)这一操作的就叫解析器(resolver)了。

解析器实际上是一段程序,它包含在操作系统的 Socket 库中,Socket 库是用于调用网络功能的程序组件集合,其中包含的程序组件可以让其他的应用程序调用操作系统的网络功能。

通过解析器向 DNS 服务器发出查询

在编写浏览器等应用程序的时候,只要像图 1.11 这样写上解析器的程序名称 gethostbyname以及 Web 服务器的域名www.lab.glasscom.com就可以了,这样就完成了对解析器的调用。(我们假设 gethostbyname 这个程序实现了解析器的全部功能,实际上,实现解析器的功能需要多个程序相互配合,可能还会从 gethostbyname 程序中调用其他的程序。)

解析器的内部原理

当控制流程转移到解析器后,解析器会生成要发送给 DNS 服务器的查询消息。这个过程与浏览器生成要发送给 Web 服务器的 HTTP 请求消息的过程类似,我们当然也需要知道 DNS 服务器的 IP 地址。只不过这个 IP 地址是作为 TCP/IP 的一个设置项目事先设置好的,不需要再去查询了,这就是我们在设置网络适配器的时候,设置网络适配器属性,在其中配置的 DNS 服务器的地址的作用

然后解析器会根据 DNS 的规格,生成一条表示"请告诉我www.lab.glasscom.com的 IP 地址"的数据,(采用UDP协议),并将它发送给 DNS 服务器(图 1.12③)。发送消息这个操作并不是由解析器自身来执行,而是要委托给操作系统内部的协议栈来执行(协议栈:操作系统内部的网络控制软件,也叫"协议驱动"“TCP/IP驱动"等)。这是因为和浏览器一样,解析器本身也不具备使用网络收发数据的功能。解析器调用协议栈后,控制流程会再次转移,协议栈会执行发送消息的操作,然后通过网卡将消息发送给 DNS 服务器(图 1.12④⑤)


计算机的内部结构就是这样一层一层的。也就是说,很多程序组成不同的层次,彼此之间分工协作。当接到上层委派的操作时,本层的程序并不会完成所有的工作,而是会完成一部分工作,再将剩下的部分委派到下层来完成。(类似于 Java 中的栈内存)

全世界 DNS 服务器的大接力

DNS 服务器的基本工作

web 服务器的域名有很多都是像www.lab.glasscom.com这样以www开头的,但这并不是一种规定,只是因为最早设计 Web 的时候,很多 Web 服务器都采用了 www 这样的命名,后来就形成了一个惯例而已。

DNS 服务器会从域名与 IP 地址的对照表中查找相应的记录,并返回 IP 地址

域名的层次结构

首先,DNS 服务器中的所有信息都是按照域名以分层次的结构来保存的。DNS 中的域名都是用句点来分隔的,比如www.lab.glasscom.com,这里的句点代表了不同层次之间的界限,就相当于公司里面的组织结构不用部、科之类的名称来划分,只是用句点来分隔而已。在域名中,越靠右的位置表示其层级越高,比如www.lab.glasscom.com这个域名如果按照公司里的组织结构来说,大概就是"com 事业集团 glasscom 部 lab 科的 www"这样。其中,一个层级称为一个域。因此,com 域的下一层是 glasscom 域,再下一层是 lab 域,再下面才是 www 这个名字。

每个域都是作为一个整体来处理的。换句话说就是,一个域的信息是作为一个整体存放在 DNS 服务器中的,不能将一个域拆开来存放在多台 DNS 服务器中。但是一台 DNS 服务器中也可以存放多个域的信息

这里再补充一点。对于公司域来说,例如现在需要为每一个事业集团配备一台 DNS 服务器,分别管理各事业集团自己的信息,但我们之前也说过一个域是不可分割的,这该怎么办呢?没关系,我们可以在域的下面创建下级域,又称为子域,然后再将它们分别分配给各个事业集团(应该这么理解lab.glasscom.combusiness.glasscom.com是两个域,而且他们都是glasscom.com的子域,同时,lab.glasscom.combusiness.glasscom.com的域名信息都必须作为一个整体存放在一个 DNS 服务器上,意思是lab.glasscom.com的信息只能存在于一台 DNS 服务器上,不能两台 DNS 域名服务器都存着lab.glasscom.com的信息都可以解析lab.glasscom.comlab.glasscom.com也是一样,当然,这两个域也可以存在于同一个 DNS 服务器上。一个域名只能存在于一台 DNS 服务器上的原因是方便查找,在进行子域名查找的时候可以确定只用去一台唯一的子域名服务器查找即可,一个域名可以存在于多台 DNS 服务器上的话,这个时候就得去多台 DNS 服务器上查询了,查询时间会指数级增加)

互联网中的域也是一样,通过创建下级的域来分配给不同的国家、公司和组织使用。通过实际的域名可能更容易理解,比如www.nikkeibp.co.jp这个域名,最上层的 jp 代表分配给日本这个国家的域;下一层的 co 是日本国内进行分类的域,代表公司;再下层的 nikkeibp 就是分配给某个公司的域;最下层的 www 就是服务器的名称。

寻找相应的 DNS 服务器并获取 IP 地址

关键在于如何找到我们要访问的 Web 服务器的信息归哪一台 DNS 服务器管。

互联网中有数万台 DNS 服务器,肯定不能一台一台挨个去找。我们可以采用下面的办法。首先,将负责管理下级域的 DNS 服务器的 IP 地址注册到它们的上级 DNS 服务器中,然后上级 DNS 服务器的 IP 地址再注册到更上一级的 DNS 服务器中,以此类推。也就是说,负责管理lab.glasscom.com这个域的 DNS 服务器的 IP 地址需要注册到glasscom.com域的 DNS 服务器中,而glasscom.com域的 DNS 服务器的 IP 地址又需要注册到 com 域的 DNS 服务器中。这样,我们就可以通过上级 DNS 服务器查询出下级 DNS 服务器的 IP 地址,也就可以向下级 DNS 服务器发送查询请求了

在互联网中,com 和 jp 的上面还有一级域,称为根域。根域不像 com、jp 那样有自己的名字,因此在一般书写域名时经常被省略,如果要明确表示根域,应该像www.lab.glasscom.com.这样在域名的最后再加上一个句点,而这个最后的句点就代表根域。根域的 DNS 服务器中保管着 com、jp 等的 DNS 服务器的信息。由于上级 DNS 服务器保管着所有下级 DNS 服务器的信息,所以我们可以从根域开始一路往下顺藤摸瓜找到任意一个域的 DNS 服务器。

除此之外还需要完成另一项工作,那就是将根域的 DNS 服务器信息保存在互联网中所有的 DNS 服务器中。这样一来,任何 DNS 服务器就都可以找到并访问根域 DNS 服务器了。分配给根域 DNS 服务器的 IP 地址在全世界仅有 13 个,而且这些地址几乎不发生变化,因此将这些地址保存在所有的 DNS 服务器中也并不是一件难事。实际上,根域 DNS 服务器的相关信息已经包含在 DNS 服务器程序的配置文件中了,因此只要安装了 DNS 服务器程序,这些信息也就被自动配置好了。

到这里所有的准备工作就都完成了。下面就来看一看域名解析是如何进行的

如图 1.16 所示,客户端首先会访问最近的一台 DNS 服务器(也就是客户端的 TCP/IP 设置中填写的 DNS 服务器地址),假设我们要查询www.lab.glasscom.com这台 Web 服务器的相关信息(图 1.16①)。由于最近的 DNS 服务器中没有存放www.lab.glasscom.com这一域名对应的信息,所以我们需要从顶层开始向下查找。最近的 DNS 服务器中保存了根域 DNS 服务器的信息,因此它会将来自客户端的查询消息转发给根域 DNS 服务器(图 1.16②)。根域服务器中也没有www.lab.glasscom.com这个域名,但根据域名结构可以判断这个域名属于 com 域,因此根域 DNS 服务器会返回它所管理的 com 域中的 DNS 服务器的 IP 地址,意思是"虽然我不知道你要查的那个域名的地址,但你可以去 com 域问问看”。接下来,最近的 DNS 服务器又会向 com 域的 DNS 服务器发送查询消息(图 1.16③)。com 域中也没有www.lab.glasscom.com这个域名的信息,和刚才一样,com 域服务器会返回它下面的glasscom.com域的 DNS 服务器的 IP 地址。以此类推,只要重复前面的步骤,就可以顺藤摸瓜找到目标 DNS 服务器(图 1.16⑤),只要向目标 DNS 服务器发送查询消息,就能够得到我们需要的答案,也就是www.lab.glasscom.com的 IP 地址了。(费这么大劲,只为拿到域名对一个的 IP 地址)。


dns 自顶向下的解析过程也印证了为什么,com 等域名是顶级域名。

这一段真的说的好清楚!!!

这个过程,在《Linux 实操篇 - 网络配置》的主机名解析过程分析 (Hosts、DNS)小节中也有记录。

通过缓存加快 DNS 服务器的响应

DNS 服务器有一个缓存功能,可以记住之前查询过的域名。如果要查询的域名和相关信息已经在缓存中,那么就可以直接返回响应,接下来的查询可以从缓存的位置开始向下进行。相比每次都从根域找起来说,缓存可以减少查询所需的时间。并且,当要查询的域名不存在时,“不存在"这一响应结果也会被缓存。这样,当下次查询这个不存在的域名时,也可以快速响应。

这个缓存机制中有一点需要注意,那就是信息被缓存后,原本的注册信息可能会发生改变,这时缓存中的信息就有可能是不正确的。因此,DNS 服务器中保存的信息都设置有一个有效期,当缓存中的信息超过有效期后,数据就会从缓存中删除。而且,在对查询进行响应时,DNS 服务器也会告知客户端这一响应的结果是来自缓存中还是来自负责管理该域名的 DNS 服务器

委托协议栈发送消息

数据收发操作概览

向操作系统内部的协议栈发出委托时,需要按照指定的顺序来调用 Socket 库中的程序组件。发送数据是一系列操作相结合来实现的。

如果不能理解这个操作的全貌,就无法理解其中每个操作的意义。因此,我们先来介绍一下收发数据操作的整体思路。

我们可以把数据通道想象成一条管道,将数据从一端送入管道,数据就会到达管道的另一端然后被取出。

建立管道的关键在于管道两端的数据出入口,这些出入口称为套接字。我们需要先创建套接字,然后再将套接字连接起来形成管道。

收发数据的操作分为若干个阶段,可以大致总结为以下 4 个。

  • 创建套接字(创建套接字阶段)

  • 将管道连接到服务器端的套接字上(连接阶段)

  • 收发数据(通信阶段)

  • 断开管道并删除套接字(断开阶段)

这 4 个操作都是由操作系统中的协议栈来执行的,浏览器等应用程序并不会自己去做连接管道、放入数据这些工作,而是通过调用 Socket 库中的程序组件来委托协议栈来代劳。本章将要介绍的只是这个"委托"的操作。关于协议栈收到委托之后具体是如何连接管道和放入数据的,我们将在第 2 章介绍。

创建套接字阶段

这个图和代码流程非常像。

Create -> connect -> read/write -> close(释放资源)

我们常见的 web 编程、文件读写都是分这几步。这么说起来,其实写入 socket 和写入文件,还真的没有什么区别。


应用程序是通过"描述符"这一类似号码牌的东西来识别套接字的。

连接阶段:把管道接上去

IP 地址是为了区分网络中的各个计算机而分配的数值。因此,只要知道了 IP 地址,我们就可以识别出网络上的某台计算机。但是,连接操作的对象是某个具体的套接字,因此必须要识别到具体的套接字才行,而仅凭 IP 地址是无法做到这一点的。所以需要端口来确定套接字,当同时指定 IP 地址和端口号时,就可以明确识别出某台具体的计算机上的某个具体的套接字。

如果说描述符(socket 库中的 socket 方法的返回结果是 Socket 的 Id)是用来在一台计算机内部识别套接字的机制,那么端口号就是用来让通信的另一方能够识别出套接字的机制

那为什么有的网址只有一个域名,没有端口号呢?服务器上所使用的端口号是根据应用的种类事先规定好的。比如:Web 是 80 号端口,电子邮件是 25 号端口

端口号的规则是全球统一的,为了避免重复和冲突,端口号和 IP 地址一样都是由 IANA(Internet Assigned Number Authority,互联网编号管理局)这一组织来统一管理的。


描述符:应用程序用来识别套接字的机制

IP 地址 + 端口号:客户端和服务器之间用来识别对方套接字的机制(这个角度很独特)


在操作系统内核中,通过源 ip+ 源端口 + 目的 ip+ 目的端口 + 协议,来唯一确定一个套接字

通信阶段:传递消息

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

调用 read 时需要指定用于存放接收到的响应消息的内存地址,这一内存地址称为接收缓冲区。于是,当服务器返回响应消息时,read 就会负责将接收到的响应消息存放到接收缓冲区中。由于接收缓冲区是一块位于应用程序内部的内存空间,因此当消息被存放到接收缓冲区中时,就相当于已经转交给了应用程序

断开阶段:收发数据结束

断开的过程如下。Web 使用的 HTTP 协议规定,当 Web 服务器发送完响应消息之后,应该主动执行断开操作 A,因此 Web 服务器会首先调用 close 来断开连接。断开操作传达到客户端之后,客户端的套接字也会进入断开阶段。接下来,当浏览器调用 read 执行接收数据操作时,read 会告知浏览器收发数据操作已结束,连接已经断开。浏览器得知后,也会调用 close 进入断开阶段。

HTTP 协议将 HTML 文档和图片都作为单独的对象来处理,每获取一次数据,就要执行一次连接、发送请求消息、接收响应消息、断开的过程。这个效率很低,因此后来人们又设计出了能够在一次连接中收发多个请求和响应的方法。在 HTTP 版本 1.1 中就可以使用这种方法,在这种情况下,当所有数据都请求完成后,浏览器会主动触发断开连接的操作。


拓展

建立通道主要是 TCP 协议实现的功能,耗时也是因为三次握手四次挥手耗时,而不是 HTTP 协议耗时,HTTP 协议只是按照格式生成了要发送的信息,并交给 TCP 协议层,让 TCP 协议层以 TCP 协议发送这个数据而已,那是不是一个 HTTP 信息就新建一个 TCP 连接,肯定不是的,一个 TCP 连接在创建后会用于多个 HTTP 请求的发送,在 HTTP1.1 中消息头中有一个属性 Connection,并且默认开启持久连接,即Connection:keep-alive,除非请求中写明Connection: close,那么浏览器和服务器之间是会维持一段时间的 TCP 连接,不会一个请求结束就断掉。回看本章:生成 HTTP 请求消息这一小节的图。

关于Connection请求头和Keep-Alive请求头,请看《HTTP/1.x》的Connection小节和Keep-Alive小节

那一个 TCP 连接中 HTTP 请求发送可以同时发送么(比如一起发三个请求,再三个响应一起接收)?不行。

HTTP/1.1 存在一个问题,单个 TCP 连接在同一时刻只能处理一个请求,意思是说:两个请求的生命周期不能重叠,任意两个 HTTP 请求从开始到结束的时间在同一个 TCP 连接里不能重叠。虽然 HTTP/1.1规范中规定了 Pipelining 来试图解决这个问题,但是这个功能在浏览器中默认是关闭的。因为 Pipelining 规定,一个支持持久连接(指的是 TCP 重用)的客户端可以在一个连接中发送多个请求(不需要等待任意请求的响应)。收到请求的服务器必须按照请求收到的顺序发送响应。为什么这么规定呢?由于 HTTP/1.1 是个文本协议,同时返回的内容也并不能区分对应于哪个发送的请求,所以顺序必须维持一致。比如你向服务器发送了两个请求GET/query?q=AGET/query?q=B,服务器返回了两个结果,浏览器是没有办法根据响应结果来判断响应对应于哪一个请求的。必须保证按照请求顺序返回响应也就导致了一个问题,在建立起一个 TCP 连接之后,假设客户端在这个连接连续向服务器发送了几个请求。按照标准,服务器应该按照收到请求的顺序返回结果,假设服务器在处理首个请求时花费了大量时间,那么后面所有的请求都需要等着首个请求结束才能响应。那还不如直接顺序发起请求算了,所以 Pipelining 实际用处不大,所以现代浏览器默认是不开启 HTTP Pipelining 的。

但是,HTTP/2.0 提供了 Multiplexing 多路传输特性,可以在一个 TCP 连接中同时完成多个 HTTP 请求。

关于HTTP/2.0是如何解决请求并发问题的,请看《HTTP 协议基础》的HTTP 协议的发展历史小节和《HTTP/2.x》


TCP 协议在连接和断开的时候会有三次握手、四次挥手的细节,这一小节没讲。

(猜测)TCP 的连接的复用很简单,第二个请求开始的时候,其序号只需要在第一个请求结束的时候服务器返回的 ACK 号上继续运算即可


此外,http 协议是无状态短链接,在服务端为了识别多个用户是否来自于同一个用户,即记录状态,开发了 Session 技术

0%