一篇文章从0搞定计算机网络,面试小case

【关于作者】

关于作者我,目前在蚂蚁金服搬砖任职,在营销投放领域工作了多年,目前在专注于内存数据库相关的应用学习,如果你有任何技术交流或大厂内推及面试咨询等相关问题,都可以从我的个人博客(https://0522-isniceday.top/)联系上我~

1.网络分层

1.1.OSI7层模型

image-20200708085822940

1.2.TCP/IP四层模型

image-20200708234353206

可以发现,TCP/IP体系少了表示层和会话层,数据链路层和物理层用链路层取代。

  • 应用层将数据传输给具体的应用:最高层,应用层的任务是通过应用进程间交互来实现特定网络应用。主要负责把应用程序中的用户数据传达给另一台主机或同一主机上的其他应用程序。这是所有应用程序协议的运行层,如SMTP、FTP、SSH、HTTP等;
  • 传输层通用的数据传输服务:负责向两个主机中的进程之间的通信提供通用的数据传输服务。UDP是基本的传输层协议,提供了不可靠的无连接数据报传输服务;
  • 网络层寻址和路由功能:负责为分组交换网上的不同主机提供通信服务。该层定义了寻址和路由功能,主要协议是IP协议(Internet Protocol),它定义了IP地址,它在路由中的功能是将数据报传输到充当IP路由器的下一个主机,该主机更接近最终数据目的地;
  • 链路层负责处理与传输媒介的相关细节:也称为数据链路层或者网络接口层,通常包括操作系统中设备驱动程序和计算机对应的网络接口卡

1.3.数据包的封装与分用

image-20200726223316970

1.4.协议簇

image-20200726204748249

2.物理层

有哪些通信交互方式?单工、半双工通信、全双工通信?

单工通信,又称为单向通信,只有一个方向的通信,如无线电广播,电视广播;

image-20200712122203747

半双工通信,又称为双向交替通信,双方都可以收发信息,只能交替进行;

全双工通信,又称为双向同时通信,双方可以同时发送和接收数据。

image-20200712122339778

3.数据链路层

3.1.数据帧格式

image-20200715085955337

前导:用在发送方和接收方之间同步时钟和bit流;

SFD:帧开始界定符,只有一个byte,内容固定为:10101011 (0xAB);

DST:目标MAC地址;

SRC:源MAC地址;

长度或类型:0800时,表示IP数据报,0806表示ARP请求/应答,0835表示RARP请求/应答;

FCS:帧检验序列,用于数据帧的差错检测;

3.2.ARP

通过ip地址获取目标的mac地址

image-20200713231114251

3.3.交换机

为什么需要交换机?

集线器(hub):局域网中,一台机器发送信息,会通过hub将消息以广播的形式给其他局域网内的机器,

交换机:但是hub会有一个缺点,就是会把不属于某台机器的消息也发送给他,因此能不能只给需要的主机发送消息呢?

因此有了交换机,当一台电脑A向交换机发送数据时,交换机会把电脑A的IP和MAC地址记住,保存到一个转发表中,如果转发表中暂时找不到目标IP地址的MAC地址,那么首先还是会广播消息,最终转发表会记录所有请求过交换机的电脑IP和MAC。当然,转发表也是有过期时间的

IP地址与MAC

IP地址相当于是收货地址,是会改变的,mac地址相当于网卡的唯一标识,也就是你的主机的身份证,因此网络上发送数据不可能只指定身份证,而需要执行邮寄地址

4.网络层

前面我们将的数据链路层,其实只能在局域网内进行通信,因为都是通过MAC地址进行传达信息的,要想跨局域网,那么就得用到IP地址了,这就是网络层要做的事情了。

首先我们来介绍下网络的一个协议:ICMP协议。

4.1.ICMP协议

ICMP:用于获取包发送途中的异常探测,诊断信息,以及发送途中经过的路由器、往返时间。IP协议是无法支持这些的

ICMP并不为IP网络提供可靠性,它只是用于反馈各种故障和配置信息。丢包不会触发ICMP

4.1.1.格式

ICMP报文是在IP数据报内部传输的,格式如下:

image-20200715084813447

而ICMP报文的格式如下:

image-20200715085522615

  • 类型有15个不同的值,描述特定类型的ICMP报文;
  • 某些ICMP报文还是用代码字段的值来进一步描述不同的条件;
  • 校验和字段用于ICMP报文的差错检查。

4.1.2.查询报文

有关信息采集和配置的ICMP报文,ping就是采取的ICMP查询报文

4.1.2.1.PING

ping请求的处理流程:

image-20200716085927626

ping程序是用到了网络层的ICMP协议,不经过传输层

ping失败的原因

  1. IP输入错误
  2. 网络配置不正确,如错误的子网掩码
  3. 防火墙阻止了ping
  4. 硬件故障

4.1.3.差错报文

差错报文是有关IP数据报传递的ICMP报文,要是发送IP数据包中途产生了异常,那么就会响应ICMP报文

但是不是所有情况都会响应ICMP差错报文,如以下场景:

  • ICMP差错报文不会产生另一个ICMP差错报文;
  • 目的地址是广播地址或者多播地址的IP数据报不会产生差错报文;
  • 作为链路层广播的数据报不会产生差错报文;
  • 源地址不是单个主机(源地址为零地址、环回地址、广播地址或者多波地址)的数据报不会产生差错报文;

4.2.traceroute程序

traceroute工具用于确定从发送者到目的地路径上的路由器。

traceroute主要是通过故意设置特殊的TTL,来达到追踪目的地路径上的路由器的功能。

TTL运行原理

TTL:是 Time To Live的缩写,该字段指定IP包被路由器丢弃之前允许通过的最大网段数量。每经过一个路由器,TTL就会减一,然后再把IP包转发出去,如果TTL减到0了,路由器就会丢弃收到的TTL=0的IP包,并向IP包的发送者发送一个ICMP差错报文,类型为11,代码为0:传输期间生存时间为0。

4.3.网关和路由器

4.3.1.IP协议

IP是TCP/IP协议簇中最核心的协议,所有TCP、UDP、ICMP等数据都以IP数据包格式传输

4.3.1.1.IP协议特点
  1. IP协议是不可靠的传输协议:传输出现异常时,IP层都会丢弃数据包,并且可能会响应一个ICMP差错消息给发送端,而任何要求的可靠性必须由上层如TCP协议来提供
  2. IP协议是无连接的:每个数据报相互独立,可以不按照发送顺序接受,不用维护连接状态等

4.3.1.2.IP数据报格式

image-20200719221922958

  • 版本:协议版本号,指明IPv4还是IPv6;
  • 头部长度:最长60个字节;
  • 服务类型:包含3bit优先权子字段(已被忽略),4bit TOS子字段(分别代表最小时延、最大吞吐量、最高可靠性和最小费用)和1bit未用位但必须置0;
  • 总长度:指的是整个IP数据报的长度,单位字节;
  • 标识符:唯一地标识主机发送的每一份数据报,通常每发送一个数据报就+1;
  • 标志:主要用于IP分片;
  • 分片偏移:主要用于IP分片;
  • 生存期:设置数据报可以经过最多的路由器数;
  • 协议:主要表明IP数据是什么协议,用于对数据报进行分用;
  • 头部校验和:校验数据报是否正确;
  • 源IP地址:发送IP数据报的IP地址;
  • 目的IP地址:IP数据报目的IP地址;
  • 选项:可选数据;
  • IP数据:具体的IP数据;

4.3.2.路由器

路由器一般充当一个网关,属于三层设备。其会把MAC头和IP头取下来根据内容进行处理。路由器有5个网口,分别可以连接5个局域网,每个网口有和局域网的IP地址相同的网段,每个网口都是对应局域网的网关,5个网口中一般包含一个外网网口,外网网口用于连接到WAN上。

路由器除了具有交换机的功能外,更拥有路由表作为发送数据包时的依据,在有多种选择的路径中选择最佳的路径。

一层设备、二层设备、三层设备分别有什么区别?

路由器是属于OSI第三层的产品,交換机是OSI第二层的产品。

第二层的产品功能在于,将网络上各个电脑的MAC地址记在MAC地址表中,当局域网中的电脑要经过交换机去交换传递数据时,就查询交换机上的MAC地址表中的信息,将数据包发送给指定的电脑,而不会像第一层的产品(如集线器)每台在网络中的电脑都发送。

而路由器除了有交换机的功能外,更拥有路由表作为发送数据包时的依据,在有多种选择的路径中选择最佳的路径。此外,并可以连接两个以上不同网段的网络,而交换机只能连接两个。路由表存储了(向前往)某一网络的最佳路径、该路径的“路由度量值”以及下一个(跳路由器)

网关地址一般是网段的第一个或者第二个,如192.168.23.0/24这个网段,网关地址可能是192.168.23.1/24或者192.168.23.2/24。

NAT网关:在不同的局域网中,私有IP地址是会重复的,而我们要访问公网的时候,一定要分配一个共有IP地址,所以,我们在访问公网的时候,需要路由器帮忙把私有IP变为共有IP,这种叫做NAT网关,普通内网之间的通信用到的称为转发网关。

4.3.2.1.转发网关

image-20200719171542654

主机A要访问主机B,流程如下:

  • 主机A发现要访问的主机B不是在同一个网段(是一个网段则直接通过ARP获得目标id的mac地址,将包发送出去即可,怎么判断同一个网段呢?需要 CIDR 和子网掩码),准备先找到网关,把消息发给网关,网关地址是192.168.1.1,主机A通过ARP获取到了网关的MAC地址,然后发送如下数据包:

    • SRC MAC: 主机A的MAC
      DST MAC: 路由器A的192.168.1.1网口的MAC
      SRC IP : 192.168.1.3
      DST IP : 192.168.3.4
      
  • 路由器A的192.168.1.1网口接收包之后,准备把包转发出去。而路由器A中的路由表中匹配到了,要想发送给192.168.3.4/24,需要从192.168.2.1这个网口出去,下一跳地址为192.168.2.2/24。路由器通过ARP拿到了下一跳192.168.2.2/24d的MAC地址,然后发送如下数据包:

    • SRC MAC: 路由器A的192.168.2.1网口的MAC
      DST MAC: 路由器B的192.168.2.2网口的MAC
      SRC IP : 192.168.1.3
      DST IP : 192.168.3.4
      
  • 路由器B的192.168.2.2网口接收包之后,准备把包转发出去。路由器B中判断到目标IP在192.168.3.1这个网口所在的局域网,于是通过ARP拿到了192.168.3.4的MAC地址,然后发送如下数据包:

    • SRC MAC: 路由器B的192.168.3.1网口的MAC
      DST MAC: 主机192.168.3.4网口的MAC
      SRC IP : 192.168.1.3
      DST IP : 192.168.3.4
      

最终,主机B收到数据包。

可以发现在转发网关中,源IP和目的IP地址都是不会变的,因为整个内网不可能有冲突的IP

但是,假如我们要访问外网,情况就不一样了,最终可能会请到到另一个局域网,另一个局域网的私有IP是可能跟我们所在的局域网一样的,为了避免冲突,于是就有了NAT网关。专门在把数据包发送出去之前,把IP改为公网IP。

3.2.2.2.NAT网关

现在假设主机A要访问另一个城市的主机B,这里为了演示NAT,我们把模型简化一下,假设路由器出去之后就是公网IP了,如下:

image-20200719184500782

假设路由器A和路由器B都直接接入了互联网。

现在主机A想访问主机B:

  • 由于是不同的局域网,主机A不会知道主机B的IP的,而主机B接入互联网的之后,领取到了一个互联网的IP,就是上图路由器WAN口的IP:203.0.113.103,所以主机B会把这个IP作为主机B的IP,最终发出如下IP数据包:

    • SRC MAC: 主机A的MAC
      DST MAC: 路由器A的192.168.1.1网口的MAC
      SRC IP : 192.168.1.3
      DST IP : 203.0.113.103
      
  • 192.168.1.1网口接收到包之后,发现要想访问203.0.113.103,就要从203.0.113.102这个网口出去,发给路由器B,路由器B中判断到目标IP就是203.0.113.103这个网口,于是通过ARP拿到了203.0.113.103的MAC地址,然后发送如下数据包:

    • SRC MAC: 路由器A的203.0.113.102网口的MAC
      DST MAC: 路由器B的203.0.113.103网口的MAC
      SRC IP : 203.0.113.102
      DST IP : 203.0.113.103
      
    • 因为消息是要发到公网的,最终SRC IP会被NAT转化为公网的IP 203.0.113.102;

  • 最终路由器B接收到消息,通过NAPT得到最终接收数据报的IP为当前局域网的192.168.1.3/24,最终把消息转发给了这个IP所在的主机B。

NAPT是如何把一个公网IP翻译为局域网IP的?

传统的NAT(traditional NAT)包括基本NAT(basic NAT)和网络地址端口转换(Network Address Port Translation, NAPT)。基本NAT只执行IP地址的重写,本质上是将私有地址改写为一个公共地址,这往往取自于一个由ISP提供的地址池或共有地址范围,这种NAT不是最流行的,因为无助于减少需要使用的IP地址数量。

比较流行的做法是使用NAPT,NAPT使用传输层标识符如TCP或者UDP端口,或者ICMP查询标识符来确定一个特定的数据报到底和NAT内部哪台私有主机相关联。

如果局域网两个端口号一样,那么NAPT会重写端口号,保证不一致。如下图,三个局域网的IP需要转换为公网IP,由于有两个的端口重复了,于是NAPT进行了端口重写:

image-20200719184357681

两者主要的区别在于 IP 地址是否改变。不改变 IP 地址的网关,我们称为转发网关;改变 IP 地址的网关,我们称为NAT 网关。

3.3.路由策略

3.3.1.静态路由

**静态路由,其实就是在路由器上,配置一条一条规则。**这些规则包括:想访问 B站(它肯定有个网段),从 2 号口出去,下一跳是 IP2;想访问教学视频站(它也有个自己的网段),从 3 号口出去,下一跳是 IP3,然后保存在路由器里。

每当要选择从哪只手抛出去的时候,就一条一条的匹配规则,找到符合的规则,就按规则中设置的那样,从某个口抛出去,找下一跳 IPX。

3.3.2.动态路由

内网路由协议

基于链路状态算法实现的OSPF协议(Open Shortest Path First, 开放式最短路径优先):主要用于数据中心内部,因此也成为内网路由协议(Interior Gateway Protocol,IGP),关键是找到最短的路径。

OSPF是一种链路状态路由协议。可以将其视为网络的分布式地图。

外网路由协议

基于距离矢量算法实现的BGP协议(Border Gateway Protocol,外网路由协议):距离矢量,就是每个路由器都保存一个路由表,路由表每行保存了下一跳的路由器,以及距离下一跳路由器的距离。也成为边界网关协议。

在BGP的世界中,每个路由域都称为自治系统或AS。BGP所做的工作通常是通过选择遍历最少自治系统的路由:最短的AS路径来帮助选择通过Internet的路径。

我们会把重点放在传输层以上,所以动态路由协议这部分我们暂时不做不深入研究。

4.传输层

4.1.UDP

UDP协议处理的事情比较少,类似于IP协议,数据可能丢失,包的顺序无法得到保证,UDP和后边介绍的TCP不一样,是无状态的。

image-20200719222327297

  • 源端口号:发送数据报方使用的端口号,用于标识发送进程;
  • 目的端口号:接收数据包方使用的端口号,用于标识接收进程;
  • UDP长度:UDP头部和UDP负载数据的字节长度;
  • UDP校验和:UDP校验和覆盖UDP头部和UDP数据和一个伪头部(区别:IP头部校验和只覆盖IP头部),伪头部衍生子IPv4头部字段的12个字节,或者衍生子IPv6头部字段的一个40字节的伪头部;
  • 负载数据:具体的UDP数据。

UDP和TCP的端口号主要拿来区分哪个进程的数据包,TCP和UDP的端口号相同也没关系,因为两者是相互独立的。每个请求都有源IP、目标IP、源端口号、目标端口、协议五个元素来标识的,每个协议的端口池是完全独立的。在UDP/TCP协议中源端口和目的端口都只有16位,也就是说端口的取值范围为0~65535

4.1.2.UDP的特点

  1. 数据可能丢失
  2. 无状态,不需要像TCP那样建立连接
  3. 没有拥塞机制。来一包就发一个

4.1.3.UDP使用场景

  1. 需要资源少,在网络情况比较好的内网,或者对对包不敏感的场合。如DHCP和TFTP就是基于UDP的
  2. 广播场景,不需要一对一建立连接,如DHCP
  3. 需要时延低,允许丢包,不关注网络拥塞的场景,如视频直播这种流媒体,实时游戏,通信,物联网等领域

4.2.TCP

4.2.1.TCP的简述

  1. TCP是面向连接的可靠的服务,面向连接是指TCP的两个应用程序必须在他们可交换数据之前,相互建立一个TCP连接
  2. TCP提供了一种面向字节流的抽象概念,TCP不会自动插入记录标志或者消息边界,这意味着TCP没有限制应用程序的写范围。发送端分两次发10字节和30字节,接收端可能会以两个20字节的方式写入

4.2.2.TCP的数据报格式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oiTWJU34-1679930505454)(C:/Users/98347/Desktop/%E5%AD%A6%E4%B9%A0%E5%8A%A0%E6%B2%B9%E5%95%8A/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/image/image-20200721222551224-a.png-itzhai)]

