Netty-4-网络编程模式

news2024/12/24 18:04:49

我们经常听到各种各样的概念——阻塞、非阻塞、同步、异步,这些概念都与我们采用的网络编程模式有关。

例如,如果采用BIO网络编程模式,那么程序就具有阻塞、同步等特质。

诸如此类,不同的网络编程模式具有不同的特点,这些网络编程模式就相当于我们的网络编程套路。

因此,了解并掌握网络编程模式是学习Netty和使用Netty进行网络编程的必经之路。

下面我们学习网络编程模式以及Netty如何对它们提供支持。

网络编程的3种模式

当我们去饭店吃饭时,会经常遇到以下三种模式。

  • 排队打饭模式。这种模式主要出现在食堂等场所。人们在窗口前排队,饭菜打好后才走,若饭菜没有打好,人们一般是不会主动离开的。
  • 点餐等待被叫模式。在这种模式下,我们会收到点餐号,饭店备好饭菜后就呼叫点餐号,我们需要自行去取。
  • 包厢模式。这是我们最喜欢的模式,点餐后什么都不用管,坐在那里等着饭菜被服务员端上桌即可。

为了方便解释,我们可以将就餐模式与服务器应用做类比。

例如,把饭店比作服务器,而把饭菜比作数据。

这样的话,饭菜好了就相当于数据就绪,而端菜行为则可以当作读取数据。

通过进行类比,我们发现就餐的3种模式其实正好对应经典的3种网络编程I/O模式。
在这里插入图片描述
阻塞与非阻塞之间的区别在于要不要一直等,直到饭菜做好。

换言之,对于阻塞而言,在数据没有传输过来之前,会阻塞等待,直到数据到来;写的过程也类似,当缓冲区满时,写操作也会被阻塞,直到缓冲区"可写”。

但是,对于非阻塞而言,遇到这些情况则不做任何停留, 直接返回。

同步和异步之间的区别在于数据就绪后谁来读,类似于“饭菜好了谁来端”的问题。

数据就绪后,如果需要应用程序自行读取,就是同步过程;数据就绪后,如果由系统直接读取并回调给程序,就是异步过程。

在区分完以上两组概念后,我们可以做下对应:BIO是阻塞同步方式;NIO是非阻塞同步方式;AIO是非阻塞异步方式。

网络编程模式的选择要点

从表面上看,我们一般倾向于使用新的模式。

例如,我们青睐的顺序可能是AIO-NTO-BIO但是实际上,看似明显的优先顺序并不是什么"黄金准则”。

我们需要结合更多的因素来决定如何做出选择。

服务的连接数

假设应用程序的连接数有限,比如只有一两个连接,我们就无法预见NIO模式的性能肯定比传统程序的BIO模式好。

不过,可以预见的是,NIO模式的代码实现复杂度肯定高于BIO模式。

因此,在选择I/O模式时,我们需要了解应用程序到底能够支持多少个连接。

服务将要部署到的平台

对于Windows平台,我们一般都会优先选择AIO模式。
但是对于Linux平台,情况将可能有所不同,毕竟Linux平台对AIO模式的支持还不够成熟。

另外,有关NIO和AIO模式的一些测试表明,其实在Linux平台上,NIO和AIO模式的代码实现并无太大性能差别。

因此,对于大多数使用Linux作为服务器的应用平台而言,NIO模式或许才是更好的选择。

当前已有的架构和可用的实现

如果当前项目一直使用某种模式,那么我们一般很少有勇气去直接变革和采用新的模式,而是修修补补并沿用旧的模式。

此外,即使对于全新的项目,也不一定会选择我们想用的模式。例如,假设要选择AIO模式,但我们的技术栈是基于Netty的,那么AIO模式用不了。

综上所述,不同网络编程模式有自己适用的场景,我们不能仅仅依据推出时间的前后就直接做出决策。

换言之,具体问题具体分析、具体场景具体选择才是永恒之道。

Netty对网络编程模式的支持

在这里插入图片描述

Netty目前只推崇NIO模式。为什么不推荐另外两种模式呢?

Netty为什么不推荐BIO/OIO模式?

