【图解IO与Netty系列】Netty源码解析——服务端启动
Netty源码解析——服务端启动
- Netty案例复习
- Netty原理复习
- Netty服务端启动源码解析
- bind(int)
- initAndRegister()
- channelFactory.newChannel()
- init(channel)
- config().group().register(channel)
- startThread()
- run()
- register0(ChannelPromise promise)
- doBind0(...)
今天我们一起来学习Netty源码,对Netty有一个深入的认知,既能掌握其使用和原理,又能对它底层的设计有一个大概的认知
Netty案例复习
在阅读源码前,我们再看一下Netty服务端的启动代码
public static void main(String[] args) throws Exception {EventLoopGroup bossGroup = new NioEventLoopGroup(1);EventLoopGroup workerGroup = new NioEventLoopGroup();try {ServerBootstrap b = new ServerBootstrap();b.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG, 100).handler(new LoggingHandler(LogLevel.INFO)).childHandler(new ChannelInitializer<SocketChannel>() {@Overridepublic void initChannel(SocketChannel ch) throws Exception {ChannelPipeline p = ch.pipeline();p.addLast(new EchoServerHandler());}});ChannelFuture f = b.bind(PORT).sync();f.channel().closeFuture().sync();} finally {bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}
- 我们创建了两个EventLoopGroup已经,然后设置到ServerBootstrap上,bossGroup中的EventLoop处理accept事件,workerGroup中的EventLoop处理read事件时。
- 再通过ServerBootstrap设置好一些其他的参数,其中包括Netty服务端接收连接请求用的NioServerSocketChannel,NioServerSocketChannel会注册到bossGroup上,监听accept事件
- 然后设置好ChannelInitializer,服务端接收到客户端连接之后,会创建一个NioSocketChannel,ChannelInitializer就是用于初始化连接建立成功后的NioSocketChannel
- 最后就是通过ServerBootstrap的bind(int inetPort)绑定一个端口
Netty原理复习
我们再来复习一下Netty的原理,带着对Netty原理的认知去看源码,效率才会高,不至于走偏。
- 当调用了ServerBootstrap的bind(int inetPort)绑定端口后,我们注册的NioServerSocketChannel就会被注册到bossGroup上的唯一一个NioEventLoop上,然后NioEventLoop会把ServerSocketChannel注册到Selector上监听accept事件,并启动事件循环
- 当有客户端连接后,boosGroup中的EventLoop会获得一个SocketChannel然后将其包装成NioSocketChannel,然后NioServerSocketChannel对应的ChannelHandler会把它注册到workerGroup中
- workerGroup会将NioSocketChannel注册到其中一个NioEventLoop上,并使用我们配置的ChannelInitializer初始化NioSocketChannel
- workerGroup中的NioEventLoop的事件循环中监听到Channel有read事件时发生,就会调用Channel对应的ChannelPipeline进行处理
- ChannelPipeline通过责任链模式,逐一调用pipeline上的ChannelHandler处理事件
Netty服务端启动源码解析
接下来我们就开始读源码了,我们从ServerBootstrap的bind方法走起。
bind(int)
/*** Create a new {@link Channel} and bind it.*/public ChannelFuture bind(int inetPort) {return bind(new InetSocketAddress(inetPort));}
bind方法的作用就是创建一个Channel并绑定端口,创建的Channel类型就是我们指定的NioServerSocketChannel。
bind方法会调用doBind方法。
private ChannelFuture doBind(final SocketAddress localAddress) {final ChannelFuture regFuture = initAndRegister();...doBind0(regFuture, channel, localAddress, promise);...}
省略了非关键代码后,剩下的就是关键的两行:
- initAndRegister()方法做的事情就是创建并初始化Channel然后注册到NioEventLoop上。
- doBind0(…)方法给NioServerSocockChannel中的ServerSocketChannel绑定端口。
initAndRegister()
我们进入initAndRegister()方法,同样是只看核心代码,其他的代码不关心。
final ChannelFuture initAndRegister() {...channel = channelFactory.newChannel();init(channel);...ChannelFuture regFuture = config().group().register(channel);...}
- channelFactory.newChannel():反射调用NioServerSocketChannel的构造方法进行实例化。
- init(channel):为NioServerSocketChannel的ChannelPipeline添加ChannelInitializer。
- config().group().register(channel):把NioServerSocketChannel中的ServerSocketChannel注册到workerGroup的NioEventLoop的Selector上。
channelFactory.newChannel()
channelFactory.newChannel()会进入到ReflectiveChannelFactory的newChannel()方法,里面就是调用Constructor构造器的newInstance()方法反射实例化一个Channel,这个构造器就是NioServerSocketChannel的构造器。是在我们调用ServerBootstrap的channel(NioServerSocketChannel.class)方法时设置进去的。
我们进入NioServerSocketChannel的构造方法看看。
public NioServerSocketChannel() {this(newSocket(DEFAULT_SELECTOR_PROVIDER));}
newSocket(…)方法就是创建了一个NIO原生的ServerSocketChannel,然后进入重载的构造方法。
public NioServerSocketChannel(ServerSocketChannel channel) {super(null, channel, SelectionKey.OP_ACCEPT);config = new NioServerSocketChannelConfig(this, javaChannel().socket());}
继续调用父类的构造方法,可以看到指定了事件类型是accept事件。然后进入到父类AbstractNioChannel的构造方法。
protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {super(parent);this.ch = ch;this.readInterestOp = readInterestOp;...ch.configureBlocking(false);...}
AbstractNioChannel的构造方法除了继续调用父类的构造方法以外,还保存了ServerSocketChannel和关注的事件类型OP_ACCEPT,然后设置ServerSocketChannel为非阻塞。
继续进入父类的构造方法。
protected AbstractChannel(Channel parent) {...unsafe = newUnsafe();pipeline = newChannelPipeline();}
创建了一个unsafe对象,和一个ChannelPipeline。unsafe其实是Netty内部自己使用的一个工具类,先不用管,后面会看到怎么用,这里看看如何创建ChannelPipeline。
protected DefaultChannelPipeline(Channel channel) {...tail = new TailContext(this);head = new HeadContext(this);head.next = tail;tail.prev = head;}
我们知道ChannelPipeline中的ChannelHandler都是包在ChannelHandlerContext里面的,而这里先创建了一个头部Context和尾部Context。
也就是这样:
channelFactory.newChannel()的大体逻辑如下:
init(channel)
我们回到initAndRegister()方法,再来看init方法。
@Overridevoid init(Channel channel) {...ChannelPipeline p = channel.pipeline();...p.addLast(new ChannelInitializer<Channel>() {@Overridepublic void initChannel(final Channel ch) {...ch.eventLoop().execute(new Runnable() {@Overridepublic void run() {pipeline.addLast(new ServerBootstrapAcceptor(ch, currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));}});}});}
init方法就是拿到NioServerSocketChannel的ChannelPipeline,往里面添加了一个ChannelInitializer,ChannelInitializer会在ServerSocketChannel被注册到Selector后,异步的给ChannelPipeline添加一个ServerBootstrapAcceptor,这个ServerBootstrapAcceptor是一个专门处理连接事件的Handler。当然ChannelInitializer还会从ChannelPipeline中把自己删除。
config().group().register(channel)
我们回到initAndRegister()方法,再往后看一下config().group().register(channel)。
@Overridepublic ChannelFuture register(Channel channel) {return next().register(channel);}
进入NioEventLoopGroup的父类MultithreadEventLoopGroup的register方法中。next()方法返回一个NioEventLoop,然后调用NioEventLoop的register(channel)方法。进入到NioEventLoop的父类SingleThreadEventLoop的register方法中。
@Overridepublic ChannelFuture register(final ChannelPromise promise) {...promise.channel().unsafe().register(this, promise);...}
promise.channel().unsafe()获取到NioServerSocketChannel中的Unsafe对象,然后调用Unsafe对象的register(…)
@Overridepublic final void register(EventLoop eventLoop, final ChannelPromise promise) {...if (eventLoop.inEventLoop()) {register0(promise);} else {...eventLoop.execute(new Runnable() {@Overridepublic void run() {register0(promise);}});... }}
Unsafe的register方法中,判断当前线程是否是eventLoop的线程,如果是,直接调用register0方法注册Channel,否则调用eventLoop的execute方法开启一个任务异步注册Channel。
private void execute(Runnable task, boolean immediate) {boolean inEventLoop = inEventLoop();addTask(task);if (!inEventLoop) {startThread();...}...}
execute方法首先调用addTask(task)方法,把这个任务放入taskQueue中。然后判断当选线程不是当前EventLoop的线程,因此调用startThread()开启EventLoop的线程,启动事件循环。
addTask方法就是把这个Runnable放入到NioEventLoop内部的队列当中taskQueue中,在NioEventLoop的事件循环的每一轮的最后,会处理taskQueue中的任务,我们就不细看了。我们接下来看一下startThread方法。
startThread()
private void startThread() {if (state == ST_NOT_STARTED) {if (STATE_UPDATER.compareAndSet(this, ST_NOT_STARTED, ST_STARTED)) {...doStartThread();...}}}
startThread方法先修改NioEventLoop的状态state为开启,防止再次调用startThread方法的时候重复开启线程。然后调用doStartThread()方法。
private void doStartThread() {...executor.execute(new Runnable() {@Overridepublic void run() {...SingleThreadEventExecutor.this.run();...}});}
doStartThread()方法里面调用了executor的execute方法,异步执行SingleThreadEventExecutor.this.run()方法,这个run方法就会进入到NioEventLoop的run方法中,里面就是事件循环。
这个executor其实是一个单线程的线程池,可以看做就是NioEventLoop的唯一一个线程,startThread方法并没有再创建一个线程,而是往NioEventLoop预先创建好的线程提交一个任务,这个任务会启动NioEventLoop的事件循环。
run()
接下来进入NioEventLoop的run方法看看,所谓的事件循环是什么。
@Overrideprotected void run() {...for (;;) {...strategy = select(curDeadlineNanos);if (strategy > 0) {processSelectedKeys();}...runAllTasks();...}}
可以看到,就是先调用select方法,这个select方法自然是调用NioEventLoop的Selector的select方法,当然现在还没有任何Channel被注册,因此这里是不会select到东西的。然后processSelectedKeys()是处理所有就绪事件的方法,因为这里还没有Channel被注册,自然也没有就绪事件发生。因此最后只有runAllTasks()被执行,也就是获取taskQueue中刚刚放进去的任务,这个任务的执行就会触发ServerSocketChannel的注册。
runAlllTasks方法执行taskQueue中目前唯一一个的任务,也就是刚刚放进去的用于注册ServerSocketChannel的任务,这个任务会调用Unsafe的register0(ChannelPromise promise)方法。
register0(ChannelPromise promise)
private void register0(ChannelPromise promise) {...doRegister();...}
register0调用doRegister()方法,进入AbstractNioChannel的doRegister()方法。
@Overrideprotected void doRegister() throws Exception {...selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);...}
doRegister方法中把Channel注册到Selector的代码就是上面这一行,javaChannel()返回NIO原生的Channel类型,然后调用register方法,eventLoop().unwrappedSelector()返回的就是Selector,这个Selector作为register方法的参数,这样NIO的Channel就被注册到Selector中了,这里的Channel当然是ServerSocketChannel。
这里注册完ServerSocketChannel之后,就会调用ChannelInitializer初始化ChannelPipeline。
doBind0(…)
我们回调doBind方法,initAndRegister()方法就已经看完了,再看下面的doBind0方法。
private static void doBind0(...,final Channel channel,...) {channel.eventLoop().execute(new Runnable() {@Overridepublic void run() {...channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE);...}});}
dobind0()方法也是通过NioEventLoop执行异步任务的方式去绑定端口,channel.bind(localAddress, promise)方法最终会进入NioServerSocketChannel的doBind(SocketAddress localAddress)方法。
protected void doBind(SocketAddress localAddress) throws Exception {if (PlatformDependent.javaVersion() >= 7) {javaChannel().bind(localAddress, config.getBacklog());} else {javaChannel().socket().bind(localAddress, config.getBacklog());}}
然后就是调用ServerSocketChannel的bind(SocketAddress local, int backlog)方法绑定指定端口。
至此,Netty服务端就启动起来,下面是Netty服务端启动的源码流程图。
服务端启动之后,就可以接收并处理客户端的请求了,后续的流程就放到下一次再作分析。
相关文章:
【图解IO与Netty系列】Netty源码解析——服务端启动
Netty源码解析——服务端启动 Netty案例复习Netty原理复习Netty服务端启动源码解析bind(int)initAndRegister()channelFactory.newChannel()init(channel)config().group().register(channel)startThread()run()register0(ChannelPromise promise)doBind0(...) 今天我们一起来学…...
Python酷库之旅-比翼双飞情侣库(10)
目录 一、xlrd库的由来 二、xlrd库优缺点 1、优点 1-1、支持多种Excel文件格式 1-2、高效性 1-3、开源性 1-4、简单易用 1-5、良好的兼容性 2、缺点 2-1、对.xlsx格式支持有限 2-2、功能相对单一 2-3、更新和维护频率低 2-4、依赖外部资源 三、xlrd库的版本说明 …...
2024年全国青少信息素养大赛python编程复赛集训第二天编程题分享
整理资料不容易,感谢各位大佬给个点赞和分享吧,谢谢 大家如果不想阅读前边的比赛内容介绍,可以直接跳过:拉到底部看集训题目 (一)比赛内容: 【小学组】 1.了解输入与输出的概念,掌握使用基本输入输出和简单运算 为主的标准函数; 2.掌握注释的方法; 3.掌握基本数…...
Java | Leetcode Java题解之第151题反转字符串中的单词
题目: 题解: class Solution {public String reverseWords(String s) {StringBuilder sb trimSpaces(s);// 翻转字符串reverse(sb, 0, sb.length() - 1);// 翻转每个单词reverseEachWord(sb);return sb.toString();}public StringBuilder trimSpaces(S…...
web前端教程全套:从入门到精通的全方位探索
web前端教程全套:从入门到精通的全方位探索 在数字时代的浪潮中,Web前端技术作为连接用户与数字世界的桥梁,日益受到重视。本文将围绕Web前端教程的全套内容,从四个方面、五个方面、六个方面和七个方面展开深入剖析,旨…...
什么是端口转发?路由器如何正确的设置端口转发和范围转发?(外网访问必备设置)
文章目录 📖 介绍 📖🏡 演示环境 🏡📒 端口转发 📒🚀 端口转发的应用场景💡 路由器如何设置端口转发(示例)💡 端口范围转发(示例)🎯 范围转发的应用场景🛠️ 设置范围转发📝 范围转发实操示例🎈 注意事项 🎈⚓️ 相关链接 ⚓️📖 介绍 📖 …...
【AI基础】第六步:纯天然保姆喂饭级-安装并运行qwen2-7b
整体步骤类似于 【AI基础】第五步:纯天然保姆喂饭级-安装并运行chatglm3-6b-CSDN博客。 此系列文章列表: 【AI基础】概览 【AI基础】第一步:安装python开发环境-windows篇_下载安装ai环境python 【AI基础】第一步:安装python开发环…...
基于粒子群优化算法的的微电网多目标优化调度----算法改进
前言: 当阅读过前一篇我的博客之后,并且认真去读懂了那篇文章末尾的代码,那么,后续的算法改进对于你来说应当是很容易的了。前文中提及过,粒子群在进行迭代时,每迭代一次,都会根据自己个体最优值…...
计算机网络 —— 网络层 (路由协议)
计算机网络 —— 网络层 (路由协议) 什么是路由协议内部网关协议RIP关键特性 OSPF主要特点 外部网关协议BGP关键特性 我们今天来看路由协议: 什么是路由协议 路由协议是网络设备(主要是路由器)用来决定数据包在网络中…...
HCIA 15 AC+FIT AP结构WLAN基础网络
本例配置ACFIT,即瘦APAC组网。生活中家庭上网路由器是胖AP,相当于ACFIT二合一集成到一个设备上。 1.实验介绍及拓扑 某企业网络需要用户通过 WLAN 接入网络,以满足移动办公的最基本需求。 1. AC 采用旁挂核心组网方式,AC 与AP …...
给Windows软件添加异常捕获模块生成dump文件(附源码)
软件在运行过程中会时常发生内存越界、内存访问为例、stack overflow线程栈溢出、空指针与野指针等异常崩溃,仅仅是依靠Debug和Release下的调试是远远不够的,因为有些崩溃不是必现的,或者是Debug下很难出现的。所以我们需要在软件中添加异常捕获的模块,在捕获到异常时生成包…...
C语言| 数组
直接定义一个数组,并给所有元素赋值。 数组的下标从0开始,下标又表示数组的长度。 【程序代码】 #include <stdio.h> int main(void) { int a[5] {1, 2, 3, 4, 5}; int i; for(i0; i<5; i) { printf("a[%d] %d\…...
upload-labs第八关教程
upload-labs第八关教程 一、源代码分析代码审计 二、绕过分析点绕过上传eval.php使用burp suite进行抓包修改放包,查看是否上传成功使用中国蚁剑进行连接 一、源代码分析 代码审计 $is_upload false; $msg null; if (isset($_POST[submit])) {if (file_exists(U…...
平板消解加热台-温度均匀,防腐蚀-实验室化学分析
DBF系列防腐电热板 是精致路合金加热板块表面经进口高纯实验级PFATeflon氟塑料防腐不粘处理,专为实验室设计的电加热产品,是样品前处理中,加热、消解、煮沸、蒸酸、赶酸等处理的得力助手。可以满足物理、化学、生物、环保、制药、食品、饮品…...
Ubuntu基础-vim编辑器
目录 前言: 一. 安装 二. 配置 三. 基本使用 1.使用 Vim 编辑文本文件 2.代码编辑 3.多窗口编辑 四. 总结 前言: Vim 是从 VI 发展出来的一个文本编辑器,具有代码补充、错误跳转等功能,在程序员中被广泛使用。它的设计理念是命令的组合ÿ…...
Java 网站开发入门指南:如何用java写一个网站
Java 网站开发入门指南:如何用java写一个网站 Java 作为一门强大的编程语言,在网站开发领域也占据着重要地位。虽然现在 Python、JavaScript 等语言在网站开发中越来越流行,但 Java 凭借其稳定性、可扩展性和丰富的生态系统,仍然…...
Armbian OS(基于ubuntu24) 源码编译mysql 5.7
最近弄了个S905X3的盒子刷完Armbian OS (基于ubuntu24),开始折腾Arm64之旅。第一站就遇到了MySQL的问题,由于MySQL没有提供Arm64版本,又不想塞Docker镜像,因此选择源码来编译MySQL5.7。下面记录详细过程和遇…...
React+TS前台项目实战(六)-- 全局常用组件Button封装
文章目录 前言Button组件1. 功能分析2. 代码注释说明3. 使用方式4. 效果展示(1)有加载动画,执行promise函数(2)无加载动画,执行click事件 总结 前言 今天这篇主要讲全局按钮组件封装,可根据UI设…...
Vite支持的React项目使用SASS指南
前言 在现代前端开发中,SASS是一种广受欢迎的CSS扩展语言,它提供了许多实用功能,如变量、嵌套、部分和混合等。 本教程将指导您在一个使用Vite作为构建工具的React项目中如何配置和使用SASS。 使用步骤 1、创建一个Vite React项目 首先确…...
实验12 路由重分布
实验12 路由重分布 一、 原理描述二、 实验目的三、 实验内容四、 实验配置五、 实验步骤 一、 原理描述 在大型网络的组建过程中,隶属不同机构的网络部分往往会根据自身的实际情况来选用路由协议。例如,有些网络规模很小,为了管理简单&…...
version-manager最好用的SDK版本管理器,v0.6.2发布
项目地址:https://github.com/gvcgo/version-manager 中文文档:https://gvcgo.github.io/vdocs/#/zh-cn/introduction 功能特点: 跨平台,支持Windows,Linux,MacOS支持多种语言和工具,省心受到…...
MFC工控项目实例之三theApp变量传递对话框参数
承接专栏《MFC工控项目实例之二主菜单制作》 用theApp变量传递对话框参数实时改变iPlotX坐标轴最小值、最大值。 1、新建IDD_SYS_DATA对话框,类名SYS_DATA。 三个编辑框IDC_EDIT1、IDC_EDIT2、IDC_EDIT3变量如图 2、SEAL_PRESSURE.h中添加代码 #include "re…...
C# OpenCvSharp 图像处理函数-图像拼接-hconcat、vconcat、Stitcher
在图像处理和计算机视觉领域,图像拼接是一个常见的操作。OpenCvSharp是一个用于.NET平台的OpenCV封装库,可以方便地进行图像处理。本文将详细介绍如何使用OpenCvSharp中的hconcat、vconcat函数以及Stitcher类进行图像拼接,并通过具体示例帮助读者理解和掌握这些知识点。 函…...
软件方案评审与模块优化:从FOC模块出发的电控平台建设
一、背景 洞悉模块发展趋势,定制行业应用特点,明确优化方向与阶段性目标 随着科技进步的飞速发展,模块化设计已成为众多行业产品开发的核心理念。无论是软件系统、硬件组件,还是复杂系统中的功能模块,都需要对其发展…...
emoji_call_read
这道题我觉得可以记录一下。 主要函数,一样,先考虑怎么泄露libc基址。 但,0x20实在太小,组成不了连续3个ret syscall。 而且文件中也没pop rdi;ret这个gadget,只能另寻他法。 我们注意到: main函数中的这…...
一键自动粘贴,高效处理邮箱地址,让你的工作效率翻倍提升!
在信息爆炸的时代,邮箱地址已成为我们日常工作和生活中的必备元素。无论是商务沟通、报名注册还是信息传递,邮箱地址都扮演着至关重要的角色。然而,手动复制粘贴邮箱地址的繁琐操作往往让人头疼不已,不仅效率低下,还容…...
【Java】Java 使用 Graphics2D 在图片上添加文字,并解决图片变红问题
文章目录 【Java】Java 使用 Graphics2D 在图片上添加文字,并解决图片变红问题完整案例 【Java】Java 使用 Graphics2D 在图片上添加文字,并解决图片变红问题 完整案例 public static void main(String[] args) {try {String filePath "D:\\Works…...
初识docker插件
文章目录 一、Docker插件概述二、Docker插件的优势三、Docker插件的使用例子1. 安装Docker插件2. 启用Docker插件3. 创建卷并使用 四、常用的Docker插件五、总结 Docker插件(Docker Plugin)是Docker扩展功能的一种重要方式,它允许用户为Docke…...
springboot整合redis以及使用
在Spring Boot中整合Redis,并使用Redis作为缓存或数据存储,是非常常见和有用的场景。Redis作为一种高性能的键值存储系统,可以用来加速数据访问、会话管理、消息队列等多种用途。下面是整合和使用Redis的基本步骤: 1. 添加Redis依…...
ARM32开发--电源管理单元
知不足而奋进 望远山而前行 目录 文章目录 前言 学习目标 学习内容 PMU 电源域 VDD/VDDA域 备份域 1.2V域 省电模式 睡眠模式 深度睡眠模式 待机模式 几种模式总结 WFI和WFE指令 案例需求 模式初始化 源码 总结 前言 在嵌入式系统中,有效的电池管…...
帮别人做诈骗网站技术/百度一下你就知道了主页
环境 VMware 15 Windows server 2016 原因 虚拟机与宿主机之间,虚拟机可以ping通宿主机,但是宿主机却ping不通虚拟机; 且 虚拟机和宿主机不在同一网段内。 新的解决方法 ①、虚拟机中配置好 IP地址 ②、Nat 这个是VMnet8 ③、启动 …...
台州网站建设优化案例/北京做网页的公司
目录 概念/介绍 CRONTAB语法及应用 1. 查看当前用户的定时任务 2. 编辑当前用户的定时任务 3. 删除当前用户的定时任务 4. 任务详解 5. 注意: 概念/介绍 crontab命令用于设置周期性被执行的指令。该命令从标准输入设备读取指令,并将其存放于“cr…...
做网站的技术支持/网站排名优化软件哪家好
什么是RDD:RDD叫做分布式数据集,是Spark最基本的数据抽象,代码中是一个抽象类,它代表的是一个不可变、可分区、里面的元素可并行计算的集合。RDD里面封装的是计算逻辑。RDD的属性:1. 一个分区(Partition),即数据集的基…...
java开发网站的优势/百度人气榜排名
如何去掉默认注释?* window -- Preferences -- Java -- Code Style -- Code Templates* 选择你不想要的内容,通过右边Edit编辑。* 注意:请只删除注释部分,不是注释部分的不要删除。 行号的显示和隐藏* 显示:在代码区域的最左边的…...
企业网站建设排名官网/网站外链购买
Problem 2245 动态树Accept: 17 Submit: 82Time Limit: 3000 mSec Memory Limit : 65536 KB Problem Description YellowStar拥有一棵神奇的动态树,该树由n个带权结点,n-1条边构成,任意两个结点互相可达,标号为i结点的权值为…...
企业网站做几个合适/营销推广方法有哪些
第4章 需求工程 软件需求是决定软件开发是否成功的一个关键因素。 一、软件工程 需求的概念涵盖用户角度(系统的外部行为)和开发人员角度(系统的内部特征),关键在于需求一定要文档化。 软件需求划分:业务需…...