如上图,头部深黄色部分为TCP特有的重点字段,后面TCP相关功能基本都是靠这些特有的字段来实现的。

  • 源端口号和目的端口号:同UDP一样,主要用于区分数据应该转发给哪个应用;
  • 序列号:这个序号为了解决乱序的问题,32位无符号数,满了后再从0开始
  • 确认号:确认已经接收到了哪里,该值表示确认号的发送方期待接受的下一个序列号,该字段只有在ACK位字段被启用的情况下才有效,所以也成为ACK号或者ACK段
  • 状态位:
    • ACK:回复状态,启用该状态的情况下,确认号有效,连接建立之后一般都是启用状态
    • SYN:发起一个连接
    • RST:重置连接,经常因为错误导致
    • FIN:结束连接,表示该报文的发送方已经结束向对方发送数据
    • CWR:拥塞窗口减小,发送方降低发送速率
    • ECE:ECN回显,发送方接收到了一个更早的拥塞通告;
    • URG:紧急,表示紧急指针字段有效,很少用到;
    • PSH:推送,表示接收方应该尽快给应用程序传送这个数据——没有被可靠的实现或用到;
  • 窗口大小:流量的窗口大小,用于流量控制,通信双方各声明一个窗口,这个大小表明了自己当前的处理能力
  • 校验和:覆盖了TCP的头部和数据,以及伪头部数据(与UDP使用的相似的伪头部进行计算);
  • 紧急指针:只有在UGE位启用的是偶才有效
  • 选项:如最大段大小等其他的可选项;
  • 数据:TCP数据报的数据内容。

