Arise的个人博客 Arise的个人博客

俱往矣,数风流人物,还看今朝

目录
Netty-入门知识Java IO与NIO
/  

Netty-入门知识Java IO与NIO

Netty-入门知识Java IO与NIO

网络编程框架

image.png

I/O模型【1】

Linux IO流程:

  • 等待数据准备好(Waiting for the data to be ready)

  • 从内核向进程复制数据(Copying the data from the kernel to the process)

image.png

I/O模型划分:

  • 阻塞式I/O模型

    阻塞式I/O模型
  • 非阻塞式I/O模型

    非阻塞式I/O模型
  • I/O复用

    I/O复用模型
  • 信号驱动式I/O

    信号驱动式I/O模型
  • 异步I/O

    异步I/O模型

总结对比:

image.png

同步 vs 异步

POSIX标准【2】将同步I/O和异步I/O定义为:

  • 同步I/O操作:导致请求进程阻塞,直到I/O操作完成。

  • 异步I/O操作:不导致请求进程阻塞。

Single Thread Server

image.png
  	private int port;
  
  	public SingleThreadEchoServer(int port) {
  		this.port = port;
  	}
  
  	public void startServer() {
  		ServerSocket echoServer = null;
  		int i = 0;
  		System.out.println("服务器在端口[" + this.port + "]等待客户请求......");
  		try {
  			echoServer = new ServerSocket(this.port);
  			while (true) {
  				Socket clientRequest = echoServer.accept();
  				// 处理client的请求...
  			}
  		} catch (IOException e) {
  			System.out.println(e);
  		}
  	}

MultiThread Server

image.png
	private int port;
	
	public MultiThreadedEchoServerV1(int port) {
		this.port = port;
	}
	
	public void startServer() {
 		ServerSocket echoServer = null;
		int i = 0;
		System.out.println("服务器在端口[" + this.port + "]等待客户请求......");
		try {
			echoServer = new ServerSocket(port);
			while (true) {
				Socket clientRequest = echoServer.accept();
				// 给每个请求分配一个线程来处理
				new Thread(new ThreadedServerHandler(clientRequest, i++)).start();
			}
		} catch (IOException e) {
			System.out.println(e);
		}
	}
public class ThreadedServerHandler implements Runnable {
	Socket clientSocket = null;
	int clientNo = 0;

	ThreadedServerHandler(Socket socket, int i) {
		if (socket != null) {
			clientSocket = socket;
			clientNo = i;
			System.out.println("创建线程为[" + clientNo + "]号客户服务...");
		}
	}

	@Override
	public void run() {
		PrintStream os = null;
		BufferedReader in = null;
		try {
			in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
			os = new PrintStream(clientSocket.getOutputStream());
			String inputLine;
			while ((inputLine = in.readLine()) != null) {
				// 输入'Quit'退出
				if (inputLine.equals("Quit")) {
					System.out.println("关闭与客户端[" + clientNo + "]......" + clientNo);
					os.close();
					in.close();
					clientSocket.close();
					break;
				} else {
					System.out.println("来自客户端[" + clientNo + "]的输入: [" + inputLine + "]!");
					os.println("来自服务器端的响应:" + inputLine);
				}
			}
		} catch (IOException e) {
			System.out.println("Stream closed");
		}
	}
}

对server的改进版:

	public void startServer() {
		ServerSocket echoServer = null;
		Executor executor = Executors.newFixedThreadPool(5);
		int i = 0;
		System.out.println("服务器在端口[" + this.port + "]等待客户请求......");
		try {
			echoServer = new ServerSocket(port);
			while (true) {
				Socket clientRequest = echoServer.accept();
				executor.execute(new ThreadedServerHandler(clientRequest, i++));
			}
		} catch (IOException e) {
			System.out.println(e);
		}
	}

Java NIO

变迁:

  1. NIO 1: JSR 51

  2. NIO 2: JSR203

Java NIO:

  1. NIO

    • Buffers
    • Channels
    • Selectors
  2. NIO 2.0

    • Update
    • New File System API
    • Asynchronous I/O

Java IO vs NIO:

image.png

Java NIO主要组成

