40 张图带你搞懂 TCP 和 UDP

前言

欢迎阅读「程序员cxuan」 的文章,从今日后,你就是个人读者了。git

个人 github bestJavaer 已经收录此文章,目录在程序员

github.com/crisxuan/be…github

但愿你能够给我一个 star 哦!算法

这一篇文章是计算机网络连载文章的第四篇,历史文章请阅读api

一不当心画了 24 张图剖析计网应用层协议!缓存

TCP/IP 基础知识安全

计算机网络基础知识总结服务器

下面开始本篇文章。微信

运输层位于应用层和网络层之间,是 OSI 分层体系中的第四层,同时也是网络体系结构的重要部分。运输层主要负责网络上的端到端通讯。markdown

运输层为运行在不一样主机上的应用程序之间的通讯起着相当重要的做用。下面咱们就来一块儿探讨一下关于运输层的协议部分

运输层概述

计算机网络的运输层很是相似于高速公路,高速公路负责把人或者物品从一端运送到另外一端,而计算机网络的运输层则负责把报文从一端运输到另外一端,这个端指的就是 端系统。在计算机网络中,任意一个能够交换信息的介质均可以称为端系统,好比手机、网络媒体、电脑、运营商等。

在运输层运输报文的过程当中,会遵照必定的协议规范,好比一次传输的数据限制、选择什么样的运输协议等。运输层实现了让两个互不相关的主机进行逻辑通讯的功能,看起来像是让两个主机相连同样。

运输层协议是在端系统中实现的,而不是在路由器中实现的。路由只是作识别地址并转发的功能。这就好比快递员送快递同样,固然是要由地址的接受人也就是 xxx 号楼 xxx 单元 xxx 室的这我的来判断了!

TCP 如何判断是哪一个端口的呢?

还记得数据包的结构吗,这里来回顾一下

数据包通过每层后,该层协议都会在数据包附上包首部,一个完整的包首部图如上所示。

在数据传输到运输层后,会为其附上 TCP 首部,首部包含着源端口号和目的端口号。

在发送端,运输层将从发送应用程序进程接收到的报文转化成运输层分组,分组在计算机网络中也称为 报文段(segment)。运输层通常会将报文段进行分割,分割成为较小的块,为每一块加上运输层首部并将其向目的地发送。

在发送过程当中,可选的运输层协议(也就是交通工具) 主要有 TCPUDP ,关于这两种运输协议的选择及其特性也是咱们着重探讨的重点。

TCP 和 UDP 前置知识

在 TCP/IP 协议中可以实现传输层功能的,最具表明性的就是 TCP 和 UDP。提起 TCP 和 UDP ,就得先从这两个协议的定义提及。

TCP 叫作传输控制协议(TCP,Transmission Control Protocol),经过名称能够大体知道 TCP 协议有控制传输的功能,主要体如今其可控,可控就表示着可靠,确实是这样的,TCP 为应用层提供了一种可靠的、面向链接的服务,它可以将分组可靠的传输到服务端。

UDP 叫作 用户数据报协议(UDP,User Datagram Protocol),经过名称能够知道 UDP 把重点放在了数据报上,它为应用层提供了一种无需创建链接就能够直接发送数据报的方法。

怎么计算机网络中的术语对一个数据的描述这么多啊?

在计算机网络中,在不一样层之间会有不一样的描述。咱们上面提到会将运输层的分组称为报文段,除此以外,还会将 TCP 中的分组也称为报文段,然而将 UDP 的分组称为数据报,同时也将网络层的分组称为数据报

可是为了统一,通常在计算机网络中咱们统一称 TCP 和 UDP 的报文为 报文段,这个就至关因而约定,到底如何称呼不用过多纠结啦。

套接字

在 TCP 或者 UDP 发送具体的报文信息前,须要先通过一扇 ,这个门就是套接字(socket),套接字向上链接着应用层,向下链接着网络层。在操做系统中,操做系统分别为应用和硬件提供了接口(Application Programming Interface)。而在计算机网络中,套接字一样是一种接口,它也是有接口 API 的。

使用 TCP 或 UDP 通讯时,会普遍用到套接字的 API,使用这套 API 设置 IP 地址、端口号,实现数据的发送和接收。

如今咱们知道了, Socket 和 TCP/IP 没有必然联系,Socket 的出现只是方便了 TCP/IP 的使用,如何方便使用呢?你能够直接使用下面 Socket API 的这些方法。

方法 描述
create() 建立一个 socket
bind() 套接字标识,通常用于绑定端口号
listen() 准备接收链接
connect() 准备充当发送者
accept() 准备做为接收者
write() 发送数据
read() 接收数据
close() 关闭链接

套接字类型

套接字的主要类型有三种,下面咱们分别介绍一下

  • 数据报套接字(Datagram sockets):数据报套接字提供一种无链接的服务,并且并不能保证数据传输的可靠性。数据有可能在传输过程当中丢失或出现数据重复,且没法保证顺序地接收到数据。数据报套接字使用UDP( User DatagramProtocol)协议进行数据的传输。因为数据报套接字不能保证数据传输的可靠性,对于有可能出现的数据丢失状况,须要在程序中作相应的处理。
  • 流套接字(Stream sockets):流套接字用于提供面向链接、可靠的数据传输服务。可以保证数据的可靠性、顺序性。流套接字之因此可以实现可靠的数据服务,缘由在于其使用了传输控制协议,即 TCP(The Transmission Control Protocol)协议
  • 原始套接字(Raw sockets): 原始套接字容许直接发送和接收 IP 数据包,而无需任何特定于协议的传输层格式,原始套接字能够读写内核没有处理过的 IP 数据包。

套接字处理过程

在计算机网络中,要想实现通讯,必须至少须要两个端系统,至少须要一对两个套接字才行。下面是套接字的通讯过程。

  1. socket 中的 API 用于建立通讯链路中的端点,建立完成后,会返回描述该套接字的套接字描述符

就像使用文件描述符来访问文件同样,套接字描述符用来访问套接字。

  1. 当应用程序具备套接字描述符后,它能够将惟一的名称绑定在套接字上,服务器必须绑定一个名称才能在网络中访问
  2. 在为服务端分配了 socket 而且将名称使用 bind 绑定到套接字上后,将会调用 listen api。listen 表示客户端愿意等待链接的意愿,listen 必须在 accept api 以前调用。
  3. 客户端应用程序在流套接字(基于 TCP)上调用 connect 发起与服务器的链接请求。
  4. 服务器应用程序使用acceptAPI 接受客户端链接请求,服务器必须先成功调用 bind 和 listen 后,再调用 accept api。
  5. 在流套接字之间创建链接后,客户端和服务器就能够发起 read/write api 调用了。
  6. 当服务器或客户端要中止操做时,就会调用 close API 释放套接字获取的全部系统资源。

虽然套接字 API 位于应用程序层和传输层之间的通讯模型中,可是套接字 API 不属于通讯模型。套接字 API 容许应用程序与传输层和网络层进行交互。

在往下继续聊以前,咱们先播放一个小插曲,简单聊一聊 IP。

聊聊 IP

