网络编程-Netty篇

导读:本篇文章讲解 网络编程-Netty篇,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

知识点前文请阅读:nio基础


非阻塞 vs 阻塞

  • 阻塞

  • 阻塞模式下,相关方法都会导致线程暂停

    • ServerSocketChannel.accept 会在没有连接建立时让线程暂停

    • SocketChannel.read 会在没有数据可读时让线程暂停

    • 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置

  • 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持

  • 但多线程下,有新的问题,体现在以下方面

    • 32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低

    • 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接

示例:

服务器端

	// 使用nio 理解 阻塞模式,单线程
	public static void main(String[] args) throws IOException {
		ByteBuffer buffer = ByteBuffer.allocate(16);

		// 1.创建服务器
		ServerSocketChannel ssc = ServerSocketChannel.open();

		// 2.绑定服务器的监听端口
		ssc.bind(new InetSocketAddress(8080));

		// 3.建立连接的集合
		List<SocketChannel> channels = new ArrayList<>();
		while (true) {
			// 4.accept 建立与客户端的连接 , SocketChannel 用来与客户端通信
			log.debug("connecting...");
			SocketChannel sc = ssc.accept();	// * 阻塞方法,没有客户端连接时,线程停止运行
			log.debug("connected... {}", sc);
			channels.add(sc);

			// 5.接收客户端发送的数据
			for (SocketChannel channel : channels) {
				// 读取客户端的数据
				log.debug("before read... {}", channel);
				channel.read(buffer);			// * 阻塞方法,等待客户端发送数据,线程停止运行
				// 打印客户端的数据
				buffer.flip();
				String s = StandardCharsets.UTF_8.decode(buffer).toString();
				System.err.println(s);
				// 读完后切换为写模式,才可以继续写数据
				buffer.clear();
				log.debug("after read... {}", channel);
			}
		}
	}

客户端

	public static void main(String[] args) throws IOException {
		SocketChannel sc = SocketChannel.open();
		sc.connect(new InetSocketAddress("localhost", 8080)); // 这里使用debug 进行写数据 sc.write(Charset.defaultCharset().encode("hello"));
		System.err.println("waiting...");		// 这里打断点
	}

打印,第一个客户端先写入一个hello, 再写入一个 hi , 服务器端第一个客户端会打印 hello,当有第二个客户端连接时,才会打印hi

17:15:35.523 [main] DEBUG com.lixx.demo.netty.Server1 - connecting...
17:15:42.101 [main] DEBUG com.lixx.demo.netty.Server1 - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:59436]
17:15:42.104 [main] DEBUG com.lixx.demo.netty.Server1 - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:59436]
hello
17:16:14.656 [main] DEBUG com.lixx.demo.netty.Server1 - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:59436]
17:16:14.656 [main] DEBUG com.lixx.demo.netty.Server1 - connecting...
17:16:33.803 [main] DEBUG com.lixx.demo.netty.Server1 - connected... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:59445]
17:16:33.803 [main] DEBUG com.lixx.demo.netty.Server1 - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:59436]
17:16:33.803 [main] DEBUG com.lixx.demo.netty.Server1 - after read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:59436]
17:16:33.803 [main] DEBUG com.lixx.demo.netty.Server1 - before read... java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:59445]
hi

总结:阻塞模式下,一个线程只可以处理一个连接

  • 非阻塞

  • 非阻塞模式下,相关方法都不会让线程暂停

    • 在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行

    • SocketChannel.read 在没有数据可读时,会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept

    • 写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去

  • 但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu

  • 数据复制过程中,线程实际还是阻塞的(AIO 改进的地方)

示例:

服务器端

	// 使用nio 理解 非阻塞模式,单线程
	public static void main(String[] args) throws IOException {
		ByteBuffer buffer = ByteBuffer.allocate(16);

		// 1.创建服务器
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.configureBlocking(false);    // 非阻塞模式,影响ssc.accept()方法

		// 2.绑定服务器的监听端口
		ssc.bind(new InetSocketAddress(8080));

		// 3.建立连接的集合
		List<SocketChannel> channels = new ArrayList<>();
		while (true) {
			// 4.accept 建立与客户端的连接 , SocketChannel 用来与客户端通信
			SocketChannel sc = ssc.accept();    // * 非阻塞方法,线程还会继续运行,如果没有连接建立,sc是null

			if (null != sc) {
				log.debug("connected... {}", sc);
				sc.configureBlocking(false);    // 非阻塞模式,影响 sc.read()方法
				channels.add(sc);
			}

			// 5.接收客户端发送的数据
			for (SocketChannel channel : channels) {
				// 读取客户端的数据
				int read = channel.read(buffer);// * 非阻塞方法,线程继续运行,如果没有读到数据,read 返回 0
				if (read > 0) {
					// 打印客户端的数据
					buffer.flip();
					String s = StandardCharsets.UTF_8.decode(buffer).toString();
					System.err.println(s);
					// 读完后切换为写模式,才可以继续写数据
					buffer.clear();
					log.debug("after read... {}", channel);
				}
			}
		}
	}

