Netty 介绍、使用场景及案例

Netty 介绍、使用场景及案例

在这里插入图片描述

1、Netty 介绍

https://github.com/netty/netty

Netty是一个高性能、异步事件驱动的网络应用程序框架,用于快速开发可扩展的网络服务器和客户端。它是一个开源项目,最初由JBoss公司开发,现在由社区维护。Netty的设计和实现以处理高并发、低延迟、可靠性和灵活性为目标,因此非常适合构建各种网络应用,包括网络服务器、代理、聊天应用、在线游戏、实时通信和分布式系统等。

以下是一些Netty的主要特点和优势:

  1. 异步和事件驱动:Netty采用异步非阻塞的IO模型,允许处理大量并发连接而不会阻塞应用程序线程。它使用事件驱动的方式来处理网络事件,这使得编写高效的网络应用程序变得更容易。

  2. 高性能:Netty在性能方面表现出色,其底层的NIO实现充分利用了现代操作系统的异步IO特性,能够处理大量并发连接和数据传输,同时保持低延迟。

  3. 可扩展性:Netty提供了灵活的扩展机制,可以轻松地定制和扩展功能,以满足不同应用程序的需求。它支持各种协议和编解码器,如HTTP、WebSocket、TLS/SSL等。

  4. 安全性:Netty内置了对TLS/SSL的支持,可以加密网络连接以确保数据的安全传输。

  5. 多协议支持:Netty支持多种网络协议,包括TCP、UDP、HTTP、WebSocket等,使其适用于各种应用场景。

  6. 大型社区和活跃开发:Netty有一个庞大的开发社区,不断更新和改进框架,以适应新的技术和需求。

  7. 文档丰富:Netty提供了详细的文档和示例代码,使开发者可以快速上手并学习如何使用框架。

总之,Netty是一个强大的网络应用程序框架,适用于构建高性能、可扩展和可靠的网络应用。它在许多大型互联网公司和开源项目中被广泛使用,并且在处理网络通信方面具有广泛的应用。如果您需要开发网络应用程序或服务器,特别是需要处理大量并发连接和低延迟的场景,Netty是一个值得考虑的选择。

Netty 概述:
Netty是一个基于Java的高性能网络应用框架,它提供了简单而强大的网络编程接口,使得开发者可以轻松地构建各种类型的网络应用程序,包括服务器和客户端。Netty是一个开源项目,广泛用于构建可伸缩性、高性能、可维护性好的网络服务器和客户端应用。它提供了一组易于使用的API,用于处理底层的网络通信,包括TCP、UDP、HTTP等协议,以及各种编解码、数据传输和其他网络相关的功能。Netty的设计理念是简单而灵活,同时具备高性能和可扩展性。

2、原生 NIO 存在的问题

原生Java NIO(New I/O)提供了一种非阻塞I/O的编程方式,相对于传统的阻塞I/O(BIO)来说,它在某些情况下可以提供更好的性能,但也存在一些问题和挑战:

  1. 复杂性:NIO编程模型相对复杂,需要程序员处理底层的缓冲区、通道、选择器等概念,编写代码较为繁琐。

  2. 可读性:NIO代码通常相对难以理解和维护,因为需要处理很多底层细节,使得代码可读性较差。

  3. 错误处理:NIO中的错误处理相对复杂,需要处理各种异常和错误状态,容易引入bug。

  4. 编程难度:NIO编程难度较大,需要处理事件驱动的异步编程模型,容易出现并发问题。

  5. 性能限制:虽然NIO可以提供非阻塞I/O,但在高并发和高负载情况下,仍然存在性能瓶颈,需要合理的线程管理和资源调度。

Netty作为一个网络编程框架,通过对原生NIO的封装和优化,解决了上述问题,提供了更加简洁、高效、可维护的网络编程接口,使得开发者能够更容易地构建高性能的网络应用。它的异步、事件驱动、高性能和可扩展性等特点使得它在网络编程领域得到广泛应用。

3、Netty 线程模型

线程模型基本介绍:

  1. 传统阻塞I/O服务模型: 传统的阻塞I/O服务模型是最简单的,每个连接都需要一个独立的线程来处理,这导致了线程数量的快速增长,对系统资源的浪费。

  2. Reactor模式: Reactor模式是一种基于事件驱动的模型,它通过一个事件循环来处理所有的I/O操作。它通常包括一个主线程(Reactor)和多个工作线程,主线程负责接收连接,工作线程负责处理I/O操作。

不同的线程模式对程序性能的影响:

不同的线程模式对程序性能有显著的影响。传统阻塞I/O服务模型通常会导致资源浪费和性能下降,因为每个连接都需要一个线程,线程的创建和销毁开销很大。

Reactor模式通过事件驱动的方式可以显著提高性能,特别是在高并发情况下。但是,Reactor模式的性能仍然受限于单个主线程的处理能力。

