Netty系列-5 Netty启动流程

背景

Netty程序有固定的模板格式,以ServerBootstrap为例:

public class NettyServer {public void start(int port) {ServerBootstrap serverBootstrap = new ServerBootstrap();EventLoopGroup boosGroup = new NioEventLoopGroup(1);EventLoopGroup workGroup = new NioEventLoopGroup(1);try {serverBootstrap.group(boosGroup, workGroup).channel(NioServerSocketChannel.class).handler(new LoggingHandler(LogLevel.INFO)).childOption(ChannelOption.SO_BACKLOG, 128).childHandler(new ChannelInitializer<NioSocketChannel>() {@Overrideprotected void initChannel(NioSocketChannel channel) {channel.pipeline().addLast(new StringEncoder());channel.pipeline().addLast(new StringDecoder());channel.pipeline().addLast(new MyChannelInboundHandler());}});ChannelFuture channelFuture = serverBootstrap.bind(port).sync();channelFuture.channel().closeFuture().sync();} catch (Exception exception) {System.out.println(exception);} finally {boosGroup.shutdownGracefully();workGroup.shutdownGracefully();}}private static class MyHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {System.out.println(msg);}}public static void main(String[] args) {new NettyServer().start(9998);}
}

将负责处理连接的和负责业务处理的NioEventLoopGroup线程池对象、通道类型、处理器Handler、子处理器Handler、连接配置等保存至ServerBootstrap对象,然后调用ServerBootstrap对象的bind方法启动Netty程序。

可以将ServerBootstrap和Bootstrap理解为启动引导器,将信息告知引导器,然后由引导器负责完成后续的启动流程。介绍启动流程前,需要了解一下ServerBootstrap和Bootstrap。

1.ServerBootstrap与Bootstrap

ServerBootstrap和Bootstrap作为Netty的启动引导类,分别用于启动Netty服务端和客户端。
首先看一下类的定义,二者都继承自AbstractBootstrap, 继承关系如下图:
在这里插入图片描述
AbstractBootstrap的核心属性:

public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {private volatile SocketAddress localAddress;volatile EventLoopGroup group;private volatile ChannelHandler handler;private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();private final Map<AttributeKey<?>, Object> attrs = new ConcurrentHashMap<AttributeKey<?>, Object>();//...
}

localAddress用于记录本地ip和端口;group用于记录Netty使用的线程池对象NioEventLoopGroup;handler用于记录处理消息的ChannelHandler(后续加入Pipline);options和attrs记录属性,后续将被添加到通道上。
Bootstrap的核心属性如下所示:

public class Bootstrap extends AbstractBootstrap<Bootstrap, Channel> {private final BootstrapConfig config = new BootstrapConfig(this);private volatile SocketAddress remoteAddress;//...
}

Bootstrap用于启动客户端,因此需要指定远程服务器地址,保存在remoteAddress属性中;config属性保存了Bootstrap的详细信息,后续启动过程中,可直接通过该属性获取配置信息。
ServerBootstrap的核心属性如下所示:

public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel> {private final ServerBootstrapConfig config = new ServerBootstrapConfig(this);private final Map<ChannelOption<?>, Object> childOptions = new LinkedHashMap<ChannelOption<?>, Object>();private final Map<AttributeKey<?>, Object> childAttrs = new ConcurrentHashMap<AttributeKey<?>, Object>();private volatile EventLoopGroup childGroup;private volatile ChannelHandler childHandler;
}

ServerBootstrap用于启动服务器,与Bootstrap相同,选择将所有信息保存在配置对象config中。
由于服务端接收客户端连接后,还需要创建子通道,并将通道信息注册到选择器上;因此需要保存子通道相关的信息:childGroup、childHandler、childOptions和childAttrs.
鉴于服务端逻辑较为复杂, 且对异步流程的设计更为巧秒,本文选择服务端启动为例进行介绍Netty的启动流程。

2.启动流程图

