从输入URL到打开页面到底发生了什么
date
Jun 8, 2021
slug
overview-of-computer-networks
status
Published
tags
计算机网络
type
Post
summary
从输入URL到打开页面到底发生了什么
一张提纲挈领的图

几个问题
- 为什么网络协议要分层,这样的优缺点是什么
- 分层以后,每层有每层的协议,可以独立工作,一层的改变并不影响另一层
- 但是这样分层可能导致同样的逻辑在这层用一次,在那层用一次
- 分这么多层性能会差嘛
- 并不会,每一层层层封装最终也不过是多出来很少字节的数据,性能浪费没有想象的那么高
1 应用层
1.1 解析URL
URL (Uniform Resource Locator) 浏览网页差不多是从这一步开始,这里介绍下url的格式,这个链接的blog说的很详细,但是由于绝大多数网页都是HTTP/HTTPS协议,所以这一部分可以省略,同样,80端口号是默认的,一般也不用写,而由于http支持服务器将网页重定向,所以www也可以不用写,因此用户访问网页的时候只用键入一部分就可以了,比如
https://www.mcppy.com
和mcppy.com
都可以进入同一个网站。1.2 生成HTTP请求消息
解析完URL之后,我们就知道应该访问的目标在那里了,接下来浏览器会用HTTP协议来访问web服务器
HTTP协议:
他定义服务端与客户端交互消息的内容和步骤。
客户端发出一个request,客户端回应一个response
request报文主要包含两个部分
- 对什么(url)
- 进行怎么样的操作,这部分被称作方法,有人叫作http动词这部分MDN有解释

整个http请求报文大概是这样的

- 第一行是请求行,包括三部分,用空格隔开
- 紧接着是报文头,是KV结构、属性名:属性值
- 待补充
response报文

待补充
1.3 向DNS服务器查web服务器的ip地址
- 因为ip地址是一串数字,很难记,所以就有了域名这个东西,但是知道ip地址才能传输数据,所以就有了DNS服务器,就跟小时候的电话本一样,知道域名可以查ip,知道ip可以查域名
- DNS是树状结构,能很快的查询到域名

1.3.1 通过解析器向dns服务器查询
向DNS服务器查web服务器的ip地址就是向dns服务器发消息,并接受服务器的响应消息。
我们电脑上有对应的dns客户端,叫做解析器,在操作系统的Socket库中,我们查ip是通过这个解析器向dns服务器发出查询,在编写浏览器等应用程序的时候,只要协商解析器程序名字+web服务器的域名就完成了对解析器的调用。调用解析器以后,是解析器向dns服务器发送潮汛消息,然后dns服务器返回响应消息,响应消息中包含查到的ip地址,解析器会取出ip地址,并将其写入浏览器知道的内存地址中。浏览器向web服务器发消息的时候,只要从该内存地址取出ip地址,并将它与http请求一起发给操作系统。

1.3.2 解析器
浏览器调用解析器的时候,浏览器本身的程序会被暂停,此时控制流程转移,当控制流畅转移到解析器以后,解析器会生成要发给dns服务器的请求,这个发送的操作是解析器委托给操作系统内部的协议栈来发送的,协议栈会通过网卡把消息发给dns服务器。
如果要访问的web服务器已经在dns服务器上注册的话,这个信息就能被找到,然后再被写入响应消息返回给协议栈,再经过解析器,写入web浏览器的指定的内存区域中。
向dns服务器发消息的时候,也要知道dns的ip地址,这个ip地址是被作为TCP/IP的一个设置事先写好了

