Netty(应用场景,入门案例,架构模型BIO和Reactor,核心组件剖析)

本文详细介绍了Netty的异步事件驱动架构,从基础的NIO原理到实战案例,包括服务端与客户端的搭建,以及高性能架构设计,探讨了Java IO模型、Reactor线程模型和ByteBuf缓冲区的使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Netty通信技术进阶

1. Netty介绍

1.1. Netty简介

Netty是由JBOSS提供的一个java开源框架。是一个基于NIO的客户、服务器端编程框架,用以快速开发高性能、高可靠性的网络服务器和客户端程序

官网
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.

1.1.1. 异步、事件驱动

同步、异步是相对的,在请求或执行过程中,如果会阻塞等待,就是同步操作,反之就是异步操作。
在这里插入图片描述
在我们熟悉的Ajax请求,就是异步并且是基于事件驱动的:

$(function(){ 
  var list = {}; 
  $.ajax({ 
  //请求方式 
  type : "POST",

  //请求的媒体类型 
  contentType: "application/json;charset=UTF-8", 
  //请求地址 
  url : "http://127.0.0.1/admin/list/", 
  //数据,json字符串 
  data : JSON.stringify(list), 
  //请求成功 
  success : function(result) { 
  console.log(result); 
  },
  //请求失败,包含具体的错误信息 
  error : function(e){ 
  console.log(e.status); 
  console.log(e.responseText); 
  } 
 }); 
});
1.1.2. 核心架构

在这里插入图片描述

  • 核心
    可扩展的事件模型
    统一的通信api
    无论是http还是socket都使用统一的api,简化了操作
    零拷贝机制与字节缓冲区
  • 传输服务
    支持socket以及datagram(数据报)
    支持http协议
    In-VM Pipe (管道协议)
  • 协议支持
    http 以及 websocket
    SSL 安全套接字协议支持
    Google Protobuf (序列化框架)
    支持zlib、gzip压缩
    支持大文件的传输
    RTSP(实时流传输协议,是TCP/IP协议体系中的一个应用层协议)
    支持二进制协议并且提供了完整的单元测试
1.1.3. Netty优势
  • Netty是基于Java的NIO实现的,Netty将各种传输类型、协议的实现API进行了统一封装,实现了 阻塞和非阻塞Socket。
  • 基于事件模型实现,可以清晰的分离关注点,让开发者可以聚焦业务,提升开发效率。
  • 高度可定制的线程模型-单线程、一个或多个线程池,如SEDA(Staged Event-DrivenArchitecture):
    SEDA:把一个请求处理过程分成几个Stage,不同资源消耗的Stage使用不同数量的线程来处理,Stage期间使用事件驱动的异步通信模式。
  • Netty只依赖了JDK底层api,没有其他的依赖,如:Netty 3.X依赖JDK5以上,Netty4.x依赖JDK6以上。
  • Netty在网络通信方面更加的高性能、低延迟,尽可能的减少不必要的内存拷贝,提高性能。

1.2. Netty应用场景

Netty的应用场景是非常广泛的,比如:互联网行业的、游戏行业、大数据行业、医疗行业、金融等行业。

  • 互联网行业

在互联网行业项目中,最具代表性的就是分布式系统架构的远程服务调用,通过RPC的方式进行高性能的服务调用,目前主流的RPC框架底层均采用了Netty作为网络通信组件。
比如:阿里巴巴的分布式服务治理框架Dubbo,底层就是使用Netty作为通信组件。
gRPC:是Google提供的高性能RPC框架,底层也使用了Netty。

  • 游戏行业

市场越来越多的主流大型网络游戏的服务器, 都采用JAVA进行开发, Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈,非常方便定制和开发私有协议栈。账号登陆服务器、地图服务器之间可以方便的通过 Netty 进行高性能的通信。

  • 大数据行业

大数据行业中的许多技术也采用了Netty作为通信组件,如:Flink、Spark、Elasticsearch等
官方列出了使用Netty的一些项目
在这里插入图片描述

1.3. Netty VS 原生NIO

JDK 原生也有一套网络应用程序 API,但使用繁杂, 不够完善, 需要消耗开发者较多精力去完善整个
NIO场景。
在网络编程方面,一般都不会选择原生的NIO,而是会选择Netty、Mina等封装后的框架,主要原因
是:

  • NIO的类库和API繁杂,使用麻烦,需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等。
  • 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。这是因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序。
  • 可靠性能力补齐,工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等问题,NIO编程的特点是功能开发相对容易,但是可靠性能力 补齐的工作量和难度都非常大。
  • JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官 方声称在JDK1.6版本的update18修复了该问题,但是直到JDK 1.7版本该问题仍旧存在,只不过 该BUG发生概率降低了一些而已,它并没有得到根本性解决。
    具体问题

2. Netty入门案例

开发环境:JDK8 + Idea

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.itcast.myrpc</groupId>
    <artifactId>itcast-MyRPC</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.50.Final</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <!-- java编译插件 -->
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.2</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

2.2. Netty服务端实现

2.2.1. Netty服务端启动配置

MyRPCServer实现:

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 MyRPCServer {

    /**
     * 服务启动
     */
    public void start(int port) throws Exception{

        //主线程,负责客户端的连接的建立,它不处理业务逻辑
        EventLoopGroup boss = new NioEventLoopGroup(1);

        // 工作线程,默认的线程数为:cpu核数*2
        EventLoopGroup worker = new NioEventLoopGroup(2);

        try {
            // 服务启动辅助对象
            ServerBootstrap serverBootstrap = new ServerBootstrap();
           //设置线程组
            serverBootstrap.group(boss, worker); 
            //配置server的通道类型
            serverBootstrap.channel(NioServerSocketChannel.class) 
                    //worker处理器
                    .childHandler(new MyChannelInitializer()); 

            //ByteBuf 的分配要设置为非池化,否则不能切换到堆缓冲器模式
//            serverBootstrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT);

            //绑定端口,启动服务
            ChannelFuture future = serverBootstrap.bind(port).sync();

            System.out.println("服务器启动成功,端口为:" + port);

            //等待服务关闭
            future.channel().closeFuture().sync();
        } finally {
            //优雅的关闭
            boss.shutdownGracefully();
            worker.shutdownGracefully();
        }

    }

}