IPInternet Protocol(网际互连协议)的缩写,是 TCP/IP 体系中的网络层协议。设计 IP 的初衷主要想解决两类问题

  • 提升网络扩展性:实现大规模网络互联
  • 对应用层和链路层进行解藕,让两者独立发展。

IP 是整个 TCP/IP 协议族的核心,也是构成互联网的基础。为了实现大规模网络的互通互联,IP 更加注重适应性、简洁性和可操做性,并在可靠性作了必定的牺牲。IP 不保证分组的交付时限和可靠性,所传送分组有可能出现丢失、重复、延迟或乱序等问题。

咱们知道,TCP 协议的下一层就是 IP 协议层,既然 IP 不可靠,那么如何保证数据可以准确无误地到达呢?

这就涉及到 TCP 传输机制的问题了,咱们后面聊到 TCP 的时候再说。

端口号

在聊端口号前,先来聊一聊文件描述以及 socket 和端口号的关系

为了方便资源的使用,提升机器的性能、利用率和稳定性等等缘由,咱们的计算机都有一层软件叫作操做系统,它用于帮咱们管理计算机可使用的资源,当咱们的程序要使用一个资源的时候,能够向操做系统申请,再由操做系统为咱们的程序分配和管理资源。一般当咱们要访问一个内核设备或文件时,程序能够调用系统函数,系统就会为咱们打开设备或文件,而后返回一个文件描述符fd(或称为ID,是一个整数),咱们要访问该设备或文件,只能经过该文件描述符。能够认为该编号对应着打开的文件或设备。

而当咱们的程序要使用网络时,要使用到对应的操做系统内核的操做和网卡设备,因此咱们能够向操做系统申请,而后系统会为咱们建立一个套接字 Socket,并返回这个 Socket 的ID,之后咱们的程序要使用网络资源,只要向这个 Socket 的编号 ID 操做便可。而咱们的每个网络通讯的进程至少对应着一个 Socket。向 Socket 的 ID 中写数据,至关于向网络发送数据,向 Socket 中读数据,至关于接收数据。并且这些套接字都有惟一标识符——文件描述符 fd。

端口号是 16 位的非负整数,它的范围是 0 - 65535 之间,这个范围会分为三种不一样的端口号段,由 Internet 号码分配机构 IANA 进行分配

  • 周知/标准端口号,它的范围是 0 - 1023
  • 注册端口号,范围是 1024 - 49151
  • 私有端口号,范围是 49152 - 6553

一台计算机上能够运行多个应用程序,当一个报文段到达主机后,应该传输给哪一个应用程序呢?你怎么知道这个报文段就是传递给 HTTP 服务器而不是 SSH 服务器的呢?

是凭借端口号吗?当报文到达服务器时,是端口号来区分不一样应用程序的,因此应该借助端口号来区分。

举个例子反驳一下 cxuan,假如到达服务器的两条数据都是由 80 端口发出的你该如何区分呢?或者说到达服务器的两条数据端口同样,协议不一样,该如何区分呢?

因此仅凭端口号来肯定某一条报文显然是不够的。

互联网上通常使用 源 IP 地址、目标 IP 地址、源端口号、目标端口号 来进行区分。若是其中的某一项不一样,就被认为是不一样的报文段。这些也是多路分解和多路复用 的基础。

肯定端口号

在实际通讯以前,须要先肯定一下端口号,肯定端口号的方法分为两种:

  • 标准既定的端口号

标准既定的端口号是静态分配的,每一个程序都会有本身的端口号,每一个端口号都有不一样的用途。端口号是一个 16 比特的数,其大小在 0 - 65535 之间,0 - 1023 范围内的端口号都是动态分配的既定端口号,例如 HTTP 使用 80 端口来标识,FTP 使用 21 端口来标识,SSH 使用 22 来标识。这类端口号有一个特殊的名字,叫作 周知端口号(Well-Known Port Number)

  • 时序分配的端口号

第二种分配端口号的方式是一种动态分配法,在这种方法下,客户端应用程序能够彻底不用本身设置端口号,凭借操做系统进行分配,操做系统能够为每一个应用程序分配互不冲突的端口号。这种动态分配端口号的机制即便是同一个客户端发起的 TCP 链接,也能识别不一样的链接。

多路复用和多路分解

咱们上面聊到了在主机上的每一个套接字都会分配一个端口号,当报文段到达主机时,运输层会检查报文段中的目的端口号,并将其定向到相应的套接字,而后报文段中的数据经过套接字进入其所链接的进程。下面咱们来聊一下什么是多路复用和多路分解的概念。

多路复用和多路分解分为两种,即无链接的多路复用(多路分解)和面向链接的多路复用(多路分解)

无链接的多路复用和多路分解

开发人员会编写代码肯定端口号是周知端口号仍是时序分配的端口号。假如主机 A 中的一个 10637 端口要向主机 B 中的 45438 端口发送数据,运输层采用的是 UDP 协议,数据在应用层产生后,会在运输层中加工处理,而后在网络层将数据封装获得 IP 数据报,IP 数据包经过链路层尽力而为的交付给主机 B,而后主机 B 会检查报文段中的端口号判断是哪一个套接字的,这一系列的过程以下所示

UDP 套接字就是一个二元组,二元组包含目的 IP 地址和目的端口号。

因此,若是两个 UDP 报文段有不一样的源 IP 地址和/或相同的源端口号,可是具备相同的目的 IP 地址和目的端口号,那么这两个报文会经过套接字定位到相同的目的进程。

这里思考一个问题,主机 A 给主机 B 发送一个消息,为何还须要知道源端口号呢?好比我给妹子表达出我对你有点意思的信息,妹子还须要知道这个信息是从个人哪一个器官发出的吗?知道是我这我的对你有点意思不就完了?其实是须要的,由于妹子若是要表达出她对你也有点意思,她是否是可能会亲你一口,那她得知道往哪亲吧?

这就是,在 A 到 B 的报文段中,源端口号会做为 返回地址 的一部分,即当 B 须要回发一个报文段给 A 时,B 须要从 A 到 B 中的源端口号取值,以下图所示

面向链接的多路复用与多路分解

若是说无链接的多路复用和多路分解指的是 UDP 的话,那么面向链接的多路复用与多路分解指的是 TCP 了,TCP 和 UDP 在报文结构上的差异是,UDP 是一个二元组而 TCP 是一个四元组,即源 IP 地址、目标 IP 地址、源端口号、目标端口号 ,这个咱们上面也提到了。当一个 TCP 报文段从网络到达一台主机时,这个主机会根据这四个值拆解到对应的套接字上。

上图显示了面向链接的多路复用和多路分解的过程,图中主机 C 向主机 B 发起了两个 HTTP 请求,主机 A 向主机 C 发起了一个 HTTP 请求,主机 A、B、C 都有本身惟一的 IP 地址,当主机 C 发出 HTTP 请求后,主机 B 可以分解这两个 HTTP 链接,由于主机 C 发出请求的两个源端口号不一样,因此对于主机 B 来讲,这是两条请求,主机 B 可以进行分解。对于主机 A 和主机 C 来讲,这两个主机有不一样的 IP 地址,因此对于主机 B 来讲,也可以进行分解。

UDP

终于,咱们开始了对 UDP 协议的探讨,淦起!

