BIO和NIO

BIO:阻塞式IO,这种通信模型其实就是socket。
它是通过以流的形式进行输入和输出。
该模型中有四个方法,accept()、connect()、read()、write()这些方法都会产生阻塞。因为是阻塞通信,所以这种模型相当于是一个请求产生一个线程,当请求数量越多时,线程数量越多,由此会带来内存占用,内存碎片,cpu对线程的管理调度等问题。当环境处于高并发,高访问量时,BIO就不那么适合了。
由此诞生了NIO。

NIO是jdk1.4之后提出的一套新的非阻塞式IO。
在java.nio中,定义了通道Channel和缓冲区Buffer。
Buffer是一个缓冲区,大小可以自己设定。本质上就是一个数据的结构,是传输数据的载体。Channel是一个通道,可以同时进行数据的输入和输出。

首先来看BIO:
Server端:

@Test
public void testAccept() throws Exception{
    ServerSocket server=new ServerSocket();
    server.bind(new InetSocketAddress(9999));//绑定本机,端口是9999
    Socket socket = server.accept();
    System.out.println("测试阻塞");
} 

Client端:

@Test
public void testConnect() throws IOException{
    Socket client=new Socket();
    client.connect(new InetSocketAddress(9999));
    System.out.println("测试连接");
}

可以发现,如果说服务器端没有客户端进行连接,代码就走不到测试连接那一段,一直会处于阻塞状态。当客户端一连接上就输出测试阻塞。

再看其他方法:
Server端:

@Test
public void testWrite() throws Exception {
    ServerSocket server = new ServerSocket();
    server.bind(new InetSocketAddress(9999));
    Socket socket = server.accept();
    OutputStream out = socket.getOutputStream();
    for (int i = 0; i < 10000; i++) {
        out.write("hello world".getBytes());// 当一端写出数据而没有一端去读取数据的时候,就会产生阻塞。当前数据是写入到网卡缓冲区的。
        System.out.println(i);
    }                                   
    System.out.println("写出数据");
}

Client端:

@Test
public void testRead1() throws IOException {
    Socket client = new Socket();
    client.connect(new InetSocketAddress(9999));
}
@Test
public void testRead2() throws IOException {
    Socket client = new Socket();
    client.connect(new InetSocketAddress(9999));
    InputStream inputStream = client.getInputStream();
    byte[] bs=new byte[20];//创建一个数组的缓冲区
    while(inputStream.read(bs)!=-1){
        inputStream.read(bs);
        System.out.println(new String(bs));
    }
}

执行Junit测试。首先启动Server端,然后执行测试客户端的testRead1,发现服务器端虽然是写出了一些数据,但是还不到10000的时候就阻塞掉了。因为当前输出流输出数据是输出到网卡缓冲区,有一定的输出空间。所以能进行输出。但是最终缓冲区一满,就发生阻塞。我们启动testRead2的Junit测试,发现阻塞放开并输出所有i的值,证明当有一端进行输出,另一端进行输入才不会产生阻塞。所以write这个方法也是阻塞的。同理可以验证其他方法。
由此证明bio的阻塞方式。

而对于NIO:

Server端:

@Test
public void createServer() throws IOException{
    //创建一个服务端通道
    ServerSocketChannel ssc=ServerSocketChannel.open();
    //设置为非阻塞,默认是true,false是非阻塞
    ssc.configureBlocking(false);
    //绑定一个端口
    ssc.bind(new InetSocketAddress(9999));
    //接收客户端连接
    SocketChannel sc = ssc.accept();
    System.out.println("测试accept是否是阻塞的,当前是服务器端");
}

Client端

@Test
public void createClient() throws Exception{
    SocketChannel sc =SocketChannel.open();
    sc.configureBlocking(false);//设置非阻塞模式
    sc.connect(new InetSocketAddress(9999));
    System.out.println("测试connect是否阻塞,当前是客户端");
}

从这个Junit测试中可以发现,哪怕就是服务器端开启,并没有客户端连接代码也能够往下继续输出。而对于客户端,哪怕是服务器端并不存在或者未开启,Client这边去连接,没有连接上,代码也能往下继续输出。
由此能够测试出nio中的accept、connect都是非阻塞的。同理可以测试其他方法。

但是此时使用nio也引出了另外的问题:

  1. 每当一个用户过来发送请求,就会产生一个线程,当大量用户过来时,就会产生大量的线程,由此会耗费大量的内存
  2. 用户可能只是进行连接,并没有进行读写等操作,只是连接,但是连接占用着一个线程,这样就会造成大量的线程浪费。
  3. cpu一直在轮询线程,而大部分线程又处于闲置的状态,就造成cpu的空转,而真正需要处理的线程有可能又轮询不到。

