netty中的引导Bootstrap服务端

摘要:
为了支持此模式而不为每个通道创建和配置新的引导类实例,AbstractBootstrap标记为可克隆①注意,此方法只创建引导类实例的EventLoopGroup的浅拷贝,因此后者。˃,对象˃();privatevolatileChannelHandlerhandler;//…}3、 引导服务器ServerBootstrap 3.1和ServerBootstrab类的方法组:将EventLoopGroup设置为ServerBootstra使用。因此,负责引导ServerChannel的ServerBootstrap提供了这些方法,以简化将设置应用于已接受子通道的ChannelConfig的任务。

引导一个应用程序是指对它进行配置,并使它运行起来的过程。

一、Bootstrap 类

引导类的层次结构包括一个抽象的父类和两个具体的引导子类,如图 8-1 所示

netty中的引导Bootstrap服务端第1张

服务器致力于使用一个父 Channel 来接受来自客户端的连接,并创建子 Channel 以用于它们之间的通信;

而客户端将最可能只需要一个单独的、没有父Channel 的Channel 来用于所有的网络交互。(正如同我们将要看到的,这也 适用于无连接的传输协议,如 UDP,因为它们并不是每个连接都需要一个单独的 Channel。)

为什么引导类是 Cloneable 的

你有时可能会需要创建多个具有类似配置或者完全相同配置的Channel。为了支持这种模式而又不 需要为每个 Channel 都创建并配置一个新的引导类实例, AbstractBootstrap 被标记为了 Cloneable①

注意,这种方式只会创建引导类实例的EventLoopGroup的一个浅拷贝,所以,后者 。在一个已经配置完成的引导类实例上调用clone()方法将返回另一个可以立即使用的引 导类实例。

二、AbstractBootstrap类

2.1、AbstractBootstrap成员变量

public abstract class AbstractBootstrap<B extends AbstractBootstrap<B, C>, C extends Channel> implements Cloneable {

    private volatile EventLoopGroup group;
    private volatile SocketAddress localAddress;
    private final Map<ChannelOption<?>, Object> options = new LinkedHashMap<ChannelOption<?>, Object>();
    private final Map<AttributeKey<?>, Object> attrs = new LinkedHashMap<AttributeKey<?>, Object>();
    private volatile ChannelHandler handler;
//...
}

三、引导服务器ServerBootstrap

3.1、ServerBootstrap 类的方法

group:设置 ServerBootstrap 要用的 EventLoopGroup。这个 EventLoopGroup将用于 ServerChannel 和被接受的子 Channel 的 I/O 处理
channel: 设置将要被实例化的 ServerChannel 类
channelFactory:如果不能通过默认的构造函数 ①创建Channel,那么可以提供一个ChannelFactory
localAddress: 指定 ServerChannel 应该绑定到的本地地址。如果没有指定,则将由操作系统使用一个随机地址。或者,可以通过 bind()方法来指定该 localAddress
option: 指定要应用到新创建的 ServerChannel 的 ChannelConfig 的 ChannelOption。这些选项将会通过bind()方法设置到 Channel。在 bind()方法被调用之后,设置或者改变 ChannelOption 都不会有任何的效果。所支持的 ChannelOption 取决于所使用的 Channel 类型。参见正在使用的ChannelConfig 的 API 文档
childOption: 指定当子 Channel 被接受时,应用到子 Channel 的 ChannelConfig 的ChannelOption。所支持的 ChannelOption 取决于所使用的 Channel 的类型。参见正在使用的 ChannelConfig 的 API 文档
attr: 指定 ServerChannel 上的属性,属性将会通过 bind()方法设置给 Channel。在调用 bind()方法之后改变它们将不会有任何的效果
childAttr: 将属性设置给已经被接受的子 Channel。接下来的调用将不会有任何的效果
handler: 设置被添加到ServerChannel 的ChannelPipeline中的ChannelHandler。更加常用的方法参见 childHandler()
childHandler: 设置将被添加到已被接受的子 Channel 的 ChannelPipeline 中的 ChannelHandler。handler()方法和childHandler()方法之间的区别是:前者所添加的 ChannelHandler 由接受子 Channel 的 ServerChannel 处理,而
childHandler()方法所添加的 ChannelHandler 将由已被接受的子 Channel处理,其代表一个绑定到远程节点的套接字
clone: 克隆一个设置和原始的 ServerBootstrap 相同的 ServerBootstrap
bind: 绑定 ServerChannel 并且返回一个 ChannelFuture,其将会在绑定操作完成后收到通知(带着成功或者失败的结果)