1.4 委托协议栈发送消息
1.4.1 概览
知道ip地址以后就委托系统内部的协议栈去向这个目标ip发送HTTP请求了,
收发数据就是两台计算机之间连接了一条数据通道
做管道的关键是套接字,套接字就相当于管道两边的数据出入口,把套接字连起来就是管道了。
有以下四个阶段
- 创建套接字
- 建立通道
- 收发数据
- 断开连接
浏览器或者说是操作系统里面的Socket库并不能自己收发消息
应用程序是调用Socket库里面的组件来执行数据的收发操作,但是这四个步骤都是由操作系统的协议栈来实现的
协议栈是如何接到委托的?
应用程序调用Socket库中的应用组件,来委托协议栈,Socket就相当于一个桥梁的角色,不进行实质性操作,应用程序的委托内容会被原本的给协议栈。
1.4.2 应用程序委托收发数据的过程
应用程序通过按照一定顺序调用Socket库中特定的程序组件进行委托(这些组件就是api啦)
阶段 | 调用的组件 | 需要的参数 | 作用 |
套接字阶段 | socket组件 | ㅤ | 创建套接字,返回描述符 |
接通管道阶段 | connect组件 | 描述符,服务器ip地址,端口号 | 建立连接成功,协议栈会将对方的ip地址和端口号保存在套接字里 |
发送消息 | write组件 | 描述符和要发送的数据 | 服务器收到消息 |
客户端响应 | read组件 | ㅤ | 存放响应数据给应用程序的内存空间的缓冲区 |
断开 | close组件 | ㅤ | 断开连接并且删除套接字 |
2 传输层
之前是应用程序委托协议栈发消息的过程,现在介绍协议栈如何处理并发送请求的
2.1 协议栈概览
协议栈的内部结构

- 上半部分 TCP/UDP协议
- 下半部分IP协议
- ICPMP
- ARP
- ……
2.2 创建套接字
2.2.1 套接字是啥
三个socket
- 首字母大写的表示库
- 小写的表示组件
- 套接字就表示套接字
套接字就是通信控制信息,记录控制通信操作的各种控制信息,协议栈根据这些信息判断下一步的行动
下图是在wibdows下用
netstat
命令显示的套接字内容,创建套接字的时候会在这加一行控制信息,赋予即将开始通信状态,并进行通信的准备工作。
2.2.2 调用组件的操作
创建套接字时候会分配个内存空间,写入初始状态
2.3 连接服务器
2.3.1 连接啥意思
连接就是通信双方交换控制信息,(在套接字中记录这些信息)
连接有三个目的
- 套接字刚刚创立的时候,是空的,不知道数据该发给谁,浏览器知道端口和ip地址但是在调用socket api创建套接字的时候,这些信息没有传给协议栈。所以在连接的时候需要把这些信息告诉协议栈
- 服务器这里也会创建套接字,但是套接字不知道应该和谁通信,就连应用程序也不知道。所以需要客户端向服务端传达开始通信的请求
- 创建缓冲区
2.3.2 连接过程
从调用connect API 开始 ,这个会把服务器的ip地址和端口号给协议栈的TCP模块,然后服务器就开始和客户端交换控制信息了。过程如下
- 客户端创建TCP的header,到这里,客户端就知道要给谁发信息了
- 然后发送一个SYN包建立连接和请求,等待确认应答(是客户端给服务端发送者这个包)
- 这个是TCP委托IP模块发送网络包,网络包通过网络到达服务器,服务器的IP模块给服务器的TCP模块,这时候服务器的TCP模块根据header找到端口号对应的套接字,(就是找到与等待连接的服务端的套接字一样的被TCP包记录的套接字)
- 找到以后,套接字中会写入响应信息,把状态改为正在连接
- 上述操作完成后返回响应
- (服务端给客户端)回应ACK包(针对SYN的确认应答),并且发送一个SYN包,请求建立连接
- 客户端回应ACK(针对SYN包)
- 如果连接成功,会向套接字写入服务器的信息,将状态改为连接完毕
- 为什么三次握手?
- 第一步,我先敲墙,试探看看有没有人
- 第二步,如果对方有人,就也敲下墙回应我
- 此时我知道两件事
- 对方有人
- 对方能听见我说话
- 这时对方知道几件事?
- 对方有人
- 但是不能知道对方能不能接收到我的信息
- 所以第三步,我敲下墙,告诉对方我也能听见你敲墙
- finally,我们终于建立了连接,可以商量携手逃出密室了!
想象你在一个密室,你来确认旁边的房间有没有人,然后如果有人就建立一起商量怎么逃出去,怎么办


