「网络协议」对TCP-IP不再迷茫

1,083 阅读19分钟

前言

面试时总能经常被问到TCP的三次握手和四次挥手,但是TCP真正是什么却没有深刻的去研究。本章主要是偏向基础,对于深层的知识并没有体现的过多,主要考虑的还是本篇文章主要面向的并不是网络工程师。所以如果对整体的TCP感兴趣可以自行找书籍阅读。

IP

IP是TCP/IP协议中最为核心的协议。它有几个特点,第一是不可靠,第二个是无连接。 不可靠是它不保证IP数据能成功地达到目的地。IP仅提供最好的传输服务。如果发生某种错误时,如某个路由器暂时用完了缓存区,IP有一个简单的错误处理算法:丢弃该数据,然后发送信息给信源端。任何要求的可靠性必须由上层来提供。 无连接是IP 并不维护任何关于后续数据的状态信息。每个数据的处理是相互独立的,所以IP数据可以不按发送顺序接收。如果一信源向相同的端发送两个连续的数据包A、B,每个数据包都是想不独立的,它们可以选择不同的路线,因此B可以比A先达到。

IP首部

普通的IP首部长20个字节。除非含有选项字段。 最高位在左边,记为0bit;最低位在右边,记为31bit。 4个字节的32bit值以下面的次序传输:首先是 0~7 bit,其次8~15 bit,然后16~23bit,最后是24~31bit。这种传输次序称作big endian字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。以其他形式存储二进制整数的机器,如little endian格式,则必须在传输数据之前把首部转换成网络字节序。

版本

目前的协议版本号是4,因此IP有时也称作IPv4。

首部长度

首部长度指的是首部占 32bit字的数目,包括任何选项。由于它是一个4比特字段,因此首部最长为60个字节。普通IP数据报(没有任何选择项)字段的值是5。

服务类型

服务类型(TOS)字段包括一个3bit的优先权子字段(现在已被忽略),4bit的TOS子字 段和1 bit未用位但必须置0。4bit的TO S分别代表:最小时延、最大吞吐量、最高可靠性和最小费用。4bit中只能置其中1bit。如果所有4bit均为0,那么就意味着是一般服务。RFC1340 [Reynolds and Postel 1992] 描述了所有的标准应用如何设置这些服务类型。RFC1349 [Almquist 1992]对该RFC进行了修正,更为详细地描述了TOS的特性。

总长度

总长度字段是指整个IP数据报的长度,以字节为单位。利用首部长度字段和总长度字段,就可以知道IP数据报中数据内容的起始位置和长度。由于该字段长 1 6比特,所以IP数据报最长可达65535字节(超级通道的MTU为65535。它的意思其实不是一个真正的MTU— 它使用了最长的IP数据包)。当数据报被分片时,该字段的值也随着变化。

标识

标识字段唯一地标识主机发送的每一份数据报。通常每发送一份报文它的值就会加 1。

生存时间(TTL)

TTL(time-to-live)生存时间字段设置了数据报可以经过的最多路由器数。它指定了数据 报的生存时间。 TTL的初始值由源主机设置(通常为 32或64),一旦经过一个处理它的路由器, 它的值就减去 1。当该字段的值为 0时,数据报就被丢弃,并发送 I C M P报文通知源主机。

检验和

首部检验和字段是根据IP首部计算的检验和码。它不对首部后面的数据进行计算。 ICMP、 IGMP、UDP和TCP在它们各自的首部中均含有同时覆盖首部和数据检验和码。

子网掩码

任何主机在引导时进行的部分配置是指定主机IP地址。大多数系统把IP地址存在一个磁盘文件里供引导时读取。 除了I P地址以外, 主机还需要知道有多少比特用于子网号及多少比特用于主机号。 这是在引导过程中通过子网掩码来确定的。这个掩码是一个 32 bit的值,其中值为 1的比特留给网 络号和子网号,为 0的比特留给主机号。如下图所示一个B类地址的两种不同的子网掩码格式。第 一个例子是noao.edu网络采用的子网划分方法,子网号和主机号都是 8 bit宽。 第二个例子是一个B类地址划分成10 bit的子网号和6 bit的主机号。 尽管I P地址一般以点分十进制方法表示, 但是子网掩码却经常用十六进制来表示,特别是当界限不是一个字节时,因为子网掩码是一个比特掩码。 给定IP地址和子网掩码以后,主机就可以确定 IP数据报的目的是:(1)本子网上的主机;(2)本网络中其他子网中的主机;(3)其他网络上的主机。如果知道本机的 IP地址,那么就知道 它是否为A类、B类或C类地址(从IP地址的高位可以得知),也就知道网络号和子网号之间的分 界线。而根据子网掩码就可知道子网号与主机号之间的分界线。

