目录
下面介绍的这些都属于通信细节,在应用层的角度是感觉不到的
超时重传
引入
在之前介绍tcp报头字段时,我们提到,tcp有确认应答机制 --tcp协议介绍,协议段格式(端口号,首部长度,窗口大小,序号,确认序号,6个标志位),流量控制,确认应答机制,捎带应答,三次握手的双方认知不一致问题-CSDN博客
- 当我们收到应答后,就可以确定对方已经收到了自己发送的数据
- 但也仅此而已
- 如果客户端没有收到应答,是无法确定具体情况的 -- 自己发出的数据丢了 / 发出去了,但在某个设备下排队 / 对方收到了,但应答丢了
所以,只能规定出一个时间段,如果超过这个时间段还没有收到,那就判定是丢包了,这时客户端将会超时重传
介绍
是一种网络通信协议中的错误控制机制,用于确保数据包在不可靠的网络环境中能够正确传输
去重
引入
但仔细想想:
- 如果因为数据丢了 / 因网络问题阻塞在某处(这就是真正的超时了)而重传还能理解
- 只是应答丢了就重传,岂不是服务器会收到重复报文(报头一样,数据也一样)?
而收到重复报文,也属于不可靠的一种,需要去重
解决
通过序号判断是否重复
- 每个报文的序号基本不会重复 -- 序列号32位,轻易不会重复 / 采用随机初始序列号,即使在缓冲区的偏移量相同,序号也不会一样
- 即使重复也不会冲突 -- 如果重复也是不同的连接之间,而不同连接的ip/port是不同的,所以不会冲突
所以,我们的序号不仅可以用来排序/允许少量应答丢失,还可以去重
那么,现在我们已经知道了数据丢失/阻塞可以重传,且不会重复
那么,回到最关键的问题,这个特定的时间间隔如何确定呢?
如何确定超时时间
介绍
和网络状态相关
网络好
- 数据传输速度很快,如果设定的时间较长,丢包的数据需要等待很久才能补发,可能会影响通信效率
网络不好
- 设定的时间不能太短
- 假设正常的单向通信至少需要100ms,设定的间隔却<100,那岂不是会有大量的数据会被补发?
所以,这个间隔必须是随着网络状态动态变化的
如何设置
通信建立机制
三次握手
图解
connect
只负责发起三次握手(也就是完成第一步),剩下的由双方主机内部自行完成,上层的系统调用并不过多参与
- 当客户端发起三次握手后,自己的连接状态变为SYN_SENT
- 当服务器收到第一条报文后,自己的状态变为ESTABUSHED
这些连接状态是由双方系统自己维护的
- 这些值相当于宏
- 状态改变=不同的宏被填充到"tcp_socket结构体"的状态字段上
accept
并不参与三次握手,它只会把建立好的连接拿过去
通信
图解
当客户端主动断开连接后
- 服务器调用的read函数返回0,然后结束通信流程,进入四次挥手流程
介绍
调用读/写函数进行通信
- 本质上是在向tcp的缓冲区读写
并且,在对方收到报文后,会返回应答
- 用户无法感知,由os自主发送
四次挥手
图解
介绍
如果要断开连接,会主动向对方发送带有FIN标志位的报文
- 断开连接的本质 -- 断开连接方没有数据发送给对方了(ACK不属于数据,属于管理报文,不含数据)
- 因为tcp的可靠性,服务器收到报文后,会给客户端返回应答(为了可靠地告知对方,通知到位了自己才能放心地断开)
- 这就是两次挥手的过程
而发送数据是双方都可以发,所以服务器也需要给客户端发送FIN
- 当客户端发送完第二个ACK后,等待一段时间后,状态就改为CLOSED
- 这样就完成了四次挥手
为什么是三次握手,四次挥手呢?
本质
握手里
- 客户端是把要发送的报文带上了应答属性发送的,实际上应该是4次
而挥手里
- 它也可以按照握手来把中间的两次报文揉在一起(捎带应答),变成三次挥手
所以,重点不是几次,而是这保证了双方各自可靠地发送了一次消息!
- 而且,这其中少了任何一个报文,都是不完整的
- 要么功能不完整
- 要么缺少应答
为什么握手是3次
那明明过程都是四次,为什么握手却一直被归并成3次呢?
可合并
- 因为,只要客户端发送建立连接的请求,服务器必须无条件同意 -- 这也是它叫服务器的原因,就是给客户端提供服务的
- 所以,服务器要给客户端发送带有SYN的报文是必然的,既然如此,合并在一起也是顺理成章
可靠验证全双工通路的畅通 (建立连接的本质)
这三次流程中,可以可靠地保证双方都完成了一次收发
- 也就是在可靠地验证全双工通路是否通畅
建立连接的本质,就是在测试当前双方的网络环境是否适合通信
- 通信的本质,就是在互相收发数据
但是,我们重新审视一下三次握手的过程,明明前两次报文已经完成了双方的一次收发
为什么还要有第三次呢?
- 因为第三次保证了服务端的发送能力
- 如果没有收到应答,它怎么知道自己发送成功没
就像列车在正式运营前,都要先经过测试,确认没啥问题了才会使用
让客户端承担失败成本 (SYN洪水)
如果是2次握手或者更多(偶数次),会让服务器成为双方之中先建立连接结构体的一方
如果客户端有恶意行为(SYN洪水):
- 如果客户端向服务器发送大量SYN报文,而服务器正常处理(建立连接),然后发回应答,但客户端可以选择丢弃,并重复发送SYN
- 就会导致 -- 服务器仍然会保存有连接,且不方便释放,会造成大量资源被占用,但处于闲置状态
如果客户端挂掉/其他原因导致连接失败:
- 客户端自然会在进程结束后释放资源,但服务端不知道啊,还认为是正常连接
- 那么服务端会积压很多连接结构体
像上面这样,如果让服务器对连接建立失败做兜底的话(让服务器承受失败的成本)
- 这对服务器来说很不公平,可能也会影响其他客户端
- 因为服务器:客户端=1:n
- 假设连接建立失败的概率是1%,而与服务器连接的客户端有1w个,那服务器就要承担100个连接建立失败的成本 -- 确实很不合理吧?
但在三次/更多次(奇数次)握手中,客户端优先认为连接创建成功->优先创建结构体
- 而服务器则根据是否接收到最后一条应答,来判断是否建立成功
- 也就是,由客户端来承担建立失败的成本
所以,奇数次握手可以确保在一般情况下,握手失败的连接成本是嫁接在客户端的
- 而3次是满足奇数次握手+没有明显硬伤(指只进行1次握手,很明显非常不合理)的最小次数
为什么挥手是4次
断开连接不像建立连接那样,服务器必须马上回应
- 可能在客户端想要断开连接的时候,服务器还有数据没发送完呢
- 在连接没有彻底断开前,服务器还是可以发送剩余数据给对方的,对方也能接收到
这种情况下,还需要再等一会,服务器才能给客户端发送带有FIN的报文(表明自己没有数据发送了)
- 刚好可以合并的情况反而是一种巧合,一种偶然
所以,之后我们也采用3次握手,4次挥手的称呼
半连接
介绍
是一种网络连接状态,通常出现在TCP连接的建立过程中
- 当一方试图与另一方建立连接,但另一方尚未完全响应时,就会产生半连接状态
半连接状态通常发生在三次握手的2-3步之间
- 即服务器已发送SYN-ACK包,但尚未收到客户端的ACK包
- 此时,服务器端的连接状态称为“半连接”
问题
虽然三次握手将失败成本嫁接到客户端上,但服务端那边:
- 即使还没有建立成功,依然也要维护好结构体
- 所以,依然会受到恶意攻击的影响
肉机
介绍
如果有多个客户端种了木马病毒后,可能会收到黑客影响,同一时间去和某个服务器建立连接
- 用专业术语来说,肉机 -- 被攻击者控制并用于执行恶意任务的计算机
- 虽然是正常的建立连接的行为,但因为是同一时刻去做,并且是大量行为
- 所以在一段时间内可能会将服务器的连接资源消耗殆尽
遇到这种情况,就得引入新策略
- 设置防火墙,限流啦什么的
查看状态
三次握手
listen后,accept前
当服务器启动后,即使没有accept,只要处于监听状态,就能用netstat查看,双方都建立好了连接
服务器所在主机的连接情况:
这里看到的local ip和我们显示的主机ip并不相同:
- 因为[云服务器上]为我们显示的公网ip是模拟出来的,这里看到的local ip才是真正的内网ip
连接是双向的,在客户端主机上也可以看到这个连接:
- 这里看到的local同理,是真实的内网ip
所以,可以看出来,tcp连接是否成功 与 是否调用accept 无关
- 三次握手是双方os内部自主完成的
全连接队列
引入
因为accept不参与三次握手的过程,它只是会将新建立好的连接拿上去使用
- 那么,就必然存在还未来得及被拿上去的连接结构体,且不止一个
- 那么os就得维护这些已经建立好的连接,以供上层来获取
要管理,就得先描述再组织,组织的方式是队列
- 有新的连接了就入队列
- 上层调用accept,从队列中拿出连接和特定文件相关联,返回给用户新创建的fd
而以上过程就很像cp模型,上层取,底层入队列
介绍
上面介绍的队列就是全连接队列
- 因为能入队列的都是成功建立连接的
队列为什么不能太长?
- 如果太长,当服务器因忙碌而无法获取全连接队列中的连接,就会平白占据大量资源来维护连接,且短时间内不会释放
- 服务器都这么忙了,万一它正在处理的上层逻辑中需要这些空间呢?这些却久久不释放,反而会耽误服务器的效率
- 所以,没有必要设置的太长
如果没有这个全连接队列呢?
- 忙的时候还好,不会平白占用资源
- 那万一之后服务器闲下来了呢?它还得等有客户端连接,经过三次握手后,才能拿到连接
- 那服务器就会闲置下来,多亏啊
- 创建全连接队列,相当于用空间换时间了,不让服务器处于空闲状态,一有空位就补上,可以提高整体的效率,让资源得到充分利用
比如:
- 就像去饭馆吃饭,生意非常火爆,会有老板给顾客说"我这人满了,你们去别处吃吧"吗
- 不会的吧
- 一般老板会在门口摆点椅子啥的,先让顾客拿着号等待,等有顾客出来就直接让等待的按照号码进去
listen的第二个参数
listen()的第二个参数值+1,描述的就是这个队列最大容纳量
- 刚刚已经讨论过了,这个参数不能太大,也不能没有
- 那适合的长度是多少呢?
- 由服务器资源状态,也就是主机的资源决定
全队列连接满时
引入
当我们将这个参数值改为1,并增加第三个客户端时,会出现建立失败的情况:
服务器:
客户端:
原理
注意:
- 状态的改变是一定要在完成下一个状态的前置条件的情况下发生
客户端是建立成功的状态
- 所以第三条ACK它是一定发送了的
服务器这边状态维持在了SYN_RCVD
- 因为队列容量的限制,它不允许有超出数量的连接建立成功,但又不能修改客户端已经发送了应答的事实
- 所以只能将应答直接丢弃,从而让连接无法建立成功
- 所以状态无法修改到下一个状态
双方认知不一致
这就是之前在介绍tcp报头中的标志位提到的,此时双方对是否建立完成的认识不一致:
并且承担建立失败的成本在客户端
- 因为只有客户端是连接成功的
半连接队列
引入
当然,服务端也是付出了成本的,即使没有连接成功,也有在维护结构体
- 但这个结构体会在一段时间后自动释放
这个与服务器未完成连接的客户端,在下次查看时,已经查不到了:
而客户端仍在运行,且还是连接成功的状态:
可以知道,在服务器视角,未完成连接的连接维持的时间很短,它并不会一直保存状态为SYN_RECV
介绍
被连接的一方处于SYN_RECV,这种状态叫做半连接
- 它也有对应的结构体,也是需要维护它的,所以也就有了半连接队列
- 半连接的长度由系统自主决定
- 这里面的结点不会长时间被维护,定期会清理
而连接成功的结构体维持时间较长
重新建立连接 (并没有触发RST)
当我们发送数据时,会重新触发三次握手,但依然卡在SYN_RECV状态:
- 所以数据并没有被成功发送,还停留在客户端的发送缓冲区中
- 因为连接并没有建立成功
在连接不成功的情况下,如果客户端发来的ACK丢包了,导致服务器无法确认自己发送的SYN+ACK是否成功
- 这种情况下,服务器才会判断握手不成功,从而触发RST机制
而这里的服务器实际上是收到了应答的(因为客户端是建立成功的状态)
- 它只是不做处理,可以认为是收到后丢弃掉了
- 所以这里的重新连接,并不是因为触发了RST机制
还有一点:
- 如果数据能发送给对方,才会触发对方的RST机制,发送给我带有RST的报文,来重新建立连接
- 如果不能,对方直接去重连了
- 这里的数据压根没发出去,所以不是触发了RST
SYN洪水
半连接队列不会太吃资源
- 因为数量有限,存在使用短
一个连接,会先进入半连接状态,连接成功后才会进入全连接状态
- 所以,当有大量客户端一直发起连接请求时,我们并不担心吃资源的问题
- 而是要考虑它会一直占用半连接队列的资源,使得其他正常的连接无法进入半连接队列,从而无法进入全连接队列,也就无法被accept获取,这个客户端就无法连接
- 这才是真正意义上的SYN洪水,前面我们讨论的并不全面
就像选课/抢票啥的
- 大量客户端向服务器发送请求,会显示网页繁忙
- 就是因为有大量其他用户占用了队列资源,而你没挤进去,所以无法建立连接
- 不断刷新的话,可能一会就进去了 -- 是因为你很幸运地在半连接队列中,并被挑中进入了全连接队列,从而连接上服务器
四次挥手
一方断开连接后
我们将服务器作为先断开的一方,然后可以看到他的状态变为FIN_WAIT2:
客户端这边变为CLOSE_WAIT:
情况与图中的流程相符
另一方也断开
先断开连接的一方,在对方也断开连接时,会进入TIME_WAIT状态
- 但要看到上面这种情况,我们首先得让连接被上层获取到
- 不然就直接释放了
- (后面会说原因)
在连接被获取后,客户端再退出,服务器这边的连接状态:
虽然符合我们说的,但是为什么呢?
- 客户端和服务器都相继退出,肯定是已经完成了四次挥手的,怎么主动断开的一方仍然维持着TIME_WAIT的状态呢?
- 说明连接还未完全释放
- 并且一段时间后,这个连接就查看不到了,这时连接才被释放
所以TIME_WAIT->CLOSE之间会发生什么呢?
TIME_WAIT -> CLOSE
TIME_WAIT状态
如果我们在这个期间重新启动服务器,会出现端口号绑定失败的情况:
也就是说,当我们的服务器先主动断开时.会进入TIME_WAIT状态
- 此时虽然进程已经退出了,但连接并没有被彻底断开 -> ip和port依然被使用
如果我们立即重启进程,仍然使用上次的端口号
- 而端口号无法被两个进程同时绑定,所以会绑定失败
但是,如果是紧急需要重启的服务,比如双十一时的购物平台的服务器,可能会被大量客户端的连接搞挂了
- 这样服务器就成为了主动断开连接的一方,且会强制进入TIME_WAIT
- 而TIME_WAIT状态会维持30~60s,这取决于底层timeout的时间,也取决于用户层的设置
- 在这样的情景下,如果服务器不能立即重启,它将会损失大量交易订单 ,这是我们不能允许的
如何解决 -- setsockopt()
通过设置套接字属性,让它可以允许地址复用
level
- 一般设置为SOL_SOCKET,表示在套接字层
optname
- 要设置的具体选项。不同的level有不同的选项名
- 第一个:表示ip和port都复用,第二个,老版本的
optval
- 指向包含选项值的缓冲区
- 不同的选项可能需要不同类型的数据,比如int,struct timeval等
- 就是用来设置标志是否有效,我们定义一个int opt=1即可
optlen
- optval缓冲区的大小
设置好后
设置好后,重新上面的操作,服务器就可以复用端口号了
- 这里是我们在代码里让服务器关闭连接的,所以是FIN_WAIT2状态
- 并且上一个处于TIME_WAIT的连接依然存在
为什么一般是服务端出现TIME_WAIT状态
为什么我们都是在服务器看到无法复用端口的情况呢?
- 因为客户端使用的是系统分配的随机端口,客户端退出又启动时,使用的很少是上一个端口号
- 而服务器一般是固定端口号
接下来,我们详细介绍一下TIME_WAIT状态
TIME_WAIT状态
介绍
也称为“连接终止定时等待状态”
- tcp协议规定的,主动关闭连接的一方要处于TIME_WAIT状态(通常是 2 倍的最大报文段寿命,2*MSL)的时间后才能回到CLOSED状态
MSL
一个报文从一端发到另一端的过程中,就是处于网络里
- 如果是一个正常报文,那这个时间也就是毫秒级的,叫最大传送时长
- 如果一个报文被阻塞住了,它在网络里有特定的存活时间,其最大存活时长就是MSL,这个时间不是固定的
- 要区分这两个时间概念
MSL在RFC1122中规定为2分钟,但不同os上的实现不同,centos/ubuntu上是60s
查看:
这个时间可以修改,但是否起效,不好说
所以,一般这个timeout的时间是60-120s
为什么会有TIME_WAIT状态
当双方要断开连接时,可能数据发完了,但不一定收到了,网络中可能还残留有互相发送的数据
- 等待2个MSL正好是数据一来一回的最大时间
不能说是完全保证可以
- 而是较大概率可以让双方接收到还未收到的数据并处理返回 / 丢掉这些数据
保证四次挥手的正确性
在四次挥手中,前面两次双方连接都还未释放
- 所以如果这两次的报文丢失,还可以进行补发措施
当客户端一旦发出ACK后它这边的四次挥手就完成了 -> 释放了连接结构
- 一旦最后一个ACK丢失了,就没人能补发,就导致服务器只能一直处于LAST_ACK状态中
- 因为没有收到应答
那这样,服务器可能认为是自己的数据对方没收到,就会补发FIN
- 但是,对方此时已经释放了,对方无法响应
- 如果发送多次依然没有应答,自己也就退出了
但是,这属于异常处理
- 我们不能依赖异常处理来解决问题,尽量要让他们正确地完成四次挥手
所以,要让发送方停留一段时间
- 也就是维持在TIME_WAIT状态
- 如果ACK丢失,发送方还能收到对方补发的FIN,并返回应答
- 这样可以保证有较大的概率让四次挥手正确地挥手完
处理旧数据
因为tcp具有可靠性
- 这些一直没收到的数据很可能早就补发了
- 而处理掉这些阻塞住的数据,是为了防止影响下一次的通信
- 要是双方立即又开始通信,且ip,端口号均未改变,可能会在新一轮的通信中收到旧报文
那要是消散不彻底怎么办?
- 双方的报文都有序号,而起始序号是随机的
- 这个随机序号可以规避黑客,规避历史报文对现在通信的影响
- 也就是说,即使收到了,可以通过序号来判断是否是合法数据
这也能证明为什么没有被accept获取过的连接在释放时,并不会维持TIME_WAIT,直接就断开了
- 因为压根就没通信过,也就不需要将历史数据消散
总结