整体结构流程可参考:深入浅出 TCP/IP 协议栈
好文推荐 TCP /IP协议详解
好文推荐 键入网址后,期间发生了什么?
先了解整体结构,然后逐个击破,了解细节

1. 网络通信

中继器:信号放大器

集线器(hub):是中继器的一种形式,区别在于集线器能够提供多端口服务,多口中继器,每个数据包的发送都是以广播的形式进行的,容易阻塞网络。

网桥:局域网之间建立连接的桥梁,网桥是一种对帧进行转发的技术,根据MAC分区块,可隔离碰撞。网桥将网络的多个网段在数据链路层连接起来。

交换机(switch):工作在数据链路层,交换机与网桥的细微差别在于交换机常常用来连接独立的计算机,而网桥连接的目标是LAN,所以交换机的端口较网桥多。而且集线器是以广播形式发送数据包,交换机有一个智能化的功能,可以根据相应的地址发送数据包。交换机mac表的获取?

  • 转发过滤: 当一个数据帧的目的地址在MAC地址中有映射时,它被转发到连接目的节点的端口而不是所有端口
  • 学习功能:以太网交换机了解每一个端口相连设备的MAC地址,并将地址同相应的端口映射起来存放在交换机缓存中的MAC地址表中。

路由器:连接多个逻辑上分开的网络,能够判断网络地址和选择IP路径,内部存储路由表(配置路由“”),路由表可静态设置,亦可动态设置(根据RIP路由解析协议自动记录),每经过一次路由器,TTL值就会减1。

​ ping命令使用的是ICMP协议

​ ARP协议: 根据IP地址获取mac地址 (arp -a, 查看插卡的Mac地址)

​ RARP协议:根据mac地址获取IP地址

​ IP:标记逻辑上的地址

​ MAC:标记实际转发数据时的设备地址

​ netmask:和IP地址一起来确定网络号,

​ 默认网关:发送的IP不在同一个网段内,那么会把这个数据转发给默认网关。Mac地址,在两个设备之间通信时变化(路由器),IP地址在整个通信过程中不会发生任何变化。

​ DNS服务器:域名解析服务器,根据域名解析IP地址

通信领域的单工、半双工、全双工

  • 单工通信:传输数据只支持数据在一个方向上传输(收音机)
  • 半双工:传输允许在两个方向上传输,但是,在某一时刻,只允许数据在一个方向上传输,实际上是一种切换方向的单工通信放心,如:对讲机,单行道
  • 全双工:允许数据同时在两个方向上传输,同一时间,允许发送和接收数据。如:网卡,电话,手机,socket。软件开发领域实现TCP的全双工只能是通过多线程或者多进程来处理。

OSI模型

image.png

image.png

常用的以太网帧格式

  1. MAC帧主要有两种格式 一种是以太网V2标准,一种是IEEE802.3,常用是前者。参考:Mentalflow同志
  • Ethernet II
  • image.png
    • DMAC(Destination Mac) 目的MAC地址 DMAC字段的长度是6个字节,标识帧的接收者
    • SMAC(Source Mac) 是源MAC地址,字段长度6个字节,标识发送者
    • TYPE 用于标识数据字段中包含的高层协议,该字段长度为2个字节,类型字段为0x0800的帧代表IP协议帧,类型字段值为0x0806的帧标识ARP协议帧。
    • Data 是网络层数据,最小长度必须是46字节以保证帧长至少为64字节,数据字段的最大长度是1500字节
    • FCS 循环冗余校验字段 提供了一种错误检测机制,该字段长度为4个字节
  • 802.3帧
  • image.png
  • EEE802.3帧格式类似于Ethernet_II帧,只是Ethernet_II帧的Type域被802.3帧的Length域取代,并且占用了Data字段的8个字节作为LLC和SNAP字段。
  • Length 字段定义了Data字段包含的字节数
  • LLC 逻辑链路控制,由目的服务访问点DSAP、源服务访问点SSAP和Control字段组成
  • SNAP (sub network Access Protocol)由机构代码(Org Code)和类型(TYPE)字段组成,Org code三个字节都为0。Type字段的含义与Ethernet_II帧中的Type字段相同。
  • 两台电脑之间通信的前提是什么? 在同一网段

ping命令的过程:

以太网帧 TYPE: 0x806 代表ARP协议,先通过ARP协议在同网段中广播获取目的IP的MAC地址,

2 TCP/IP协议族详解之IP协议

2.1 IP协议的功能:

- 路由寻址
- 传递服务,有两个特点:不可靠,可靠性由上层协议提供,如TCP协议,无连接(IP并不维护任何关于后续数据报的状态信息。每个数据报的处理是相互独立的,这也就是说IP数据报可以不按发送顺序接收)
- 数据包分段(Segment)和重组

2.2 IP协议头部格式

image.png

可根据Wireshark抓包工具分析数据包含义 参考:TCP/IP协议族详解(二)

  • 版本: IP协议的版本,目前版本号为4, 下一代IP协议的版本号为6
  • 首部长度: IP报头的长度,占4位,固定部分的长度(20字节)和可变部分的长度之和,最大为60字节
  • 服务类型:TOS, 目前暂没有人使用
  • 报文总长度:报头的长度和数据部分之和
  • 标识:是一个计数器,用来产生数据报的标识。唯一的标识主机发送的每一分数据报。通常每发送一个报文,它的值加一。当IP报文长度超过传输网络的MTU(最大传输单元)时必须分片,这个标识字段的值被复制到所有数据分片的标识字段中,使得这些分片在达到最终目的地时可以依照标识字段的内容重新组成原先的数据
  • 标志位:共3位。R、DF、MF三位。目前只有后两位有效,DF位:为1表示不分片,为0表示分片。MF:为1表示“更多的片”,为0表示这是最后一片
  • 片偏移:本分片在原先数据报文中相对首位的偏移位。(需要再乘以8)
  • 报文生存时间:IP报文所允许的通过路由器的最大数量。没经过一个路由器,TTL减1,当为0时,路由器将该数据报丢弃,一般是64, 当发送ICMP回显应答时设置为最大值255
  • 协议:指定IP报文所携带的数据使用的是什么协议。以便目的主机IP层知道要将数据上交到那个进程(不同的协议有专门不同的进程处理),和端口号类型,此处采用协议好。TCP的协议号为6,UDP的协议号为17, ICMP的协议号为1,IGMP的协议号为2.
  • 首部校验和:计算IP头部的校验和,检查IP报头的完整性

应用程序使用TCP/IP协议传输数据时,数据要被送入协议栈经过逐层封装,最后作为比特流在媒体上传送,其过程示意图如下所示:

image.png

注:从上图可以看到以太网帧的数据长度是有大小限制的,这个最大值称为 MTU,所以当 IP 数据包长度大于 MTU 时会被拆成多个帧传输,称为 “IP分片”-------Mr.su Blog