客户端

	public static void main(String[] args) throws IOException {
		SocketChannel sc = SocketChannel.open();
		sc.connect(new InetSocketAddress("localhost", 8080)); // 这里使用debug 进行写数据 sc.write(Charset.defaultCharset().encode("hello"));
		System.err.println("waiting...");		// 这里打断点
	}

总结:可以解决阻塞模式下遇到的问题(一个线程不能处理多个客户端的连接),但是会出现线程一直在运行问题,导致单核CPU的利用率100%,实际开发中不采用

Selector (多路复用)

  • 好处
  • 一个线程配合 selector 就可以监控多个 channel 的事件,事件发生线程才去处理。避免非阻塞模式下所做无用功

  • 让这个线程能够被充分利用

  • 节约了线程的数量

  • 减少了线程上下文切换

  •  创建
Selector selector = Selector.open();
  • 绑定 Channel 事件

也称之为注册事件,绑定的事件 selector 才会关心

channel.configureBlocking(false);
SelectionKey selectionKey = ssc.register(selector, 0, null);
  • channel 必须工作在非阻塞模式

  • FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用

  • 绑定的事件类型可以有

    • connect – 客户端连接成功时触发

    • accept – 服务器端成功接受连接时触发

    • read – 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况

    • write – 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况

  •  监听 Channel 事件

可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件

方法1,阻塞直到绑定事件发生

int count = selector.select();

方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)

int count = selector.select(long timeout);

方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

 int count = selector.selectNow();
  • select 何时不阻塞
  • 事件发生时

    • 客户端发起连接请求,会触发 accept 事件

    • 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件

    • channel 可写,会触发 write 事件

    • 在 linux 下 nio bug 发生时

  • 调用 selector.wakeup(),唤醒阻塞在select方法上的线程

  • 调用 selector.close()

  • selector 所在线程 interrupt

  • 事件发生后能否不处理

事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发

  •  为何要 iter.remove()

因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如

  • 第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey

  • 第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常

  • cancel 的作用

cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件

示例:

服务器端(发送中文会出现消息边界问题)

	public static void main(String[] args) throws IOException {
		// 1.创建selector,可以管理多个 channel
		Selector selector = Selector.open();

		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.configureBlocking(false);

		// 2. 建立 selector 和 channel 的联系 (注册)
		// SelectionKey 事件发生后,通过SelectionKey可以知道是哪个channel发生的事件,以及发生的什么事件
		SelectionKey sscKey = ssc.register(selector, 0, null);    // 管理ssc的key,第二个参数 0 表示不关注任何事件,第三个参数 null 附件的意思,表示一个事件有自己的buffer
		// 指定 selector 对哪个事件感兴趣
		sscKey.interestOps(SelectionKey.OP_ACCEPT);
		log.debug("register key: {}", sscKey);

		ssc.bind(new InetSocketAddress(8080));
		while (true) {
			// 3. 监听事件的发生,没有事件时阻塞,有事件时线程开始继续执行
			// Selector.select() 在事件有未处理时,它不会阻塞,会不停的把未处理的事件放入 selectedKeys 集合中,所以在事件发生后,必须处理或者取消,不能置之不理
			// ServerSocketChannel.accept() 是处理事件方法
			// SelectionKey.cancel() 如果拿到了事件,不需要处理,可以使用 cancel 取消事件
			selector.select();
			// 4. 处理事件 selectedKeys 所有可用事件的集合
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			// 迭代 事件集合
			while (iterator.hasNext()) {
				SelectionKey key = iterator.next();
				// 处理key时,要从 selectedKeys 集合中删除,不然下次有事件进来时,这次的key的事件已经被处理过了,但是 集合中的 key 还在,就会报null指针异常
				iterator.remove();
				log.debug("key: {}", key);
				// 5. 区分事件类型
				if (key.isAcceptable()) { // 如果是 Accept 连接事件
					// 拿到 发生事件的channel
					ServerSocketChannel channel = (ServerSocketChannel) key.channel();
					// 可以建立连接了
					SocketChannel sc = channel.accept();
					// 如果需要读取数据,sc需要在非阻塞模式上
					sc.configureBlocking(false);
					// sc也要交给 Selector 来管理(注册),不然 sc 也是在死循环等待消息,消耗cpu利用率
					// sc 上的事件 由scKey 来管理通知
					SelectionKey scKey = sc.register(selector, 0, null);
					// 对 read 事件感兴趣
					scKey.interestOps(SelectionKey.OP_READ);
					log.debug("sc: {}", sc);
				} else if (key.isReadable()) { // 如果是 read 读事件
					try {
						SocketChannel channel = (SocketChannel) key.channel();
						ByteBuffer buffer = ByteBuffer.allocate(4);
						int read = channel.read(buffer);// 正常断开,read的返回值是 -1
						if (read == -1) {
							log.debug("close : {}", key);
							key.cancel();
						} else {
							buffer.flip();
							String s = StandardCharsets.UTF_8.decode(buffer).toString();
							System.err.println(s);
						}
					} catch (IOException e) {
						e.printStackTrace();
						// 异常断开,把key从事件中删除,不再监听
						key.cancel();
					}
				}
			}
		}
	}