实现流程:

  1. 主线程配置,不处理任何业务逻辑,只是接收客户的连接请求。
  2. 工作线程配置,线程数默认是:cpu*2。
  3. 创建服务器启动类。
  4. 服务端配置: 设置线程组、配置server通道、worker线程的处理器。
  5. 同步阻塞方式启动服务端, 防止自动关闭。
  6. Netty优雅关闭
2.2.2. Netty通道初始化

MyChannelInitializer通道的初始化管理器:

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;

public class MyChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        //把handler组成一个管道  将业务处理器加入到列表中
        ch.pipeline().addLast(new MyChannelHandler());
    }
}

2.2.3. Netty通道接收处理器

MyChannelHandler连接通道接收处理器:

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;

public class MyChannelHandler extends ChannelInboundHandlerAdapter {
    
    //获取客户端发来的数据
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;

        //获取数据
        String msgStr = byteBuf.toString(CharsetUtil.UTF_8);

        System.out.println("接收到消息:" + msgStr);

        //给客户端响应ok
        ctx.writeAndFlush(Unpooled.copiedBuffer("ok", CharsetUtil.UTF_8));

        ctx.fireChannelRead(byteBuf);// 向下传递,自动释放

//        ReferenceCountUtil.release(byteBuf); //手动释放资源

//        ctx.alloc();

//        ctx.channel().alloc()
    }
    //异常处理
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
        ctx.close();
    }
}
2.2.4. Netty服务端测试
import org.junit.Test;

public class TestServer {

    @Test
    public void testStart() throws Exception{

//        System.setProperty("io.netty.noUnsafe", "true"); //netty中IO操作都是基于Unsafe完成的
//        System.setProperty("io.netty.allocator.type", "unpooled");

        MyRPCServer myRPCServer = new MyRPCServer();

        myRPCServer.start(5566);
    }
}

在这里插入图片描述
可以看到,客户端发送数据到服务端。

2.3 客户端

2.3.1 Netty客户端启动配置

MyRPCClient:

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

public class MyRPCClient {

    /**
     * 客户端启动
     */
    public void start(String host, int port) throws Exception {


        // 工作线程,默认的线程数为:cpu核数*2
        EventLoopGroup worker = new NioEventLoopGroup(2);

        try {
            // 客户端启动辅助对象
            Bootstrap bootstrap = new Bootstrap();
            //设置线程组
            bootstrap.group(worker);
            //配置通道类型
            bootstrap.channel(NioSocketChannel.class)
                    //worker处理器
                    .handler(new MyChannelHandler());

            //连接服务端
            ChannelFuture future = bootstrap.connect(host, port);

            System.out.println("连接服务端成功:" + port);

            //等待服务关闭
            future.channel().closeFuture().sync();
        } finally {
            //优雅的关闭
            worker.shutdownGracefully();
        }

    }
}

实现流程:

  1. 定义工作线程组
  2. 通过Bootstrap方式创建客户端
  3. 连接到远程服务
  4. 同步阻塞方式启动服务
2.3.2 Netty通道接收处理器

MyClientHandler:

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

public class MyChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {

    /**
     * 服务端发来的消息
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        System.out.println("接收到服务端发来的消息:" + msg.toString(CharsetUtil.UTF_8));
    }

    /**
     * 向服务端发送消息
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String msg = "hello netty!";
        ctx.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
    }

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

2.3.3 Netty客户端测试
import org.junit.Test;

public class TestClient {

    @Test
    public void testSend() throws Exception{
        MyRPCClient client = new MyRPCClient();
        client.start("127.0.0.1", 5566);
    }
}

客户端
在这里插入图片描述
服务端
在这里插入图片描述

3. Netty的高性能架构设计

3.1. Java中的IO模型

在JDK1.4之前,基于Java所有的socket通信都采用了同步阻塞模型(BIO),这种模型性能低下,当时大型的服务均采用C或C++开发,因为它们可以直接使用操作系统提供的异步IO或者AIO,使得性能得到大幅提升。
2002年,JDK1.4发布,新增了java.nio包,提供了许多异步IO开发的API和类库。新增的NIO,极大的促进了基于Java的异步非阻塞的发展和应用。
2011年,JDK7发布,将原有的NIO进行了升级,称为NIO2.0,其中也对AIO进行了支持。

3.1.1. BIO模型

java中的BIO是blocking I/O的简称,它是同步阻塞型IO,其相关的类和接口在java.io下。
BIO模型简单来讲,就是服务端为每一个请求都分配一个线程进行处理,如下
在这里插入图片描述

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;


public class BIOServer {

    public static void main(String[] args) throws IOException {
        //创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        //创建线程池
        ExecutorService executorService = Executors.newCachedThreadPool();

        //循环处理链接对象
        while (true){
            System.out.println("阻塞等待客户端链接!");
            Socket socket = serverSocket.accept();

            //读取数据
            executorService.execute(()->{
                try {
                    //获取数据流
                    InputStream is = socket.getInputStream();
                    //定义缓冲区
                    byte[] buffer = new byte[1024];
                    //循环读取数据
                    int len = 0;
                    while ((len=is.read(buffer))!=-1){
                        System.out.println(new String(buffer,0,len,"UTF-8"));
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }

}

这种模式存在的问题:

  • 客户端的并发数与后端的线程数成1:1的比例,线程的创建、销毁是非常消耗系统资源的,随着并发量增大,服务端性能将显著下降,甚至会发生线程堆栈溢出等错误。
  • 当连接创建后,如果该线程没有操作时,会进行阻塞操作,这样极大的浪费了服务器资源。
3.1.2. NIO模型

NIO,称之为New IO 或是 non-block IO (非阻塞IO),这两种说法都可以,其实称之为非阻塞IO更恰当一些。
NIO相关的代码都放在了java.nio包下,其三大核心组件:Buffer(缓冲区)、Channel(通道)、Selector(选择器/多路复用器)

  • Buffer

在NIO中,所有的读写操作都是基于缓冲区完成的,底层是通过数组实现的,常用的缓冲区是ByteBuffer,每一种java基本类型都有对应的缓冲区对象(除了Boolean类型),如:CharBuffer、IntBuffer、LongBuffer等。

读写步骤:
写入数据到buffer
调用flip()方法
从Buffer中读取数据
调用clear()或者compact()方法

三个属性 capatity position limit

  • Channel

在BIO中是基于Stream实现,而在NIO中是基于通道实现,与流不同的是,通道是双向的,既可以读也可以写。

  • Selector

Selector是多路复用器,它会不断的轮询注册在其上的Channel,如果某个Channel上发生读或写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey获取就绪Channel的集合,进行IO的读写操作。
在这里插入图片描述
可以看出,NIO模型要优于BIO模型,主要是:
通过多路复用器就可以实现一个线程处理多个通道,避免了多线程之间的上下文切换导致系统开销过大。
NIO无需为每一个连接开一个线程处理,并且只有通道真正有有事件时,才进行读写操作,这样大大的减少了系统开销。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;


public class NIOServer {

    /***
     * 向Selector注册Channel
     */
    public Selector getSelector() throws Exception{
        //创建Selector
        Selector selector = Selector.open();

        //创建Channel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);//非阻塞
        
        //创建Socket,并绑定指定端口
        ServerSocket socket = serverSocketChannel.socket();
        socket.bind(new InetSocketAddress(6677));

        //向Selector注册Channel,事件为连接准备就绪事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        return selector;
    }