2.2 IP路由寻址协议

推荐阅读如何形象说明路由协议RIP和OSPF?

RIP

RIP( Routing Information Protocol )路由信息协议

是在一个AS系统中使用地内部路由选择协议,是个非常简单的基于距离向量路由选择的协议。 它路由器生产商之间使用的第一个开放标准,是最广泛的路由协议,在所有IP路由平台上都可以得到。当使用RIP时,一台Cisco路由器可以与其他厂商的路由器连接。
RIP 主要设计来利用同类技术与大小适度的网络一起工作,因此通过速度变化不大的接线连接。RIP 比较适用于简单的校园网和区域网,不适于复杂网络的情况
RIP的算法简单,距离向量路由选择算法,, RIP使用UDP数据包更新路由信息。路由器每隔30s更新一次路由信息,如果在180s内没有收到相邻路由器的回应,则认为去往该路由器的路由不可用,该路由器不可到达。如果在240s后仍未收到该路由器的应答,则把有关该路由器的路由信息从路由表中删除。

RIP具有以下特点:
 不同厂商的路由器可以通过RIP互联;
 配置简单; • 适用于小型网络(小于15跳);
 RIPv1不支持VLSM;
 需消耗广域网带宽;
 需消耗CPU、内存资源。

OSPF

OSPF( Open Shortest Path First,开放最短路径优先)

开放式最短路径优先(Open Shortest Path First,OSPF)协议是一种为IP网络开发的内部网关路由选择协议,由IETF开发并推荐使用。OSPF协议由三个子协议组成:Hello协议、交换协议和扩散协议。其中Hello协议负责检查链路是否可用,并完成指定路由器及备份指定路由器;交换协议完成“主”、“从”路由器的指定并交换各自的路由数据库信息;扩散协议完成各路由器中路由数据库的同步维护.

OSPF 采用链路状态路由选择技术,开放最短路径优先算法迪杰斯特拉(Dijkstra)算法

OSPF协议具有以下优点:
 • OSPF能够在自己的链路状态数据库内表示整个网络,这极大地减少了收敛时间,并且支持大型异构网络的互联,提供了一个异构网络间通过同一种协议交换网络信息的途径,并且不容易出现错误的路由信息。
 • OSPF支持通往相同目的的多重路径。
 • OSPF使用路由标签区分不同的外部路由。
 • OSPF支持路由验证,只有互相通过路由验证的路由器之间才能交换路由信息;并且可以对不同的区域定义不同的验证方式,从而提高了网络的安全性。
 • OSPF支持费用相同的多条链路上的负载均衡。
 • OSPF是一个非族类路由协议,路由信息不受跳数的限制,减少了因分级路由带来的子网分离问题。
 • OSPF支持VLSM和非族类路由查表,有利于网络地址的有效管理
• OSPF使用AREA对网络进行分层,减少了协议对CPU处理时间和内存的需求。

• 适用于规模庞大、环境复杂的互联网

BGP

BGP (边界网关协议,Border Gateway Protocol)

BGP用于连接Internet。作为最新的外部网关协议,现有四个版本。
BGP 是唯一一个用来处理像因特网大小的网络协议,也是唯一能够妥善处理好不相关路由域间的多路连接协议。BGPv4是一种外部的路由协议。可认为是一种高级的距离向量路由协议
在BGP网络中,可以将一个网络分成多个自治系统。自治系统间使用eBGP广播路由,自治系统内使用iBGP在自己的网络内广播路由。

BGP路由选择方法是基于距离向量路由选择, 与传统的距离向量(1个单独的度量,如跳数)协议不同,BGP将AS外部路径的度量复杂化。BGP系统的主要功能是和其他BGP系统交换网络可达信息。网络可达信息包括列出的AS信息。这些信息有效地构造了 AS互联的拓朴图并由此清除了路由环路,同时在 AS级别上可实施策略决策。
BGP特点:

 BGP是一种外部路由协议,与OSPF、RIP不同,其着眼点不在于发现和计算路由,而在于控制路由的传播和选择最好的路由。
 BGP通过携带AS路径信息,可以彻底的解决路由循环问题。
 为了控制路由的传播和路由的选择,为路由附带属性信息。
 使用TCP作为其传输层协议,提高了协议的可靠性。端口号179。
 BGP-4支持CIDR(无类别域间选路),CIDR的引入简化了路由聚合,减化了路由表。
 BGP更新时只发送增量路由,减少了BGP传播路由占用的带宽。
 提供了丰富的路由策略。

————————————————
以上内容转自RIP、OSPF、BGP三种协议

3 TCP/IP协议族详解之ARP协议

ARP首先会发起一个请求数据包,数据包的首部包含了目标主机的IP地址,然后这个数据包会在链路层进行再次包装,生成以太网数据包,最终由以太网广播给子网内的所有主机,每一台主机都会接收到这个数据包,并取出标头里的IP地址,然后和自己的IP地址进行比较,如果相同就返回自己的MAC地址,如果不同就丢弃该数据包。ARP接收返回消息,以此确定目标机的MAC地址;与此同时,ARP还会将返回的MAC地址与对应的IP地址存入本机ARP缓存中并保留一定时间,下次请求时直接查询ARP缓存以节约资源。cmd输入 arp -a 就可以查询本机缓存的ARP数据。

ARP分组封装

ARP是为IP协议提供服务的,所以,把ARP划分到了网络层

image.png

3. 1 为什么有了IP地址还要使用Mac地址

  1. IP地址容易修改和变动,不能再网络上固定标识一个设备
  2. Mac地址一般在出场时被烧录到硬件中,不易修改,能在局域网中定位唯一一台设备
  3. 从拓扑结构和分层上分析,IP地址属于网络层,主要功能实在广域网范围内路由寻址,选择最佳路由,而Mac地址是网络接口层要形成适合于网络媒体上传输的数据帧。

请求包是广播,而应答包是单播

3.2 ARP缓存表以及相关命令

ARP高速缓存表的作用:

为了减少网络上的通信量,主机 A 在发送其 ARP 请求分组时,就将自己的 IP 地址到硬件地址的映射写入 ARP 请求分组。当主机 B 收到 A 的 ARP 请求分组时,就将主机 A 的这一地址映射写入主机 B 自己的 ARP 高速缓存中。这对主机 B 以后向 A 发送数据报时就更方便了。

注意:arp缓存表分为静态和动态两种方式,默认情况下ARP缓存的超时时限是两分钟

ARP命令:

  • arp -d 清除本机arp 缓存表
  • arp -a 查看本机当前arp表
  • arp -s 绑定arp地址(机器重启后全部失效)

3 TCP/IP协议族详解之ICMP协议

