“我眼中的Netty”这一系列的文章,着重通过自己的语言来描述自己对Netty核心概念的认识。这其中不对概念做一板一眼的定义与记录。其中在”初始“的章节中,主要介绍Netty的原由,以及Netty的设计模型。
一、BIO网络编程
1、同步&异步与阻塞&非阻塞的区别联系
这两组概念个人理解为是一个是道的层面一个是术的层面,同步&异步为道,阻塞&非阻塞为术。道的层面可以理解一个调用在没有得到结果前不会返回这就是同步,如果是异步的话就是调用会立即返回继续向下执行,被调用这通过状态的方式来通知调用者,这个执行是否结束,Node编程就是异步编程。术的层面关注的是当前执行这个线程在等待调用结果时候的状态,如果在等待结果的时候该调用不会阻塞当前线程那么就是非阻塞,如果这个调用阻塞了当前这个线程就说为是阻塞。如果想要详细的了解这两者的关系应该梳理清楚Node的异步原理以及Java的并发编程,这样会对此有一个更深度的了解。
2、BIO是这样的
通过端口地址创建一个ServerSocket,在一个死循环中不断的执行这个ServerSocket的accept方法来检查是否有客户端连接,如果有连接则会返回一个socket如果没有的话主线程将会一直的阻塞在这里,当拿到这个socket之后主线程调用一个初始好的线程池将这个socket交给一个线程去处理,因为这个处理过程不再主线程中而是在另外一个线程中的所以主线程不会被阻塞,循环将会继续在此到accept这里监听是否有新的客户端请求连接,开始下一次的循环。

3、BIO的Demo示例
public class BIOServer {
public static void main(String[] args) throws Exception {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
while (true) {
System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接....");
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");
//就创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() { //我们重写
//可以和客户端通讯
handler(socket);
}
});
}
}
//编写一个handler方法,和客户端通讯
public static void handler(Socket socket) {
try {
System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
//通过socket 获取输入流
InputStream inputStream = socket.getInputStream();
//循环的读取客户端发送的数据
while (true) {
System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
System.out.println("read....");
int read = inputStream.read(bytes);
if(read != -1) {
System.out.println(new String(bytes, 0, read
)); //输出客户端发送的数据
} else {
break;
}
}
}catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println("关闭和client的连接");
try {
socket.close();
}catch (Exception e) {
e.printStackTrace();
}
}
}
}
二、NIO非阻塞编程
1、NIO是这样的
BIO的弊端在与每一个Socket都需要一个线程去处理,当这个Socket发生读写IO操作的时候,线程发生阻塞造成系统资源的浪费,这也浪费也包括线程的上下文切换。NIO做的就是解决这个痛点他抽象出来Selector、Buffer、Channel,Selector负责监听是否有其他的客户端来请求连接,他也可以监听是否有读写事件发生,如果有的话通知相应的线程进行处理。那么既然不是立即处理的那就需要一个缓冲数据的地方起这个作用的就是Buffer,而连接客户端与服务端的通道就是Channel。这三大组件也是Netty框架实现的基础。
2、NIO的设计模型