比客户端多的方法:childHandler()、 childAttr()和 childOption()。这些调用支持特别用于服务器应用程序的操作。具体来说, ServerChannel 的实现负责创建子 Channel,这些子 Channel 代表了已被接受的连接。因此,负责引导 ServerChannel 的 ServerBootstrap 提供了这些方法,以简化将设置应用到已被接受的子 Channel 的 ChannelConfig 的任务。

图 8-3 展示了 ServerBootstrap 在 bind()方法被调用时创建了一个 ServerChannel, 并且该 ServerChannel 管理了多个子 Channel。

netty中的引导Bootstrap服务端第2张

首先还是来看一下服务器端的启动代码:

// Configure the server.
        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>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc()));
                     }
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     p.addLast(new EchoServerHandler());
                 }
             });

            // Start the server.
            ChannelFuture f = b.bind(PORT).sync();

            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down all event loops to terminate all threads.
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }

基本上也是进行了如下几个部分的初始化:

  1. EventLoopGroup: 不论是服务器端还是客户端, 都必须指定 EventLoopGroup. 在这个例子中, 指定了 NioEventLoopGroup, 表示一个 NIO 的EventLoopGroup, 不过服务器端需要指定两个EventLoopGroup, 一个是 bossGroup, 用于处理客户端的连接请求; 另一个是 workerGroup, 用于处理与各个客户端连接的 IO 操作.
  2. ChannelType: 指定 Channel 的类型. 因为是服务器端, 因此使用了 NioServerSocketChannel.
  3. Handler: 设置数据的处理器.

3.2、Channel 的初始化过程

Channel 是对 Java 底层 Socket 连接的抽象, 并且知道了客户端的 Channel 的具体类型是 NioSocketChannel, 那么自然的, 服务器端的 Channel 类型就是 NioServerSocketChannel 了.

Channel 类型的确定

Channel 的类型其实是在初始化时, 通过 ServerBootstarap.channel(NioServerSocketChannel.class), 传递了一个 NioServerSocketChannel Class 对象. 这样的话, 按照和分析客户端代码一样的流程, 我们就可以确定, NioServerSocketChannel 的实例化是通过 BootstrapChannelFactory 工厂类来完成的, 而 BootstrapChannelFactory 中的 clazz 字段被设置为了 NioServerSocketChannel.class, 因此当调用 BootstrapChannelFactory.newChannel() 时:

@Override
public T newChannel() {
    // 删除 try 块
    return clazz.newInstance();
}

就获取到了一个 NioServerSocketChannel 的实例.

最后我们也来总结一下:

  • ServerBootstrap 中的 ChannelFactory 的实现是 BootstrapChannelFactory

  • 生成的 Channel 的具体类型是 NioServerSocketChannel. 
    Channel 的实例化过程, 其实就是调用的 ChannelFactory.newChannel 方法, 而实例化的 Channel 的具体的类型又是和在初始化 ServerBootstrap 时传入的 channel() 方法的参数相关. 因此对于我们这个例子中的服务器端的 ServerBootstrap 而言, 生成的的 Channel 实例就是 NioServerSocketChannel.

NioServerSocketChannel 的实例化过程

首先还是来看一下 NioServerSocketChannel 的实例化过程.
下面是 NioServerSocketChannel 的类层次结构图:

netty中的引导Bootstrap服务端第3张

