大家好,我是石页兄,编程界的一名小学生
上善若水,水善利万物而不争
欢迎 关注微信公众号「架构染色」&& 加群交流
一、概述
学习 JKSJ 《网络编程实战》的笔记
二、关键既要
2.1、三次握手
为什么要三次握手,这个问题的本质是信道不可靠,但是通信双方需要就某个问题达成一致,而要解决这个问
题,无论你在消息中包含什么信息,三次通信(商量)是理论上的最小值。 所以三次握手不是TCP本身的要求,而是为了满足”在不可靠信道上可靠地传输信息”这一需求所导致的
白话握手
- 【客户端】我要跟你建联
- 【服务端】收到,你确定嘛?(关键是要此时要跟客户端再核实一下,万一收到的消息是早些时候在网络中迷失的,实际已无效的消息)
- 【客户端】嗯,我确定
- 【服务端】好的,我准备好了,咱们可以正式互发消息了
2.2、四次挥手
三次握手实际上也是四次,只是SYN 和 ACK 二合一了,但是对于挥手来说,这两步不能合并,因为可能有需要善后工作而不能立即回复FIN
特别注意,主动关闭方接收到对方的 FIN 包,并确认这个 FIN 包。主动关闭方进入 TIME_WAIT 状态,而接收到 ACK 的被动关闭方则进入 CLOSED 状态。经过 2MSL 时间之后,主动关闭方也进入 CLOSED 状态。主动关闭方收到收到对方的一个 FIN,被动关闭方发送FIN后未收到ACK,可能再一次发送 FIN。也就是说每次收到被关闭方的FIN后进入 TIME_WAIT 状态都会持续 2MSL ,MSL是任何 IP 数据报能够在因特网中存活的最长时间,RFC793 中规定 MSL 的时间为 2 分钟,Linux 实际设置为 30 秒;也就是说,Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
白话挥手
- 【客户端】我想关闭连接,可以吗?
- 【服务端】收到,你稍等我一下,有工作需要收尾,做完我通知你
- 【服务端】我这边完事了,你这边确定要关嘛?理论上1MSL分钟内你收到我的消息,2MSL内我收到你的确认回复,要是我没收到我再问你
- 【客户端】当收到FIN后,我确定关,可能1MSL你收不到我的回复,2MSL内我会再收到你的关闭询问,所以我2MSL后我再关闭以确保你收到了。也确保你2MSL后才能收到新链接的数据,而2MSL内收到的都不是新链接的,而是老链接的,这样避免老链接迷失的数据对新链接产生干扰
- TIME_WAIT 的引入是为了让 TCP 报文得以自然消失,同时为了让被动关闭方能够正常关闭;
- 不要试图使用
SO_LINGER
设置套接字选项,跳过 TIME_WAIT; - 现代 Linux 系统引入了更安全可控的方案(如net.ipv4.tcp_tw_reuse),可以帮助我们尽可能地复用 TIME_WAIT 状态的连接。
2.3、net.ipv4.tcp_tw_reuse 优化 TIME_WAIT
tcp_tw_reuse 是内核选项,主要用在连接的发起方。TIME_WAIT 状态的连接创建时间超过 1 秒后,新的连接才可以被复用,注意,这里是连接的发起方,可以复用处于 TIME_WAIT 的套接字为新的连接所用,避免TIME_WAIT占用2MSL不得被用。
但使用有一个前提,需要打开对 TCP 时间戳的支持,即net.ipv4.tcp_timestamps=1
(默认即为 1)。
2.4、SO_REUSEADDR优化”Address in use”
服务器端程序重启之后,若碰到“Address in use”的报错信,导致服务器程序不能很快地重启。是因为服务端程序绑定本地地址和一个端口,然后就监听在这个地址和端口上,等待客户端连接的到来。当服务端重启时,服务端就是socket的主动关闭方,按照TCP协议规范,该条连接的TCP状态会在真正断开之前进入TIME-WAIT态,至少1分钟后才能再次重启成功,这是无法接受的
SO_REUSEADDR
是用户态的选项,此选项用来告诉操作系统内核,如果端口已被占用,但是 TCP 连接状态位于 TIME_WAIT ,可以重用端口。如果端口忙,而 TCP 处于其他状态,重用端口时依旧得到“Address already in use”的错误信息。注意,这里一般都是连接的服务方。
为什么可以复用呢?原因是现代 Linux 操作系统对此进行了一些优化。
-
第一种优化是新连接 SYN 告知的初始序列号,一定比 TIME_WAIT 老连接的末序列号大,这样通过序列号就可以区别出新老连接。
-
第二种优化是开启了 tcp_timestamps,使得新连接的时间戳比老连接的时间戳大,这样通过时间戳也可以区别出新老连接。
在这样的优化之下,一个 TIME_WAIT 的 TCP 连接可以忽略掉旧连接,重新被新的连接所使用。
2.5、合理设置监听队列 backlog 的大小
backlog对程序的连接数没影响,但是影响的是还没有被Accept取出的连接。
服务器TCP内核 内维护了两个队列,称为A(未连接队列)和B(已连接队列),如果A+B的长度大于Backlog时,新的连接就会被TCP内核拒绝掉。所以,如果backlog过小,就可能出现Accept的速度跟不上,A,B队列满了,就会导致客户端无法建立连接。
需要注意的是,backlog对程序的连接数没影响,但是影响的是还没有被Accept取出的连接。
在netty实现中,backlog默认通过NetUtil.SOMAXCONN指定;也可以在服务器启动启动时,通过option方法自定义backlog的大小。
2.6 KeepAlive
TCP 有一个保持活跃的机制叫做 Keep-Alive。在 Linux 系统中以下几个sysctl 变量控制:
- 保活时间:
net.ipv4.tcp_keepalive_time
,默认设置是 7200 秒(2 小时) - 保活时间间隔:
net.ipv4.tcp_keepalive_intvl
,默认设置是 75 秒 - 保活探测次数:
net.ipv4.tcp_keepalve_probes
,默认设置是 9 次探测
所以使用 TCP 自身的 keep-Alive 机制,在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个“死亡”连接。这个时间是是通过 2 小时,加上 75 秒乘以 9 的总和。实际上,对很多对时延要求敏感的系统中,这个时间间隔是不可接受的。
2.7、Nagle 算法和延时 ACK 的组合。
1)Nagle 算法是发送端的优化算法
本质其实就是限制大批量的小数据包同时发送,为此,它提出,在任何一个时刻,未被确认的小数据包不能超过一个。这样,发送端就可以把接下来连续的几个小数据包存储起来,等待接收到前一个小数据包的 ACK 分组之后,再将数据一次性发送出去。
2)延时 ACK算法是接收端的优化算法
接收端需要对每个接收到的 TCP 分组发送 ACK 报文,但是 ACK 报文本身是不带数据的分段,如果一直这样发送大量的 ACK 报文,就会消耗大量的带宽。延时 ACK 算法在收到数据后并不马上回复,而是累计需要发送的 ACK 报文,等到有数据需要发送给对端时,将累计的 ACK捎带一并发送出去。当然,延时 ACK 机制,不能无限地延时下去,否则发送端误认为数据包没有发送成功,引起重传,反而会占用额外的网络带宽。
3)Nagle 算法和延时 ACK 的组合。
Nagle 算法和延时确认组合在一起,增大了处理时延,实际上,两个优化彼此在阻止对方。
如客户端分两次将一个请求发送出去
- 由于请求的第一部分的报文未被确认,Nagle 算法开始起作用;
- 同时延时 ACK 在服务器端起作用,假设延时时间为 200ms,服务器等待 200ms 后,对请求的第一部分进行确认;
- 接下来客户端收到了确认后,Nagle 算法解除请求第二部分的阻止,让第二部分得以发送出去
- 服务器端在收到之后,进行处理应答,同时将第二部分的确认捎带发送出去。
- 可使用 TCP_NODELAY 禁用 Nagle 算法
2.8 select、poll、epoll
1)select
select 是常见的 I/O 多路复用技术,它通过描述符集合来表示检测的 I/O 对象,通过三个不同的描述符集合来描述 I/O 事件 :可读、可写和异常。但是 select 有一个缺点,那就是所支持的文件描述符的个数是有限的。在 Linux 系统中,select 的默认最大值为 1024。没有办法对此进行配置
2)poll
poll 突破文件描述符个数限制;和 select 另一个非常不同的地方在于,poll 每次检测之后的结果不会修改原来的传入值,而是将结果保留在 revents 字段中,这样就不需要每次检测完都得重置待检测的描述字和感兴趣的事件。我们可以把 revents 理解成“returned events”。
- epoll
任一时间只有部分的socket是“活跃”的,但是select/poll每次调用都会[线性扫描]全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对“活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有“活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会
三、最后说一句
我是石页兄,如果这篇文章对您有帮助,或者有所启发的话,欢迎关注笔者的微信公众号【 架构染色 】进行交流和学习。您的支持是我坚持写作最大的动力。
另外技术群中也有许多热心的大佬时常交流互助;可扫描下方笔者微信二维码,加 V 备注“加群”,期待并欢迎您的加入。