UDP 的全称是 用户数据报协议(UDP,User Datagram Protocol),UDP 为应用程序提供了一种无需创建链接就能够发送封装的 IP 数据包的方法。若是应用程序开发人员选择的是 UDP 而不是 TCP 的话,那么该应用程序至关于就是和 IP 直接打交道的。

从应用程序传递过来的数据,会附加上多路复用/多路分解的源和目的端口号字段,以及其余字段,而后将造成的报文传递给网络层,网络层将运输层报文段封装到 IP 数据报中,而后尽力而为的交付给目标主机。最关键的一点就是,使用 UDP 协议在将数据报传递给目标主机时,发送方和接收方的运输层实体间是没有握手的。正由于如此,UDP 被称为是无链接的协议。

UDP 特色

UDP 协议通常做为流媒体应用、语音交流、视频会议所使用的传输层协议,咱们你们都知道的 DNS 协议底层也使用了 UDP 协议,这些应用或协议之因此选择 UDP 主要是由于如下这几点

  • 速度快,采用 UDP 协议时,只要应用进程将数据传给 UDP,UDP 就会将此数据打包进 UDP 报文段并马上传递给网络层,而后 TCP 有拥塞控制的功能,它会在发送前判断互联网的拥堵状况,若是互联网极度阻塞,那么就会抑制 TCP 的发送方。使用 UDP 的目的就是但愿实时性。
  • 无须创建链接,TCP 在数据传输以前须要通过三次握手的操做,而 UDP 则无须任何准备便可进行数据传输。所以 UDP 没有创建链接的时延。若是使用 TCP 和 UDP 来比喻开发人员:TCP 就是那种凡事都要设计好,没设计不会进行开发的工程师,须要把一切因素考虑在内后再开干!因此很是靠谱;而 UDP 就是那种上来直接干干干,接到项目需求立刻就开干,也无论设计,也无论技术选型,就是干,这种开发人员很是不靠谱,可是适合快速迭代开发,由于能够立刻上手!
  • 无链接状态,TCP 须要在端系统中维护链接状态,链接状态包括接收和发送缓存、拥塞控制参数以及序号和确认号的参数,在 UDP 中没有这些参数,也没有发送缓存和接受缓存。所以,某些专门用于某种特定应用的服务器当应用程序运行在 UDP 上,通常能支持更多的活跃用户
  • 分组首部开销小,每一个 TCP 报文段都有 20 字节的首部开销,而 UDP 仅仅只有 8 字节的开销。

这里须要注意一点,并非全部使用 UDP 协议的应用层都是不可靠的,应用程序能够本身实现可靠的数据传输,经过增长确认和重传机制。因此使用 UDP 协议最大的特色就是速度快。

UDP 报文结构

下面来一块儿看一下 UDP 的报文结构,每一个 UDP 报文分为 UDP 报头和 UDP 数据区两部分。报头由 4 个 16 位长(2 字节)字段组成,分别说明该报文的源端口、目的端口、报文长度和校验值。

  • 源端口号(Source Port) :这个字段占据 UDP 报文头的前 16 位,一般包含发送数据报的应用程序所使用的 UDP 端口。接收端的应用程序利用这个字段的值做为发送响应的目的地址。这个字段是可选项,有时不会设置源端口号。没有源端口号就默认为 0 ,一般用于不须要返回消息的通讯中。
  • 目标端口号(Destination Port): 表示接收端端口,字段长为 16 位
  • 长度(Length): 该字段占据 16 位,表示 UDP 数据报长度,包含 UDP 报文头和 UDP 数据长度。由于 UDP 报文头长度是 8 个字节,因此这个值最小为 8,最大长度为 65535 字节。
  • 校验和(Checksum):UDP 使用校验和来保证数据安全性,UDP 的校验和也提供了差错检测功能,差错检测用于校验报文段从源到目标主机的过程当中,数据的完整性是否发生了改变。发送方的 UDP 对报文段中的 16 比特字的和进行反码运算,求和时遇到的位溢出都会被忽略,好比下面这个例子,三个 16 比特的数字进行相加

​ 这些 16 比特的前两个和是

​ 而后再将上面的结果和第三个 16 比特的数进行相加

最后一次相加的位会进行溢出,溢出位 1 要被舍弃,而后进行反码运算,反码运算就是将全部的 1 变为 0 ,0 变为 1。所以 1000 0100 1001 0101 的反码就是 0111 1011 0110 1010,这就是校验和,若是在接收方,数据没有出现差错,那么所有的 4 个 16 比特的数值进行运算,同时也包括校验和,若是最后结果的值不是 1111 1111 1111 1111 的话,那么就表示传输过程当中的数据出现了差错。

下面来想一个问题,为何 UDP 会提供差错检测的功能?

这实际上是一种 端到端 的设计原则,这个原则说的是要让传输中各类错误发生的几率下降到一个能够接受的水平

文件从主机A传到主机B,也就是说AB主机要通讯,须要通过三个环节:首先是主机A从磁盘上读取文件并将数据分组成一个个数据包packet,,而后数据包经过链接主机A和主机B的网络传输到主机B,最后是主机B收到数据包并将数据包写入磁盘。在这个看似简单其实很复杂的过程当中可能会因为某些缘由而影响正常通讯。好比:磁盘上文件读写错误、缓冲溢出、内存出错、网络拥挤等等这些因素都有可能致使数据包的出错或者丢失,因而可知用于通讯的网络是不可靠的。

因为实现通讯只要通过上述三个环节,那么咱们就想是否在其中某个环节上增长一个检错纠错机制来用于对信息进行把关呢?

网络层确定不能作这件事,由于网络层的最主要目的是增大数据传输的速率,网络层不须要考虑数据的完整性,数据的完整性和正确性交给端系统去检测就好了,所以在数据传输中,对于网络层只能要求其提供尽量好的数据传输服务,而不可能寄但愿于网络层提供数据完整性的服务。

UDP 不可靠的缘由是它虽然提供差错检测的功能,可是对于差错没有恢复能力更不会有重传机制

TCP

UDP 是一种没有复杂的控制,提供无链接通讯服务的一种协议,换句话说,它将部分控制部分交给应用程序去处理,本身只提供做为传输层协议最基本的功能。

而与 UDP 不一样的是,一样做为传输层协议,TCP 协议要比 UDP 的功能多不少。

TCP 的全称是 Transmission Control Protocol,它被称为是一种面向链接(connection-oriented) 的协议,这是由于一个应用程序开始向另外一个应用程序发送数据以前,这两个进程必须先进行握手,握手是一个逻辑链接,并非两个主机之间进行真实的握手。

这个链接是指各类设备、线路或者网络中进行通讯的两个应用程序为了相互传递消息而专有的、虚拟的通讯链路,也叫作虚拟电路。

一旦主机 A 和主机 B 创建了链接,那么进行通讯的应用程序只使用这个虚拟的通讯线路发送和接收数据就能够保证数据的传输,TCP 协议负责控制链接的创建、断开、保持等工做。

TCP 链接是全双工服务(full-duplex service) 的,全双工是什么意思?全双工指的是主机 A 与另一个主机 B 存在一条 TCP 链接,那么应用程数据就能够从主机 B 流向主机 A 的同时,也从主机 A 流向主机 B。