4.2.3.TCP的特点

  1. 保证数据顺序传输
  2. 丢包重传,消息可靠
  3. 连接维护
  4. 流量控制,保证稳定
  5. 拥塞控制,及时调整,最大程度保证传输正常进行

4.2.3.连接管理

4.2.3.1.TCP三次握手

image-20200723224535183

  • 第一次握手:主动连接方发送一个SYN报文段指明自己想要连接的端口号,以及客户端消息的初始化序列化ISN©;
  • 第二次握手:服务器接收到消息后,也发送自己的SYN报文,包含了服务端的初始化序列号ISN(s),并设置确认号ack=客户端序列号+1;
  • 第三次握手:客户端应答服务器的SYN,将服务端的序列号+1作为ack返回给服务端。

总结一下:客户端与服务端利用SYN报文交换彼此的初始化序列号。在我们熟悉的Socket编程中,三次握手在执行connect的时候触发。

其中的ACK应答和递增的序列化是可靠性的保证。

为什么是三次握手,而不是两次或者四次?

如果是两次:

image-20200723001805421

如果去除了客户端的第三次ACK报文,那么服务端就无法知道服务端发送给客户端的ACK报文是否得到相应,可能服务端就直接结束了请求,这个时候再传消息网络层就会收到一个ICMP目的不可达的差错报文。