主要由下面3部分核心组件组成

  • Buffer
  • Channel
  • Selector

Java NIO Buffer

一个 Buffer 本质上是内存中的一块, 可以将数据写入这块内存, 从这块内存获取数据。

java.nio 定义了以下几个 Buffer 的实现(除了Boolean):

image.png

Java NIO Buffer三大核心概念:position、limit、capacity

image.png

最好理解的当然是 capacity,它代表这个缓冲区的容量,一旦设定就不可以更改。比如 capacity 为 1024 的 IntBuffer,代表其一次可以存放 1024 个 int 类型的值。一旦 Buffer 的容量达到 capacity,需要清空 Buffer,才能重新写入值。

image.png

从写操作模式到读操作模式切换的时候(flip),position 都会归零,这样就可以从头开始读写了。

写操作模式下,limit 代表的是最大能写入的数据,这个时候 limit 等于 capacity。

写结束后,切换到读模式,此时的 limit 等于 Buffer 中实际的数据大小,因为 Buffer 不一定被写满了。

java.nio.buffer,缓冲区抽象:

  • ByteBuffer

    理解capacity、limit、position、mark

    0 – mark – position – limit – capacity

  • Non-direct ByteBuffer

  • Direct ByteBuffer VS. non-direct ByteBufferNon-direct ByteBuffer

    HeapByteBuffer,标准的java类

    维护一份byte[]在JVM堆上

    创建开销小

  • Direct ByteBuffer

    底层存储在非JVM堆上,通过native代码操作

    -XX:MaxDirectMemorySize=<size>

    创建开销大

DirectByteBufferHeapByteBuffer
创建开销
存储位置Native heapJava heap
数据拷贝无需临时缓冲区做拷贝拷贝到临时DirectByteBuffer,但临时缓冲区使用缓存。 聚集写/发散读时没有缓存临时缓冲区。
GC影响每次创建或者释放的时候 都调用一次System.gc()

Buffer创建:

  1. allocate/allocateDirect方法

  2. wrap方法

Buffer读取:

  1. put/get方法
  2. flip方法
  3. mark/reset方法
  4. compact方法
  5. rewind/clear

Buffer复制 – 浅复制:

  1. duplicate方法
  2. asReadOnlyBuffer方法
  3. slice方法

Java NIO Channel

所有的 NIO 操作始于通道,通道是数据来源或数据写入的目的地,主要地, java.nio 包中主要实现的以下几个 Channel:

image.png
  • FileChannel:文件通道,用于文件的读和写

  • DatagramChannel:用于 UDP 连接的接收和发送

  • SocketChannel:把它理解为 TCP 连接通道,简单理解就是 TCP 客户端

  • ServerSocketChannel:TCP 对应的服务端,用于监听某个端口进来的请求

image.png image.png

Java NIO Selector

image.png

Selector是Java NIO中的一个组件,用于检查一个或多个NIO Channel的状态是否处于可读、可写,如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。

java.nio.channels.Selector:

  • 支持 IO 多路复用的抽象实体

  • 注册 Seletable Channel

  • SelectionKey —— 表示 Selector 和被注册的 channel 之间关系,一份凭证

  • SelectionKey 保存 channel 感兴趣的事件

  • Selector.select 更新所有就绪的 SelectionKey 的状态,并返回就绪的channel个数

  • 迭代 Selected Key 集合并处理就绪 channel

image.png