TCP 只能进行 点对点(point-to-point) 链接,那么所谓的多播,即一个主机对多个接收方发送消息的状况是不存在的,TCP 链接只能链接两个一对主机。

TCP 的链接创建须要通过三次握手,这个咱们下面再说。一旦 TCP 链接创建后,主机之间就能够相互发送数据了,客户进程经过套接字传送数据流。数据一旦经过套接字后,它就由客户中运行的 TCP 协议所控制。

TCP 会将数据临时存储到链接的发送缓存(send buffer) 中,这个 send buffer 是三次握手之间设置的缓存之一,而后 TCP 在合适的时间将发送缓存中的数据发送到目标主机的接收缓存中,实际上,每一端都会有发送缓存和接收缓存,以下所示

主机之间的发送是以 报文段(segment) 进行的,那么什么是 Segement 呢?

TCP 会将要传输的数据流分为多个块(chunk),而后向每一个 chunk 中添加 TCP 标头,这样就造成了一个 TCP 段也就是报文段。每个报文段能够传输的长度是有限的,不能超过最大数据长度(Maximum Segment Size),俗称 MSS。在报文段向下传输的过程当中,会通过链路层,链路层有一个 Maximum Transmission Unit ,最大传输单元 MTU, 即数据链路层上所能经过最大数据包的大小,最大传输单元一般与通讯接口有关。

那么 MSS 和 MTU 有啥关系呢?

由于计算机网络是分层考虑的,这个很重要,不一样层的称呼不同,对于传输层来讲,称为报文段而对网络层来讲就叫作 IP 数据包,因此,MTU 能够认为是网络层可以传输的最大 IP 数据包,而 MSS(Maximum segment size)能够认为是传输层的概念,也就是 TCP 数据包每次可以传输的最大量

TCP 报文段结构

在简单聊了聊 TCP 链接后,下面咱们就来聊一下 TCP 的报文段结构,以下图所示

TCP 报文段结构相比 UDP 报文结构多了不少内容。可是前两个 32 比特的字段是同样的。它们是 源端口号目标端口号,咱们知道,这两个字段是用于多路复用和多路分解的。另外,和 UDP 同样,TCP 也包含校验和(checksum field) ,除此以外,TCP 报文段首部还有下面这些

  • 32 比特的序号字段(sequence number field) 和 32 比特的确认号字段(acknowledgment number field) 。这些字段被 TCP 发送方和接收方用来实现可靠的数据传输。

  • 4 比特的首部字段长度字段(header length field),这个字段指示了以 32 比特的字为单位的 TCP 首部长度。TCP 首部的长度是可变的,可是一般状况下,选项字段为空,因此 TCP 首部字段的长度是 20 字节。

  • 16 比特的 接受窗口字段(receive window field) ,这个字段用于流量控制。它用于指示接收方可以/愿意接受的字节数量

  • 可变的选项字段(options field),这个字段用于发送方和接收方协商最大报文长度,也就是 MSS 时使用

  • 6 比特的 标志字段(flag field)ACK 标志用于指示确认字段中的值是有效的,这个报文段包括一个对已被成功接收报文段的确认;RSTSYNFIN 标志用于链接的创建和关闭;CWRECE 用于拥塞控制;PSH 标志用于表示马上将数据交给上层处理;URG 标志用来表示数据中存在须要被上层处理的 紧急 数据。紧急数据最后一个字节由 16 比特的紧急数据指针字段(urgeent data pointer field) 指出。通常状况下,PSH 和 URG 并无使用。

TCP 的各类功能和特色都是经过 TCP 报文结构来体现的,在聊完 TCP 报文结构以后,咱们下面就来聊一下 TCP 有哪些功能及其特色了。

序号、确认号实现传输可靠性

TCP 报文段首部中两个最重要的字段就是 序号确认号,这两个字段是 TCP 实现可靠性的基础,那么你确定好奇如何实现可靠性呢?要了解这一点,首先咱们得先知道这两个字段里面存了哪些内容吧?

一个报文段的序号就是数据流的字节编号 。由于 TCP 会把数据流分割成为一段一段的字节流,由于字节流自己是有序的,因此每一段的字节编号就是标示是哪一段的字节流。好比,主机 A 要给主机 B 发送一条数据。数据通过应用层产生后会有一串数据流,数据流会通过 TCP 分割,分割的依据就是 MSS,假设数据是 10000 字节,MSS 是 2000 字节,那么 TCP 就会把数据拆分红 0 - 1999 , 2000 - 3999 的段,依次类推。

因此,第一个数据 0 - 1999 的首字节编号就是 0 ,2000 - 3999 的首字节编号就是 2000 。。。。。。

而后,每一个序号都会被填入 TCP 报文段首部的序号字段中。

至于确认号的话,会比序号要稍微麻烦一些。这里咱们先拓展下几种通讯模型。

  • 单工通讯:单工数据传输只支持数据在一个方向上传输;在同一时间只有一方能接受或发送信息,不能实现双向通讯,好比广播、电视等。
  • 双工通讯是一种点对点系统,由两个或者多个在两个方向上相互通讯的链接方或者设备组成。双工通讯模型有两种:全双工(FDX)和半双工(HDX)
    • 全双工:在全双工系统中,链接双方能够相互通讯,一个最多见的例子就是电话通讯。全双工通讯是两个单工通讯方式的结合,它要求发送设备和接收设备都有独立的接收和发送能力。
    • 半双工:在半双工系统中,链接双方能够彼此通讯,但不能同时通讯,好比对讲机,只有把按钮按住的人才可以讲话,只有一我的讲完话后另一我的才能讲话。

单工、半双工、全双工通讯以下图所示

TCP 是一种全双工的通讯协议,所以主机 A 在向主机 B 发送消息的过程当中,也在接受来自主机 B 的数据。主机 A 填充进报文段的确认号是指望从主机 B 收到的下一字节的序号。稍微有点绕,咱们来举个例子看一下。好比主机 A 收到了来自主机 B 发送的编号为 0 - 999 字节的报文段,这个报文段会写入序号中,随后主机 A 指望可以从主机 B 收到 1000 - 剩下的报文段,所以,主机 A 发送到主机 B 的报文段中,它的确认号就是 1000 。

累积确认

这里再举出一个例子,好比主机 A 在发送 0 - 999 报文段后,指望可以接受到 1000 以后的报文段,可是主机 B 却给主机 A 发送了一个 1500 以后的报文段,那么主机 A 是否还会继续进行等待呢?

答案显然是会的,由于 TCP 只会确认流中至第一个丢失字节为止的字节,由于 1500 虽然属于 1000 以后的字节,可是主机 B 没有给主机 A 发送 1000 - 1499 之间的字节,因此主机 A 会继续等待。

在了解完序号和确认号以后,咱们下面来聊一下 TCP 的发送过程。下面是一个正常的发送过程

TCP 经过确定的确认应答(ACK) 来实现可靠的数据传输,当主机 A将数据发出以后会等待主机 B 的响应。若是有确认应答(ACK),说明数据已经成功到达对端。反之,则数据极可能会丢失。

以下图所示,若是在必定时间内主机 A 没有等到确认应答,则认为主机 B 发送的报文段已经丢失,并进行重发。