在连接数较多的情况下,BIO/OIO模式的阻塞特性就意味着耗资源、效率低。具体而言, 阻塞就意味着等待,等待就会占用线程。
考虑一下,在连接数比较多的情况下,如果每个连接上的请求又都在等待,占用的线程将会非常多,资源耗费就太大了。
不仅如此,一个进程对所能创建的线程数量也是有约束的,这一点无法克服。

其次,Netty为什么废弃AIO模式?原因主要有三点。

  1. Windows平台上的实现虽然已经非常成熟,但是Windows平台本身很少用作服务器。
  2. Linux平台经常用作服务器,但是Linux平台上AIO模式的实现还不够成熟。
  3. Linux平台上AIO模式的实现相比NIO模式而言稍复杂一些,但性能提升并不明显。

由上可知,对于Netty而言,NIO是Netty推崇的核心I/O模式。但是,有必要补充说明的是,对于NIO模式的实现方式,Netty支持的不止一种。

在这里插入图片描述
Netty的通用NIO模式的实现方式在Linux平台上使用的也是EpolL那么为什么此处还需要一套专有的Epoll相关实现呢?
“重新造轮子”无非有两个原因,自己的轮子更好、更强,这个道理同样适用于此。

具体原因包括以下两个方面。

  • Epoll相关实现能够暴露更多可控的参数:JDK的很多参数都不可调。例如,JDK的NIO模式在Linux平台上默认是水平触发且不可修改的;但对于Netty而言,水平触发和边缘触发都支持,并且可以切换(默认是边缘触发)。

  • 垃圾回收更少,性能更好:这是Netty开发者自己给出的理由。不过,我们有理由相信Netty确实做到了,否则没有必要"重新造轮子"。

Netty对网络编程模式的实现

        // 创建一个ServerBootstrap实例
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        // 设置服务器通道类型为OioServerSocketChannel
        serverBootstrap.channel(OioServerSocketChannel.class);
        // 创建一个OioEventLoopGroup实例
        OioEventLoopGroup eventLoopGroup = new OioEventLoopGroup();
        // 设置服务器线程组为创建的OioEventLoopGroup实例
        serverBootstrap.group(eventLoopGroup);

OIO模式的使用主要涉及OioServerSocketChannel和OioEventLoopGroup这两个关键类。

ServerSocketChannel 的创建

当执行 serverBootstrap.channel (OioServerSocketChannel.class)时,实际上执行的是如下方法:

//AbstractBootstrap.java
    /**
     * 设置通道的类型为指定的通道类
     * 
     * @param channelClass 通道类
     * @return 通道配置对象
     */
    public B channel(Class<? extends C> channelClass) {
        return channelFactory(new ReflectiveChannelFactory<C>(
                ObjectUtil.checkNotNull(channelClass, "channelClass")
        ));
    }

上述代码创建了 ReflectiveChannelFactory 来负责创建 OioServerSocketChannelo 顾名思义,ReflectiveChannelFactory使用“反射"方式来完成上述工作。

//ReflectiveChannelFactory.java

    private final Constructor<? extends T> constructor;  // 构造器,用于创建指定类型的对象

    public ReflectiveChannelFactory(Class<? extends T> clazz) {  // 构造函数,用于创建ReflectiveChannelFactory对象
        ObjectUtil.checkNotNull(clazz, "clazz");  // 检查clazz是否为null
        try {
            this.constructor = clazz.getConstructor();  // 获取指定类的无参构造函数
        } catch (NoSuchMethodException e) {
            throw new IllegalArgumentException("Class " + StringUtil.simpleClassName(clazz) +
                    " does not have a public non-arg constructor", e);  // 如果指定类没有无参公共构造函数,则抛出异常
        }
    }

    @Override
    public T newChannel() {  // 创建一个指定类型的对象
        try {
            return constructor.newInstance();  // 使用构造器创建对象
        } catch (Throwable t) {
            throw new ChannelException("Unable to create Channel from class " + constructor.getDeclaringClass(), t);  // 如果创建对象过程中发生异常,则抛出ChannelException异常
        }
    }

EventLoopGroup 的功能

EventLoopGroup 负责给每个通道分配 EventLoop0 例如,OioEventLoopGroup 负责给 BIOChannel分配ThreadPerChannelEventLoop (注意,这里的命名方式和其他的EventLoop不同)。

