Netty 入门

 WIKI

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

Maven坐标

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

入门案例

服务端 


import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
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.logging.LoggingHandler;

import java.util.Random;

/**
 * @author Jay
 */
public class NettyServer {
    private static NioEventLoopGroup boos = new NioEventLoopGroup();
    private static NioEventLoopGroup childGroup = new NioEventLoopGroup(2);

    public static void main(String[] args) throws InterruptedException {

        new ServerBootstrap()
                .channel(NioServerSocketChannel.class)
                .group(boos, childGroup)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
//                        helloWord(ch);
//                        withChildGroup(ch);

//                        testHandlers(ch);
                    }


                })
                .bind(8080);
    }

    /**
     * netty 入门示例
     *
     * @param ch
     */
    private static void helloWord(SocketChannel ch) {
        ch.pipeline().addLast(new StringDecoder());
        ch.pipeline().addLast(new SimpleChannelInboundHandler<String>() {
            @Override
            protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
                System.out.println(msg);
            }
        });
    }

    private static void withChildGroup(SocketChannel ch) {
        ch.pipeline().addLast(new LoggingHandler());
        ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                ByteBuf byteBuf = msg instanceof ByteBuf ? ((ByteBuf) msg) : null;
                if (byteBuf == null)
                    return;

                byte[] dst = new byte[byteBuf.readableBytes()];
                byteBuf.readBytes(dst);
                System.out.println(new String(dst));
                super.channelRead(ctx, msg);
            }
        });
    }

}

客户端


import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelInitializer;
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.StringEncoder;

/**
 * @author Jay
 */
public class NettyClient {
    public static void main(String[] args) throws InterruptedException {
        new Bootstrap()
                .channel(NioSocketChannel.class)
                .group(new NioEventLoopGroup())
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ch.pipeline().addLast(new StringEncoder());
                    }
                })
                .connect("localhost", 8080)
                .sync().channel()
                .writeAndFlush(args != null ? args[0] : "Hello Word");
    }
}

💡 提示

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

  • 把 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

  • 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<NioSocketChannel>() {
                    @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

 简单使用

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<Integer> 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

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

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

 在Netty中,"Inbound"和"Outbound"是用于描述网络数据流动方向的术语。

"Inbound"表示数据从外部流向应用程序。也就是说,数据从网络中进入Netty的通道或管道,经过一系列的处理后,最终传递给应用程序处理。例如,当接收到网络数据包时,数据会通过一系列的Inbound处理器(如解码器)进行解析和处理,然后交给应用程序进行进一步的业务逻辑处理。

"Outbound"表示数据从应用程序流向外部。也就是说,应用程序产生的数据经过一系列的处理后,通过Netty的通道或管道发送到网络。例如,应用程序需要发送一个消息给远程主机,数据会经过一系列的Outbound处理器(如编码器)进行处理和编码,然后通过网络发送到目标主机。

ctx.channel().write(msg)会使用完整的ChannelPipeline进行处理,而ctx.write(msg)会从当前位置往上(往前)依次寻找出站处理器并执行。

/**
     *  测试 入站&出站处理器的执行时的排序问题
     *  测试 ctx.channel().write(msg) 与 ctx.write(msg) 区别
     * @param ch
     */
    private static void testHandlers(SocketChannel ch) {
        ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) {
                System.out.print("入站处理器1 \t");
                ctx.fireChannelRead(msg); // 1
            }
        });
        ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) {
                System.out.println("入站处理器2 \t");
                ctx.fireChannelRead(msg); // 2
            }
        });
        ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
            @Override
            public void write(ChannelHandlerContext ctx, Object msg,
                              ChannelPromise promise) {
                System.out.print("使用 OutboundHandler 1\t");
                ctx.write(msg, promise); // 4
            }
        });

        ch.pipeline().addLast(new ChannelInboundHandlerAdapter(){
            @Override
            public void channelRead(ChannelHandlerContext ctx, Object msg) {
                System.out.print("入站处理器3 \n");
                if (Math.random() > 0.5F) {
                    System.out.println("使用 ctx.channel().write(msg)");
                    ctx.channel().write(msg); // 3
                }else{
                    System.out.println("使用 ctx.write(msg)");
                    ctx.write(msg);
                }
            }
        });

        ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
            @Override
            public void write(ChannelHandlerContext ctx, Object msg,
                              ChannelPromise promise) {
                System.out.print("使用 OutboundHandler 2\t");
                ctx.write(msg, promise); // 5
            }
        });
        ch.pipeline().addLast(new ChannelOutboundHandlerAdapter(){
            @Override
            public void write(ChannelHandlerContext ctx, Object msg,
                              ChannelPromise promise) {
                System.out.print("使用 OutboundHandler 3 \n");
                ctx.write(msg, promise); // 6
            }
        });
    }

 测试ChannelPipline 的便捷方式

import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelOutboundHandlerAdapter;
import io.netty.channel.ChannelPromise;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.http.HttpRequestEncoder;
import io.netty.handler.codec.http.HttpResponseDecoder;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
 * 对 ChannelHandler 的便捷测试方法
 * @author Jay
 */
public class ChannelPipelineTest {

    @Test
    public void testChannelPipeline() {
        // 创建 EmbeddedChannel 对象
        EmbeddedChannel channel = new EmbeddedChannel(
                new HttpRequestEncoder(),
                new HttpResponseDecoder(),
                new ChannelInboundHandlerAdapter() {
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) {
                        System.out.println("入站处理器1");
                        ctx.fireChannelRead(msg);
                    }
                },
                new ChannelOutboundHandlerAdapter() {
                    @Override
                    public void write(ChannelHandlerContext ctx, Object msg,
                                      ChannelPromise promise) {
                        System.out.println("出站处理器1");
                        ctx.write(msg, promise);
                    }
                }
        );

        // 发送输入消息
        channel.writeInbound("request");
        channel.writeOutbound("response");

        // 断言处理结果
        assertTrue(channel.finish());
        assertEquals("request", channel.readInbound());
        assertEquals("response", channel.readOutbound());
        // ... 进一步验证其他期望的结果
    }

}

        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

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的自动释放http://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

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

        关闭 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值