《Netty实战》第一部分

本文介绍了Netty实战中关于Channel的概念,强调了Channel的线程安全性和与EventLoop的绑定。讲解了ByteBuf的特性,如堆缓冲区、直接缓冲区和复合缓冲区,以及ByteBuf的操作方法。同时,提到了ChannelHandler及其相关接口,如ChannelInboundHandler和ChannelOutboundHandler,用于处理入站和出站事件,并介绍了ChannelPipeline和ChannelHandlerContext在数据传输中的作用。

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

前言

从开始打算学Netty,然后很自然的找到了《Netty实战》这本书,但是,对一个从来没写过的Netty程序的人而言,理解书中的概念确实很难呀。更何况,还几乎没有Nio网络编程的基础,所以一度书看不下去,但觉得写Netty程序不算什么很难的事情。
后来,由于一些需要,自己开始写Netty服务端和客户端,时常出现服务端发不出去消息,或者客户端收不到消息。并且用的Kotlin,异常都没有受检,就更加找不到错误了。这时回过头来看看《Netty实战》才感觉收获很多。

Channel

代表一个通道,连接客户端和服务端两头,两头都可以往Channel里面写/读东西。且Channel的读写是线程安全的。
一个Channel与一个EventLoop绑定,而一个EventLoop可以视为一个线程,用于处理Channel的回调事件。
ChannelRegistered   ->   ChannelActive
                              |
                              \/
ChannelUnregistered <-  ChannelInactive

这里写图片描述

这里写图片描述

由类图可以得到一些信息:Comparable接口保证Channel是独一无二的,AttributeMap有attr(AttrubuteKey< T >)和hasAttr(AttrubuteKey< T >)
通过localAddress()和remoteAddress()可以获取连接的信息,此外isXXX()很好理解,write(),read(),flush()也很常见。
此外很重要的是pipeline()和config()这两个方法, ChannelPipeline持有所有入站和出站的ChannelHandler. 而ChannelConfig,看下图吧

这里写图片描述

unsafe()返回一个Unsafe对象,可以更改Channel的基本信息,如bind一个新的端口,connect一个新的远程地址…

这里写图片描述

还有Channel的父接口ChannelOutboundInvoker, 根据文档,说明write()方法是通过代理ChannelHandlerContext对象来实现的

这里写图片描述

常用的Channel

名称包名描述
NIOio.netty.channel.socket.nio基于java.nio.channels的Selector
Epollio.netty.channel.epoll使用jni的eqoll()和非堵塞io,只在Linux可用的特性,速度更快
OIOio.netty.channel.socket.oio基于java.net的堵塞Stream实现
Localio.netty.channel.local在VM内部的管道传输
Embeddedio.netty.channel.embedded多用于测试ChannelHandler

ByteBuf

Netty的数据传输通过ByteBuf(抽象类)和ByteBufHolder(接口)来暴露。
相比于ByteBuffer而言,ByteBuf有以下优点:可自行扩展,复合缓冲器类型零拷贝,容量自动增长,读写切换不需要flip,读写使用不同的索引,支持链式调用,支持引用计数,支持池化。

这里写图片描述
这里写图片描述

Bytebuf有readerIndex和writerIndex, readerIndex不能超过可读的索引,否则会抛出异常。

缓冲区类型

堆缓冲区:分配释放很快,读写较慢
ByteBuf heapBuf = ...
if(heapBuf.hasArray()){               //只有堆缓存有支撑数组,判断是不是堆缓存
    byte[] array = heapBuf.array();   //如果有,获取数组的引用
    int offset = heapBuf.arrayOffset() + heapBuf.readerIndex();//计算第一个字节的偏移量
    int length = heapBuf.readableBytes(); //获取可读字节
    handleArray(array, offset, length);   //获取数组,偏移量,长度来处理
}
直接缓冲区:分配释放很慢,读写很快。有时候需要复制到堆上,变成字节数组来处理。
ByteBuf directBuf = ...
if(!heapBuf.hasArray()){               //判断为直接缓冲区
    int length = directBuf.readableBytes();//获取可读字节数
    byte[] array = new byte[length];       //分配一个新的字节数组
    directBuf.getBytes(directBuf.readableIndex(), array);
    handleArray(array, 0, length);
}
复合缓冲区:将多个ByteBuf表示为单个CompositeByteBuf的虚拟表示。如有一个header的ByteBuf和body的ByteBuf
CompositeByteBuf messageBuf = Unpooled.compositeBuffer();
ByteBuf headerBuf = ...;
ByteBuf bodyBuf = ...;
messageBuf.addComponents(headerBuf, bodyBuf);