首先, 我们来看一下它的默认的构造器. 和 NioSocketChannel 类似, 构造器都是调用了 newSocket 来打开一个 Java 的 NIO Socket, 不过需要注意的是, 客户端的 newSocket 调用的是 openSocketChannel, 而服务器端的 newSocket 调用的是 openServerSocketChannel. 顾名思义, 一个是客户端的 Java SocketChannel, 一个是服务器端的 Java ServerSocketChannel.

private static ServerSocketChannel newSocket(SelectorProvider provider) {
    return provider.openServerSocketChannel();
}

public NioServerSocketChannel() {
    this(newSocket(DEFAULT_SELECTOR_PROVIDER));
}

接下来会调用重载的构造器:

public NioServerSocketChannel(ServerSocketChannel channel) {
    super(null, channel, SelectionKey.OP_ACCEPT);
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

这个构造其中, 调用父类构造器时, 传入的参数是 SelectionKey.OP_ACCEPT. 作为对比, 我们回想一下, 在客户端的 Channel 初始化时, 传入的参数是 SelectionKey.OP_READ. 有 Java NIO Socket 开发经验的朋友就知道了, Java NIO 是一种 Reactor 模式, 我们通过 selector 来实现 I/O 的多路复用复用. 在一开始时, 服务器端需要监听客户端的连接请求, 因此在这里我们设置了 SelectionKey.OP_ACCEPT, 即通知 selector 我们对客户端的连接请求感兴趣.

接着和客户端的分析一下, 会逐级地调用父类的构造器 NioServerSocketChannel <- AbstractNioMessageChannel <- AbstractNioChannel <- AbstractChannel.
同样的, 在 AbstractChannel 中会实例化一个 unsafe 和 pipeline:

protected AbstractChannel(Channel parent) {
    this.parent = parent;
    unsafe = newUnsafe();
    pipeline = new DefaultChannelPipeline(this);
}

不过, 这里有一点需要注意的是, 客户端的 unsafe 是一个 AbstractNioByteChannel#NioByteUnsafe 的实例, 而在服务器端时, 因为 AbstractNioMessageChannel 重写了newUnsafe 方法:

@Override
protected AbstractNioUnsafe newUnsafe() {
    return new NioMessageUnsafe();
}

因此在服务器端, unsafe 字段其实是一个 AbstractNioMessageChannel#AbstractNioUnsafe 的实例.
我们来总结一下, 在 NioServerSocketChannsl 实例化过程中, 所需要做的工作:

  • 调用 NioServerSocketChannel.newSocket(DEFAULT_SELECTOR_PROVIDER) 打开一个新的 Java NIO ServerSocketChannel

  • AbstractChannel(Channel parent) 中初始化 AbstractChannel 的属性:

    • parent 属性置为 null

    • unsafe 通过newUnsafe() 实例化一个 unsafe 对象, 它的类型是 AbstractNioMessageChannel#AbstractNioUnsafe 内部类

    • pipeline 是 new DefaultChannelPipeline(this) 新创建的实例.

  • AbstractNioChannel 中的属性:

    • SelectableChannel ch 被设置为 Java ServerSocketChannel, 即 NioServerSocketChannel#newSocket 返回的 Java NIO ServerSocketChannel.

    • readInterestOp 被设置为 SelectionKey.OP_ACCEPT

    • SelectableChannel ch 被配置为非阻塞的 ch.configureBlocking(false)

  • NioServerSocketChannel 中的属性:

    • ServerSocketChannelConfig config = new NioServerSocketChannelConfig(this, javaChannel().socket())

3.2、ChannelPipeline 初始化

在初始化Channel的同时也初始化了ChannelPipeline了,新创建的 Channel 都将会被分配一个新的 ChannelPipeline,它们之间关系是永久的。见《netty中的Channel、ChannelPipeline》中的DefaultChannelPipeline 构造函数章节。

3.3、Channel 的注册

服务器端和客户端的 Channel 的注册过程一致, 因此就不再单独分析了.

关于 bossGroup 与 workerGroup

在服务器端的初始化时, 我们设置了两个 EventLoopGroup, 一个是 bossGroup, 另一个是 workerGroup. 那么这两个 EventLoopGroup 都是干什么用的呢? 其实呢, bossGroup 是用于服务端 的 accept 的, 即用于处理客户端的连接请求. 而 workerGroup, 其实就是实际上干活的啦, 它们负责客户端连接通道的 IO 操作,关于 bossGroup 与 workerGroup 的关系, 我们可以用如下图来展示:

netty中的引导Bootstrap服务端第4张

首先, 服务器端 bossGroup 不断地监听是否有客户端的连接, 当发现有一个新的客户端连接到来时, bossGroup 就会为此连接初始化各项资源, 然后从 workerGroup 中选出一个 EventLoop 绑定到此客户端连接中. 那么接下来的服务器与客户端的交互过程就全部在此分配的 EventLoop 中了.

首先在ServerBootstrap 初始化时, 调用了 b.group(bossGroup, workerGroup) 设置了两个 EventLoopGroup, 我们跟踪进去看一下:

public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) {
    super.group(parentGroup);
    ...
    this.childGroup = childGroup;
    return this;
}