TCP的服务

之前我们说过,TCP是一个面向连接的,可靠的字节流服务。 面向连接意味着两个使用 TCP的应用(通常是一个客户和一个服务器)在彼此交换数据 之前必须先建立一个 TCP连接。 字节流服务意味着两个应用程序通过TCP连接交换8 bit字节构成的字节流。 TCP不在字节流中插入记录标识符。

TCP的首部

端口号

每个TCP段都包含源端和目的端的端口号, 用于寻找发端和收端应用进程。 这两个值加 上IP首部中的源端IP地址和目的端IP地址唯一确定一个TCP连接。并且客户IP地址、客户端口号、服务器 IP地址和服务器端口号称为四元组。

序号

序号用来标识从TCP发端向TCP收端发送的数据字节流,它表示在这个报文段中的的第一 个数据字节。如果将字节流看作在两个应用程序间的单向流动,则 T C P用序号对每个字节进 行计数。序号是32 bit的无符号数,序号到达23^2 -1 后又从0开始。

SYN

当建立新的TCP连接时,SYN(同步序号用来发起一个连接)标志会变为1,序号字段包含由这个主机选择的该连接的初始序号ISN(Initial Sequence Number)。该主机要发送数据的第一个字节序号为这个 ISN加1,因为 SYN标志消耗了一个序号。

ACK

ACK确认号,TCP使用确认号来告知对方下一个期望接收的序列号,小于此确认号的所有字节都已经收到。因此确认号应当是上次已成功收到数据字节序号加1。只有ACK标志为1时确认序号字段才有效。发送ACK无需任何代价,因为 32 bit的确认序号字段和ACK标志一样,总是TCP首部的一部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置,ACK标志也总是被设置为1。 TCP有6个标志,他们分别是:

  • URG:紧急指针有效
  • ACK:确认序号有效
  • PSH:接收方应该尽快将这个报文段交给应用层
  • RST:重建连接
  • SYN:同步序号用来发起一个连接
  • FIN:发端完成发送任务

窗口大小

TCP的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数, 起始 于确认序号字段指明的值,这个值是接收端正期望接收的字节。窗口大小是一个 16 bit字段,因而窗口大小最大为65535字节。

检验和

检验和覆盖了整个的TCP报文段:TCP首部和TCP数据。这是一个强制性的字段,一定是由发端计算和存储,并由收端进行验证。TCP检验和的计算和UDP检验和的计算相似。 只有当URG标志置1时紧急指针才有效。紧急指针是一个正的偏移量,和序号字段中的值相加表示紧急数据最后一个字节的序号。TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。

MSS

最常见的可选字段是最长报文大小,又称为 MSS (Maximum Segment Size)。每个连接方 通常都在通信的第一个报文段(为建立连接而设置SYN标志的那个段)中指明这个选项。它指明本端所能接收的最大长度的报文段。

什么是端口号

传输层中用端口号作为唯一的标识。在一台主机中最大允许拥有65536个端口号。传输层就是用端口号来区分同一个主机上不同的应用程序的。操作系统为有需要的进程分配端口号,当目标主机收到数据包以后,会根据数据报文首部的目标端口号将数据发送到对应端口的进程。

端口号分类

端口号分为以下三种类型:

  1. 熟知款口号
  2. 已登记的端口号
  3. 临时端口号

熟知端口号

熟知端口号的由IANA分配和控制的,范围是0~1023。很多熟知端口号已经陪使用了,如HTTP使用80端口,HTTPS使用443端口。

已登记端口号

已登记的端口不受 IANA 控制,不过由 IANA 登记并提供它们的使用情况清单。它的范围为 1024~49151。

临时端口号

如果应用程序没有调用 bind() 函数将 socket 绑定到特定的端口上,那么 TCP 和 UDP 会为该 socket 分配一个唯一的临时端口。IANA 将 49152~65535 范围的端口称为临时端口(ephemeral port)或动态端口(dynamic port),也称为私有端口(private port),这些端口可供本地应用程序临时分配端口使用。

TCP的三次握手

我们将从两个部分了解TCP三次握手,第一部分是三次握手中序列号的变化,第二部分是三次握手的状态变化

三次握手中序列号的变化