如果客户端第一次SYN请求服务端没有ACK应答,那么会重发SYN,此时服务端可能会接收到两个SYN报文,但是并不会建立两个连接,因为会对SYN报文的序列号进行去重,

如果是四次:

因为如果建立连接的双方所发起的SYN报文都得到了响应,双方都知道对方接受了自己的请求,因此没有比较继续发包去确认了

4.2.3.2.TCP的四次挥手

连接的任何一方都可以发起关闭操作,此外,也支持双方同时关闭连接。在传统的情况下,负责发起关闭连接请求的通常是客户端。

这个流程又被称为四次挥手:

image-20210829135606775

  • 连接的主动关闭者发送一个FIN段请求关闭连接,携带了序列号seq=p,表明自己的当前序列号,并进入FIN_WAIT_1 状态
  • 连接的被动关闭者进行了ACK回应,并发送ack=p+1表明自己已经接受到了FIN的请求,进入ClOSED-WAIT状态。主动关闭者接收到ACK之后,就进入FIN_WAIT_2 状态(此时如果被动关闭者不继续发送FIN连接,则主动关闭者会一直处于FIN_WAIT_2 状态,但是linux可以通过tcp_fin_timeout 设置一个超时时间)
  • 连接的被动关闭者也发送一个FIN段请求关闭连接,携带了序列号seq=q,ack=p+1(ack和第二次挥手的一样),告诉主动关闭者已经准备好关闭连接,并进入LAST-ACK状态
  • 最后连接的主动关闭者接收到了对方的FIN请求,也回应了一个ACK,表明自己已经成功接收到了被动关闭者发送的FIN,并进入TIME-WAIT等待2MSL之后才进入CLOSED状态