2.4 收发数据
2.4.1 发数据
2.4.1.1 原理
控制流从connect到达应用程序以后,接下来就到数据收发阶段了
发数据是调用write api开始的,协议栈收到数据以后怎么操作?
第一步:将HTTP请求交给协议栈
- 协议栈只负责发送数据,应用程序调用write的时候会指定发送数据的长度
- 协议栈不是一收到数据马上发出去,会将数据存在内部的发送缓冲区里,等待应用程序的下一段数据。,应用程序交给协议栈的数据长度是由应用长度本身决定的。如果协议栈已收到数据就发出去,这样会导致发出大量小包
- 积累多少数据才发送是下面几个要素
- 数据长度
- 协议栈有个MTU参数,它是一个网络包的最大长度(包含头部信息)
- MTU - header = MSS, 指的数据部分的最大长度
- 应用程序收到数据长度>=MSS的时候就可以发出去了
- 时间
- 程序每次等到数据>=MSS时候,会发生延迟,这种情况即使数据没有积攒狗也会发出去
- 这是操作系统和决定的
- 但是协议栈留下了些选项,比如(你的命后代填满缓冲区直接发送)
第二步: 分包
如果信息很大,缓冲区被一下填满,就立即被拆分,每块数据会被放进单独的网络包里,MSS叫最大消息长度,理想是正好在ip数据包中不会被分片处理的最大数据长度。
这个MSS在三次握手的时候被计算出,两端主机发出连接请求的时候,会在TCP首部协商MSS,然后会在两者之间选个小的来用


第三步:ack确认

ACK里有
- 接收方窗口容量
- 期待的下一个编号
TCP拆分数据时,会给给个字节编号,发送数据时,会把算好的字节数写在TCP头部,发送数据的长度是整个数据包的长度假设header的长度
通过编号,接收方可以算出数据有没有遗漏,比如上次接受到1000字节,下次应该收到的应该是编号1001字节的包,如果不是就说明被遗漏了,如果没有遗漏,接受放会算出来一共接受了多少字节,写入TCP头部的ACK发给对方。
并且为了安全,ACK包并不是从1开始的,是随机的,这个是在上一步连接过程中(把SYN设为1的那一步)把这个序号的初始值告诉对方
双向传输,就是把刚刚的情形反过来
tcp这样确认数据有没有丢失,在收到ACK之前,发送过的包都在缓冲区呆着,如果没有返回ACK就重新发包
tcp还有好多控制方法 ,在下面介绍一部分

2.4.1.2 重发超时
前面说过,如果一段时间等不到ACK应答,(有两种情况,1. 数据包丢失 2. 数据包收到但是应答丢失)就会重新发包,那这个时间是多少,如果一直收不到ACK要无休止的发下去嘛?

这个时间因为网络环境不一样不能写死,所以tcp每次发包会记录往返时间和偏差,比往返时间+偏差大一点的是重发时间,在windows中是500ms,
如果重发超时得不到应答,就会再发,这个等待的时间会2倍,4倍指数级增长,达到一定次数就会关闭连接
2.4.1.3 利用窗口提高速度
如果每次发消息都要等到ACK才能执行下面的动作,那包的往返时间越长,通信性能越低
为了解决这个问题,TCP有了窗口这个概念
发送端主机在发送一个段以后不用一直等ACK,而是继续发,窗口大小就是无需等待应答还可以发数据的最大值,这个机制用了 大量的缓冲区。


