文章目录
一、TCP的概念
百度百科:传输控制协议(TCP,Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由IETF的RFC 793 [1] 定义。
二、TCP格式框架
下面我们先来看一下TCP协议端格式框架:
1. 两个问题的解决
相看两不厌,唯有敬亭山。有两句不得不提又一直在提的话,那就是,任何一层协议,都应该解决两个问题:
1.报头和有效载荷进行分离的问题
2.将自己的有效载荷交付给上层的问题
4位首部长度:表示TCP头部有多少个32bit即有多少个4字节。此处的长度为无符号长度,长度的基本单位是4字节,4位首部长度是4个比特位,范围为0000 ~ 1111,所以长度范围就是0 * 4(字节) ~ 15 * 4(字节),即0个4字节到15个4字节。报头的最大长度是60字节,但是实际上报头至少要为20字节,则选项最大为40字节,4位首部长度最小应填0101。
解决报头和有效载荷的分离问题:
首先直接读取20字节,因为20字节为报头的最小长度,此时便可以得到4位首部长度字段所表示的二进制数,再读取用该数值减去20的字节数,这便是选项的大小,当然,选项的大小也可以为0。此时报头便被读完。
解决将有效载荷交付给上层的问题:
请仔细观察审视一下我们的TCP协议格式框架,它比UDP协议多了不少字段,但好像也少了点什么东西,正确的,TCP协议格式框架中并没有TCP总长度,只有4位首部长度,这是因为TCP根本不需要数据总长度。TCP并不需要按数据块对上层进行交付,由于TCP协议是面向字节流的缘故,当TCP收到数据时,它只需要将所有的数据按序放在缓冲区里,上层是如何读取数据的TCP并不染指。
16位校验和:不知疲倦的TCP,在脱离了定长的束缚后,又该如何保证它是完整的呢。我们需要字段里16位校验和发挥了作用,16位校验和由发送端填充,采用CRC校验(循环冗余校验Cyclic Redundancy Check),接收端校验不通过,则认为数据有问题。此处的校验和不光包含TCP首部,也包含TCP数据部分。
2. 可靠性
TCP是可靠的,它的可靠性体现在以下几个方面。
2.1 确认应答
TCP基于确认应答机制,所谓的确认应答机制就是:在TCP中,当发送端的数据到达接受主机时,接受端主机会返回一个已收到消息的通知。这个消息叫做确认应答(ACK)。
2.1.1 绝对可靠性和相对可靠性
我们知道TCP是可靠的,可靠性分绝对可靠性和相对可靠性,但是是否真的存在绝对可靠性还有待商榷,在这里我们认为没有。
保证可靠性最核心的思想:只要收到了对应的应答,就认为之前的数据对方已经收到。
举个简单的例子:
在上图中,客户端向服务器发送数据,服务器为了向客户端证明自己已经收到数据,需要向客户端发送ACK1(Acknowledge character确认字符,这里的“1”为了方便说明用来表示次数),客户端接受到ACK1后,如果要告诉服务器自己已经收到了ACK1,客户端就也要向服务器发送一个ACK2,证明自己在接收到服务器第一次响应后,已经知道了自己所发数据被接收,可是这样一来,服务器又要向客户端证明自己得到了ACK2这个确认字符,于是服务器又要向客户端发送ACK3…似乎要永无止境的循环下去,这样是绝对不可取的,所以并没有所谓的绝对可靠性,因为最后一条ACK不具备可靠性,我们也不需要对ACK进行ACK。
ACK是又可能丢失的,如果2.1.1图中的ACK1丢失了(认为ACK1不携带任何数据),那么服务器是需要重传的。在超时重传(下面会介绍)这一机制下,客户端只要没有收到ACK,无论实际上服务器有没有收到数据,客户端都会认为自己发送的数据丢失,经过一段时间后,客户端会对数据进行重传,一旦重传,服务器也就意识到自己所发的ACK丢失了。
总之,确认应答机制并不是对ACK保证可靠性,而是通过ACK保证之前发送的信息是可靠的。
2.2 超时重传
2.2.1 超时重传第一种情况
在数据的传输过程中,发送方发送的数据可能会因为网路拥堵等原因发生丢包,此时发送方并不能知道自己所发送的数据丢失了,在经过特定的时间间隔且发送方没有收到确认后,发送方将会进行超时重传。
2.2.2 超时重传第二种情况
即使发送方成功发送了数据,但是接受方所返回的ACK确认也是有可能丢失的,发送方依旧接收不到确认,客户端在经过特定时间间隔且发送方没有收到确认后,发送方将会进行超时重传。
2.2.3 超时重传时间
那么,如果超时的时间如何确定?
最理想的情况下,找到一个最小的时间,保证 “确认应答一定能在这个时间内返回”。
- 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
- 如果超时时间设的太长, 会影响整体的重传效率;
- 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
- 超时以500ms为一个单位进行控制,每次判定超时重发的超时,时间都是500ms的整数倍。
- 如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传。
- 如果仍然得不到应答,等待 4*500ms 进行重传,依次类推,以指数形式递增。
- 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
小结:超时重传很好的解决了丢包问题,可是这样一来,接收方就有很大可能接收到许多重复的数据。去重这个问题该何去何从,与我们下面要讲的按序到达有关,因为只需要给数据标识相应的序号,一旦序号相同就将数据包丢弃,这样就可以达到去重效果。
2.3 按序到达
发送方有时并不只是发送一个数据段,也有可能发送多个数据段,由于数据的传输可能选择多条路径,所以接收到的顺序和发送的顺序有可能会有不同。TCP是可靠的,它对于别人给它发送的数据段,会予以反馈,证明自己收到了数据段,由于32位序号和32位确认序号的存在,它还能保证数据段的按序到达。
(发送方)32位序号:Client->Server
(接收方)32位确认序号:Server->Client
假如发送方传了三条消息,序号分别是3、4、5,那么接收方应该传的确认序号应该对应为4、5、6,即发送方的序号加1。当然,携带确认序号的ACK在传输中因为网络堵塞等原因是有可能丢失的,在这个例子中,假如确认序号的4、5都丢失了,发送方最终只收到了6,接收方还需不需要重发一下4、5的确认序号呢?答案是不需要,只要收到了6,就说明前面所发的信息都收到了,每一个ACK都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据,下一次你从哪里发。其次,序号可以用来去重。
有一个会被人忽略的问题。TCP协议格式中同时存在32位序号和32位确认序号,但是当作为发送方时,对方在意的是我们的序号;当作为接收方时,对方在意的是我们的确认序号。似乎TCP只需要一组序号就可以,但事实并非如此,由于TCP是全双工的,既可以发送消息也可以接收消息,假如在我们发送消息的同时,对方也在给我们发送消息,此时我们既作为发送方又作为接收方,就需要同时具有序号和确认序号。
总结一下,32位序号和32位确认序号,既能保证按序到达,又能保证确认应答。
3. 其余字段
16位窗口大小:16位窗口大小指发送方的接收缓冲区当中剩余空间的大小。不像UDP只具有接收缓冲区,不具有发送缓冲区,TCP同时具有发送缓冲区和接受缓冲区。在发送方所发数据丢失或接收方ACK丢失后,发送方需要进行重传,即使TCP的重传机制已经完备,但重传的过程还是占用了网络资源。为了减小资源浪费,我们在传输数据段的时候需要让接收方知道我们的剩余空间大小,这里会出现一个概念,叫做流量控制,流量控制便是通过窗口大小完成的。解决流量控制要求TCP报头当中填的是我自己的接收缓冲区中剩余空间的大小,所以当我们实际通信时,我们就知道接收方接收窗口大小,根据接收窗口大小我们可以控制自己所发数据的速度,以防止丢包。
TCP三次握手时发送和接受双方会得知其窗口大小,我们在后面会详细介绍TCP的三次握手。
6个标志位:
- SYN:请求建立连接,把携带SYN标识的称为同步报文段
- FIN:断开连接标志位,通知对方本端要关闭,把携带FIN标识的称为结束报文段
- ACK:确认字符
- PSH(Push):催促缓冲区数据的处理,提示接收端应用程序立刻从TCP缓冲区把数据带走。
- RST(Reset):连接重置,当客户端和服务器之间连接没有建立成功且客户端依旧向服务器传输数据时,服务器会向客户端传输一个RST,与客户端重新进行三次握手建立连接。
- URG:检验紧急指针、带外数据(快速数据),可以打破按序到达,有紧急指针时设置为1,大部分情况设置为0。
16位紧急指针:一个偏移量,一次只能传输一个字节。
三、TCP连接机制
1.如何理解连接:
连接本身是有成本的:空间+时间
2.TCP面向连接:
面向连接类似于电话系统,在开始通信前必须先进行一次呼叫和应答。TCP三次握手建立连接成功后,客户端和服务器都要维护连接。
四、TCP三次握手
1.为什么一定是三次握手
为什么一定是三次握手,为什么一次握手和两次握手不行?
1.一次和两次握手容易遭受SYN洪水攻击:假如是一次握手,即客户端向服务器发送完SYN后连接直接建立,这样似乎和UDP没什么不同。重要的是,建立连接便会占用服务器的部分资源,如果有不怀好意的人大量发送SYN请求那么服务器资源将会被占用。两次握手和一次握手并没本质区别,原因在于当服务器在接收到客户端建立连接的请求后便会向其发送ACK,在发送ACK的一瞬间服务器便会认为连接已经建立,即便客户端可能没有收到ACK,由此可见,两次握手和一次握手同样容易遭受到SYN洪水攻击。
2.用最小成本验证全双工
3.让服务器不要出现连接建立的误判情况,减少服务器的资源浪费:在客户端和服务器这个层面,服务器显然是比客户端重要的多的,因为服务器是一对多的,不能浪费服务器的资源。客户端和服务器谁发起最后一次ACK,谁就要建立连接,谁就会认为连接建立好,此时客户端应付出一定的牺牲,即最后一次ACK应该是由客户端发向服务器的。当然,五次握手和七次握手等等都可以有效的建立连接,但是浪费了资源,所以我们选择三次握手。
五、 TCP浪漫的四次挥手
四次挥手原因:TCP是全双工的,要求双方达成关闭链接的共识。
将客户端的TCP层和服务器端的TCP层看成两个感情已经破裂但仍驻足良久不愿离去的恋人,TCP是全双工的,如果向挥手那么双方都要进行挥手并达成共识才能彻底结束。真正的结束,不仅是客服端对服务器发送信息的信道要断开,服务器向客户端发送信息的信道也要断开。如果必须要说再见,那么客户端首先发送断开连接的请求FIN,此时客户端断开连接,只是单纯的告诉服务器,我不想和你通信了,我不会主动给你发消息了,于是服务器进入了CLOSE_WAIT状态即等待终结,等待关闭的那么一个状态。服务器也要做到洒脱,于是它回应了一个ACK。等到客户端接收到ACK后,客户端进入FIN_WAIT_2状态,它也在等待。但通信毕竟是双方的事,服务器还有感情,也要负责,它也要做出断开连接的决定。此时发送完ACK的服务器正处于LAST_ACK的状态,并也准备发起FIN,告诉客户端,我对你的信道要关闭了。ACK是数据吗?当然是,即便在第一次SYN中,客户端已经通知服务器我不会再给你发送任何数据了,但客户端在服务器的SYN下还是发送了最后的ACK,发送完ACK的客户端进入TIME_WAIT状态,似乎还在等待。最后,接收到客户端ACK的服务器彻底死心,进入CLOSED状态。客户端进行一段时间的等待后也彻底CLOSED。
但是吧,无论是客户端还是服务器向对方发送的FIN,所谓的信道关闭,只是说在应用层的层面上,不会再有信息传递了,传输层用于通信的ACK还是可以传递的。
1.不同时刻的状态
CLOSE_WAIT状态:若服务器挂满了大量CLOSE_WAIT链接,说明服务器本身代码存在问题,如服务器端上层没有关闭状态描述符。
TIME_WAIT状态:要求主动断开链接的一方要进行等待。等待的意义在于客户端无法保证最后一次发出的ACK有无被服务器正常收到,若服务器未正常收到ACK,则可能进行FIN的重传,重传若干次也将会导致链接关闭,因为这时可能网络本身存在问题;当然,在客户端等待的时间服务器可能已经收到了ACK并进行了关闭,但我们应优先考虑服务器的利益,尽量保证最后一个ACK被对方收到,进而尽快释放服务器的资源。其次,TIME_WAIT可以等待历史数据在网络上进行消散。
简单来说:1.尽快释放服务器的资源 2.等待历史传输数据在网络上消散。
TIME_WAIT时间:2MSL(Maximum Segment Lifetime 可译为“最长报文段寿命”,它是任何报文在网络上存在的最长的最长时间,超过这个时间报文将被丢弃)
六、滑动窗口
1. 主题引入
主机A发送一个消息给主机B,主机返回一个ACK,这样的通信方式叫做串行化通信,这样一发一收会导致效率比较低。我们需要解决这个问题。
因为一发一收的效率比较低,所以我们可以一次性传输多条数据,将多个段的等待时间重叠在一起,可以提高效率。同时这样的做法还可以允许一定的丢包,即少量丢包是被允许的,原因是假如发送方向对方发送了的序号为1、2、3号的数据,确认序号为2、3的ACK丢失,得到的ACK确认序号为4,也能说明发送方的1、2、3数据被成功接收。
既然一次可以传输多条数据,为什么不直接把数据一次全部交给对方呢?原因是我们需要考虑对方的接收能力。这就要知道对方16位接收窗口的大小,实际上,在完成三次握手建立连接的时候,客户端和服务器就已经知道了对方接收窗口的大小。
2. 概念及理解
我们需要知道,滑动窗口和TCP协议段中的16位的窗口大小并不是同一个概念。操作系统内核为了维护滑动窗口,需要开辟发送缓冲区来记录当前还有哪些数据没有应答,只有确认应答过的数据,才能从缓冲区删掉。滑动窗口可认为是发送缓冲区的一部分,滑动窗口的大小为接收方16位窗口大小和拥塞窗口(下文会提)取小。滑动窗口的大小是会改变的,滑动窗口越大,则网络的吞吐率即传输效率越高。
滑动窗口也可理解为一个数组,左窗口的下标为left指针,右窗口的下标为right指针。发送缓冲区则相当于一个环形队列,滑动窗口是其中的一段区间,left指针和right指针确定了它的范围。
如图,假如我们仅仅收到了确认序号位4001的ACK,但是没有收到2001、3001的ACK,那也能证明4001之前的数据被我们接收到了,滑动窗口向右移动即可。
当上层一直向发送缓冲区写入数据,发送缓冲区是会满的。一旦发送缓冲区满了,上层send()时会堵塞;当接收时,如果接收缓冲区没数据,上层send()时也将堵塞。
3. 丢包及重传问题
3.1 情况一:ACK丢失
数据包已经到达,ACK丢失。
这种情况下,部分ACK丢了不要紧,可以通过后续的ACK进行确认。
3.1 情况二:数据包丢失,快重传
数据包在传输过程中丢失了。
如图:
- 当某一段报文段丢失之后,发送端会一直收到1001这样的ACK,就像是在提醒发送端“我想要的是1001”一样。
- 如果发送端主机连续三次收到了同样一个“1001”这样的应答,就会将对应的数据1001 - 2000重新发送。
- 这个时候接收端收到了1001之后,再次返回的ACK就是7001了,因为2001 - 7000接收端之前就已经收到,被放到了接收端操作系统内核的接收缓冲区。
假如在主机A向主机B传输数据的过程,不慎有数据丢失了,那么主机B将会进行重复的ACK。当主机A接收到大量重复的ACK时,会对此进行处理,进行重传。重传机制完备,在协议中,连续收到3次重复的确认应答将会进行重传,这种机制叫做快重传,也叫高速重发控制。当然,即使没有这个机制,在超时重传的作用下,主机A在经过特定的时间后,它也将进行重传。由于滑动窗口大小的限制,超时重传不会被快重传所替代。
七、流量控制
1. 相关概念
接收端处理数据的速度是有限的,如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等一系列连锁反应。
因此TCP支持根据端的处理能力来决定发送端的发送速度,这个机制就叫做流量控制(Flow Control)。
2. 具体实现
- 接收端将自己可以接收的缓冲区大小放入TCP首部中的16位窗口大小字段,通过ACK端通知发送端。
- 窗口大小字段越大,说明网络的吞吐量越高。
- 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端。
- 发送端接收到这个窗口之后,就会减慢自己的发送速度。
- 如果接收端缓冲区满了,就会将窗口大小置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
第一次主机A和主机B通过三次握手协商好窗口大小。发送方给接受方发送数据,当接受方的缓冲区满时,发送方需要进行等待,此时上层的行为取决于缓冲区是否有足够空间,只要有空间上层就有可能继续向缓冲区传输数据,因为上层并不关心通信细节。
2.1 窗口探测及窗口更新通知
当接收方返回的窗口大小为0时,发送方滑动窗口也会为0,此时发送方将进入等待。但是发送方怎么知道接受方的窗口大小一段时间后有没有更新呢?能不能继续传输数据呢?这时发送方就会给接收方发送窗口探测报文(不携带数据),但是定期的探测可能会浪费时间,比如发送方每隔1秒发送一次窗口探测报文,但是接收方0.1秒就已经更新好了,所以只靠单向的努力是不稳妥的。接收方最清楚何时能够接收数据,当其可以接收数据时,接收方应向发送方发送窗口更新通知。以上属于通信细节,由内核自动完成。
八、 拥塞控制
1. 什么是拥塞控制
有了TCP的窗口控制,收发主机之间即使不再以一个数据段为单位发送确认应答,也能够连续发送大量数据包。然而,如果在通信刚开始时就发送大量数据,也可能会引发其他问题。
一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。在网络出现拥堵时,如果突然发送一个较大量的数据,极有可能会导致整个网络的瘫痪(大家使用的都是TCP协议,网络状态不好时,大家一起丢包,然后一起进行重发,这样就会在短时间内,导致局部网络阻塞状态加剧)。
TCP为了防止该问题的出现,在通信一开始时就会通过一个叫做慢启动的算法得出的数值,对发送数据量进行控制。
为了在发送端调节所要发送数据的量,定义了一个叫做“拥塞窗口”的概念。
2. 怎么判断是网络问题
假如发送1万个数据丢失几个报文,这是正常的丢包现象,如果丢失了一半的报文,这就是网络的问题了。
3. 慢启动
流量控制是通信双方控制,因为是发送端根据接收方的能力来决定发送数据的大小。
拥塞控制是多人一起控制,在一定的区域内,大家共享网络,并且底层用的都是TCP/IP协议,网络状态不好时,大家一起减少数据的发送,减轻网络的负担。
此处引入一个概念称为拥塞窗口,发送开始的时候,定义拥塞窗口为1,每次收到一个ACK应答,拥塞窗口加1,每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口,否则仍会引起缓冲区的拥塞。
如图,主机A第一次先传输1000个数据,等到收到ACK时第二次再传输2000个,第三次发送4000个…指数增长,但并不能一直增长下去。
上面这样的拥塞窗口增长速度,是指数级别的。“慢启动”只是只初始时慢,但是增长速度非常快。
- 为了不增长的那么快,不能使拥塞窗口单纯的加倍。
- 当TCP开始启动时,慢启动阈值等于窗口最大值
- 在每次超时重传的时候,慢启动阈值会变成原来的一般,同时拥塞窗口置为1
- 当拥塞窗口超过阈值的时候,不再按照指数方式增长,而是按照线性方式增长。
九、 延迟应答
如果不考虑拥塞窗口,发送端给接受端发消息的多少取决于接收端的接收能力。如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小。
- 假设接收端缓冲区为1M,一次收到了500K的数据; 如果立刻应答,返回的窗口就是500K。
- 但实际上可能处理端处理的速度很快,10ms之内就把500K数据从缓冲区消费掉了。
- 在这种情况下, 接收端处理还远没有达到自己的极限,即使窗口再放大一些, 也能处理过来。
- 如果接收端稍微等一会再应答,比如等待200ms再应答,那么这个时候返回的窗口大小就是1M。
由于数量限制和时间限制,并不是所有的包都可以延迟应答,具体的数量和时间,依操作系统不同也有差异,一般数量为2,时间为200ms。
十、 捎带应答
在延迟应答的基础上,我们发现,很多情况下客户端和服务器在应用层也是“一发一收”的,这意味着,比如客户端给服务器说一句“见到你很高兴”,服务器也会给客户端发送一个“我也是”。那么这时ACK就可以搭顺风车,和服务器回应的“我也是”一起回复给客户端。
简单来说,捎带应答就是将发送数据的ACK部分由0置为1,然后再携带上正文部分。从而将两条数据(一条为ACK,一条为正文)合并为一条数据(ACK+正文)。
十一、 面向字节流
因为发送缓冲区和接收缓冲区的存在,TCP的通信方式叫做面向字节流。由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
- 写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节。
- 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read100个字节,也可以一次read一个字节,重复100次。
十二、 粘包问题
粘包问题所谓的“包”,是指应用层的数据包。所谓的粘包问题,因为TCP是面向字节流的,假如我们直接在缓冲区传输两个request,在应用层必须将request一个一个读取并处理,必须保证读取的request是完整的,万一多读或者少读,都会影响各个请求的完整性。
为了避免粘包问题,需要明确两个数据包之间的边界。这个问题是应用层解决的,HTTP的空行和Content-Length,本质上就是为了在应用层明确报文与报文之间的边界。对于定长的包,保证每次都按固定大小读取即可,对于变长的包,一方面可以在包头的位置约定一个包总长度的字段,另一方面可以在包和包之间使用明确的分隔符,如空行等。
十三、 TCP异常情况
文件的生命周期随进程。
1.进程终止: 进程终止会释放文件描述符,仍然可以发送FIN。和正常关闭没有什么区别。
2.机器重启: 和进程终止的情况相同。
3.机器掉电/网线断开: 接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset。即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在。如果对方不在,也会把连接释放。
十四、 TCP总结
1. 可靠性与提高效率
为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能。
可靠性:
- 校验和
- 序列号
- 确认应答
- 超时重发
- 连接管理
- 流量控制
- 拥塞控制
提高性能:
- 滑动窗口
- 快速重传
- 延迟应答
- 捎带应答
其他:
- 定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)。
2. 基于TCP应用层协议
- HTTP
- HTTPS
- SSH
- Telnet
- SMTP
- FTP