Netty编程(五)——NIO模块下的阻塞和非阻塞模式

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

阻塞

阻塞模式的设计

  • 阻塞模式下,相关方法都会导致线程暂停
    • ServerSocketChannel.accept 会在没有连接建立时让线程暂停,即使之后有客户端向服务端发送消息,服务端也接受不到,直到有新客户端连接服务端,不再阻塞在accept方法上。
    • SocketChannel.read 会在通道中没有数据可读时让线程暂停,即使之后有新客户端向服务端发起连接请求也接受不了,直到读取完毕,不再阻塞在read方法上
    • 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
  • 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
  • 但多线程下,有新的问题,如果连接数过多,必然导致 OOM(out of memory),并且线程太多,反而会因为频繁上下文切换导致性能降低
    • 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间闲置,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接

对于NIO阻塞模式下,单线程服务端的代码要有下面几个步骤:

  1. 打开服务端的ByteBuffer用来存储数据
  2. 创建服务器 ServerSocketChannel
  3. ServerSocketChannel绑定监听端口
  4. 创建一个连接集合 ,用来装所有用户的连接 SocketChannel
  5. accept 建立与客户端连接,SocketChannel 用来与客户端通信
  6. 接收客户端发送的数据,遍历集合,看有没有用户有发来的数据
public class NIOServer {
    public static void main(String[] args) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(100);
        try(ServerSocketChannel serverSocket = ServerSocketChannel.open())
        {
            serverSocket.bind(new InetSocketAddress(8080));
            List<SocketChannel> channels = new ArrayList<>();
            while(true)
            {
                SocketChannel sc = serverSocket.accept();
                channels.add(sc);

                for (SocketChannel asc :channels)
                {
                    asc.read(buffer);
                    buffer.flip();
                    ByteBufferUtil.debugAll(buffer);
                    buffer.clear();
                }
            }
        }catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}
复制代码

对于客户端比较简单了,首先创建与i个SocketChannel,然后连接服务端的端口,之后使用write方法通过通道发送数据就行了

public class NIOClient {
    public static void main(String[] args) {
        try(SocketChannel socketChannel = SocketChannel.open())
        {
            socketChannel.connect(new InetSocketAddress("localhost",8080));
            System.out.println("connecting...");
        }catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}
复制代码

阻塞模式的分析

之前的博客Java网络编程(一)—— 简单的C/S程序 - 掘金 (juejin.cn)介绍的使用Socket也能实现类似的作用,但是使用Socket需要得到输入流和输出流,没有使用通道方便,但是他依然没有改变阻塞模式的严重问题:当没有新的客户端尝试连接服务器时,服务端程序就会阻塞在accept方法上,不会读取其他客户端发来的消息;当没有用户发来消息时,服务端程序就会阻塞在
read方法上,不会接收新客户端发来的消息。

非阻塞

非阻塞模式的设计

上面分析的在阻塞模式下会在两个地方发生阻塞——accept方法、read方法,因此在NIO中提供了非阻塞状态:

  • 可以通过ServerSocketChannel的configureBlocking(false)方法将获得连接设置为非阻塞的。此时若没有连接,accept会返回null
  • 可以通过SocketChannel的configureBlocking(false)方法将从通道中读取数据设置为非阻塞的。若此时通道中没有数据可读,read会返回-1
public class NIOServer {
    public static void main(String[] args) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(100);
        try(ServerSocketChannel serverSocket = ServerSocketChannel.open())
        {
            serverSocket.bind(new InetSocketAddress(8080));

            List<SocketChannel> channels = new ArrayList<>();

            while(true)
            {
                // 设置为非阻塞模式,没有连接时返回null,不会阻塞线程
                serverSocket.configureBlocking(false);

                SocketChannel sc = serverSocket.accept();

                if(sc!=null)
                {
                    System.out.println("----new client----");
                    channels.add(sc);
                }


                for (SocketChannel asc :channels)
                {
                    // 设置为非阻塞模式,若通道中没有数据,会返回0,不会阻塞线程
                    asc.configureBlocking(false);
                    int readsize = asc.read(buffer);
                    if(readsize>0){
                        buffer.flip();
                        ByteBufferUtil.debugAll(buffer);
                        buffer.clear();
                    }
                }
            }
        }catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}
复制代码

非阻塞模式的分析

使用非阻塞模式确实不会发生阻塞,至少不会对业务造成影响,但是从性能分析,这种非阻塞状态下会发生CPU空转情况,即可能一直while循环,占用CPU资源。