3、NIO的Demo示例
-
使用Buffer进行文件拷贝
public class NIOFileChannel03 { public static void main(String[] args) throws Exception { int flag = 0; FileInputStream fileInputStream = new FileInputStream("1.txt"); FileChannel fileChannel01 = fileInputStream.getChannel(); FileOutputStream fileOutputStream = new FileOutputStream("2.txt"); FileChannel fileChannel02 = fileOutputStream.getChannel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); while (true) { byteBuffer.clear(); //清空buffer int read = fileChannel01.read(byteBuffer); System.out.println("read =" + read); if(read == -1) { //表示读完 break; } //将buffer 中的数据写入到 fileChannel02 -- 2.txt byteBuffer.flip(); fileChannel02.write(byteBuffer); } //关闭相关的流 fileInputStream.close(); fileOutputStream.close(); } } -
Selector的使用
public class NIOServer { public static void main(String[] args) throws Exception{ //创建ServerSocketChannel -> ServerSocket ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); //得到一个Selecor对象 Selector selector = Selector.open(); //绑定一个端口6666, 在服务器端监听 serverSocketChannel.socket().bind(new InetSocketAddress(6666)); //设置为非阻塞 serverSocketChannel.configureBlocking(false); //把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1 //循环等待客户端连接 while (true) { //这里我们等待1秒,如果没有事件发生, 返回 if(selector.select(1000) == 0) { //没有事件发生 System.out.println("服务器等待了1秒,无连接"); continue; } //如果返回的>0, 就获取到相关的 selectionKey集合 //1.如果返回的>0, 表示已经获取到关注的事件 //2. selector.selectedKeys() 返回关注事件的集合 // 通过 selectionKeys 反向获取通道 Set<SelectionKey> selectionKeys = selector.selectedKeys(); System.out.println("selectionKeys 数量 = " + selectionKeys.size()); //遍历 Set<SelectionKey>, 使用迭代器遍历 Iterator<SelectionKey> keyIterator = selectionKeys.iterator(); while (keyIterator.hasNext()) { //获取到SelectionKey SelectionKey key = keyIterator.next(); //根据key 对应的通道发生的事件做相应处理 if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接 //该该客户端生成一个 SocketChannel SocketChannel socketChannel = serverSocketChannel.accept(); System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode()); //将 SocketChannel 设置为非阻塞 socketChannel.configureBlocking(false); //将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel //关联一个Buffer socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4.. } if(key.isReadable()) { //发生 OP_READ //通过key 反向获取到对应channel SocketChannel channel = (SocketChannel)key.channel(); //获取到该channel关联的buffer ByteBuffer buffer = (ByteBuffer)key.attachment(); channel.read(buffer); System.out.println("form 客户端 " + new String(buffer.array()).toString()); } //手动从集合中移动当前的selectionKey, 防止重复操作 keyIterator.remove(); } } } }
三、Netty设计模式
1、Netty概述
Netty作为一个网络层框架在诸多的微服务中间件中都有广泛的应用,他是基于Java的NIO之上构建,但是由于原生的NIO 操作复杂需要考虑诸多的因素,所以就诞生了设计良好易于使用、高性能、高健壮、安全的Netty。

2、Netty的设计模型
Netty是基于JavaNIO的,上面的我们知道NIO包含三大组件Selector、Channel、Buffer三大组件,基于这三大组件常见的三种模型可以有单Reactor单线程、单Reactor多线程、多Reactor多线程,三大种类。其中Netty的设计模式是在多Reactor多线程的基础上发展而来的。
- 单Reactor单线程:这样的模式可以一个线程监听多个Socket,实现多路复用。但是如果一个IO的操作非常耗时,那么他所关联的其他Socket就也会就如阻塞因为当前线程正在处理其他Socket的操作。如果一个Socket发生了阻塞那么并不一定会导致整个线程都阻塞,他可以运行其他的Socket。
- 单Reactor多线程:上面的模式是对Socket动作的监听以及IO操作的处理都在一个新线程中,此模型中Reactor的线程负责事件的监听以及内容的分发,对于具体的业务逻辑处理则由Worker线程池进行。这样相对于上面的单Reactor单线程就提供了更多的性能起码不会以因为IO事件的处理而对事件的监听造成影响
- 主从Reactor多线程:这三种的模型的关系就像是层层递进,此模型相对于单Reactor的来说,他将Reactor负责的工作进一步拆分,主Reactor负责对对连接事件的监听,从Reactor负责对读写事件进行监听。正如那句”没有什么是加一层解决不了的“所说,复杂的可伸缩的Netty就是在一层加一层的基础上诞生。这里面说的主Reactor可以对标Netty服务端的BossGroup(他也是可以包含多个EventLoop的,但是❓如何实现多个Selector对一个Channel监听,这就是一个问题了),而从Reactor可以对比与WorkGroup。客户端我们知道只有一个Group,并且他也是只包含一个EventLoop,这是因为他只需要对一个Channel进行监听此处应该可以类比为单Reactor单线程,因为EventLoop在Netty的设计中是从始至终绑定一个线程的,所以说监听与IO的业务逻辑处理都是在一个线程中的。

服务端创建BossGroup的时候虽然我们可以通过入参指定此Group可以有多个EventLoop但是在通过bind进行端口绑定的时候应该还是通过一定的算法选取了其中的一个,所以说BossGroup在创建的时候还是直接指定他的EventLoop为1即可,否则就算创建了多个最后选的也就只有一个,下面我们来说一下这个创建的这一块源码的流程:
1、在通过group(bossGroup, workerGroup)来为引导绑定Group的时候是将这两个Group分别绑定在了相关的属性上,其中bossGroup的入参是绑定到了AbstractBootstrap的group上了,对group中EventLoop的绑定是发生在bind的这一步骤的
2、调用bind的时候会通过层层调用在AbstractBootstrap的initAndRegister,首先是通过channelFactory.newChannel();获得一个Channel,之后调用config().group().register(channel);的方法获取到当前Bootstrap中绑定的Group获取到,我要注册的EventLoop
3、由于Group中可能是有多个EventLoop的所以获取是需要一定的算法的,这是通过调用DefaultEventExecutorChooserFactory.PowerOfTwoEventExecutorChooser.next;这里采用到了的是元子类以及与操作防止越界异常
@Override public EventExecutor next() { return executors[idx.getAndIncrement() & executors.length - 1]; }
4、通过上面的步骤获取到一个EventLoop,这个EventLoop相对是Nio 中的一个EventLoop,相关的源码在另一个文章中再做详细的介绍。
本文深入探讨了Netty的核心概念,从BIO(同步阻塞I/O)开始,解释了同步与异步、阻塞与非阻塞的区别,并给出BIO的简单示例。接着,介绍了NIO(非阻塞I/O)的优势,如Selector、Buffer和Channel的使用,以及NIO在解决BIO问题上的改进。最后,讨论了Netty基于NIO设计的多Reactor多线程模型,阐述了其在服务端的BossGroup和WorkerGroup的角色,以及如何通过主从Reactor模式提高性能和可扩展性。
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EatsWsTR-1651676111535)(F:\typroa\aimages\image-20220419110558917.png)]](https://i-blog.csdnimg.cn/blog_migrate/c7e613e1d18b0618aad9e7afacaff794.png)
1299

被折叠的 条评论
为什么被折叠?