主机 A 给主机 B 的响应可能因为网络抖动等缘由没法到达,那么在通过特定的时间间隔后,主机 A 将从新发送报文段。

主机 A 没有收到主机 B 的响应还多是由于主机 B 在发送给主机 A 的过程当中丢失。

如上图所示,由主机 B 返回的确认应答,因为网络拥堵等缘由在传送的过程当中丢失,并无到达主机 A。主机 A 会等待一段时间,若是在这段时间内主机 A 仍没有等到主机 B 的响应,那么主机 A 会从新发送报文段。

那么如今就存在一个问题,若是主机 A 给主机 B 发送了一个报文段后,主机 B 接受到报文段发送响应,此刻因为网络缘由,这个报文段并未到达,等到一段时间后主机 A 从新发送报文段,而后此时主机 B 发送的响应在主机 A 第二次发送后失序到达主机 A,那么主机 A 应该如何处理呢?

TCP RFC 并未为此作任何规定,也就是说,咱们能够本身决定如何处理失序到达的报文段。通常处理方式有两种

  • 接收方马上丢弃失序的报文段
  • 接收方接受时许到达的报文段,并等待后续的报文段

通常来讲一般采起的作法是第二种。

传输控制

利用窗口控制提升速度

前面咱们介绍了 TCP 是以数据段的形式进行发送,若是通过一段时间内主机 A 等不到主机 B 的响应,主机 A 就会从新发送报文段,接受到主机 B 的响应,再会继续发送后面的报文段,咱们如今看到,这一问一答的形式还存在许多条件,好比响应未收到、等待响应等,那么对崇尚性能的互联网来讲,这种形式的性能应该不会很高。

那么如何提高性能呢?

为了解决这个问题,TCP 引入了 窗口 这个概念,即便在往返时间较长、频次不少的状况下,它也能控制网络性能的降低,听起来很牛批,那它是如何实现的呢?

以下图所示

咱们以前每次请求发送都是以报文段的形式进行的,引入窗口后,每次请求均可以发送多个报文段,也就是说一个窗口能够发送多个报文段。窗口大小就是指无需等待确认应答就能够继续发送报文段的最大值。

在这个窗口机制中,大量使用了 缓冲区 ,经过对多个段同时进行确认应答的功能。

以下图所示,发送报文段中高亮部分便是咱们提到的窗口,在窗口内,便是没有收到确认应答也能够把请求发送出去。不过,在整个窗口的确认应答没有到达以前,若是部分报文段丢失,那么主机 A 将仍会重传。为此,主机 A 须要设置缓存来保留这些须要重传的报文段,直到收到他们的确认应答。

在滑动窗口之外的部分是还没有发送的报文段和已经接受到的报文段,若是报文段已经收到确认则不可进行重发,此时报文段就能够从缓冲区中清除。

在收到确认的状况下,会将窗口滑动到确认应答中确认号的位置,如上图所示,这样能够顺序的将多个段同时发送,用以提升通讯性能,这种窗口也叫作 滑动窗口(Sliding window)

窗口控制和重发

报文段的发送和接收,必然伴随着报文段的丢失和重发,窗口也是一样如此,若是在窗口中报文段发送过程当中出现丢失怎么办?

首先咱们先考虑确认应答没有返回的状况。在这种状况下,主机 A 发送的报文段到达主机 B,是不须要再进行重发的。这和单个报文段的发送不同,若是发送单个报文段,即便确认应答没有返回,也要进行重发

窗口在必定程度上比较大时,即便有少部分确认应答的丢失,也不会从新发送报文段。

咱们知道,若是在某个状况下因为发送的报文段丢失,致使接受主机未收到请求,或者主机返回的响应未到达客户端的话,会通过一段时间重传报文。那么在使用窗口的状况下,报文段丢失会怎么样呢?

以下图所示,报文段 0 - 999 丢失后,可是主机 A 并不会等待,主机 A 会继续发送余下的报文段,主机 B 发送的确认应答却一直是 1000,同一个确认号的应答报文会被持续不断的返回,若是发送端主机在连续 3 次收到同一个确认应答后,就会将其所对应的数据重发,这种机制要比以前提到的超时重发更加高效,这种机制也被称为 高速重发控制。这种重发的确认应答也被称为 冗余 ACK(响应)

主机 B 在没有接收到本身指望序列号的报文段时,会对以前收到的数据进行确认应答。发送端则一旦收到某个确认应答后,又连续三次收到一样的确认应答,那么就会认为报文段已经丢失。须要进行重发。使用这种机制能够提供更为快速的重发服务

流量控制

前面聊的是传输控制,下面 cxuan 再和你聊一下 流量控制。咱们知道,在每一个 TCP 链接的一侧主机都会有一个 socket 缓冲区,缓冲区会为每一个链接设置接收缓存和发送缓存,当 TCP 创建链接后,从应用程序产生的数据就会到达接收方的接收缓冲区中,接收方的应用程序并不必定会立刻读区缓冲区的数据,它须要等待操做系统分配时间片。若是此时发送方的应用程序产生数据过快,而接收方读取接受缓冲区的数据相对较慢的话,那么接收方中缓冲区的数据将会溢出

可是还好,TCP 有 流量控制服务(flow-control service) 用于消除缓冲区溢出的状况。流量控制是一个速度匹配服务,即发送方的发送速率与接受方应用程序的读取速率相匹配。

TCP 经过使用一个 接收窗口(receive window) 的变量来提供流量控制。接受窗口会给发送方一个指示到底还有多少可用的缓存空间。发送端会根据接收端的实际接受能力来控制发送的数据量。

接收端主机向发送端主机通知本身能够接收数据的大小,发送端会发送不超过这个限度的数据,这个大小限度就是窗口大小,还记得 TCP 的首部么,有一个接收窗口,咱们上面聊的时候说这个字段用于流量控制。它用于指示接收方可以/愿意接受的字节数量。

那么只知道这个字段用于流量控制,那么如何控制呢?

发送端主机会按期发送一个窗口探测包,这个包用于探测接收端主机是否还可以接受数据,当接收端的缓冲区一旦面临数据溢出的风险时,窗口大小的值也随之被设置为一个更小的值通知发送端,从而控制数据发送量。

下面是一个流量控制示意图

发送端主机根据接收端主机的窗口大小进行流量控制。由此也能够防止发送端主机一次发送过大数据致使接收端主机没法处理。

如上图所示,当主机 B 收到报文段 2000 - 2999 以后缓冲区已满,不得不暂时中止接收数据。而后主机 A 发送窗口探测包,窗口探测包很是小仅仅一个字节。而后主机 B 更新缓冲区接收窗口大小并发送窗口更新通知给主机 A,而后主机 A 再继续发送报文段。

在上面的发送过程当中,窗口更新通知可能会丢失,一旦丢失发送端就不会发送数据,因此窗口探测包会随机发送,以免这种状况发生。

链接管理