1、客户端发送的一个段是SYN报文,这个报文只有SYN标记被置位。SYN报文不携带数据,但是要占用一个序列号,下次发送数据序列号要加一。客户端会随机选择一个数字作为初始序列号。 这里有个疑问就是为什么SYN不携带数据还要占用一个序列号呢? 这是因为在TCP中只要需要对方确认的,都要消耗一个序列号,如果这个段没有收到确认,会一直重传直到达到指定的次数为之。 2、服务端收到客户端的SYN段以后,将SYN和ACK标记都置位。 SYN 标记的作用与步骤 1 中的一样,也是同步服务端生成的初始序列号。ACK 用来告知发送端之前发送的 SYN 段已经收到了,「确认号」字段指定了发送端下次发送段的序号,这里等于客户端 ISN 加一。 与前面类似 SYN + ACK 端虽然没有携带数据,但是因为 SYN 段需要被确认,所以它也要消耗一个序列号。 3、客户端发送三次握手最后一个ACK段,这个ACK段用来确认收到了服务端发送的SYN段。因为这个ACK段不携带任何数据,且不需要再被确认,这个ACK段不消耗任何序列号。

三次握手的状态变化

对于客户端而言:

  • 初始状态是处于CLOSED状态。CLOSED并不是一个真实的状态,而是一个假想的起点和终点。
  • 客户端调用connect以后会发送SYN同步报文给服务端,然后进入SYN-SENT阶段,客户端将保持这个阶段直到它收到了服务端的确认包
  • 如果在SYN-SENT状态收到了服务端的确认包,它将发送确认服务端SYN报文的ACK包,同时进入ESTABLISHED状态,表明自己已经准备好发送数据。 对于服务端而言:
  • 初始状态同样是CLOSED状态
  • 在执行bind、listen调用以后进入LISTEN状态,等待客服端链接
  • 当收到客户端的SYN同步报文以后,会回复确认同时发送自己的SYN同步报文,这时服务端进入SYN-RCVD阶段等待客户端的确认
  • 当收到客户端的确认报文以后,进入ESTABLISHED状态,这时双方就可以相互发送数据了。

TCP的四次挥手

1、客户端调用close方法,执行主动关闭,会发送一个FIN报文给服务端,从这以后哭护短不能在发送数据给服务端了,客户端进入FIN-WAIT-1状态。FIN报文其实就是将FIN标志位设置为1。 2、服务端收到FIN包以后回复确认ACK报文给客户端,服务端进入CLOSE-WAIT,客户端收到ACK以后进入FIN-WAIT-2状态。 3、服务端也没有数据要发送了,发送FIN报文给客户端,然后进入LAST-ACK状态,等待客户端的ACK。 4、客户端收到服务端的FIN报文以后,回复ACK报文用来确认第三步里的FIN报文,进入TIME-WAIT状态,等待2个MSL以后进入CLOSED状态,服务端收到ACK以后进入CLOSED状态。

TCP的十一种状态

TCP在连接和断开连接中一共有11种状态,TCP之所以复杂主要原因就是因为它是一个含有状态的协议。

1、CLOSED

这个状态是一个「假想」的状态,是TCP连接还没有开始建立连接或者连接已经彻底释放的状态。 从CLOSE状态转换为其他状态有两种可能:

  1. 被动打开:一般来说,服务器会监听一个特定的端口,等待客户端的新连接,同时会进入LISTEN状态,这种称之为被动打开。
  2. 主动打开:客户端注重发送一个SYN包准备三次握手,称之为主动打开。

2、LISTEN

一端(通常是服务端)调用bind、listen系统调用监听特定端口时进入LISTEN状态,等待客户端发送SYN报文三次握手建立连接。处于LISTEN状态的连接收到SYN包以后会发送SYN+ACK给对端,同时进入SYN+RCVD阶段。

3、SYN-SENT

客户端发送SYN报文等待ACK的过程进入SYN-SENT状态。同时会开启一个定时器,如果超时还没有收到ACK会重发SYN。

4、SYN-RCVD

服务端收到SYN报文以后会回复SYN+ACK,然后等待对端ACK的时候进入SYN-RCVD。

5、ESTABLISHED

SYN-SENT或者SYN-RCVD状态的连接收到对端确认ACK以后进入ESTABLISHED状态,连接建立成功。 ESTABLISHED状态的连接有两种可能的状态转换方式:

  1. 调用close等系统调用主动关闭连接,这个时候会发送FIN包给对端,同时自己进入FIN-WAIT-1状态。
  2. 收到对端的FIN包,执行被动关闭,收到FIN包以后会回复ACK,同时自己进入CLOSE-WAIT状态。

6、FIN-WAIT-1

主动关闭的一方发送了FIN包,等待对端回复ACK时进入FIN-WAIT-1状态。 FIN—WAIT1状态的切换有如下几种情况:

  • 当收到ACK以后,FIN-WAIT-1状态会转换为FIN-WAIT-2状态
  • 当收到FIN以后,会回复对端ACK,FIN-WAIT-1状态会转换为CLOSING状态
  • 当收到FIN+ACK以后,会回复对端ACK,FIN-WAIT-1状态会转换为TIME—WAIT状态,跳过FIN-WAIT-2状态

