TCP协议基础知识(一)

2021-10-11
学习

前言

之前学习TCP协议时,完全没有理解TCP协议的设计理念,致使学习过程艰难曲折,中途放弃,后面又陆陆续续学了HTTP协议,了解了OSI网络模型的本质,知识体系有了一定的拓展。最近再开始学习TCP协议时,对协议的理解也比之前透彻,但是TCP方面的书籍和资料,语言大多晦涩难懂,并且TCP相关的知识浩如烟海,涉及到很多细节。所以这里将TCP协议涉及到的基本概念做一个总结和记录,看完本文之后再去看书,会有助于对书中细节的理解。

学习TCP协议对开发几乎不能提供什么实际的帮助,但是学习TCP协议可以帮助深入理解网络协议、或者快速的定位Web问题。此外TCP协议的设计思想有助于拓宽知识边界,从整体上提升开发人员对项目的理解能力。

OSI网络模型

在讲解TCP协议之前,我们首先来回顾一下OSI网络模型的具体分层、分层的本质以及为什么要分层。

OSI网络模型从上到下依次是:

  • 应用层,常见协议有HTTP,SSH,FTP,SFMTP
  • 传输层,常见协议有TCP,UDP
  • 网络层,常见协议有IP协议,ARP协议
  • 数据链路层
  • 物理层

当客户端向服务端发送请求(数据)时,主动发起请求的一方网络数据包的封装是从上到下的,每层都会将上层传递过来的数据作为载荷,并在数据载荷的开头添加自己的头信息。当数据到达服务端时,数据包的拆封是从下往上的,每层协议都会剥离掉自己需要的头信息,然后再将数据传递到上层,最终到达应用层。简单概括网络分层的本质就是在网络数据传递之前每一层协议都会向数据包中添加必要的头信息,传递给目标主机之后层一从又会从数据包中层层剥离头信息。

网络分层优点显而易见,各层协议完全解偶,易于复用,并且各司其职,负责不同的功能,比如应用层软件主要将数据呈现给用户,传输层主要通过端口号将数据交给不同的应用层软件,网络层利用IP地址使数据在不同的网络路由器之间传输,数据链路层利用MAC地址表将不同的数据转发给特定的交换机端口,物理层负责将数据转换成光信号在光纤传输,或者转换成电磁波以WI-FI形式传输等等。

但是需要强调的是各层数据头在传输过程中也不是一成不变的,例如数据链路层数据包头经过不同的交换机会剥离并封装成新的包含下一个交换机的目标MAC地址,对这部分内容感兴趣的参考之前的文章

TCP头部字段

TCP协议是可靠的,面向连接的,基于字节流的全双工协议

之所以说TCP协议是可靠的,面向连接的,主要是因为TCP建立连接需要经历三次握手。三次握手主要作用是创建一个逻辑连接,双方确定后续通信的序列号,和窗口缩放等信息。由于网络层的IP地址协议是不可靠,无连接的协议,并且不能保证传输的数据包的顺序和可靠性,所以TCP协议发展出了一系列复杂的机制来保证传输可靠性。

基于字节流的含义是TCP没有固定的报文边界,假设TCP要发送1480字节的数据,可以分两次1000,480将数据写入socket,也可以一次写入。接收端收到数据后,可以分多次读取,每次读取一部分数据,也可以一次性全部读取。这里可以对比应用层HTTP协议,HTTP协议是基于文本的协议,例如它的请求行,请求头和请求体,以及每个部分中不同的字段主要是依靠换行符、回车符或空行分隔,应用层软件可以依靠这些特殊字符解析协议的内容。

全双工的含义是通信的双方可以同时接收和发送数据。

前文说过,每层网络协议主要为数据添加了不同的数据头,那我们先来看一下传输层的TCP协议头:
TCP-header-Page-1.png
TCP头部包含基本头部信息和可选项,基本头部信息固定20字节长度,可选项在不同类型的传输层数据包中字段和长度是不同的,后面会讨论这部分内容。首先先来了解一下常见的头部的字段。

端口号