显然, 这个方法初始化了两个字段, 一个是 group = parentGroup, 它是在 super.group(parentGroup) 中初始化的, 另一个是 childGroup = childGroup. 接着我们启动程序调用了 b.bind 方法来监听一个本地端口. bind 方法会触发如下的调用链:

AbstractBootstrap.bind -> AbstractBootstrap.doBind -> AbstractBootstrap.initAndRegister

AbstractBootstrap.initAndRegister()

final ChannelFuture initAndRegister() {
    final Channel channel = channelFactory().newChannel();
    ... 省略异常判断
    init(channel);
    ChannelFuture regFuture = group().register(channel);
    return regFuture;
}

这里 group() 方法返回的是上面我们提到的 bossGroup, 而这里的 channel 我们也已经分析过了, 它是一个是一个 NioServerSocketChannsl 实例, 因此我们可以知道, group().register(channel) 将 bossGroup 和 NioServerSocketChannsl 关联起来了.
那么 workerGroup 是在哪里与 NioSocketChannel 关联的呢?
我们继续看 init(channel) 方法:

@Override
void init(Channel channel) throws Exception {
    ...
    ChannelPipeline p = channel.pipeline();

    final EventLoopGroup currentChildGroup = childGroup;
    final ChannelHandler currentChildHandler = childHandler;
    final Entry<ChannelOption<?>, Object>[] currentChildOptions;
    final Entry<AttributeKey<?>, Object>[] currentChildAttrs;

    p.addLast(new ChannelInitializer<Channel>() {
        @Override
        public void initChannel(Channel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            ChannelHandler handler = handler();
            if (handler != null) {
                pipeline.addLast(handler);
            }
            pipeline.addLast(new ServerBootstrapAcceptor(
                    currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
        }
    });
}

init 方法在 ServerBootstrap 中重写了, 从上面的代码片段中我们看到, 它为 pipeline 中添加了一个 ChannelInitializer, 而这个 ChannelInitializer 中添加了一个关键的 ServerBootstrapAcceptor handler. 关于 handler 的添加与初始化的过程, 我们留待下一小节中分析, 我们现在关注一下 ServerBootstrapAcceptor 类.
ServerBootstrapAcceptor 中重写了 channelRead 方法, 其主要代码如下:

@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;
    child.pipeline().addLast(childHandler);
    ...
    childGroup.register(child).addListener(...);
}

ServerBootstrapAcceptor 中的 childGroup 是构造此对象是传入的 currentChildGroup, 即我们的 workerGroup, 而 Channel 是一个 NioSocketChannel 的实例, 因此这里的 childGroup.register 就是将 workerGroup 中的某个EventLoop 和 NioSocketChannel 关联了. 既然这样, 那么现在的问题是, ServerBootstrapAcceptor.channelRead 方法是怎么被调用的呢? 其实当一个 client 连接到 server 时, Java 底层的 NIO ServerSocketChannel 会有一个 SelectionKey.OP_ACCEPT 就绪事件, 接着就会调用到 NioServerSocketChannel.doReadMessages:

@Override
protected int doReadMessages(List<Object> buf) throws Exception {
    SocketChannel ch = javaChannel().accept();
    ... 省略异常处理
    buf.add(new NioSocketChannel(this, ch));
    return 1;
}

在 doReadMessages 中, 通过 javaChannel().accept() 获取到客户端新连接的 SocketChannel, 接着就实例化一个 NioSocketChannel, 并且传入 NioServerSocketChannel 对象(即 this), 由此可知, 我们创建的这个 NioSocketChannel 的父 Channel 就是 NioServerSocketChannel 实例 .
接下来就经由 Netty 的 ChannelPipeline 机制, 将读取事件逐级发送到各个 handler 中, 于是就会触发前面我们提到的 ServerBootstrapAcceptor.channelRead 方法啦.

handler 的添加过程

服务器端的 handler 的添加过程和客户端的有点区别, 和 EventLoopGroup 一样, 服务器端的 handler 也有两个, 一个是通过 handler() 方法设置 handler 字段, 另一个是通过 childHandler() 设置 childHandler 字段. 通过前面的 bossGroup 和 workerGroup 的分析, 其实我们在这里可以大胆地猜测: handler 字段与 accept 过程有关, 即这个 handler 负责处理客户端的连接请求; 而 childHandler 就是负责和客户端的连接的 IO 交互.
那么实际上是不是这样的呢? 来, 我们继续通过代码证明.

在 关于 bossGroup 与 workerGroup 小节中, 我们提到, ServerBootstrap 重写了 init 方法, 在这个方法中添加了 handler:

@Override
void init(Channel channel) throws Exception {
    ...
    ChannelPipeline p = channel.pipeline();

    final EventLoopGroup currentChildGroup = childGroup;
    final ChannelHandler currentChildHandler = childHandler;
    final Entry<ChannelOption<?>, Object>[] currentChildOptions;
    final Entry<AttributeKey<?>, Object>[] currentChildAttrs;

    p.addLast(new ChannelInitializer<Channel>() {
        @Override
        public void initChannel(Channel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            ChannelHandler handler = handler();
            if (handler != null) {
                pipeline.addLast(handler);
            }
            pipeline.addLast(new ServerBootstrapAcceptor(
                    currentChildGroup, currentChildHandler, currentChildOptions, currentChildAttrs));
        }
    });
}

上面代码的 initChannel 方法中, 首先通过 handler() 方法获取一个 handler, 如果获取的 handler 不为空,则添加到 pipeline 中. 然后接着, 添加了一个 ServerBootstrapAcceptor 实例. 那么这里 handler() 方法返回的是哪个对象呢? 其实它返回的是 handler 字段, 而这个字段就是我们在服务器端的启动代码中设置的:

b.group(bossGroup, workerGroup)
 ...
 .handler(new LoggingHandler(LogLevel.INFO))

那么这个时候, pipeline 中的 handler 情况如下:

netty中的引导Bootstrap服务端第5张

根据我们原来分析客户端的经验, 我们指定, 当 channel 绑定到 eventLoop 后(在这里是 NioServerSocketChannel 绑定到 bossGroup)中时, 会在 pipeline 中发出 fireChannelRegistered 事件, 接着就会触发 ChannelInitializer.initChannel 方法的调用.
因此在绑定完成后, 此时的 pipeline 的内如如下:

netty中的引导Bootstrap服务端第6张

前面我们在分析 bossGroup 和 workerGroup 时, 已经知道了在 ServerBootstrapAcceptor.channelRead 中会为新建的 Channel 设置 handler 并注册到一个 eventLoop 中, 即:

@Override
@SuppressWarnings("unchecked")
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    final Channel child = (Channel) msg;
    child.pipeline().addLast(childHandler);
    ...
    childGroup.register(child).addListener(...);
}

