一、浏览器生成消息

1.生成HTTP请求消息

用户点击超链接或者在浏览器中直接输入 URL。


URL 全称 Uniform Resource Locator (统一资源定位符)。用于标识和定位互联网上的资源,如网页、图像、视频等。它是一个字符串,包含了访问资源所需的信息。它是URI的一种特定形式,用于定位资源的位置。

一个标准的 URL 可以包含协议(Protocol),主机名(Hostname),端口(Port),路径(Path),查询字符串(Query String)和 片段标识符(Fragment Identifier)。其中协议与主机名必须包含,其它内容为可选内容。

协议指定了访问资源所使用的通信协议,常见的协议有HTTP(超文本传输协议)、HTTPS(安全超文本传输协议)、FTP(文件传输协议)等。例如 http: , ftp: , file: , mailto: 等。

主机名指定了存储资源的服务器的名称或IP地址。如 “www.example.com”。

端口号用于指定服务器上提供服务的特定端口。如果未指定端口,通常使用默认端口号。例如,HTTP的默认端口号是80,HTTPS的默认端口号是443。

路径指定了服务器上资源的位置。如"/page.html"。服务器一般都会设置默认路径(一般称之为主页),URL中缺失路径信息时则直接使用默认路径。

查询字符串用于向服务器传递参数。它由一个问号(?)后面跟随的键值对组成,键和值之间用等号(=)连接,不同键值对之间使用与号(&)分隔。如"https://www.example.com/search?q=keyword"中的"q=keyword"。