如果把服务器比作一个房子,房子里面提供各种个样的服务,那么端口号作用就是访问这些服务的大门,TCP协议头的前四个字节是源端口号和目标端口号,他们分别占两个字节,由此可知端口号最大值为2^16 - 1,即0~66635。

但是这么多的端口号其分配并不是随意的,0~1023范围内的端口号称之为系统端口(System Ports),系统端口号由专门的机构IANA(互联网号码分配局,Internet Assigned Numbers Authority)制定协议来分配,常见的系统端口号有HTTP的80端口,SSH的22端口,HTTPS的443端口。1024~49151范围内的端口称之为用户端口号(User Ports),比如常见的用户端口号有MySQL的3306。这两类的端口号都可以在IANA查询。其余的端口号称之为动态端口号或私有端口号(Dynamic Ports/Private Ports),这些端口号是供本机应用程序临时分配使用的,现实中不同的操作系统选择的临时端口号范围会略有不同,并且可以允许通过修改操作系用文件进行设置。需要注意的是主动发起连接的一方也会被分配一个临时端口号,每次通信分配的端口是不同的。接下来我们用Wireshark(Wireshark教程参考这篇文章)抓包查看一下这两个端口号,启动Wireshark后,输入显示过滤器http.host == “wttr.in”,然后在命令行中发送两次HTTP请求:

1
curl http://wttr.in/Shanghai?0

可以看到两次分配的源端口是不同的,目标端口号都是80
tcp-port.png

序列号 & 确认号 & Flags位

序列号(Sequence Number)数值指的是TCP发送到接收端的一个字节序号,TCP数据是分块传输的,该字节代表了某个报文段数据(数据块)的第一个字节。序列号占用4个字节的长度,其范围为0~2^32-1,达到2^32-1后再从0开始循环。

确认号(Acknowledgment Number)指的是期待发送方发送的下一个序列号,即最后被成功接收的字节序号加1,表示小于这个数值的字节已经都收到。这个字段一般存在于接收方收到数据后回复给发送方的ACK数据包里,需要指出的是不是所有的数据包需要确认包,比如确认包本身,也不是收到数据包后立刻就会确认,可以延迟确认。这个字段要生效,要求Flags标志位的ACK置为1。

flags标识位指的是第14字节的8个符号位:CWR,ECU,ACK,SYN,RST,FIN等字段,他们各自占用一个比特,其值只能为0或者1。主要用来标识该数据包的类型或作用。常用的标识位有以下几个:

  • SYN:主要是发起连接时,同步双方的初始序列号
  • ACK:数据确认包,接收方收到数据后,回复给发送方的数据包。
  • RST:强制断开链接的数据包,比如服务端链接已经丢失,客户端在不知道的情况下依然向服务端发送数据,服务端表示自己无法处理。
  • FIN:数据发送完毕,准备断开链接,后面不会再有数据包发送。
  • PSH:告知接收方收到数据以后立马交给应用层,不能缓存起来。

之所以把这三个头部字段放在一起,是因为要使用这个三个字段来讨论一下经典的三次握手和四次挥手的过程。三次握手和四次挥手主要用到以下几个头部字段:序列号Seq,主动发起请求的一方(通常称为客户端)第一次握手时会生成初始序列号。符号位SYN通常代表客户端想要创建连接。确认号ack和符号位ACK置为1通常是服务端向客户端确认某个序号之前的报已经收到。
TCP-header-shakehands.png

三次握手和四次挥手

TCP三次握手和四次挥手的过程如下:
TCP-header-shakehands1.png

