Netty

目录

引言:

什么是Netty?

Netty和Tomcat有什么区别?

为什么Netty受欢迎?

Netty为什么并发高

Netty为什么传输快

为什么说Netty封装好?

使用示例:

[步骤1: 添加Netty依赖](#步骤1: 添加Netty依赖)

[步骤2: 创建服务器启动类](#步骤2: 创建服务器启动类)

[步骤3: 创建服务器初始化类](#步骤3: 创建服务器初始化类)

[步骤4: 创建服务器处理器类](#步骤4: 创建服务器处理器类)

结论:


引言:

什么是Netty?

Netty 是一个利用 Java 的高级网络的能力,隐藏其背后的复杂性而提供一个易于使用的 API 的客户端/服务器框架。

Netty 是一个广泛使用的 Java 网络编程框架(Netty 在 2011 年获得了Duke's Choice Award,见https://www.java.net/dukeschoice/2011)。它活跃和成长于用户社区,像大型公司 Facebook 和 Instagram 以及流行 开源项目如 Infinispan, HornetQ, Vert.x, Apache Cassandra 和 Elasticsearch 等,都利用其强大的对于网络抽象的核心代码。

Netty和Tomcat有什么区别?

Netty和Tomcat最大的区别就在于通信协议,Tomcat是基于Http协议的,他的实质是一个基于http协议的web容器,但是Netty不一样,他能通过编程自定义各种协议,因为netty能够通过codec自己来编码/解码字节流,完成类似redis访问的功能,这就是netty和tomcat最大的不同。

有人说netty的性能就一定比tomcat性能高,其实不然,tomcat从6.x开始就支持了nio模式,并且后续还有APR模式------一种通过jni调用apache网络库的模式,相比于旧的bio模式,并发性能得到了很大提高,特别是APR模式,而netty是否比tomcat性能更高,则要取决于netty程序作者的技术实力了。

为什么Netty受欢迎?

如第一部分所述,netty是一款收到大公司青睐的框架,在我看来,netty能够受到青睐的原因有三:

  1. 并发高
  2. 传输快
  3. 封装好

Netty为什么并发高

Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高,两张图让你了解BIO和NIO的区别:

阻塞IO的通信方式:

非阻塞IO的通信方式

从这两图可以看出,NIO的单线程能处理连接的数量比BIO要高出很多,而为什么单线程能处理更多的连接呢?原因就是图二中出现的Selector

当一个连接建立之后,他有两个步骤要做,第一步是接收完客户端发过来的全部数据,第二步是服务端处理完请求业务之后返回response给客户端。NIO和BIO的区别主要是在第一步。

在BIO中,等待客户端发数据这个过程是阻塞的,这样就造成了一个线程只能处理一个请求的情况,而机器能支持的最大线程数是有限的,这就是为什么BIO不能支持高并发的原因。

而NIO中,当一个Socket建立好之后,Thread并不会阻塞去接受这个Socket,而是将这个请求交给Selector,Selector会不断的去遍历所有的Socket,一旦有一个Socket建立完成,他会通知Thread,然后Thread处理完数据再返回给客户端------这个过程是不阻塞的 ,这样就能让一个Thread处理更多的请求了。

下面两张图是基于BIO的处理流程和netty的处理流程,辅助你理解两种方式的差别:

BIO的处理流程

NIO的处理流程

除了BIO和NIO之外,还有一些其他的IO模型,下面这张图就表示了五种IO模型的处理流程:

五种常见的IO模型

  • BIO,同步阻塞IO,阻塞整个步骤,如果连接少,他的延迟是最低的,因为一个线程只处理一个连接,适用于少连接且延迟低的场景,比如说数据库连接。
  • NIO,同步非阻塞IO,阻塞业务处理但不阻塞数据接收,适用于高并发且处理简单的场景,比如聊天软件。
  • 多路复用IO,他的两个步骤处理是分开的,也就是说,一个连接可能他的数据接收是线程a完成的,数据处理是线程b完成的,他比BIO能处理更多请求。
  • 信号驱动IO,这种IO模型主要用在嵌入式开发,不参与讨论。
  • 异步IO,他的数据请求和数据处理都是异步的,数据请求一次返回一次,适用于长连接的业务场景。

以上摘自Linux IO模式及 select、poll、epoll详解

Netty为什么传输快

Netty的传输快其实也是依赖了NIO的一个特性------零拷贝 。我们知道,Java的内存有堆内存、栈内存和字符串常量池等等,其中堆内存是占用内存空间最大的一块,也是Java对象存放的地方,一般我们的数据如果需要从IO读取到堆内存,中间需要经过Socket缓冲区,也就是说一个数据会被拷贝两次才能到达他的的终点,如果数据量大,就会造成不必要的资源浪费。

Netty针对这种情况,使用了NIO中的另一大特性------零拷贝,当他需要接收数据的时候,他会在堆内存之外开辟一块内存,数据就直接从IO读到了那块内存中去,在netty里面通过ByteBuf可以直接对这些数据进行直接操作,从而加快了传输速度。

下两图就介绍了两种拷贝方式的区别,摘自Linux 中的零拷贝技术,第 1 部分

传统数据拷贝:

零拷贝:

上文介绍的ByteBuf是Netty的一个重要概念,他是netty数据处理的容器,也是Netty封装好的一个重要体现,将在下一部分做详细介绍。

为什么说Netty封装好?

要说Netty为什么封装好,这种用文字是说不清的,直接上代码:

  • 阻塞I/O

    public class PlainOioServer {

    复制代码
      public void serve(int port) throws IOException {
          final ServerSocket socket = new ServerSocket(port);     //1
          try {
              for (;;) {
                  final Socket clientSocket = socket.accept();    //2
                  System.out.println("Accepted connection from " + clientSocket);
    
                  new Thread(new Runnable() {                        //3
                      @Override
                      public void run() {
                          OutputStream out;
                          try {
                              out = clientSocket.getOutputStream();
                              out.write("Hi!\r\n".getBytes(Charset.forName("UTF-8")));                            //4
                              out.flush();
                              clientSocket.close();                //5
    
                          } catch (IOException e) {
                              e.printStackTrace();
                              try {
                                  clientSocket.close();
                              } catch (IOException ex) {
                                  // ignore on close
                              }
                          }
                      }
                  }).start();                                        //6
              }
          } catch (IOException e) {
              e.printStackTrace();
          }
      }

    }

  • 非阻塞IO

    public class PlainNioServer {
    public void serve(int port) throws IOException {
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    serverChannel.configureBlocking(false);
    ServerSocket ss = serverChannel.socket();
    InetSocketAddress address = new InetSocketAddress(port);
    ss.bind(address); //1
    Selector selector = Selector.open(); //2
    serverChannel.register(selector, SelectionKey.OP_ACCEPT); //3
    final ByteBuffer msg = ByteBuffer.wrap("Hi!\r\n".getBytes());
    for (;;) {
    try {
    selector.select(); //4
    } catch (IOException ex) {
    ex.printStackTrace();
    // handle exception
    break;
    }
    Set<SelectionKey> readyKeys = selector.selectedKeys(); //5
    Iterator<SelectionKey> iterator = readyKeys.iterator();
    while (iterator.hasNext()) {
    SelectionKey key = iterator.next();
    iterator.remove();
    try {
    if (key.isAcceptable()) { //6
    ServerSocketChannel server =
    (ServerSocketChannel)key.channel();
    SocketChannel client = server.accept();
    client.configureBlocking(false);
    client.register(selector, SelectionKey.OP_WRITE |
    SelectionKey.OP_READ, msg.duplicate()); //7
    System.out.println(
    "Accepted connection from " + client);
    }
    if (key.isWritable()) { //8
    SocketChannel client =
    (SocketChannel)key.channel();
    ByteBuffer buffer =
    (ByteBuffer)key.attachment();
    while (buffer.hasRemaining()) {
    if (client.write(buffer) == 0) { //9
    break;
    }
    }
    client.close(); //10
    }
    } catch (IOException ex) {
    key.cancel();
    try {
    key.channel().close();
    } catch (IOException cex) {
    // 在关闭时忽略
    }
    }
    }
    }
    }
    }

  • Netty

    public class NettyOioServer {

    复制代码
      public void server(int port) throws Exception {
          final ByteBuf buf = Unpooled.unreleasableBuffer(
                  Unpooled.copiedBuffer("Hi!\r\n", Charset.forName("UTF-8")));
          EventLoopGroup group = new OioEventLoopGroup();
          try {
              ServerBootstrap b = new ServerBootstrap();        //1
    
              b.group(group)                                    //2
               .channel(OioServerSocketChannel.class)
               .localAddress(new InetSocketAddress(port))
               .childHandler(new ChannelInitializer<SocketChannel>() {//3
                   @Override
                   public void initChannel(SocketChannel ch) 
                       throws Exception {
                       ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {            //4
                           @Override
                           public void channelActive(ChannelHandlerContext ctx) throws Exception {
                               ctx.writeAndFlush(buf.duplicate()).addListener(ChannelFutureListener.CLOSE);//5
                           }
                       });
                   }
               });
              ChannelFuture f = b.bind().sync();  //6
              f.channel().closeFuture().sync();
          } finally {
              group.shutdownGracefully().sync();        //7
          }
      }

    }

从代码量上来看,Netty就已经秒杀传统Socket编程了,但是这一部分博大精深,仅仅贴几个代码岂能说明问题,在这里给大家介绍一下Netty的一些重要概念,让大家更理解Netty。

  • Channel

    数据传输流,与channel相关的概念有以下四个,上一张图让你了解netty里面的Channel。

    Channel一览:

  • Channel,表示一个连接,可以理解为每一个请求,就是一个Channel。
  • ChannelHandler,核心处理业务就在这里,用于处理业务请求。
  • ChannelHandlerContext,用于传输业务数据。
  • ChannelPipeline,用于保存处理过程需要用到的ChannelHandler和ChannelHandlerContext。
  • ByteBuf
    ByteBuf是一个存储字节的容器,最大特点就是使用方便,它既有自己的读索引和写索引,方便你对整段字节缓存进行读写,也支持get/set,方便你对其中每一个字节进行读写,他的数据结构如下图所示:

ByteBuf数据结构

他有三种使用模式:

  1. Heap Buffer 堆缓冲区
    堆缓冲区是ByteBuf最常用的模式,他将数据存储在堆空间。
  2. Direct Buffer 直接缓冲区
    直接缓冲区是ByteBuf的另外一种常用模式,他的内存分配都不发生在堆,jdk1.4引入的nio的ByteBuffer类允许jvm通过本地方法调用分配内存,这样做有两个好处
    • 通过免去中间交换的内存拷贝, 提升IO处理速度; 直接缓冲区的内容可以驻留在垃圾回收扫描的堆区以外。
    • DirectBuffer 在 -XX:MaxDirectMemorySize=xxM大小限制下, 使用 Heap 之外的内存, GC对此"无能为力",也就意味着规避了在高负载下频繁的GC过程对应用线程的中断影响.
  3. Composite Buffer 复合缓冲区
    复合缓冲区相当于多个不同ByteBuf的视图,这是netty提供的,jdk不提供这样的功能。

除此之外,他还提供一大堆api方便你使用,在这里我就不一一列出了,具体参见ByteBuf字节缓存

  • Codec
    Netty中的编码/解码器,通过他你能完成字节与pojo、pojo与pojo的相互转换,从而达到自定义协议的目的。
    在Netty里面最有名的就是HttpRequestDecoder和HttpResponseEncoder了。

使用示例:

提供一个实际的使用示例,演示如何使用Netty实现一个简单的服务器。从创建服务器启动类开始,解释各个步骤的作用并给出具体的代码示例。说明如何配置ChannelInitializer和添加自定义的业务处理器。

下面是使用Netty实现基本服务器的代码步骤示例:

步骤1: 添加Netty依赖

在项目的构建文件(如Maven或Gradle)中,添加Netty的依赖。

java 复制代码
<dependencies>
    <dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-all</artifactId>
        <version>4.1.66.Final</version>
    </dependency>
</dependencies>

步骤2: 创建服务器启动类

创建一个Java类作为服务器的启动类。

java 复制代码
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 Server {
    private final int port;

    public Server(int port) {
        this.port = port;
    }

    public void start() throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap bootstrap = new ServerBootstrap();
            bootstrap.group(bossGroup, workerGroup)
                     .channel(NioServerSocketChannel.class)
                     .childHandler(new ServerInitializer());

            ChannelFuture future = bootstrap.bind(port).sync();
            future.channel().closeFuture().sync();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        int port = 8080; // 设置服务器监听的端口
        new Server(port).start();
    }
}

步骤3: 创建服务器初始化类

创建一个ChannelInitializer的子类,用于配置服务器的ChannelPipeline。

java 复制代码
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

public class ServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();

        // 添加字符串编码和解码器
        pipeline.addLast(new StringEncoder());
        pipeline.addLast(new StringDecoder());

        // 添加自定义的业务处理器
        pipeline.addLast(new ServerHandler());
    }
}

步骤4: 创建服务器处理器类

创建一个ChannelInboundHandlerAdapter的子类来处理接收到的请求。

java 复制代码
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

public class ServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        // 处理接收到的请求
        String request = (String) msg;
        System.out.println("Received request: " + request);

        // 发送响应
        String response = "Hello, Client!";
        ctx.writeAndFlush(response);
    }

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

