初识Java中的BIO和NIO

Java中的I/O操作分为三种模式:同步阻塞式(BIO),同步非阻塞式(NIO),异步非阻塞式(AIO),下面主要讲解BIO和NIO。

1.1 什么是BIO

     BIO(Blocking IO):面向流传输(input/output),同步阻塞式I/O。

  • InputStream: 输入流(用于读取字节)
  • OutputStream: 输出流(用于写入字节)

在JDK1.4之前,Java网络编程中使用java.net包中的api,数据读写则使用Socket类中I/O流来传输。

     BIO概述:服务器实现模式为在接收到客户端连接请求之后,为每个客户端创建一个新的线程去处理;之所以使用多线程,是因为在进行I/O操作时,不知道套接字什么时候有可读/写数据,socket的accept()、read()、write()三个方法是阻塞的,在单线程应用程序中,意味着这个线程被阻塞期间,对所有的I/O请求处理都停顿;所以利用多核多线程的优势,让CPU去处理更多的事情。

 

1.1.1 处理方式

通过多线程实现在不同的线程中去处理不同客户端的I/O请求(1:1)。

初识Java中的BIO和NIO_第1张图片

图中处理流程仅个人理解

1.1.2 服务端代码示例

ServerSocket serverSocket = new ServerSocket(8888); // 监听端口8888
		try {
			while (true) {
				Socket socket = serverSocket.accept(); //阻塞式接收socket连接
				new Thread(() -> {  //创建一个新的线程
					byte[] b = new byte[1024];
					try {
						InputStream in = socket.getInputStream();
						int len = in.read(b); // 读取客户端发送的数据
						System.out.println("接收到客户端数据:" + new String(b,0,len));
						OutputStream out = socket.getOutputStream();
						out.write("successful...".getBytes()); // 返回数据到客户端
						//关闭连接
						in.close(); out.close(); socket.close();
					} catch (Exception e) {
						e.printStackTrace();
					}
				}).start();
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			serverSocket.close();
		}

​

以代码in.read(b)为例:

   如果当前套接字中没有可读数据时,read方法会一直阻塞,直到发生如下三种事件:

  1. 有数据可读
  2. 可用数据已经读取完毕
  3. 发生空指针或者I/O异常

1.1.3 存在的问题

    虽然使用多线程解决了多个客户端I/O请求服务端导致阻塞的问题,在活动连接数不是特别高的情况下,这种模型是不错的选择,但是当连接十万百万时,这种模型是无能为力的,因为严重依赖于线程,对于线程创建和开销、上下文切换,可能导致内存溢出系统不可用的情况。

    这里顺便提一下(伪异步I/O):伪异步I/O的依然是采用同步阻塞模型,服务端线程池和队列的方式来处理客户端的连接,无论多少个客户端连接都不会导致内存溢出等问题,但是当线程池中线程的数量达到了最大值时,新的客户端连接会一直在队列中等待(线程池释放某个客户端线程的资源)线程池有空闲位置;如果队列数量满时,会造成大量客户端请求连接等待超时。

所以必然需要一种更高效的I/O处理模型:多路复用I/O。

 

2.1 什么是NIO

     NIO(官方New IO,又称Non Blocking IO):面向缓冲区(channel传输),同步非阻塞式I/O,多路复用器(选择器)

  • Channel:用来传输字节数据,可以进行双向传输(read/write)

  • Buffer:缓存区用来存储字节数据

  • Selector:多路复用器,所有的客户端连接请求(channel)注册到选择器上面,单个线程轮询处理多个channel连接

在JDK1.4,Windows下采用select模型,Linux下采用Linux I/O模型 epoll(有兴趣可以了解下Linux I/O模型);NIO编程直接使用java.nio包中的api,数据传输和读写则使用Channel和ByteBuffer。

     NIO概述:服务端实现模式为一个请求(read/write)一个线程,客户端有连接请求会被注册到Selector中,多路复用器轮询到连接有I/O读写时才会启动一个线程去处理数据;基于事件模型单线程来处理所有I/O请求,只有当读写事件到来时启动线程去处理。

 

2.1.1 处理方式

当Selector轮询到某个注册的channel中有read/write请求时,才会去启动一个线程去处理(如果使用轮询的线程去处理业务可能会阻塞耗时,影响事件接收)。

Selector的好处在于:单线程来处理更多的客户端连接请求(M:1)。

初识Java中的BIO和NIO_第2张图片

图中处理流程仅个人理解

2.1.2 服务端代码示例 

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();	
		serverSocketChannel.configureBlocking(false);//设置为非阻塞模式
		serverSocketChannel.bind(new InetSocketAddress(8888));//监听端口8888
		//获取选择器
		Selector selector = Selector.open();
		//将通道注册到选择器,等待连接
		serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT);
		//获取选择器中已经准备就绪的事件
		while (selector.select() > 0) {
			//获取当前选择器所有注册的监听事件
			Iterator selectionKeys  = selector.selectedKeys().iterator();
			while (selectionKeys.hasNext()) {
				//获取事件	
				SelectionKey sk = selectionKeys.next();
				if (sk.isAcceptable()) {
					//获取客户端连接
					SocketChannel socketChannel = serverSocketChannel.accept();
					socketChannel.configureBlocking(false); //切换非阻塞模式
					socketChannel.register(selector,SelectionKey.OP_READ);//注册选择器为读模式
				}else if(sk.isReadable()){ //(读就绪)有可读数据
					new Thread(() -> {						
						//获取当前选择器上读就绪状态的通道						
						//读取客户端数据,这里省略
					}).start();	
				}else if(sk.isWritable()){ //(写就绪)可写数据,一般不需要去注册该(可写)事件,在读取数据后写入即可
					new Thread(() -> {						
						//获取当前选择器上写就绪状态的通道
						//写入数据到客户端,这里省略
					}).start();	
				}
				selectionKeys.remove(); //移除通道中的事件
			}
		}
		//关闭通道
		serverSocketChannel.close();

代码中的selector.select()是阻塞的,会等待新事件的到来(事件分发器);I/O读写使用单独的线程处理。

SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,它的read方法返回值有以下三种结果:

  1. 返回值大于0:读取到字节数
  2. 返回值等于0:没有读取到字节,可忽略
  3. 返回值为-1:客户端链路已经关闭,需要关闭SocketChannel,释放资源

2.1.3 原生NIO的不足

     1.JDK提供的java.nio包进行NIO编程会比较复杂(需要熟练掌握nio包和Java多线程),Selector空轮询Bug,ByteBuffer读写时需要使用flip()和clear()进行切换,包括字节数据出入站的编解码等问题,建议使用NIO框架(Netty,Mina)来进行开发。

     2.传输数据量过大,读写过程长时,由于同步需要等待整个操作完成才会返回,这时需要考虑业务场景来使用。

 

总结

  BIO(同步阻塞I/O) NIO(同步非阻塞I/O)
传输类型 面向流传输 面向缓冲区
内存开辟 堆内存 有直接内存
使用场景 连接数少、延迟低 并发高、数据量小

 

 

你可能感兴趣的:(Java网络编程)