客户端

	public static void main(String[] args) throws IOException {
		SocketChannel sc = SocketChannel.open();
		sc.connect(new InetSocketAddress("localhost", 8080)); // 这里使用debug 进行写数据 sc.write(Charset.defaultCharset().encode("hello"));
		//sc.close();	// 正常断开
		System.err.println("waiting...");		// 这里打断点
	}
  • 消息边界问题

ByteBuffer.allocate(4)

// 第一次发送消息 ’hi‘

hi
//第二次发送消息 ‘hello‘ , 消息读取了两次接收,出现消息边界问题
hell
o
// 第三次发送消息 ‘你好’,出现消息边界问题,UTF-8编码一个中文是3个字节
你�
��

处理消息边界的三种方式

  • 1:是固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽

  • 2:是按分隔符拆分,缺点是效率低

  • 3:TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量

    • Http 1.1 是 TLV 格式

    • Http 2.0 是 LTV 格式

 服务器端

	public static void main(String[] args) throws IOException {
		// 1.创建selector,可以管理多个 channel
		Selector selector = Selector.open();

		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.configureBlocking(false);

		// 2. 建立 selector 和 channel 的联系 (注册)
		// SelectionKey 事件发生后,通过SelectionKey可以知道是哪个channel发生的事件,以及发生的什么事件
		SelectionKey sscKey = ssc.register(selector, 0, null);    // 管理ssc的key,第二个参数 0 表示不关注任何事件
		// 指定 selector 对哪个事件感兴趣
		sscKey.interestOps(SelectionKey.OP_ACCEPT);
		log.debug("register key: {}", sscKey);

		ssc.bind(new InetSocketAddress(8080));
		while (true) {
			// 3. 监听事件的发生,没有事件时阻塞,有事件时线程开始继续执行
			// Selector.select() 在事件有未处理时,它不会阻塞,会不停的把未处理的事件放入 selectedKeys 集合中,所以在事件发生后,必须处理或者取消,不能置之不理
			// ServerSocketChannel.accept() 是处理事件方法
			// SelectionKey.cancel() 如果拿到了事件,不需要处理,可以使用 cancel 取消事件
			selector.select();
			// 4. 处理事件 selectedKeys 所有可用事件的集合
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			// 迭代 事件集合
			while (iterator.hasNext()) {
				SelectionKey key = iterator.next();
				// 处理key时,要从 selectedKeys 集合中删除,不然下次有事件进来时,这次的key的事件已经被处理过了,但是 集合中的 key 还在,就会报null指针异常
				iterator.remove();
				log.debug("key: {}", key);
				// 5. 区分事件类型
				if (key.isAcceptable()) { // 如果是 Accept 连接事件
					// 拿到 发生事件的channel
					ServerSocketChannel channel = (ServerSocketChannel) key.channel();
					// 可以建立连接了
					SocketChannel sc = channel.accept();
					// 如果需要读取数据,sc需要在非阻塞模式上
					sc.configureBlocking(false);
					ByteBuffer buffer = ByteBuffer.allocate(16);
					// sc也要交给 Selector 来管理(注册),不然 sc 也是在死循环等待消息,消耗cpu利用率
					// sc 上的事件 由scKey 来管理通知, 第三个参数 null 附件的意思,表示一个事件有自己的buffer
					SelectionKey scKey = sc.register(selector, 0, buffer);
					// 对 read 事件感兴趣
					scKey.interestOps(SelectionKey.OP_READ);
					log.debug("sc: {}", sc);
				} else if (key.isReadable()) { // 如果是 read 读事件
					try {
						SocketChannel channel = (SocketChannel) key.channel();
						ByteBuffer buffer = (ByteBuffer) key.attachment();
						int read = channel.read(buffer);// 正常断开,read的返回值是 -1
						if (read == -1) {
							log.debug("close : {}", key);
							key.cancel();
						} else {
							split(buffer);
							// 需要扩容
							if (buffer.position() == buffer.limit()) {
								ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
								buffer.flip();
								newBuffer.put(buffer);
								key.attach(newBuffer);
							}
						}
					} catch (IOException e) {
						e.printStackTrace();
						// 异常断开,把key从事件中删除,不再监听
						key.cancel();
					}
				}
			}
		}
	}

	/**
	 * 处理黏包、半包
	 * 遇到\n符读一次,没有读完的和下次组合在一起读
	 */
	private static void split(ByteBuffer source) {
		source.flip();    // 切换到读模式
		for (int i = 0; i < source.limit(); i++) {
			// 遇到换行符,表示找到一条完整的消息
			if (source.get(i) == '\n') {
				// 得到消息长度,= 换行符的索引 + 1 - 数据起始位置
				int len = i + 1 - source.position();
				// 把完整的消息写入一个新的 ByteBuffer
				ByteBuffer target = ByteBuffer.allocate(len);
				// 从 source 读,向 target 写
				for (int j = 0; j < len; j++) {
					target.put(source.get());
				}
				// 打印结果
				target.flip();
				String s = StandardCharsets.UTF_8.decode(target).toString();
				System.err.println(s);
			}
		}
		source.compact(); // 切换到写模式,这里不能使用 clear()切换到写模式,因为clear会把 buffer数据清空从position 0 重新写
	}

