java高并发之NIO与NIO主力框架

本文介绍了Java NIO的原理与组件,包括Buffer、Channel和Selector,并阐述了NIO相对于BIO的优势。此外,对比了主流NIO框架Mina、Netty和Grizzly,特别是探讨了Netty为何在众多框架中脱颖而出,总结了Netty的高性能、易用性和广泛适用性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

NIO

NIO是jdk1.4出现的新的流。
BIO-Blocking IO - 同步式阻塞式IO流----UDP/TCP
NIO - New IO - 同步式非阻塞式IO流
AIO - AsynchronousIO - 异步式非阻塞式IO流 - jdk1.8

BIO的缺点

1.会产生阻塞行为:receive/accept/connect/read/write
2.一对一的连接:每连接一个客户端,再服务器端就要开启一个线程去处理请求,在客户端较多的情况下,服务端就会产生大量的线程,耗费内存。
3.连接建立之后如果不发生任务的操作,那么就会导致服务器中的这个线程依然被占用,耗费服务器的资源
4.无法实现定点操作。

NIO介绍

三个基本组件

Buffer - 缓冲区

容器:存储数据,在底层存储数据的时候实际上是以数组形式来储存的。
capacity:容量位,指定缓冲区的容量。
limit:限制位,限制操作位所能达到的尺度。
position:操作位,指定要操作的位置。
mark:标记位,标记位置,认为标记位置之前的数据是已经操作过的没有错误数据。
mark <= position <= limit <= capacity
filp:反转缓冲区,先将限制位挪到操作位上,然后将操作位归零,清空标记位。
clear:清空缓冲区,将操作位归零,将limit挪到capacity,将标记位清空。
reset:重置缓冲区,将操作位挪到标记位。
rewind:重绕缓冲区,将操作位归零,将标记位清空。缓冲区多次读取。

import java.nio.ByteBuffer;

public class BufferDemo {

	public static void main(String[] args) {

		// 创建缓冲区对象
		// ByteBuffer底层依靠字节数组来存储数据
		// ByteBuffer buffer = ByteBuffer.allocate(10);

		// 在创建缓冲区的时候传入字节数组,并且先定了字节的数组的大小
		// 虽然这种方式给定了数据,但是position依然从第0位开始计算
		ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
		// buffer.put((byte)97);
		// 获取操作位
		// System.out.println(buffer.position());
		// System.out.println(buffer.capacity());
		// System.out.println(buffer.limit());
		// 添加数据
		// buffer.put("abc".getBytes());
		// buffer.put("def".getBytes());
		// System.out.println(buffer.position());

		// 反转缓冲区
		// buffer.limit(buffer.position());
		// 设置操作位
		// buffer.position(0);
		// buffer.flip();

		// 获取数据
		// byte b = buffer.get();
		// System.out.println(b);

		// 实际上判断操作位是否小于限制位
		// while (buffer.hasRemaining()) {
		// byte b = buffer.get();
		// System.out.println(b);
		// }

		// 将缓冲区转化为数组
		byte[] data = buffer.array();
		// buffer.flip();
		// System.out.println(new String(data,0,buffer.limit()));
		System.out.println(new String(data, 0, buffer.position()));

	}

}
Channel - 通道

传输数据,是面向缓冲区的。在java中,channel默认也是阻塞的,需要手动将其设置位非阻塞模式。
BIO: File、UDP - DatagramSocket、TCP - Socket, ServerSocket
NIO: FileChannel、UDP - DatagramChannel、TCP - SocketChannel, ServerSocketChannel
FileChannel - 操作文件,可以利用通道实现相同平台之间的零拷贝技术。
客户端实现

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class ClientDemo {

	public static void main(String[] args) throws IOException, InterruptedException {

		// 创建客户端的通道
		SocketChannel sc = SocketChannel.open();

		// NIO默认是阻塞的,手动设置为非阻塞
		sc.configureBlocking(false);

		// 发起连接 - 即使连接失败,也会继续向下执行
		sc.connect(new InetSocketAddress("localhost", 8090));

		// 如果单独使用channel,需要将它进行手动阻塞
		// 判断连接是否建立
		// 这个方法底层会判断连接是否建立,如果建立则继续往下执行
		// 如果这个连接没有建立,那么在底层会试图再次建立连接
		// 如果试图连接多次失败,那么会抛出异常
		while (!sc.finishConnect())
			;

		// 写出数据
		sc.write(ByteBuffer.wrap("hello".getBytes()));
		
		Thread.sleep(10);

		// 读取数据
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		sc.read(buffer);
		buffer.flip();
		System.out.println(new String(buffer.array(), 0, buffer.limit()));

		// 关闭
		sc.close();
	}

}

服务端实现

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;

public class ServerDemo {