    /***
     * 循环处理每个Channel事件
     */
    public void listen() throws Exception {
        Selector selector = getSelector();
        //循环处理每个Channel
        while (true){
            //该方法会阻塞,直到至少有一个准备就绪的事件发生
            selector.select();
            //获取所有准备就绪的事件
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()){
                //获取准备就绪的事件对象
                SelectionKey selectionKey = iterator.next();
                //处理对应的事件信息
                process(selectionKey,selector);
                //从Selector中移除该Channel
                iterator.remove();
            }
        }
    }


    /***
     * 数据处理
     */
    public void process(SelectionKey key, Selector selector) throws IOException {
        //判断事件,并做相关处理
        if(key.isAcceptable()){
            //连接准备就绪事件
            ServerSocketChannel server = (ServerSocketChannel) key.channel();
            SocketChannel channel = server.accept();
            //向Selector注册读事件
            channel.configureBlocking(false);
            channel.register(selector,SelectionKey.OP_READ);
        }else if(key.isReadable()){
            //读准备就绪事件
            SocketChannel channel = (SocketChannel) key.channel();
            //创建ByteBuffer并且设置最大大小为1024
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //将Channel中的数据写入到ByteBuffer中
            int len = channel.read(byteBuffer);
            while (len != -1) {
                //重置读的位置
                byteBuffer.flip();
                while(byteBuffer.hasRemaining()){
                    //读数据
                    System.out.print((char) byteBuffer.get());
                }
                //重置ByteBuffer指针数据,准备写入数据到ByteBuffer中
                byteBuffer.clear();
                len = channel.read(byteBuffer);
            }
        }
    }

    public static void main(String[] args) throws Exception {
        new NIOServer().listen();
    }

客户端在请求数据的过程中,不用保持一个连接,不能做其他事情。
NIO用轮询代替了始终保持一个连接。

在这里插入图片描述
这个案例是4种注册后的状态
读 : SelectionKey.OP_READ (1)
写 : SelectionKey.OP_WRITE (4)
连接:SelectionKey.OP_CONNECT (8)
接收 : SelectionKey.OP_ACCEPT (16)

3.1.3. AIO模型

在NIO中,Selector多路复用器在做轮询时,如果没有事件发生,也会进行阻塞,如何能把这个阻塞也优化掉呢?那么AIO就在这样的背景下诞生了。
AIO是asynchronous I/O的简称,是异步IO,该异步IO是需要依赖于操作系统底层的异步IO实现。
AIO的基本流程是:用户线程通过系统调用,告知kernel内核启动某个IO操作,用户线程返回。kernel内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。

  • kernel的数据准备

将数据从网络物理设备(网卡)读取到内核缓冲区。

  • kernel的数据复制

将数据从内核缓冲区拷贝到用户程序空间的缓冲区。
在这里插入图片描述
客户端在请求数据的过程中,不用保持一个连接,可以做其他事情。

客户端向服务端请求数据。服务端若有,则返回数据;若无,则告诉客户端“没有数据”。客户端收到“没有数据”的回复后,就做自己的其他事情。服务端有了数据之后,就主动通知客户端,并把数据返回去。

如此一来,整个请求流程中,不仅维持连接的消耗没了而且客户端可以做别的事情了,节约了客户端的时间。

需要提的是,这里解决了连接的消耗,但是也必然引入了别的消耗。这里让客户端能先做别的事情,也肯定会带来新的麻烦。

别的消耗是指,服务端需要主动通知客户端,关于“通知”的业务逻辑肯定是需要消耗资源的。新的麻烦是指,客户端本来在做别的事情,突然前面的事情又插过来要做了,必然引入了一个多线程的协调工作。