字节操作

随机访问:
ByteBuf buf = ...;
for(int i = 0;i < buf.capacity();i++){
    byte b = buf.getByte(i);
    System.out.println((char)b);
}
顺序访问:0 <= readerIndex <= writerIndex <= capacity
可丢弃字节:discardReadBytes()
可读字节:readXXX(), readBytes(ByteBuf dst)
可写字节:writeXXX(), writeBytes(ByteBuf src)
标记:mark(int readlimit), reset(), markReaderIndex(), markWriterIndex(), resetWriterIndex(), resetReaderIndex()
查找操作:ByteBufProcessor只有一个process(byte val), 如
ByteBuf buffer = ...;
int index = buffer.forEachByte(ByteBufProcessor.FIND_CR);

派生缓冲区:

slice(int, int)
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty", utf8);
ByteBuf sliced = buf.slice(0, 5);
println(sliced.toString(utf8));
buf.setByte(0,(byte)'j');
assert buf.getByte(0) == sliced.getByte(0);     //证明数据是共享的,获取引用
copy(int, int)
Charset utf8 = Charset.forName("UTF-8");
ByteBuf buf = Unpooled.copiedBuffer("Netty", utf8);
ByteBuf copy = buf.copy(0, 5);
println(copy.toString(utf8));
buf.setByte(0,(byte)'j');
assert buf.getByte(0) != copy.getByte(0);     //证明数据是共享的,获取引用
get()/set()从索引开始写,保持索引不变。read()/write()从索引开始写,会调整索引。
其他操作,isReadable(), isWritable(), readableBytes(), writableBytes(), capacity(), maxCapacity(), hasArray(), array()

ByteBufHolder接口

这里写图片描述

content() 返回其持有的ByteBuf
copy()返回深拷贝,包括一个其所包含的ByteBuf的非共享拷贝
duplicate()返回浅拷贝,包括一个其所包含的ByteBuf的共享拷贝

ByteBufAllocator接口