在继续介绍下面有意思的特性以前,咱们先来把关注点放在 TCP 的链接管理上,由于没有 TCP 链接,也就没有后续的一系列 TCP 特性什么事儿了。假设运行在一台主机上的进程想要和另外一台主机上的进程创建一条 TCP 链接,那么客户中的 TCP 会使用下面这些步骤与服务器中的 TCP 创建链接。

  • 首先,客户端首先向服务器发送一个特殊的 TCP 报文段。这个报文段首部不包含应用层数据,可是在报文段的首部中有一个 SYN 标志位 被置为 1。所以,这个特殊的报文段也能够叫作 SYN 报文段。而后,客户端随机选择一个初始序列号(client_isn) ,并将此数字放入初始 TCP SYN 段的序列号字段中,SYN 段又被封装在 IP 数据段中发送给服务器。

  • 一旦包含 IP 数据段到达服务器后,服务端会从 IP 数据段中提取 TCP SYN 段,将 TCP 缓冲区和变量分配给链接,而后给客户端发送一个链接所容许的报文段。这个链接所容许的报文段也不包括任何应用层数据。然而,它却包含了三个很是重要的信息。

    这些缓冲区和变量的分配使 TCP 容易受到称为 SYN 泛洪的拒绝服务攻击。

    • 首先,SYN 比特被置为 1 。
    • 而后,TCP 报文段的首部确认号被设置为 client_isn + 1
    • 最后,服务器选择本身的初始序号(server_isn),并将其放置到 TCP 报文段首部的序号字段中。

    若是用大白话解释下就是,我收到了你发起创建链接的 SYN 报文段,这个报文段具备首部字段 client_isn。我赞成创建该链接,我本身的初始序号是 server_isn。这个容许链接的报文段被称为 SYNACK 报文段

  • 第三步,在收到 SYNACK 报文段后,客户端也要为该链接分配缓冲区和变量。客户端主机向服务器发送另一个报文段,最后一个报文段对服务器发送的响应报文作了确认,确认的标准是客户端发送的数据段中确认号为 server_isn + 1,由于链接已经创建,因此 SYN 比特被置为 0 。以上就是 TCP 创建链接的三次数据段发送过程,也被称为 三次握手

一旦完成这三个步骤,客户和服务器主机就能够相互发送报文段了,在之后的每个报文段中,SYN 比特都被置为 0 ,整个过程描述以下图所示

在客户端主机和服务端主机创建链接后,参与一条 TCP 链接的两个进程中的任何一个都能终止 TCP 链接。链接结束后,主机中的缓存和变量将会被释放。假设客户端主机想要终止 TCP 链接,它会经历以下过程

客户应用进程发出一个关闭命令,客户 TCP 向服务器进程发送一个特殊的 TCP 报文段,这个特殊的报文段的首部标志 FIN 被设置为 1 。当服务器收到这个报文段后,就会向发送方发送一个确认报文段。而后,服务器发送它本身的终止报文段,FIN 位被设置为 1 。客户端对这个终止报文段进行确认。此时,在两台主机上用于该链接的全部资源都被释放了,以下图所示

在一个 TCP 链接的生命周期内,运行在每台主机中的 TCP 协议都会在各类 TCP 状态(TCP State) 之间进行变化,TCP 的状态主要有 LISTEN、SYN-SEND、SYN-RECEIVED、ESTABLISHED、FIN-WAIT-一、FIN-WAIT-二、CLOSE-WAIT、CLOSING、LAST-ACK、TIME-WAIT 和 CLOSED 。这些状态的解释以下

  • LISTEN: 表示等待任何来自远程 TCP 和端口的链接请求。
  • SYN-SEND: 表示发送链接请求后等待匹配的链接请求。
  • SYN-RECEIVED: 表示已接收并发送链接请求后等待链接确认,也就是 TCP 三次握手中第二步后服务端的状态
  • ESTABLISHED: 表示已经链接已经创建,能够将应用数据发送给其余主机

上面这四种状态是 TCP 三次握手所涉及的。

  • FIN-WAIT-1: 表示等待来自远程 TCP 的链接终止请求,或者等待先前发送的链接终止请求的确认。

  • FIN-WAIT-2: 表示等待来自远程 TCP 的链接终止请求。

  • CLOSE-WAIT: 表示等待本地用户的链接终止请求。

  • CLOSING: 表示等待来自远程 TCP 的链接终止请求确认。

  • LAST-ACK: 表示等待先前发送给远程 TCP 的链接终止请求的确认(包括对它的链接终止请求的确认)。

  • TIME-WAIT: 表示等待足够的时间以确保远程 TCP 收到其链接终止请求的确认。

  • CLOSED: 表示链接已经关闭,无链接状态。

上面 7 种状态是 TCP 四次挥手,也就是断开连接所设计的。

TCP 的链接状态会进行各类切换,这些 TCP 链接的切换是根据事件进行的,这些事件由用户调用:OPEN、SEND、RECEIVE、CLOSE、ABORT 和 STATUS。涉及到 TCP 报文段的标志有 SYN、ACK、RST 和 FIN ,固然,还有超时。

咱们下面加上 TCP 链接状态后,再来看一下三次握手和四次挥手的过程。

三次握手创建链接

下图画出了 TCP 链接创建的过程。假设图中左端是客户端主机,右端是服务端主机,一开始,两端都处于CLOSED(关闭)状态。

  1. 服务端进程准备好接收来自外部的 TCP 链接,通常状况下是调用 bind、listen、socket 三个函数完成。这种打开方式被认为是 被动打开(passive open)。而后服务端进程处于 LISTEN 状态,等待客户端链接请求。
  2. 客户端经过 connect 发起主动打开(active open),向服务器发出链接请求,请求中首部同步位 SYN = 1,同时选择一个初始序号 sequence ,简写 seq = x。SYN 报文段不容许携带数据,只消耗一个序号。此时,客户端进入 SYN-SEND 状态。
  3. 服务器收到客户端链接后,,须要确认客户端的报文段。在确认报文段中,把 SYN 和 ACK 位都置为 1 。确认号是 ack = x + 1,同时也为本身选择一个初始序号 seq = y。请注意,这个报文段也不能携带数据,但一样要消耗掉一个序号。此时,TCP 服务器进入 SYN-RECEIVED(同步收到) 状态。
  4. 客户端在收到服务器发出的响应后,还须要给出确认链接。确认链接中的 ACK 置为 1 ,序号为 seq = x + 1,确认号为 ack = y + 1。TCP 规定,这个报文段能够携带数据也能够不携带数据,若是不携带数据,那么下一个数据报文段的序号还是 seq = x + 1。这时,客户端进入 ESTABLISHED (已链接) 状态
  5. 服务器收到客户的确认后,也进入 ESTABLISHED 状态。

TCP 创建一个链接须要三个报文段,释放一个链接却须要四个报文段。

四次挥手

数据传输结束后,通讯的双方能够释放链接。数据传输结束后的客户端主机和服务端主机都处于 ESTABLISHED 状态,而后进入释放链接的过程。