目前AIO模型存在的不足:

  • 需要完成事件的注册与传递,这里边需要底层操作系统提供大量的支持,去做大量的工作。
  • Windows 系统下通过 IOCP 实现了真正的异步 I/O。但是,就目前的业界形式来说,Windows系统,很少作为百万级以上或者说高并发应用的服务器操作系统来使用。
  • 而在 Linux 系统下,异步IO模型在2.6版本才引入,目前并不完善。所以,这也是在 Linux 下,实现高并发网络编程时都是以 NIO多路复用模型模式为主。

3.2. Reactor线程模型

Reactor线程模型不是Java专属,也不是Netty专属,它其实是一种并发编程模型,是一种思想,具有指导意义。比如,Netty就是结合了NIO的特点,应用了Reactor线程模型所实现的。
Reactor模型中定义的三种角色:

  • Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等。
  • Acceptor:处理客户端新连接,并分派请求到处理器链中。
  • Handler:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。

常见的Reactor线程模型有三种,如下:

  • Reactor单线程模型
  • Reactor多线程模型
  • 主从Reactor多线程模型
3.2.1. 单Reactor单线程模型

在这里插入图片描述
Reacter是监听者,事件过来了,Reacter分配给Handler处理(监听和业务下发)
新的连接经过acceptor处理,把当前连接分配给Handler处理
主要是业务处理耗时间

说明:
Reactor充当多路复用器角色,监听多路连接的请求,由单线程完成
Reactor收到客户端发来的请求时,如果是新建连接通过Acceptor完成,其他的请求由Handler完成。
Handler完成业务逻辑的处理,基本的流程是:Read(知道要干嘛) --> 业务处理 --> Send(响应) 。
这种模型的优缺点:
优点
结构简单,由单线程完成,没有多线程、进程通信等问题。
适合用在一些业务逻辑比较简单、对于性能要求不高的应用场景。
缺点
由于是单线程操作,不能充分发挥多核CPU的性能。
当Reactor线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重Reactor线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。
可靠性差,如果该线程进入死循环或意外终止,就会导致整个通信系统不可用,容易造成单点故障。

3.2.2. 单Reactor多线程模型

在这里插入图片描述
说明:
在Reactor多线程模型相比较单线程模型而言,不同点在于,Handler不会处理业务逻辑,只是负责响应用户请求,真正的业务逻辑,在另外的线程中完成。
这样可以降低Reactor的性能开销,充分利用CPU资源,从而更专注的做事件分发工作了,提升整个应用的吞吐。
但是这个模型存在的问题:

  • 多线程数据共享和访问比较复杂。如果子线程完成业务处理后,把结果传递给主线程Reactor进行发送,就会涉及共享数据的互斥和保护机制。
  • Reactor承担所有事件的监听和响应(如果是TCP长连接会出现问题),只在主线程中运行,可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。

能解决业务处理的性能瓶颈问题,但不能解决长连接影响性能效率的问题

为了解决性能问题,产生了第三种主从Reactor多线程模型。

3.2.3. 主从Reactor多线程模型

在这里插入图片描述
在主从模型中,将Reactor分成2部分:
MainReactor负责监听server socket,用来处理网络IO连接建立操作,将建立的socketChannel指定注册给SubReactor。(所有TCP连接和SubReactor建立了长连接,MainReactor只负责连接)
SubReactor主要完成和建立起来的socket的数据交互和事件业务处理操作。(可集群)
该模型的优点:
响应快,不必为单个同步事件所阻塞,虽然Reactor本身依然是同步的。
可扩展性强,可以方便地通过增加SubReactor实例个数来充分利用CPU资源。
可复用性高,Reactor模型本身与具体事件处理逻辑无关,具有很高的复用性。

3.3. Netty功能设计

在这里插入图片描述
Netty 功能特性如下:
传输服务,支持 BIO 和 NIO。
容器集成,支持 OSGI(开放服务网关协议)、JBossMC、Spring、Guice 容器。
协议支持,HTTP、Protobuf、二进制、文本、WebSocket 等一系列常见协议都支持。还支持通过
实行编码解码逻辑来实现自定义协议。
Core 核心,可扩展事件模型、通用通信 API、支持零拷贝的 ByteBuf 缓冲对象。

3.4. Netty模型

Netty模型是基于Reactor模型实现的,对于以上三种模型都有非常好的支持,也非常的灵活,一般情况,在服务端会采用主从架构模型,基本示意图如下:
在这里插入图片描述
说明:
在Netty模型中,负责处理新连接事件的是BossGroup,负责处理其他事件的是WorkGroup。Group就是线程池的概念。
NioEventLoop表示一个不断循环的执行处理任务的线程,用于监听绑定在其上的读/写事件。
通过Pipeline(管道)执行业务逻辑的处理,Pipeline中会有多个ChannelHandler,真正的业务逻辑是在ChannelHandler中完成的。

4. Netty核心组件剖析

4.1. Channel

Channel可以理解为是socket连接,在客户端与服务端连接的时候就会建立一个Channel,它负责基本的IO操作,比如:bind()、connect(),read(),write() 等。
主要作用:

  • 通过Channel可获得当前网络连接的通道状态。
  • 通过Channel可获得网络连接的配置参数(缓冲区大小等)。
  • Channel提供异步的网络I/O操作,比如连接的建立、数据的读写、端口的绑定等。
    不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,常用的 Channel 类型:
  • NioSocketChannel,NIO的客户端 TCP Socket 连接。
  • NioServerSocketChannel,NIO的服务器端 TCP Socket 连接。
  • NioDatagramChannel, UDP 连接。
  • NioSctpChannel,客户端 Sctp 连接。
  • NioSctpServerChannel,Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件IO。

4.2. EventLoop、EventLoopGroup

