NIO 阻塞与非阻塞

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/M_Joes_17/article/details/82118880

Socket读写数据底层原理     

       首先,对于 TCP 通信来说,每个 TCP Socket 在内核中都有一个发送缓冲区和一个接收缓冲区,TCP 的全双工的工作模式及 TCP 的滑动窗口便依赖于这两个独立的 Buffer 及此 Buffer 的填充状态。接收缓冲区把数据缓存入内核,若应用进程一直没有调用 Socket 的 read 方法进行读取的话,则此数据会一直被缓存在接收缓冲区内。不管进程是否读取 Socket,对端发来的数据都会经由内核接收并且缓存到 Socket 的内核接收缓冲区中。read 所做的工作,就是把内核接收缓冲区中的数据复制到应用层用户的 Buffer 里面,仅此而已。进程调用 Socket 的 send 发送数据的时候,最简单的情况(也是一般情况)是将数据从应用层用户的 Buffer 里复制到 Socket 的内核发送缓冲区中,然后 send 便会在上层返回。换句话说,send 返回时,数据不一定会被发送到对端(和 write写文件有点类似),send 仅仅是把应用层 Buffer 的数据复制到 Socket 的内核发送 Buffer 中

       而对于 UDP 通信来说,每个 UDP Socket 都有一个接收缓冲区,而没有发送缓冲区,从概念上来说就是只要有数据就发,不管对方是否可以正确接收,所以不缓冲,不需要发送缓冲区。

TCP/IP 的滑动窗口和流量控制机制

       前面我们提到,Socket 的接收缓冲区被 TCP 和 UDP 用来缓存网络上收到的数据,一直保存到应用进程读走为止。

       对于 TCP 来说,如果应用进程一直没有读取,则 Buffer 满了之后,发生的动作是:通知对端 TCP 协议中的窗口关闭,保证 TCP 套接口接收缓冲区不会溢出,保证了 TCP 是可靠传输的,这个便是滑动窗口的实现。因为对方不允许发出超过通告窗口大小的数据,所以如果对方无视窗口大小而发出了超过窗口大小的数据,则接收方 TCP 将丢弃它,这就是 TCP 的流量控制原理。

       而对于 UDP 来说,当接收方的 Socket 接收缓冲区满时,新来的数据报无法进入接收缓冲区,此数据报就会被丢弃,UDP 是没有流量控制的,快的发送者可以很容易地淹没慢的接收者,导致接收方的 UDP丢弃数据报

传统阻塞模式

       明白了 Socket 读写数据的底层原理,我们就容易理解传统的“阻塞模式”了:对于读取 Socket数据的过程而言,如果接收缓冲区为空,则调用 Socket 的 read 方法的线程会阻塞,直到有数据进入接收缓冲区中;而对于写数据到 Socket 中的线程而言,如果待发送的数据长度大于发送缓冲区的空余长度,则会阻塞在 write 方法上,等待发送缓冲区的报文被发送到网络上,然后继续发送下一段数据,循环上述过程直到数据都被写入到发送缓冲区为止。

       从上述的程来看,传统的 Socket 阻塞模式直接导致每个 Socket 都必须绑定一个线程来操作数据,参与通信的任意一方如果处理数据的速度较慢,则都会直接拖累另一方导致另一方的线程不得不浪费大量的时间在 I/O 等待上,所以,每个 Socket 要绑定一个单独的线程正是传统Socket 阻塞模式的根本“缺陷”。之所以这里加了“缺陷”两个字,是因为这种模式在一些特定场合下效果是最好的,比如只有少量的 TCP 连接通信,双方都非常快速地传输数据,此时这种模式的性能最高。

NIO 非阻塞模式

       现在我们可以开始分析“非阻塞”模式了,它就是要解决 I/O 线程与 Socket 解耦的问题,因此,它引入了事件机制来达到解耦的目的。我们可以认为 NIO 底层中存在一个 I/O 调度线程,它不断扫描每个 Socket 的缓冲区,当发现写入缓冲区为空(或者不满)的时候,它会产生一个Socket 可写事件,此时程序就可以把数据写入 Socket 里,如果一次写不完,则等待下次可写事件的通知;而当发现读取缓冲区里有数据的时候,它会产生一个 Socket 可读事件,程序收到这个通知事件时,就可以从 Socket 读取数据了。

上述原理听起来很简单,但实际上有很多容易陷入的“坑”,如下所述。

  • 收到可写事件时,想要一次性地写入全部数据,而不是将剩余数据放入 Session 中,等待下次可写事件的到来。

  • 写完数据并且没有可写数据的时候,在应答数据报文已经全部发送给客户端的情况下,需要取消对可写事件的“订阅”,否则 NIO 调度线程总是报告 Socket 可写事件,导致 CPU 使用率狂飙。因此,如果没有数据可写,就不要订阅可写事件

    扫描二维码关注公众号,回复: 2926758 查看本文章
  • 如果来不及处理发送的数据,就需要暂时“取消订阅”可读事件,否则数据从 Socket 里读取以后,下次还会很快发送过来,而来不及处理的数据积压到内存队列中,最终会导致内存溢出

此外,NIO 里还有一个容易被忽略的高级问题,即业务数据处理逻辑是使用 NIO 调度线程来执行还是用另外线程池里的线程来执行

如果数据报文的处理逻辑比较简单,不存在耗时和阻塞的情况,则可以直接用 NIO 调度线程来执行这段逻辑,避免线程上下文切换带来的损耗;如果数据报文的处理逻辑比较复杂,耗时比较多,而且可能存在阻塞和执行时间不确定的情况,则建议放入线程池里去异步执行防止 I/O 调度线程被阻塞

猜你喜欢

转载自blog.csdn.net/M_Joes_17/article/details/82118880
今日推荐