而这里的 childHandler 就是我们在服务器端启动代码中设置的 handler:

b.group(bossGroup, workerGroup)
 ...
 .childHandler(new ChannelInitializer<SocketChannel>() {
     @Override
     public void initChannel(SocketChannel ch) throws Exception {
         ChannelPipeline p = ch.pipeline();
         if (sslCtx != null) {
             p.addLast(sslCtx.newHandler(ch.alloc()));
         }
         //p.addLast(new LoggingHandler(LogLevel.INFO));
         p.addLast(new EchoServerHandler());
     }
 });

后续的步骤就没有什么好说的了, 当这个客户端连接 Channel 注册后, 就会触发 ChannelInitializer.initChannel 方法的调用, 此后的客户端连接的 ChannelPipeline 状态如下:

netty中的引导Bootstrap服务端第7张

最后我们来总结一下服务器端的 handler 与 childHandler 的区别与联系:

  • 在服务器 NioServerSocketChannel 的 pipeline 中添加的是 handler 与 ServerBootstrapAcceptor.

  • 当有新的客户端连接请求时, ServerBootstrapAcceptor.channelRead 中负责新建此连接的 NioSocketChannel 并添加 childHandler 到 NioSocketChannel 对应的 pipeline 中, 并将此 channel 绑定到 workerGroup 中的某个 eventLoop 中.

  • handler 是在 accept 阶段起作用, 它处理客户端的连接请求.

  • childHandler 是在客户端连接建立以后起作用, 它负责客户端连接的 IO 交互.