这里写图片描述

    ByteBufAllocator DEFAULT = ByteBufUtil.DEFAULT_ALLOCATOR;

    static {
        //判断是不是android,是则Unpooled, 否则默认是Pooled
        String allocType = SystemPropertyUtil.get(
                "io.netty.allocator.type", PlatformDependent.isAndroid() ? "unpooled" : "pooled");
        allocType = allocType.toLowerCase(Locale.US).trim();

        ByteBufAllocator alloc;
        if ("unpooled".equals(allocType)) {
            alloc = UnpooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else if ("pooled".equals(allocType)) {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: {}", allocType);
        } else {
            alloc = PooledByteBufAllocator.DEFAULT;
            logger.debug("-Dio.netty.allocator.type: pooled (unknown: {})", allocType);
        }

        DEFAULT_ALLOCATOR = alloc;

        THREAD_LOCAL_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.threadLocalDirectBufferSize", 64 * 1024);
        logger.debug("-Dio.netty.threadLocalDirectBufferSize: {}", THREAD_LOCAL_BUFFER_SIZE);

        MAX_CHAR_BUFFER_SIZE = SystemPropertyUtil.getInt("io.netty.maxThreadLocalCharBufferSize", 16 * 1024);
        logger.debug("-Dio.netty.maxThreadLocalCharBufferSize: {}", MAX_CHAR_BUFFER_SIZE);
    }

Unpooled缓冲区

主要方法:
  • buffer(): 返回一个未池化的堆内存存储的ByteBuf
  • directBuffer(): 返回一个未池化的直接内存存储的ByteBuf
  • wrappedBuffer(): 返回一个包装了给定数据的ByteBuf
  • copiedBuffer(): 返回一个复制了给定数据的ByteBuf

ByteBufUtil: 用于操作ByteBuf的静态的辅助方法,

(咳咳,之前我白写吗?)
  • hexDump(ByteBuf)
  • decodeHexDump(CharSequence)
  • writeUtf8(ByteBuf, CharSequence)
  • copy()
  • encodeString()
  • decodeString()

引用计数

Channel ch = ...;
ByteBufAllocator allocater = ch.alloc();
...
ByteBuf buffer = allocator.directBuffer();
assert buffer.refCnt() == 1;

boolean released = buffer.release();

ChannelHandler

主要为ChannelInboundHandler, ChannelOutBoundHandler这两个接口,而可以使用的实现类有这些:编码器,解码器(Object msg的类型取决于编解码器), ChannelInboundHandlerApater抽象类, ChannelOutboundHandlerAdpater抽象类,还有一个客户端很容易使用的SimpleChannelInboundHandler类
此外,在Handler处理的时候,可能会有异常,这个异常会随着pipeline传播,在pipeline的最后需要处理异常。
典型用途是:
  • 将数据从一种格式变为另一种格式
  • 提供异常的通知
  • 提供Channel的生命周期的回调通知
  • 提供用户自定义事件的回调的通知
生命周期有:
  • handlerAdded: 把ChannelHandler添加到ChannelPipeline时调用
  • handlerRemoved: 把ChannelHandler从ChannelPipeline中移除
  • exceptionCaught: 在处理过程中在ChannelPipeline中有错误产生

ChannelInboundHandler, ChannelOutBoundHandler全是回调方法。

这里写图片描述
这里写图片描述

ChannelHandlerAdapter

只多了isSharable()方法,基本不变,两个HandlerAdapter抽象类的实现方法,全都是委托给了ChannelHandlerAdapter的fire前缀同名方法。fire表示发射,意思是发给pipeline中的下一个handler

资源泄露

现在可能还用不着,相关类为ResourceLeakDetector.java
释放资源为ReferenceUtil.release(msg);

ChannelPipeline

每一个Channel在创建时候,都会被分配一个新的ChannelPipeline对象,这是捆绑的,不能改也不能分。
入站事件,从pipeline头部传递到尾部, 经过所有InboundHandler
出站事件,从pipeline尾部传递到头部, 经过所有OutboundHandler
之后会分析其默认实现DefaultChannelPipeline, 底层还是使用AbstractChannelContext的方法。

这里写图片描述
这里写图片描述
这里写图片描述


ChannelHandlerContext

这里写图片描述

这里写图片描述

ChannelHandlerContext 、 Channel 和 ChannelPipeline

这里写图片描述

使用Pipeline来write()

会将数据从Pipeline开始向后传输

这里写图片描述

ChannelHandlerContext ctx = ..;
ChannelPipeline pipeline = ctx.pipeline();
pipeline.write(Unpooled.copiedBuffer("Netty in Action",
CharsetUtil.UTF_8));

使用ctx来write()

这里写图片描述

ChannelHandlerContext ctx = ..;
ctx.write(Unpooled.copiedBuffer("Netty in Action", CharsetUtil.UTF_8));

异常处理

//入站异常
public class InboundExceptionHandler extends ChannelInboundHandlerAdapter {
     @Override
     public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

这里写图片描述
这里写图片描述

    //出站异常
    ChannelFuture future = channel.write(someMessage);
    future.addListener(new ChannelFutureListener() {
        @Override
        public void operationComplete(ChannelFuture f) {
            if (!f.isSuccess()) {
                f.cause().printStackTrace();
                f.channel().close();
            }
        }
    });

这里写图片描述


EventLoop和线程模型

这里写图片描述

JDK任务调度

这里写图片描述
这里写图片描述

Netty任务调度

这里写图片描述

这里写图片描述

这里写图片描述

线程管理

这里写图片描述

1.异步传输

这里写图片描述

2.阻塞传输

这里写图片描述


Bootstrap

这里写图片描述

ServerBootstrap会使用一个父Channel来接收来自客户端的连接,并创建子Channel来用于他们之间的连接。而Bootstrap只需要一个单独的,没有父Channel的Channel来用于所有的网络的交互。

这里写图片描述
这里写图片描述

引导客户端

这里写图片描述

这里写图片描述
这里写图片描述


ServerBootstrap

这里写图片描述

这里写图片描述


服务器接收请求并作为客户端

这里写图片描述
这里写图片描述

添加多个ChannelHandler

这里写图片描述
这里写图片描述

ChannelOption和属性

这里写图片描述
这里写图片描述

这里写图片描述

这里写图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值