//ThreadPerChannelEventLoop.java

    @Override
    protected void run() {
        for (;;) {
            // 从任务队列中取出一个任务
            Runnable task = takeTask();
            if (task != null) {
                // 执行任务
                task.run();
                // 更新最后一次执行时间
                updateLastExecutionTime();
            }

            // 获取当前的通道
            Channel ch = this.ch;
            if (isShuttingDown()) {
                // 如果正在关闭
                if (ch != null) {
                    // 关闭通道
                    ch.unsafe().close(ch.unsafe().voidPromise());
                }
                // 如果确认关闭
                if (confirmShutdown()) {
                    break;
                }
            } else {
                // 如果没有正在关闭
                if (ch != null) {
                    // 处理注销
                    if (!ch.isRegistered()) {
                        // 执行所有任务
                        runAllTasks();
                        // 注销
                        deregister();
                    }
                }
            }
        }
    }

ThreadPerChannelEventLoop在本质上相当于任务的执行体,而任务本身就是执行通道上的读写操作。例如,当写数据时,写数据这一操作会被当作任务提交给SingleThreadEventExecutor (ThreadPerChannelEventLoop 的父类)的 execute 方法来执行。


//SingleThreadEventExecutor.java

    private void execute(Runnable task, boolean immediate) {
        // 判断是否在事件循环中
        boolean inEventLoop = inEventLoop();
        // 添加任务到任务队列
        addTask(task);
        if (!inEventLoop) {
            // 启动线程
            startThread();
            // 如果已经关闭
            if (isShutdown()) {
                // 是否从任务队列中移除任务
                boolean reject = false;
                try {
                    // 如果移除了任务,则标记为拒绝
                    if (removeTask(task)) {
                        reject = true;
                    }
                } catch (UnsupportedOperationException e) {
                    // 任务队列不支持移除任务,只能继续希望在任务完全终止之前能够取而代之。
                    // 最坏情况下在任务终止时进行日志记录。
                }
                // 如果被拒绝,则进行拒绝处理
                if (reject) {
                    reject();
                }
            }
        }

        // 如果不希望通过添加任务唤醒线程以及立即执行
        if (!addTaskWakesUp && immediate) {
            // 唤醒线程
            wakeup(inEventLoop);
        }
    }

最终执行的是OioByteStreamChannel#doWriteBytes()方法。

//OioByteStreamChannel.java

    @Override
    protected void doWriteBytes(ByteBuf buf) throws Exception {
        // 获取输出流
        OutputStream os = this.os;
        // 如果输出流为空,则抛出未连接异常
        if (os == null) {
            throw new NotYetConnectedException();
        }
        // 将ByteBuf中的字节写入输出流中
        buf.readBytes(os, buf.readableBytes());
    }

要完成对一种I/O模式的支持,我们至少需要两个组件: SocketChannel负责完成具体的读写务;NioEventLoop负责任务的执行。当需要切换I/O模式时,直接替换掉这些实现即可。

常见疑问

水平触发和边缘触发的定义

Netty既支持水平触发也支持边缘触发。Netty确实提供了这两种触发方式的定义。

public enum EpollMode {
    EDGE_TRIGGERED,
    LEVEL_TRIGGERED;

    private EpollMode() {
    }
}

那么什么是水平触发和边缘触发?在理论层次上进行解析。

水平触发

当被监控的文件描述符上有可读写事件时,通知用户去读写。如果用户一次没有读写完数据,就一直通知用户。

在用户确实不怎么关心这个文件描述符的情况下,频繁通知用户会导致用户真正关心的那些文件描述符的处理效率降低。

比如:点餐后,饭菜做好了(数据就绪),服务员端上来问你吃不吃(读写数据)。 不管你吃溢还是吃不完,服务员总是过来反复提醒你吃饭。

边缘触发

当被监控的文件描述符上有可读写事件时,通知用户去读写,但只通知一次,这就需要用户一次性把数据读写完。

如果用户没有一次性读写完数据,那就需要等待下一次新的数据到来时,才能读写上次未读写完的数据。

比如:服务员端来饭菜后,你没有一次性吃完,等你想吃剩下的饭菜时,就必须再次点餐才行。