为什么要有TIME-WAIT?

因为可能主动关闭者可能对于被动关闭者的FIN请求的ACK包对方收不到,此时被动关闭者就会进行重试,而TIME-WAIT就是留了对方重试的时间,如果没有TIME-WAIT,由于端口复用,那么其重试的包可能发送给了另外一个程序,由于序列号的存在,新的程序肯定不会消费该包,并且会发送一个RET给对方,此时对方就明白早已断开连接。

等待的时间设为 2MSL,MSLMaximum Segment Lifetime报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃

序列号

每个连接都要有不同的序号。这个序号的起始序号是随着时间变化的,可以看成一个 32 位的计数器,每 4ms 加一,如果计算一下,如果到重复,需要 4 个多小时,那个绕路的包早就死翘翘了,因为我们都知道 IP 包头里面有个 TTL,也即生存时间

A 要告诉 B,我这面发起的包的序号起始是从哪个号开始的,B 同样也要告诉 A,B 发起的包的序号起始是从哪个号开始的。为什么序号不能都从 1 开始呢?因为这样往往会出现冲突。

例如,A 连上 B 之后,发送了 1、2、3 三个包,但是发送 3 的时候,中间丢了,或者绕路了,于是重新发送,后来 A 掉线了,重新连上 B 后,序号又从 1 开始,然后发送 2,但是压根没想发送 3,但是上次绕路的那个 3 又回来了,发给了 B,B 自然认为,这就是下一个包,于是发生了错误。

4.2.4.数据传输

4.2.4.1.如何保证可靠传输:ACK+序列号

假设主机A通过TCP向主机B发送数据,当主机A的数据到达主机B时,主机B会发送一个确认应答消息ACK。主机A收到ACK之后,就知道自己的数据已经被对方接收了

image-20200724081253191

4.2.4.2.窗口管理

TCP头部中,为了保证包的顺序问题以及丢包问题,我们重点可以关注了如下三个字段:

  1. 序列号
  2. 确认号
  3. 窗口:表明自己的处理能力,代表着可用缓存的大小,以字节为单位

image-20200725114826908

滑动窗口:TCP每个端的数据收发量都是通过滑动窗口来实现的,其中包括发送窗口和接收窗口

发送窗口

image-20200725125106588

  • SND.WND:提供窗口的大小是由返回的ACK中的窗口大小字段来规定的
  • SND.UNA:窗口左边界的值
  • SND.UNA + SND.WND:记录窗口右边界的值
  • SND.NEXT:记录下次发送的数据

窗口包含下面三个操作:

  1. 关闭:窗口左边界左移,当已发送的数据得到ACK的时候,就会进行关闭
  2. 打开:窗口右边界右移,当接收端处理了确认的数据之后,其窗口值就会变大,这个时候通过打开操作让提供窗口大小变大;
  3. 收缩:窗口右边界左移,使得提供窗口大小减小

接收窗口结构

image-20200725143723481

如何保证包的顺序问题:

image-20200725222605426

4.2.4.3.超时重传机制

关于如何解决包的丢失问题,有如下三种方案

  1. 基于计时器的重传超时机制(Retransmission Ttimeout, RTO)
  2. 基于反馈信息的快速重传机制
  3. 带选择确认的重传SACK

基于计时器的重传超时机制(Retransmission Ttimeout, RTO)

TCP发送数据时有一个重传计时器,如果计时器超时仍然没有接收到ACK信息,那么会进行重传操作

超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢

image-20200725230608270

关于重传时间间隔的问题:

而TCP的基于计时器的重传策略是如果发生重试,可以有两种处理方式:

  • 一种是基于拥塞控制机制,减小发送窗口大小;
  • 另一种是超时时间间隔会一直加倍:每当遇到一次超时重传的时候,都会将下一次超时时间间隔设为先前值的两倍

重传时间需要讲到自适应重传算法,一种计算重传时间的算法,大致流程:

TCP通过采样RTT的时间,进行加权平均,算出一个值,最终得到一个估计的重传时间。

> 初始值:原始值
> 测量之后:RTO = RTTs + 4*RTTd
> (RTTs:加权平均值,RTTd:偏差值)
>

基于反馈信息的快速重传机制

当接收方收到一个序号大于下一个所期望的报文段时,就检测到了数据流中的一个间格,于是发送三个冗余的 ACK,客户端收到后,就在定时器过期之前,重传丢失的报文段。

例如,接收方发现 6、8、9 都已经接收了,就是 7 没来,那肯定是丢了,于是发送三个 6 的 ACK,要求下一个是 7。客户端收到 3 个,就会发现 7 的确又丢了,不等超时,马上重发。

