32.Netty源码之服务端如何处理客户端新建连接


highlight: arduino-light

服务端如何处理客户端新建连接

Netty 服务端完全启动后,就可以对外工作了。接下来 Netty 服务端是如何处理客户端新建连接的呢? 主要分为四步:

md Boss NioEventLoop 线程轮询客户端新连接 OP_ACCEPT 事件; ​ 构造 初始化Netty 客户端 NioSocketChannel; ​ 注册 Netty 客户端 NioSocketChannel 到 Worker 工作线程中; ​ 从 Worker group 选择一个 eventLoop 工作线程;注册到选择的eventLoop的Selector ​ 注册 OP_READ 事件到 NioSocketChannel 的事件集合。 ​

下面我们对每个步骤逐一进行简单的介绍。

接收新连接

bossGroup的EventLoop是一个线程是一个线程是一个线程。所以等服务器端启动起来以后就会执行线程的run方法逻辑。

java protected void run() {    for (;;) {        try {            try {            switch (selectStrategy.calculateStrategy                   (selectNowSupplier, hasTasks())) {                case SelectStrategy.CONTINUE:                    continue;                case SelectStrategy.BUSY_WAIT:                case SelectStrategy.SELECT:                    select(wakenUp.getAndSet(false)); // 轮询 I/O 事件                    if (wakenUp.get()) {                        selector.wakeup();                   }                default:               }           } catch (IOException e) {                rebuildSelector0();                handleLoopException(e);                continue;           }            cancelledKeys = 0;            needsToSelectAgain = false;            final int ioRatio = this.ioRatio;            if (ioRatio == 100) {                try {                    // 处理 I/O 事件                    processSelectedKeys();               } finally {                    runAllTasks(); // 处理所有任务               }           } else {                final long ioStartTime = System.nanoTime();                try {                    processSelectedKeys(); // 处理 I/O 事件               } finally {                    final long ioTime = System.nanoTime() - ioStartTime;                    // 处理完 I/O 事件,再处理异步任务队列                    runAllTasks(ioTime * (100 - ioRatio) / ioRatio);               }           }       } catch (Throwable t) {            handleLoopException(t);       }        try {            if (isShuttingDown()) {                closeAll();                if (confirmShutdown()) {                    return;               }           }       } catch (Throwable t) {            handleLoopException(t);       }   } } ​

NioEventLoop#processSelectedKeys

java // processSelectedKeys private void processSelectedKeys() { if (selectedKeys != null) { //不用JDK的selector.selectedKeys(), 性能更好(1%-2%),垃圾回收更少 processSelectedKeysOptimized(); } else { processSelectedKeysPlain(selector.selectedKeys()); } }

服务器端监听 OP_ACCEPT 事件读取消息

NioEventLoop#processSelectedKeysOptimized

Netty 中 Boss NioEventLoop 专门负责接收新的连接,关于 NioEventLoop 的核心源码我们下节课会着重介绍,在这里我们只先了解基本的处理流程。当客户端有新连接接入服务端时,Boss NioEventLoop 会监听到 OP_ACCEPT 事件,源码如下所示:

```java private void processSelectedKeysOptimized() { for (int i = 0; i < selectedKeys.size; ++i) { final SelectionKey k = selectedKeys.keys[i]; // null out entry in the array to allow to have it GC'ed once the Channel close // See https://github.com/netty/netty/issues/2363 selectedKeys.keys[i] = null;

//呼应于channel的register中的this: //selectionKey = javaChannel().register(eventLoop()//                            .unwrappedSelector(), 0, this);final Object a = k.attachment();//因为客户端和服务器端的都继承自AbstractNioChannelif (a instanceof AbstractNioChannel) {//会进入判断processSelectedKey(k, (AbstractNioChannel) a);} else {@SuppressWarnings("unchecked")NioTask<SelectableChannel> task = (NioTask<SelectableChannel>) a;processSelectedKey(k, task);}if (needsToSelectAgain) {// null out entries in the array to allow to have it GC'ed once the Channel close// See https://github.com/netty/netty/issues/2363selectedKeys.reset(i + 1);selectAgain();i = -1;}}
}

```

NioEventLoop#processSelectedKey

```java private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); if (!k.isValid()) { final EventLoop eventLoop; try { eventLoop = ch.eventLoop(); } catch (Throwable ignored) { return; }

if (eventLoop != this || eventLoop == null) {return;}// close the channel if the key is not valid anymoreunsafe.close(unsafe.voidPromise());return;}try {int readyOps = k.readyOps();if ((readyOps & SelectionKey.OP_CONNECT) != 0) {int ops = k.interestOps();ops &= ~SelectionKey.OP_CONNECT;k.interestOps(ops);unsafe.finishConnect();}if ((readyOps & SelectionKey.OP_WRITE) != 0) {ch.unsafe().forceFlush();}//处理读请求(断开连接)或接入连接if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT))!= 0 || readyOps == 0) {//开始处理请求 服务器端处理的是OP_ACCEPT 接收新连接 unsafe.read();}} catch (CancelledKeyException ignored) {unsafe.close(unsafe.voidPromise());}
}

```

NioMessageUnsafe#read

NioServerSocketChannel 所持有的 unsafe 是 NioMessageUnsafe 类型。

我们看下 NioMessageUnsafe.read() 方法中做了什么事。

```java public void read() { assert eventLoop().inEventLoop(); final ChannelConfig config = config(); final ChannelPipeline pipeline = pipeline(); final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle(); allocHandle.reset(config); boolean closed = false; Throwable exception = null; try { try {

do {//readBuf一开始是一个空List//while 循环不断读取 Buffer 中的数据//创建底层SocketChannel并封装为NioSocketChannel放到readBuf返回1int localRead = doReadMessages(readBuf); //执行完上面的方法 readBuf放的是新创建的NioSocketChannelif (localRead == 0) {break;}if (localRead < 0) {closed = true;break;}allocHandle.incMessagesRead(localRead);//是否需要继续读 因为是建立连接 所以总共读取的字节数是0 不会继续进入循环//这里一次最多处理16个连接} while (allocHandle.continueReading());} catch (Throwable t) {exception = t;}int size = readBuf.size();//readBuf放的是新创建的NioSocketChannelfor (int i = 0; i < size; i ++) {readPending = false;// 对于服务器端NioServerSocketChannel来说 // handler有// 1.head // 2.ClientLoggingHandler // 3.ServerBootstrapAcceptor// 4.tail// 接下来就是调用服务器端的每个handler的channelRead方法 传播读取事件// 比如ClientLoggingHandler的channelRead用于打印接收到的消息到日志// 比如 serverBootStrapAcceptor的channelRead //用于向客户端的SocketChannel的pipeline添加handler//就是把服务器端方法中的childHandler都添加到客户端的NioSocketChannel的pipeline//具体看serverBootStrapAcceptor的channelRead 方法pipeline.fireChannelRead(readBuf.get(i)); }readBuf.clear();allocHandle.readComplete();// 传播读取完毕事件pipeline.fireChannelReadComplete(); // 省略其他代码
} finally {if (!readPending && !config.isAutoRead()) {removeReadOp();}
}

} ```

可以看出 read() 方法的核心逻辑就是通过 while 循环不断读取数据,然后放入 List 中,这里的数据其实就是新连接。每次最多处理16个。

需要重点跟进一下 NioServerSocketChannel 的 doReadMessages() 方法。

接前面NioMessageUnsafe#read

继续接着NioMessageUnsafe#read看

1.NioServerSocketChannel #doReadMessages

接收&&创建&初始化客户端连接

```java protected int doReadMessages(List buf) throws Exception { //Netty 先通过 JDK 底层的 accept() 获取 JDK 原生的 SocketChannel //想想这里 在NIO编程的时候 是做了判断 如果是OPACCEPT事件 //执行 SocketChannel sChannel = ssChannel.accept(); //这里的accept方法返回的就是原生的SocketChannel SocketChannel ch = SocketUtils.accept(javaChannel()); try { if (ch != null) { //根据原生的 SocketChannel构造 Netty 客户端 NioSocketChannel //NioSocketChannel 的创建同样会完成几件事: //创建核心成员变量 id、unsafe、pipeline; //注册 SelectionKey.OPREAD 事件; //设置 Channel 的为非阻塞模式; //新建客户端 Channel 的配置。 //this是NioServerSocketChannel //最后把NioSocketChannel添加到buf返回1 //super(parent, ch, SelectionKey.OP_READ); //这里不是注册读事件只是赋值 buf.add(new NioSocketChannel(this, ch)); return 1; } } catch (Throwable t) { logger.warn("Failed to create a new channel from an accepted socket.", t); try { ch.close(); } catch (Throwable t2) { logger.warn("Failed to close a socket.", t2); } } return 0; }

protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) { super(parent); this.ch = ch; //设置读事件到NioSocketChannel this.readInterestOp = readInterestOp; try { //非阻塞模式 ch.configureBlocking(false); } catch (IOException e) { try { ch.close(); } catch (IOException e2) { logger.warn( "Failed to close a partially initialized socket.", e2); }

throw new ChannelException("Failed to enter non-blocking mode.", e);}
}

public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException { try { return AccessController.doPrivileged(new PrivilegedExceptionAction () { @Override public SocketChannel run() throws IOException { // 非阻塞模式下,没有连接请求时,返回null return serverSocketChannel.accept(); } }); } catch (PrivilegedActionException e) { throw (IOException) e.getCause(); } } ```

这时就开始执行第二个步骤:构造 Netty 客户端 NioSocketChannel。Netty 先通过 JDK 底层的 accept() 获取 JDK 原生的 SocketChannel,然后将它封装成 Netty 自己的 NioSocketChannel。

新建 Netty 的客户端 Channel 的实现原理与上文中我们讲到的创建服务端 Channel 的过程是类似的,只是服务端 Channel 的类型是 NioServerSocketChannel,而客户端 Channel 的类型是 NioSocketChannel。

NioSocketChannel 的创建同样会完成几件事:创建核心成员变量 id、unsafe、pipeline;

注册 SelectionKey.OP_READ 事件;设置 Channel 的为非阻塞模式;新建客户端 Channel 的配置。

成功构造客户端 NioSocketChannel 后,接下来会通过 pipeline.fireChannelRead() 触发 channelRead 事件传播。对于服务端来说,此时 Pipeline 的内部结构如下图所示。

图片6.png

2.pipeline.fireChannelRead

上文中我们提到了一种特殊的处理器 ServerBootstrapAcceptor,在下面它就发挥了重要的作用。channelRead 事件会传播到 ServerBootstrapAcceptor.channelRead() 方法,channelRead() 会将客户端 Channel 分配到工作线程组中去执行。具体实现如下:

触发服务器端hanlder#channelRead

ServerBootstrapAcceptor#channelRead

java //ServerBootstrapAcceptor负责接收客户端连接 创建连接后,对连接的初始化工作。 // ServerBootstrapAcceptor.channelRead() 方法 public void channelRead(ChannelHandlerContext ctx, Object msg) { final Channel child = (Channel) msg; //childHandler是我们自定义的EchoServer的代理类 child.pipeline().addLast(childHandler); setChannelOptions(child, childOptions, logger); setAttributes(child, childAttrs); try { // 注册客户端 Channel到工作线程组 //1.MultithreadEventLoopGroup#register childGroup.register(child).addListener(new ChannelFutureListener() { @Override public void operationComplete(ChannelFuture future) throws Exception { if (!future.isSuccess()) { forceClose(child, future.cause()); } } }); } catch (Throwable t) { forceClose(child, t); } }

ServerBootstrapAcceptor 开始就把 msg 强制转换为 Channel。难道不会有其他类型的数据吗?

因为 ServerBootstrapAcceptor 是服务端 Channel 中一个特殊的处理器,而服务端 Channel 的 channelRead 事件只会在新连接接入时触发,所以这里拿到的数据都是客户端新连接。

register():注册客户端 Channel

java //MultithreadEventLoopGroup#register //从workGroup中选择一个EventLoop注册到channel @Override public ChannelFuture register(Channel channel) { return next().register(channel); }

```java //io.netty.channel.nio.AbstractChannel#register0 private void register0(ChannelPromise promise) { try { if (!promise.setUncancellable() || !ensureOpen(promise)) { return; } boolean firstRegistration = neverRegistered; //绑定选择器 doRegister(); neverRegistered = false; registered = true; //给客户端添加处理器 pipeline.invokeHandlerAddedIfNeeded(); safeSetSuccess(promise); pipeline.fireChannelRegistered();

//NioServerSocketChannel的注册不会走进下面if(isActive())//NioSocketChannel可以走进去if(isActive())。因为accept后就active了。if (isActive()) {if (firstRegistration) {//第一次注册需要触发pipeline上的hanlder的read事件//实际上就是注册OP_ACCEPT/OP_READ事件:创建连接或者读事件//首先会进入DefaultChannelPipeLine的read方法pipeline.fireChannelActive();} else if (config().isAutoRead()) {//第二次注册的时候beginRead();}}} catch (Throwable t) {// Close the channel directly to avoid FD leak.closeForcibly();closeFuture.setClosed();safeSetFailure(promise, t);}}

```

客户端SocketChannel绑定selector
AbstractNioChannel#doRegister

java //io.netty.channel.nio.AbstractNioChannel#doRegister @Override protected void doRegister() throws Exception { boolean selected = false; for (;;) { try { logger.info("initial register: " + 0); //这里的事件类型仍然是0 //attachement是NioSocketChannel selectionKey = javaChannel().register (eventLoop().unwrappedSelector(), 0, this); return; } catch (CancelledKeyException e) { if (!selected) { // Force the Selector to select now as the "canceled" //SelectionKey may still be // cached and not removed because no //Select.select(..) operation was called yet. eventLoop().selectNow(); selected = true; } else { // We forced a select operation on the selector before but the SelectionKey is still cached // for whatever reason. JDK bug ? throw e; } } } }

DefaultChannelPipeline.HeadContext#read

java //io.netty.channel.DefaultChannelPipeline.HeadContext#read @Override public void read(ChannelHandlerContext ctx) { //实际上就是注册OP_ACCEPT/OP_READ事件:创建连接或者读事件 unsafe.beginRead(); }

```java @Override public final void beginRead() { assertEventLoop();

if (!isActive()) {return;}try {doBeginRead();} catch (final Exception e) {invokeLater(new Runnable() {@Overridepublic void run() {pipeline.fireExceptionCaught(e);}});close(voidPromise());}}

```

```java @Override protected void doBeginRead() throws Exception { // Channel.read() or ChannelHandlerContext.read() was called final SelectionKey selectionKey = this.selectionKey; if (!selectionKey.isValid()) { return; }

readPending = true;final int interestOps = selectionKey.interestOps();//super(parent, ch, SelectionKey.OP_READ);//假设之前没有监听readInterestOp,则监听readInterestOpif ((interestOps & readInterestOp) == 0) {//NioServerSocketChannel: readInterestOp = OP_ACCEPT = 1 << 4 = 16logger.info("interest ops: " + readInterestOp);selectionKey.interestOps(interestOps | readInterestOp);}
}

```

ServerBootstrapAcceptor 通过 childGroup.register() 方法会完成第三和第四两个步骤.

1.将 NioSocketChannel 注册到 Worker 工作线程中

2.并注册 OP_READ 事件到 NioSocketChannel 的事件集合。

在注册过程中比较有意思的一点是,它会调用 pipeline.fireChannelRegistered() 方法传播 channelRegistered 事件,然后再调用 pipeline.fireChannelActive() 方法传播 channelActive 事件。

兜了一圈,这又会回到之前我们介绍的 readIfIsAutoRead() 方法,此时它会将 SelectionKey.OP_READ 事件注册到 Channel 的事件集合。

添加自定义handler到客户端SocketChannel
pipeline.invokeHandlerAddedIfNeeded

总结

java •接受连接本质: ​ selector.select()/selectNow()/select(timeoutMillis) 发现 OP_ACCEPT 事件,处理: ​ •SocketChannel socketChannel = serverSocketChannel.accept() ​ •selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this); ​ •selectionKey.interestOps(OP_READ); ​

关于服务端如何处理客户端新建连接的具体源码,我在此就不继续展开了。这里留一个小任务,建议你亲自动手分析下 childGroup.register() 的相关源码,从而加深对服务端启动以及新连接处理流程的理解。有了服务端启动源码分析的基础,再去理解客户端新建连接的过程会相对容易很多。

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

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

相关文章

Azure创建自定义VM镜像

创建一个虚拟机&#xff0c;参考 https://blog.csdn.net/m0_48468018/article/details/132267096&#xff0c;入站端口开启80&#xff0c;22 进行远程远程连接 使用CLI命令部署NGINX,输入如下命令 sudo su apt-get update -y apt-get install nginx git -y最后的效果 4. 关闭…

RocketMQ部署 Linux方式和Docker方式

一、Linux部署 准备一台Linux机器&#xff0c;部署单master rocketmq节点 系统ip角色模式CENTOS10.4.7.126Nameserver,brokerMaster 1. 配置JDK rocketmq运行需要依赖jdk&#xff0c;安装步骤略。 2. 下载和配置 从官网下载安装包 https://rocketmq.apache.org/zh/downlo…

Docker碎碎念

docker和虚拟机的区别 虚拟机&#xff08;VM&#xff09;是通过在物理硬件上运行一个完整的操作系统来实现的。 每个虚拟机都有自己的内核、设备驱动程序和用户空间&#xff0c;它们是相互独立且完全隔离的。 虚拟机可以在不同的物理服务器之间迁移&#xff0c;因为它们是以整…

【C++ 记忆站】引用

文章目录 一、引用概念二、引用特性1、引用在定义时必须初始化2、一个变量可以有多个引用3、引用一旦引用一个实体&#xff0c;再不能引用其他实体 三、常引用四、使用场景1、做参数1、输出型参数2、大对象传参 2、做返回值1、传值返回2、传引用返回 五、传值、传引用效率比较六…

服务器数据恢复-EqualLogic存储RAID5数据恢复案例

服务器数据恢复环境&#xff1a; 一台DELL EqualLogic存储中有一组由16块SAS硬盘组建的RAID5阵列。存储存放虚拟机文件&#xff0c;采用VMFS文件系统&#xff0c;划分了4个lun。 服务器故障&检测&分析&#xff1a; 存储设备上有两个硬盘指示灯显示黄色&#xff0c;存储…

虫情测报灯

在农业生产过程中&#xff0c;农作物的虫害问题永远都是放在首位的。随着现代生活科技的发展和社会进步&#xff0c;人们对物质也有了新的要求。伴随农作物品种的增加&#xff0c;农药和化肥的使用也在导致农业虫害问题日益加剧&#xff0c;在这种不良的耕作状态下&#xff0c;…

Vue初识别--环境搭建--前置配置过程

问题一&#xff1a; 在浏览器上的扩展程序上添加了vue-devtools后不生效&#xff1a; 解决方式&#xff1a;打开刚加入的扩展工具Vue.js devtools的允许访问文件地址设置 问题二&#xff1a;Vue新建一个项目 创建一个空文件夹hrsone&#xff0c;然后在VSCode中打开这个空文件夹…

HCIP第五节------------------------------------------ospf

一、OSPF基础 1、动态路由分类 2、距离矢量协议 运行距离矢量路由协议的路由器周期性地泛洪自己的路由表。通过路由的交互&#xff0c;每台路由器都从相邻的路由器学习到路由&#xff0c;并且加载进自己的路由表中&#xff0c;然后再通告给其他相邻路由器。 对于网络中的所有…

Docker:Windows container和Linux container

点击"Switch to Windows containers"菜单时&#xff1a; 提示 然后 实际上是运行&#xff1a;com.docker.admin.exe start-service

七夕特辑——3D爱心(可监听鼠标移动)

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

sql面试题

user登陆表数据如下&#xff0c;求出连续登录3天及以上的用户 方法1&#xff1a;排序&#xff0c;dt列求出本行和前面第2行的日期差&#xff0c;等于2则三天连续 SELECT DISTINCT uid FROM (SELECT uid,dt,lag(dt,2) over(PARTITION BY uid ORDER BY dt) AS lag_dt FROM USER…

Excel设置某列或者某行不某行不可以编辑,只读属性

设置单元格只读的三种方式: 1、通过单元格只读按钮&#xff0c;设置为只为 设置行或者列的只读属性&#xff0c;可以设置整行或者整列只读 2、设置单元格编辑控件为标签控件(标签控件不可编辑) 3、通过锁定行&#xff0c;锁定行的修改。锁定的行与只读行的区别在于锁定的行不…

聚观早报|京东称在技术投入没有止境;木蚁机器人完成B2轮融资

【聚观365】8月18日消息 京东零售CEO表示在技术上投入没有止境 木蚁机器人完成B2轮超亿元融资 耐能推出AI芯片KL730 三星电子泰勒晶圆厂首家客户是AI半导体厂商 韩国新能源汽车7月出口额同比大增36% 京东零售CEO表示在技术上投入没有止境 近日&#xff0c;京东零售CEO辛利…

计算机竞赛 图像识别-人脸识别与疲劳检测 - python opencv

文章目录 0 前言1 课题背景2 Dlib人脸识别2.1 简介2.2 Dlib优点2.3 相关代码2.4 人脸数据库2.5 人脸录入加识别效果 3 疲劳检测算法3.1 眼睛检测算法3.3 点头检测算法 4 PyQt54.1 简介4.2相关界面代码 5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是…

205、仿真-51单片机直流数字电流表多档位切换Proteus仿真设计(程序+Proteus仿真+原理图+流程图+元器件清单+配套资料等)

毕设帮助、开题指导、技术解答(有偿)见文未 目录 一、硬件设计 二、设计功能 三、Proteus仿真图 四、原理图 五、程序源码 资料包括&#xff1a; 方案选择 单片机的选择 方案一&#xff1a;STM32系列单片机控制&#xff0c;该型号单片机为LQFP44封装&#xff0c;内部资源…

LabVIEW开发最小化5G系统测试平台

LabVIEW开发最小化5G系统测试平台 由于具有大量存储能力和数据的应用程序的智能手机的激增&#xff0c;当前一代产品被迫提高其吞吐效率。正交频分复用由于其卓越的品质&#xff0c;如单抽头均衡和具有成本效益的实施&#xff0c;现在被广泛用作物理层技术。这些好处是以严格的…

Redis五大基本数据类型及其使用场景

文章目录 **一 什么是NoSQL&#xff1f;****二 redis是什么&#xff1f;****三 redis五大基本类型**1 String&#xff08;字符串&#xff09;**应用场景** 2 List&#xff08;列表&#xff09;**应用场景** 3 Set&#xff08;集合&#xff09;4 sorted set&#xff08;有序集合…

导出Excel一些格式、样式的 代码

1.合并单元格 // 合并单元格&#xff08;开始行, 结束行, 开始列, 结束列&#xff09;CellRangeAddress regionRow0 new CellRangeAddress(0, 0, 0, 10);sheet.addMergedRegion(regionRow0);2.单元格根据汉字自动匹配颜色 HSSFConditionalFormattingRule orange scf.createCo…

Mathematica(42)-计算N个数值的和

比如&#xff0c;我们要用Mathematica求得到下面的式子&#xff1a; 这就需要用到一个函数&#xff1a;Sum 具体地&#xff0c;Sum函数的使用形式如下&#xff1a; 因此&#xff0c;按照公式就可以得到下面的结果&#xff1a; 如果&#xff0c;我们想要将求和号也加进去&#…

python3 0基础学习----数据结构(基础+练习)

python 0基础学习笔记之数据结构 &#x1f4da; 几种常见数据结构列表 &#xff08;List&#xff09;1. 定义2. 实例&#xff1a;3. 列表中常用方法.append(要添加内容) 向列表末尾添加数据.extend(列表) 将可迭代对象逐个添加到列表中.insert(索引&#xff0c;插入内容) 向指定…