以上代码示例演示了如何使用Netty实现一个简单的基于字符串通信的服务器。你可以根据需要自定义和扩展这些代码,以适应你的具体业务需求。

  • Netty的应用场景:列举和解释Netty在实际项目中的常见应用场景。例如,网络通信、实时数据传输、分布式系统和物联网等。描述每个场景的特点和要求,并说明Netty如何满足这些需求。

  • 性能优化:详细介绍如何对使用Netty构建的应用程序进行性能优化。提供一些实用的技巧和建议,例如使用连接池、优化线程模型、调整缓冲区大小、合理处理异常和错误等。

  • 实践经验分享:分享一些在实际项目中使用Netty的经验教训和最佳实践。从错误处理、线程安全性到跨平台兼容性,分享一些学习到的经验和解决方案,避免一些常见的陷阱和问题。

结论:

总结Netty的优点和适用情况。强调Netty作为一个强大而灵活的网络应用程序框架,在构建高性能和可靠的网络应用程序时的重要性。鼓励读者深入学习和尝试使用Netty来提升自己的开发能力。

相关推荐
cjy00011143 分钟前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本2 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji34162 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan2 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer3 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor3563 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor3563 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer4 小时前
Spring BOOT 启动参数
java·spring boot·后端
子木HAPPY阳VIP5 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪
人间打气筒(Ada)5 小时前
如何基于 Go-kit 开发 Web 应用:从接口层到业务层再到数据层
开发语言·后端·golang