TCP 断开链接须要历经的过程以下

  1. 客户端应用程序发出释放链接的报文段,并中止发送数据,主动关闭 TCP 链接。客户端主机发送释放链接的报文段,报文段中首部 FIN 位置为 1 ,不包含数据,序列号位 seq = u,此时客户端主机进入 FIN-WAIT-1(终止等待 1) 阶段。

  2. 服务器主机接受到客户端发出的报文段后,即发出确认应答报文,确认应答报文中 ACK = 1,生成本身的序号位 seq = v,ack = u + 1,而后服务器主机就进入 CLOSE-WAIT(关闭等待) 状态,这个时候客户端主机 -> 服务器主机这条方向的链接就释放了,客户端主机没有数据须要发送,此时服务器主机是一种半链接的状态,可是服务器主机仍然能够发送数据。

  3. 客户端主机收到服务端主机的确认应答后,即进入 FIN-WAIT-2(终止等待2) 的状态。等待客户端发出链接释放的报文段。

  4. 当服务器主机没有数据发送后,应用进程就会通知 TCP 释放链接。这时服务端主机会发出断开链接的报文段,报文段中 ACK = 1,序列号 seq = w,由于在这之间可能已经发送了一些数据,因此 seq 不必定等于 v + 1。ack = u + 1,在发送完断开请求的报文后,服务端主机就进入了 LAST-ACK(最后确认)的阶段。

  5. 客户端收到服务端的断开链接请求后,客户端须要做出响应,客户端发出断开链接的报文段,在报文段中,ACK = 1, 序列号 seq = u + 1,由于客户端从链接开始断开后就没有再发送数据,ack = w + 1,而后进入到 TIME-WAIT(时间等待) 状态,请注意,这个时候 TCP 链接尚未释放。必须通过时间等待的设置,也就是 2MSL 后,客户端才会进入 CLOSED 状态,时间 MSL 叫作最长报文段寿命(Maximum Segment Lifetime)

  6. 服务端主要收到了客户端的断开链接确认后,就会进入 CLOSED 状态。由于服务端结束 TCP 链接时间要比客户端早,而整个链接断开过程须要发送四个报文段,所以释放链接的过程也被称为四次挥手。

什么是 TIME-WAIT

我上面只是简单提到了一下 TIME-WAIT 状态和 2MSL 是啥,下面来聊一下这两个概念。

MSL 是 TCP 报文段能够存活或者驻留在网络中的最长时间。RFC 793 定义了 MSL 的时间是两分钟,可是具体的实现还要根据程序员来指定,一些实现采用了 30 秒的这个最大存活时间。

那么为何要等待 2MSL 呢?

主要是由于两个理由

  • 为了保证最后一个响应可以到达服务器,由于在计算机网络中,最后一个 ACK 报文段可能会丢失,从而导致客户端一直处于 LAST-ACK 状态等待客户端响应。这时候服务器会重传一次 FINACK 断开链接报文,客户端接收后再从新确认,重启定时器。若是客户端不是 2MSL ,在客户端发送 ACK 后直接关闭的话,若是报文丢失,那么双方主机会没法进入 CLOSED 状态。
  • 还能够防止已失效的报文段。客户端在发送最后一个 ACK 以后,再通过通过 2MSL,就可使本连接持续时间内所产生的全部报文段都从网络中消失。从保证在关闭链接后不会有还在网络中滞留的报文段去骚扰服务器。

这里注意一点:在服务器发送了 FIN-ACK 以后,会当即启动超时重传计时器。客户端在发送最后一个 ACK 以后会当即启动时间等待计时器。

说好的 RST 呢

说好的 RSTSYNFIN 标志用于链接的创建和关闭,那么 SYN 和 FIN 都现身了,那 RST 呢?也是啊,咱们上面探讨的都是一种理想的状况,就是客户端服务器双方都会接受传输报文段的状况,还有一种状况是当主机收到 TCP 报文段后,其 IP 和端口号不匹配的状况。假设客户端主机发送一个请求,而服务器主机通过 IP 和端口号的判断后发现不是给这个服务器的,那么服务器就会发出一个 RST 特殊报文段给客户端。

所以,当服务端发送一个 RST 特殊报文段给客户端的时候,它就会告诉客户端没有匹配的套接字链接,请不要再继续发送了

上面探讨的是 TCP 的状况,那么 UDP 呢?

使用 UDP 做为传输协议后,若是套接字不匹配的话,UDP 主机就会发送一个特殊的 ICMP 数据报。

SYN 洪泛攻击

下面咱们来讨论一下什么是 SYN 洪泛攻击

咱们在 TCP 的三次握手中已经看到,服务器为了响应一个收到的 SYN,分配并初始化变量链接和缓存,而后服务器发送一个 SYNACK 做为响应,而后等待来自于客户端的 ACK 报文。若是客户端不发送 ACK 来完成最后一步的话,那么这个链接就处在一个挂起的状态,也就是半链接状态。

攻击者一般在这种状况下发送大量的 TCP SYN 报文段,服务端继续响应,可是每一个链接都完不成三次握手的步骤。随着 SYN 的不断增长,服务器会不断的为这些半开链接分配资源,致使服务器的链接最终被消耗殆尽。这种攻击也是属于 Dos 攻击的一种。

抵御这种攻击的方式是使用 SYN cookie ,下面是它的工做流程介绍

  • 当服务器收到一个 SYN 报文段时,它并不知道这个报文段是来自哪里,是来自攻击者主机仍是客户端主机(虽然攻击者也是客户端,不过这么说更便于区分) 。所以服务器不会为报文段生成一个半开链接。与此相反,服务器生成一个初始的 TCP 序列号,这个序列号是 SYN 报文段的源和目的 IP 地址与端口号这个四元组构造的一个复杂的散列函数,这个散列函数生成的 TCP 序列号就是 SYN Cookie,用于缓存 SYN 请求。而后,服务器会发送带着 SYN Cookie 的 SYNACK 分组。有一点须要注意的是,服务器不会记忆这个 Cookie 或 SYN 的其余状态信息
  • 若是客户端不是攻击者的话,它就会返回一个 ACK 报文段。当服务器收到这个 ACK 后,须要验证这个 ACK 与 SYN 发送的是否相同,验证的标准就是确认字段中的确认号和序列号,源和目的 IP 地址与端口号以及和散列函数的是否一致,散列函数的结果 + 1 是否和 SYNACK 中的确认值相同。(大体是这样,说的不对还请读者纠正) 。若是有兴趣读者能够自行深刻了解。若是是合法的,服务器就会生成一个具备套接字的全开链接。
  • 若是客户端没有返回 ACK,即认为是攻击者,那么这样也不要紧,服务器没有收到 ACK,不会分配变量和缓存资源,不会对服务器产生危害。

拥塞控制

有了 TCP 的窗口控制后,使计算机网络中两个主机之间再也不是以单个数据段的形式发送了,而是可以连续发送大量的数据包。然而,大量数据包同时也伴随着其余问题,好比网络负载、网络拥堵等问题。TCP 为了防止这类问题的出现,使用了 拥塞控制 机制,拥塞控制机制会在面临网络拥塞时遏制发送方的数据发送。

拥塞控制主要有两种方法

  • 端到端的拥塞控制: 由于网络层没有为运输层拥塞控制提供显示支持。因此即便网络中存在拥塞状况,端系统也要经过对网络行为的观察来推断。TCP 就是使用了端到端的拥塞控制方式。IP 层不会向端系统提供有关网络拥塞的反馈信息。那么 TCP 如何推断网络拥塞呢?若是超时或者三次冗余确认就被认为是网络拥塞,TCP 会减少窗口的大小,或者增长往返时延来避免
  • 网络辅助的拥塞控制: 在网络辅助的拥塞控制中,路由器会向发送方提供关于网络中拥塞状态的反馈。这种反馈信息就是一个比特信息,它指示链路中的拥塞状况。