现实中最常见的情况是客户端主动发起请求,服务端响应请求。

  1. 第一次挥手之前客户端和服务端都处于Closed状态,客户端主动发起第一次握手请求,其flgs位的SYN值会被设置为1,同时客户端会生成初始序列号x,seq=x。之后客户端处于SYN-SENT状态。
  2. 服务端最初的状态也是CLOSED状态,在启动监听某个端口之后处于LISTEN状态。收到上一步的数据包后,知道客户端想要建立连接,然后将SYN位和ACK为都置为1,并生成自己的初始序列号y,seq=y,同时将上一步收到的客户端的初始序列号加一设为确认号,ack=x+1。然后将这个TCP数据包发送给客户端。自己的状态变为SYN-RCVD状态。
  3. 客户端收到上一步的数据包后,会检查确认号ack的值是否是自己第一次发起握手时生成初始序列号x+1。确认之后,会生成一个新的数据包,其ACK位设为1,序列号seq=x+1,确认号ack的值设置为上一步收序列号+1,即ack=y+1。同时自己处于ESTABLISHED状态。
  4. 服务端收到上一步的数据包之后,确认序列号seq和确认号ack之后状态变为ESTABLISHED,连接建立。

四次挥手的过程如下:

  1. 客户端主动关闭连接,将标识位FIN置为1,即发送FIN报文给服务端,此时客户端不能在发送数据给服务端,但还是可以接受服务端发来的数据。此时客户端进入FIN-WAIT-1状态。FIN报文既可以携带数据,也可以不携带数据,但不管携带与否都会消耗一个序列号。
  2. 服务端收到FIN报文后,回复ACK报文给客户端,此时服务端处于CLOSE-WAIT状态,客户端收到这个ACK报文后处于FIN-WAIT-2状态。
  3. 服务端没有数据要发给客户端之后,发送FIN报文给客户端请求关闭连接。跟第一步类似该报文不管是否携带数据都需要消耗一个序列号seq。发送之后自己处于LAST- ACK状态。
  4. 客户端收到服务端的FIN报文之后,回复ACK确认上一步的FIN报文并进入TIME-WAIT状态,客户端在等待两个MSL之后会进入CLOSED状态,服务端收到客户端的ACK后进入CLOSED状态。后面介绍为什么客户端要等待两个MSL。

三次握手可以变为四次吗?四次挥手可以变为四次吗?

需要注意的是,三次握手是可以变为四次的,第二次握手时,服务端发送的SYN+ACK报文可以分两次发送,即可以先发送ACK报文,确认收到客户端的握手请求。再发送SYN报文表示自己也要建立服务端到客户端方向的连接。

类似的,四次挥手也可以变成三次挥手,第二步和第三部服务端向客户端发送的ACK报文和FIN报文可以同时发送,即同时发送ACK+FIN报文。

三次握手和四次挥手的Wireshark抓包步骤同上一章节,不过为了查看对应http请求的TCP握手过程,我们可以右键点击上个截图中http记录,在弹出的菜单中选择Follow > TCP Stream即可查看所有的TCP请求。
tcp-header-shakehands2.png
可以看到图中经过三次握手建立连接,HTTP传输数据,之后的挥手关闭连接是三次挥手,查看序号为20的TCP数据包的Flags位的ACK和FIN都置为1说明他的ACK报文和FIN报文是同时发送的。

头部长度

头部长度是一个4位的字段,表示TCP头部的总大小,其单位为32位比特,即4个字节。一个4位的二进制数能表示的最大十进制数是15,所以TCP头部的总长度最大值为15*4字节=60字节,固定的头部字段占20字节,可选项占用0~40字节。TCP头部字段的长度必须是32位的倍数。

窗口大小

通过TCP连接的双方,在接收到数据后,数据并不会立马就被上层协议取走,而是会先放入缓存中,缓存并不是无限大的,所以通信的双方需要事先沟通好自己可以接受的数据大小,这就是窗口大小字段的目的。

窗口大小字段窗口大小字段的单位是字节,其作用是向对方声明自己的接收窗口大小,下次发送数据时,调整发送包的大小。比如服务端接收到客户端的数据后,在回复的ACK数据包里声明自己的接收窗口是100,客户端收到这个包后,会调整自己的发送窗口,下次发送数据包时,将会根据这个值来调整发送的数据包的大小不能超过100字节。这个字段主要实现下面将要提到的滑动窗口和流量控制的概念。发送方和接收方会在三次握手时告知对方自己的初始窗口大小。

校验和,紧急指针

校验和指的是发送方会对发送的TCP数据和头部进行计算并保存,然后接收方验证。紧急指针不常使用,该字段只有在URG位字段被置位时才生效。