客户端

	public static void main(String[] args) throws IOException {
		SocketChannel sc = SocketChannel.open();
		sc.connect(new InetSocketAddress("localhost", 8080));
		sc.write(Charset.defaultCharset().encode("hello\n1234567890abcde\naaaaaaaaaa\n"));
		sc.close();	// 正常断开
		System.err.println("waiting...");
	}

    // 输出
    hello
    1234567890abcde
    aaaaaaaaaa
  • ByteBuffer 大小分配
  • 每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 channel 共同使用,因此需要为每个 channel 维护一个独立的 ByteBuffer(allocate 附件)

  • ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小可变的 ByteBuffer

    • 一种思路是首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能,参考实现 Java Resizable Array

    • 另一种思路是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗

  • 处理 write 写事件
  • 非阻塞模式下,无法保证把 buffer 中所有数据都写入 channel,因此需要追踪 write 方法的返回值(代表实际写入字节数)

  • 用 selector 监听所有 channel 的可写事件,每个 channel 都需要一个 key 来跟踪 buffer,但这样又会导致占用内存过多,就有两阶段策略

    • 当消息处理器第一次写入消息时,才将 channel 注册到 selector 上

    • selector 检查 channel 上的可写事件,如果所有的数据写完了,就取消 channel 的注册

    • 如果不取消,会每次可写均会触发 write 事件

 服务器端

	public static void main(String[] args) throws IOException {
		ServerSocketChannel ssc = ServerSocketChannel.open();
		ssc.configureBlocking(false);

		Selector selector = Selector.open();
		ssc.register(selector, SelectionKey.OP_ACCEPT);

		ssc.bind(new InetSocketAddress(8080));

		while (true) {
			selector.select();
			Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
			while (iterator.hasNext()) {
				SelectionKey key = iterator.next();
				iterator.remove();
				if (key.isAcceptable()) {
					SocketChannel sc = ssc.accept();
					sc.configureBlocking(false);
					SelectionKey scKey = sc.register(selector, 0, null);
					scKey.interestOps(SelectionKey.OP_READ);
					// 1.向客户端发送大量数据
					StringBuilder sb = new StringBuilder();
					for (int i = 0; i < 8000000; i++) {
						sb.append("a");
					}
					ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString());

					// 2.返回值表示实际写入的字节数
					int write = sc.write(buffer);
					System.err.println(write);

					// 3. 判断是否有剩余内容
					if (buffer.hasRemaining()) {
						// 4. 关注可写事件
						scKey.interestOps(scKey.interestOps() + SelectionKey.OP_WRITE);	// 不带 interestOps() 会覆盖 scKey的其他事件
						// 5. 把 未写完的数据 挂到 scKey 上
						scKey.attach(buffer);
					}
				} else if (key.isWritable()) {
					ByteBuffer buffer = (ByteBuffer) key.attachment();
					SocketChannel sc = (SocketChannel) key.channel();
					int write = sc.write(buffer);
					System.err.println(write);
					// 6.清理操作
					if (!buffer.hasRemaining()) {
						key.interestOps(key.interestOps() - SelectionKey.OP_WRITE); // 不需要关注可写事件了
						key.attach(null);	// 内容都写完了,需要清除buffer
					}
				}
			}
		}
	}