7、FIN-WAIT-2

处于FIN-WAIT-2状态的连接收到ACK确认包以后进入FIN-WAIT-2状态,这个时候主动关闭放的FIN包已经被对方确认,等待被动关闭放发送FIN包。 当收到对端的FIN包以后,主动关闭方进入TIME—WAIT状态。

8、CLOSE-WAIT

当有一方想关闭连接的时候,调用close等系统调用关闭TCP连接会发送FIN包给对端,这个被动关闭方,收到FIN包以后进入CLOSE-WAIT状态。 当被动关闭方有数据要发送给对端的时候,可以继续发送数据。当没有数据发送给对方时,也会待用close等系统调用关闭TCP连接,发送FIN包给主动关闭的一方,同时进入LAST-ACK状态。

9、TIME-WAIT

这个状态时收到了被动关闭方的FIN包,发送确认ACK给对端,开启2MSL定时器,定时器到期时进入CLOSED状态,连接释放。后面有个章节专门介绍。

10、LAST-ACK

被动关闭的一方,发送FIN包给对端等待ACK确认时的状态。当收到ACK以后,进入CLOSED状态,连接释放。

11、CLOSING

CLOSING状态在「同时关闭」的情况下出现。这里的同时关闭中的「同时」其实并不是时间意义上的同时,而是指的是在发送 FIN 包还未收到确认之前,收到了对端的 FIN 的情况。

TIME-WAIT

在上个章节中我们提到过TIME-WAIT状态,其中我们介绍到在发送确认ACK给对端后,开启2MSL定时器。此时就会有一个疑问什么是MSL。

MSL

MSL全称是Max Segment Lifetime(报文最大生存时间)是TCP报文在网络中的最大生存时间。这个值与IP报文头的TTL字段有密切的关系。 IP报文头中有一个8位的存活时间字段(Time to live,TTL)。这个存活时间储存的不是具体的时间,而是一个IP报文最大可经过的路由数,每经过一个路由,TTL减1,当TTL减到0时这个IP报文会被丢弃。

TIME—WAIT存在的原因

第一个原因:数据报文可能在发送途中延迟但最终会到达,因此要等老的重复报文段在网络中过期失效,这样可以避免用相同源端口和目标端口创建新连接时收到旧连接的数据包,造成数据错乱。 第二个原因:确保可靠实现TCP全双工终止连接。关闭连接的四次挥手中,最终的ACK由主动关闭方发出,如果这个ACK丢失,对端(被动关闭方)将重发FIN,如果主动关闭方不维持TIME—WAIT直接进入CLOSED状态,则无法重传ACK,被动关闭方因此不能及时可靠释放,

为什么时间是两个MSL

  • 1个MSL确保四次挥手中主动关闭方最后的ACK报文最终能达到对端
  • 1个MSL确保对端没有收到ACK重传的FIN报文可以到达

keepalive原理

网络故障和系统宕机都将使对端无法收到数据。TCP不会采用类似轮询的方式来询问对端是否有东西要给他。

那TCP是如何解决的呢?

TCP有个状态不属于之前讲的11个状态中,即半打开状态(half open)。这一个情况就是如果在未告知另一端的情况下通讯的一段关闭或者终止连接,那么就认为该条TCP连接处于半打开状态。这种情况发现在通信的一方的主机崩溃、电源断掉的情况下。只要不尝试通过半开连接来传输数据,正常工作的一段将不会检测出另外一段已经崩溃。

为什么大部分应用程序都没有开启keepalive选项?

现在大部分应用程序都没有开启 keepalive 选项,一个很大的原因就是默认的超时时间太长了,从没有数据交互到最终判断连接失效,需要花 2.1875 小时(7200 + 75 * 9),显然太长了。但如果修改这个值到比较小,又违背了 keepalive 的设计初衷(为了检查长时间死连接)

TCP的keepalive与HTTP的keep-alive有什么区别?

HTTP的keepalive是保证TCP连接没有过早关闭,而TCP的keepalive是heart做链路检测。

参考

深入理解TCP协议:从原理到实战

后语

觉得还可以的,麻烦走的时候能给点个赞,大家一起学习和探讨!

还可以关注我的博客希望能给我的github上点个Start,小伙伴们一定会发现一个问题,我的所有用户名几乎都与番茄有关,因为我真的很喜欢吃番茄❤️!!!

想跟车不迷路的小伙还希望可以关注公众号 前端老番茄 或者扫一扫下面的二维码👇👇👇。关注后可以拉你进入前端进阶群,一起探讨,一起交流。还可以获取前端资料。 我是一个编程界的小学生,您的鼓励是我不断前进的动力,😄希望能一起加油前进。