	public static void main(String[] args) throws IOException, InterruptedException {

		// 创建服务器端的通道
		ServerSocketChannel ssc = ServerSocketChannel.open();

		// 绑定要监听的端口
		ssc.bind(new InetSocketAddress(8090));

		// 设置为非阻塞
		ssc.configureBlocking(false);

		// 接受连接
		SocketChannel sc = ssc.accept();

		// 手动阻塞
		while (sc == null)
			sc = ssc.accept();

		// 准备一个缓冲区用于存储数据
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		// 读取数据
		sc.read(buffer);
		buffer.flip();
		System.out.println(new String(buffer.array(), 0, buffer.limit()));

		sc.write(ByteBuffer.wrap("接收成功~~~".getBytes()));
		Thread.sleep(10);
		
		ssc.close();
	}

}
Selector - 多路复用选择器

进行选择,是面向通道进行操作。要求通道在使用的时候必须设置位非阻塞。
客户端:可连接,可读,可写
服务端:可接收,可读,可写
通过Selector可以实现利用同一个服务器端来处理多个客户端的数据 ,也就是说我们可以用少量线程处理大量的请求,在底层处理的时候实际上依然是同步的。
客户端实现

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class ClientDemo {

	public static void main(String[] args) throws IOException {

		// 创建客户端的通道
		SocketChannel sc = SocketChannel.open();

		// 获取选择器
		Selector selc = Selector.open();

		// 选择器管理的连接要求必须是非阻塞的
		sc.configureBlocking(false);

		// 将客户端注册到选择器身上,并且申请了一个可连接事件
		sc.register(selc, SelectionKey.OP_CONNECT);

		// 发起连接
		sc.connect(new InetSocketAddress("localhost", 8090));

		// 从这儿开始的代码针对多客户端来进行操作的
		while (true) {

			// 选择出注册过的通道
			selc.select();

			// 针对通道的不同事件类型进行处理
			Set<SelectionKey> keys = selc.selectedKeys();

			Iterator<SelectionKey> it = keys.iterator();
			while (it.hasNext()) {
				// 根据事件类型进行处理
				SelectionKey key = it.next();

				// 判断是否是可连接事件
				if (key.isConnectable()) {

					// 从当前事件中获取到对应的通道
					SocketChannel scx = (SocketChannel) key.channel();

					// 如果是可连接事件,判断连接是否成功
					while (!scx.finishConnect())
						;

					// 如果连接成功了,可能会向服务器端发数据或者读数据
					scx.register(selc, SelectionKey.OP_WRITE | SelectionKey.OP_READ);
				}

				// 判断是否是可写事件
				if (key.isWritable()) {

					// 从当前事件中获取到对应的通道
					SocketChannel scx = (SocketChannel) key.channel();

					// 写出数据
					scx.write(ByteBuffer.wrap("hello~~~".getBytes()));

					// 需要去掉可写事件
					// 获取这个通道身上的所有的事件
					scx.register(selc, key.interestOps() ^ SelectionKey.OP_WRITE);

				}

				// 判断是否是可读事件
				if (key.isReadable()) {

					// 从当前事件中来获取到对应的通道
					SocketChannel scx = (SocketChannel) key.channel();

					// 读取数据
					ByteBuffer buffer = ByteBuffer.allocate(1024);
					scx.read(buffer);
					buffer.flip();
					System.out.println(new String(buffer.array(), 0, buffer.limit()));

					// 需要去掉这个可读事件
					scx.register(selc, key.interestOps() ^ SelectionKey.OP_READ);
				}

				// 处理完这一大类事件之后
				it.remove();
			}

		}

	}

}