存在的意义:由于IP协议无差错报告和差错纠正机制,缺少一种为主机和管理查询的机制。如, 当IP数据报在网络中超时了它的TTL, 那么路由器就会将这个数据报丢失,但是没有对这个丢弃操作返回错误报告。为了弥补这个缺点,所以产生了ICMP协议。注:ICMP没有纠正错误的机制

image.png

ICMP是网络层协议,报文首先封装成IP数据报,然后再传送给下一层,在IP数据报中的协议位,标识值为1(PRO:0x1f)

3.1 Ping命令使用详解

ping命令是基于ICMP的查询报文,分为回送请求和回送应答,请求类型为8, 应答类型为0。

C:\Windows\System32>ping www.baidu.com

正在 Ping www.a.shifen.com [14.215.177.39] 具有 32 字节的数据:
来自 14.215.177.39 的回复: 字节=32 时间=7ms TTL=54
来自 14.215.177.39 的回复: 字节=32 时间=8ms TTL=54
来自 14.215.177.39 的回复: 字节=32 时间=8ms TTL=54
来自 14.215.177.39 的回复: 字节=32 时间=7ms TTL=54

14.215.177.39 的 Ping 统计信息:
    数据包: 已发送 = 4,已接收 = 4,丢失 = 0 (0% 丢失),
往返行程的估计时间(以毫秒为单位):
    最短 = 7ms,最长 = 8ms,平均 = 7ms

详细流程:

  1. 首先经过DNS服务器,将域名解析成IP地址,即www.a.shifen.com [14.215.177.39]
  2. 然后Ping命令发送一个带有32字节数数据的ICMP请求,收到回复后显示结果,也就是上面的 来自 14.215.177.39 的回复: 字节=32 时间=7ms TTL=54, 其中字节表示测试数据长度(可以通过-l参数指定测试数据的大小,例如ping -l 1024 www.baidu.com); 时间表示包的往返时间(一去一来所用的时间); TTL为54(请求包的TTL为64),也就是回复的包进过了10个路由器

3.2 tracert命令详解

Linux下的命令是traceroute, windows下的命令是tracert。

tracert是路由跟踪程序,用于确定 IP 数据报访问目标所经过的路径。

tracert 命令用 IP 生存时间 (TTL) 字段和 ICMP 错误消息来确定从一个主机到网络上其他主机的路由。 在工作环境中有多条链路出口时,可以通过该命令查询数据是经过的哪一条链路出口。

tracert一般用来检测故障的位置,我们可以使用用tracert IP命令确定数据包在网络上的停止位置,来判断在哪个环节上出了问题,虽然还是没有确定是什么问题,但它已经告诉了我们问题所在的地方,方便检测网络中存在的问题。

常用相关命令:

tracert -d www.baidu.com // 不讲地址解析成主机名,能够更快显示路由器路径

tracert -h 5 www.baidu.com // 指定跟踪的跃点数

tracert -w 10 www.baidu.com //指定等待每个应答的时间(以毫秒为单位)。默认值为 3000 毫秒(3 秒)

4. UDP 用户数据报协议

4.1 UDP用户数据报协议

无连接的简单的面向数据报的运输层协议。

  • 特点: UDP数据报文中包括目的端口号和源端口号信息,由于通讯不需要连接,所以可以实现广播发送。UDP传输数据时有大小限制,每个被传输的数据报必须限定在64KB之内不可靠传输协议,发送方所发送的数据报并不一定以相同的次序到达接收方。传输速度快

  • 适用场景:UDP一般用于多点通信和实时数据的业务,注重速度流畅

    • 语音广播
    • 视频会议系统
    • QQ
    • TFTP SNMP RIP(路由信息协议,如报告股票市场,航空信息)
    • DNS(域名解释)

4.2 创建UDP网络程序流程:

  • 1.创建客户端套接字
  • 2.发送/接受数据
  • 3.关闭套接字

image.png

通信流程:

20200531124111.png

5 TCP/IP协议族详解之TCP协议

面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793定义。

  • 特性:
    • 面向连接,通信双方必须先建立连接,双方都必须为该连接分配一定的内核资源,以管理连接的状态和连接上的传输。
    • 可靠传输:
      • TCP采用发送应答机制
      • 超时重传:发送端发出一个报文段之后就会启动定时器,在定时时间内没收到应答就重发这个报文段,为了保证不发生丢包,就给每一个包一个序号,同时序号也保证了传送到接收端实体的包按序接收。然后接收端对已成功收到的包回一个 ACK包。如果发送端在合理的RTT内未收到确认,对应的数据包将被假设为已丢失,将会进行重传
      • 错误校验:TCP用一个校验和函数来检验数据是否有错误;在发送和接收时都要计算校验和。
      • 流量控制和阻塞管理:流量控制用来避免主机发送得过快而使接收方来不及完全收下

tcpmode.png

5.1 创建TCP网络程序流程

  • 服务端

    • # coding: utf-8
      
      import socket
      
      # 创建tcp套接字
      tcpserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      
      addr = ('localhost', 7777)
      
      # 绑定ip
      tcpserver.bind(addr)
      # 开启监听
      tcpserver.listen(5)
      # 接收客户端请求
      print(f'TCP 服务器已开启:{addr}')
      while True:
          newSocket, clientAddr = tcpserver.accept()
          while True:
              data = newSocket.recv(1024)
              if len(data) > 0:
                  print('receive from [%s]:%d, data: %s' % (*clientAddr, data.decode('utf-8')))
              else:
                  break
              newSocket.send('thank you!'.encode('utf-8'))
          newSocket.close()
      
      tcpserver.close()
      
  • 客户端

    • # coding: utf-8
      
      import socket
      
      tcpclient = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
      
      dest = ('localhost', 7777)
      
      tcpclient.connect(dest)
      
      while True:
          sendData = input('send: #some msg#')
      
          if len(sendData) > 0:
              tcpclient.send(sendData.encode('utf-8'))
          else:
              break
      
          recvData = tcpclient.recv(1024)
          print(recvData.decode('utf-8'))
      
      tcpclient.close()
      

5.2 TCP的数据包格式