有了 Channel 连接服务,连接之间可以消息流动。如果服务器发出的消息称作“出站”消息,服务器接受的消息称作“入站”消息。那么消息的“出站”/“入站”就会产生事件(Event)。
例如:连接已激活;数据读取;用户事件;异常事件;打开链接;关闭链接等等。
有了事件,就需要一个机制去监控和协调事件,这个机制(组件)就是EventLoop。 在 Netty 中每个 Channel 都会被分到一个 EventLoop。一个 EventLoop 可以服务于多个 Channel。
每个 EventLoop 会占用一个 Thread,同时这个 Thread 会处理 EventLoop 上面发生的所有 IO 操作和事件
在这里插入图片描述
EventLoopGroup 是用来生成 EventLoop 的,在前面的例子中,第一行代码就是 new NioEventLoopGroup();

// 主线程,不处理任何业务逻辑,只是接收客户的连接请求 
EventLoopGroup boss = new NioEventLoopGroup(1);
 // 工作线程,线程数默认是:cpu*2 
EventLoopGroup worker = new NioEventLoopGroup();

如果没有指定线程数大小,默认线程数为:cpu核数*2,源码如下:

 private static final int DEFAULT_EVENT_LOOP_THREADS;

    static {
        DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt(
                "io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));

        if (logger.isDebugEnabled()) {
            logger.debug("-Dio.netty.eventLoopThreads: {}", DEFAULT_EVENT_LOOP_THREADS);
        }
    }

上图关系为:

  • 一个 EventLoopGroup 包含一个或者多个 EventLoop;
  • 一个 EventLoop 在它的生命周期内只和一个 Thread 绑定;
  • 所有由 EventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理;
  • 一个 Channel 在它的生命周期内只注册于一个 EventLoop;
  • 一个 EventLoop 可能会被分配给一个或多个 Channel

4.3. ChannelHandler

ChannelHandler对使用者而言,可以说是最重要的组件了,因为对于数据的入站和出站的业务逻辑的编写都是在ChannelHandler中完成的。
在前面的例子中,MyChannelHandler就是实现了channelRead方法,获取到客户端传来的数据。

对于数据的出站和入站,有着不同的ChannelHandler类型与之对应:

  • ChannelInboundHandler 入站事件处理器
  • ChannelOutBoundHandler 出站事件处理器

接口继承关系如下
在这里插入图片描述
ChannelHandlerAdapter提供了一些方法的默认实现,可减少用户对于ChannelHandler的编写。

ChannelInboundHandlerAdapter 与 SimpleChannelInboundHandler的区别:
在服务端编写ChannelHandler时继承的是ChannelInboundHandlerAdapter
在客户端编写ChannelHandler时继承的是SimpleChannelInboundHandler
两者的区别在于,前者不会释放消息数据的引用,而后者会释放消息数据的引用。
在这里插入图片描述

4.4. ChannelPipeline

在Channel的数据传递过程中,对应着有很多的业务逻辑需要处理,比如:编码解码处理、读写操作等,那么对于每种业务逻辑实现都需要有个ChannelHandler完成,也就意味着,一个Channel对应着多个ChannelHandler,多个ChannelHandler如何去管理它们,它们的执行顺序又该是怎么样的,这就需要ChannelPipeline进行管理了。
一个Channel包含了一个ChannelPipeline,而ChannelPipeline中维护了一个ChannelHandler的列表。
ChannelHandler与Channel和ChannelPipeline之间的映射关系,由ChannelHandlerContext进行维护。
它们关系如下:
在这里插入图片描述
ChannelHandler按照加入的顺序会组成一个双向链表,入站事件从链表的head往后传递到最后一个ChannelHandler,出站事件从链表的tail向前传递,直到最后一个ChannelHandler,两种类型的ChannelHandler相互不会影响

4.5. Bootstrap

Bootstrap是引导的意思,它的作用是配置整个Netty程序,将各个组件都串起来,最后绑定端口、启动Netty服务。
Netty中提供了2种类型的引导类,一种用于客户端(Bootstrap),而另一种(ServerBootstrap)用于服务器。
它们的区别在于:
ServerBootstrap 将绑定到一个端口,因为服务器必须要监听连接,而 Bootstrap 则是由想要连接到远程节点的客户端应用程序所使用的。
引导一个客户端只需要一个EventLoopGroup,但是一个ServerBootstrap则需要两个。
因为服务器需要两组不同的 Channel
第一组将只包含一个 ServerChannel,代表服务器自身的已绑定到某个本地端口的正在监听的套接字。
第二组将包含所有已创建的用来处理传入客户端连接
在这里插入图片描述
与ServerChannel相关联的EventLoopGroup 将分配一个负责为传入连接请求创建 Channel 的EventLoop。一旦连接被接受,第二个 EventLoopGroup 就会给它的 Channel 分配一个 EventLoop。

4.6. Future

Future提供了一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符,它将在未来的某个时刻完成,并提供对其结果的访问。
JDK 预置了 interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以 Netty 提供了它自己的实现——ChannelFuture,用于在执行异步操作的时候使用。

  • ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个ChannelFutureListener实例。
  • 监听器的回调方法operationComplete(),将会在对应的 操作完成时被调用 。然后监听器可以判断该操作是成功地完成了还是出错了。
  • 每个 Netty 的出站 I/O 操作都将返回一个 ChannelFuture,也就是说,它们都不会阻塞。所以说,Netty完全是异步和事件驱动
    在这里插入图片描述

4.7 深入剖析核心ByteBuf缓冲区

4.7.1 工作原理

Java NIO 提供了ByteBuffer 作为它 的字节容器,但是这个类使用起来过于复杂,而且也有些繁琐。

数据操作指针只有一个
读写切换效率不高

Netty 的 ByteBuffer 替代品是 ByteBuf,一个强大的实现,既解决了JDK API 的局限性, 又为网络应用程序的开发者提供了更好的API。
在这里插入图片描述

从结构上来说,ByteBuf 由一串字节数组构成。数组中每个字节用来存放信息。
ByteBuf 提供了两个索引一个用于读取数据,一个用于写入数据。这两个索引通过在字节数组中移动,来定位需要读或者写信息的位置。
当从 ByteBuf 读取时,它的 readerIndex(读索引)将会根据读取的字节数递增。
同样,当写 ByteBuf 时,它的 writerIndex(写索引) 也会根据写入的字节数进行递增。
ByteBuf内部空间结构:
在这里插入图片描述
discardable bytes – 可丢弃的字节空间
readable bytes – 可读的字节空间
writable bytes --可写的字节空间
capacity bytes – 最大的可容量空间
如果 readerIndex 超过了 writerIndex 的时候,Netty 会抛出 IndexOutOf-BoundsException 异常。

4.7.2 索引指针详解

ByteBuf的三个指针:

  • readerIndex(读指针)

指示读取的起始位置, 每读取一个字节, readerIndex自增累加1。 如果readerIndex 与writerIndex 相等,ByteBuf 不可读 。

  • writerIndex(写指针)

指示写入的起始位置, 每写入一个字节, writeIndex自增累加1。如果增加到 writerIndex 与capacity() 容量相等,表示 ByteBuf 已经不可写。

  • maxCapacity(最大容量)

指示ByteBuf 可以扩容的最大容量, 如果向ByteBuf写入数据时, 容量不足, 可以进行扩容。
ByteBuf内部指针:
在这里插入图片描述
在这里插入图片描述

4.7.3 缓冲区的使用

读取操作:

 public static void main(String[] args) {

        //构造
        ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);

        System.out.println("byteBuf的容量为:" + byteBuf.capacity());
        System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
        System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());

