Netty 入门

 WIKI

netty Wiki · GitHubhttps://github.com/netty/netty/wiki

Maven坐标


    io.netty
    netty-all
    4.1.39.Final

入门案例

服务端 

new ServerBootstrap()
    .group(new NioEventLoopGroup()) // 1
    .channel(NioServerSocketChannel.class) // 2
    .childHandler(new ChannelInitializer() { // 3
        protected void initChannel(NioSocketChannel ch) {
            ch.pipeline().addLast(new StringDecoder()); // 5
            ch.pipeline().addLast(new SimpleChannelInboundHandler() { // 6
                @Override
                protected void channelRead0(ChannelHandlerContext ctx, String msg) {
                    System.out.println(msg);
                }
            });
        }
    })
    .bind(8080); // 4

客户端

new Bootstrap()
    .group(new NioEventLoopGroup()) // 1
    .channel(NioSocketChannel.class) // 2
    .handler(new ChannelInitializer() { // 3
        @Override
        protected void initChannel(Channel ch) {
            ch.pipeline().addLast(new StringEncoder()); // 8
        }
    })
    .connect("127.0.0.1", 8080) // 4
    .sync() // 5
    .channel() // 6
    .writeAndFlush(new Date() + ": hello world!"); // 7

Netty 入门_第1张图片

提示

一开始需要树立正确的观念

  • 把 channel 理解为数据的通道

  • 把 msg 理解为流动的数据,最开始输入是 ByteBuf,但经过 pipeline 的加工,会变成其它类型对象,最后输出又变成 ByteBuf

  • 把 handler 理解为数据的处理工序

    • 工序有多道,合在一起就是 pipeline,pipeline 负责发布事件(读、读取完成...)传播给每个 handler, handler 对自己感兴趣的事件进行处理(重写了相应事件处理方法)

    • handler 分 Inbound 和 Outbound 两类

  • 把 eventLoop 理解为处理数据的工人

    • 工人可以管理多个 channel 的 io 操作,并且一旦工人负责了某个 channel,就要负责到底(绑定)

    • 工人既可以执行 io 操作,也可以进行任务处理,每位工人有任务队列,队列里可以堆放多个 channel 的待处理任务,任务分为普通任务、定时任务

    • 工人按照 pipeline 顺序,依次按照 handler 的规划(代码)处理数据,可以为每道工序指定不同的工人

组件

Channel &ChannelFuture(close) &Promise

Netty 入门_第2张图片

  • I/O 事件和请求是在   ChannelPipeline 中完成的.
  • io.netty.channel.Channel 的操作是异步的
  •  SocketChannel 有父 是ServerSocketChannel [TCP]
  • 用完 Channel后调用close(),关闭资源

        通过Bootstrap(ServerBootstrap)初始化好连接之后 调用 channel()即返回ChannelFuture 对象,通过sync() 会阻塞等待直到当前线程获取到Channel,还可以使用回调的方式


import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringEncoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;

import java.util.Scanner;

@Slf4j
public class CloseFutureClient {

    private static NioEventLoopGroup group;

    public static void main(String[] args) throws InterruptedException {
        group = new NioEventLoopGroup();
        ChannelFuture channelFuture = new Bootstrap()
                .channel(NioSocketChannel.class)
                .group(group)
                .handler(new ChannelInitializer() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
                        ch.pipeline().addLast(new StringEncoder());
                    }
                })
                .connect("localhost", 8080);
        Channel channel = channelFuture.sync().channel();
        log.debug("{}", channel);
        new Thread(() -> {
            Scanner scanner = new Scanner(System.in);
            while (true) {
                String line = scanner.next();
                if (line.equals("q")) {
                    channel.close();
                    channel.close();
                    // debug 与 close 不是同一线程执行放的无法保证顺序
//                    log.debug("close");
                    break;
                }
                channel.writeAndFlush(line);
            }
        }).start();

        // * 推荐两种种方式关闭 channel
        if (Math.random() > 0.5)
            closeWithSync(channel);
        else
            closeWithListener(channel);
        // channel 关闭但线程池没有结束?
        // 获取 group 对象 调用 shutdownGracefully() [在 closeFuture 中添加方法调用]
    }

    // 通过向Channel中添加回调的方式执行操作
    private static void closeWithListener(Channel channel) {
        ChannelFuture closeFuture = channel.closeFuture();
        closeFuture.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                log.debug("关闭之后的操作");
                group.shutdownGracefully();
            }
        });
    }
    
    // 同步阻塞方式获取Channel
    private static void closeWithSync(Channel channel) throws InterruptedException {
        channel.closeFuture().sync();
        log.debug("close");
    }
}