Epoll既支持水平触发也支持边缘触发,那么该如何选择呢?如果选择水平触发,就要注意效率和资源利用率;而如果选择边缘触发,就要注意自身是否能一次性完成数据的读写。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1333408.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【大数据】NiFi 的基本使用

NiFi 的基本使用 1.NiFi 的安装与使用1.1 NiFi 的安装1.2 各目录及主要文件 2.NiFi 的页面使用2.1 主页面介绍2.2 面板介绍 3.NiFi 的工作方式3.1 基本方式3.2 选择处理器3.3 组件状态3.4 组件的配置3.4.1 SETTINGS&#xff08;通用配置&#xff09;3.4.2 SCHEDULING&#xff0…

博弈论:理解决策背后的复杂动态

1.基本概念 博弈论是一门研究具有冲突和合作元素决策制定的数学理论。它不仅适用于经济学&#xff0c;还广泛应用于政治学、心理学、生物学等领域。博弈论的核心在于分析参与者&#xff08;称为“玩家”&#xff09;在特定情境下的策略选择&#xff0c;以及这些选择如何影响最…

工资发放 C语言xdoj92

题目描述&#xff1a; 公司财务要发工资现金&#xff0c;需要提前换取100元、50元、20元、10元、5元和1元的人民币&#xff0c; 请输入工资数&#xff0c;计算张数最少情况下&#xff0c;各自需要多少张。 输入格式&#xff1a;共一行&#xff0c;输入一个正整数。 输出格式&am…

游戏软件提示d3dcompiler_43.dll的五个解决方法,亲测靠谱

在使用电脑进行工作&#xff0c;玩游戏的时候&#xff0c;我们常常会遇到一些错误提示&#xff0c;其中之一就是“D3DCompiler_43.dll丢失”的提示。D3DCompiler_43.dll是一个非常重要的动态链接库文件。它是由DirectX SDK提供的&#xff0c;用于编译和优化DirectX着色器代码的…

50 个具有挑战性的概率问题 [04/50]:尝试直至首次成功

一、说明 你好&#xff0c;我最近对与概率相关的问题产生了兴趣。我偶然发现了 Frederick Mosteller 所著的《五十个具有挑战性的概率问题及其解决方案》这本书。我认为创建一个系列来讨论这些可能作为面试问题出现的迷人问题会很有趣。每篇文章仅包含 1 个问题&#xff0c;使其…

【Qt之Quick模块】6. QML语法详解_1 基础语法与三种导入语句

前言 通过以上1-5文档的介绍&#xff0c;Quick与QML的概念及QML语法、类型、文件作用等已叙述个大概&#xff0c;接下来是对QML语法进行展开来说。 其实&#xff0c;学习任何一门语言或者做任何一件事情&#xff0c;并不用一开始就要求尽善尽美&#xff0c;做个无懈可击&…

【Python从入门到进阶】45、Scrapy框架核心组件介绍

接上篇《44、Scrapy的基本介绍和安装》 上一篇我们学习了Scrapy框架的基础介绍以及环境的搭建&#xff0c;本篇我们来学习一下Scrapy框架的核心组件的使用。 下面的核心组件的介绍&#xff0c;仍是基于这幅图的机制&#xff0c;大家可以再回顾一下&#xff1a; 注&#xff1a;…

数学的雨伞下:理解世界的乐趣

这本书没有一个公式&#xff0c;却讲透了数学的本质&#xff01; 《数学的雨伞下&#xff1a;理解世界的乐趣》。一本足以刷新观念的好书&#xff0c;从超市到对数再到相对论&#xff0c;娓娓道来。对于思维空间也给出了一个更容易理解的角度。 作者&#xff1a;米卡埃尔•洛奈…

Ubuntu20.04纯命令配置PCL(点云库)

Ubuntu20.04纯命令配置PCL&#xff08;点云库&#xff09; 最近在学习点云库&#xff08;PCL&#xff09;的使用&#xff0c;第一步就是在自己的电脑安装配置PCL。 首先&#xff0c;对于ubuntu 16.04以上版本&#xff0c;可以直接使用命令进行安装&#xff0c;新建好一个文件夹…

分析冒泡排序

