三次握手
三次握手过程
- TCP服务器进程先创建传输控制块TCB,时刻准备接受客户进程的连接请求,此时服务器就进入了LISTEN(监听)状态;
- TCP客户进程也是先创建传输控制块TCB,然后向服务器发出连接请求报文,此时是报文首部中的同步位SYN=1,同时选择一个初始序列号 seq=x ,此时,TCP客户端进程进入了 SYN-SENT(同步已发送状态)状态。TCP规定,SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号。
- TCP服务器收到请求报文后,如果同意连接,则发出确认报文。确认报文中应该 ACK=1,SYN=1,确认号是ack=x+1,同时也要为自己初始化一个序列号 seq=y,此时,TCP服务器进程进入了 SYN-RCVD(同步收到)状态。这个报文也不能携带数据,但是同样要消耗一个序号。
- TCP客户进程收到确认后,还要向服务器给出确认。确认报文的ACK=1,ack=y+1,自己的序列号seq=x+1,此时,TCP连接建立,客户端进入ESTABLISHED(已建立连接)状态。TCP规定,ACK报文段可以携带数据,但是如果不携带数据则不消耗序号。
- 当服务器收到客户端的确认后也进入ESTABLISHED状态,此后双方就可以开始通信了。
三次握手期间调用的系统调用
- 当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;
- 服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;
- 客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。
为什么建立连接使用三次握手而不是两次握手
主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,这将导致不必要的错误和资源的浪费。如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
TCP进行通信时的初始序列号为什么是随机的
考虑场景,B是服务器,A是一个合法的客户端,C假冒A(比如模拟IP等)和B进行通信。
C假冒A,B接受后把ACK会直接发给A。由于A没有发送过seq=ISN _C的请求,当A收到ISN_C的ack后直接发送reset 给B,最终关闭了链接。如下图所示:
假如初始序列号不是随机的,而是可以推测的,那么C就可以拿到ISN_B,然后模拟一个ACK过去,B最终会建立链接,C开始传递数据,这就会产生非常严重的安全问题(比如SYN泛洪),所以ISN随机是必须的。
如果没有资源限制,一个服务器最多可以承载多少连接?
一个TCP连接由一个四元组所确定(源IP,源端口,目的IP,目的端口)。任意一个元素改变,都代表一个新的连接,以Nginx为例,它的端口是固定使用80。另外服务器的IP也是固定的,那么理论上最多可以建立 2 ^ 32 (ip数) × 2 ^ 16 (端口数) 个连接。
实际上,能建立的连接远小于这个数字。我们每打开一个连接都对应一个文件描述符fd,而linux系统在多个位置都限制了可打开的文件描述符的数量。
Address in use错误信息
在实践中,我们可能会经常碰到一个问题,当 TCP 服务进程重启之后,总是碰到“Address in use”的报错信息,TCP 服务进程不能很快地重启,而是要过一会才能重启成功。这是为什么呢?
当我们重启 TCP 服务进程的时候,意味着通过服务器端发起了关闭连接操作,服务端会出现 TIME_WAIT 状态的连接,TIME_WAIT 状态的连接使用的 IP+PORT 仍然被认为是一个有效的 IP+PORT 组合,相同机器上不能够在该 IP+PORT 组合上进行绑定,那么执行 bind() 函数的时候,就会返回了 Address already in use 的错误。而等 TIME_WAIT 状态的连接结束后,重启 TCP 服务进程就能成功。
重启 TCP 服务进程时,如何避免“Address in use”的报错信息?
我们可以在调用 bind 前,对 socket 设置 SO_REUSEADDR 属性,可以解决这个问题。SO_REUSEADDR 作用是:如果当前启动进程绑定的 IP+PORT 与处于TIME_WAIT 状态的连接占用的 IP+PORT 存在冲突,但是新启动的进程使用了 SO_REUSEADDR 选项,那么该进程就可以绑定成功。
四次挥手
四次挥手过程
- 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
- 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
- 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
- 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
- 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCP后,才进入CLOSED状态。
- 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCP后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
为什么建立连接协议是三次握手,而关闭连接却是四次握手
这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建立连接请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你可以未必会马上会关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。
为什么关闭连接是四次挥手而不是三次挥手
因为TCP是全双工通信的。被动关闭方发送ACK报文只是确认主动关闭方发来的FIN报文,但并不代表自身的数据已经传输完毕。即主动关闭方断开连接的时候,只是主动关闭方的数据已传输完毕,而被动关闭方或许还有数据未发送,所以要等到被动关闭方要发送的数据全部传输完成后,被动关闭方发送FIN报文,才能可靠的关闭连接。
三次挥手只能将被动关闭方发出的ACK报文和FIN报文合并,但这样合并是有问题的。会造成主动关闭方的FIN报文长时间未得到响应而进行超时重传等等,造成了不必要的资源浪费甚至更意想不到的问题。
挥手报文丢失会发生什么?
第一次挥手报文丢失:当客户端调用close函数后,就会向服务端发送FIN报文,试图与服务端断开联系,此时客户端进入FIN_WAIT_1状态。如果客户端一直收不到ack应答报文的话,就会触发超时重传机制,最大重传次数由tcp_orphan_retries参数决定。当超过指定次数时,就不再发送报文,直接进入close状态
第二次挥手报文丢失:当接受到客户端的FIN报文,就会先回应一个ack报文,此时服务端进入close_wait状态。当ack报文丢失时,ack是不会重传的。服务端的ack报文丢失了,客户端就会触发超时重传,直到收到ack报文或则到达超时重传次数。
第三次挥手报文丢失:类似于第一次挥手报文丢失。
第四次挥手报文丢失:类似于第二次挥手报文丢失。
TIME_WAIT状态详解
TIME_WAIT的意义(关闭TCP连接为什么要等于2MSL)
MSL表示最长报文段寿命。
(1)保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,由于RTO(重传超时时间)远小于MSL,因此服务器会触发超时重传,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
(2)防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
为什么不是一个MSL?
如果最后一个ACK包丢了,检测到丢包时用了一个MSL,这时主动关闭端就关闭了,对方再次重传的时候,主动关闭端就接收不到了,就会出现死循环.
TIME_WAIT过多产生原因及危害
正常的TCP客户端连接在关闭后,会进入一个TIME_WAIT的状态,持续的时间一般在1-4分钟,对于连接数不高的场景,1-4分钟其实并不长,对系统也不会有什么影响,但如果短时间内(例如1s内)进行大量的短连接,则可能出现这样一种情况:客户端所在的操作系统的socket端口和文件描述符被用尽,系统无法再发起新的连接!
举例:假设每秒建立了1000个短连接(Web场景下是很常见的,例如每个请求都去访问memcached),设TIME_WAIT的时间是1分钟,则1分钟内需要建立6W个短连接,由于TIME_WAIT时间是1分钟,这些短连接1分钟内都处于TIME_WAIT状态,都不会释放,而Linux默认的本地端口范围配置是:net.ipv4.ip_local_port_range = 32768 61000不到3W,因此这种情况下新的请求由于没有本地端口就不能建立了。
TIME_WAIT过多解决方法:
- 可以改为长连接,但代价较大,长连接太多会导致服务器性能问题并且安全性也较差;
CLOSE_WAIT状态详解
CLOSE_WAIT的意义
比如是客户端要与服务端断开连接,先发一个FIN表示自己要主动断开连接了,服务端会先回一个ACK,这时表示客户端没数据要发了,但有可能服务端数据还没发完,所以要经历一个close_wait,等待服务端数据发送完,再回一个FIN和ACK。
CLOSE_WAIT产生太多原因
close_wait 按照正常操作的话应该很短暂的一个状态,接收到客户端的fin包并且回复客户端ack之后,会继续发送FIN包告知客户端关闭关闭连接,之后迁移到Last_ACK状态。但是close_wait过多只能说明没有迁移到Last_ACK,也就是服务端是否发送FIN包,只有发送FIN包才会发生迁移,所以问题定位在是否发送FIN包。FIN包的底层实现其实就是调用socket的close方法,这里的问题出在没有执行close方法。说明服务端socket忙于读写。
CLOSE_WAIT太多解决方法
socket读控制: 当读取的长度为0时(读到结尾),立即close。如果read返回-1,出现错误,检查error返回码,有三种情况:INTR(被中断,可以继续读取),WOULDBLOCK(表示当前socket_fd被阻塞了),AGAIN(表示现在没有数据稍后重新读取)。如果不是AGAIN,立即close。
TCP三次握手和四次挥手的11种状态
- LISTEN: 首先服务端需要打开一个socket进行监听,状态为LISTEN。
- SYN_SENT: 客户端通过应用程序调用connect进行active open.于是客户端tcp发送一个SYN以请求建立一个连接.之后状态置为SYN_SENT。
- SYN_RECV: 服务端应发出ACK确认客户端的SYN,同时自己向客户端发送一个SYN. 之后状态置为SYN_RECV
- ESTABLISHED: 代表一个打开的连接,双方可以进行或已经在数据交互了。
- FIN_WAIT1: 主动关闭(active close)端应用程序调用close,于是其TCP发出FIN请求主动关闭连接,之后进入FIN_WAIT1状态。
- CLOSE_WAIT:被动关闭(passive close)端TCP接到FIN后,就发出ACK以回应FIN请求(它的接收也作为文件结束符传递给上层应用程序),并进入CLOSE_WAIT。
- FIN_WAIT2: 主动关闭端接到ACK后,就进入了FIN-WAIT-2。
- LAST_ACK:被动关闭端一段时间后,接收到文件结束符的应用程序将调用CLOSE关闭连接。这导致它的TCP也发送一个FIN,等待对方的ACK.就进入了LAST-ACK。
- TIME_WAIT: 在主动关闭端接收到FIN后,TCP就发送ACK包,并进入TIME-WAIT状态。
- CLOSING: 这种状态比较特殊,实际情况中应该是很少见,属于一种比较罕见的例外状态。正常情况下,当你发送FIN报文后,按理来说是应该先收到(或同时收到)对方的 ACK报文,再收到对方的FIN报文。但是CLOSING状态表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文。什么情况下会出现此种情况呢?其实细想一下,也不难得出结论:那就是如果双方几乎在同时close一个SOCKET的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭SOCKET连接。
- CLOSED: 被动关闭端在接受到ACK包后,就进入了closed的状态。连接结束