下面我们用一幅图来总结一下服务器端的 handler 添加流程:

netty中的引导Bootstrap服务端第8张

转自:

https://segmentfault.com/a/1190000007283053#articleHeader1

免责声明:文章转载自《netty中的引导Bootstrap服务端》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇Vue.js框架:IDEA中vue文件代码自动补全设置怎么彻底关闭卸载删除Cortana小娜进程,最简单下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

[Android Memory] App调试内存泄露之Context篇(上)

转载自:http://www.cnblogs.com/qianxudetianxia/p/3645106.html Context作为最基本的上下文,承载着Activity,Service等最基本组件。当有对象引用到Activity,并不能被回收释放,必将造成大范围的对象无法被回收释放,进而造成内存泄漏。 下面针对一些常用场景逐一分析。 1. CallBa...

物联网架构成长之路(35)-利用Netty解析物联网自定义协议

一、前言   前面博客大部分介绍了基于EMQ中间件,通信协议使用的是MQTT,而传输的数据为纯文本数据,采用JSON格式。这种方式,大部分一看就知道是熟悉Web开发、软件开发的人喜欢用的方式。由于我也是做web软件开发的,也是比较喜欢这种方式。阿里的物联网平台,也是推荐这种方式。但是,但是做惯硬件开发,嵌入式开发就比较喜欢用裸TCP-Socket连接。采用...

Spring MVC原理图

步骤: 1.发起请求到前端控制器(DispatcherServlet) 2.前端控制器请求处理器映射器(HandlerMapping)查找Handler(可根据xml配置、注解进行查找) 3.处理器映射器(HandlerMapping)向前端控制器返回Handler 4.前端控制器调用处理器适配器(HandlerAdapter)执行Handler 5....

面试题--赵银科技

1.pubilc A{ public void test(){} }  public B extends A{ protected void test(){} } 这样有问题吗?为什么?  错, 2.public A{ public long test(){} }  public B extends A{ public int test(){} } 这样有...

[php]laravel框架容器管理的一些要点

本文面向php语言的laravel框架的用户,介绍一些laravel框架里面容器管理方面的使用要点。文章很长,但是内容应该很有用,希望有需要的朋友能看到。php经验有限,不到位的地方,欢迎帮忙指正。 1. laravel容器基本认识 laravel框架是有一个容器框架,框架应用程序的实例就是一个超大的容器,这个实例在bootstrap/app.php内进行...

CentOS7 初始化配置

一、在安装的时候配置网卡名称的参数 1. 选择“Install Centos 7” 2. 按Tab,打开kernel启动选项后,增加 net.ifnames=0 biosdevname=0 二、最小化安装完成之后必备安装软件 # 添加epel源,安装基础软件,设置主机名rpm -ivh http://mirrors.aliyun.com/epel/epe...