TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)

一、TCP报文

TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)

【重要的字段】:

  • 序号:Seq序号,占32位,用来标识从TCP源端向目的端发送的字节流,发起方发送数据时对此进行标记;
  • 确认序号:Ack序号,占32位,只有ACK标志位为1时,确认序号字段才有效,Ack=Seq+1;
  • 标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:
    • URG:紧急指针(urgent pointer)有效;
    • ACK:确认序号有效;
    • PSH:接收方应该尽快将这个报文交给应用层;
    • RST:重置连接;
    • SYN:发起一个新连接;
    • FIN:释放一个连接。

二、三次握手

  在 TCP/IP 协议中,TCP 协议提供可靠的连接服务,采用三次握手建立一个连接。
TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)

1. 第一次握手Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

2. 第二次握手Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

3. 第三次握手Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

  通过这样的三次握手,客户端与服务端建立起可靠的双工的连接,开始传送数据。 三次握手的最主要目的是保证连接是双工的,可靠更多的是通过重传机制来保证的。

(1)SYN flood攻击

  在三次握手过程中,服务器发送SYN-ACK之后,收到客户端的ACK之前的TCP连接称为半连接(half-open connect).此时服务器处于Syn_RECV状态. 当收到ACK后,服务器转入ESTABLISHED状态。
TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)
  Syn攻击就是攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。

// 检测是否被Syn攻击
netstat -n -p TCP | grep SYN_RECV

  一般较新的TCP/IP协议栈都对这一过程进行修正来防范Syn攻击,修改tcp协议实现。主要方法有SynAttackProtect保护机制、SYN cookies技术、增加最大半连接和缩短超时时间等.但是不能完全防范syn攻击。

(2)为什么需要三次握手?

  为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。可以用聊天来类比:
  甲—>乙:你吃饭了吗?
  已—>甲:我吃了,你呢?
  甲—>乙:我也吃了。

(3)在三次握手过程中,如果服务器一直收不到客户端的ack会发生什么?

  服务端会给每个待完成的半连接都设一个Timer,如果超过时间还没有收到客户端的ACK消息,则重新发送一次SYN-ACK消息给客户端,直到重试超过一定次数时才会放弃。这个时候服务器需要分配内核资源维护半连接。


三、四次挥手

TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)

1. 第一次挥手Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态;

2. 第二次挥手Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态;

3. 第三次挥手Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态;

4. 第四次挥手Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

(1)为什么建立连接是三次握手,而关闭连接却是四次挥手呢?

  这是因为服务端的 LISTEN 状态下的 SOCKET 当收到 SYN 报文的建连请求后,它可以把 ACK 和 SYN(ACK 起应答作用,而 SYN 起同步作用)放在一个报文里来发送。但关闭连接时,当收到FIN 报文通知时,如果能将ACK、FIN放在一个报文里那么就有了三次挥手,但是这是不可能,因为ACK是服务器B一收到FIN报文底层就回发的,而服务器B的FIN是应用层调用close()激发的,所以它这里的 ACK 报文和 FIN 报文在发送的时间上都是分开的,不可能同时发送。

(2)socket中的close是一次就关闭的吗?半关闭状态是怎么产生的?

  使用close中止一个连接,但它只是减少描述符的引用计数,并不直接关闭连接,只有当描述符的引用计数为0时才关闭连接。
  客户端的半关闭状态:收到服务器的ACK后,暂时关闭写端,但是读端并没有关闭,依然可以接受来自服务器的数据。

(3)如果已经建立了连接, 但是客户端突发故障了怎么办?

  TCP设有一个保活计时器,显然,客户端如果出现故障,服务器不能一直等下去,白白浪费资源。服务器每收到一次客户端的请求后都会重新复位这个计时器,时间通常是设置为2小时,若两小时还没有收到客户端的任何数据,服务器就会发送一个探测报文段,以后每隔75分钟发送一次。若一连发送10个探测报文仍然没反应,服务器就认为客户端出了故障,接着就关闭连接。