image.png

  • 源端口和目的端口:各占16bit=2字节

  • 序列号(Seq):占32位=4字节 range=[0:2^32] 表示数据的第一个字节的序列号,TCP的数据交互式基于序列号(控制华东窗口),发送方通过序列号控制发送的数据,以及超时重传,接收方通过序列号控制乱序重排。

    接收方根据三次握手后确认的首字节序列号+数据长度,计算得到最后一个字节的序列号,并将其加1作为ack应答。

  • 确认号(ACK):占4个字节,表示期望下次收到的序列号。比如服务器收到客户端发来的报文段,其序列号字段值为501,并通过计算可知数据长度为200,所以服务器可以算出最后一个字节的序列号为700。这表明服务器正确收到了客户端发送的序列号到700为止的数据,因此,服务器期望下次收到的序列号为701,并将其作为确认号放入应答报文段中

    确认号和序列号范围相同,当溢出时从0开始

  • 数据偏移:占4bit, 表示TCP报文段的第一个数据距离报文段起始处有多远。数据偏移代表的是4字节的倍数,由于4位二进制最大的可以表示15, 所以数据偏移最大值为4*15=60字节,即TCP报文首部最大长度。最小为20字节,偏移值=5。

  • 保留 占6位,占未使用,可能是预留其他控制标志位,或者对齐字节位

  • 控制位,用于说明报文段的性质。每个控制字段占1位

    • 紧急URG:开启时表示此数据包处于紧急状态应优先处理

    • 确认标志位ACK:开启表明确认号有效,TCP规定连接建立后发送的所有报文段ACK位都必须置1

    • 推送PSH:该控制位很少使用,因为TCP会自己决定什么时候应该使用PUSH操作。

    • 复位RST:用于复位,表示连接出现错误,应当立即关闭。当TCP接收到复位报文段后会通知应用程序连接被复位,随后关闭连接

    • 同步SYN:连接建立的过程中用于同步序列号,告知对方自己的起始序列号。可以根据对方的序列号初始化缓冲区起点(滑动窗口)

      SYN=1,ACK=0时表示一个连接请求报文段,SYN=1,ACK=1表示一个连接接收报文段

    • 终止FIN:用于释放连接,报文段中FIN控制位为1表示已经将数据发送完毕。等待关闭连接

    • 窗口:占2个字节,表示发送该报文段的一方能够接收的字节数,表明期望接受到的数据包字节数,用于拥塞控制。窗口值范围为[0:2^16−1]

    • 校验和:占2个字节,用于检验报文段是否出错。发送方根据发送的报文段计算检验和填入报文段首部,接收方根据接收的报文段重新计算,如果不匹配,表明报文段出错

    • 紧急指针:占2个字节,表示紧急数据的个数。在紧急状态下(URG打开),指出窗口中紧急数据的位置(末端)。

    • 选项:用于支持一些特殊的变量,比如最大分组长度(MSS),MSS指的是数据的最大长度而不是TCP报文段长度。在将数据发送之前,会根据MSS将数据进行合理的切分,即单次发送的报文段中的数据不能超过MSS,所以MSS应该适当调大一些以降低网络中的报文段个数

    查缺补漏

    MSS(Maximum Segment Size):MSS 是TCP选项中最经常出现,也是最早出现的选项。MSS选项占4byte。MSS是每一个TCP报文段中数据字段的最大长度,注意:只是数据部分的字段,不包括TCP的头部。TCP在三次握手中,每一方都会通告其期望收到的MSS(MSS只出现在SYN数据包中)如果一方不接受另一方的MSS值则定位默认值536byte。
    MSS值太小或太大都是不合适。太小,例如MSS值只有1byte,那么为了传输这1byte数据,至少要消耗20字节IP头部+20字节TCP头部=40byte,这还不包括其二层头部所需要的开销,显然这种数据传输效率是很低的。MSS过大,导致数据包可以封装很大,那么在IP传输中分片的可能性就会增大,接受方在处理分片包所消耗的资源和处理时间都会增大,如果分片在传输中还发生了重传,那么其网络开销也会增大。因此合理的MSS是至关重要的。MSS的合理值应为保证数据包不分片的最大值,对于以太网MSS可以达到1460byte,在IP层中有一个类似的概念,MTU(Maximum Transfer Unit)

    MTU=MSS+TCP Header + IP Header

    image.png

    为什么需要MSS?

    主要是为了最大程度的保证传输的高效和稳定性

    那么MTU和MSS又有什么必然联系呢?虽然MTU限制了IP层的报文大小,但分层网络模型本来不就是为了对上层提供透明的服务么?即使一个很大的TCP报文传递给IP层,IP层也应该可以经过分段等手段成功传输报文才对。

    理论上来说是没错的,UDP中就不存在MSS,UDP生成任意大的UDP报文,然后包装成IP报文根据底层网络的MTU分段进行发送。MSS存在的本质原因就是TCP和UDP的根本不同:TCP提供稳定的连接。假设生成了很大的TCP报文,经过IP分段进行发送,而其中一个IP分段丢失了,则TCP协议需要重发整个TCP报文,造成了严重的网络性能浪费,而相对的由于UDP无保证的性质,即使丢失了IP分段也不会进行重发。所以说,MSS存在的核心作用,就是避免由于IP层对TCP报文进行分段而导致的性能下降

    通常将MSS设置为MTU-40(20字节IP头部+20字节TCP头部),在TCP建立连接时由连接双方商定,双方得到的MSS值可能并不相同,建立MSS所基于MTU的值基于路径MTU发现机制获取。

    参考:

    TCP报文段首部格式

    TCP Maximum Segment Size (MSS)

    TCP Maximum Segment Size (MSS) and Relationship to IP Datagram Size

5.3 TCP 三次握手

抛出疑惑:为什么是三次握手而不是二次或者四次握手?

TCP作为一种可靠传输控制协议,核心思想:既要保证数据可靠传输,又要提高传输的效率,而用三次就可以满足以上两方面的需求。

TCP的可靠性就是通过三次握手就是确认通信双方数据原点的初始序列号 (Initial Sequence Number)

通俗的描述:客户端A发出连接请求,由操作系统动态随机选取一个32位长的序列号(Initial Sequence Number), 假设A的初始序列号是1000, 以该序列号为原点,对自己将要发送的每个字节进行编号,1001, 1002..., 并把自己的初始序列号INS告诉B, 什么样的编号的数据是合法的,方便服务端B对A的每一个编号的字节数据进行确认。如:如果A收到B确认编号为2001,则意味着字节编号为1001-2000,共1000个字节已经安全到达。

同理B也是类似的操作,假设B的初始序列号ISN为2000,以该序列号为原点,对自己将要发送的每个字节的数据进行编号,2001,2002,2003…,并把自己的初始序列号ISN告诉A,以便A可以确认B发送的每一个字节。如果B收到A确认编号为4001,则意味着字节编号为2001-4000,共2000个字节已经安全到达。

image.png

第一次握手:

客户端向服务端发送连接请求报文段,报文段的头部中SYN=1, ACK=0, seq=x。请求发送后,客户端进入SYN-SENT状态

  • SYN=1, ACK=0 标识该报文段为连接请求报文
  • seq=x, 标识本次TCP通信客户端数据字节流的初始序列号
  • TCP规定:SYN=1的报文段不能有数据部分,但要消耗掉一个字节,一个序号

第二次握手:

服务端处于监听状态LISTEN,收到连接请求报文后,如果同意连接,返回一个应答 SYN=1, ACK=1, seq=y, ack=x+1, 进入SYN-RCVD状态

第三次握手:

当客户端收到服务器的应答后,还要向服务端发送一个确认报文段,表示服务端发来的连接同意应答已经成功收到,且收到服务端的出示序列号y

确认报文为:ACK=1, seq=x+1, ack=y+1。

为什么连接建立需要三次握手,而不是2次握手?

防止失效的连接请求报文段被服务端接收,从而产生错误,失效的连接请求:若客户端向服务端发送的连接请求丢失,客户端等待应答超时后就会再次发送连接请求,此时,上一个连接请求就是『失效的』---《计算机网络》谢希仁版

三次握手中存在的漏洞:SYN flood!,攻击者通过向服务器发起大量的SYN报文,把服务器的SYN报文连接的队列生生耗尽,导致正常的连接请求得不到处理,目前只能进行减缓,别没有解决补丁

  1. 在web应用程序中可以使用安全的CSRF令牌环节问题。CSRF攻击将在服务器造成持久的变化而没有处理要求,除非使用了有效的CSRF令牌。

  2. 首保丢弃:可通过丢弃客户端的第一个SYN报文来达到防御的目的,TCP是一种可靠的协议,为了确保所有的数据包都能到达服务器,设计了一个重传机制。真实的客户端访问,在一定的时间内如果没有收到服务器的回复,将会再次发送SYN报文。

  3. 内核层面进行缓解:

    • 增大tcp_max_syn_backlog
    • 减小tcp_synack_retries
    • 启用tcp_syncookies: 当启用tcp_syncookies时,backlog满了后,linux内核生成一个特定的n值,而不并把客户的连接放到半连接的队列backlog里(即没有存储任何关于这个连接的信息,不浪费内存)。当客户端提交第三次握手的ACK包时,linux内核取出n值,进行校验,如果通过,则认为这个是一个合法的连接。(加密的INS)

    注:tcp_max_syn_backlog 在 syn_cookies 开启时是无效的,这两个选项存在冲突

5.4 TCP四次挥手

image.png

第一挥手

若A认为数据发送完成,就会向B发送连接释放请求,该请求只有报文头,头重携带的主要参数为:FIN=1, seq=u, 此时A进入FIN-WAIT-1状态

  • FIN=1即TCP报文段中的控制位FIN置1表示该数据报为连接释放请求
  • seq=u, u-1是A向B发送的最后一个字节的序号

第二次挥手

B收到连接释放请求后,会通知相应的应用程序,告诉它连接已经释放,此时B进入CLOSE_WAIT状态, 报文头:ACK=1, seq=v, ack=u+1

  • ACK=1, 除了TCP连接请求报文段以外,TCP通信过程中数据报的ACK控制为都为1
  • seq=v, v-1表示B向A发送的最后一个字节的序号
  • ack=u+1 表示希望收到第u+1个字节开始的报文段,已经成功接收了签u个字节数据

A收到该应答后进入 FIN_WAIT_2状态,等待B发送连接释放请求

第二次挥手后,A->B方向的连接已经释放,A不会再发送数据,但B->A方向的连接仍然存在。
此时会有一个孤儿连接的问题,可以参考文章TCP连接的性能优化

第三次挥手

当B向A发送完所有数据后,向A发送连接释放请求,请求头: FIN=1, ACK=1, seq=w, ack=u+1 B进入 LAST_ACK状态

第四次挥手

A收到释放请求后,向B发送确认应答,A进入TIME_WAIT状态。该状态会持续2MSL(Maximum Segment Lifetime)时间,(报文最大生存时间),若该时间段内B没有发送请求的话,就进入CLOSED状态,关闭TCP。当B收到确认应答后,也进入CLOSED状态, 关闭TCP。

为什么A要先进入TIME-WAIT状态,等待2MSL时间后才进入CLOSED状态?

为了保证B能收到A的确认应答。
若A发完确认应答后直接进入CLOSED状态,那么如果该应答丢失,B等待超时后就会重新发送连接释放请求,但此时A已经关闭了,不会作出任何响应,因此B永远无法正常关闭。在模拟tcpserver的时候,如果是服务器先close的时候,在2MSL中(也就是2-4分钟之内并不会马上释放端口)不过在实际应用中可以通过设置 SO_REUSEADDR选项达到不必等待2MSL时间结束再使用此端口。

参考TCP 为什么是三次握手,而不是两次或四次?-[大闲人柴毛毛]
参考:TCP的三次握手与四次挥手理解及面试题

5.5 TCP 中的半连接和全连接队列

img

由于TCP建立连接需要进行3次握手,一个新连接在到达ESTABLISHED状态可以被accept系统调用返回给应用程序前,必须经过一个中间状态SYN RECEIVED(见上图)。这意味着,TCP/IP协议栈在实现backlog队列时,有两种不同的选择:

  1. 仅使用一个队列,队列规模由listen系统调用backlog参数指定。当协议栈收到一个SYN包时,响应SYN/ACK包并且将连接加进该队列。当相应的ACK响应包收到后,连接变为ESTABLISHED状态,可以向应用程序返回。这意味着队列里的连接可以有两种不同的状态:SEND RECEIVEDESTABLISHED。只有后一种连接才能被accept系统调用返回给应用程序。
  2. 使用两个队列——SYN队列(待完成连接队列)和accept队列(已完成连接队列)。状态为SYN RECEIVED的连接进入SYN队列,后续当状态变更为ESTABLISHED时移到accept队列(即收到3次握手中最后一个ACK包)。顾名思义,accept系统调用就只是简单地从accept队列消费新连接。在这种情况下,listen系统调用backlog参数决定accept队列的最大规模。

Linux实现了第二种方案,使用两个队列——一个SYN队列,长度系统级别可设置以及一个accept队列长度由应用程序指定

深入理解TCP握手过程中建连接的流程和队列

如上图所示,这里有两个队列:syns queue(半连接队列);accept queue(全连接队列)

三次握手中,在第一步server收到client的syn后,把相关信息放到半连接队列中,同时回复syn+ack给client(第二步);

比如syn floods 攻击就是针对半连接队列的,攻击方不停地建连接,但是建连接的时候只做第一步,第二步中攻击方收到server的syn+ack后故意扔掉什么也不做,导致server上这个队列满其它正常请求无法进来

accept队列已满的情况下,内核会强制限制SYN包的接收速率。如果有大量SYN包待处理,它们其中的一些会被丢弃。这样看来,就完全依靠客户端重传SYN包了,这种行为跟BSD实现一样。

如何查看全连接队列大小?

在服务端可以使用 ss 命令,来查看 TCP 全连接队列的情况:

需要注意的是 ss命令获取的Recv-Q/Send-Q 在「LISTEN 状态」和「非 LISTEN 状态」所表达的含义是不同的

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

  • 在LISTEN状态时
    • RECV-Q:当前,全连接队列的大小,也就是当前已经完成三次握手并等待服务端accept()的TCP连接个数;
    • SEND-Q:当前全连接最大队列长度,上面的输出结果说明监听8088端口的TCP服务进程,最大全连接长度为128;

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

  • 非LISTEN状态时
    • Recv-Q:已经收到但未被应用程序读取的字节数
    • Send-Q:已发送但未收到确认的字节数

