文章目录

  • 从输入URL到打开页面到底发生了什么
    • 1 应用层
      • 1.1 解析URL
      • 1.2 生成HTTP请求消息
      • 1.3 向DNS服务器查web服务器的ip地址
        • 1.3.1 通过解析器向dns服务器查询
        • 1.3.2 解析器
      • 1.4 委托协议栈发送消息
        • 1.4.1 概览
        • 1.4.2 应用程序委托收发数据的过程
    • 2 传输层
      • 2.1 协议栈概览
        • ……
      • 2.2 创建套接字
        • 2.2.1 套接字是啥
        • 2.2.2 调用组件的操作
      • 2.3 连接服务器
        • 2.3.1 连接啥意思
        • 2.3.2 连接过程
      • 2.4 收发数据
        • 2.4.1 发数据
          • 2.4.1.1 原理
          • 2.4.1.2 重发超时
          • 2.4.1.3 利用窗口提高速度
          • 2.4.1.4 窗口控制与重发控制
          • 2.4.1.5 流控制
          • 2.4.1.6 拥堵控制--慢启动
        • 2.4.2 收数据
      • 2.5 从服务器断开并删除套接字
        • 2.5.1 断开连接
        • 2.3.2 删除套接字
  • 3 网络层
    • 3.1 IP网络包的传输方式
    • 3.2 IP包收发操作
  • 4 链路层
  • 5 物理层

从输入URL到打开页面到底发生了什么

每个分层都有他的道理,这就跟JS代码加不加分号一样是很私人的事情,这里我为了方便理解就按照五层协议学习的

一张提纲挈领的图

几个问题

  1. 为什么网络协议要分层,这样的优缺点是什么

    1. 分层以后,每层有每层的协议,可以独立工作,一层的改变并不影响另一层
    2. 但是这样分层可能导致同样的逻辑在这层用一次,在那层用一次
  2. 分这么多层性能会差嘛
    1. 并不会,每一层层层封装最终也不过是多出来很少字节的数据,性能浪费没有想象的那么高

1 应用层

1.1 解析URL

URL (Uniform Resource Locator) 浏览网页差不多是从这一步开始,这里介绍下url的格式,这个链接的blog说的很详细,但是由于绝大多数网页都是HTTP/HTTPS协议,所以这一部分可以省略,同样,80端口号是默认的,一般也不用写,而由于http支持服务器将网页重定向,所以www也可以不用写,因此用户访问网页的时候只用键入一部分就可以了,比如https://www.mcppy.commcppy.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请求了,

收发数据就是两台计算机之间连接了一条数据通道

做管道的关键是套接字,套接字就相当于管道两边的数据出入口,把套接字连起来就是管道了。

有以下四个阶段

  1. 创建套接字
  2. 建立通道
  3. 收发数据
  4. 断开连接

浏览器或者说是操作系统里面的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模块,然后服务器就开始和客户端交换控制信息了。过程如下

  1. 客户端创建TCP的header,到这里,客户端就知道要给谁发信息了

  2. 然后发送一个SYN包建立连接和请求,等待确认应答(是客户端给服务端发送者这个包)

    1. 这个是TCP委托IP模块发送网络包,网络包通过网络到达服务器,服务器的IP模块给服务器的TCP模块,这时候服务器的TCP模块根据header找到端口号对应的套接字,(就是找到与等待连接的服务端的套接字一样的被TCP包记录的套接字)
    2. 找到以后,套接字中会写入响应信息,把状态改为正在连接
    3. 上述操作完成后返回响应
  3. (服务端给客户端)回应ACK包(针对SYN的确认应答),并且发送一个SYN包,请求建立连接

  4. 客户端回应ACK(针对SYN包)

    1. 如果连接成功,会向套接字写入服务器的信息,将状态改为连接完毕
  5. 为什么三次握手?

    想象你在一个密室,你来确认旁边的房间有没有人,然后如果有人就建立一起商量怎么逃出去,怎么办

    • 第一步,我先敲墙,试探看看有没有人
    • 第二步,如果对方有人,就也敲下墙回应我
      • 此时我知道两件事

        • 对方有人
        • 对方能听见我说话
      • 这时对方知道几件事?
        • 对方有人
        • 但是不能知道对方能不能接收到我的信息
    • 所以第三步,我敲下墙,告诉对方我也能听见你敲墙
    • finally,我们终于建立了连接,可以商量携手逃出密室了!

    TCP头部格式