#include <stdio.h> int main() { int arr[10] { 2,5,1,3,6,4,7,8,9,0 }; int i 0; int j 0; for( i 0 ;i < sizeof(arr)/sizeof(arr[0]) - 1 ; i) 红色的代表数组一共有n个元素&#xff0c;则需要n-1次 { for( j 0 // 这里可以让数组从哪一…

人工智能轨道交通行业周刊-第69期(2023.12.11-12.24)

本期关键词&#xff1a;集装箱智能管理、智慧工地、智能应急机器人、车辆构造、大模型推理 1 整理涉及公众号名单 1.1 行业类 RT轨道交通人民铁道世界轨道交通资讯网铁路信号技术交流北京铁路轨道交通网上榜铁路视点ITS World轨道交通联盟VSTR铁路与城市轨道交通RailMetro轨…

UG在实体上刻字

当我们想在实体上显示文字的时候&#xff0c;需要用到文本命令&#xff0c;菜单-插入-曲线-文本 文本命令中的具体用法 当在曲线和平面上显示文字的时候&#xff0c;只需要输入文字&#xff0c;并选中相应的曲线或者平面即可 当在曲面上显示文字的时候&#xff0c;设置如下 当文…

Vue3中的混入(mixins)

本文主要介绍Vue3中的混入&#xff08;mixins&#xff09;。 目录 一、在普通写法中使用混入&#xff1a;二、在setup写法中使用混入&#xff1a; 混入是Vue中一种用于在组件中共享可复用功能的特性。在Vue 3中&#xff0c;混入的使用方式有所改变。 一、在普通写法中使用混入…

c++11--类型自动推导

1.自动类型推断 1.1.auto a.auto声明变量的类型必须由编译器在编译时期推导而得。 int main(){double foo();auto x 1;//x类型为intauto y foo();// y类型为doubleauto z;// errreturn 0; }b.auto声明得变量必须被初始化。 c.针对指针和引用 推导类型是指针类型时&#xff0…

抠图、换背景、正装图证件照制作方法

本篇灵感是最近又要使用别的底色的正装照的图片。上学的时候&#xff0c;要求证件照的底色是蓝底、党员档案里要求图片的底色是红底、 将来上班的证件照要求是白底&#xff0c;并且无论是考研还是找工作都是制作简历的时候&#xff0c;根据简历的样板不同需要更换不同的底色。 …

HBase 集群搭建

文章目录 安装前准备兼容性官方网址 集群搭建搭建 Hadoop 集群搭建 Zookeeper 集群解压缩安装配置文件高可用配置分发 HBase 文件 服务的启停启动顺序停止顺序 验证进程查看 Web 端页面 安装前准备 兼容性 1&#xff09;与 Zookeeper 的兼容性问题&#xff0c;越新越好&#…

jQuery实现响应式瀑布流 - 实现灯箱效果

在这之前&#xff0c;有写过一篇关于实现瀑布流的文章&#xff0c;后期有人留言提出需要添加灯箱效果的功能&#xff0c;所以这次则讲述下如何实现此功能。由于该篇接上篇写的&#xff1a;jQuery实现响应式瀑布流效果&#xff08;jQueryflex&#xff09;_jquery瀑布流插件-CSDN…

驾驶未来:百度Apollo自动驾驶技术的探索与实践(文末赠送apollo周边)

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《linux深造日志》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! ⛳️ 粉丝福利活动 ✅参与方式&#xff1a;通过连接报名观看课程&#xff0c;即可免费获取精美周边 ⛳️活动链接&#xf…

Java之Synchronized与锁升级

Synchronized与锁升级 一、概述 在多线程并发编程中 synchronized 一直是元老级角色&#xff0c;很多人都会称呼它为重量级锁。但是&#xff0c;随着 Java SE 1.6 对 synchronized 进行了各种优化之后&#xff0c;有些情况下它就并不那么重了。 本文详细介绍 Java SE 1.6 中为…

智能算法(GA、DBO等)求解阻塞流水车间调度问题(BFSP)

先做一个声明&#xff1a;文章是由我的个人公众号中的推送直接复制粘贴而来&#xff0c;因此对智能优化算法感兴趣的朋友&#xff0c;可关注我的个人公众号&#xff1a;启发式算法讨论。我会不定期在公众号里分享不同的智能优化算法&#xff0c;经典的&#xff0c;或者是近几年…