四、TIME_WAIT状态

  TIME_WAIT状态对大并发服务器的影响,应尽可能在服务器避免出现TIME_WAIT状态,如果服务器端主动断开连接,服务端就会进入TIME_WAIT状态(主动发起关闭连接的一方),协议设计上,应该让客户端主动断开连接,这样就把TIME_WAIT状态分散到大量的客户端。如果客户端不活跃了,一些客户端不断开连接,这样子就会占用服务器端的连接资源。服务器端也有有个机制踢掉不活跃的连接。

(1)为什么需要TIME_WAIT状态/为什么 TIME_WAIT 状态还需要等 2MS L后才能返回到 CLOSED 状态??

  • 保证可靠地终止TCP连接:处于TIME_WAIT状态的发送端会向目的端发送ACK,如果此时ACK丢失,目的端会超时重传FIN报文段,目的端收到重传的报文段最少需要2MSL,所以发送端会等待2MSL时间;

  • 保证迟到的TCP报文段有足够的时间识别被丢弃

(2)TIME_WAIT会带来哪些问题?

  • 作为服务器,短时间内关闭了大量的Client连接,就会造成服务器上出现大量的TIME_WAIT连接,占据大量的tuple,严重消耗着服务器的资源

  • 作为客户端,短时间内大量的短连接,会大量消耗Client机器的端口,毕竟端口只有65535个,端口被耗尽了,后续就无法再发起新的连接了。

(3)服务端time_wait过多的处理办法?

  通过调整内核参数解决,编辑文件/etc/sysctl.conf,加入以下内容:

/*表示开启SYN Cookies。当出现SYN等待队列溢出时,
启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭*/
net.ipv4.tcp_syncookies = 1;
/*表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,
默认为0,表示关闭*/
net.ipv4.tcp_tw_reuse = 1;
/*表示开启TCP连接中TIME-WAIT sockets的快速回收,
默认为0,表示关闭*/
net.ipv4.tcp_tw_recycle = 1;
/*修改系默认的 TIMEOUT 时间
然后执行 /sbin/sysctl -p 让参数生效*/
net.ipv4.tcp_fin_timeout = 30;

五、connect()、listen()和accept()三者之间的关系

(1)connect()函数分析

  对于客户端的 connect() 函数,该函数的功能为客户端主动连接服务器,建立连接是通过三次握手,而这个连接的过程是由内核完成,不是这个函数完成的,这个函数的作用仅仅是通知 Linux 内核,让 Linux 内核自动完成 TCP 三次握手连接,最后把连接的结果返回给这个函数的返回值(成功连接为0, 失败为-1)。通常的情况,客户端的 connect() 函数默认会一直阻塞(可以使用fcntl()函数或者ioctl()函数把connect()变为非阻塞),直到三次握手成功或超时失败才返回(正常的情况,这个过程很快完成)。

(2)listen()函数分析

  对于服务器,它是被动连接的。这里需要注意的是,listen()函数不会阻塞,它主要做的事情为,将该套接字和套接字对应的连接队列长度告诉 Linux 内核,然后,listen()函数就结束。

  所以,只要 TCP 服务器调用了 listen(),客户端就可以通过connect() 和服务器建立连接,而这个连接的过程是由内核完成。

  在被动状态的socket有两个队列,一个是正在进行三次握手的socket队列,一个是完成三次握手的socket队列。在握手完成后会从正在握手队列移到握手完成的队列,此时已经建立连接。

(3)accept()函数分析

  accept()函数功能是,从连接队列头部取出一个已经完成的连接,如果这个队列没有已经完成的连接,accept()函数就会阻塞,直到取出队列中已完成的用户连接为止。TCP 的连接队列满后,会延时连接

  accept就是从已经完成三次握手的socket队列里面取,如果服务器端不调用accept则客户端能完成的连接就是此队列的大小。


六、三次握手、四次挥手总结

TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)
TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)


七、滑动窗口(流量控制)

  在确认应答机制中,对每一个发送的数据段,都要给一个ACK确认应答。收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。尤其是数据往返时间较长的时候。那么我们可不可以一次发送多个数据段呢:滑动窗口所谓的流量控制就是让发送方的发送速率不要太快,让接收方来得及接受。利用滑动窗口机制可以很方便的在TCP连接上实现对发送方的流量控制。