带选择确认的重传SACK

这种方式需要在 TCP 头里加一个 SACK 的东西,可以将缓存的地图发送给发送方。例如可以发送 ACK6、SACK8、SACK9,有了地图,发送方一下子就能看出来是 7 丢了

4.2.4.5.流量控制

流量控制总结为一句话就是,控制发送方发送窗口的窗口大小(该字段通过接收方的ACK包返回),如果接收方的消息处理不过来,可以减小发送方的窗口,如果接收方比较空闲,则可以增大发送方的窗口大小

如果接收方的包应用程序一直没有处理,最终会导致接收端没有更多空间来存储达到的数据,那么窗口右边界可能就不会打开了,最终接收的窗口大小变为0

image-20200726121531246

此时,接收方会发送一个零窗口通告(TCP ZeroWindow),告知发送端不要再发送数据了,我已经处理不过来了,于是发送方就暂停发送数据了,等待接收端的窗口更新(TCP Window Update)通知

image-20200726133315644

接收方为了防止发送方通知窗口更新的ACK消息丢失,会定时方式(TCP ZeroWindowProbe)请求,要求接收端返回TCP ZeroWindowProbeAck,看看是否窗口是否已经增加了

4.2.5.5.拥塞控制

流量控制通过滑动窗口实现,而拥塞控制则是通过拥塞窗口了,其本质也是为了避免丢包和超时重传

反映网络传输能力的变量称为拥塞窗口(congestion window),记为cwnd。

可以理解为**滑动窗口是为接收方服务的,而拥塞窗口是为整个网络通道服务的,拥塞窗口大小又会受制于接收方滑动窗口大小,并且会因为网络原因进行调整。**因为网络通道中的任何一个环节都有可能影响整体的传输效率。

实际可用窗口大小:W为接收端滑动窗口awnd和拥塞窗口cwnd的较小者,W = min(cwnd, awnd)

那么发送方如何判断网络是不是满了呢?

对于TCP而言,整个网络路径对其都是一个黑盒,而TCP的拥塞控制主要用来避免丢包超时重传,如果发生了这两种现象,就说明发送速度太快,要慢一点

那么发送方如何控制拥塞窗口的变化呢?

答案就是慢启动:

当TCP建立连接后,cwnd设置为1,当接收到1个ACK之后,cwnd加1,两个ACK就加2,可以看出这是指数级增长,涨到什么时候是个头呢?其中有一个阈值ssthresh (slow start threshhold)为65535 ,当超过这个值时,我们可能就需要调整我们cwnd的增长策略了,此时cwnd会调整为收到一个ack,增长1/8,收到8个ACK,cwnd才加1,此时增长的速度明显放缓

如果碰到了丢包和超时重传,拥塞窗口cwnd会如何变化?

两种方案:

  1. 此时代表出现了拥塞,这个时候会将ssthresh 设为cwnd/2,cwnd重新设置为1,重新开始慢启动。这真是一旦超时重传,马上回到解放前。但是这种方式太激进了,将一个高速的传输速度一下子停了下来,会造成网络卡顿
  2. 前面讲的快速重传算法,接收端发现丢了包时,,发送三次前一个包的 ACK,于是发送端就会快速的重传,不必等待超时再重传。TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分。此时,cwnd减半为cwnd/2,shthresh = cwnd,并且此时一个ACK,cwnd还是增长1,然后往复。

但是单独依靠丢包和超时重传来判断拥塞也不太准确!?:

第一个问题是丢包并不代表着通道满了,也可能是管子本来就漏水。例如公网上带宽不满也会丢包,这个时候就认为拥塞了,退缩了,其实是不对的。

第二个问题是 TCP 的拥塞控制要等到将中间设备都填充满了,才发生丢包,从而降低速度,这时候已经晚了。其实 TCP 只要填满管道就可以了,不应该接着填,直到连缓存也填满。

为了优化这两个问题,后来有了TCP BBR 拥塞算法。它企图找到一个平衡点,就是通过不断的加快发送速度,将管道填满,但是不要填满中间设备的缓存,因为这样时延会增加,在这个平衡点可以很好的达到高带宽和低时延的平衡。

image-20210829162358023

4.3.Socket编程

4.3.1.是什么

Socket是一个抽象层,主要是把TCP/IP复杂的操作抽象成几个简单的接口提供给应用层调用,进而实现应用进程在网络中通信。Socket主要是端到端之间的传输协议(网络层之上的协议)。

Socket是一种高层的抽象网络API,是一种端到端的通信,只能访问到端到端协议之上的网络层和传输层

image-20200726175908288

4.3.2.基于TCP的Socket通信交互