//        while (byteBuf.isReadable()){ //方法一:内部通过移动readerIndex进行读取
//            System.out.println((char)byteBuf.readByte());
//        }

        //方法二:通过下标直接读取
//        for (int i = 0; i < byteBuf.readableBytes(); i++) {
//            System.out.println((char)byteBuf.getByte(i));
//        }
//
        //方法三:转化为byte[]进行读取
        byte[] bytes = byteBuf.array();
        for (byte b : bytes) {
            System.out.println((char)b);
        }

    }

写入操作

 public static void main(String[] args) {

        //构造空的字节缓冲区,初始大小为10,最大为20
        ByteBuf byteBuf = Unpooled.buffer(10,20);

        System.out.println("byteBuf的容量为:" + byteBuf.capacity());
        System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
        System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());

        for (int i = 0; i < 5; i++) {
            byteBuf.writeInt(i); //写入int类型,一个int占4个字节
        }
        System.out.println("ok");

        System.out.println("byteBuf的容量为:" + byteBuf.capacity());
        System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
        System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());

        while (byteBuf.isReadable()){
            System.out.println(byteBuf.readInt());
        }

    }

丢失处理

public static void main(String[] args) {
//通过discardReadBytes()方可以将已经读取的数据进行丢弃处理,就可以回收已经读取的字节空间
        ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);

        System.out.println("byteBuf的容量为:" + byteBuf.capacity());
        System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
        System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());

        while (byteBuf.isReadable()){
            System.out.println((char)byteBuf.readByte());
        }

        byteBuf.discardReadBytes(); //丢弃已读的字节空间

        System.out.println("byteBuf的容量为:" + byteBuf.capacity());
        System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
        System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());

    }

在这里插入图片描述

清理功能:

  public static void main(String[] args) {
//通过clear() 重置readerIndex 、 writerIndex 为0,需要注意的是,重置并没有删除真正的内容
        ByteBuf byteBuf = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);

        System.out.println("byteBuf的容量为:" + byteBuf.capacity());
        System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
        System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());


        byteBuf.clear(); //重置readerIndex 、 writerIndex 为0

        System.out.println("byteBuf的容量为:" + byteBuf.capacity());
        System.out.println("byteBuf的可读容量为:" + byteBuf.readableBytes());
        System.out.println("byteBuf的可写容量为:" + byteBuf.writableBytes());

    }

官方的文档:

Clears this buffer. The position is set to zero, 
the limit is set to the capacity, and the mark is discarded. 
(清除这个缓冲区。位置设置为零,限制设置为容量,并丢弃标记。)
 Invoke this method before using a sequence of channel-read or put operations to fill this buffer. For example:
  在使用通道读取或放置操作序列填充缓冲区之前调用此方法, 示例:
   buf.clear(); // Prepare buffer for reading (准备读取缓冲区) 
   in.read(buf); // Read data (读取数据) 
   This method does not actually erase the data in the buffer, but it is named as if it did because it will most often be used in situations in which that might as well be the case. 
   (这个方法实际上并没有擦除缓冲区中的数据,但它的名称似乎是擦除的意思,因为它最常用于可能就是擦 除数据, 但并不真正清除数据的情况下。)

那保留下来的数据有什么作用?
调用clear方法后, 如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先写入一些数据,那么使用compact()方法。compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。Buffer准备好新的写入数据了,并且不会覆盖未读的数据。

4.7.4 缓冲区使用模式

根据存放缓冲区的不同分为三类:
堆缓冲区(HeapByteBuf),内存的分配和回收速度比较快,可以被JVM自动回收,缺点是,如果进行socket的IO读写,需要额外做一次内存复制,将堆内存对应的缓冲区复制到内核Channel中,性能会有一定程度的下降。
由于在堆上被 JVM 管理,在不被使用时可以快速释放。可以通过 ByteBuf.array() 来获取 byte[] 数据。
直接缓冲区(DirectByteBuf),非堆内存,它在堆外进行内存分配,相比堆内存,它的分配和回收速度会慢一些,但是将它写入或从Socket Channel中读取时,由于减少了一次内存拷贝,速度比堆内存块。(零拷贝)
**复合缓冲区,**顾名思义就是将上述两类缓冲区聚合在一起。Netty 提供了一个CompsiteByteBuf,可以将堆缓冲区和直接缓冲区的数据放在一起,让使用更加方便。