Netty线程模型:

Netty主要基于主从Reactor多线程模型做了一定的改进,其中主从Reactor多线程模型包括多个Reactor。以下是Netty线程模型的一些特点:

  1. 单Reactor单线程: Netty可以采用单Reactor单线程模型,这是一种简单的模型,适用于处理低并发的情况。主Reactor负责接收连接,子Reactor负责处理I/O操作。

  2. 单Reactor多线程: 这种模型使用多个工作线程来处理I/O操作,可以提高并发性能。主Reactor接收连接并将其分发给工作线程处理。

  3. 主从Reactor多线程: 这是Netty中最常用的模型,包括一个主Reactor和多个从Reactor,以及每个从Reactor对应的工作线程池。主Reactor负责接收连接,从Reactor负责处理I/O操作。这种模型可以在高并发情况下充分利用多核处理器,提高性能。

Netty线程模型的优越性:

Netty的线程模型的优越性在于其高度可扩展性和性能。通过采用主从Reactor多线程模型,Netty可以轻松地适应高并发的情况,同时充分利用多核处理器,提供出色的性能表现。此外,Netty还提供了异步事件处理和内存管理等高级功能,使得开发网络应用变得更加方便和高效。

4、案例:Netty TCP服务

它抽象出了两组线程池,即BossGroup和WorkerGroup,来处理不同的网络任务。

  1. BossGroup和WorkerGroup都是NioEventLoopGroup类型,代表了事件循环组。每个NioEventLoopGroup包含多个NioEventLoop,每个NioEventLoop都是一个不断循环执行处理任务的线程。

  2. BossGroup负责接收客户端的连接请求,它的主要工作是轮询监听accept事件,当有新的连接请求时,会处理该事件,并生成NioSocketChannel,然后将它注册到某个WorkerGroup的NioEventLoop上的Selector中。这个过程确保了每个连接的读写操作都会由WorkerGroup来处理。

  3. WorkerGroup负责处理网络的读写操作,它的主要工作是轮询监听read和write事件,当有数据需要读写时,会处理对应的事件,即在NioSocketChannel上执行读写操作。此外,WorkerGroup也负责处理任务队列中的任务,这些任务通常是业务逻辑相关的任务。

  4. 每个BossGroup下的NioEventLoop循环执行的步骤包括轮询accept事件、处理accept事件建立连接、继续处理任务队列中的任务。

  5. 每个WorkerGroup下的NioEventLoop循环执行的步骤包括轮询read和write事件、处理I/O事件、处理任务队列中的任务。

  6. 在处理业务逻辑时,每个WorkerGroup的NioEventLoop会使用管道(pipeline)来管理不同的处理器(Handler)。管道中包含了通道(channel),通过管道可以获取到对应的通道,从而处理具体的业务逻辑。

通过BossGroup和WorkerGroup的组合,实现了高效的网络通信处理。BossGroup专门处理连接请求,WorkerGroup专门处理读写操作和业务逻辑,通过事件循环的方式,实现了高性能的网络编程。管道和处理器的使用也使得开发者能够方便地定制和扩展网络应用程序。

NettyServer

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class NettyServer {
    public static void main(String[] args) throws Exception {

        // 创建BossGroup和WorkerGroup
        // 说明
        // 1. 创建两个线程组,bossGroup和workerGroup
        // 2. bossGroup只是处理连接请求,真正的和客户端业务处理,会交给workerGroup完成
        // 3. 两个都是无限循环
        // 4. bossGroup和workerGroup含有的子线程(NioEventLoop)的个数
        //    默认实际CPU核数 * 2
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); // 默认 CPU核数 * 2

        try {
            // 创建服务器端的启动对象,配置参数
            ServerBootstrap bootstrap = new ServerBootstrap();
            // 使用链式编程来进行设置
            bootstrap.group(bossGroup, workerGroup) // 设置两个线程组
                    .channel(NioServerSocketChannel.class) // 使用NioSocketChannel作为服务器的通道实现
                    .option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列等待连接个数
                    .childOption(ChannelOption.SO_KEEPALIVE, true) // 设置保持活动连接状态
                    .childHandler(new ChannelInitializer<SocketChannel>() {// 创建一个通道初始化对象(匿名对象)
                        // 给pipeline设置处理器
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            System.out.println("客户SocketChannel hashCode=" + ch.hashCode()); // 可以使用一个集合管理SocketChannel,再推送消息时,可以将业务加入到各个channel对应的NIOEventLoop的taskQueue或scheduleTaskQueue
                            ch.pipeline().addLast(new NettyServerHandler());
                        }
                    }); // 给我们的workerGroup的EventLoop对应的管道设置处理器

            System.out.println("...服务器 is ready...");

            // 绑定一个端口并同步生成了一个ChannelFuture对象(也就是立马返回这样一个对象)
            // 启动服务器(并绑定端口)
            ChannelFuture cf = bootstrap.bind(6668).sync();

            // 给cf注册监听器,监控我们关心的事件
            cf.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (cf.isSuccess()) {
                        System.out.println("监听端口 6668 成功");
                    } else {
                        System.out.println("监听端口 6668 失败");
                    }
                }
            });

            // 对关闭通道事件进行监听
            cf.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