image-20210829165335199

  • bind():TCP服务需要监听IP和端口:

    接受到的网络包,需要根据端口来找到应用程序,而ip是因为存在多个网卡,就会有多个ip,因此只能读取监听的ip上网卡的数据

  • listen():当服务端有了 IP 和端口号,就可以调用 listen 函数进行监听,当调用这个函数之后,服务端就进入了这个状态,这个时候客户端就可以发起连接了(connect()),通过socket创建的套接字为主动套接字,在执行listen函数之后,指示内核应该接收指向该套接字的连接请求,并返回一个监听套接字

    以下是listen()[3]函数定义:

    #include <sys/types.h>          /* See NOTES */
    #include <sys/socket.h>
    
    int listen(int sockfd, int backlog);
    

    backlog是已连接队列和未完成连接队列的和

  • accept():

    内核中,每个Socket维护两个队列:

    1. 已建立连接的队列,这时候连接三次握手已经完毕,处于 established 状态
    2. 还没有完全建立连接的队列,这个时候三次握手还没完成,处于 syn_rcvd 的状态

    服务端调用 accept 函数,拿出一个已经完成的连接进行处理。如果还没有完成已完成的连接,此时就需要服务器进入阻塞等待状态,直到获取客户端的已连接套接字并返回

    如下图,在调用accept()之后,阻塞等待客户连接到达,然后获取一个已连接套接字:

    image-20201025165112038

  • connect():服务端等待的时候,客户端可以通过connect发起连接,内核会给客户端分配一个临时端口,一旦握手成功,服务端的accept就会返回另外一个socket(连接Socket,后续数据都是通过这个Socket进行传输)

监听的Socket和真正用来传输数据的Socket是两个,一个叫做监听Socket,一个叫做连接Socket

关于监听套接字和已连接套接字

注意,这里要区分好服务端的监听套接字和已连接套接字,服务端调用socket()返回的是监听套接字,bind()和listen()函数入参也是监听套接字。

一旦有客户端请求过来了于是产生了一个已连接套接字,后续和客户端的交互是通过这个已连接套接字进行的。监听套接字只负责监听客户端请求并获取和客户端的已连接套接字,其中每建立一个请求都会产生一个已连接套接字,但是监听套接字只会有一个。

Socket的本质

内核中,Socket是一个文件,对应的就有文件描述符,每一个进程都有一个数据结构taskStruct,其中存储了进程级的文件描述符列表的指针file_struce,而文件描述符fd就是一个非负整数,是这个数组的下标,而数组存储了该fd指向系统级文件描述表(file_table)表项的指针,该表项就实际存储了inode的地址,但是该inode并不像其他文件一样存储在磁盘,而是存储在内存中,在这个 inode 中,指向了 Socket 在内核中的 Socket 结构

Socket结构中主要是两个队列:队列中存储的是一个缓存sk_buff,这个缓存中能够看到完整的包的结构(该结构就是TCP中的两个滑动窗口吗?),

  1. 发送队列
  2. 等待队列

image-20210829172155297

4.3.3.基于UDP协议的Socket通信交互

UDP不需要listen以及accept,但是需要bind IP和端口,UDP 是没有维护连接状态的,因而不需要每对连接建立一组 Socket,而是只要有一个 Socket,就能够和多个客户端通信。也正是因为没有连接状态,每次通信的时候,都调用 sendto 和 recvfrom,都可以传入 IP 地址和端口。

image-20210829172341089

4.3.4.服务器如何处理更多的请求

一个Socket处理的请求受限于什么呢?

  1. 理论上的最大连接数

    {本机 IP, 本机端口, 对端 IP, 对端端口}其中只有对端ip和对端端口可以发生改变

    最大 TCP 连接数 = 客户端 IP 数×客户端端口数。对 IPv4,客户端的 IP 数最多为 2 的 32 次方,客户端的端口数最多为 2 的 16 次方,也就是服务端单机最大 TCP 连接数,约为 2 的 48 次方。

  2. 文件描述符的限制

  3. 内存的限制

因此针对该进程记录所有连接套接字socket的fd而言,如果单线程去处理所有的socket(fd list),那么可能会非常的慢

因此在资源有限的情况下,想要处理更多的请求,就需要降低每个项目所占用的资源

  1. 方法一:将项目外包给其他公司(多进程方式)

    主要通过fork的方式实现,因为fork会复制父进程的进程级别的fd列表,因此fork出的子进程也可以去处理已建立连接的socket请求,但是采取fork方式过于麻烦

  2. 方法二:将项目转包给独立的项目组(多线程方式)

    针对进程级别的fd列表,该方法会创建多个线程去处理socket的数据,例如一个连接socket就创建一个线程去处理,但是这种方式会受限于计算机的性能,一台机器无法创建很多进程或者线程。有个C10K,它的意思是一台机器要维护 1 万个连接,就要创建 1 万个进程或者线程,那么操作系统是无法承受的。如果维持 1 亿用户在线需要 10 万台服务器,成本也太高了

  3. 方法三:一个项目组支撑(IO多路复用,一个线程维护多个Socket)

    将进程级别的文件描述符列表(也就是socket列表)的数据放在一个文件描述符集合fd_set中,然后通过select函数去监听这个集合是否发生了变化,一旦有变化那么就会依次查看每个文件描述符。那些有变化的文件描述符在fd_set对应的位设置为1,表示socket可读或者可写,从而可以进行读写操作,然后再调用 select,接着盯着下一轮的变化。但是这种方式采取的是轮询的方式去查看fd_set的改变情况,大大限制的socket的数量( 通过FD_SETSIZE 限制)

  4. 方法四:一个项目组支撑多个项目(IO 多路复用,从“派人盯着”到“有事通知”)

    将select的轮询改为事件通知机制会好很多,当socket发生变化的时候,主动通知项目组,然后项目组再根据然后项目组再根据项目进展情况做相应的操作。

    epoll函数:通过注册callback函数的方式,当某个fd发生变化时,就会主动通知

5.应用层

5.1.DNS解析

DNS服务器:

image-20210831142351297

  • 根DNS服务器:返回顶级域 DNS 服务器的 IP 地址
  • 顶级域DNS服务器:返回权威 DNS 服务器的 IP 地址
  • 权威DNS服务器:返回相应主机的 IP 地址

5.1.1.DNS解析流程

image-20210831142633906

DNS 递归查询过程如下:

  1. 先去本地缓存host文件查找
  2. 如果没有再去请求本地DNS服务器,也就是网络服务商(ISP)的服务器
  3. 本地DNS如果找不到则去请求根域名服务器,根域名服务器全球共有13套,不用于直接解析
  4. 然后根域名服务器根据一级域名(.com、.cn等)转向顶级域名服务器,然后根据二级域名,例如163.com,让本地DNS服务器去寻找权威DNS域名服务器
  5. 此时权威DNS域名服务器会将域名对应的IP地址告诉本地DNS服务器
  6. 本地DNS服务器再将IP信息返回给客户端,再去建立连接

5.2.http协议

http是基于TCP协议的,因此需要先建立TCP连接,

目前使用的 HTTP 协议大部分都是 1.1。在 1.1 的协议里面,默认是开启了 Keep-Alive 的,这样建立的 TCP 连接,就可以在多次请求中复用

5.2.1.http请求的构建

image-20210831150312910

请求行:

  • URL:例如 http://www.163.com
  • 版本:HTTP 1.1
  • 方法:
    1. GET
    2. POST
    3. PUT
    4. DELETE

首部(header):首部是 key value,通过冒号分隔。这里面,往往保存了一些非常重要的字段。

  • Accept-Charset:客户端可以接收的字符集
  • Content-Type:正文的格式,例如JSON
  • Cache-control:当客户端发送的请求中包含 max-age 指令时,如果判定缓存层中,资源的缓存时间数值比指定时间的数值小,那么客户端可以接受缓存的资源;当指定 max-age 值为 0,那么缓存层通常需要将请求转发给应用集群
  • If-Modified-Since:如果服务器的资源在某个时间之后更新了,那么客户端就应该下载最新的资源;如果没有更新,服务端会返回“304 Not Modified”的响应,那客户端就不用下载了,也会节省带宽

5.2.2.HTTP 返回的构建

image-20210831151500469

状态行:HTTP请求的结果

首部(header):

实体:

5.3.https

http协议会有报文被截取的风险,因此我们需要针对报文进行加密,加密方式分为如下两种:

  1. 对称加密

    双方都持有一个秘钥,然后双方都通过此秘钥进行加密解密,但是秘钥也需要在互联网上传播,因此也有被截取的风险,因此这个方法也不可靠

  2. 非对称加密

    非对称加密是指私钥放在服务端,公钥放在客户端,客户端通过公钥加密的报文只能通过私钥去解密,但是服务端返回给客户端的报文也需要加密,此时只能够客户端也去持有一个公钥私钥,然后服务端通过客户端的秘钥去加密报文并返回,此时又会有一个问题,那就是客户端和服务端如何去获取对方的公钥呢?这也是数字证书解决的问题

5.3.1.数字证书

如果我创建了一个公钥私钥,服务端或者客户端如何能保证这个公钥是对的,会不会有黑客冒充给我公钥呢?

这个时候就需要权威部门的介入了,就是说每个人都能有自己的秘钥,但是只有权威部门(CA)盖章的公钥才是准确的,而这个我们就称为证书。

证书有什么?

  1. 公钥
  2. 证书所有者,
  3. 发布机构和有效期

证书如何生成呢?会不会有人冒充生成证书呢?

生成证书需要发起一个请求,然后将这个请求发给一个权威机构去认证,这个权威机构我们称为CACertificate Authority)。

证书请求可以通过这个命令生成。

openssl req -key cliu8siteprivate.key -new -out cliu8sitecertificate.req

请求CA机构对证书盖章时候,会同时根据请求内容生成一个签名,同时发送给CA机构,相当于CA机构给服务端背书,CA会针对这个签名进行加密(使用CA的密匙)

此时客户端如果想拿服务端的公钥,就需要通过证书去获取(服务端会将证书传给客户端),客户端可以通过CA的公钥去解密外卖证书的签名(该签名在服务端CA签名的时候会发给CA机构,CA机构会根据CA机构的私钥去对签名进行加密),客户端通过CA公钥解密成功,并且验证签名无误之后,就说明这个证书是正确的

客户端的CA公钥哪里来的?

默认安装在操作系统中,并且有一个证书管理器去管理

客户端的CA公钥可不可能也不可靠呢?

会出现,但是证书以证书链的形式存在,就是说,CA 的公钥也需要更牛的 CA 给它签名,然后形成 CA 的证书。要想知道某个 CA 的证书是否可靠,要看 CA 的上级证书的公钥,能不能解开这个 CA 的签名。样层层上去,直到全球皆知的几个著名大 CA,称为root CA,做最后的背书。通过这种层层授信背书的方式,从而保证了非对称加密模式的正常运转。

5.3.2.https的工作模式

非对称加密在性能上不如堆成加密,因此https协议实际是将两者结合,公钥私钥主要用于传输对称加密的秘钥,而真正的双方大数据量的通信都是通过对称加密进行的

image-20210831161135767

参考链接:

https://www.itzhai.com/articles/comprehend-the-underlying-principles-of-network-programming.html

https://time.geekbang.org/column/intro/100007101

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

哈哈哈张大侠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值