整体的启动流程如下图所示:
在这里插入图片描述
Netty的启动流程整体上看由两个线程完成:启动线程和NioEventLoop线程。NioEventLoop的启动由启动线程触发,启动线程执行创建和配置通道、启动NioEventLoop等主线任务,将注册和绑定等操作委托给NioEventLoop线程执行;线程间通过Future异步交互。以下将结合代码分章节对上图进行介绍。
建议阅读本文前先阅读Netty系列的其他文章。

3.启动流程

启动的入口函数为serverBootstrap.bind(port)

// 调用异步方法bind得到ChannelFuture,并调用sync阻塞等待异步逻辑执行完成
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();// 持续监听,直到关闭Netty
channelFuture.channel().closeFuture().sync();

跟踪bind方法:

public ChannelFuture bind(int inetPort) {return bind(new InetSocketAddress(inetPort));
}public ChannelFuture bind(SocketAddress localAddress) {// doBind前的校验validate();return doBind(ObjectUtil.checkNotNull(localAddress, "localAddress"));
}

校验并运行doBind方法,所有核心逻辑均在doBind方法中。

3.1 doBind方法

private ChannelFuture doBind(final SocketAddress localAddress) {// 1.initAndRegisterfinal ChannelFuture regFuture = initAndRegister();final Channel channel = regFuture.channel();if (regFuture.cause() != null) {return regFuture;}if (regFuture.isDone()) {ChannelPromise promise = channel.newPromise();// 2.doBind0doBind0(regFuture, channel, localAddress, promise);return promise;} else {final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);regFuture.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture future) throws Exception {Throwable cause = future.cause();if (cause != null) {promise.setFailure(cause);} else {promise.registered();// 2.doBind0doBind0(regFuture, channel, localAddress, promise);}}});return promise;}
}

doBind的核心方法是initAndRegister和doBind0,前者完成通道的创建、初始化和注册,后者执行绑定操作。initAndRegister成功执行完成,doBind0才可以执行。

上述代码可以简化为:

private ChannelFuture doBind(final SocketAddress localAddress) {// 1.initAndRegisterfinal ChannelFuture regFuture = initAndRegister();final Channel channel = regFuture.channel();if (regFuture.cause() != null) {return regFuture;}ChannelPromise promise = channel.newPromise();// 阻塞等待regFuture异步逻辑执行完成regFuture.sync();// 执行doBind0doBind0(regFuture, channel, localAddress, promise);return promise;
}

之所以程序写的则这么复杂,是提高了程序效率:执行donBind的线程(这里是主线程)退出去可以执行其他任务,而不用阻塞在此。

ChannelFuture channelFuture = serverBootstrap.bind(port);//... 执行其他任务channelFuture.sync();

源码中根据regFuture异步注册是否完成有两个分支:
[1] 已完成,直接调用doBind0;
[2] 未完成,封装PendingRegistrationPromise对象,向regFuture添加监听器并返回PendingRegistrationPromise对象。后续阻塞在PendingRegistrationPromise上的线程依赖监听器唤醒(注册完成后唤醒,详细唤醒流程参考第二章的启动流程图);

