socket系统调用背后:Linux内核做了什么?
-
listen 与 accept
内核在我们调用listen方法后,就已经为这个监听端口建立了SYN队列和ACCEPT队列,当客户端使用connect方法向服务器发起TCP连接,客户端的SYN包到达服务器后,内核会将这一信息放到SYN队列,同时回一个SYN+ACK包给客户端,客户端再次发来了ACK包后,内核会把连接从SYN队列中取出,再把这个连接放到ACCEPT队列中。
服务器调用accept方法建立连接时,实际上就是直接从ACCEPT队列中取出已经建好的连接。
-
syn flood攻击
SYN Flood 是一种广为人知的 DoS(拒绝服务攻击) 想象一个场景:客户端大量伪造 IP 发送 SYN 包,服务端回复的 ACK+SYN 去到了一个「未知」的 IP 地址,势必会造成服务端大量的连接处于 SYN_RCVD 状态,而服务器的半连接队列大小也是有限的,如果半连接队列满,也会出现无法处理正常请求的情况。
如何应对syn flood攻击?
- 增加 SYN 连接数:tcp_max_syn_backlog
- 减少
SYN+ACK
重试次数:tcp_synack_retries - SYN Cookie 机制
-
-
send
Tcp连接建立时,就可以判断出双方网络之间最适宜的、不会再次切分的报文大小,TCP层把它叫做MSS(最大报文段长度)。应用程序调用send方法后,内核的主要任务是把用户态的内存内容拷贝到内核态的TCP缓冲区,如果内核缓冲区暂时不足,会在超时时间内等待。TCP套接口的send调用成功返回仅仅表示我们可以重新使用应用进程缓冲区,它并不是告诉我们对方收到数据。TCP发给对方的数据,对方在收到数据时必须给矛确认,只有在收到对方的确认时,本方TCP才会把TCP发送缓冲区中的数据删除。
-
recv
首先内核为TCP准备了两个队列,receive队列是允许用户进程直接读取的,它是将已经接收到的TCP报文,去除了TCP首部、排好序放入的、用户进程可以直接按序读取的队列,out_of_order队列存放乱序报文。
当调用recv调用时,对接收到的有序报文会直接进入receive队列,无序报文会进入out_of_order队列并排序后放入receive队列,之后内核会将TCP缓冲区中的内容拷贝到应用程序的用户态内存中,最后返回拷贝的字节数。
-
close
close函数对已连接的套接字执行 close 操作就可以,若成功则为 0,若出错则为 -1。这个函数会对套接字引用计数减一,一旦发现套接字引用计数到 0,就会对套接字进行彻底释放,并且会关闭TCP 两个方向的数据流。
- 套接字引用计数是什么意思呢?
因为套接字可以被多个进程共享,你可以理解为我们给每个套接字都设置了一个积分,如果我们通过 fork 的方式产生子进程,套接字就会积分 +1, 如果我们调用一次 close 函数,套接字积分就会 -1。这就是套接字引用计数的含义。
- close 函数具体是如何关闭两个方向的数据流呢?
在输入方向,系统内核会将该套接字设置为不可读,任何读操作都会返回异常。
在输出方向,系统内核尝试将发送缓冲区的数据发送给对端,并最后向对端发送一个 FIN 报文,接下来如果再对该套接字进行写操作会返回异常。
-
在客户端在send数据后立刻close,会导致服务端向客户端发送响应报文时出现异常
解决方法:1.注册一个信号处理函数,对 SIGPIPE 信号进行处理,避免程序莫名退出
2.客户端使用shutdown函数半关闭
-
TIME_WAIT 状态:
只有主动断开的那一方才会进入 TIME_WAIT 状态,且会在那个状态持续 2 个 MSL(Max Segment Lifetime)
TIME_WAIT 状态存在的原因:
-
数据报文可能在发送途中延迟但最终会到达,因此要等老的“迷路”的重复报文段在网络中过期失效,这样可以避免用相同源端口和目标端口创建新连接时收到旧连接姗姗来迟的数据包,造成数据错乱。TIME_WAIT 等待时间是 2 个 MSL,已经足够让一个方向上的包最多存活 MSL 秒就被丢弃,保证了在创建新的 TCP 连接以后,老连接姗姗来迟的包已经在网络中被丢弃消逝,不会干扰新的连接。
-
确保可靠实现 TCP 全双工终止连接。关闭连接的四次挥手中,最终的 ACK 由主动关闭方发出,如果这个 ACK 丢失,对端(被动关闭方)将重发 FIN,如果主动关闭方不维持 TIME_WAIT 直接进入 CLOSED 状态,则无法重传 ACK,被动关闭方因此不能及时可靠释放。
-
为什么时间是两个MSL?
- 1 个 MSL 确保四次挥手中主动关闭方最后的 ACK 报文最终能达到对端
- 1 个 MSL 确保对端没有收到 ACK 重传的 FIN 报文可以到达
-
2MS = 去向 ACK 消息最大存活时间(MSL) + 来向 FIN 消息的最大存活时间(MSL)
socket相关控制参数
-
keepalive
-
半打开 half open:如果在未告知另一端的情况下通信的一端关闭或终止连接,那么就认为该条TCP连接处于半打开状态。 这种情况发现在通信的一方的主机崩溃、电源断掉的情况下。 只要不尝试通过半开连接来传输数据,正常工作的一端将不会检测出另外一端已经崩溃。
-
机制原理:
定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
-
-
Nagle算法与延迟回收
-
Nagle 算法要求,当一个 TCP 连接中有在传数据(已经发出但还未确认的数据)时,小于 MSS 的报文段就不能被发送,直到所有的在传数据都收到了 ACK。同时收到 ACK 后,TCP 还不会马上就发送数据,会收集小包合并一起发送。
-
如果收到一个数据包以后暂时没有数据要分给对端,它可以等一段时间(Linux 上是 40ms)再确认。如果这段时间刚好有数据要传给对端,ACK 就可以随着数据一起发出去了。如果超过时间还没有数据要发送,也发送 ACK,以免对端以为丢包了。这种方式成为「延迟确认」。
-
-
重用地址与重用端口
SO_REUSEADDR :服务端主动断开连接以后,需要等 2 个 MSL 以后才最终释放这个连接,重启以后要绑定同一个端口,默认情况下,操作系统的实现都会阻止新的监听套接字绑定到这个端口上。启用 SO_REUSEADDR 套接字选项可以解除这个限制,默认情况下这个值都为 0,表示关闭。
SO_REUSEPORT:默认情况下,一个 IP、端口组合只能被一个套接字绑定,Linux 内核从 3.9 版本开始引入一个新的 socket 选项 SO_REUSEPORT,又称为 port sharding,允许多个套接字监听同一个IP 和端口组合。
RST报文的出现与处理
在 TCP 协议中 RST 表示复位,用来异常的关闭连接,发送 RST 关闭连接时,不必等缓冲区的数据都发送出去,直接丢弃缓冲区中的数据,连接释放进入CLOSED
状态。而接收端收到 RST 段后,也不需要发送 ACK 确认。
RST出现的几种场景:
-
因为系统崩溃或者网络异常导致对端无 FIN 包
通过给 read 操作设置超时来解决,Linux 系统的 TCP 协议栈会不断尝试将发送缓冲区的数据发送出去,大概在重传 12 次、合计时间约为 9 分钟之后,协议栈会标识该连接异常,这时,阻塞的 read 调用会返回一条 TIMEOUT 的错误信息。如果此时程序还执着地往这条连接写数据,写操作会立即失败,返回一个 SIGPIPE 信号给应用程序。
-
对端有FIN包发出
对端如果有 FIN 包发出,可能的场景是对端调用了 close 或 shutdown 显式地关闭了连接,也可能是对端应用程序崩溃,操作系统内核代为清理所发出的。从应用程序角度上看,无法区分是哪种情形。