打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
计算机学习笔记:网络是怎么连接的(上)

这篇文章是我之前写的(2023年1月),也属于计算机学习笔记一部分。现在整理一下发上来,

作为一个涉世未深的服务端人员,在日常的开发工作中,有时候会遇到一些这样的“窘境”:比如在排查一个服务的故障的时候,前端感到很疑惑,自己是照规范发的请求呀,这还有请求的失败记录呢,而服务端也很疑惑,从日志来看根本没有收到请求呀。最终可能一番折腾,发现是因为Nginx超时了,这个可怜的请求就像一艘小小的帆船,葬身在网络的汪洋大海之中。网络确实像是大海,平时风平浪静,波澜不兴,让你感觉不到它的存在,但是可能不知道什么时候就突然阴风怒号,浊浪排空了。而工作进程,也因此被耽误。

所以当时我找了一本网络相关的书来看:户根勤的《网络是怎样连接的》。看完觉得收获挺大的,尤其觉得这本书写的特别好,篇幅不长,定位是科普、通俗读物,但其实还是讲的还挺细挺深的,我觉得很适合有一定基础的读者,作为复习和加深理解。于是对这本书的主要内容进行了梳理,也加入了一些自己另外学到的内容。

连接的全流程

浏览器生成请求消息

    让我们从在浏览器中输入一个网址后点击回车开始说起。

    首先,浏览器会对你输入的网址进行解析并生成一个HTTP消息。这个消息中包含了你要访问的服务器的域名,请求的类型,你传过去的内容,你能接受的内容等等。关于消息的结构作用这一块大家也都比较熟悉了,我们就快速略过。直接往下,讲讲消息是怎么发出去的。


DNS解析

    上面我们说了浏览器会解析网址并生成HTTP消息,但要注意的是:浏览器本身并不具备将消息发送到网络中的功能。这一功能实质上是浏览器委托操作系统实现的。而在委托操作系统发送消息时,我们必须提供给操作系统通信对象的IP地址而不是域名。

    因此我们需要先解决一个问题:就是网址中服务器域名对应的IP地址是什么?那么这个问题是如何解决的呢?简单来说就是:DNS服务器解决的。当我们去询问DNS服务器'www.baidu.com'的IP地址是什么的时候,它就会告诉我们它的ip地址是168.1.1这样。

    但我们不想止步于此,而是想再深入一些:浏览器是如何向DNS服务器发出查询的呢?

    首先,对应于DNS服务器,我们的计算机上有一个相应的DNS客户端的部分,它的名字叫DNS解析器。这个解析器实际上是一段程序,包含在操作系统的Socket库中。这个库(以我的理解就和我们平时说的jar包差不多含义),是一堆通用程序组件的集合。Socket库也是一种库,其中包含的程序组件可以让其他的应用程序调用操作系统的网络功能,比如:创建Socket, 链接Socket等等,而解析器则是这个库中的一种程序组件。

    从图中可以看到,当浏览器需要查询一个域名对应的IP的时候,它实际上是调用了操作系统的Socket库中的一个gethostbyname()的方法,在这个方法中,操作系统会帮我们把这一系列事情做好(生成UDP消息,发给DNS服务器,从响应中取出IP地址放到内存中),最后我们从内存中取出我们要的ip地址就可以了。这里我们衍生出两个问题:

  1. 我们需要ip地址来访问服务器,而ip地址需要DNS服务器告诉我们,但我们又怎么知道DNS服务器的ip地址呢?听起来像一个鸡生蛋,蛋生鸡的问题。

  2. 从DNS服务器获得域名对应的ip具体是怎么实现的呢?世界上的域名数量数不胜数,是怎么做到高效准确查询的呢?

    第一个问题的答案其实很简单,就是这个DNS服务器的ip是事先设置好的。解析器会根据这个ip地址来发送消息。第二个问题则相对复杂。为了理解这个过程,我们需要先了解DNS服务器群的结构。这个结构有上下级的概念,其中最高一层是根域服务器,在全世界仅有13个。而这些信息也会预先配置在计算机中。根域名服务器往下是如com, jp, cn这样的层级,再往下是glasscom, baidu等层级。以此类推。而当我们要查询某个域名对应的IP的时候,流程则如下图(注意我们一开始访问的其实是离我们最近的服务器):

    对DNS的查询需求显然是巨大的,因此DNS服务器中有一个缓存功能(浏览器中也有),如果要查询的域名已经在缓存中,就会直接返回响应,而不必再每次从根域名开始查找。其中每个保存的信息都会设置一个有效期,超过有效期后数据会从缓存删除以解决域名注册信息改变的问题。