片段标识符用于指定资源中的特定片段或位置。它由一个井号(#)后面跟随的片段名称组成。如"https://www.example.com/page.html#section1"中的"section1"。


浏览器首先要解析URL,并根据网址的含义来生成请求消息。

假设浏览器使用 HTTP 协议来访问服务器。


HTTP(HyperText Transfer Protocol)是一种用于在Web浏览器和Web服务器之间传输数据的应用层协议。它是基于客户端-服务器模型的协议,客户端发起请求,服务器提供响应。

定义了不同类型的请求方法(如GET、POST、PUT、DELETE等)和状态码(如200 OK、404 Not Found等),以便客户端和服务器之间进行适当的交互。

HTTP消息由请求消息和响应消息两种类型组成,它们都有相似的基本格式,包括起始行、头部字段和可选的消息体。当客户端发送HTTP请求到服务器时,服务器会对请求进行处理,并生成一个HTTP响应消息返回给客户端。

HTTP请求消息格式

  • 起始行:包含请求方法、请求的URL和HTTP协议版本。例如:GET /path/to/resource HTTP/1.1

  • 头部字段:以键值对的形式描述请求的各种属性,如Host、User-Agent、Content-Type等。每个字段占据一行,以冒号分隔键和值。例如:

    Host: www.example.com
    User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.96 Safari/537.36
    Content-Type: application/json
    
  • 空行:头部字段结束后需要一个空行来表示头部结束。

  • 消息体(可选):对于某些请求(如POST请求),可以包含一个消息体,用于传输请求的数据。消息体的格式和内容根据请求的需要而定。

HTTP响应消息格式

  • 起始行:包含HTTP协议版本、状态码和状态消息。例如:HTTP/1.1 200 OK

  • 头部字段:与请求消息类似,包含描述响应属性的键值对。例如:

    Content-Type: text/html
    Content-Length: 1024
    Server: Apache/2.4.29 (Ubuntu)
    
  • 空行:头部字段结束后需要一个空行来表示头部结束。

  • 消息体(可选):对于某些响应(如包含实体主体的响应),可以包含一个消息体,用于传输响应的数据。


1 条请求消息中只能写一个 URI,因此如果需要获取多个文件,必须对每个文件单独发送 1 条请求。

比如说我要显示的一个网页上有文字也有图片,则先发送 1 条请求获得文本文件,浏览器在显示文字时会对标签进行搜索,当搜索到图片相关标签时,会为显示图片预留空间,这时再向服务器发送请求消息以获取图片资源。

2.查询接收方IP地址

生成请求消息后,浏览器委托操作系统向Web服务器发送请求。

但是我们首先需要向DNS服务器查询接收方域名对应的IP地址。


互联网和局域网都是基于 TCP/IP 的思路进行设计的。可以理解为一些小的子网(用集线器连接起来的几台计算机)通过路由器连接起来组成一个大的网络。

在网络中,所有设备都会被分配一个地址,即 IP 地址。我们可以通过 IP 地址访问对应的设备。

实际的 IP 地址是一串32比特的数字,以一字节为一组分成四组。用十进制表示后用原点隔开。其中一部分是网络号,一部分是主机号。我们需要一串附加信息,即子网掩码,来表示 IP 地址的内部结构。

如图所示:

需要注意的是,网络号与主机号的边界不一定与字节的边界是吻合的。

主机号全为0时代表整个子网,主机号全为1时代表向子网上所有设备发送包,即“广播”。


域名和 IP 并用比仅用服务器名称确定通信对象更合理。最主要的是大小问题,一个 IP 地址四字节,而一个域名最短也要十几字节,往往需要几十字节。处理大量域名会使路由器不堪重负。即使使用更高性能的路由器,它的速度依旧是有限的。使用 IP 地址显然效率更高。

所以我们让人使用域名(当然直接用IP也行),让路由器使用 IP。既照顾到了我记不住电话号码的特点,也照顾到了路由器的工作效率。

这时我们就需要设计一种机制,实现通过域名查询 IP,或者通过 IP 反查域名。伟大的 DNS 服务器就诞生了。


DNS(Domain Name System)全程为域名服务系统,可以为各种信息关联相应的名称,而不仅仅是域名。

我们的计算机上有对应的 DNS 解析器,它实际上是一段程序,包含在操作系统的 Socket 库(包含的程序组件可以让其他的应用程序调用操作系统的网络功能)中。调用解析器后即可向最近的 DNS 服务器发送查询信息,随后 DNS 返回响应消息。再由解析器取出查询到的 IP 存入指定内存供后续使用。

DNS 服务器上会事先保存三种消息的记录数据。包括域名、Class(当今除了互联网没有其他网络,它的值永远是代表互联网的IN)、记录类型(类型为A,表示域名对应的是 IP 地址,类型为MX则表示对应的是邮件服务器,当然不止这两种)。

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


域名存在一定的层次结构,例如:www.baidu.com ,越靠右表示层级越高,以点分割。

具有层次结构的域名信息会注册到 DNS 服务器中,一个域的信息是作为一个整体放在 DNS 服务器中的,一个域不能拆开放在多个 DNS 服务器中。当然,一个 DNS 服务器可以存放多个域的信息。

域名的层次结构可以类比成一个电话号码,其中不同的级别就像电话号码的各个部分。DNS服务器则像是一个电话簿,它存储了域名和IP地址的对应关系。

  • 根DNS服务器就像是电话簿中的国家代码,它是整个DNS系统的顶级部分。它存储了全球顶级域名(如.com、.net、.org)的信息,类似于存储了全世界的电话号码区域代码。
  • 顶级域DNS服务器就像是电话簿中的区域代码,它负责管理特定顶级域名(如.com)下的二级域名(如example.com)。它存储了二级域名对应的DNS服务器地址,就像存储了某个区域的电话号码前缀。
  • 二级域DNS服务器就像是电话簿中的具体号码,它负责管理特定二级域名(如example.com)下的子域名(如mail.example.com、blog.example.com)。它存储了子域名的IP地址,就像存储了具体的电话号码。

当你输入一个域名时,你的设备首先会询问本地DNS服务器,就像你查找电话号码时先看本地电话簿。如果本地DNS服务器有缓存的解析结果,它会立即返回IP地址。如果没有,本地DNS服务器会向更高级别的DNS服务器发出查询请求,就像你从本地电话簿找不到号码时,要向更大的电话簿查询。

查询过程如图所示:

另外,DNS 服务器有一个缓存功能,可以记住之前查过的域名。所以经常不必从根域开始找。但是信息被缓存后,如果原本的注册信息发生了改变,这时缓存中的信息就不正确了。所以缓存信息是有时效性的,需要及时清理更新。

3.委托协议栈发送消息

向操作系统内部的协议栈发出委托时,需要按照指定的顺序来调用 Socket 库中的程序组件。

要在服务器和客户端之间收发消息,需要在二者之间建立通信管道。(当然这是一个抽象的概念)

建立管道的关键在于管道两端的数据出入口(不是真的出入口),即套接字。服务器程序一般会在启动后就创建好套接字并等待客户端连接管道。管道连接时由客户端发起,断开时可以由任意一方发起。

管道断开后删除套接字,通信结束。

以上操作均由操作系统中的协议栈完成。

创建套接字阶段:

实际上我们常常进行多个数据的通信操作,比如同时访问多台web服务器,所以会创建多个不同的套接字。用来识别不同套接字的方法就是为每个套接字分配描述符。

连接阶段:

通过调用 Socket 库中的 connect 程序组件来完成这一操作。调用 connect 时需要三个参数:描述符(创建套接字后由协议栈返回)、服务器 IP 地址(通过 DNS 查询得到)、端口号。

关于端口号的作用:

IP 地址就像电话号码,我可以通过拨打电话号码呼叫持有电话号码的一端,但是接听人不一定是我们想找的。这是我们告诉电话另一头“我要找xxx”,xxx即类似于端口号。

为什么不能让 描述符 承担端口号的作用?

目前看来,描述符仅限委托创建套接字的应用程序交互时使用,网络连接的另一方不知道描述符相关信息。

总的来说,描述符是用来在一台计算机内部识别套接字的机制,端口号就是用来让通信的另一方能够识别出套接字的机制。


客户端和服务器的端口号是怎么被对方得知的?

服务器的端口号是根据应用的种类事先规定好的,比如常见的Web对应80号端口,电子邮件对应25号端口。

客户端在创建套接字时,协议栈会为这个套接字分配一个端口号并在执行连接操作时将它告知服务器。


通信阶段:

使用 Socket 库中的 write 程序组件。调用 write 时,需要指定描述符和发送数据,接着协议栈会将数据发送到服务器。

服务器向客户端返回响应消息后,客户端通过 read 程序组件接收消息。调用 read 时需要指定用于存放接收到的相应地址的内存地址,即接收缓冲区(一块位于应用程序内部的内存空间)。

断开阶段:

调用 close 组件实现断开操作。不同的协议规定,首先断开的一放既可能是服务器也可能是客户端。

二、用电信号传输TCP/IP数据

TCP/IP 的简单分层结构:

工作大体自上而下委托。

协议栈的上半部分主要是负责用 TCP 和 UDP 协议收发数据的部分,接受应用程序的委托,执行收发数据的操作。

下半部分是用 IP 协议控制网络包收发操作的部分。IP 同时包含 ICMP 协议(告知网络包传送过程中产生的错误以及各种控制信息)和 ARP 协议(根据 IP 地址查询的以太网的 MAC 地址)。

1.套接字的本体及创建过程

套接字本质上是协议栈内部一块用于存放控制信息的内存空间。其中记录了用于控制通信操作的控制信息。(IP 地址、端口号、通信操作的进行状态等)

协议栈是根据套接字中记录的控制信息来工作的。

Windows系统可以通过 netstat 显示套接字内容。(a:显示正在通信的套接字,也显示包括尚未开始通信等状态的所有套接字,n:显示 IP 地址和端口号,o:显示使用该套接字的程序 PID)


从左到右分别是:协议类型、本地IP、通信对象IP、状态和进程标识符(操作系统为了标识程序而分配的编号)

Local Address 显示 0.0.0.0 表示不绑定 IP 地址(?)

Foreign Address 显示 0.0.0.0 表示通信尚未开始。

通信状态一栏,LISTENING 表示等待对方连接。ESTABLISHED 表示正在进行数据通信。

创建套接字的过程:

协议栈分配存放套接字所需内存空间 ——> 在套接字的内存空间写入初始状态控制信息 ——> 将表示这个套接字的描述符告知应用程序

2.连接服务器的过程

连接并不是物理上的连接,而是指通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作。

连接操作主要有以下目的

  • 把服务器的 IP 地址和端口号等信息告知协议栈
  • 客户端向服务器传达开始通信的请求
  • 为执行数据收发操作时用来临时存放收发数据的缓冲区分配内存空间

通信操作中使用的控制信息可以分为两类:

  1. 客户端和服务器互相联络时交换的控制信息,即头部中记录的信息

    这些信息在整个通信过程中都会发挥作用。这一类控制信息的内容大小由协议进行定义,会被添加在客户端与服务器之间传递的网络包的开头。在连接阶段,数据收发还没有开始,网络包中只有这些控制信息位于网络包的开头,因此被称为头部。各协议有不同的控制信息,因此以 xx头部 命名以示区分。(TCP头部、IP头部、MAC头部)

  1. 保存在套接字(协议栈中的内存空间)中,用来控制协议栈操作的信息

    保存了程序传递来的信息、通信对象接收到的信息、收发数据操作的执行状态等信息。套接字的控制信息本质上和协议栈是一体的,因此控制信息的内容会根据协议栈本身的实现方式不同而不同。协议栈的实现方式主要和操作系统的不同有关。但是协议栈的实现方式不同不影响不同系统之间的通信。(?)

连接操作的实际过程

1.调用 Socket 库中的 connect

connect(<描述符>,<服务器的 IP 地址和端口号>,...)

2.以上的调用提供了服务器的信息,这些信息将被传递给协议栈中的 TCP 模块,接着 TCP 模块会与该 IP 地址对应的对象,即服务器的 TCP 模块交换控制信息。

  • 客户端创建一个包含表示开始数据收发操作的控制信息的头部,将头部中的控制位的 SYN 比特设置为 1,并设置适当的序号和窗口大小
  • TCP 模块将信息传递给 IP 模块并委托它进行发送
  • 网络包通过网络到达服务器,由服务器的 IP 模块进行接收并传递给服务器的 TCP 模块
  • 服务器的 TCP 模块根据接收到的 TCP 头部的信息,从处于等待连接状态的套接字中找到与 TCP 头部中端口号一致的套接字。
  • 套接字写入相应信息,状态改为正在连接。
  • 服务器的 TCP 模块返回响应,整个过程和客户端类似,此外,需要将 ACK 控制位设为 1,表示已经接收到相应的网络包。
  • 服务器的 TCP 模块将 TCP 头部传递给 IP 模块,委托 IP 模块向客户端返回响应
  • 网络包回到客户端,通过 IP 模块到达 TCP 模块,通过 TCP 头部的信息确认连接服务器的操作是否成功。若 SYN 为 1 则表示连接成功,向套接字中写入服务器的 IP 地址,端口号等信息,同时将状态改为连接完毕。
  • 客户端将 ACK 比特设置为 1 并发回服务器,告诉服务器刚才的响应已收到。

服务器接收到最后这个返回包,连接才算全部完成。

3.协议栈的连接操作结束,connect 执行完毕,控制流程被交回到应用程序。

3.数据的收发

1.将 HTTP 请求交给协议栈

调用 write 将要发送的数据交给协议栈,协议栈收到数据后执行发送操作:

  • 应用程序在调用 write 时会指定发送数据的长度。但是协议栈并不关心程序送来的数据究竟是什么内容。
  • 大多数情况下,协议栈收到数据后会将其暂时存放在缓冲区内,而不是直接发送出去。这么做是为了提高网络效率。至于要积累多少数据才能发送,不同种类和版本的操作系统有所不同。如果数据过大超过最大传输单元的大小,协议栈会对它进行拆分,并保证每一块数据前面都加上 TCP 头部。

数据积累量主要有两个判断要素:

一个是网络包能容纳的数据长度。协议栈会根据几个参数进行判断。

MTU:一个网络包的最大长度,以太网中一般是1500字节。

MSS:MTU - 头部的长度

另一个要素是时间。协议栈内部有一个计时器便作此用。

但这两个要素其实是相互矛盾的。如果长度优先,那么网络的效率会提高,但可能会因为等待填满缓冲区而延迟。如果时间优先,延迟时间减少,网络效率会降低。

其中的平衡由协议栈的开发者自行把控。同时协议栈也为应用程序保留了控制发送时机的余地。开发者可以自行指定。


2.确认网络包已收到

TCP 具备确认对方是否成功收到网络包以及当对方没收到时进行重发的功能。

因此在发送包之后需要进行确认操作,确认操作有以下原理:

  • TCP 模块在拆分数据时,会计算好每一块数据相当于从头开始的第几个字节,并将算好的字节数写在 TCP 头部中。对应 TCP 头部中的“序号”字段。接收方用整个网络包的长度 - 头部的长度 即可得到数据的长度。
  • 通过这些信息,接收方可以检查收到的网络包是否有遗漏。如果确认无遗漏,接收方会将目前为止接收到的数据长度加起来,计算出总共收到的字节数并将这个数值写入 TCP 头部的 ACK 号中发送给发送方(同时需要将控制位中的 ACK 比特设置为 1,代表 ACK号 字段有效),以上操作称为确认响应。

实际通信过程中,“序号”并不是从 1 开始的,而是用随机数计算出的初始值,以防通信过程被恶意预测。

初始序号在进行连接操作时会告知对方(将 SYN 设为 1 那里)。

双向传输是一样的。

通过 “序号” 和 “ACK号” 可以确认接收方是否收到了网络包。

网卡、集线器、路由器都没有错误补偿机制,一旦检测到错误就直接丢弃对应的包。如果发生网络中断,服务器宕机等问题,TCP 在尝试几次重传无效后会强制结束通信并向应用程序报错。

3.根据网络包的平均往返时间调整 ACK 号等待时间

网络传输繁忙时发生拥塞,服务器的物理距离也存在远近,ACK 号的返回有快有慢,等待时间需要根据实际情况进行调整。

TCP 采取了动态调整等待时间的方法,根据 ACK 号返回所需要的时间来判断。TCP 会在发送数据过程中持续测量 ACK 号的返回时间并做出调整。

4.窗口

TCP 采用滑动窗口方式来管理数据发送和 ACK 号的操作。发送一个包后,不等待 ACK 号返回,而是直接发送后续的一系列包。这样可以有效利用等待 ACK 号的这段时间。

但是滑动窗口方式存在一些问题,不等待 ACK 号返回就发送大量包,很可能出现发送包的频率超过接收方处理能力的情况。

接收方的 TCP 收到包后把它们放进缓冲区,计算 ACK 号后将数据块组装还原并传递给应用程序。如果传太快了缓冲区可能会溢出,则说明超出了接收方的处理能力。

为了解决这一问题,接收方需要告知发送方自己最多能接收多少数据,然后发送方根据这个值对数据发送操作进行控制。

在 TCP 调优参数中,窗口大小代表着能够接受的最大数据量。

数据的接收方无需每次都向发送方告知窗口大小,因为发送方可以自行计算。当接收方将缓冲区中的数据取出传递给应用程序,导致缓冲区容量变化时,才需要更新窗口大小。

接收方在发送 ACK 号和窗口更新时,不会马上发包,而是会等待一段时间,如果这段时间内产生了其它通知操作,则会将所有信息合并打包发送。这样可以减少发送包的数量。

5.接收 HTTP 响应消息

浏览器在委托协议栈发送请求消息之后,会调用 read 程序来获取响应消息。随后控制流程转移到协议栈,由协议栈执行接收操作。接收数据同样暂存到缓冲区中。

协议栈尝试从缓冲区中取出数据并传递给应用程序,此时请求消息刚刚发送出去,响应数据可能还没返回。此时缓冲区中是空的。这时,协议栈会将此项工作暂时挂起,等服务器返回的响应消息到达之后再执行接收操作。

协议栈接收信息的过程(总结):

检查收到的数据块和 TCP头部的内容,判断是否有数据丢失,没有问题则返回 ACK 号 ——> 将数据块暂存到接收缓冲区中,并将它们按顺序还原 ——> 将数据交给应用程序 ——> 找到合适的时机向发送方发送窗口更新

4.从服务器断开并删除套接字

1.断开连接

应用程序判断所有数据均已发送完毕时,数据发送完毕的一方会发起断开过程,不同的应用程序会选择不同的断开时机。协议栈在设计上允许任意一方先发起断开过程。

假设服务器先发起断开:

服务器一方的应用程序调用 close ——> 服务器的协议栈生成包含断开信息的 TCP 头部,具体来说就是将控制位中的 FIN 比特设为 1 ——> 协议栈委托 IP 模块向客户端发送数据 ——> 服务器的套接字中记录下断开操作相关信息

客户端方面:

当收到服务器发来的 FIN 为 1 的 TCP 头部时,客户端的协议栈会将自己的套接字标记为进入断开操作状态 ——> 为了告知服务器已收到 FIN 为 1 的包,客户端向服务器返回一个 ACK 号 ——> 告知应用程序来自服务器的数据已全部收到 ——> 调用 close 结束收发操作 ——> 生成 FIN 为 1 的 TCP 包,委托 IP 模块发送给服务器

等到服务器返回 ACK 号 ,二者通信就完全结束了。

2.删除套接字

通信结束后,套接字会在等待一段时间后被删除而不是立即被删除。这是为了防止断开操作过程中有信息发送失败,而套接字率先被删除而引发错误。

一般等到网络中的包重传全部结束后删除套接字,几分钟的时间比较多。

5.TCP 整体流程总结

如图:

Posted on 2023-07-20 20:03  Chen,qiuyan  阅读(147)  评论(0编辑  收藏  举报