目录
[一. 建立连接------三次握手](#一. 建立连接——三次握手)
[1. 三次握手过程描述](#1. 三次握手过程描述)
[2. TCP连接建立相关问题](#2. TCP连接建立相关问题)
[二. 释放连接------四次挥手](#二. 释放连接——四次挥手)
[1. 四次挥手过程描述](#1. 四次挥手过程描述)
[2. TCP连接释放相关问题](#2. TCP连接释放相关问题)
[三. TCP状态转换](#三. TCP状态转换)
前言
TCP------传输控制协议(Transmission Control Protocol)。是一种面向连接的传输层通信协议
什么是连接
TCP连接是指通过TCP协议在网络中建立的一种可靠的通信链路。TCP连接在应用层之间提供可靠,高效的通信方式,广泛应用于互联网上的各种应用,如网页浏览,电子邮件,文件传输等。
建立连接在操作系统中是需要有对应的数据结构管理,维护的。所以连接的建立,维护都是有成本的
为什么要建立连接
- 更好的保证可靠性:建立连接的过程其实就是让通信双方验证各自的发送能力和接收能力是否正常,当前发送接收两端信道是否通畅。
- 协商一些重要参数,如:序号初始值,MSS,是否启用SACK,等等。
MSS和SACK可参看【计算机网络】TCP协议报头详解的选项部分
TCP建立连接,通信,断开连接的流程总图如下:
一. 建立连接------三次握手
1. 三次握手过程描述
【1. 服务器初始化状态】
服务器端进程先创建传输控制块(TCB),socket()创建套接字listenfd ,bind()将套接字和端口号绑定,服务器进程准备接收客户端的连接请求 。然后服务器进程调用listen()函数,使listenfd成为监听套接字 ,后续连接都从监听套接字获取。此时服务器端进程处于监听(listen)状态,紧接着调用accept()函数,在listenfd套接字中等待客户的连接到来
服务器端进程调用函数顺序:socket => bind => listen => accept。当执行到accept函数时,服务器进程会一直处于阻塞状态,直到有客户连接请求到达才返回
【2. 客户端发起连接请求,发送SYN同步报文段,第一次握手】
客户端进程也创建传输控制块(TCB),socket()创建套接字,然后向服务器端发送连接请求报文段,这时请求报文段的首部标志位SYN=1,同时选择一个初始序号seq=x,这个初始序号x是随机产生的整数ISN。TCP规定,SYN报文段(即SYN=1的TCP报文段)不能携带数据,但要消耗一个序号 。此时,客户端进程进入SYN-SENT(同步报文已发送)状态
客户端进程函数调用顺序:socket => connect。当客户端调用connect函数时,操作系统会自动bind(),客户端进程就会向服务器进程发送连接请求的SYN同步报文段
SYN=1 的报文段称为同步报文段
ISN (Initial Sequence Number)初始序列号
seq序号 ,即当前发送的报文首字节的编号
【3. 服务器同意建立连接,回复确认信息,第二次握手】
服务器端进程收到连接请求报文,若同意建立连接,从listenfd中获取客户端信息 ,并且由服务器端操作系统 向客户端进程发送SYN报文段给出确认。在确认报文的首部中,SYN=1,ACK=1,确认号ack=x+1 。同时也为自己选择一个初始序号seq = y。这个确认报文也不能携带数据,但同样要消耗一个序号。这时,TCP服务器进入SYN-RCVD(同步报文已收到)状态
TCP协议规定,只要接收方接收到数据,必须给发送方发送确认报文------报文首部ACK标志位为1
ack确认号 ,期望对方发送的下一个报文首字节的编号
【4. 客户端确认连接,发送确认连接信息,第三次握手】
客户端进程收到服务器进程的确认报文后,还要向服务器进程的SYN报文给出确认。在确认报文首部,ACK=1,确认号ack=y+1,序号seq=x+1。TCP标准规定,ACK报文可以携带数据,但如果不携带数据,则不消耗序号。在这种情况,客户端进程的下一个数据报文序号仍为seq=x+1。客户端在发送确认报文后 ,认为连接成功建立,先进入ESTABLISHED(已建立连接)状态。
当服务器端进程收到客户端发送的确认报文后 ,认为连接成功建立,进入ESTABLISHED(已建立连接)状态
上面给出的TCP连接建立过程叫做**"三次握手"** 。注意,上图服务器发送给客户端的报文,也可以拆分为两个报文 ,即先发送ACK=1(ack=x+1)的确认报文,再发送SYN=1(seq=y)的同步报文。客户端收到服务器端的同步报文段,发送确认报文,过程会变成"四次握手",但效果一样
2. TCP连接建立相关问题
问题1:TCP建立连接可以只有2次握手吗?
回答:肯定不行。理由:在握手过程中,我们看到客户服务分别在最后一次握手建立连接。如果是两次握手,那么最后的报文由服务器发送,客户端接收
如此,服务器就会先于客户端建立连接。这是万万不可的。
- 如果确认报文丢失,那么服务器就会维护不成功的TCP连接
- 会很容易遭受SYN洪水攻击,即攻击者发送大量SYN请求,两次握手会使得服务器对每个送达的SYN都建立连接,如此会消耗大量资源,容易导致服务器崩溃
- 出现已失效的连接请求报文突然又传送到服务器端,维护失效的连接
"已失效的连接请求报文"是如何产生的呢?
假定一种异常情况:客户端A发送的第一个连接请求报文在某个网络结点滞留了,延误到客户端A认为该报文丢失失效,本次连接失败时,请求报文到达了服务器B。此时该请求报文已经失效,但B并不知道,所以给客户端A发送确认报文,如果只有两次握手,那么此时服务器B发出确认报文,建立连接,进入ESTABLISHED状态
由于A没有再发出建立连接的请求,因此不会处理B的确认报文,也不会向B发送数据。但B却认为连接成功建立,并一直等待A发送数据,浪费资源
所以握手次数绝不能是偶数次,因为这样会使得服务器先建立连接,将维护连接的成本嫁接给服务器
问题2:TCP连接建立为什么需要3次握手?
理由一:奇数次握手,客户端优先建立连接。
理由二:防止已失效的连接请求报文突然又传送到了服务器端,因而产生错误。
理由三:三次握手是客户服务器验证双方信道通畅 ,发送接收能力无误的最小成本
- 一二次握手,验证了客户端的发送和接收能力
- 二三次握手,验证了服务器端的发送和接收能力
问题3:在TCP连接建立过程中,如果服务器一直收不到客户端的ACK确认报文,会发生什么?
操作系统会给每个处于SYN-RCVD状态的服务器进程设定一个计时器,如果超过一定时间还没有收到客户端第三次握手的ACK确认报文,将会重新发送第二次握手的确认报文,直到重发达到一定次数才会放弃
问题4:初始序列号ISN为什么要随机初始化?
seq序号表示的是发送的TCP报文数据部分的起始字节位置,服务器/客户端可以通过序号正确读取数据。如果不是随机分配起始序列号,那么黑客就会很容易获取客户端与服务器之间TCP通信的初始序列号,然后通过伪造序列号让通信主机读取到携带病毒的TCP报文,发起网络攻击
问题5:SYN洪水攻击如何解决
SYN洪水攻击:攻击者在短时间内伪造大量不存在的IP地址,向服务器不断地发送连接请求的SYN同步报文。服务器需要为每个请求发送SYN-ACK确认报文,并等待客户端的确认,但因为是不存在IP地址,所以要像问题三中那样,不断发送SYN-ACK确认报文,直到重发到一定次数才会放弃,但这样同样消耗资源。
解决方法:
- 缩短SYN Timeout时间。由于SYN洪水攻击的效果取决于服务器上保持的半连接数,这个值=SYN攻击频率*SYN Timeout,所以通过缩短从接收到SYN报文到确认这个报文失效并丢弃的时间,可以成倍地降低服务器的载荷
- 设置SYN Cookie。给每一个连接请求的IP地址分配一个Cookie,如果短时间内连续收到某个IP地址的大量重复SYN报文,就认定收到了攻击,以后这个IP地址的报文将直接丢弃
- 使用防火墙。SYN 洪水很容易就能被防火墙拦截
二. 释放连接------四次挥手
TCP连接的释放可以用"四次挥手"的过程来描述。数据传输结束后,通信双方都可以释放连接。现在客户端和服务器都处于ESTABLISHED状态。释放连接的过程如下图所示:
1. 四次挥手过程描述
【1. 客户端主动断开连接,调用close(fd)发送释放连接的FIN报文,第一次挥手】
客户端进程调用close(fd)关闭套接字,操作系统发送释放连接的FIN结束报文,并停止发送数据,主动关闭TCP连接 。在结束报文的首部,标志位FIN=1,序号字段seq=u,等于前面已发送的数据的最后一个字节的序号+1.此时客户端进入FIN_WAIT_1(终止等待)状态,等待服务器发送确认报文。
TCP规定,FIN报文即使不携带数据,也要消耗一个序号。与SYN报文一样
【2. 服务器收到客户端发送的结束报文,发出确认报文段,第二次挥手】
服务器收到客户端发送的释放连接的FIN结束报文,立即发送确认报文,确认号ack=u+1,序号seq=v,等于服务器前面已发送的数据的最后一个字节的序号+1。服务器进入CLOSE_WAIT(关闭等待)状态。TCP服务器进程此时通知上层应用程序,客户端不向服务器发送数据了,但服务器若有数据发送,客户端仍要接收,此时TCP连接处于半关闭(half-close)状态。
客户端收到服务器的确认报文后,进入FIN_WAIT2(终止等待2)状态,等待服务器发送的FIN结束报文
【3. 服务器调用close(connfd)关闭套接字,释放连接,发送FIN结束报文,第三次挥手】
当服务器没有要向客户端发送的数据时,其应用进程调用close(connfd)通知TCP释放连接,向客户端发送FIN结束报文。结束报文中,FIN=1,假定序号seq=w(半关闭状态,服务器可能还发送了一些数据 ),同时还必须重复上次已发送过的确认号ack=u+1。此时,服务器进程进入LAST_ACK(最后确认)状态,等待客户端确认。
【4. 客户端收到服务器的FIN结束报文,发送确认报文,第四次挥手】
客户端在收到服务器的FIN结束报文后,向服务器发送ACK确认报文。在报文首部中,ACK=1,确认序号ack=w+1,序号seq=u+1(第一次挥手的FIN报文消耗一个序号 )。然后客户端进入TIME_WAIT(时间等待)状态
此时TCP连接还没有释放掉,必须经过时间等待计时器(TIME_WAIT timer) 设置的时间2MSL后,A才进入CLOSED状态,时间MSL(Maximum Segment Lifetime,最长报文寿命)即一个TCP报文存活的最长时间 。RFC793建议2分钟,现在可以根据情况使用更小的MSL值。因此客户端进入TIME_WAIT状态,要经过4分钟才进入CLOSED状态,才可以建立下一个连接,当客户端撤销相应的传输控制块TCB,才结束这次TCP连接
服务器只要收到客户端的确认报文,就进入CLOSED状态。同样,服务器在撤销相应的传输控制块TCB后,就结束此次TCP连接
可以发现,先发起释放连接请求的一方,后结束TCP连接
2. TCP连接释放相关问题
问题1:为什么建立连接是三次握手,关闭连接是四次挥手
首先,建立连接也可以是四次握手,中间的SYN和ACK可以分成两次报文。其次关闭连接,中间服务器给客户端发送的确认报文和FIN结束报文也可以合并,但可能服务器方还有数据需要传输,FIN结束报文在上层调用close()时发送,此时服务器已没有数据传输。
发送FIN报文,只是表示本端不再继续发送数据,但还可以接收数据。TCP通信时全双工的,收到FIN报文,只是关闭一个方向的连接,此时TCP处于半关闭状态
问题2:为什么客户端在TIME_WAIT需要等待2MSL才进入CLOSED状态
理由一:保证客户端发送的最后一个ACK报文能到达对端,保证可靠的终止TCP连接。因为如果出现网络拥塞,该报文可能丢失,因而会使LAST_ACK状态的服务器收不到确认报文,而超时重传FIN报文,而客户端能在2MSL时间内收到重传的FIN报文。接着客户端重传一次确认,重新启动2MSL计时器。最后双方正常进入CLOSED状态
理由二:防止已失效的连接请求报文出现在本次TCP连接。客户端在发送完最后一个ACK报文后,再经过2MSL后,就可以使本次TCP连接持续时间内所产生的所有报文都从网络上消散。这样可以使下一个新的TCP连接不会出现之前旧的请求报文
问题3:TIME_WAIT状态何时出现?TIME_WAIT会带来哪些问题?
TIME_WAIT状态是主动发起关闭连接的一方在收到对方发送的FIN结束报文,并本端发送ACK报文后的状态。
TIME_WAIT的引入是为了让TCP报文得以自然消散,同时为了让被动关闭的一方能够正常关闭连接
- 服务器主动关闭连接:短时间关闭大量客户端连接,会出现大量TIME_WAIT状态,此时TCB并没有释放,占据大量的tuple(源IP地址、目的IP地址、协议号、源端口、目的端口),严重消耗着服务器的资源。
- 客户端主动关闭连接:短时间内大量的短连接,会大量消耗客户端主机的端口号,毕竟端口号只有65535个,断开耗尽了,后续就无法启用新的TCP连接了。
问题4:解决TIME_WAIT状态引起的bind()函数执行失败
问题场景:因为主动关闭连接最终会进入TIME_WAIT状态,此时bind的IP地址,端口号都没有释放,重新启动服务会导致bind端口号失败。
解决方法:可以使用setsockopt()函数,设置socket描述符的SO_REUSEADDR选项,该选项可以让端口号被释放后立即再次使用,表示允许创建端口号相同但IP地址不同的多个socket描述符
问题5:半连接,半打开,半关闭的区别
半连接:在TCP连接建立的三次握手中,主动发起连接请求的一方不发最后一次的ACK确认报文,使得服务器阻塞在SYN_RCVD(同步收到)状态
半打开:如果TCP通信一方异常关闭(如断网,断电,进程被kill掉),而通信对端并不知情,此时TCP连接处于半打开状态,如果双方不进行数据通信,是无法发现问题的。解决方法是引入心跳机制,设置一个保活计时器(keepalive timer) ,以检测半打开状态,检测到就发送RST复位报文,重新建立连接
半关闭:主动发起连接关闭请求的一方A发送了FIN结束报文段,对端B回复了ACK确认报文段后,B并没有立即发送本端的FIN结束报文段给A。此时A端处于FIN_WAIT-2(结束等待2)状态,A仍然可以接收B发送过来的数据,但是A已经不能再向B发送数据了。这时的TCP连接为半关闭状态。
补充:
在Socket编程中,服务器Listen的第二个参数
三次握手是操作系统自助完成的,但服务器需要accept返回,才会拿到客户端的连接信息。如果服务器没有accept,这个连接就是全连接 。backlog是全连接队列的容量-1。
Linux内核协议栈为TCP连接管理使用两个队列:
- 半连接队列(用来保存SY_SENT和SYN_RECV状态的请求)
- 全连接队列(accept队列,用来保存处于ESTABLISHED状态,但应用层没有调用accept取走的请求)
三. TCP状态转换
为了更清晰地看出TCP连接的各种状态之间的关系,下图给出了TCP的状态转换示意图。
说明:紫色框框是TCP状态,红色是服务器进程的正常状态转换,蓝色是客户端进程的正常状态转换,黑色是异常变迁,即出现问题时的状态转换。
服务端状态转化:
- 【CLOSE -> 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状态,等待客户端发送FIN结束报文的ACK确认报文
- 【LAST_ACK -> CLOSED】:服务器收到了对FIN结束报文的ACK确认报文,释放连接,撤销传输控制块TCB,释放相应的连接管理资源
客户端状态转化:
- 【CLOSED -> SYN_SENT】:客户端调用connect(),发送SYN同步报文段
- 【SYN_SENT -> ESTABLISHED】:connect()调用成功,进入ESTABLISHED状态,开始读写数据
- 【ESTABLISHED -> FIN_WAIT1】:客户端主动调用close(),向服务器发送FIN结束报文段,同时进入FIN_WAIT1状态
- 【FIN_WAIT1 -> FIN_WAIT2】:客户端收到服务器发送的对FIN结束报文的ACK确认报文,则进入FIN_WAIT2状态,开始等待服务器的FIN结束报文
- 【FIN_WAIT2 -> TIME_WAIT】:客户端收到服务器发送的FIN结束报文,进入TIME_WAIT状态,并发送最后一个ACK确认报文
- 【TIME_WAIT -> CLOSED】:客户端等待2MSL时间,才会进入CLOSED状态,释放连接资源
结束语
本篇博客到此结束,感谢看到此处。
欢迎大家纠错和补充
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。