QQ登录问题

    在那个头发能遮住左眼,聊天中充满了“我晕”,“你是GG还是MM”的年代里。有时会出现一种科学无法解释的现象:就是浏览器无法上网,QQ却能正常登陆。

    那么今天我们终于能理解这个背后的原因了:其实就是DNS配置错了。浏览器无法获得域名对应的IP地址,自然也就无法访问了。而QQ是直接通过IP连接服务器的,因此可以正常登陆。

协议栈(通信)

    至此,我们就获得了目标服务器的IP地址了,接下来就可以委托操作系统内部的协议栈向这个地址发消息了。这一步其实不仅限于浏览器,对于各种使用网络的应用来说都是共通的。

    发消息这个过程需要按照指定的顺序来调用Socket库的多个程序组件,以在两台计算机之间建立并维护一条数据通道,我们可以把它想象成一条管道,将数据从一端送入管道,数据就会到达另一端然后被取出。这样的管道两端的出入口就叫socket,也叫套接字(我每次看到这个词脑海中浮现的都是一只袜子(sock)...)。我们大致捋一下收发数据操作的4个阶段:

  1. 创建套接字阶段

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

  3. 通信阶段(收发数据)

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

    这一部分也是网络连接的核心(尤其对于软件工程师来说)。接下来我们依次对这几个阶段进行介绍

创建套接字阶段

    计算机经常会同时进行多个数据的通信操作。比如说我们在浏览器中打开两个窗口,同时访问两个web服务器,那么这就是两套数据的收发。所以我们需要创建两个不同的套接字来收发数据。

    那么进一步地,我们需要有一些信息来区分这些不同的套接字,而这也是这个阶段所做的事情。调用socket库中的socket方法后,会返回一个叫描述符的东西,这个东西是用于识别特定的套接字的,在后面的操作中,只要我们出示这个描述符,协议栈就知道我们希望用哪一个套接字来连接或收发数据了。

    具体到物理层面,协议栈首先会分配用于存放一个套接字所需的内存空间,并往里面存入相关的控制信息,至此,创建套接字的操作就完成了。

连接阶段

    客户端应用程序创建好套接字后,就可以委托协议栈将客户端创建的套接字与服务器那边的套接字连接起来了。这时候我们其实不知道要和谁通信,也不知道对方的信息。所以说连接实际上是通信双方交换控制信息(比如ip, 端口等)。

    应用程序将通过调用Socket库中的connect()组件完成这一操作。而调用这一组件需要提供三个信息:描述符,服务器IP地址以及端口号。前两个我们已经有了,那端口号从哪儿来呢?这个端口号是事先约定的,比如http请求是80端口,https是443端口,电子邮件是25端口等等。

    具体来看看:当调用connect()时,传入的信息会传递给协议栈中的TCP模块,然后TCP模块就会与其中IP地址对应的对象(也即服务器)的TCP模块交换控制信息。这一交互过程就是我们常说的三次握手。三次握手成功后,连接就成功建立起来了。