异步提升的是什么

  • 单线程没法异步提高效率,必须配合多线程、多核 cpu 才能发挥异步的优势

  • 异步并没有缩短响应时间,反而有所增加

  • 合理进行任务拆分,也是利用异步的关键

io.netty.util.concurrent.Promise

Netty 入门_第3张图片

 简单使用

import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.util.concurrent.DefaultPromise;
import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.ExecutionException;

@Slf4j
public class TestNettyPromise {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 此处仅作为线程池使用
        NioEventLoopGroup executorGroup = new NioEventLoopGroup();
        // 为 Promise 指定一个 线程
        final DefaultPromise promise = new DefaultPromise(executorGroup.next());

        //
        Runnable task = () -> {
            try {
                log.debug("开始计算");
                Thread.sleep(3000L);
                if (Math.random() > 0.5)
                    throw new RuntimeException("Random number generator");
                promise.trySuccess(80);
            } catch (Exception e) {
                e.printStackTrace();
                promise.setFailure(e);
            }
        };
        // 创建任务并提交给一个新的线程
        new Thread(task, "MyThread").start();

        log.debug("获取结果:{},", promise.get());
    }
}

ChannelHandler & ChannelPipeline

io.netty.channel.ChannelHandler

Netty 入门_第4张图片

 打个比喻,每个 Channel 是一个产品的加工车间,Pipeline 是车间中的流水线,ChannelHandler 就是流水线上的各道工序,而后面要讲的 ByteBuf 是原材料,经过很多工序的加工:先经过一道道入站工序,再经过一道道出站工序最终变成产品

 ChannelHandler 使用了拦截过滤的设计模式核心 J2EE 模式 - 拦截过滤器 (oracle.com)https://www.oracle.com/java/technologies/intercepting-filter.html

Netty 入门_第5张图片

        ChannelInboundHandler 的参考示例 io.netty.channel.SimpleChannelInboundHandler 入站(读取消息的 Handler 注意 ByteBuf 的释放)

@Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        boolean release = true;
        try {
            if (acceptInboundMessage(msg)) {
                @SuppressWarnings("unchecked")
                I imsg = (I) msg;
                channelRead0(ctx, imsg);
            } else {
                release = false;
                ctx.fireChannelRead(msg);
            }
        } finally {
            if (autoRelease && release) {
                ReferenceCountUtil.release(msg);
            }
        }
    }

ByteBuf 与java.nio.ByteBuffer

Netty 入门_第6张图片

Netty 入门_第7张图片

retain & release

        由于 Netty 中有堆外内存的 ByteBuf 实现,堆外内存最好是手动来释放,而不是等 GC 垃圾回收。

  • UnpooledHeapByteBuf 使用的是 JVM 内存,只需等 GC 回收内存即可

  • UnpooledDirectByteBuf 使用的就是直接内存了,需要特殊的方法来回收内存

  • PooledByteBuf 和它的子类使用了池化机制,需要更复杂的规则来回收内存

回收内存的源码实现,请关注下面方法的不同实现

protected abstract void deallocate();