服务端实现

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class ServerDemo {
	
	public static void main(String[] args) throws IOException {
		
		// 创建服务器端的通道
		ServerSocketChannel ssc = ServerSocketChannel.open();
		
		// 绑定要监听的端口号
		ssc.bind(new InetSocketAddress(8090));
		
		// 开启选择器
		Selector selc = Selector.open();
		
		ssc.configureBlocking(false);
		
		// 将通道注册到选择器上,需要注册一个可接受事件
		ssc.register(selc, SelectionKey.OP_ACCEPT);
		
		while(true){
			
			// 选择出已经注册的连接
			selc.select();
			
			// 根据事件的不同进行分别的处理
			Set<SelectionKey> keys = selc.selectedKeys();
			
			Iterator<SelectionKey> it = keys.iterator();
			while (it.hasNext()) {
				// 将事件取出来分别进行处理
				SelectionKey key = it.next();
				
				// 判断可接受事件
				if(key.isAcceptable()){
					
					// 从事件中获取到对应的通道
					ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
					
					// 接受连接
					SocketChannel sc = sscx.accept();
					sc.configureBlocking(false);
										
					// 注册读写事件
					sc.register(selc, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
					
				}
				
				// 判断可读事件
				if(key.isReadable()){
					
					// 从事件中获取到对应的通道
					SocketChannel sc = (SocketChannel) key.channel();
					
					// 读取数据
					ByteBuffer buffer = ByteBuffer.allocate(1024);
					sc.read(buffer);
					buffer.flip();
					System.out.println(new String(buffer.array(),0, buffer.limit()));
					
					// 需要去掉可读事件
					sc.register(selc, key.interestOps() ^ SelectionKey.OP_READ);
					
				}
				
				// 判断可写事件
				if(key.isWritable()){
					
					// 从事件中获取到对应的通道
					SocketChannel sc = (SocketChannel) key.channel();
					
					// 写出数据
					sc.write(ByteBuffer.wrap("收到".getBytes()));
					
					// 需要去掉可写事件
					sc.register(selc, key.interestOps() ^ SelectionKey.OP_WRITE);
					
				}
				
				// 去掉这一大类的事件
				it.remove();
				
			}
			
		}
	}

}
NIO的优势

1.非阻塞:提高传输效率。
2.一对多的连接:可以用一个或者少量的服务器中的线程来处理大量请求,从个人节省服务器的内存资源。
3.即使已建立连接,只要没有对应的读写时间,那么依然不能够使用服务器来处理。
4.利用通道实现数据的双向传输。
5.因为利用缓冲区来存储数据,所以可以对缓冲区中的数据实现定点操作。

主流的NIO框架对比

流行基于Java NIO通信框架有Mina、Netty、Grizzly等。
Mina出身于开源界的大牛Apache组织;
Netty出身于商业开源大亨Jboss;
Grizzly则出身于Sun公司

设计理念
Mina

Mina(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能 和高可用性的网络应用程序提供了非常便利的框架。当前发行的 Mina 版本2.04支持基于 Java NIO 技术的 TCP/UDP 应用程序开发、串口通讯程序,Mina 所支持的功能也在进一步的扩展中。 目前,正在使用Mina的应用 包括:Apache Directory Project、AsyncWeb、AMQP(Advanced Message Queuing Protocol)、RED5 Server(Macromedia Flash Media RTMP)、ObjectRADIUS、 Openfire等等

Netty

Netty是一款异步的事件驱动的网络应用框架和工具,用于快速开发可维护的高性能、高扩展性协议服务器和客户 端。也就是说,Netty是一个NIO客户端/服务器框架,支持快速、简单地开发网络应用,如协议服务器和客户端。 它极大简化了网络编程,如TCP和UDP套接字服务器。

Grizzly

Grizzly是一种应用程序框架,专门解决编写成千上万用户访问服务器时候产生的各种问题。使用JAVA NIO作为基 础,并隐藏其编程的复杂性。容易使用的高性能的API。带来非阻塞socket到协议处理层。利用高性能的缓冲和缓 冲管理使用高性能的线程池。
从设计的理念上来看,Mina的设计理念是最为优雅的。当然,由于Netty的主导作者与Mina的主导作者是同一人, 出自同一人之手的Netty在设计理念上与Mina基本上是一致的。而Grizzly在设计理念上就较差了点,几乎是 JavaNIO的简单封装。

Netty为什么这么火?

Netty是目前最流行的由JBOSS提供的一个Java开源框架NIO框架,Netty提供异步的、事件驱动的网络应用程序框 架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。 相比JDK原生NIO,Netty提供了相对十分 简单易用的API,非常适合网络编程。Netty是完全基于NIO实现的,所以Netty是异步的。 作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机制,用户可以方便的主动 获取或者通过通知机制获得IO操作结果。 Netty无疑是NIO的老大,它的健壮性、功能、性能、可定制性和可扩展性在同类框架都是首屈一指的。它已经得 到成百上千的商业/商用项目验证,如Hadoop的RPC框架Avro、RocketMQ以及主流的分布式通信框架Dubbo等 等。 为什么这么火,是有原因的。

Netty的优点直接总结如下:

API使用简单,开发门槛低;
功能强大,预置了多种编解码功能,支持多种主流协议;
定制能力强,可以通过ChannelHandler对通信框架进行灵活地扩展;
性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优;
成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为NIO的BUG而烦 恼;
社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会加入;
经历了大规模的商业应用考验,质量得到验证。在互联网、大数据、网络游戏、企业应用、电信软件等 众多行业得到成功商用,证明了它已经完全能够满足不同行业的商业应用了。

与Mina相比的优势:

  1. 都是Trustin Lee的作品,Netty更晚;
  2. Mina将内核和一些特性的联系过于紧密,使得用户在不需要这些特性的时候无法脱离,相比下性能会有 所下降,Netty解决了这个设计问题;
  3. Netty的文档更清晰,很多Mina的特性在Netty里都有;
  4. Netty更新周期更短,新版本的发布比较快;
  5. 它们的架构差别不大,Mina靠apache生存,而Netty靠jboss,和jboss的结合度非常高,Netty有对 google protocal buf的支持,有更完整的ioc容器支持(spring,guice,jbossmc和osgi);
  6. Netty比Mina使用起来更简单,Netty里你可以自定义的处理upstream events或/和downstream events,可以使用decoder和encoder来解码和编码发送内容;
  7. Netty和Mina在处理UDP时有一些不同,Netty将UDP无连接的特性暴露出来;而Mina对UDP进行了高 级层次的抽象,可以把UDP当成"面向连接"的协议,而要Netty做到这一点比较困难。
  8. 从任务调度粒度上看,mina会将有IO任务的session写入队列中,当循环执行任务时,则会轮询所有的 session,并依次把session中的所有任务取出来运行。这样粗粒度的调度是不公平调度,会导致某些请 求的延迟很高。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值