三次握手

    所有TCP连接都要经历三次握手。这一点对于使用TCP连接的应用的性能产生了非常大的性能影响,因为每次传输应用程序,都必须经历一此完整的往返。比如下图中,我从中国连接架设在美国的服务器,连接耗时就花去300ms。

    三次握手带来的延迟使得每创建一个新的连接都要付出很大代价,因此想办法重用连接能有效提高TCP应用的性能。

    在HTTP1.0中,连接是不能复用的,就是说我点开一个页面,里面有3个http请求,那就要建立3次TCP连接。

    到了HTTP1.1,它是默认开启持久连接的,同样3个http请求,可以只建立1次TCP连接,可以看到我打开这个页面里的多个请求中,只有第一个请求有建立连接的时间损耗。但是浏览器需要处理完一个HTTP请求才能才能发起下一个。就是比如我两个tab同时访问网站A,那么这里也会建立两次TCP连接。所以这种复用应该是串行的。

    再到了HTTP2.0中,这一点再次得到了优化。实现了多路复用,可以只使用一个连接即可并行地发起多个请求和响应。

    此外在Linux 3.7以后的内核支持一种叫TFO(TCP fast open)的东西,可以提高页面加载时间。

通信阶段

    至此,套接字就链接起来了,接下来就可以调用Socket库的write组件,传入描述符和数据内容就可以向对方发送数据。而当消息返回后,可以通过Socket库的read组件委托协议栈来读取响应的数据。调用read时需指定用于存放响应数据的内存地址(接收缓冲区)。这个接收缓冲区是一块位于应用程序内部的内存空间,因此当消息存放到此处时,其实相当于已经转交给了应用程序。

    TCP是可靠的连接。而实现这一点的手段就在于消息的确认和重传机制。从图中可以看到会通过ACK来确认包被收到。如果没有收到对应的ACK,就认为包没有被收到,要重发。而这就涉及到两个问题:

    第一个是对于等待ACK的时间设置。

    有时候网络繁忙会发送拥塞,如果我们等待时间太短的话,可能我们以为包丢失了重传了包,结果ACK又到了,这就进一步加重了网络的堵塞。但是等待时间太长的话,包的重传就会有很大的延迟,也会影响速度。尤其考虑到服务器物理距离的远近,ACK的返回时间会有很大的波动。因此TCP没有为等待时间设置一个固定的值,而是采用了动态调整的方式:它会在发送数据的过程中持续测量ACK号返回的时间,如果ACK号返回变慢,则相应地延长等待时间,反之则缩短等待时间。

    第二个则是如何在带确认机制的情况下提高发送的效率

滑动窗口

    如果每发送一个包就等待一个ACK号,等ACK号到了再发送下一个包,在等待的过程中什么都不错,那就太浪费了。因此TCP其实会采用一种叫滑动窗口的机制来提高数据收发的效率,以将等待ACK号的这段时间利用起来。

    从图中可以看到,使用滑动窗口后,收发的效率大大提高了但是这样就可能出现发送包的频率超过对方接受处理的能力的情况,因此在通信的过程中,接受方需要告知发送方自己最多能接受多少数据,然后发送方根据这个值对数据发送操作进行调整。

(注意,窗口有两个,一个拥塞窗口,一个流量窗口,收发速度取这两者的最小值)

    有时候我们的流量控制窗口可能制约我们无法充分利用带宽,所以要注意对流量控制窗口大小的观察与调整。比如假设流量窗口大小为16KB,往返时间为100ms。那么不管两端的实际带宽有多大,这个TCP连接的传输速率都不会超过1,31Mbps。所以你的带宽很高,传输速度却很慢,那窗口大小可能就是主要原因。

    还需要注意的一点是,TCP的收发速度不仅有接收方接收能力决定,还受到网络状况的影响。尤其在刚建立起连接时,TCP会采用一种“慢启动”的算法。也即将窗口(拥塞窗口)大小设置在一个比较小的值(4个TCP端),然后在分组被确认后成倍增长,但一旦丢包率增加,又会成倍减少再缓慢上升。

    无论带宽多大,每个TCP连接都必须经过慢启动阶段,所以我们其实无法一上来就完全利用连接的最大带宽。尤其在初始窗口是4段的情况下。但是现在很多操作系统已经更新了内核,采用了增大后的值(RFC6928规定的10段)