Netty 这里采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口

  • 每个 ByteBuf 对象的初始计数为 1

  • 调用 release 方法计数减 1,如果计数为 0,ByteBuf 内存被回收

  • 调用 retain 方法计数加 1,表示调用者没用完之前,其它 handler 即使调用了 release 也不会造成回收

  • 当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用谁来负责 release 呢?

不是我们想象的(一般情况下)

ByteBuf buf = ...
try {
    ...
} finally {
    buf.release();
}

        因为 pipeline 的存在,一般需要将 ByteBuf 传递给下一个 ChannelHandler,如果在 finally 中 release 了,就失去了传递性(当然,如果在这个 ChannelHandler 内这个 ByteBuf 已完成了它的使命,那么便无须再传递)

        基本规则是,谁是最后使用者,谁负责 release,详细分析如下

  • 起点,对于 NIO 实现来讲,在 io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read 方法中首次创建 ByteBuf 放入 pipeline(line 163 pipeline.fireChannelRead(byteBuf))

  • 入站 ByteBuf 处理原则

    • 对原始 ByteBuf 不做处理,调用 ctx.fireChannelRead(msg) 向后传递,这时无须 release

    • 将原始 ByteBuf 转换为其它类型的 Java 对象,这时 ByteBuf 就没用了,必须 release

    • 如果不调用 ctx.fireChannelRead(msg) 向后传递,那么也必须 release

    • 注意各种异常,如果 ByteBuf 没有成功传递到下一个 ChannelHandler,必须 release

    • 假设消息一直向后传,那么 TailContext 会负责释放未处理消息(原始的 ByteBuf)

  • 出站 ByteBuf 处理原则

    • 出站消息最终都会转为 ByteBuf 输出,一直向前传,由 HeadContext flush 后 release

  • 异常处理原则

    • 有时候不清楚 ByteBuf 被引用了多少次,但又必须彻底释放,可以循环调用 release 直到返回 true

        如果使用了 ByteBuf 的池化(PooledByteBufAllocator)功能,需要在操作完成后将 ByteBuf 归还到池中。这可以通过调用 ReferenceCountUtil.release(msg) 方法来实现,该方法会自动处理归还操作。

ByteBuf的自动释放icon-default.png?t=N5F7http://t.csdn.cn/YBHfj

io.netty.channel.SimpleChannelInboundHandler#channelRead 释放消息.

public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        boolean release = true;
        try {
            if (acceptInboundMessage(msg)) {
                @SuppressWarnings("unchecked")
                I imsg = (I) msg;
                // 如果仅处理 String 类型的数据,我们仅需要继承 SimpleChannelInboundHandler
                // 重写 channelRead0 即可
                channelRead0(ctx, imsg);
            } else {
                release = false;
                ctx.fireChannelRead(msg);
            }
        } finally {
            if (autoRelease && release) {
                ReferenceCountUtil.release(msg);
            }
        }
    }

TailContext 释放未处理消息逻辑

// io.netty.channel.DefaultChannelPipeline#onUnhandledInboundMessage(java.lang.Object)
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);
    }
}

具体代码

// io.netty.util.ReferenceCountUtil#release(java.lang.Object)
public static boolean release(Object msg) {
    if (msg instanceof ReferenceCounted) {
        return ((ReferenceCounted) msg).release();
    }
    return false;
}

EventLoop

Netty 入门_第8张图片

         一个EventLoop如果有group 一定是属于一个EventLoopGroup,EventLoop可能管理一个以上的Channel.

Netty 入门_第9张图片

        关闭 Netty 应用程序通常就像关闭通过 创建的所有 EventLoopGroup一样简单。它返回一个 future,当 EventLoopGroup 已完全终止并且属于该组的所有通道都已关闭时,它会通知您。shutdownGracefully()

示例程序 ECHO

netty/example/src/main/java/io/netty/example/echo at 4.1 · netty/netty · GitHubhttps://github.com/netty/netty/tree/4.1/example/src/main/java/io/netty/example/echo

你可能感兴趣的:(网络编程,Netty,java,开发语言)