全连接队列溢出会发生什么?

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

从上面的输出结果,可以发现当前 TCP 全连接队列上升到了 129 大小,超过了最大 TCP 全连接队列

当超过了 TCP 最大全连接队列,服务端则会丢掉后续进来的 TCP 连接,丢掉的 TCP 连接的个数会被统计起来,我们可以使用 netstat -s 命令来查看:

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

上面看到的 41150 times ,表示全连接队列溢出的次数,注意这个是累计值。可以隔几秒钟执行下,如果这个数字一直在增加的话肯定全连接队列偶尔满了。

当服务端并发处理大量请求时,如果 TCP 全连接队列过小,就容易溢出。发生 TCP 全连接队溢出的时候,后续的请求就会被丢弃,这样就会出现服务端请求数量上不去的现象。

全连接队列满了,会做什么操作?

当全连接队列满了,Linux的默认行为是丢弃该连接,当然我们还可以选择项客户端发送RST复位报文,告诉客户端连接已经建立失败。

cat /proc/sys/net/ipv4/tcp_abort_on_overflow

0 # 默认值是0

tcp_abort_on_overflow共两个值,分别是0,1

  • 0:表示如果全连接队列满了,那么server扔掉client发过来的ack
  • 1:表示如果全连接队列满了,那么server发送一个reset包给client' 表示废掉这个握手过程和这个连接

如果要想知道客户端连接不上服务端,是不是服务端 TCP 全连接队列满的原因,那么可以把 tcp_abort_on_overflow 设置为 1,这时如果在客户端异常中可以看到很多 connection reset by peer 的错误,那么就可以证明是由于服务端 TCP 全连接队列溢出的问题。

通常情况下,应当把 tcp_abort_on_overflow 设置为 0,因为这样更有利于应对突发流量。

举个例子,当 TCP 全连接队列满导致服务器丢掉了 ACK,与此同时,客户端的连接状态却是 ESTABLISHED,进程就在建立好的连接上发送请求。只要服务器没有为请求回复 ACK,请求就会被多次重发。如果服务器上的进程只是短暂的繁忙造成 accept 队列满,那么当 TCP 全连接队列有空位时,再次接收到的请求报文由于含有 ACK,仍然会触发服务器端成功建立连接

tcp_abort_on_overflow 设为 0 可以提高连接建立的成功率,只有你非常肯定 TCP 全连接队列会长期溢出时,才能设置为 1 以尽快通知客户端。

如何增大全连接队列

TCP全连接队列最大值取决于 somaxconnbacklog之间的最小值,也就是min(somaxconn, backlog), 可以从内核代码中得知

//Linux 2.6.35 net/socket.c
SYSCALL_DEFINE2(listen, int, fd, int, backlog) {
    // ...
    // /proc/sys/net/core/somaxconn
    somaxconn = sock_net(sock->sk)->core.sysctl_somaxconn;
    // TCP全连接队列最大值 = min(somaxconn, backlog)
    if ((unsigned)backlog > somaxconn) 
        backlog = somaxconn;
    // ...
}
  • somaxconn 是Linux内核的参数, 默认是128, 可以通过 /proc/sys/net/core/somaxconn来修改值
  • backloglisten(int, sockfd, int backlog)函数中backlog的大小, Nginx默认是511, 可以通过修改配置文件设置长度

修改全连接最大值

echo 5000 > /proc/sys/net/core/somaxconn

Nginx 的backlog 修改值为5000

# /usr/local/nginx/conf/nginx.conf
server {
	listen 8088 default backlog=5000;
	server_name localhost;
	# ....
}

需要重启Nginx服务, 因为要重新调用listen()函数, TCP全连接队列才会重新初始化。

重启完后 Nginx 服务后,服务端执行 ss 命令,查看 TCP 全连接队列大小:

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

增大TCP全连接后,做压测,使用wrk工具发出3万个连接

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

服务端执行 ss 命令,查看 TCP 全连接队列使用情况:

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

从上面的执行结果,可以发现全连接队列使用增长的很快,但是一直都没有超过最大值,所以就不会溢出,那么 netstat -s 就不会有 TCP 全连接队列溢出个数的显示:

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

说明 TCP 全连接队列最大值从 128 增大到 5000 后,服务端抗住了 3 万连接并发请求,也没有发生全连接队列溢出的现象了。

如果持续不断地有连接因为 TCP 全连接队列溢出被丢弃,就应该调大 backlog 以及 somaxconn 参数

如何查看半连接队列大小?

半连接的长度,并没有像全连接一样的参数,但是可以通过查看处于SYN_RECV状态的TCP连接

# 查看当前tcp 半连接队列的长度
netstat -natp | grep SYN_RECV | wc -l
256   # 表示当前处于半连接状态的TCP连接有256个

可以使用hping3工具模拟SYN攻击

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

当服务端受到 SYN 攻击后,连接服务端 ssh 就会断开了,无法再连上。只能在服务端主机上执行查看当前 TCP 半连接队列大小:

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

同时,还可以通过 netstat -s 观察半连接队列溢出的情况:

netstat -s | grep "SYNs to LISTEN"

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

上面输出的数值是累计值,表示共有多少个 TCP 连接因为半连接队列溢出而被丢弃。隔几秒执行几次,如果有上升的趋势,说明当前存在半连接队列溢出的现象

上面模拟 SYN 攻击场景时,服务端的 tcp_max_syn_backlog 的默认值如下:

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

我个人的Ubuntu 查看是128,不同操作系统配置不同

但是在测试的时候发现,服务端最多只有 256 个半连接队列,而不是 512,所以半连接队列的最大长度不一定由 tcp_max_syn_backlog 值决定的

TCP 第一次握手(收到 SYN 包)的 Linux 内核代码如下,其中缩减了大量的代码,只需要重点关注 TCP 半连接队列溢出的处理逻辑:

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

从源码中,我可以得出共有三个条件因队列长度的关系而被丢弃的:

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

  1. 如果半连接队列满了,并且没有开启 tcp_syncookies,则丢弃
  2. 如果全连接队列满了,且没有重传 SYN+ACK包的连接请求多余1个,则会丢弃
  3. 如果没有开启tcp_syncookies, 并且max_syn_backlog减去当前半连接队列长度小于(max_syn_backlog>>2),则会丢弃

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

  • 全连接队列的最大值是sk_max_ack_backlog=min(somaxconn, backlog)
  • 半连接队列的最大值是max_qlen_log变量,查看源码在哪里定义的这个变量

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