客户端

	public static void main(String[] args) throws IOException {
		SocketChannel sc = SocketChannel.open();
		sc.connect(new InetSocketAddress("localhost", 8080));

		// 3.接收服务器端的数据
		int count = 0;
		while (true) {
			ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024);
			count += sc.read(buffer);
			System.err.println(count);
			buffer.clear();
		}
	}

零拷贝

  • 传统 IO 问题

传统的 IO 将一个文件通过 socket 写出

File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");

byte[] buf = new byte[(int)f.length()];
file.read(buf);

Socket socket = ...;
socket.getOutputStream().write(buf);

内部工作流程是这样的:

网络编程-Netty篇

  1. java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 java 程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用 DMA(Direct Memory Access)来实现文件读,其间也不会使用 cpu

    DMA 也可以理解为硬件单元,用来解放 cpu 完成文件 IO

  2. 内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 cpu 会参与拷贝,无法利用 DMA

  3. 调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区,cpu 会参与拷贝

  4. 接下来要向网卡写数据,这项能力 java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

可以看到中间环节较多,java 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的

  • 用户态与内核态的切换发生了 3 次,这个操作比较重量级

  • 数据拷贝了共 4 次

  • NIO 优化

通过 DirectByteBuf

  • ByteBuffer.allocate(10) HeapByteBuffer 使用的还是 java 内存

  • ByteBuffer.allocateDirect(10) DirectByteBuffer 使用的是操作系统内存

 网络编程-Netty篇

大部分步骤与优化前相同,不再赘述。唯有一点:java 可以使用 DirectByteBuf 将堆外内存映射到 jvm 内存中来直接访问使用

  • 这块内存不受 jvm 垃圾回收的影响,因此内存地址固定,有助于 IO 读写

  • java 中的 DirectByteBuf 对象仅维护了此内存的虚引用,内存回收分成两步

    • DirectByteBuf 对象被垃圾回收,将虚引用加入引用队列

    • 通过专门线程访问引用队列,根据虚引用释放堆外内存

  • 减少了一次数据拷贝,用户态与内核态的切换次数没有减少

进一步优化(底层采用了 linux 2.1 后提供的 sendFile 方法),java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据

网络编程-Netty篇

  1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu

  2. 数据从内核缓冲区传输到 socket 缓冲区,cpu 会参与拷贝

  3. 最后使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

可以看到

  • 只发生了一次用户态与内核态的切换

  • 数据拷贝了 3 次

 进一步优化(linux 2.4)

网络编程-Netty篇

  1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu

  2. 只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗

  3. 使用 DMA 将 内核缓冲区的数据写入网卡,不会使用 cpu

整个过程仅只发生了一次用户态与内核态的切换,数据拷贝了 2 次。所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到 jvm 内存中,零拷贝的优点有

  • 更少的用户态与内核态的切换

  • 不利用 cpu 计算,减少 cpu 缓存伪共享

  • 零拷贝适合小文件传输


下一章知识点请阅读:Netty入门

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/72531.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!