TCP协议
TCP协议段格式
首先一点必须知道TCP协议三个特点:面向连接、可靠传输、面向字节流。可靠传输是有确认应答机制、超时重传机制、滑动窗口机制、拥塞避免控制机制、捎带应答机制和延时应答机制保障的,下文将详细说明。
TCP全称为 “传输控制协议(Transmission Control Protocol”). 人如其名, 要对数据的传输进行一个详细的控制;
16位源/目的端口号: 表示数据是从哪个进程来, 到哪个进程去,都是2字节,uint_16的整数;
16位校验和: 发送端填充, CRC校验. 接收端校验不通过, 则认为数据有问题. 此处的检验和不光包含TCP首部, 也
包含TCP数据部分.
16位紧急指针: 标识哪部分数据是紧急数据;
32位序号:保存的是消息发送方维护的序号;
32位确认号: 期望对端发送数据的时候从哪一个序号开始;
4位首部长度: 含义是4个比特位,最大值是15,但这并不是真实的TCP报头的长度,而是一个数值,真正的TCP报头长度 = 4位首部长度计算出来的值 * 4(单位字节) 也就是说最大值为60
6位标志位:
- URG: 紧急指针是否有效;
- ACK: 确认号是否有效;
- PSH: 提示接收端应用程序立刻从TCP缓冲区把数据读走;
- RST: 对方要求重新建立连接; 我们把携带RST标识的称为复位报文段;
- SYN: 请求建立连接; 我们把携带SYN标识的称为同步报文段;
- FIN: 通知对方, 本端要关闭了, 我们称携带FIN标识的为结束报文段;
16位窗口大小: 讲滑动窗口详细说;
40字节头部选项: 可以加上一些选项,例如MSS:最大报文段长度;
三次握手和四次挥手
三次握手须知 : 1 数据包名称 2 双方连接状态 3 包序管理;连接状态在后文讲述。
四次挥手须知 : 1 双发包名称 2 双方连接状态;连接状态在后文讲述。
在TCP连接中客户端和服务端各维护一套序号,范围都是:(0~2^32),在三次握手的时候,起始序号都是随机的;
包序管理:
- TCP连接双发总共维护两套序号,客户端一套,服务端一套;
- 客户端在发送数据的时候,消耗的是客户端维护的序号,在回复ACK的时候,是确认服务端的序号;
- 服务端在发送数据的时候,消耗的是服务端维护的序号,在回复ACK的时候,是确认客户端的序号;
- 纯ACK数据包不消耗序号。
1. 三次握手:
第一次:SYN(seq=0):首先客户端告诉服务端自己的起始序号是从0开始的,并发送给服务端一个SYN报文,并且客户端消耗掉了0;
第二次:ACK(ack=1)+SYN(seq=0):ack=1,服务端确认收到了客户端发送的SYN报文,而客户端发送SYN报文的序号为0,确认的时候告诉客户端,你维护的序号为0的数据我收到了,seq=0,并且服务端告诉客户端自己的起始序号是从0开始的,也就是消耗掉了0,下次期望你的序号从1开始发送;
第三次:ACK(ack=1,seq=1):ack=1,客户端告诉服务端确认序号为0的数据包,并且告诉接下里期望服务端给自己发送的序号从1开始。seq=1,客户端维护的序号是1,由于发送纯ACK报文不消耗序号,所以并没有消耗掉这个序号。
2. 发送数据
第一次:PSH(seq=1,ack=1),seq=1是客户端发送数据的起始序号,客户端发送一条数据为i am client,数据长度位11字节。
第二次:ACK(seq=1,ack=12),seq=1是服务端发送数据的起始序号,ack=12是因为服务端收到了客户端发送的前11个字节的数据,期望客户端下次发送数据的时候序号是从12开始的。
第三次:PSH(seq=1,ack=12),因为发送纯ACK不消耗序号,因此这次服务端给客户端发送的序号和上次一样。但是发送了数据i am server,数据长度是11。
第四次:ACK(seq=12,ack=12), seq=12是客户端给服务端回复的,ack=12是服务端希望客户端下次发送数据的序号。
3. 四次挥手
在四次挥手描述的时候,一定不要说客户端和服务端,要说主动断开连接方和被动断开连接方。
第一次:主动断开连接方发送一个断开连接报文(FIN)给被动断开连接方。
第二次:被动断开连接方发送一个确认应答报文(ACK)给主动断开连接方。
第三次:被动断开连接方发送一个断开连接报文(FIN)给主动断开连接方。
第四次:主动断开连接方发送一个确认应答报文(ACK)给被动断开连接方。
确认应答(ACK)机制
当发送方发送一个报文的时候,需要带上序号,当我们接收发收到这个报文的时候,需要对该报文进行确认;确认的具体做法是给发送方发送一个确认报文ACK,而确认报文当中的确认序号就是告诉发送方,自己期望的下一个序号是多少。
超时重传机制
当消息发送方,发送一条消息之后,机会开启一个重传计时器,用来计算消息发送出去的时间,当消息发送的时间大于RTO的时候,还没有收到确认的报文,则重传该报文。
一般有两种情况会触发超时重传机制:应答丢失的情况和发送方数据丢失。不论是数据丢失还是接收方发送的确认报文丢失,对于消息的发送方而言都没有收到确认报文,所以在RTO时间之后会进行重传,图例如下:
RTO(超时重传时间)是动态计算 = RTT(cur) * i + RTT(prev) * (1 - i):tcp中默认情况下i=0.9;
RTT(报文往返时间):从发送报文开始计算,直到收到确认应答,所经历的时间被称为报文往返时间。图例如下:
连接管理机制
在正常情况下, 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状态.
理解TIME_WAIT状态
TCP协议规定,主动断开连接方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态。MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;可以通cat/proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值;
为什么是TIME_WAIT的时间是2MSL?
MSL是TCP报文的最大生存时间,TIME_WAIT的状态是为了等待连接上所有的分组的消失。单纯的想法,发送端只需要等待一个MSL就足够了。这是不够的,假设现在一个MSL的时候,接收端需要发送一个应答,这时候,我们也必须等待这个应答的消失,这个应答的消失也是需要一个MSL,所以我们需要等待2MSL。
理解CLOSE_WAIT状态
什么情况下,连接处于CLOSE_WAIT状态呢?
在被动关闭连接情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态。 通常来讲,CLOSE_WAIT状态的持续时间应该很短,正如SYN_RCVD状态。但是在一些特殊情况下,就会出现连接长时间处CLOSE_WAIT状态的情况。出现大量close_wait的现象,主要原因是某种情况下对方关闭了socket链接,但是我方忙与读或者写,没有关闭连接。代码需要判断socket,一旦读到0,断开连接,read返回负,检查一下errno,如果不是AGAIN,就断开连接。
小结: CLOSE_WAIT状态只有被动断开连接方有这种状态,对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG。只需加上对应的close即可。
滑动窗口
对每一个发送的数据段,都要给一个ACK确认应答。收到ACK后在发送下一个数据段,这样的缺点就是性能比较差,尤其是数据往返的时间比较长的现象。既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)。
在窗口的数据是已经发送到网络上,但是还没有完成确认分组的集合。相对于之前学到的确认应答机制,滑动窗口机制的引入,不必让发送方等待一个报文的确认到达之后,再发送下一个报文,而是允许发送多个报文到网络上。这样的话双方就提高了发送数据的效率。
窗口大小=允许不接收到确认应答就可以发送到网络上的分组的数量。窗口可以是动态变化的,是根据网络转发能力决定的,网络良好,窗口就会变大,数据发送也就会快;网络比较差,窗口变小,数据发送的慢。窗口大小一般是15,TCP连接开始的时候取值为8。对于TCP通信双方来说都维护了一套窗口,发送窗口和接受窗口,发送窗口保存的是没有确认的报文,发送缓冲区的一部分,接收窗口等同于接受缓冲区。
窗口移动时,一定要收到最早分组的确认应答之后,才可以向后进行滑动。
窗口大小是在三次握手期间进行协商的 win=43690字节 = Window size value(342)* 窗口平衡因子(2^7=128)
那么如果出现了丢包, 如何进行重传? 这里分两种情况讨论
情况一: 数据包已经抵达, ACK被丢了
这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认;
情况二: 数据包就直接丢了
当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001"一样;如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;这种机制被称为 “高速重发控制”(也叫 “快重传”).
拥塞控制
拥塞控制机制要点:慢启动、拥塞避免、快重传、快恢复
1. 慢启动和拥塞避免
当双方建立连接后,一开始不要发送大量的数据,而是先发送少量的数据,特侧网络中的拥塞程度,当网络中情况比较好时,再逐渐增大发送的数据量。慢启动之后有一个慢开始门限ssthresh,当拥塞窗口大小小于慢开始门限的时候,执行慢开始算法;超过慢开始门限之后则执行拥塞避免算法(随着传输轮次的增加,拥塞窗口的大小依次加1);
2. 拥塞窗口的大小
在发送发有发送数据的发送窗口,还需要考虑拥塞窗口,一般情况下拥塞窗口等于发送窗口;当拥塞窗口小于发送窗口时,按照拥塞窗口的大小发送数据。
3. 快重传和快恢复
当执行慢开始算法或者拥塞避免算法的时候,一旦有丢包现象,则触发TCP执行快恢复算法。新的慢开始门限 = 拥塞窗口 / 2,然后继续执行拥塞避免算法。
延时应答
前提:
- 接收方在恢复数据的时候,会通知发送方自己的接受能力;
- 发送方通过接受方的接收能力,调整发送窗口大小(假设网络情况良好,意味着要先排除拥塞窗口的干扰);
- 接收方通告的接收能力越大,发送窗口越大,也就是意味着传输效率越高,反之,发送窗口越小。
- 接收方的能力大小,其实取决于应用层的接受程序的能力,应用层接受的越快,TCP接收缓冲区空闲的越多,通告发送方自己接收能力的时候就越大,促使发送方的发送窗口越大。
延时应答:TCP接收方在回复确认ACK的时候,稍微等一小会(200ms),再回复ACK(本质就是等应用层的程序从接受缓冲区将数据读走)。
捎带应答
捎带应答的前提是双方都想给对方发送数据,并且发送数据比较频繁。捎带应答机制指的是在发送数据的时候捎带着回复AC给对端。例如下图所示:
面向字节流
创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区;
- 调用write时, 数据会先写入发送缓冲区中;
- 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
- 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
- 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;然后应用程序可以调用read从接收缓冲区拿数据;
- 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据。这个概念叫做全双工。
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:
- 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
- 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
粘包问题
首先要知道,粘包问题中的指的是应用层的数据包。在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段.站在传输层的角度, TCP是一个一个报文过来的。按照序号排好序放在缓冲区中。站在应用层的角度, 看到的只是一串连续的字节数据.那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包。
那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界
对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可。
TCP保活机制
- 当连接空闲2小时(当前双方都没有给对方发送数据),就会触发TCP保活机制,每隔75秒发送一个保活探测包,总共发10次;
- 如果当前10个保活探测包都没有确认应答,则认为对端状态不对,则关闭该连接。
- 不管是客户端还是服务端在闲置2小时,都会给对端发送保活探测包;
- 当我们发送一个数据之后,就会启动保活计时器,当收到应答,则重置保活计时器;或者当收到对端发送的数据的时候,也会重置保活计时器。
0号窗口
0号窗口时告诉对端,当前自己的接收能力为0,不需要对端再给我发送数据了。导致这样的原因是接收方没有调用recv函数,当前接收方的接收缓存区已经满了,就会导致接收方给发送方发送0号窗口。