NettyServerHandler

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelPipeline;
import io.netty.util.CharsetUtil;

/**
 * 说明:
 * 1. 我们自定义一个Handler需要继承Netty规定好的某个HandlerAdapter(规范)
 * 2. 这时我们自定义一个Handler,才能称为一个handler
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    // 读取数据事件(这里我们可以读取客户端发送的消息)
    /*
    1. ChannelHandlerContext ctx:上下文对象,含有管道pipeline,通道channel,地址
    2. Object msg:就是客户端发送的数据,默认Object
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("服务器读取线程 " + Thread.currentThread().getName() + " channle =" + ctx.channel());
        System.out.println("server ctx =" + ctx);
        System.out.println("看看channel和pipeline的关系");
        Channel channel = ctx.channel();
        ChannelPipeline pipeline = ctx.pipeline(); // 本质是一个双向链表

        // 将msg转成一个ByteBuf
        // ByteBuf是Netty提供的,不是NIO的ByteBuffer.
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("客户端发送消息是:" + buf.toString(CharsetUtil.UTF_8));
        System.out.println("客户端地址:" + channel.remoteAddress());
    }

    // 数据读取完毕
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
        // writeAndFlush是write + flush
        // 将数据写入到缓存,并刷新
        // 一般讲,我们对这个发送的数据进行编码
        ctx.writeAndFlush(Unpooled.copiedBuffer("hello, 客户端~(>^ω^<)喵1", CharsetUtil.UTF_8));
    }

    // 发生异常后,一般是需要关闭通道
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        ctx.close();
    }
}

NettyClient

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

public class NettyClient {
    public static void main(String[] args) throws Exception {

        //客户端需要一个事件循环组
        EventLoopGroup group = new NioEventLoopGroup();


        try {
            //创建客户端启动对象
            //注意客户端使用的不是 ServerBootstrap 而是 Bootstrap
            Bootstrap bootstrap = new Bootstrap();

            //设置相关参数
            bootstrap.group(group) //设置线程组
                    .channel(NioSocketChannel.class) // 设置客户端通道的实现类(反射)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new NettyClientHandler()); //加入自己的处理器
                        }
                    });

            System.out.println("客户端 ok..");
            //启动客户端去连接服务器端
            //关于 ChannelFuture 要分析,涉及到netty的异步模型
            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
            //对关闭通道事件  进行监听
            channelFuture.channel().closeFuture().sync();
        }finally {
            group.shutdownGracefully();
        }
    }
}

NettyClientHandler

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;

/**
 * 说明:
 * 1. 当通道就绪时会触发channelActive方法,用于向服务器发送初始消息。
 * 2. 当通道有读取事件时会触发channelRead方法,用于处理从服务器接收到的消息。
 * 3. 如果发生异常,会触发exceptionCaught方法,通常会在发生异常时关闭连接。
 */
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    // 当通道就绪时会触发该方法
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("Client " + ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("Hello, server: (>^ω^<)喵", CharsetUtil.UTF_8));
    }

    // 当通道有读取事件时会触发
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("Server's reply: " + buf.toString(CharsetUtil.UTF_8));
        System.out.println("Server's address: " + ctx.channel().remoteAddress());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}

5、Task使用场景

任务队列中的Task具有三种典型的使用场景,这些场景展示了Netty异步模型的优势和灵活性:

  1. 用户程序自定义的普通任务:在这种情况下,用户可以将自定义的任务提交到任务队列中,这些任务可以是耗时的操作,但由于Netty的异步模型,不会阻塞主线程。举例来说,可以在一个连接处理器中,将一些需要耗时处理的任务交给任务队列处理,以确保不会阻塞其他连接的处理。

  2. 用户自定义定时任务:Netty允许用户定义定时任务,这些任务会在一定的延迟后执行。这对于执行定期操作非常有用,例如定时向客户端发送心跳消息或执行其他周期性任务。

以下是前两种场景的示例代码:

// 解决方案1: 用户程序自定义的普通任务
ctx.channel().eventLoop().execute(new Runnable() {
    @Override
    public void run() {
        // 执行耗时操作
    }
});

