一、TCP 协议段格式
1.1、4位首部长度
4位首部长度的基本单位是4字节,也就是说如果4位首部长度填6,那报头长度就是24字节。报头长度的取值范围为[0,60]字节,也就是说选项的最大长度为40字节。
二、确认应答机制
发送数据和发送应答,一般是由双方操作系统自动完成的,通信细节由操作系统自动解决了。服务端和客户端发送的都是报文。
2.1、32位序号
客户端可能一次性给服务端发送了多条报文,为了区分报文,每一个TCP报文在发送时都会填写上序号。如果服务端收到了多条报文,服务端就可以按照序号在接收缓冲区中对报文进行按序到达。
2.2、32位确认序号
收到的报文的序号加1就是服务端发送回给客户端的报文的确认序号。这就表明服务端告诉客户端,该确认序号之前的数据我已经全部收到了,下次发送请从这次发送的确认序号开始。
三、捎带应答机制
捎带应答机制可以用来解答为什么tcp报头里序号和确认序号要分开。在真实的tcp通信过程中,一个报文可能既作为应答又携带了要传送的信息,这个时候就需要用确认序号标识应答,序号标识要传送的信息,如果序号和确认序号不分开,就无法实现捎带应答机制。
四、标志位
4.1、SYN、ACK、FIN
我们的服务器在通信的过程中,一定会收到不同客户端的请求建立连接、发送正常报文、请求断开连接的报文,所以TCP报文是需要类型的,用于区分不同的报文。标志位就是用来区分不同报文的。在服务器和客户端进行三次握手时,客户端先向服务器发送SYN标志位置一的TCP报文(此时服务端要处于listen状态),服务端收到了再接着向客户端发送SYN标志位和ACK标志位置一的TCP报文,客户端再向服务器发送ACK标志位置一的TCP报文(此时服务端要处于accept状态),完成三次握手。操作系统要对连接进行管理,就会消耗时间和空间成本。四次挥手时客户端先给服务器发送FIN标志位为1的报文,服务器响应回ACK标志位为1的报文;同样的,服务器也要给客户端发送FIN标志位为1的报文,客户端响应回ACK标志位为1的报文,这就是四次挥手。
TCP报文中ACK标志位置一加上数据就可以实现捎带应答。
4.2、URG
URG标志位被置一的报文16位紧急指针被启用,紧急指针对应的数据会被上层优先处理。16位紧急指针表示紧急数据在数据中的偏移量。紧急数据只有一个字节。
五、超时重传机制
在此有一个约定,如果我没有收到应答,就认为对方没有收到报文。如果超过一个固定的时间我没有收到应答,我这端就要进行超时重传。Linux中(BSDUnix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。如果重发一次之后,仍然得不到应答,等待2*500ms后再进行重传。如果仍然得不到应答,等待4*500ms进行重传。依次类推,以指数形式递增。累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
六、连接管理机制
三次握手四次挥手在上面标志位中已经有提到了。在此做一个补充,三次握手并不能保证通信双方百分百把连接建立好了,而是经过三次握手后,通信双方都认为链接已经建立好了。如果三次握手并没有完成客户端直接向服务器发送数据,服务器会向客户端发送回RST标志位置一的报文,让客户端回到SYN_SENT状态重新发送三次握手。RST置一就是让双方链接重置,重新进行三次握手,用来解决链接出现异常的问题。
一次握手或者是两次握手服务器都可能受到客户端一次性发过来的大量的SYN所形成的SYN洪水攻击,此时服务器的链接数可能瞬间被打满,无法再接受其他链接的到来,这样一台客户端就可以让服务端瘫痪。三次握手服务器再建立好链接之前客户端要先建立好链接,这样服务器不太可能因为一台主机的攻击就陷入瘫痪,至少需要多台主机,三次握手首先解决了一次两次握手所存在的这一明显漏洞。另外一点,三次握手过程中,客户端和服务器都会经过一次收发,也就确认了自己的收发能力(确认全双工),也可以确保双方OS是健康的且愿意通信的。
在四次挥手时,如果服务器主动断开链接,服务器会在TIME_WAIT状态下等待一段时间,此时如果再绑定该端口就会绑定失败,导致服务器无法立即重启。使用setsockpot可以设置端口号复用。TIME_WAIT状态是为了等待网络中历史报文的消散,在等的这段时间内也可以等待如果最后一次挥手的ACK丢了,一方再发送FIN时我可以收到然后再发ACK,尽量的保证四次挥手的完成。
七、流量控制机制
16位窗口大小可以用来通知对方自己的接收能力。 接收端将自己可以接收的缓冲区大小放入TCP首部中的 "窗口大小" 字段,通过 ACK 端通知发送端;窗口大小字段越大,说明网络的吞吐量越高;接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;发送端接受到这个窗口之后,就会减慢自己的发送速度;如果接收端缓冲区满了,就会将窗口置为 0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。如果我这一端的窗口大小一直为0,另一端就会给我发送PSH标志位为1的报文,催促我这一边的上层尽快地把数据读走(需要数据尽快交付的情况都可以将PSH标志位置一)。
八、滑动窗口
滑动窗口是发送缓冲区的一部分, 滑动窗口里的数据无需等待确认应答而可以继续发送。不考虑网络状况,滑动窗口的大小一般就是对方接收缓冲区中剩余空间的大小。为了能够实现超时重传,一旦将滑动窗口中的数据发出去后,滑动窗口不会立即向右移动,要等待报文确认收到应答后才会向右移动,这也保证了如果没有收到应答超时了可以重发报文。滑动窗口的变更是根据收到的确认序号来变更的。
假设是1001到2000这一段报文丢了, 1001到2000后续的被对方收到的报文所返回的ACK报文中的确认序号会一直是1001,告诉我是1001开始的这一段报文丢了,要进行重发。
九、拥塞控制
虽然 TCP 有了滑动窗口,能够高效可靠的发送大量的数据。但是如果在刚开始阶段就发送大量的数据,仍然可能引发问题。因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的。TCP引入 慢启动机制, 先发少量的数据,探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。
此处引入一个概念称为拥塞窗口。 发送开始的时候, 定义拥塞窗口大小为 1;每次收到一个 ACK 应答,拥塞窗口加 1;每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的滑动窗口;像上面这样的拥塞窗口增长速度,是指数级别的。"慢启动" 只是指初使时慢,但是增长速度非常快。为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.此处引入一个叫做慢启动的阈值。当拥塞窗口超过这个阈值的时候,不再按照指数方式增长,而是按照线性方式增长。
当 TCP 开始启动的时候,慢启动阈值等于拥塞窗口最大值;在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1;少量的丢包,我们仅仅是触发超时重传;大量的丢包,我们就认为网络拥塞;当 TCP 通信开始后,网络吞吐量会逐渐上升;随着网络发生拥堵,吞吐量会立刻下降;拥塞控制,归根结底是 TCP 协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。
数据链路层中的MTU限制了一次性发送的报文长度不能太长 。
十、延迟应答
如果接收数据的主机立刻返回 ACK 应答,这时候返回的窗口可能比较小。假设接收端缓冲区为 1M。一次收到了 500K 的数据;如果立刻应答,返回的窗口就是 500K;但实际上可能处理端处理的速度很快,10ms 之内就把 500K 数据从缓冲区消费掉了;在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来,如果接收端稍微等一会再应答,比如等待 200ms 再应答, 那么这个时候返回的窗口大小就是1M。 窗口越大,网络吞吐量就越大,传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率。