if (regFuture.isDone()) {ChannelPromise promise = channel.newPromise();// 2.doBind0doBind0(regFuture, channel, localAddress, promise);return promise;
} else {final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);regFuture.addListener(new ChannelFutureListener() {@Overridepublic void operationComplete(ChannelFuture future) throws Exception {Throwable cause = future.cause();if (cause != null) {promise.setFailure(cause);} else {promise.registered();// 2.doBind0doBind0(regFuture, channel, localAddress, promise);}}});return promise;
}

说明:分支[1] 在使用Netty过程中基本不会发生,是对一种小概率事件的效率优化。
后续将介绍注册流程需要经历:创建任务、任务加入队列、启动线程、从任务队列获取任务并执行、设置promise结果等操作;其中启动线程需要等待CPU的调度,除非设置给ServerBootstrap的NioEventLoopGroup对象的线程已被启动过(实际开发一般为新创建),否则启动netty的线程进入上述判断逻辑的时间点将绝大概率早于注册流程完成时间点。

理解上述逻辑后,initAndRegister和doBind0属于时间上顺序执行的逻辑,以下分小节分别介绍。

3.2 initAndRegister

initAndRegister忽略异常分支的主线逻辑如下:

final ChannelFuture initAndRegister() {// 步骤1:创建通道Channel channel = channelFactory.newChannel();// 步骤2:初始化通道init(channel);// 步骤3:注册通道ChannelFuture regFuture = config().group().register(channel);// 步骤4:返回异步对象return regFuture;
}

3.2.1 创建通道

通过serverBootstrap.channel(NioServerSocketChannel.class)将通道类型传递给serverBootstrap对象,这里通过反射调用NioServerSocketChannel的无参构造函数创建通道对象。

3.2.2 初始化通道

void init(Channel channel) {setChannelOptions(channel, newOptionsArray(), logger);setAttributes(channel, attrs0().entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY));final EventLoopGroup currentChildGroup = childGroup;final ChannelHandler currentChildHandler = childHandler;final Entry<ChannelOption<?>, Object>[] currentChildOptions;synchronized (childOptions) {currentChildOptions = childOptions.entrySet().toArray(EMPTY_OPTION_ARRAY);}final Entry<AttributeKey<?>, Object>[] currentChildAttrs = childAttrs.entrySet().toArray(EMPTY_ATTRIBUTE_ARRAY);ChannelPipeline p = channel.pipeline();p.addLast(new ChannelInitializer<Channel>() {@Overridepublic void initChannel(final Channel ch) {final ChannelPipeline pipeline = ch.pipeline();ChannelHandler handler = config.handler();if (handler != null) {pipeline.addLast(handler);}ch.eventLoop().execute(new Runnable() {@Overridepublic void run() {pipeline.addLast(new ServerBootstrapAcceptor(ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));}});}});
}

逻辑分为三个部分:
[1] 将预置在ServerBootStrap的options和attrs属性设置到channel上;
[2] 创建匿名ChannelInitializer对象,其initChannel方法将添加预置在ServerBootStrap的handler对象以及ServerBootstrapAcceptor对象到channel的pipeline上;
[3] 将ChannelInitializer对象添加到channel的pipeline中;
注意:上述提到的channel,都指代NioServerSocketChannel对象。
ServerBootstrapAcceptor是一个特殊的channelHandler,为客户端连接创建通道(NioSocketChannel)并向选择器注册,因此需要保存NioSocketChannel的线程池对象、子处理器Handler、子通道连接配置等信息;ServerBootstrapAcceptor作用将在Netty处理消息时详细介绍。

3.2.3 注册通道

使用选择器从NioEventLoopGroup中选择一个NioEventLoop,并调用NioEventLoop对象的register方法:

public ChannelFuture register(Channel channel) {return next().register(channel);
}

进入NioEventLoop的register方法:

public ChannelFuture register(Channel channel) {return register(new DefaultChannelPromise(channel, this));
}

获取channel的unsafe对象,并调用其register方法:

public ChannelFuture register(final ChannelPromise promise) {promise.channel().unsafe().register(this, promise);return promise;
}

unsafe对象是Netty对底层IO的封装,进入unsafe的register方法(删除异常分支逻辑):

public final void register(EventLoop eventLoop, final ChannelPromise promise) {AbstractChannel.this.eventLoop = eventLoop;if (eventLoop.inEventLoop()) {register0(promise);} else {eventLoop.execute(new Runnable() {@Overridepublic void run() {register0(promise);}});}
}

这里的核心逻辑是register0,unsafe将register0委托给NioEventLoop线程异步执行。
注意:这里的eventLoop.execute(Runnable)会将Runnable保存在任务队列中,并将第一次启动NioEventLoop线程,启动后会从任务队列中取出任务再执行。可以参考: Netty系列-1 NioEventLoopGroup和NioEventLoop介绍

register0(剔除异常分支逻辑)的主线逻辑如下:

private void register0(ChannelPromise promise) {boolean firstRegistration = neverRegistered;// 步骤1:注册channeldoRegister();// 步骤2:设置状态变量registered为已注册neverRegistered = false;registered = true;// 步骤3: 注册的后处理操作pipeline.invokeHandlerAddedIfNeeded();// 步骤4:设置Promise执行结果为成功状态safeSetSuccess(promise);// 步骤5: 向pipeline管道触发channelRegister事件pipeline.fireChannelRegistered();//...
}

步骤1: 注册channel

@Override
protected void doRegister() throws Exception {boolean selected = false;for (;;) {try {selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);return;} catch (CancelledKeyException e) {//...异常逻辑}}
}

javaChannel()是NIO的ServerSocketChannel通道对象, eventLoop().unwrappedSelector()是NIO的选择器对象,抽象出来为: SelectionKey selectionKey = serverSocketChannel.register(selector,0 , this);

注意: 这里的attachment为this, 即NioServerSocketChannel对象。

步骤2:设置状态变量registered为已注册

registered表示通道是否已向select注册成功,使用该状态变量可以防止反复注册。

步骤3: 注册的后处理操作
invokeHandlerAddedIfNeeded将会调用callHandlerAddedForAllHandlers方法

final void invokeHandlerAddedIfNeeded() {assert channel.eventLoop().inEventLoop();if (firstRegistration) {firstRegistration = false;callHandlerAddedForAllHandlers();}
}

callHandlerAddedForAllHandlers在介绍Pipeline时介绍过:向pipeline添加处理器时,如果通道未注册,则添加处理器操作会被封装成延时任务;callHandlerAddedForAllHandlers执行时会调用这些任务:实现向pipeline添加handler,如果Handler是ChannlInitializer类型,则会调用ChannlInitializer的init方法。

步骤4:设置Promise执行结果为成功状态

safeSetSuccess(promise)方法通过promise.trySuccess()设置promise对象为执行成功:

public boolean trySuccess() {return trySuccess(null);
}public boolean trySuccess(V result) {return setSuccess0(result);
}private boolean setSuccess0(V result) {return setValue0(result == null ? SUCCESS : result);
}private boolean setValue0(Object objResult) {if (RESULT_UPDATER.compareAndSet(this, null, objResult) ||RESULT_UPDATER.compareAndSet(this, UNCANCELLABLE, objResult)) {if (checkNotifyWaiters()) {notifyListeners();}return true;}return false;
}

其中checkNotifyWaiters通过notifyAll()唤醒所有阻塞在该Promise上的线程,notifyListeners依次调用监听器的operationComplete方法。

此时章节3.1doBind中向regFuture添加的ChannelFutureListener监听器对象的operationComplete方法将被调用。

步骤5: 向pipeline管道触发channelRegister事件

此时,channelRegister事件将沿着pipeline的ctx链进行传递。

3.3 doBind0

继续跟进doBind0代码:

private static void doBind0(final ChannelFuture regFuture, final Channel channel, final SocketAddress localAddress, final ChannelPromise promise) {// 将绑定任务提交给channel绑定的NioEventLoop执行channel.eventLoop().execute(new Runnable() {@Overridepublic void run() {if (regFuture.isSuccess()) {channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);} else {promise.setFailure(regFuture.cause());}}});
}

注意这里的入参: channel是NioServerSocketChannel对象,待绑定的通道;localAddress是本地的地址;

regFuture是一个异步执行结果, 表示注册操作是否完成;promise也是一个异步执行结果,用于根据regFuture的状态设置promise。这里的promise是返回给Netty启动线程的:

// 这里sync阻塞等待的就是这个promise对象
ChannelFuture channelFuture = serverBootstrap.bind(port).sync();

当对promise设置结果时,sync会从阻塞中被唤醒。
继续跟进channel.bind方法:

public ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {// 将bind操作委托给pipelinereturn pipeline.bind(localAddress, promise);
}public final ChannelFuture bind(SocketAddress localAddress, ChannelPromise promise) {return tail.bind(localAddress, promise);
}

bind属于出站操作,pipeline将从tail开始逆向搜索ctx,直到第一个可以处理bind方法的ctx或者进入HeadContext:

public void bind(ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise) {unsafe.bind(localAddress, promise);
}

HeadContext将任务委托给unsafe对象(省去异常分支逻辑和校验逻辑):

public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {//...doBind(localAddress);//...// 设置promise的成功状态(同时唤醒阻塞的线程)safeSetSuccess(promise);
}

核心逻辑为执行doBind(localAddress)以及设置promise对象的状态为执行成功。

protected void doBind(SocketAddress localAddress) throws Exception {if (PlatformDependent.javaVersion() >= 7) {javaChannel().bind(localAddress, config.getBacklog());} else {javaChannel().socket().bind(localAddress, config.getBacklog());}
}

javaChannel()得到的Nio的ServerSocketChannel,因为Netty以NIO为基础进行的封装。

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Kubernetes配置管理(kubernetes)

实验环境&#xff1a; 在所有节点上拉取镜像&#xff1b;然后把资源清单拉取到第一个master节点上&#xff1b; 同步会话&#xff0c;导入镜像&#xff1a; configmap/secret 配置文件的映射 变量&#xff1a; 基于valuefrom的方式 cm--》pod 特点&#xff1a;变量的名称可…

[JavaEE] IP协议

目录 一、 IP协议 1.1 基本概念 1.2 协议头格式 1.3 特殊IP 二、 地址管理 2.1 网段划分 2.2 CIDR(Classless Interdomain Routing) 2.3 私有IP地址和公网IP地址 2.4 NAT(Network Address Translation)-网络地址转换 2.5 路由选择 三、数据链路层 3.1 认识以太网 3…

什么是AQS

目录 AQS 介绍 原理 以可重入的互斥锁 ReentrantLock 为例 以倒计时器 CountDownLatch 以例 AQS 资源共享方式 实现自定义同步器 示例 性能优化 AQS 介绍 AQS &#xff08;AbstractQueuedSynchronizer &#xff09;&#xff0c;抽象队列同步器。AQS 是一个功能强大且…

cmd命令大全详解

CMD是Windows操作系统中的命令行解释器&#xff0c;它允许用户通过键入命令来执行各种操作。以下是一些常用的CMD命令及其简要说明&#xff1a; dir - 显示目录中的文件和子目录。 cmddir cd - 更改当前目录。 cmdcd [目录路径] mkdir - 创建新目录。 cmdmkdir [目录名] rmd…

Vue.js 与 Flask/Django 后端配合开发实战

Vue.js 与 Flask/Django 后端配合开发实战 在现代的 Web 开发中&#xff0c;前后端分离已成为一种主流架构&#xff0c;其中前端使用 Vue.js 等流行的框架&#xff0c;后端采用 Flask 或 Django 提供 API 接口。在这种开发模式下&#xff0c;前端负责页面的交互和动态效果&…

将Mixamo的模型和动画导入UE5

首先进入Mixamo的官网 , 点击 Character 选择一个模型 (当然你也可以自己上传模型/绑定动画) 然后点击下载 , 这个作为带骨骼的模型 选择FBX格式 , T Pose 直接下载 点击 Animations 选择动画 , 搜索 idle 默认站立动画 点击下载 , 格式选择 FBX , 不带模型只要骨骼 , 帧数选6…

前端面试经验总结2(经典问题篇)

谈谈你对前端的理解 前端主要负责产品页面部分的实现&#xff0c;是最贴近于用户的程序员。 基本工作要求&#xff1a; 1.参与项目&#xff0c;通过与团队成员&#xff0c;UI设计&#xff0c;产品经理的沟通&#xff0c;快速高质量的实现效果图&#xff0c;并能够精确到1px 2.做…

大模型培训讲师叶梓:Llama Factory 微调模型实战分享提纲

LLaMA-Factory ——一个高效、易用的大模型训练与微调平台。它支持多种预训练模型&#xff0c;并且提供了丰富的训练算法&#xff0c;包括增量预训练、多模态指令监督微调、奖励模型训练等。 LLaMA-Factory的优势在于其简单易用的界面和强大的功能。用户可以在不编写任何代码的…

TypeScript介绍和安装

TypeScript介绍 TypeScript是由微软开发的一种编程语言&#xff0c;它在JavaScript的基础上增加了静态类型检查。静态类型允许开发者在编写代码时指定变量和函数的类型&#xff0c;这样可以在编译时捕获潜在的错误&#xff0c;而不是等到运行时才发现问题。比如&#xff0c;你…

论文笔记:iCaRL: Incremental Classifier and Representation Learning

1. Contribution 提出了一种新的训练策略&#xff0c;iCaRL&#xff1a;允许以增量方式学习&#xff1a;只需要同时存在一小部分类别的训练数据&#xff0c;新类别可以逐步添加。同时学习分类器和数据表示&#xff1a;iCaRL能够同时学习强大的分类器和数据表示&#xff0c;这与…

[SAP ABAP] SELECTION-SCREEN

SELECTION-SCREEN用来调节系统生成的画面 REPORT z437_test_2024.TABLES: mara, zdbt_sch_437.SELECTION-SCREEN BEGIN OF BLOCK b1 WITH FRAME TITLE TEXT-001. " Title1 PARAMETERS:p_1 DEFAULT A,p_2 TYPE char10. SELECTION-SCREEN END OF BLOCK b1.SELECTION-SCREEN …

实现微信小程序中点击单词显示在input的交互功能指南

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

基于SSH的酒店管理系统的设计与实现 (含源码+sql+视频导入教程+文档+PPT)

&#x1f449;文末查看项目功能视频演示获取源码sql脚本视频导入教程视频 1 、功能描述 基于SSH的酒店管理系统拥有三种角色 管理员&#xff1a;用户管理、房间分类管理、房间信息管理、开房管理、退房管理、开房和预订记录查询等 前台&#xff1a;房间分类管理、房间信息管…

【Go】-Websocket的使用

目录 为什么需要websocket 使用场景 在线教育 视频弹幕 Web端即时通信方式 什么是web端即时通讯技术&#xff1f; 轮询 长轮询 长连接 SSE websocket 通信方式总结 Websocket介绍 协议升级 连接确认 数据帧 socket和websocket 常见状态码 gorilla/websocket实…

LaTex符号不好记忆?

总结在Matlab中常用的LaTeX符号如下&#xff1a; 1. **希腊字母**&#xff1a; - \alpha 表示 α - \beta 表示 β - \gamma 表示 γ - \delta 表示 δ - \epsilon 表示 ε - \zeta 表示 ζ - \eta 表示 η - \theta 表示 θ - \iota 表示 ι -…

1-仙灵之谜(区块链游戏详情介绍)

1-仙灵之谜&#xff08;区块链游戏详情介绍&#xff09; 前言&#xff08;该游戏仅供娱乐&#xff09;正文 前言&#xff08;该游戏仅供娱乐&#xff09; 依稀记得本科那会儿参加了一个区块链实验室&#xff0c;那时每周末大家都会爬山或者抽出一下午讨论区块链以及未来&#x…

全国省、市、县(区)土地利用类型及面积面板数据(2019-2022年)

土地利用类型是根据土地利用方式和地域差异对土地资源单元进行划分的基本土地地域单元。 2019年-2022年全国省、市、县&#xff08;区&#xff09;土地利用类型及面积面板数据_土地利用类型数据下载资源-CSDN文库https://download.csdn.net/download/2401_84585615/89466102 …

9.28每日作业

1> 创建一个新项目&#xff0c;将默认提供的程序都注释上意义 01Demo.pro QT core gui # QT表示要引入的类库 core&#xff1a;核心库例如IO操作在该库中 gui&#xff1a;图形化界面库 # 如果要使用其他类库中的相关函数&#xff0c;则需要加对于的类库后&#…

IO(Reader/Writer)

1.Reader a.简介 i.是Java的IO库提供的另一种输入流。和InputStream的区别是&#xff1a;InputStream是字节流&#xff0c;以byte为单位&#xff0c;Reader是字符流&#xff0c;以char为单位。 ii.java.io.Reader是所有字符输入流的超类。 b.FileReader i.FileReader默认的编…

QT基础 制作简单登录界面

作业&#xff1a; 1、创建一个新项目&#xff0c;将默认提供的程序都注释上意义 01zy.pro代码 QT core gui # QT表示要引入的类库 core&#xff1a;核心库例如IO操作在该库中 gui&#xff1a;图形化界面库 # 如果要使用其他类库中的相关函数&#xff0c;则需要加对…