// 解决方案2: 用户自定义定时任务
ctx.channel().eventLoop().schedule(new Runnable() {
    @Override
    public void run() {
        // 延迟一定时间后执行任务
    }
}, 5, TimeUnit.SECONDS);
  1. 非当前Reactor线程调用Channel的各种方法:有时候,需要在业务线程中处理某个特定连接的操作,例如向特定用户推送消息。这会导致非当前Reactor线程调用Channel的方法,这种情况下,Netty会将这些操作提交到任务队列中,以确保线程安全和异步执行。(外部线程调用:有时,其他部分的代码可能会在非当前Reactor线程上调用Netty的Channel方法,例如在业务线程中找到特定用户的连接并向其发送消息。这也属于非当前Reactor线程的情况。)

无论是定时任务、自定义任务还是外部线程调用,Netty都提供了机制来确保线程安全和异步执行,以避免对Reactor线程的阻塞和提高性能。任务队列和异步执行是Netty的核心特性,使其成为高性能和可扩展的网络编程框架。

6、案例:Netty HTTP服务

TestServer

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;

public class TestServer {
    public static void main(String[] args) throws Exception {

        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new TestServerInitializer());

            ChannelFuture channelFuture = serverBootstrap.bind(16668).sync();

            channelFuture.channel().closeFuture().sync();

        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

TestServerInitializer

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpServerCodec;


public class TestServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {

        //向管道加入处理器

        //得到管道
        ChannelPipeline pipeline = ch.pipeline();

        //加入一个netty 提供的httpServerCodec codec =>[coder - decoder]
        //HttpServerCodec 说明
        //1. HttpServerCodec 是netty 提供的处理http的 编-解码器
        pipeline.addLast("MyHttpServerCodec", new HttpServerCodec());
        //2. 增加一个自定义的handler
        pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());

        System.out.println("ok~~~~");

    }
}

TestHttpServerHandler

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;

import java.net.URI;

/*
说明
1. SimpleChannelInboundHandler 是 ChannelInboundHandlerAdapter
2. HttpObject 客户端和服务器端相互通讯的数据被封装成 HttpObject
 */
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {


    //channelRead0 读取客户端数据
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {


        System.out.println("对应的channel=" + ctx.channel() + " pipeline=" + ctx
                .pipeline() + " 通过pipeline获取channel" + ctx.pipeline().channel());

        System.out.println("当前ctx的handler=" + ctx.handler());

        //判断 msg 是不是 httprequest请求
        if (msg instanceof HttpRequest) {

            System.out.println("ctx 类型=" + ctx.getClass());

            System.out.println("pipeline hashcode" + ctx.pipeline().hashCode() + " TestHttpServerHandler hash=" + this.hashCode());

            System.out.println("msg 类型=" + msg.getClass());
            System.out.println("客户端地址" + ctx.channel().remoteAddress());

            //获取到
            HttpRequest httpRequest = (HttpRequest) msg;
            //获取uri, 过滤指定的资源
            URI uri = new URI(httpRequest.uri());
            if ("/favicon.ico".equals(uri.getPath())) {
                System.out.println("请求了 favicon.ico, 不做响应");
                return;
            }
            //回复信息给浏览器 [http协议]

            ByteBuf content = Unpooled.copiedBuffer("hello, 我是服务器", CharsetUtil.UTF_8);

            //构造一个http的相应,即 httpresponse
            FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);

            response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());

            //将构建好 response返回
            ctx.writeAndFlush(response);
        }
    }
}

7、Netty的核心模块和组件

  1. Bootstrap和ServerBootstrap:Bootstrap用于客户端的启动引导,而ServerBootstrap用于服务端的启动引导。它们用于配置和启动整个Netty应用程序,包括串联各个组件。

  2. Channel和ChannelFuture:Channel表示一个网络连接的通道,它可以用于执行网络I/O操作。ChannelFuture用于处理异步操作,可以注册监听器来处理操作成功、失败或取消时的事件。

  3. ChannelHandler:ChannelHandler是一个接口,用于处理I/O事件或拦截I/O操作,并将它们转发到ChannelPipeline中的下一个处理程序。通常需要自定义ChannelHandler来实现业务逻辑。

  4. ChannelPipeline:ChannelPipeline是一组ChannelHandler的集合,它负责处理和拦截入站和出站的事件和操作。它的作用是串联和管理ChannelHandler,允许用户完全控制事件的处理方式。

  5. Selector:Selector是Netty基于的多路复用机制,用于实现非阻塞I/O。它可以同时监听多个通道上的事件,以便高效地管理多个连接。

  6. EventLoopGroup:EventLoopGroup是一组EventLoop的抽象,用于管理多个EventLoop线程。在Netty中,通常会有两个EventLoopGroup,一个用于Boss线程,负责接受客户端连接,另一个用于Worker线程,负责处理I/O操作。

  7. ChannelOption:ChannelOption用于设置Channel的配置参数,例如接收缓冲区大小等。

  8. ByteBuf:ByteBuf是Netty用于操作缓冲区的工具类,它是数据容器,用于存储和处理数据。

8、案例:Netty 群聊系统

服务器端:可以监测用户上线,离线,并实现消息转发功能
客户端:通过 channel 可以非阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息