断开阶段

    想发的数据发完后,就进入断开阶段。我们常说的四次挥手也是发现在这一阶段。通信的其中一方会调用close来断开连接,接下来另一端调用read执行接收数据操作时,read会告知它通信阶段已结束,因此它也会调用close进入断开阶段。至此。整个通信过程就结束了。

网卡

    前面我们一直提到的网络包,直到协议栈的IP模块这个环节,他们还都是存放在内存中的一串数字信息,没法直接发送给对方。想要在网线上传输,我们需要将数字信息转换为电信号或光信号。而负责这个操作的是电脑中的网卡。当我们打开计算机,操作系统被启动后,网卡驱动程序会对网卡进行初始化,然后网卡进入可用的状态。(网卡的相关结构见下图)

    下面我们看下网卡是如何将包转换成电信号并发送进网线中的。

网卡发送包

    首先网卡驱动从IP模块获取包,将其复制到网卡内的缓冲区中,然后向MAC模块发送发送包的命令。

    接下来MAC模块从缓冲区将包取出,并在开头加上报头和起始帧分界符,在末尾加上用于检测错误的帧校验序列(FCS)。这里我们停一下,解释一下这几个东西和他们的作用:

    报头是一串长度为56比特像1010101010...这样的1和0交替的比特序列,它的作用是确定包的读取时机,接收方在收信号时,遇到这样的波形就可以判断读取数据的时机(就像裁判员的:“各就位,预备...”)

    而FCS则是一个通过公式对包中从头到尾的所有内容计算出的一串32比特的序列,用于检查包传输过程中因噪声导致的波形紊乱、数据错误。如果原始数据中某一个比特发送变化,那么接收方计算出的FCS和发送方计算出的FCS就会不一样。我们就能根据此判断此包有没有错误,有则将其抛弃。

    然后我们讲一下我们是如何通过电信号来读取数据的:

    用电信号表达数字信息时,我们需要让0和1两种比特分别对应特定的电压和电流,通过电信号来读取数据的过程就是通过测量信号中的电压和电流变化,还原出0和1的两种比特值。然而有一个问题是,当出现连续一串的0或1时,如何判断出比特的界限在哪里?答案是将数据信号和时钟信号叠加。由于时钟信号是按固定频率变化的,可以根据时钟周期和接收到的信号逆推出真实的数据信号。

    接下来PHY模块会将这些电信号转为可在网线上传输的格式,并通过网线发送出去(以太网规格中对不同的网线类型和速率以及对应的信号格式进行了规定)。

网卡接收包

    接下来我们说一说网卡接收网络包时的操作过程,过程会反过来:

  1. 首先PHY模块会将信号转换成通用格式发送给MAC模块,

  2. MAC模块再从头开始将信号转换为数字信息,存放在缓存区中。当到达信号末尾时,会检查FCS,如果自己计算的和给定的FCS有差异,则将此包丢弃。

  3. 如果FCS校验没问题,就看一下MAC头部中接收方MAC地址和自己的MAC地址是否一致,以判断是不是发给自己的包,如果不是则直接丢弃。否则就将包放入缓存区中。

  4. 然后网卡会通过中断的方式告知操作系统收到了一个包。然后对应的中断处理程序会去调用网卡驱动从网卡的缓存区中取出收到的包,并通过MAC头部判断包的协议类型,比如是TCP/IP或AppleTalk等等。

  5. 网卡会将包交给对应的协议栈,接下来协议栈会判断这个包应该交给哪个应用程序去处理。

    其实我们的数据包从我们电脑网卡出来后,并不是直接就进入互联网了。它需要由交换机、路由器等设备一步步转发,从局域网到接入网再进入到互联网。但这篇文章已经有点长了,关于这个的过程我们留到下一篇再做详细介绍

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
40张图大揭秘:输入网址后到底发生了什么
「面试」计算机网络基础知识点
理解 TCP/IP 网络栈
TCP/IP建链三次握手和断开链接四次握手
Http、socket和TCP/IP
Http和Socket连接区别 - The World Is Completed and ...
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服