从上面的代码中,我们可以算出 max_qlen_log 是 8,于是代入到 检测半连接队列是否满的函数 reqsk_queue_is_full

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

也就是 qlen >> 8 什么时候为 1 就代表半连接队列满了。这计算这不难,很明显是当 qlen 为 256 时,256 >> 8 = 1

至此,总算知道为什么上面模拟测试 SYN 攻击的时候,服务端处于 SYN_RECV 连接最大只有 256 个。

可见,半连接队列最大值不是单单由 max_syn_backlog 决定,还跟 somaxconnbacklog 有关系

在 Linux 2.6.32 内核版本,它们之间的关系,总体可以概况为:TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

  1. max_syn_backlog > min(somaxconn, backlog)时, 半连接队列最大值是max_qlen_log = min(somaxconn, backlog) * 2
  2. max_syn_backlog < min(somaxconn, backlog)时, 半连接队列的最大值是 max_qlen_log = max_syn_backlog * 2

半连接队列的大小与什么有关?

半连接队列最大值 max_qlen_log 就表示服务端处于 SYN_REVC 状态的最大个数吗?

max_qlen_log理论半连接队列最大值,并不一定代表服务端处于 SYN_REVC 状态的最大个数。当触发条件3的时候 如果没有开启tcp_syncookies, 并且max_syn_backlog减去当前半连接队列长度小于(max_syn_backlog>>2),则会丢弃

服务端处于 SYN_RECV 状态的最大个数是 193,正好是触发了条件 3,所以处于 SYN_RECV 状态的个数还没到「理论半连接队列最大值 256」,就已经把 SYN 包丢弃了,

所以,服务端处于 SYN_RECV 状态的最大个数分为如下两种情况:

  • 如果「当前半连接队列」没超过「理论半连接队列最大值」,但是超过 max_syn_backlog – (max_syn_backlog >> 2),那么处于 SYN_RECV 状态的最大个数就是 max_syn_backlog – (max_syn_backlog >> 2)
  • 如果「当前半连接队列」超过「理论半连接队列最大值」,那么处于 SYN_RECV 状态的最大个数就是「理论半连接队列最大值」;

每个Linux内核的理论半连接最大值计算方式不同,在上面我们是针对 Linux 2.6.32 版本分析的「理论」半连接最大值的算法,可能每个版本有些不同。比如在 Linux 5.0.0 的时候,「理论」半连接最大值就是全连接队列最大值,但依然还是有队列溢出的三个条件。

如果SYN半连接队列已满,只能丢弃链接吗?

并不是这样,开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接,在前面我们源码分析也可以看到这点,当开启了 syncookies 功能就不会丢弃连接。

syncookies 是这么做的:服务器根据当前状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功,如下图所示

TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?

syncookies 参数主要有以下三个值:

  • 0 值,表示关闭该功能;
  • 1 值,表示仅当 SYN 半连接队列放不下时,再启用它;
  • 2 值,表示无条件开启功能;

echo 1 > /proc/sys/net/ipv4/tcp_syncookies

如何防御SYN攻击?

  • 增大半连接队列;
    • 增大半连接队列,半连接队列大小跟somaxconn, 和blocklog的值有关,所以三者都要增大
  • 开启 tcp_syncookies 功能
  • 减少 SYN+ACK 重传次数
    • 当服务端受到 SYN 攻击时,就会有大量处于 SYN_REVC 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接。那么针对 SYN 攻击的场景,我们可以减少 SYN+ACK 的重传次数,以加快处于 SYN_REVC 状态的 TCP 连接断开。
    • echo 1> /proc/sys/net/ipv4/tcp_synack_retries 设置重传次数为1次

转自-TCP 半连接队列和全连接队列满了会发生什么?又该如何应对?
关于TCP 半连接队列和全连接队列
[译文]深入理解Linux TCP backlog
TCP面试题(一)之TCP的三次握手和accept()的顺序
TCP的accept发生在三次握手的哪个阶段?

5.6 TCP 的流量控制和拥塞控制

流量控制

如果发送者发送数据过快,接收者来不及接收,那么就会有分组丢失。为了避免分组丢失,控制发送者的发送速度,使得接收者来得及接收,这就是流量控制。流量控制根本目的是防止分组丢失,它是构成TCP可靠性的一方面,流量控制由滑动窗口协议(连续ARQ协议)实现。滑动窗口协议既保证了分组无差错、有序接收,也实现了流量控制。主要的方式就是接收方返回的 ACK 中会包含自己的接收窗口的大小,并且利用大小来控制发送方的数据发送。

image.png

TCP规定,即使设置为零窗口,也必须接收一下几种报文

  • 零窗口探测报文段
  • 确认报文段
  • 携带紧急数据的报文段

确认丢失和确认迟到

image.png

持续计时器

存在这样一种情况:发送方接收到零窗口报文之后将发送窗口设置为0,停止发送数据。但等到接收方有足够缓存,发送了非零窗口大小的报文,但是这个报文中途丢失,那么发送方的发送窗口就一直为0导致死锁。
为此,TCP为每一个连接设有一个持续计时器(Persistence Timer):当TCP连接的一方收到对方的零窗口通知时就启动持续计时器。若持续计时器时间到期,就发送一个零窗口探测报文段(携有1字节的数据),那么收到这个报文段的一方就在确认这个探测报文段时给出了现在的窗口值。若窗口仍然是零,则收到这个报文段的一方就重新设置持续计时器;若窗口不是零,则死锁的僵局就可以打破了。

延迟ACK

如果TCP对每个数据包都发送一个ACK确认,那么只是一个单独的数据包为了发送一个ACK代价比较高,所以TCP会延迟一段时间,如果这段时间内有数据发送到对端,则捎带发送ACK,如果在延迟ACK定时器触发时候,发现ACK尚未发送,则立即单独发送;

延迟ACK好处:

  1. 避免糊涂窗口综合症。
  2. 发送数据的时候将ACK捎带发送,不必单独发送ACK。如果延迟时间内有多个数据段到达,那么允许协议栈发送一个ACK确认多个报文段。减少流量消耗。

糊涂窗口综合症:TCP接收方的缓存已满,而交互式的应用进程一次只从接收缓存中读取1字节(这样就使接收缓存空间仅腾出1字节),然后向发送方发送确认,并把窗口设置为1个字节(但发送的数据报为40字节的的话)。当发送方又发来1个字节的数据(发送方的IP数据报是41字节),接收方发回确认,仍然将窗口设置为1个字节。这样,网络的效率很低。要解决这个问题,可让接收方等待一段时间,使得或者接收缓存已有足够空间容纳一个最长的报文段或者等到接收方缓存已有一半的空闲空间。只要出现这两种情况,接收方就发回确认报文,并向发送方通知当前的窗口大小。此外,发送方也不要发送太小的报文段,而是把数据报积累成足够大的报文段,或达到接收方缓存的空间的一半大小。