GroupChatServer

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class GroupChatServer {
    private int port; //监听端口
    public GroupChatServer(int port) {
        this.port = port;
    }
    //编写run方法,处理客户端的请求
    public void run() throws Exception {
        //创建两个线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8个NioEventLoop
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            //获取到pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            //向pipeline加入解码器
                            pipeline.addLast("decoder", new StringDecoder());
                            //向pipeline加入编码器
                            pipeline.addLast("encoder", new StringEncoder());
                            //加入自己的业务处理handler
                            pipeline.addLast(new GroupChatServerHandler());
                        }
                    });
            System.out.println("netty 服务器启动");
            ChannelFuture channelFuture = b.bind(port).sync();
            //监听关闭
            channelFuture.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new GroupChatServer(7000).run();
    }	
}

GroupChatServerHandler

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class GroupChatServerHandler extends SimpleChannelInboundHandler<String> {

    //这样写还要自己遍历Channel
    //public static List<Channel> channels = new ArrayList<Channel>();

    //使用一个hashmap 管理私聊(私聊本案例并未实现,只是提供个思路)
    //public static Map<String, Channel> channels = new HashMap<String,Channel>();

    //定义一个channle 组,管理所有的channel
    //GlobalEventExecutor.INSTANCE) 是全局的事件执行器,是一个单例
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");


    //handlerAdded 表示连接建立,一旦连接,第一个被执行
    //将当前channel 加入到  channelGroup
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        //将该客户加入聊天的信息推送给其它在线的客户端

        //该方法会将 channelGroup 中所有的channel 遍历,并发送消息,我们不需要自己遍历
        channelGroup.add(channel);
        channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 加入聊天" + sdf.format(new java.util.Date()) + " \n");


        //私聊如何实现
//         channels.put("userid100",channel);


    }

    //断开连接, 将xx客户离开信息推送给当前在线的客户
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

        Channel channel = ctx.channel();
        channelGroup.writeAndFlush("[客户端]" + channel.remoteAddress() + " 离开了\n");
        channelGroup.remove(channel);
        System.out.println("channelGroup size" + channelGroup.size());

    }

    //表示channel 处于活动状态, 提示 xx上线
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        //这个是给服务端看的,客户端上面已经提示xxx加入群聊了
        System.out.println(ctx.channel().remoteAddress() + " 上线了~");
    }

    //表示channel 处于不活动状态, 提示 xx离线了
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {

        System.out.println(ctx.channel().remoteAddress() + " 离线了~");
    }

    //读取数据,转发给在线的每一个客户端
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {

        //获取到当前channel
        Channel channel = ctx.channel();
        //这时我们遍历channelGroup, 根据不同的情况,回送不同的消息

        channelGroup.forEach(ch -> {
            if (channel != ch) { //不是当前的channel,转发消息
                ch.writeAndFlush("[客户]" + channel.remoteAddress() + " 发送了消息" + msg + "\n");
            } else {//回显自己发送的消息给自己
                ch.writeAndFlush("[自己]发送了消息" + msg + "\n");
            }
        });
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        //关闭通道
        ctx.close();
    }
}

GroupChatClient

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.util.Scanner;


public class GroupChatClient {

    //属性
    private final String host;
    private final int port;

    public GroupChatClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    public void run() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();

        try {


            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {

                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {

                            //得到pipeline
                            ChannelPipeline pipeline = ch.pipeline();
                            //加入相关handler
                            pipeline.addLast("decoder", new StringDecoder());
                            pipeline.addLast("encoder", new StringEncoder());
                            //加入自定义的handler
                            pipeline.addLast(new GroupChatClientHandler());
                        }
                    });

            ChannelFuture channelFuture = bootstrap.connect(host, port).sync();
            //得到channel
            Channel channel = channelFuture.channel();
            System.out.println("-------" + channel.localAddress() + "--------");
            //客户端需要输入信息,创建一个扫描器
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String msg = scanner.nextLine();
                //通过channel 发送到服务器端
                channel.writeAndFlush(msg + "\r\n");
            }
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new GroupChatClient("127.0.0.1", 7000).run();
    }
}

GroupChatClientHandler

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;

public class GroupChatClientHandler extends SimpleChannelInboundHandler<String> {

    //从服务器拿到的数据
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
        System.out.println(msg.trim());
    }
}

9、案例:Netty 心跳检测

MyServer

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