由此引出Selector
Selector是一个多路的复用器。就像是路由器和交换机。在一个Selector上可以同时注册多个非阻塞的通道,从而只需要很少的线程数既可以管理许多通道。
引入Selector之后,Selector会监听注册在其身上的事件。如果某个线程对应的客户端没有发生任何时间,Selector就会把这个线程阻塞掉,cpu就不会轮询这个处于阻塞状态的线程。从而避免了空转,节省了cpu资源。而当一旦某个线程的事件被Selector监听到,这个线程就会从阻塞状态被唤醒。
SelectionKey代表一个事件、并在Selector上注册事件:

  • OP_ACCEPT 对应 isAcceptable()方法
  • OP_CONNECT 对应 isConnectable()方法
  • OP_READ 对应 isReadable()方法
  • OP_WRITE 对应 isWritable()方法

结合Buffer、Selector:
Server端:

public static void main(String[] args) throws Exception {
    System.out.println("服务端启动");
    //创建一个ServerSocketChannel对象
    ServerSocketChannel ssc=ServerSocketChannel.open();
    //设置非阻塞式
    ssc.configureBlocking(false);
    //当前通道绑定9999款口号
    ssc.socket().bind(new InetSocketAddress(9999));
    //创建一个selector
    Selector selector=Selector.open();
    //注册事件,对服务通道来说,最初要注册ACCEPT事件
    ssc.register(selector, SelectionKey.OP_ACCEPT);
    while(true){
        //该方法会产生阻塞,当有事件产生时,阻塞放开。
        selector.select();
        //获取到的监听事件集合,代码走到这,即表示有事件发生,所以需要获取到事件的集合
        Set selectedKeys = selector.selectedKeys();
        Iterator iterator = selectedKeys.iterator();
        //遍历事件集合里的每个事件
        while(iterator.hasNext()){
            //当前一个sk就代表一个具体的事件 
            SelectionKey sk = iterator.next();
            if(sk.isAcceptable()){//客户端接入事件 
                //获取服务端通道
                ServerSocketChannel server=(ServerSocketChannel) sk.channel();
                SocketChannel sc = server.accept();
                //将通道设置为非阻塞模式
                sc.configureBlocking(false);
                //在通道上注册读事件和写事件
                sc.register(selector, SelectionKey.OP_READ|SelectionKey.OP_WRITE);
                System.out.println("有客户端接入,当前处理的线程编号是:"+Thread.currentThread().getId());

            }
            if(sk.isReadable()){//如果是读事件,相当于客户端向服务端发送数据,服务端读取数据
                SocketChannel sc=(SocketChannel) sk.channel();
                ByteBuffer bf=ByteBuffer.allocate(20);
                //将数据读取到缓冲区
                sc.read(bf);
                System.out.println("数据:"+new String(bf.array()+"\r\n 当前线程编号:"+Thread.currentThread().getId()));
                //移除读事件的监听
                //interestOps(),获取的是监听事件的状态:0000 0101
                sc.register(selector, sk.interestOps()&~SelectionKey.OP_READ);
            }
            if(sk.isWritable()){//如果是写事件,服务器端向客户端写数据
                SocketChannel sc=(SocketChannel) sk.channel();
                ByteBuffer bf=ByteBuffer.wrap("这是一条服务器端向客户端发送的数据。二黑二黑,我是大黑!".getBytes());
                sc.write(bf);
                sc.register(selector, sk.interestOps()&~SelectionKey.OP_WRITE);
            }
            //处理完事件之后将当前事件移除,避免同一事件被重复处理
            iterator.remove();
        }
    }

}

Client端:

public static void main(String[] args) throws Exception {
    System.out.println("客户端启动");
    Selector selector = Selector.open();
    SocketChannel sc = SocketChannel.open();
    sc.configureBlocking(false);
    sc.connect(new InetSocketAddress(9999));
    sc.register(selector, SelectionKey.OP_CONNECT);
    while (true) {
        selector.select();
        Set set = selector.selectedKeys();
        Iterator iterator = set.iterator();
        while (iterator.hasNext()) {
            SelectionKey sk = iterator.next();
            if (sk.isConnectable()) {
                SocketChannel s = (SocketChannel) sk.channel();
                if (!s.isConnected()) {
                    s.finishConnect();
                }
                s.register(selector, SelectionKey.OP_WRITE|SelectionKey.OP_READ);//向Selector注册读和写的事件
            }
            if (sk.isWritable()) {
                SocketChannel s = (SocketChannel) sk.channel();
                ByteBuffer buffer = ByteBuffer.wrap("这是一条客户端向服务器端发送的数据。大黑大黑,我是二黑!".getBytes());
                while (buffer.hasRemaining()) {
                    s.write(buffer);
                }
                s.register(selector, sk.interestOps() & ~SelectionKey.OP_WRITE);
            }
            if(sk.isReadable()){
                SocketChannel s = (SocketChannel) sk.channel();
                ByteBuffer buffer=ByteBuffer.allocate(1024);
                s.read(buffer);
                System.out.println(new String(buffer.array()));
                s.register(selector, sk.interestOps() & ~SelectionKey.OP_READ);
            }
            iterator.remove();
        }
    }
}

这里我们能够观察到,每启动一个服务,一个客户端,都是通过同一个线程来处理的请求。实现了Selector的监听机制,达到了复用,防止了cpu空转。

你可能感兴趣的:(NIO)