2.4.1.4 窗口控制与重发控制
- 情况一: 应答丢失
在这种情况下,只要收到了ACK700,就说明前面的数据都被收到了,这种叫做 累计确认

- 情况二: 报文丢失
高速重发,不已时间为标准,如果发送端连续接受同一个确认应答,就会对数据进行重发

但是有一个问题,客户端不知道要重发一个还是重发所有,它并不知道连续的这三个ACK是谁传回来的,所以就有了SACK方法。
会在TCP头部加上个SACK地图,这样就知道是哪一块数据没有收到了,就会准确的重发那一段

还有个D-SACK来告诉发送方哪些包被重复接收了
场景一,ACK丢包
- 在这个例子中,ACK包丢了,触发了重发机制,于是就发送了3000--3500的包
- 接受方发现重复接收了就返回个SACK = 3000-3500的包

场景二:网络延迟
- 发送方1000--1499丢失了,于是一直收到重复的ACK 1000报文,三次触发了高速重发
- 然后1000--1499的包终于被发送,所以接收方给发送方回了个SACK = 1000 -- 1500但是此时ACK已经是3000了
- 所以发送方就知道是网络延时的问题了

所以D-SACK有三个好处
- 可以让发送方知道是不是网络延迟了
- 可以让发送方知道是发出去的包丢了还是ACK丢了
- 可以知道是不是有包被重复的接收了
2.4.1.5 流控制
如果发送方不考虑接收方的情况乱发数据,会导致接收方缓冲区溢出,处理不过来的情况,如果丢包又会触发重发机制,因此就有了流控制
数据的接收能力是由【接收方】的窗口大小决定的,这个窗口大小是【接收端】告诉【发送端】自己可以接收多大的数据,这个数据是动态的

如图,接收到3001以后的数据的时候,缓冲区满了,只能暂时停止发送数据,这时发送方会发一个窗口测探的包,如果接收到窗口更新才会继续发数据,要不然发送数据的过程会被中断。如果窗口更新的包丢了会影响通信,因此发送方会时不时的给接收方发送窗口探测的包
2.4.1.6 拥堵控制--慢启动
因为带宽的限制,如果通信刚开始发送大量的数据包,会导致网络拥堵,如果有很多主机通信,这时突然发个很大的数据包,会导致网络瘫痪,所以在通信一开始的时候,有个叫做慢启动的方法。对发送的数据量控制
tcp怎么进行拥堵控制的,这要结合tcp的工作机制,以及对三个问题的回答来看
- TCP必须使用端到端的拥堵控制而不是网络辅助的拥堵控制,因为ip层不提供网络控制的反馈,也就是说,TCP是通过发送端和接收端的数据包接收情况计算出来拥堵的,如果TCP发送方没感觉堵塞就加大数据传输速率,如果感到堵塞就降低发送的效率
- TCP包是怎么知道拥堵的
- TCP包是怎么控制发送的速度的
- TCP包感受到拥堵的时候,是采用什么样的算法调节发送速率呢
问题一:
- 把一个TCP发送方的丢包事件定义为要不然超时,要不然接收到来自接收方三个ACK(前面说过,丢包有两种情况,一个是ack丢,一个是数据包丢,分别对应时间和数据量,ack丢会引发超时,数据包丢会引起三个冗余的ACK)
- 当堵塞的时候,在这条路径上的一台(或者多台)路由器的缓存会溢出,这样会导致删除一个TCP包,删除这个TCP包会引起发送方的丢包事件,当出现丢包事件是,就认为是网络堵塞
问题二:
- 为了控制TCP发送方是如何限制流量的,我们引入一个概念,拥堵窗口(congestion window)在发送方一次发送的数据量要比cwnd和接收方的窗口大小(rwnd)小,
- 控制变量,如果不发生拥堵,而且TCP接收方的缓存足够大(与流量控制做对比),多大都能吃的下,还假设总有报文要发送,并且时延忽略不计的情况,因此粗略的说,速率等于CWND/RTT(往返时间)字节/秒,所以通过调节cwnd,发送方能控制发送数据的速率
问题三:算法
分为三个阶段
- 慢启动
- 拥堵避免
- 快速修复
(其中1.2是强制的)
大概说一下:
为了探测网络拥堵状况,我们有以下两个策略
- 先发个数据包测试一下,如果没有丢包,下次就发2MSS个,如果还是没有拥堵,下次就发三个,这样以此类推
- 一个一个发,太慢了,所以可能一开始发1个,下次发两个包,下次两个包分别加1,是4个,下下次是8个,这是假设没有网络拥堵的情况,但是这样指数增长太快了,很快就会拥堵,发生丢包,这时候大概就是指数增长
大概是线性增长,这样的
<img src="https://typora-danan.oss-cn-beijing.aliyuncs.com/img/typora/20210525111523.png" alt="image-20210525111523828" style="zoom:25%;" />
<img src="https://typora-danan.oss-cn-beijing.aliyuncs.com/img/typora/20210525111804.png" alt="image-20210525111804125" style="zoom:25%;" />
具体算法