public class MyServer {
    public static void main(String[] args) throws Exception {


        //创建两个线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        // 并没有传入参数,因此默认会创建多个NioEventLoop(通常是CPU核心数的两倍)
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {

            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(bossGroup, workerGroup);
            serverBootstrap.channel(NioServerSocketChannel.class);
            //在bossGroup增加一个日志处理器
            serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {

                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();
                    //加入一个netty 提供 IdleStateHandler
                    /*
                    说明
                    1. IdleStateHandler 是netty 提供的处理空闲状态的处理器
                    2. long readerIdleTime : 表示多长时间没有读, 就会发送一个心跳检测包检测是否连接
                    3. long writerIdleTime : 表示多长时间没有写, 就会发送一个心跳检测包检测是否连接
                    4. long allIdleTime : 表示多长时间没有读写, 就会发送一个心跳检测包检测是否连接

                    5. 文档说明
                    triggers an {@link IdleStateEvent} when a {@link Channel} has not performed
                   read, write, or both operation for a while.
                    6. 当 IdleStateEvent 触发后 , 就会传递给管道 的下一个handler去处理,通过调用(触发)
                   下一个handler 的 userEventTiggered , 在该方法中去处理 IdleStateEvent(读空闲,写空闲,读写空闲)
                    7.handlerRemoved有时候是无法感知连接断掉,所以还是需要心跳包的检测来判断连接是否还有效
                     */
                    pipeline.addLast(new IdleStateHandler(3, 5, 7, TimeUnit.SECONDS));
                    //加入一个对空闲检测进一步处理的handler(自定义)
                    pipeline.addLast(new MyServerHandler());
                }
            });

            //启动服务器
            ChannelFuture channelFuture = serverBootstrap.bind(7000).sync();
            channelFuture.channel().closeFuture().sync();

        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

MyServerHandler

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleStateEvent;

public class MyServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * @param ctx 上下文
     * @param evt 事件
     * @throws Exception
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {

        if (evt instanceof IdleStateEvent) {

            //将  evt 向下转型 IdleStateEvent
            IdleStateEvent event = (IdleStateEvent) evt;
            String eventType = null;
            switch (event.state()) {
                case READER_IDLE:
                    eventType = "读空闲";
                    break;
                case WRITER_IDLE:
                    eventType = "写空闲";
                    break;
                case ALL_IDLE:
                    eventType = "读写空闲";
                    break;
            }
            System.out.println(ctx.channel().remoteAddress() + "--超时时间--" + eventType);
            System.out.println("服务器做相应处理..");

            //如果发生空闲,我们关闭通道
            // ctx.channel().close();
        }
    }
}

10、案例:WebSocket 服务器和客户端长连接

MyServer

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.stream.ChunkedWriteHandler;

public class MyServer {
    public static void main(String[] args) throws Exception {


        //创建两个线程组
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup(); //8个NioEventLoop
        try {

            ServerBootstrap serverBootstrap = new ServerBootstrap();

            serverBootstrap.group(bossGroup, workerGroup);
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.handler(new LoggingHandler(LogLevel.INFO));
            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {

                @Override
                protected void initChannel(SocketChannel ch) throws Exception {
                    ChannelPipeline pipeline = ch.pipeline();

                    //因为基于http协议,使用http的编码和解码器
                    pipeline.addLast(new HttpServerCodec());
                    //http是以块方式写,添加ChunkedWriteHandler处理器
                    pipeline.addLast(new ChunkedWriteHandler());

                    /*
                    说明
                    1. http数据在传输过程中是分段, HttpObjectAggregator ,就是可以将多个段聚合
                    2. 这就就是为什么,当浏览器发送大量数据时,就会发出多次http请求
                     */
                    pipeline.addLast(new HttpObjectAggregator(8192));
                    /*
                    说明
                    1. 对应websocket ,它的数据是以 帧(frame) 形式传递
                    2. 可以看到WebSocketFrame 下面有六个子类
                    3. 浏览器请求时 ws://localhost:7000/hello 表示请求的uri
                    4. WebSocketServerProtocolHandler 核心功能是将 http协议升级为 ws协议 , 保持长连接
                    5. 是通过一个 状态码 101
                     */
                    pipeline.addLast(new WebSocketServerProtocolHandler("/hello"));

                    //自定义的handler ,处理业务逻辑
                    pipeline.addLast(new MyTextWebSocketFrameHandler());
                }
            });

            //启动服务器
            ChannelFuture channelFuture = serverBootstrap.bind(7888).sync();
            channelFuture.channel().closeFuture().sync();

        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

MyTextWebSocketFrameHandler

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

import java.time.LocalDateTime;

//这里 TextWebSocketFrame 类型,表示一个文本帧(frame)
public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {

        System.out.println("服务器收到消息 " + msg.text());

        //回复消息
        ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器时间" + LocalDateTime.now() + " " + msg.text()));
    }

    //当web客户端连接后, 触发方法
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        //id 表示唯一的值,LongText 是唯一的 ShortText 不是唯一
        System.out.println("handlerAdded 被调用" + ctx.channel().id().asLongText());
        System.out.println("handlerAdded 被调用" + ctx.channel().id().asShortText());
    }


    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {

        System.out.println("handlerRemoved 被调用" + ctx.channel().id().asLongText());
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("异常发生 " + cause.getMessage());
        ctx.close(); //关闭连接
    }
}