拥塞控制

拥塞控制与流量控制的区别 :
拥塞控制是防止过多的数据注入到网络中,可以使网络中的路由器或链路不致过载,是一个全局性的过程。
流量控制是点对点通信量的控制,是一个端到端的问题,主要就是抑制发送端发送数据的速率,以便接收端来得及接收。

image.png

  1. 慢启动

发送方维持一个叫做拥塞窗口cwnd(congestion window)的状态变量。拥塞窗口的大小取决于网络的拥塞程度,并且动态地在变化。发送方让自己的发送窗口等于拥塞窗口,另外考虑到接受方的接收能力,发送窗口可能小于拥塞窗口。

慢开始算法的思路就是,不要一开始就发送大量的数据,先探测一下网络的拥塞程度,也就是说由小到大逐渐增加拥塞窗口的大小。

这里用报文段的个数作为拥塞窗口的大小举例说明慢开始算法,实际的拥塞窗口大小是以字节为单位的。如下图:

preview

从上图可以看到,一个传输轮次所经历的时间其实就是往返时间RTT,而且没经过一个传输轮次(transmission round),拥塞窗口cwnd就加倍。

为了防止cwnd增长过大引起网络拥塞,还需设置一个慢开始门限ssthresh状态变量。ssthresh的用法如下:当cwnd<ssthresh时,使用慢开始算法。

当cwnd>ssthresh时,改用拥塞避免算法。
当cwnd=ssthresh时,慢开始与拥塞避免算法任意

注意,这里的“慢”并不是指cwnd的增长速率慢,而是指在TCP开始发送报文段时先设置cwnd=1,然后逐渐增大,这当然比按照大的cwnd一下子把许多报文段突然注入到网络中要“慢得多”

  1. 拥塞避免算法

拥塞避免算法让拥塞窗口缓慢增长,即每经过一个往返时间RTT就把发送方的拥塞窗口cwnd加1,而不是加倍。这样拥塞窗口按线性规律缓慢增长。

无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有按时收到确认,虽然没有收到确认可能是其他原因的分组丢失,但是因为无法判定,所以都当做拥塞来处理),就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。

整个拥塞控制的流程如下图:

img

(1)拥塞窗口cwnd初始化为1个报文段,慢开始门限初始值为16
(2)执行慢开始算法,指数规律增长到第4轮,即cwnd=16=ssthresh,改为执行拥塞避免算法,拥塞窗口按线性规律增长
(3)假定cwnd=24时,网络出现超时(拥塞),则更新后的ssthresh=12,cwnd重新设置为1,并执行慢开始算法。当cwnd=12=ssthresh时,改为执行拥塞避免算法

关于 乘法减小(Multiplicative Decrease)和加法增大(Additive Increase):

“乘法减小”指的是无论是在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞,就把慢开始门限ssthresh设置为出现拥塞时的发送窗口大小的一半,并执行慢开始算法,所以当网络频繁出现拥塞时,ssthresh下降的很快,以大大减少注入到网络中的分组数。“加法增大”是指执行拥塞避免算法后,使拥塞窗口缓慢增大,以防止过早出现拥塞。常合起来成为AIMD算法。

注意:“拥塞避免”并非完全能够避免了阻塞,而是使网络比较不容易出现拥塞。

  1. 快重传算法

由于TCP采用的是累计确认机制,即当接收端收到比期望序号大的报文段时,便会重复发送最近一次确认的报文段的确认信号,我们称之为冗余ACK(duplicate ACK)。
如图所示,报文段1成功接收并被确认ACK 2,接收端的期待序号为2,当报文段2丢失,报文段3失序到来,与接收端的期望不匹配,接收端重复发送冗余ACK 2
这样,如果在超时重传定时器溢出之前,接收到连续的三个重复冗余ACK(其实是收到4个同样的ACK,第一个是正常的,后三个才是冗余的),发送端便知晓哪个报文段在传输过程中丢失了,于是重发该报文段,不需要等待超时重传定时器溢出,大大提高了效率。这便是快速重传机制。

为什么是三次冗余?
首先要明白一点,即使发送端是按序发送,由于TCP包是封装在IP包内,IP包在传输时乱序,意味着TCP包到达接收端也是乱序的,乱序的话也会造成接收端发送冗余ACK。那发送冗余ACK是由于乱序造成的还是包丢失造成的,这里便需要好好权衡一番,因为把3次冗余ACK作为判定丢失的准则其本身就是估计值
以上参看;TCP的快速重传机制

快重传要求接收方在收到一个失序的报文段后就立即发出重复确认(为的是使发送方及早知道有报文段没有到达对方,可提高网络吞吐量约20%)而不要等到自己发送数据时捎带确认。快重传算法规定,发送方只要一连收到三个重复确认就应当立即重传对方尚未收到的报文段,而不必继续等待设置的重传计时器时间到期。如下图:

img

ICMP会引起重新传递么?
答案是:不会,TCP会坚持用自己的定时器,但是TCP会保留下ICMP的错误并且通知用户。

  1. 快恢复算法

快重传配合使用的还有快恢复算法,有以下两个要点:

当发送方连续收到三个重复确认时,就执行“乘法减小”算法,把ssthresh门限减半(为了预防网络发生拥塞)。但是接下去并不执行慢开始算法,考虑到如果网络出现拥塞的话就不会收到好几个重复的确认,所以发送方现在认为网络可能没有出现拥塞。所以此时不执行慢开始算法,而是将cwnd设置为ssthresh减半后的值,然后执行拥塞避免算法,使cwnd缓慢增大。如下图:TCP Reno版本是目前使用最广泛的版本。

img

注意:在采用快恢复算法时,慢开始算法只是在TCP连接建立时和网络出现超时时才使用

转自-TCP流量控制、拥塞控制

快速了解TCP的流量控制与拥塞控制

5.7 TCP 性能优化

参考文章面试官:换人!他连 TCP 这几个参数都不懂

面试官:换人!他连 TCP 这几个参数都不懂

5.8 为什么 TCP 协议有粘包问题

TCP 协议粘包问题是因为应用层协议开发者的错误设计导致的,他们忽略了 TCP 协议数据传输的核心机制 — 基于字节流,其本身不包含消息、数据包等概念,所有数据的传输都是流式的,需要应用层协议自己设计消息的边界,即消息帧(Message Framing),我们重新回顾一下粘包问题出现的核心原因:

  1. TCP 协议是基于字节流的传输层协议,其中不存在消息和数据包的概念;
  2. 应用层协议没有使用基于长度或者基于终结符的消息边界,导致多个消息的粘连;

解决粘包问题的方法: 在应用层协议中,最常见的两种解决方案就是基于长度或者基于终结符(Delimiter),HTTP 协议的消息边界就是基于长度实现的:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 138

有时间要看看谢希仁的计算机网络这本书了