//默认使用的是DirectByteBuf,如果需要使用HeapByteBuf模式,则需要进行系统参数的设置 
System.setProperty("io.netty.noUnsafe", "true"); //netty中IO操作都是基于Unsafe完成 的
//ByteBuf 的分配要设置为非池化,否则不能切换到堆缓冲器模式 
serverBootstrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT);
4.7.5 ByteBuf 的分配

Netty 提供了两种 ByteBufAllocator 的实现,分别是:
PooledByteBufAllocator,实现了 ByteBuf 的对象的池化,提高性能减少并最大限度地减少内存碎片。
UnpooledByteBufAllocator,没有实现对象的池化,每次会生成新的对象实例。

//通过ChannelHandlerContext获取ByteBufAllocator实例 
ctx.alloc(); 
//通过channel也可以获取 
channel.alloc();
//Netty默认使用了PooledByteBufAllocator 
//可以在引导类中设置非池化模式 
serverBootstrap.childOption(ChannelOption.ALLOCATOR, UnpooledByteBufAllocator.DEFAULT); 

//或通过系统参数设置 
System.setProperty("io.netty.allocator.type", "pooled"); 
System.setProperty("io.netty.allocator.type", "unpooled");
4.7.6 ByteBuf的释放

ByteBuf如果采用的是堆缓冲区模式的话,可以由GC回收,但是如果采用的是直接缓冲区,就不受GC的管理,就得手动释放,否则会发生内存泄露。
关于ByteBuf的释放,分为手动释放与自动释放

    1. 手动释放
      手动释放,就是在使用完成后,调用ReferenceCountUtil.release(byteBuf); 进行释放。
      通过release方法减去 byteBuf 的使用计数,Netty 会自动回收 byteBuf 。
 @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;

        //获取数据
        String msgStr = byteBuf.toString(CharsetUtil.UTF_8);

        System.out.println("接收到消息:" + msgStr);

        //给客户端响应ok
        ctx.writeAndFlush(Unpooled.copiedBuffer("ok", CharsetUtil.UTF_8));


        ReferenceCountUtil.release(byteBuf); //手动释放资源
    }

手动释放可以达到目的,但是这种方式会比较繁琐,如果一旦忘记释放就可能会造成内存泄露。

    1. 自动释放
      自动释放有三种方式,分别是:入站的TailHandler、继承SimpleChannelInboundHandler、HeadHandler的出站释放。
      TailHandler:
      Netty的ChannelPipleline的流水线的末端是TailHandler,默认情况下如果每个入站处理器
      Handler都把消息往下传,TailHandler会释放掉ReferenceCounted类型的消息。
  @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf byteBuf = (ByteBuf) msg;

        //获取数据
        String msgStr = byteBuf.toString(CharsetUtil.UTF_8);

        System.out.println("接收到消息:" + msgStr);

        //给客户端响应ok
        ctx.writeAndFlush(Unpooled.copiedBuffer("ok", CharsetUtil.UTF_8));

        ctx.fireChannelRead(msg);
    }

在DefaultChannelPipeline中的TailContext内部类会在最后执行:

 protected void onUnhandledInboundChannelInactive() {
    }

    /**
     * Called once a message hit the end of the {@link ChannelPipeline} without been handled by the user
     * in {@link ChannelInboundHandler#channelRead(ChannelHandlerContext, Object)}. This method is responsible
     * to call {@link ReferenceCountUtil#release(Object)} on the given msg at some point.
     */
    protected void onUnhandledInboundMessage(Object msg) {
        try {
            logger.debug(
                    "Discarded inbound message {} that reached at the tail of the pipeline. " +
                            "Please check your pipeline configuration.", msg);
        } finally {
            ReferenceCountUtil.release(msg);
        }
    }