页面

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<script>
    var socket;
    //判断当前浏览器是否支持websocket
    if (window.WebSocket) {
        //go on
        socket = new WebSocket("ws://localhost:7888/hello");
        //相当于channelReado, ev 收到服务器端回送的消息
        socket.onmessage = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = rt.value + "\n" + ev.data;
        }

        //相当于连接开启(感知到连接开启)
        socket.onopen = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = "连接开启了.."
        }

        //相当于连接关闭(感知到连接关闭)
        socket.onclose = function (ev) {

            var rt = document.getElementById("responseText");
            rt.value = rt.value + "\n" + "连接关闭了.."
        }
    } else {
        alert("当前浏览器不支持websocket")
    }

    //发送消息到服务器
    function send(message) {
        if (!window.socket) { //先判断socket是否创建好
            return;
        }
        if (socket.readyState == WebSocket.OPEN) {
            //通过socket 发送消息
            socket.send(message)
        } else {
            alert("连接没有开启");
        }
    }
</script>
<form onsubmit="return false">
    <textarea name="message" style="height: 300px; width: 300px"></textarea>
    <input type="button" value="发送消息" onclick="send(this.form.message.value)">
    <textarea id="responseText" style="height: 300px; width: 300px"></textarea>
    <input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
</form>
</body>
</html>

在这里插入图片描述
在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

qt初入门2:qt选择一个文件或者目录,获取当前目录,操作文件目录等整理

最近用qt操作文件或者目录的动作比较多&#xff0c;简单整理一下常用的接口&#xff0c;方便回顾。 总的来说&#xff0c;其实就是用文件选择对话框QFileDialog类&#xff0c;以及操作文件信息的QFileInfo类&#xff0c;以及相关QCoreApplication中静态成员函数获取一些信息&a…

对话姿美堂创始人董事长徐熙明:不受短期诱惑和外界噪音的影响,坚持做难而正确的事

“ 客户至上 ” 整理 | 云舒&凯丰 编辑 | 梦遥 出品&#xff5c;极新&#xff06;北京电子商务协会 在过去几年中&#xff0c;直播电商经历了显著的发展&#xff0c;成为数字化零售领域的一个重要分支。随着智能手机和高速互联网的普及&#xff0c;消费者的购物习惯发生…

聚焦老年生活与健康,“老有所依·情暖夕阳”元岗街社区微型养老博览会顺利开展

尊老敬老是中华民族的传统美德&#xff0c; 爱老助老是全社会的共同责任。 家有一老&#xff0c;如有一宝&#xff0c; 长者的生活情况是一个家庭的头等大事&#xff0c; 做好长者服务是街道和社区的重要工作。 2024年1月6日&#xff0c;由元岗街道党工委、元岗街道办事处、…

OpenGl 19高级GLSL

一.GLSL的内建变量 在着色器中&#xff0c;需要当前着色器以外地方的数据的话&#xff0c;必须把数据传进来。之前我们是通过uniform类型和采样器来完成的。之外&#xff0c;GLSL还支持另外几个以gl为前缀的变量&#xff0c;提供更多读写数据的方式&#xff0c;比如说顶点着色…

ES高级查询

ES中提供了一种强大的检索数据方式&#xff0c;这种检索方式称为Query DSL&#xff0c;这种方式的丰富查询语法让ES检索变得更强大&#xff0c;更简洁。 1.常见查询 1.1查询所有[match_all] match_all关键字&#xff1a;返回索引中的全部文档。 GET /products/_search { &…

acwing4986.互质数的个数

题目不难 有个好的细节想着分享一下 一开始写的有点问题&#xff5e;需要特判掉一个... #include<bits/stdc.h> using namespace std; using ll long long; const int N 1e510;const ll mod 998244353;ll qmi(ll a,ll b){ll ans 1;while(b){if(b&1)ans ans*a%…

(20)Linux初始文件描述符

前言&#xff1a;本章我们介绍 O_WRONLY, O_TRUNC, O_APPEND 和 O_RDONLY。之后我们开始讲解文件描述符。 一、系统传递标记位 1、O_WRONLY C 语言在 w 模式打开文件时&#xff0c;文件内容是会被清空的&#xff0c;但是 O_WRONLY 好像并非如此&#xff1f; 代码演示&…

H264码流进行RTP包封装

一.H264基本概念 H.264从框架结构上分为视频编码层&#xff08;VCL&#xff09;和网络抽象层&#xff08;NAL&#xff09;&#xff0c;VCL功能是进行视频编解码&#xff0c;包括运动补偿预测&#xff0c;变换编码和熵编码等功能&#xff1b;NAL用于采用适当的格式对VCL视频数据…

【竞技宝】DOTA2:梦幻联赛开战在即 中国区前两名将晋级正赛