【窗口类型】:

  • 接收端窗口rwnd:接收端缓冲区大小。接收端将此窗口值放在 TCP 报文的首部中的窗口字段,传送给发送端;

  • 拥塞窗口cwnd:发送端缓冲区大小;

  • 发送窗口swnd:发送窗口的上限值 = Min [rwnd, cwnd]。

【滑动窗口】:
TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)

发送端已发送了 400 字节的数据,但只收到对前 200 字节数据的确认,同时窗口大小不变。还可发送 300 字节;发送端收到了对方对前 400 字节数据的确认,但对方通知发送端必须把窗口减小到 400 字节。现在发送端最多还可发送 400 字节的数据。


八、拥塞控制

  拥塞控制也就是考虑当前的网络环境,动态调整窗口大小,没有发生拥塞情况,则窗口增大,拥塞了窗口减小,如此往复,最终应该接近与接收端的窗口大小。

【慢开始和拥塞避免】:

  在开始发送信息时,由于不知道具体的网络环境,为避免大量信息造成的拥塞现象,此时的拥塞窗口以最小值(即拥塞窗口和接收端窗口中的较小值)进行数据发送,并设定门限值作为慢启动算法和拥塞避免算法的分割点。慢启动是指以最小的拥塞窗口按照指数形式递增,达到门限值后,以拥塞避免算法,即线性递增方式增大拥塞窗口(这里递增时间间隔为一个往返时间RTT)。
  在上述过程中,无论是窗口大小指数递增或者线性递增,当发生拥塞现象,则门限值更新为当前窗口大小的一半,拥塞窗口大小变为最小值,重复上述递增过程(此时属于网络环境限制,所以在接收端和拥塞窗口两个限制条件中选择拥塞窗口作为限制)。
TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)

【快重传和快恢复】:

  发送端连续收到三个重复的ack时,表示该数据段已经丢失,需要重发。当收到三个表示同一个数据段的ack时,不需要等待计时器超时,立即重新发送数据段(当时这三个ack要在超时之前到达发送端),因为能够收到接收端的ack确认信息,所以数据段只是单纯的丢失,而不是因为网络拥塞导致,所以此时不需要拥塞窗口更新为最小值进行慢启动(如果这样的话,反倒因为拥塞窗口的增长需要时间,可能导致性能降低),此时需要设置拥塞窗口大小为:门限值大小+3,当然此处的门限值已经更新为拥塞窗口值的一半大小,该行为也就是所谓的“乘法减少”,更新之后按照拥塞避免算法继续进行。


九、粘包问题

  TCP是基于字节流传输的,只维护发送出去多少,确认了多少,没有维护消息与消息之间的边界,因而可能导致粘包问题。
TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)

  粘包问题本质上要在应用层维护消息与消息的边界。解决方案如下:

  • 定长包;

  • 包尾\r\n(ftp);

  • 包头加上包头长度;

  • 更复杂的应用层协议


十、TCP状态转换图

TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)

【服务器(实线)】:

TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)

【客户端(虚线)】:

TCP协议详解(TCP报文、三次握手、四次挥手、TIME_WAIT状态、滑动窗口、拥塞控制、粘包问题、状态转换图)

参考:http://harlon.org/2018/04/11/networksocket4/
https://bbs.****.net/topics/390566881
https://blog.****.net/lianghe_work/article/details/46443691
https://blog.****.net/htyurencaotang/article/details/11569905
https://www.cnblogs.com/popduke/p/5823801.html#1ref
https://blog.****.net/qzcsu/article/details/72861891
https://blog.****.net/omnispace/article/details/52701752
https://blog.****.net/lianghe_work/article/details/46458889
https://blog.****.net/sinat_36629696/article/details/80740678
https://blog.****.net/q1007729991/article/details/70142341?utm_source=blogxgwz0
https://www.cnblogs.com/gaopeng527/p/5255757.html
https://blog.****.net/wo16fafafa/article/details/52317050
http://harlon.org/2018/04/08/networksocket2/
https://www.cnblogs.com/thrillerz/p/6464203.html