下图描述了这两种拥塞控制方式

TCP 拥塞控制

若是你看到这里,那我就暂定认为你了解了 TCP 实现可靠性的基础了,那就是使用序号和确认号。除此以外,另一个实现 TCP 可靠性基础的就是 TCP 的拥塞控制。若是说

TCP 所采用的方法是让每个发送方根据所感知到的网络的拥塞程度来限制发出报文段的速率,若是 TCP 发送方感知到没有什么拥塞,则 TCP 发送方会增长发送速率;若是发送方感知沿着路径有阻塞,那么发送方就会下降发送速率。

可是这种方法有三个问题

  1. TCP 发送方如何限制它向其余链接发送报文段的速率呢?
  2. 一个 TCP 发送方是如何感知到网络拥塞的呢?
  3. 当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢?

咱们先来探讨一下第一个问题,TCP 发送方如何限制它向其余链接发送报文段的速率呢

咱们知道 TCP 是由接收缓存、发送缓存和变量(LastByteRead, rwnd,等)组成。发送方的 TCP 拥塞控制机制会跟踪一个变量,即 拥塞窗口(congestion window) 的变量,拥塞窗口表示为 cwnd,用于限制 TCP 在接收到 ACK 以前能够发送到网络的数据量。而接收窗口(rwnd) 是一个用于告诉接收方可以接受的数据量。

通常来讲,发送方未确认的数据量不得超过 cwnd 和 rwnd 的最小值,也就是

LastByteSent - LastByteAcked <= min(cwnd,rwnd)

因为每一个数据包的往返时间是 RTT,咱们假设接收端有足够的缓存空间用于接收数据,咱们就不用考虑 rwnd 了,只专一于 cwnd,那么,该发送方的发送速率大概是 cwnd/RTT 字节/秒 。经过调节 cwnd,发送方所以能调整它向链接发送数据的速率。

一个 TCP 发送方是如何感知到网络拥塞的呢

这个咱们上面讨论过,是 TCP 根据超时或者 3 个冗余 ACK 来感知的。

当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢 ?

这个问题比较复杂,且容我娓娓道来,通常来讲,TCP 会遵循下面这几种指导性原则

  • 若是在报文段发送过程当中丢失,那就意味着网络拥堵,此时须要适当下降 TCP 发送方的速率。
  • 一个确认报文段指示发送方正在向接收方传递报文段,所以,当对先前未确认报文段的确认到达时,可以增长发送方的速率。为啥呢?由于未确认的报文段到达接收方也就表示着网络不拥堵,可以顺利到达,所以发送方拥塞窗口长度会变大,因此发送速率会变快
  • 带宽探测,带宽探测说的是 TCP 能够经过调节传输速率来增长/减少 ACK 到达的次数,若是出现丢包事件,就会减少传输速率。所以,为了探测拥塞开始出现的频率, TCP 发送方应该增长它的传输速率。而后慢慢使传输速率下降,进而再次开始探测,看看拥塞开始速率是否发生了变化。

在了解完 TCP 拥塞控制后,下面咱们就该聊一下 TCP 的 拥塞控制算法(TCP congestion control algorithm) 了。TCP 拥塞控制算法主要包含三个部分:慢启动、拥塞避免、快速恢复,下面咱们依次来看一下

慢启动

当一条 TCP 开始创建链接时,cwnd 的值就会初始化为一个 MSS 的较小值。这就使得初始发送速率大概是 MSS/RTT 字节/秒 ,好比要传输 1000 字节的数据,RTT 为 200 ms ,那么获得的初始发送速率大概是 40 kb/s 。实际状况下可用带宽要比这个 MSS/RTT 大得多,所以 TCP 想要找到最佳的发送速率,能够经过 慢启动(slow-start) 的方式,在慢启动的方式中,cwnd 的值会初始化为 1 个 MSS,而且每次传输报文确认后就会增长一个 MSS,cwnd 的值会变为 2 个 MSS,这两个报文段都传输成功后每一个报文段 + 1,会变为 4 个 MSS,依此类推,每成功一次 cwnd 的值就会翻倍。以下图所示

发送速率不可能会一直增加,增加总有结束的时候,那么什么时候结束呢?慢启动一般会使用下面这几种方式结束发送速率的增加。

  • 若是在慢启动的发送过程出现丢包的状况,那么 TCP 会将发送方的 cwnd 设置为 1 并从新开始慢启动的过程,此时会引入一个 ssthresh(慢启动阈值) 的概念,它的初始值就是产生丢包的 cwnd 的值 / 2,即当检测到拥塞时,ssthresh 的值就是窗口值的一半。

  • 第二种方式是直接和 ssthresh 的值相关联,由于当检测到拥塞时,ssthresh 的值就是窗口值的一半,那么当 cwnd > ssthresh 时,每次翻番均可能会出现丢包,因此最好的方式就是 cwnd 的值 = ssthresh ,这样 TCP 就会转为拥塞控制模式,结束慢启动。

  • 慢启动结束的最后一种方式就是若是检测到 3 个冗余 ACK,TCP 就会执行一种快速重传并进入恢复状态。

拥塞避免

当 TCP 进入拥塞控制状态后,cwnd 的值就等于拥塞时值的一半,也就是 ssthresh 的值。因此,没法每次报文段到达后都将 cwnd 的值再翻倍。而是采用了一种相对保守的方式,每次传输完成后只将 cwnd 的值增长一个 MSS,好比收到了 10 个报文段的确认,可是 cwnd 的值只增长一个 MSS。这是一种线性增加模式,它也会有增加逾值,它的增加逾值和慢启动同样,若是出现丢包,那么 cwnd 的值就是一个 MSS,ssthresh 的值就等于 cwnd 的一半;或者是收到 3 个冗余的 ACK 响应也能中止 MSS 增加。若是 TCP 将 cwnd 的值减半后,仍然会收到 3 个冗余 ACK,那么就会将 ssthresh 的值记录为 cwnd 值的一半,进入 快速恢复 状态。

快速恢复

在快速恢复中,对于使 TCP 进入快速恢复状态缺失的报文段,对于每一个收到的冗余 ACK,cwnd 的值都会增长一个 MSS 。当对丢失报文段的一个 ACK 到达时,TCP 在下降 cwnd 后进入拥塞避免状态。若是在拥塞控制状态后出现超时,那么就会迁移到慢启动状态,cwnd 的值被设置为 1 个 MSS,ssthresh 的值设置为 cwnd 的一半。

后记

若是你能用心看到这里,我相信你定会有所收获。

这篇文章写的时间很长,图中不少样式和配色都是精挑细选,若是你仔细阅读,能够看到个人用心良苦。

若是你以为文章写的还不错,欢迎你帮助 cxuan 扩散一下,这将是我继续更新的动力,切忌不要白嫖,会让本身变得廉价,好的文章值得分享。

请记得给我一个赞哦!

另外,我本身肝了六本 PDF,微信搜索 程序员cxuan 关注公众号后,在后台回复 cxuan ,领取所有 PDF,这些 PDF。

相关文章
相关标签/搜索