北京时间2024年1月12日&#xff0c;近期DOTA2刚刚结束了别墅杯东南亚/中国区的封闭预选赛&#xff0c;而别墅杯的正赛还要等到下个月才会正式开打&#xff0c;而即将在明天开始进行的是梦幻联赛S22的中国区预选赛&#xff0c;除官方直邀的XG战队直接晋级正赛之外&#xff0c;其…

【Leetcode】2085. 统计出现过一次的公共字符串

文章目录 题目思路代码 题目 2085. 统计出现过一次的公共字符串 思路 使用两个哈希表 words1Count 和 words2Count 分别统计两个数组中每个单词的出现次数。然后遍历 words1Count 中的每个单词&#xff0c;如果该单词在 words1 中出现了一次&#xff0c;且在 words2 中也出…

深入理解计算机系统(2):信息的表示和处理

信息存储 大多数计算机使用 8 位的块&#xff0c;或者字节(byte)&#xff0c;作为最小的可寻址的内存单位&#xff0c;而不是访问内存中单独的位。机器级程序将内存视为一个非常大的字节数组&#xff0c;称为虚拟内存(virtual memory)。内存的每个字节都由一个唯一的数字来标识…

地表最强,接口调试神器Postman ,写得太好了!

postman是一款支持http协议的接口调试与测试工具&#xff0c;其主要特点就是功能强大&#xff0c;使用简单且易用性好 。 无论是开发人员进行接口调试&#xff0c;还是测试人员做接口测试&#xff0c;postman都是我们的首选工具之一 。 那么接下来就介绍下postman到底有哪些功…

连续多级主管

背景 组织中一般会有个直接主管&#xff0c;或者汇报主管&#xff0c;有的组织可能有多个主管&#xff0c;更有甚者一个人能可能在不同的业务项目中&#xff0c;这样这个人可能存在n个主管&#xff0c;这样在设计流程中就会衍生出很多问题来。一起看一款审批软件的设置&#x…

加入 The Sandbox,在「小王子:友谊与慈善的冒险」中踏上奇幻旅途

这次体验不仅是一次冒险&#xff0c;更是一次充满爱心的愉悦探索。此外&#xff0c;您购买的 NFT 还将用于慈善教育项目&#xff1a;它由安托万德圣埃克苏佩里青少年基金会资助&#xff0c;其中一部分销售收入将用于支持这个慈善机构。这次寓教于乐的旅程交织着对爱、友谊的思考…

MySQL比较运算符详解

MySQL比较运算符详解 一、常用的比较运算符二、比较运算符的使用方法2.1 等于运算符&#xff08;&#xff09;2.2 不等于运算符&#xff08;<>或!&#xff09;2.3 大于运算符&#xff08;>&#xff09;2.4 小于运算符&#xff08;<&#xff09;2.5 大于等于运算符&…

开源云真机平台-Sonic实际使用过程踩坑及解决方法(持续更新)

开源云真机平台-Sonic实际使用过程踩坑及解决方法(持续更新) 1、执行Python 自定义脚本时requests文件报错 Script stderrTraceback (most recent call last): File "D:TestToolssonic-agent-2.6 2-windows x86 64 onic-ent-v2.6.2-windows X86 64tes-utDut2b713b90-493-…

【MFC实践】基于MFC向导C++制作计算器(附文件)

一、写在前面1.1 什么是MFC向导&#xff1f;1.2 使用MFC向导制作计算器1.3安装visual studio 2022和MFC插件 二、设计计算器界面1.1 新创建MFC项目1.2 设计计算器界面1.3 添加相关变量1.4 算法的一些问题及解决方式1.5 计算功能的实现1.6 其它功能的实现1.6.1 DEL功能1.6.2 C置…

Linux——firewalld防火墙(二)

一、firewalld高级配置 1、IP地址伪装 地址伪装&#xff08;masquerade):通过地址伪装&#xff0c;NAT设备将经过设备的包转发到指定接收方&#xff0c;同时将通过的数据包的源地址更改为其自己的接口地址。当返回的数据包到达时&#xff0c;会将目的地址修改为原始主机的地址…

(26)Linux 进程通信之共享内存(共享储存空间)

共享内存是System V版本的最后一个进程间通信方式。共享内存&#xff0c;顾名思义就是允许两个不相关的进程访问同一个逻辑内存&#xff0c;共享内存是两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常为同一段物理内存。进程可以将同一…

6.4、SDN在云数据中心的应用案例分析

云数据中心中的虚拟子网包含网关和IP网段,IP分配给各个服务器,服务器间能够互相通信或通过网关访问外部网络。 在SDN云数据中心内,用户可以随时订购任意网段的虚拟子网,而且这些子网是可以在不同用户之间复用的,也就是说,不同用户可以使用相同的私有网段。 SDN云数据中心…