需要注意的是,如果没有进行向下传递,那么在TailHandler中是不会进行释放操作的。
ChannelPipeline的作用与特性:

  1. 内部结构是一个双向链表
  2. 每个节点都有一个ChannelHandler实例,这个实例可以是ChannelInboundHandler类型或ChannelOutboundHandler类型。
  3. ChannelInboundHandler只处理inbound事件,ChannelOutboundHandler只处理outbound事 件。
  4. inbound入站事件处理顺序是由链表的头到链表尾,outbound事件的处理顺序是由链表尾到链表头。
  5. inbound入站事件由netty内部触发,最终由netty外部的代码消费。outbound事件由netty外部的代码触发,最终由netty内部消费。
    SimpleChannelInboundHandler
    当ChannelHandler继承了SimpleChannelInboundHandler后,在
    SimpleChannelInboundHandler的channelRead()方法中,将会进行资源的释放,我们的业务代码也需要写入到channelRead0()中。
  private void write(Object msg, boolean flush, ChannelPromise promise) {
        ObjectUtil.checkNotNull(msg, "msg");
        try {
            if (isNotValidPromise(promise, true)) {
                ReferenceCountUtil.release(msg);
                // cancelled
                return;
            }
        } catch (RuntimeException e) {
        //释放
            ReferenceCountUtil.release(msg);
            throw e;
        }

使用

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

public class MyChannelHandler extends SimpleChannelInboundHandler<ByteBuf> {

    /**
     * 服务端发来的消息
     *
     * @param ctx
     * @param msg
     * @throws Exception
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
        System.out.println("接收到服务端发来的消息:" + msg.toString(CharsetUtil.UTF_8));
    }

    /**
     * 向服务端发送消息
     *
     * @param ctx
     * @throws Exception
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        String msg = "hello netty!";
        ctx.writeAndFlush(Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8));
    }

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

HeadHandler
出站处理流程中,申请分配到的 ByteBuf,通过 HeadHandler 完成自动释放。(最后一站)
出站处理用到的 Bytebuf 缓冲区,一般是要发送的消息,通常由应用所申请。在出站流程开始的时候,通过调用 ctx.writeAndFlush(msg),Bytebuf 缓冲区开始进入出站处理的 pipeline 流水线。
在每一个出站Handler中的处理完成后,最后消息会来到出站的最后一棒 HeadHandler,再经过一轮复杂的调用,在flush完成后终将被release掉。

执行链
在这里插入图片描述

4.7.7. 小结
  • 入站处理流程中,如果对原消息不做处理,调用 ctx.fireChannelRead(msg) 把原消息往下传,由流水线最后一棒
    TailHandler 完成自动释放。
  • 如果截断了入站处理流水线,则可以继承 SimpleChannelInboundHandler ,完成入站ByteBuf自动释放。
  • 出站处理过程中,申请分配到的 ByteBuf,通过 HeadHandler 完成自动释放。
  • 入站处理中,如果将原消息转化为新的消息并调用ctx.fireChannelRead(newMsg)往下传,那必须把原消息release掉;
  • 入站处理中,如果已经不再调用 ctx.fireChannelRead(msg) 传递任何消息,也没有继承
  • SimpleChannelInboundHandler 完成自动释放,那更要把原消息release掉

在这里插入图片描述

5.Netty应用实例-通讯系统

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

   <module>Netty-Service</module>
        <module>Netty-Client</module>
    </modules>

    <dependencies>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>5.0.0.Alpha1</version>
        </dependency>
    </dependencies>

5.3 服务端工程

  • 服务端工程NettyServer
  • 接收客户请求, 并打印客户端发送的消息
  • 消息采用内置String作为编码与解码器
  • 开启信息输入监听线程, 发送消息至客户端
  • NettyProviderServer类:
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;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * Description :
 * Version :1.0
 */
public class NettyProviderServer {

    private int port;
    private NettyProviderServer (int port){
        this.port=port;
    }

    //netty服务端启动
    public void runServer() throws Exception {
        NioEventLoopGroup boss = new NioEventLoopGroup();

        NioEventLoopGroup worker = new NioEventLoopGroup();

        try {
            ServerBootstrap sbs = new ServerBootstrap();
            //绑定group和worker
            sbs.group(boss, worker)
                    //绑定交互的协议
                    .channel(NioServerSocketChannel.class)
                    // tcp最大缓存链接 个数,它是tcp的参数, tcp_max_syn_backlog(半连接上限数量, CENTOS6.5默认是128)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    //保持连接
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    //打印日志级别
                    .handler(new LoggingHandler(LogLevel.INFO))
                    //管道
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel socketChannel) throws Exception {
                            //管道注册handler   处理队列
                            ChannelPipeline pipeline = socketChannel.pipeline();
                            //编码通道处理
                            pipeline.addLast("decode", new StringDecoder());
                            //转码通道处理
                            pipeline.addLast("encode", new StringEncoder());
                            //处理接收到的请求
                            //这里相当于过滤器,可以配置多个
                            pipeline.addLast(new NettyServerHandler());
                        }
                    });

            System.out.println("------------server 启动------------");

            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        while (true) {
                            BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
                            String str = in.readLine();
                            if (NettyServerHandler.channelList.size() > 0) {
                                for (Channel channel : NettyServerHandler.channelList) {
                                    channel.writeAndFlush(str);
                                }
                            }
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }).start();
            ;

            //绑定端口,开始接受连接
            ChannelFuture cf = sbs.bind(port).sync();
            cf.channel().closeFuture().sync();
        } finally {
             boss.shutdownGracefully();
             worker.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new NettyProviderServer(9911).runServer();
    }
}

服务端监听9911端口, 客户端连接时需指定此端口。
处理的handler

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

import java.util.ArrayList;
import java.util.List;

/**
 * Description :
 * Version :1.0
 */
public class NettyServerHandler extends ChannelInboundHandlerAdapter {

    public static List<Channel> channelList = new ArrayList<Channel>();

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        channelList.add(ctx.channel());
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("server--收到消息:" + msg);

        ctx.writeAndFlush(Unpooled.copiedBuffer("客户端,我收到了", CharsetUtil.UTF_8));
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("server--读取数据出现异常");
        cause.printStackTrace();
        ctx.close();
    }
}

5.4 客户端工程

  • 客户端工程NettyClient
  • 发起请求, 与服务端建立连接。
  • 监听服务端下发消息, 并将信息打印出来。
  • 开启信息输入监听线程, 将消息发送至服务端。
  • NettyClientServer类
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.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;

import java.io.BufferedReader;
import java.io.InputStreamReader;

/**
 * Description :
 * Version :1.0
 */
public class NettyClientServer {

    //要请求的服务器的ip地址和端口
    private String ip;
    private int port;

    public NettyClientServer(String ip,int port){
        this.ip=ip;
        this.port=port;
    }


    public void runServer() throws Exception {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();

        Bootstrap bs = new Bootstrap();
        bs.group(bossGroup)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        //管道注册handler
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline.addLast("decode", new StringDecoder());
                        pipeline.addLast("ecode", new StringEncoder());
                        pipeline.addLast(new NettyClientHandler());
                    }
                });

        System.out.println("------------client 启动------------");

        //客户端开启
        ChannelFuture future = bs.connect(ip, port).sync();

        String reqStr = "客户端发起连接请求";

        Channel channel = future.channel();
        channel.writeAndFlush(reqStr);

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (true){
                        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
                        String msg=in.readLine();
                        channel.writeAndFlush(msg);
                    }
                }catch (Exception e){
                     e.printStackTrace();
                }
            }
        }).start();

    }


    public static void main(String[] args) throws Exception {
         new NettyClientServer("127.0.0.1",9911).runServer();
    }
}
  1. NettyClientHandler类
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;

/**
 * Description :
 * Version :1.0
 */
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    public static Channel serverChannel=null;

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        System.out.println("client 读取数据出现异常");
        cause.printStackTrace();
        ctx.close();
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
         serverChannel=ctx.channel();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        System.out.println("client--收到消息:"+msg);
    }
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值