TCP,即Transmission Control Protocol,传输控制协议。能够在可靠性和效率方面对数据的传输进行一个详细的控制。
TCP对数据传输提供的管控机制,主要体现在两个方面:可靠性和效率。在保证数据可靠传输的前提下,尽可能的提高数据传输效率。
确认序号ACK:自该序号之前,前面的数据都已经发送完毕!
TCP将每个字节的数据都进行编号,即为序列号。每一个ACK都带有对应的确认序列号,告诉发送方,我已经收到了哪些数据,下一次你从哪里开始发送。
这两种丢包情况在特定的时间间隔内主机A都会接收不到来自对方的ACK,就会进行重发,这时主机B就会收到很多重复的数据包,那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。它是怎么做到的呢?
利用序列号去重。接收方接收到的数据会先放在内核的“接收缓冲区”中,根据序列号进行判断,此时在应用程序中读取出来的数据就是不重复的。
※ 如果ACK返回超时,这个时间如何确定?
最理想的情况是设置一个最小时间,保证“确认应答一定能在这个时间段返回”;但是随着网络的差异,设置是有差异的;如果超时时间设定太长,会影响整体的传输效率;如果太短,会频发发送重复的包。
TCP为了保证在任何环境下都能有比较高性能的通信,因此会动态计算这个最大超时时间。
Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。如果重发一次之后,仍然得不到应答,等待 2500ms 后再进行重传。如果仍然得不到应答,等待 4500ms 进行重传。依次类推,以指数形式递增(让重试的频率尽量降低)。累计到一定的重传次数,TCP认为网络或者对端主机出现异常,会触发RST复位报文段,尝试重置连接;之后放弃连接强制关闭,最后对资源进行回收。
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接。
三次握手是客户端向服务器发送请求建立连接;
四次挥手是客户端和服务器都可以发送请求断开连接。
服务器状态转换:
[CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态,等待客户端连接;
[LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段),就将该连接放入内核等待队列中,并向客户端发送SYN确认报文。
[SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文,就进入ESTABLISHED状态,可以进行读写数据了。
[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close),服务器会收到结束报文段,服务器返回确认报文段并进入CLOSE_WAIT;
[CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据);当服务器真正调用close关闭连接时,会向客户端发送FIN,此时服务器进入LAST_ACK状态,等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
[LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK,彻底关闭连接。
客户端状态转换:
[CLOSED -> SYN_SENT] 客户端调用connect,发送同步报文段;
[SYN_SENT -> ESTABLISHED] connect调用成功,则进入ESTABLISHED状态,开始读写数据;
[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时,向服务器发送结束报文段,同时进入FIN_WAIT_1;
[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认,则进入FIN_WAIT_2,开始等待服务器的结束报文段;
[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段,进入TIME_WAIT,并发出LAST_ACK;
[TIME_WAIT -> CLOSED] 客户端要等待一个2MSL(Max Segment Life,报文最大生存时间)的时间,才会进入CLOSED状态。
问题1:为什么要三次握手?
①确认主机A和B之间的传输是否连通,各自发送和接收能力是否正常(投石问路的过程)
②协商参数。选择传输中合适的参数(eg. TCP的序号从几开始…)
问题2:三次握手可以是四次吗?
三次握手:SYN同步报文段,正常情况下为0,在尝试建立连接的请求中SYN=1;ACK确认报文段,正常情况下为0,确认应答时ACK=1;SYN和ACK中间两次操作可以合并在一起;应答ACK和发起请求SYN是同时触发的,分为四次握手完全可以。但是没有必要!
问题3:四次挥手中的FIN和ACK为什么不是同时触发?
FIN结束报文段:FIN的触发表面上是socket.close(),实际上是内核里面释放了对应PCB的文件描述符(eg:被垃圾回收机制回收、进程结束…)
ACK和FIN不是同时触发的。服务器只要收到FIN就会立即出发ACK,这是由内核完成的。而发送FIN是由用户代码控制的(代码中出现了socket.close()时才会触发FIN,再比如代码中有延迟sleep(1000)就不同时了)。
问题4:有关TIME_WAIT状态,为什么TIME_WAIT的时间是2MSL?
MSL是TCP报文是最大生存时间,因此TIME_WAIT持续存在2MSL的话,①能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的);②同时在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失,那么服务器会再重发一个FIN。这时虽然客户端的进程不在了,但是TCP连接还在,服务器进入TIME_WAIT状态(延迟一段时间但是状态依然存在),仍然可以重发LAST_ACK);如果服务器出现大量的TIME_WAIT状态,主动发起FIN的一方会进入这个状态,需要排查服务器是否主动断开连接。
问题5:服务器出现大量的CLOSE_WAIT状态?
原因就是服务器没有正确的关闭 socket,导致四次挥手没有正确完成。这是一个 BUG。只需要加上对应的 close 即可解决问题。
上面提到的确认应答机制,对于每一个发送的数据段,都要给一个ACK确认应答,保证其可靠性;收到ACK之后呢在发送下一个数据段。但是这样有一个缺点,就是性能比较差,特别是数据往返时间较长的时候,所以我们引入滑动窗口机制提升数据传输的效率。
窗口大小指的是无需等待确认应答而可以继续发送数据的最大值(一次批量发送数据的长度)。上图的窗口大小就是400个字节(四个段)。
发送前四个段的时候,不需要等待任何ACK,直接发送;收到第一个ACK后,滑动窗口向后移动,继续发送第五个段的数据;依次类推;操作系统内核为了维护这个滑动窗口,需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答;只有确认应答过的数据,才能从缓冲区删掉;窗口越大,则网络的吞吐率就越高;传输效率越高,同时资源开销也越多。
问题:那么如果出现了丢包,如何进行重传呢?
以上两种情况:
①数据包已经到达,返回的确认ACK丢失。
这种情况下,部分ACK丢了并不要紧,可以通过后续的ACK进行确认。
②数据包就直接丢了!
接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区满了,这个时候如果发送端持续发送,就很有可能出现丢包的现象,由此引发丢包重传等一系列的连锁反应。因此TCP支持根据接收缓冲区的剩余空间大小,来动态的决定发送端的发送速度,即控制窗口大小。这个机制就叫做流量控制(Flow Control)。
那么,接收端如何把窗口大小告诉发送端呢?
TCP首部中,有一个16位窗口字段,就是存放了窗口大小信息。
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,通过ACK端通知发送端;窗口大小字段越大,说明网络的吞吐量越高;接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;发送端接受到这个窗口之后,就会减慢自己的发送速度;如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。
那么问题来了,16位数字最大表示65535,那么TCP窗口最大就是65535字节么?
实际上,TCP首部40字节选项中还包含一个窗口扩大因子M,实际窗口大小是窗口字段的值左移 M位。
虽然有了TCP滑动窗口机制确保效率,但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。当前的网络状态可能就已经比较拥堵。在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的。
TCP引入 慢启动 机制,先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据;
不丢包 --> 网络流畅 --> 增加发送速率
丢包 --> 网络拥堵 --> 减小发送速率
拥塞窗口:发送开始的时候,定义拥塞窗口大小为1;每次收到一个ACK应答,拥塞窗口加1;每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口;
拥塞窗口增长速度,是指数级别的。“慢启动” 只是指初使时慢,但是增长速度非常快。
为了不增长的那么快,因此不能使拥塞窗口单纯的加倍。此处引入一个叫做慢启动的阈值。
当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。
如果接收数据的主机立刻返回ACK应答,这时候返回的窗口可能比较小。窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。
在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 “一发一收” 的。意味着客户端给服务器说了 “How are you”,服务器也会给客户端回一个 “Fine, thank you”;
那么,这个时候ACK就可以搭顺风车,和服务器回应的 “Fine,thank you” 一起返回给客户端。这样可以提高数据传输的效率。
创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
TCP是面向字节流的数据传输协议,站在传输层的角度,TCP是一个一个报文过来的,按照序号排好序放在缓冲区中;站在应用层的角度,看到的只是一连串的字节数据,应用程序从缓冲区中取数据,不知道是从哪个部分开始到哪个部分结束;这就出现了粘包问题。这里的“包”指的是应用层的数据包。
只要是面向字节流的数据传输,都会存在类似的问题。(文件传输)
如何避免粘包问题呢? (明确两个包之间边界!)
①对于定长的包,保证每次都按固定大小读取。
②对于变长的包,可以在每个数据包前面的位置,约定一个包总长度的字段,接收方读取数据时根据长度取数据。
还可以在包之间设定明确的分隔符或结束符(自定义应用层协议),保证不和正文冲突即可。
思考:对于UDP协议来说,是否也存在 “粘包问题” 呢?
对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是一个一个把数据交付给应用层。就有很明确的数据边界。
站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收。不会出现"半个"的情况。
进程终止:进程终止会释放文件描述符,相当于调用close()方法,释放对应的PCB,触发四次挥手,但仍然可以发送FIN。和正常关闭没有什么区别。(进程终止不代表连接终止)
机器重启:和进程终止的情况相同。重启要先杀掉进程,仍然可以进行四次挥手。
机器掉电/网线断开:接收端认为连接还在,一旦接收端有写入操作,接收端发现连接已经不在了,就会进行reset。即使没有写入操作,TCP自己也内置了一个保活定时器,会定期询问对方是否还在。如果对方不在,也会把连接释放。
另外,应用层的某些协议,也有一些这样的检测机制。例如HTTP长连接中,也会定期检测对方的状态。例如QQ,在QQ断线之后,也会定期尝试重新连接。
掉电有两种情况:
①接收方掉电:发送方拿不到ACK,之后进入超时重传,几次重传之后,触发RST复位报文段,之后放弃连接,对资源回收。
②发送方掉电:此时,接收方还在尝试接收数据。采用保活机制(心跳包机制)每隔一段时间,向对方发送一个PING包,期待对方返回一个PONG包,若没有返回,说明对方已经挂了。
为什么TCP这么复杂?因为要保证可靠性,同时又尽可能的提高性能。
可靠性:校验和、 序列号(按序到达)、确认应答、超时重发、 连接管理、 流量控制、 拥塞控制
提高传输效率:滑动窗口、快速重传、 延迟应答、 捎带应答
其他:定时器(超时重传定时器,保活定时器,TIME_WAIT定时器等)
基于TCP应用层协议有哪些?
HTTP 、HTTPS、SSHTelnet、FTP、SMTP
当然,也包括自己写TCP程序时自定义的应用层协议。