慢启动:
- 一个TCP连接开始时,cwnd通常被设置为一个mss的较小值,在慢启动状态,cwnd的值以一个MSS开始,并且每当传输的报文被确定,就加一个MSS

- 那要何时结束呢?有几种答案
- 如果时超时丢包,tcp发送方将cwnd设置为1,并且重新开始慢启动,还记录了一个慢启动阈值(ssthresh),ssthresh = cwnd/2
- 与ssthresh关联,如果指数增长到慢启动阈值的时候,继续指数增长会有些鲁莽,所以,当cwnd = ssthresh时,会进入拥堵避免模式
- 如果监测到三个ACK包,TCP快速重传并进入快速恢复状态
拥堵避免:
- 这时候cwnd的值大概时上次遇到拥塞的一般,可能里拥塞不遥远,所以这时候不能指数增长,每一次RTT只将cwnd的值加一个MSS
如果MSS时1460字节,cwnd时14600字节,在一个RTT内发送10个报文段,假设每个报文段一个ACK,将增加十分之一MSS的拥塞窗口的长度
- 那什么时候结束拥堵避免呢?当然时出现丢包的时候
- 出现超时时:与慢启动一样,cwnd被设置为一个MSS
- 丢包事件出现时:ssthresh的值被更新为cwnd的一半,由三个ACK触发的时候,网络继续送发送方到接收方发送报文段,因此TCP对这种丢包的行为,相比于超时的丢包不那么剧烈。 tcp将cwnd的值减半,🧐ssthresh的值记录为cwnd的值的一半,接下来进入快速恢复状态
快速恢复
- 对于引起TCP进入快速恢复状态的缺失的报文段,对每个冗余的ACK,cwnd的值加一个MSS
- 当对丢失报文段的一个ACK到达时,TCP在降低cwnd后进入拥堵避免状态
- 如果出现超时事件,快速恢复在执行玩如同在慢启动和拥堵避免相同动作以后,进入慢启动状态
- 丢包事件出现后,cwnd被设置为1MSS,并将ssrhresh设置为cwnd的一半
2.4.2 收数据
- 前面是协议栈收到浏览器 委托后发送http请求消息的一系列操作,接下来要等待web服务器的响应消息,这个协议栈也参与,这个本来应该是最后再讲,但是先在这说一下
- 浏览器在委托协议栈发送请求以后,会调用read,来获取响应消息。控制流从浏览器到协议栈,
- 接收数据时候先把数据放在缓冲区中,首先,协议栈尝试从缓冲区把数据取出给浏览器,但是这时候可能请求消息刚发,响应消息还要等等,因此,这时候接收缓冲区可能没数据,接收的操作也无法继续,这是协议栈会把应用程序的委托,也就是把数据从缓冲区取出给浏览器的工作挂起,等服务器返回响应消息到达以后再执行操作
- 协议栈收数据的具体操作
- 首先,协议栈会检查收到的数据块和TCP头部的内容,判断是否有数据丢失,如果没有问题则返回ACK号。
- 然后,协议栈将数据块暂存到接收缓冲区中,并将数据块按顺序连接起来还原出原始的数据,最后将数据交给应用程序。
- 具体来说,协议栈会将接收到的数据复制到应用程序指定的内存地址中,然后将控制流程交回应用程序。
- 将数据交给应用程序之后,协议栈还需要找到合适的时机向发送方发送窗口更新
2.5 从服务器断开并删除套接字
2.5.1 断开连接
因为TCP连接是双向传输的对等的模式,两方都在发数据,所以收发数据结束的时间点应该是应用程序判断所有数据已经收发完毕的时候,数据发送先完毕的一方先断开连接,举个例子
我们假设是客户端先发送完数据客户端:我发完数据了,我要关了昂(调用close api,协议栈生成断开信息的头部:FIN设成1)服务端:我知道你要关了,但是我还有数据没发完(ack)客户端:我数据发完了,可以关了(FIN:1)服务端:我知道了(ACK)
2.3.2 删除套接字
这个套接字不是马上删除的,一般过几分钟再删除,因为最后这四次挥手可能发生无数次的意外,误操作,导致误操作的原因很多,比如在上个例子里面,最后一步服务端的ACK丢了,那客户端可能会重复的发FIN,但是如果这时候服务端的套接字被删除了,端口被释放了,如果这时候这个端口恰巧连了别的端口,那就在这个新的套接字开始工作了
3 网络层
3.1 IP网络包的传输方式
其实 TCP是分段的,由IP传送的,这样的话,其实这时候结构是这样的
HTTP要传送报文的时候,会以流的形式把报文,通过一条 打开的TCP通道按照顺序传输,TCP收到数据流以后,会把数据切成小块,并且把这些小的数据块封装到IP分组中,再通过网络来传递,这些工作都是TCP/IP软件处理的,是个黑盒
接下来的物理硬件是路由器和集线器,分别对应的是IP协议和以太网协议
- IP协议根据目标地址判断下一个路由器的位置
- 以太网协议将包传输到下一个转发设备
具体来说,TCP/IP包有两个头部
- MAC头部(以太网协议)
- IP头部(ip协议)
接下来的数据是这么被传输的
- 发送方把要访问的服务器IP地址写到IP头部,IP协议根据这个地址找到下一个路由器的位置
- 接下来,IP协议委托以太网协议把包传输过去,这时,IP协议找下一个路由器的以太网地址,把原来的MAC地址删掉,把新的写入进去,这样以太网协议就知道把包发在哪个路由器上了
- 网络包传输的时候,会经过集线器,集线器根据以太网工作,为了判断包接下来到哪去,集线器里有一张表,可以根据这张表判断下一步去哪
- 包到达下一个路由器以后,路由器有个IP协议的表,可以根据这个表还有目的IP地址,查出来接下来发到哪个路由器里面,还要查出下一个路由器 的MAC地址,记录到MAC地址上
- 其实虽然说IP模块负责发送数据包,其实是集线器,路由器等网络设备发送的,IP包只是个入口而已

3.2 IP包收发操作
- TCP模块先委托IP模块发包,就是先把数据块的前面加上TCP头部,再传给IP模块,
- IP模块收到以后,会在前面加上IP头部和MAC头部
- ip头部是ip协议规定的
- MAC头部是把包传给最近的路由器所需要的信息
- 接下来封装好的包通过网络硬件(网卡)发出去,网卡把数字信号(01)转化成电信号,给网线什么的发出去
- 包收到后,对方会做出响应,返回的包也会通过转发设备发送回来,我们需要接收,