文章目录
- 传输层协议TCP基本认识
- TCP协议的格式
- TCP的可靠性初步理解——确认应答机制
- 暂时理解TCP的通信过程
- TCP的确认号和确认序号
- 确认号和确认序号的意义
- 捎带应答
- TCP中其他字段的理解
- 16位窗口大小
- 标志位
- 标志位的本质
- 标志位的意义
- 以SYN + ACK标志位简单理解TCP连接三次握手
- 以FIN标志位简单理解TCP断连四次挥手
- PSH标志位
- RST标志位
- 16位紧急指针 && URG标志位
传输层协议TCP基本认识
在讲解完UDP的相关原理之后,我们将在UDP原理的基础上,进行对TCP原理的探究!
TCP协议的格式
我们先来认识一下TCP协议的格式:
光是从格式上来看,我们就能发现,TCP是比UDP要复杂的多的多!但是没有关系,我们将一步步地来解决TCP中可能遇到的、也可能是不理解的问题!
还是一样,对于TCP报头而言,以下两个问题如何解决:
1.报头和有效载荷如何分离?
2.如何分用?
分离:
对于TCP的报头来说,绝大部分情况下,报头大小都是固定的,即20字节(不包含选项+数据)!
但是,还有剩下一小部分是什么情况呢?
其实是因为:TCP的报头中,选项字段可能存在,也可能不存在!这就导致了TCP报文的报头的大小是浮动的。那这是一件比较棘手的事情。这如何解决?
解决方案:在报头中存在一个字段——4位首部长度
这个4位首部长度,可以表示0 ~ 15的数字,可TCP报头的最小都是20byte了,这是怎么回事?
所以这就说明,这个0 ~ 15的基本单位必然不可能是1byte,其实是4byte!
所以,0 ~ 15,其实表示的是0 ~ 60字节!表示的是首部长度!
首部 = 20字节基础报头 + 选项报头!
所以得出,选项的大小应该是0 ~ 40byte!而4位首部长度,最小值也应该是5!
有了上面的认识,我们想让报头和有效载荷分离就是比较简单的事情了。直接先读取出20字节的基础报头,得出4位首部长度的值。然后根据对应关系计算得出选项报头的大小即可!
分用:
在能够把报头和有效载荷分离的情况下,想要分用报头也是一件非常简单的事情!
直接把报头拿出来,根据TCP的格式就可以提取出相应的字段,交付给对应接口使用即可。
还有最后一个问题:
UDP协议的报头中,存在一个16位长度字段,表示整个UDP报文的长度。
但是,在TCP这里,怎么只有标识首部长度的字段,而没有整个TCP报文的长度呢?
其实是因为:
TCP是面向字节流的!在TCP协议看来,它根本不关心自己读到的是什么,只认为它是一个个的字节!TCP根本不关心报文的边界:
TCP协议有两对发送<->接收的缓冲区。信息来的时候,或者写入的时候,TCP这一层根本就不关心到底是什么数据。在TCP看来,就是一个个的字节。
只是TCP这一层规定好了对应的格式!然后发送到对端的时候,报头和数据分离,数据会放到接收缓冲区。至于数据应该怎么被安全的传输到应用层,这个工作需要由应用层根据应用层的协议、具体的业务来进行处理,这不是TCP的范畴!
所以,对于TCP的报头来说,根本没有存在整个报文长度字段的必要!
TCP的可靠性初步理解——确认应答机制
我们需要先了解TCP通信的可靠性相关话题,这是理解TCP通信的基础!然后以此理解TCP格式中的部分字段。在此之后,我们会再来理解其它的没有提及的片段!
首先需要声明的一点是:不存在100%可靠的协议!
但是我们又常常能够见到各种教材和资料中都说:TCP具有可靠性,这是怎么一回事呢?
现在我们就需要对TCP的可靠性有一个全新的、正确的认识:
1.具有应答,保证的是历史消息的可靠性!
2.最新的消息,我们是没有办法保证它的可靠性的,因为它没有应答!
在TCP的可靠性中,处于核心地位的就是上面的 确认应答机制!
暂时理解TCP的通信过程
其实我们现在是没有办法完完全全理解TCP的通信过程,以及通信中出现的一些问题的!
但是,我们肯定能够知道:TCP协议是一个基于确认应答机制的通信协议!
最简单的通信流程就如上图所示:
服务器和客户端都可以向对方发送信息,都需要接收到对方的应答来保证自己历史发送的数据是可靠的!-> 在TCP中,通信的双方地位是平等的!
通过上面最简单的例子, 我们就知道通信的双方为什么能够说通信可靠!
当然,有很多问题我们没有说,如应答丢失等,这些我们后面说!
TCP的确认号和确认序号
但是,在今后对于通信的理解的时候,我们需要重新说明的一点是:
在很多教材、资料中,都很喜欢把通信的流程图认为就是发一个简单的信息,一个简单的字段!但其实这是不对的!我们需要牢记:在TCP通信中,传输的至少是一个TCP报头!
传输的报文中,可以只有报头而没有数据!我们在HTTP协议那里也是见过:
在浏览器访问ip:port的时候,就是向服务器申请一个HTTP请求,是不带正文的!
现在我们需要看一个新的场景:
上面讲TCP基本通信流程的时候,是一边发送一个报文,一边给应答。是有很明显的串行顺序的!但是,这种串行的顺序进行通信,其实是有很大的效率问题的!因为这样一个个来做发送<->应答效率还是有点低了!
那有没有一种可能:发送报文是连续放松的,返回应答也是连续的呢?
答案是:完全有可能,如下图所示:
这种就是并行的进行发送和返回,效率会高一些!但是,有一个很大的问题:
既然是连续发送的报文,那么返回应答报文的时候:
1.发送方怎么知道返回的应答是针对于哪一个发送的报文的?
2.应答可能会丢失,若是丢失了若干个怎么判断可靠情况?
先解决第一个问题:
就是通过TCP报头中的32位序号和32位确认序号来做判断的!
发送方每次发送一个报文的时候,都会在序号字段上设置对应的数字,代表一个序号!
然后,返回应答的一方,收到某个32位序号为x的报文,就会设置应答报文中的确认序号为x+1,然后返回给发送端!
也就是说:确认序号 = 序号 + 1
!
确认序号的物理意义是:代表该序号之前的所有序号的报文都已经接收成功,下一次需要从该序号开始发送报文!
当然,实际上并不是一定要按照确认序号开始发送,可能也是别的数字,但是一定是>=确认序号的!这点我们先不做过多理解,知道有这么一回事即可!
第二个问题:
如果说返回的应答丢失了几个怎么办?假设今天返回的应答报文丢失了确认序号为201、301的两个,发送方只收到101和401,怎么判断呢?
只需要紧紧抓住确认序号的定义:代表着该序号之前的所有报文信息都已经接收成功!
收到101 -> 101以前的都成功。
收到401 -> 401以前的都成功。
->在发送方看来,收到401就直接默认401以前都成功了,哪怕201和301丢失了,但是因为收到了401,所以还是能认为401以前的所有都接收成功!
如果发送方的几个报文丢失了怎么办?如丢失了200、300,接受方只收到100和400?
对于接收方来说,收到100,返回101!但是,因为200和300丢失,,是不能直接跳过未收到的报文,从而直接发送401回去的!
因为确认序号代表该序号之前的所有报文信息都已经接收成功! 200和300都没接收到,这怎么能够发401回去呢?不符合确认序号的定义了!
但是,对于应答丢失来说,如果能够返回401,这一定能够说明:
200和300接收方一定收到了,只不过应答丢失了!所以,收到401确实是可以认为401之前的全部接收成功!这是接收方和发送方的区别!
确认号和确认序号的意义
现在已经初步认识了确认号和确认序号了,但是,还有一些问题:
1.为什么要同时存在两个这样的字段?通过上面的场景来看,直接用一个字段不就好了?
2.这个确认号和确认序号的意义到底是什么?
现在,让我们一起来解释这两个问题!
问题一:
从上面的场景来看,确实是只需要一个字段来表示序号即可!但是,上面的场景是单一的。
假设客户端向服务器发送报文,有没有这么一种可能:
今天的服务器在返回应答的同时,也需要携带对应的信息发送给客户端做应答呢?
举个更通俗的例子:
A和B打电话:
A:B你吃了没(A向B发送信息)
B:A我吃了(对A的信息应答),我吃的饺子,你吃的什么(同时携带信息,需要A应答)
A:我吃的xxx(对B的携带信息进行应答),你在家吗(再向B发送信息)…
…
上面的过程就很生动形象的展示了TCP通信双方的,可能在应答中携带信息的情况!
所以,通信的双方都可能称为发送方或者应答方,所以,是需要两个字段来标识序号的!
问题二:
我们可以发现,上述的过程中,序号和确认序号做到了三点:
1.让报文按序到达。
2.解决了信息传输不可靠问题(当然会有对应的机制处理丢包,这里先不说)。
3.针对于报文的乱序问题,也是可以处理的!也可以接收报文在并发发送的乱序问题!
捎带应答
这里只需要理解刚刚上面这一种情况,就能理解捎带应答!
即今天的服务器在返回应答的同时,也需要携带对应的信息发送给客户端做应答呢?
当一方做应答的时候,又想携带信息让接收方做应答,这种情况就是捎带应答:
如上图所示!
TCP中其他字段的理解
我们现在已经了解了TCP报头中的若干字段:
源端口号、目的端口号、序号、确认序号、4位首部长度。
当然,TCP报头中还有一些字段是需要我们进行了解的,下面我们一起来看看。
当然,我们这里只是进行简单的理解,具体操作我们放在后面讲解。
16位窗口大小
我们先来理解第一个,就是TCP报头中的16位窗口大小。
TCP协议的通信现在是否存在一个情况:
就是说一方发过去的报文,但是接收方的接收缓冲区已经满了!那么,这个时候收方也没办法接收该报文,只能把它丢弃了!
但是,TCP是一个可靠的协议,可以做重传(具体的重传逻辑我们先弱化)。
上面都是我们过往对TCP的理解,不需要过多解释。但是,一个报文,需要从一台主机通过网络发送到另一台主机,是需要经过各种操作、转发、消费算例、网络带宽。结果到最后送给对方主机的时候还是被丢弃了!这真的合理吗?
答案是不合理!在网络的通信中是希望能尽量避免这种情况的出现的!
如何解决呢?
->如果说,发送方能够知道对方是否能够接收我们发送过去的报文,不就可以解决这个问题?
在TCP的报头中,16位窗口大小,就是来表示当前接收缓冲区的大小的,但是,是表示自己的,还是表示对方的呢?
答案必然是自己的!因为具体缓冲区内还有多少空间,只有自己知道。同时,我们告诉对方当前我方的剩余空间,对方就可以根据这个空间做流量控制!
即发送方的发送速度需要参考对方的接收窗口大小,从而进行合理的流量控制!
16位窗口大小的意义
所以,通过接收对方发来的报文,拿出对方16位窗口大小的字段的值,就可以在发送之前知道对方的接受能力了!然后通过合理的流量控制算法进行控制。
标志位
在TCP的报头中,存在着这么几个字段:
这些就是标志位!它们分别是干什么的呢?
标志位的本质
首先,我们需要搞清楚标志位的本质是什么:
其实标志位就是在TCP报头中特定位置的一个比特位罢了!其中有6位是被保留起来的(这个部分不做了解)!剩下的6个需要我们理解。
有时候我们可以在一些教材/资料上看到这样的说法:
A向B发送xxx标志位,B向A返回xxx标志位
。
在今天的我们来看:
我们需要丢弃这种说法。始终要记住:在TCP的通信中,最少也得是一个报文。
所谓向对方发送xxx标志位,其实就是发送报文的时候,在对应的比特位上置1罢了!
我们也可以从tcp结构体中一探究竟:
struct tcphdr {__be16 source; // 源端口__be16 dest; // 目的端口__be32 seq; // 序列号__be32 ack_seq; // 确认号
#if defined(__LITTLE_ENDIAN_BITFIELD)__u16 res1:4, // 保留位doff:4, // 数据偏移(头部长度)fin:1, // FIN 标志syn:1, // SYN 标志rst:1, // RST 标志psh:1, // PSH 标志ack:1, // ACK 标志urg:1, // URG 标志ece:1, // ECN-Echo (RFC 3168)cwr:1; // Congestion Window Reduced
#elif defined(__BIG_ENDIAN_BITFIELD)// 大端序下的相同定义(位域顺序相反)
#else
#error "Adjust your <asm/byteorder.h> defines"
#endif__be16 window; // 窗口大小__sum16 check; // 校验和__be16 urg_ptr; // 紧急指针(URG=1时有效)
};
标志位的意义
TCP报头中,每个字段存在都是有它对应的意义的。我们先不谈单一的字段的作用,我们就从宏观角度上来理解,即TCP报头中设置这个标志位的意义是什么?有什么作用?
我们就以服务器为例子:
服务器是和客户端连接使用的。那么,在服务器中,肯定存在着一系列的报文!
但是,所有的报文都是请求同一个操作的吗?
↓
有没有可能:有些报文请求的就是连接、有些请求的是断开连接、甚至还有各种各样的,不同种类的报文?答案是必然的,肯定有。
所以,接受方肯定会接收到不同种类的报文,而针对于不同种类的报文是有不同的做法的!
那么,对于接收方来讲,首要任务就是要识别出报文的种类 -> 需要对应的标志位来标识!
所以,在TCP的报头中,标志位(其实就是特定字段中的一个比特位,不同的比特位对应的标志位是不同的!),就用来标识报文的种类!
以SYN + ACK标志位简单理解TCP连接三次握手
ACK(ACKNOWLEDGEMENT): 确认号是否有效
SYN(SYNCHRONOUS): 请求建立连接; 我们把携带SYN标识的称为同步报文段
我们需要先简单讲一下TCP建立连接的三次握手的过程,以便于理解上述的两个标志位:
假设以客户端向服务器发送连接请求为例:
首先,SYN是同步标志位,我们目前可以简单理解为这是用来发起连接请求的标志位即可!
ACK就是确认应答的标志位!在该位置设置1,代表接收方已经收到了报文,保证历史发送信息的可靠性。
我们现在需要转而理解:为什么需要进行三次握手?
首先,第一次C向S发送报文,带有SYN标志位,即申请连接。然后S->C返回ACK应答,这是确保C->S的发送的连接请求是可靠的!
但是,TCP是全双工,双方都要进行连接,所以S->C发送ACK的同时,可以带上SYN(捎带应答),这样子不仅是保证历史数据可靠,也是向对方发送请求。
然后C还得向S发送一个带有ACK标志位的报文!因为要保证S->C的信息是可靠的!
所以,基于确认应答机制的理解:
通过这三次握手,就已经是在协商SC双方通信的意愿了!
只不过是:在三次握手阶段,因为双方没有完全建立好连接的共识,所以双方发送的都是报头,即不带正文数据的!
这里还可以提出一个小问题:
当建立连接成功后,第一次向对方发送带有数据的报文的,怎么知道对方接收缓冲区大小呢?流量控制怎么做?
答案其实很简单:第一次发送数据 = 第一次发送报文!
其实早在双方三次握手期间,就可以在16位窗口大小的字段上设值,然后双方就可以在正式通信之前来协商双方的接受能力了!
其余的细节我们将在后续的TCP连接管理部分进行讲解!
以FIN标志位简单理解TCP断连四次挥手
FIN(FINISH): 通知对方, 本端要关闭了, 我们称携带 FIN 标识的为结束报文段
比如下图客户端和服务器断开连接四次挥手示意图所示:
还是一样的,通过上述的四次挥手:
就能完成客户端和服务器之间双方之间的通信连接关闭了(TCP是全双工,需要两边进行关闭)。我们现在只需要知道FIN标志位是用来标志断开连接报文的即可!
其余的细节和上述的三次握手一样,我们讲放在后续的TCP连接管理进行说明。
PSH标志位
PSH(PUSH): 提示接收端应用程序立刻从 TCP 缓冲区把数据读走
这个是什么意思呢?
假设今天C->S发送了报文,但是接收方可能因为上层应用层比较忙,还来不及从缓冲区中读取相应的数据。所以就会不断地挤压在接收方的缓冲区内!
但是,有一些服务,是需要服务器进行快速相应的。所以,TCP的协议就规定了:
当包头中的PSH位被设为的时候,接收方就需要尽快的把数据从接收缓冲区中读到上层去。
举一个场景进行理解:
比如我们通过xshell登录的Linux云服务器主机,我们输入的命令,其实最终都是通过网络发送给远端的主机,然后执行完后再返回给我们的!
而我们操作这台主机,正常情况下肯定是希望输入的命令能够快速被执行的!所以,我们在xshell上输入的命令,形成报文发送到远端的时候,就有可能设置了PSH标志位!
RST标志位
RST(RESET): 对方要求重新建立连接; 我们把携带 RST 标识的称为复位报文段
我们会发现,当在学习网络的时候,画双边通信流程图的时候,总是习惯性把报文传输斜着画过去到对方那里!这是为什么呢?
答案:因为省略了一个要素,即报文传输是有时间的!从上往下,时间是在不断地后移。
我们就以双方进行三次握手为例子:
对于主动发起连接的一方来说:它只要能够收到对方的应答并且把自己的的应答发送出去,对于这一方来说,就可以认为自己建立连接成功了!
但是对于被动回应连接的一方,它需要等对方返回最后一次(第三次挥手)的应答成功接收后,才能认为这一方是建立连接成功!
👇
上述想说的是:
通信的双方建立连接成功的时间是有偏差的!即对连接是否成功建立的认知不一致!
上述讲那么多,是想说明什么呢?先不急,很快就出结论了:
当通信的双方建立连接的时候,是一定能成功的吗?不一定!
还是有一定的概率会建立失败的!
那么,有没有一种可能:
在上述的情况下(我们就假设主动发起连接的是Client,被动的是Server):
Client端认为连接建立好了!就会发送数据过去!我们知道,建立连接的过程中不能带数据!
所以,在Server端看来:
我都没成功建立连接啊,怎么对面发来数据了呢?所以,Server端就会在报文中的ACK位置设1,同时,RST位也设为1。等到Client看到RST=1的时候就知道,对方没建立成功,需要重新执行连接过程,就重新进行三次握手了!
反过来服务器主动请求连接也是一样的道理,这里就不再过多的赘述了!
16位紧急指针 && URG标志位
URG: 紧急指针是否有效
URG为1,紧急指针有效,反之无效!
那么在了解URG标志位之前,我们需要先了解什么是紧急指针!
在今天的我们看来,所谓的接收缓冲区和发送缓冲区,其实就是一个字符数组!这个数组容量比较大而已。当然,我们也可以把这么一个缓冲区当成字符队列:
从头出数据,从尾入数据!这就类似于一个队列。队列最大的特点就是有序!可以保证先到先得的特性。但是,过于追求公平性也不是什么好事!
在我们的生活中,有一些人是一定会享受到特权的:
比如排队的时候:军人优先、老人优先
急诊治疗的时候:严重者优先
所以,在TCP报头中有这么一个字段:16位紧急指针:
它表示的是:当前有效载荷中,特定偏移量的位置是紧急数据,需要优先处理!
也就是说,当接收到的报文发现,URG被设置为1,就表明紧急指针有效。这个时候去可以读取出紧急指针的值,得到一个有效载荷的偏移量。就这一个字节是紧急数据,需要“特权”!!
比如这一个字节可以是设置一个状态码!
当然,也可能是在一个网络下载任务中,本来缓冲区内存的是一系列的下载的数据!如果没有特权,那么我们后面想要暂停的时候,这个暂停信息只能在下载数据后面排队!
这是有点好笑了,我们要停止下载,但是还得下载完才能停止。这还不如不按停止!
所以,这时候就可以采用紧急指针的做法,把对应的停止、结束等信息存放在紧急指针指向的位置!这样子系统接收到后就会优先处理了!
当然实际上,也有别的方案来解决这个问题。这里只是提供一种思路。
带外数据——out-of-band:
当然,在系统中,紧急数据也被称为带外数据!即out-of-band。
在接口send和recv的flags选项中,有这么一个选项:
MSG_OOBThis flag requests receipt of out-of-band data that would not be received in the normal datastream. Some protocols place expedited data at the head of the normal data queue, and thus this flag cannot be used with such protocols.
意思就是:
可以在flags中设置对应的选项,使用send可以发送带外数据!使用recv可以接收带外数据!
所谓的带外数据,其实就是让这个数据绕靠正常的字节流!即不会在正常数据的字节流出现!
当然,现在来说,URG这个标志位用的其实比较少!了解一下即可。