实战:

  1. 创建Selector(Creating a Selector)

    selector = Selector.open();
    // Opens a socket channel.
    socketChannel = SocketChannel.open();
    socketChannel.configureBlocking(false);
    
  2. 注册Channel到Selector上(Registering Channels with the Selector)

    // register的第二个参数,这个参数是一个“关注集合”,代表关注的channel状态,
    // 有四种基础类型可供监听, 用SelectionKey中的常量表示如下:
    // SelectionKey.OP_CONNECT
    // SelectionKey.OP_ACCEPT
    // SelectionKey.OP_READ
    // SelectionKey.OP_WRITE
    socketChannel.register(selector, SelectionKey.OP_CONNECT);
    socketChannel.connect(new InetSocketAddress(host, port));
    
  3. 从Selector中选择channel(Selecting Channels via a Selector)

    select()方法的返回值是一个int,代表有多少channel处于就绪了。也就是自上一次select后有多少channel进入就绪。

  4. selectedKeys()

    在调用select并返回了有channel就绪之后,可以通过选中的key集合来获取channel,这个操作通过调用selectedKeys()方法:

    while (!stop) {
       try {
          // select()方法的返回值是一个int,代表有多少channel处于就绪了。也就是自上一次select后有多少channel进入就绪。
          // 一旦向Selector注册了一个或多个channel后,就可以调用select来获取channel
          // select方法会返回所有处于就绪状态的channel, select方法具体如下:
          // int select()、int select(long timeout)、int selectNow()
          selector.select(1000);
          // 在调用select并返回了有channel就绪之后,可以通过选中的key集合来获取channel,
          // 这个操作通过调用selectedKeys()方法:
          Set<SelectionKey> selectedKeys = selector.selectedKeys();
          Iterator<SelectionKey> it = selectedKeys.iterator();
          SelectionKey key = null;
          while (it.hasNext()) {
             key = it.next();
             it.remove();
             try {
                 if(key.isAcceptable()) {
                     // a connection was accepted by a ServerSocketChannel.
                 } else if (key.isConnectable()) {
                     // a connection was established with a remote server.
                 } else if (key.isReadable()) {
                     // a channel is ready for reading
                 } else if (key.isWritable()) {
                     // a channel is ready for writing
                 }
                 key.cancel();
             } catch (Exception e) {
                if (key != null) {
                   key.cancel();
                   if (key.channel() != null)
                      key.channel().close();
                }
             }
          }
       } catch (Exception e) {
          e.printStackTrace();
          System.exit(1);
       }
    }
    

    NIO带来了什么?

    1. 事件驱动模型
    2. 避免多线程
    3. 单线程处理多任务
    4. 非阻塞I/O,I/O读写不再阻塞,而是返回0
    5. 基于block的传输,通常比基于流的传输更高效
    6. 更高级的IO函数,zero-copy【3】
    7. IO多路复用大大提高了Java网络应用的可伸缩性和实用性

NIO Tips【4】

通常情况下,操作系统的一次写操作分为两步: 1. 将数据从用户空间拷贝到系统空间。 2. 从系统空间往网卡写。同理,读操作也分为两步: ① 将数据从网卡拷贝到系统空间; ② 将数据从系统空间拷贝到用户空间。

对于NIO来说,缓存的使用可以使用DirectByteBuffer和HeapByteBuffer。如果使用了DirectByteBuffer,一般来说可以减少一次系统空间到用户空间的拷贝。但Buffer创建和销毁的成本更高,更不宜维护,通常会用内存池来提高性能。

如果数据量比较小的中小应用情况下,可以考虑使用heapBuffer;反之可以用directBuffer。

  1. 使用NIO != 高性能

    NIO不一定更快的场景

    1. 客户端应用
    2. 连接数<1000
    3. 并发程度不高
    4. 局域网环境下
  2. NIO并没有完全屏蔽平台差异(Linux poll/select/epoll, FreeBSD Kqueue),它仍然是基于各个操作系统的I/O系统实现的,差异仍然存在。使用NIO做网络编程构建事件驱动模型并不容易,陷阱重重。

  3. 使用NIO做网络编程很容易

    1. 离散的事件驱动模型,编程困难
    2. 陷阱重重
  4. 推荐大家使用成熟的NIO框架,如Netty,MINA等。解决了很多NIO的陷阱,并屏蔽了操作系统的差异,有较好的性能和编程模型。

参考文献

【1】DMA:https://en.wikipedia.org/wiki/Direct_memory_access

【2】POSIX:https://en.wikipedia.org/wiki/POSIX

【3】zero-copy:https://medium.com/@xunnan.xu/its-all-about-buffers-zero-copy-mmap-and-java-nio-50f2a1bfc05c

【4】https://tech.meituan.com/2016/11/04/nio.html


标题:Netty-入门知识Java IO与NIO
作者:lmmarise
地址:http://tbeau.oicp.io/articles/2020/05/27/1590512511357.html

image.png