socket系统调用背后:Linux内核做了什么?

  1. 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 机制
  1. send

Tcp连接建立时,就可以判断出双方网络之间最适宜的、不会再次切分的报文大小,TCP层把它叫做MSS(最大报文段长度)。应用程序调用send方法后,内核的主要任务是把用户态的内存内容拷贝到内核态的TCP缓冲区,如果内核缓冲区暂时不足,会在超时时间内等待。TCP套接口的send调用成功返回仅仅表示我们可以重新使用应用进程缓冲区,它并不是告诉我们对方收到数据。TCP发给对方的数据,对方在收到数据时必须给矛确认,只有在收到对方的确认时,本方TCP才会把TCP发送缓冲区中的数据删除。

  1. recv

首先内核为TCP准备了两个队列,receive队列是允许用户进程直接读取的,它是将已经接收到的TCP报文,去除了TCP首部、排好序放入的、用户进程可以直接按序读取的队列,out\_of\_order队列存放乱序报文。


当调用recv调用时,对接收到的有序报文会直接进入receive队列,无序报文会进入out\_of\_order队列并排序后放入receive队列,之后内核会将TCP缓冲区中的内容拷贝到应用程序的用户态内存中,最后返回拷贝的字节数。

  1. 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 显式地关闭了连接,也可能是对端应用程序崩溃,操作系统内核代为清理所发出的。从应用程序角度上看,无法区分是哪种情形。

标签: Linux, 报文, C++, TCP, 接字, 连接

相关文章推荐

添加新评论,含*的栏目为必填