2.4 收发数据

2.4.1 发数据

2.4.1.1 原理

控制流从connect到达应用程序以后,接下来就到数据收发阶段了

发数据是调用write api开始的,协议栈收到数据以后怎么操作?

第一步:将HTTP请求交给协议栈

  • 协议栈只负责发送数据,应用程序调用write的时候会指定发送数据的长度
  • 协议栈不是一收到数据马上发出去,会将数据存在内部的发送缓冲区里,等待应用程序的下一段数据。,应用程序交给协议栈的数据长度是由应用长度本身决定的。如果协议栈已收到数据就发出去,这样会导致发出大量小包
  • 积累多少数据才发送是下面几个要素
    1. 数据长度

      • 协议栈有个MTU参数,它是一个网络包的最大长度(包含头部信息)
      • MTU - header = MSS, 指的数据部分的最大长度
      • 应用程序收到数据长度>=MSS的时候就可以发出去了
    2. 时间
      • 程序每次等到数据>=MSS时候,会发生延迟,这种情况即使数据没有积攒狗也会发出去
    3. 这是操作系统和决定的
    4. 但是协议栈留下了些选项,比如(你的命后代填满缓冲区直接发送)

第二步: 分包

如果信息很大,缓冲区被一下填满,就立即被拆分,每块数据会被放进单独的网络包里,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发送方没感觉堵塞就加大数据传输速率,如果感到堵塞就降低发送的效率
  1. TCP包是怎么知道拥堵的
  2. TCP包是怎么控制发送的速度的
  3. TCP包感受到拥堵的时候,是采用什么样的算法调节发送速率呢

问题一:

  • 把一个TCP发送方的丢包事件定义为要不然超时,要不然接收到来自接收方三个ACK(前面说过,丢包有两种情况,一个是ack丢,一个是数据包丢,分别对应时间和数据量,ack丢会引发超时,数据包丢会引起三个冗余的ACK)
  • 当堵塞的时候,在这条路径上的一台(或者多台)路由器的缓存会溢出,这样会导致删除一个TCP包,删除这个TCP包会引起发送方的丢包事件,当出现丢包事件是,就认为是网络堵塞

问题二:

  • 为了控制TCP发送方是如何限制流量的,我们引入一个概念,拥堵窗口(congestion window)在发送方一次发送的数据量要比cwnd和接收方的窗口大小(rwnd)小,
  • 控制变量,如果不发生拥堵,而且TCP接收方的缓存足够大(与流量控制做对比),多大都能吃的下,还假设总有报文要发送,并且时延忽略不计的情况,因此粗略的说,速率等于CWND/RTT(往返时间)字节/秒,所以通过调节cwnd,发送方能控制发送数据的速率

问题三:算法

分为三个阶段

  1. 慢启动

  2. 拥堵避免

  3. 快速修复

    (其中1.2是强制的)

大概说一下:

为了探测网络拥堵状况,我们有以下两个策略

  1. 先发个数据包测试一下,如果没有丢包,下次就发2MSS个,如果还是没有拥堵,下次就发三个,这样以此类推

    大概是线性增长,这样的

    1. 一个一个发,太慢了,所以可能一开始发1个,下次发两个包,下次两个包分别加1,是4个,下下次是8个,这是假设没有网络拥堵的情况,但是这样指数增长太快了,很快就会拥堵,发生丢包,这时候大概就是指数增长

具体算法

慢启动:

  • 一个TCP连接开始时,cwnd通常被设置为一个mss的较小值,在慢启动状态,cwnd的值以一个MSS开始,并且每当传输的报文被确定,就加一个MSS
  • 那要何时结束呢?有几种答案
    • 如果时超时丢包,tcp发送方将cwnd设置为1,并且重新开始慢启动,还记录了一个慢启动阈值(ssthresh),ssthresh = cwnd/2
    • 与ssthresh关联,如果指数增长到慢启动阈值的时候,继续指数增长会有些鲁莽,所以,当cwnd = ssthresh时,会进入拥堵避免模式
    • 如果监测到三个ACK包,TCP快速重传并进入快速恢复状态

拥堵避免