上文已經瞭解到了Netty中的啟動器、反應器、通道、處理器、流水線,下面來了解一下Netty中較為重要的ByteBuf緩衝區。
ByteBuf原理
優勢
- Pooling池化,減少了記憶體複製和GC,提升了效率。
- 讀寫分開儲存,索引也分開了,不需要切換讀寫模式。
- 方法可鏈式呼叫,引入了引用計數法,方便了池化與記憶體回收。
重要屬性
- readerIndex(讀指標):讀取的起始位置,每讀取一個位元組,就加1,當它等於writerIndex時,說明已經讀完了。
- writerIndex(寫指標):寫入的起始位置,每寫入一個位元組,就加1,當它等於capacity()時,說明當前容量滿了。此時可擴容,如果不能繼續擴容,則不能寫了。
- maxCapacity(最大容量):可以擴容的最大容量,當前容量等於這個值時,說明不能再擴容了。
引用計數
Netty採用“計數器”來追蹤ByteBuf的生命週期,主要是用於對池化的支援。(池化就是當ByteBuf的引用為0時,就會放到物件快取池中,當需要用緩衝區時,可以直接從這個池裡面取出來用,而不用重新建立一個了)。通過原始碼可以發現ByteBuf實現了一個類ReferenceCounted。這個類就是用於引用計數的。
當建立完ByteBuf時,引用數為1, 通過refCnt()方法可以獲取當前緩衝區的引用數,呼叫retain()方法可以使引用數加1,呼叫release()方法可以使引用數減1,當引用數為0時,緩衝區就會被完全釋放。如果池化了就放到緩衝池中。如果沒池化就分兩種情況,如果是分配在堆記憶體上的,就通過JVM的垃圾回收機制把它回收,如果分配在堆外直接記憶體上,就通過本地方法來釋放堆外記憶體。在Handler處理器中,Netty會自動給流水線在最後加一個處理器用來呼叫release()去釋放緩衝區,如果要在中間中斷流水線,則需要自己呼叫release()釋放緩衝區。
Allocator分配器
Netty提供了ByteAllocator的兩種實現:PoolByteAllocator(池化)和UnpooledByteAllocator(未池化)。Netty預設使用的是PoolByteAllocator,預設使用的記憶體是堆外直接記憶體(寫入速度比堆記憶體更快,池化分配器配合堆外直接記憶體,可將堆外緩衝區複用(彌補了堆外分配和釋放空間的代價較高的缺點),從來大大提升了效能)。
淺層複製
淺層複製有兩個方法,切片淺層複製和整體淺層複製
- slice切片淺層複製 :切片只複製了原緩衝區的可讀部分,不會複製底層陣列(引用同一個),也不會增加引用數。
- duplicate整體淺層複製 :這個是將整體都複製了,可讀可寫,但是引用還是一樣的(同slice)。
ByteBuf使用
來寫個小例子,分析一下。這裡的Logger使用的是自己封裝的靜態日誌類。分析堆疊資訊封裝一個SLF4J的靜態類
public class testBuffer {
public static void main(String[] args) {
// 預設使用池化緩衝區,分配的是堆外直接記憶體
ByteBuf buf = ByteBufAllocator.DEFAULT.buffer(9, 100);
// 使用堆記憶體來分配緩衝區記憶體
ByteBuf buf1 = ByteBufAllocator.DEFAULT.heapBuffer(9, 100);
// 寫入一個位元組陣列
buf.writeBytes(new byte[] { 1, 2, 3, 4 });
Logger.info("引用次數--[{}]", buf.refCnt());
// 依次讀出來
int i = 0;
while (buf.isReadable() && i < buf.readableBytes()) {
// 讀位元組,不改變指標(讀完後,readIndex還是0)
Logger.info("取一個位元組--[{}]", buf.getByte(i++));
}
Logger.info("buf是否使用的堆記憶體--[{}]", buf.hasArray());
Logger.info("buf1是否使用的堆記憶體--[{}]", buf1.hasArray());
int len = buf.readableBytes();
byte[] array = new byte[len];
// 把資料讀取到堆記憶體中
buf.getBytes(buf.readerIndex(), array);
Logger.info("buf讀出的資料--[{}]", array);
// 與原緩衝區buf的底層引用一樣
ByteBuf slice = buf.slice();
// 增加一次淺層複製的引用
slice.retain();
// 減少一次原緩衝區的引用
buf.release();
// 會發現兩個引用是同一個
Logger.info("引用次數--[{}]", buf.refCnt());
Logger.info("切片結果--{}", slice);
Logger.info("引用次數--[{}]", slice.refCnt());
}
}
複製程式碼
執行結果:
21:02:59.693 [main] INFO byteBuf.test1 - 引用次數--[1]
21:02:59.695 [main] INFO byteBuf.test1 - 取一個位元組--[1]
21:02:59.696 [main] INFO byteBuf.test1 - 取一個位元組--[2]
21:02:59.696 [main] INFO byteBuf.test1 - 取一個位元組--[3]
21:02:59.696 [main] INFO byteBuf.test1 - 取一個位元組--[4]
21:02:59.696 [main] INFO byteBuf.test1 - buf是否使用的堆記憶體--[false]
21:02:59.696 [main] INFO byteBuf.test1 - buf1是否使用的堆記憶體--[true]
21:02:59.696 [main] INFO byteBuf.test1 - buf讀出的資料--[[1, 2, 3, 4]]
21:02:59.698 [main] INFO byteBuf.test1 - 引用次數--[1]
21:02:59.698 [main] INFO byteBuf.test1 - 切片結果--UnpooledSlicedByteBuf(ridx: 0, widx: 4, cap: 4/4, unwrapped: PooledUnsafeDirectByteBuf(ridx: 0, widx: 4, cap: 9/100))
21:02:59.698 [main] INFO byteBuf.test1 - 引用次數--[1]
複製程式碼
回顯伺服器實戰
回顯伺服器的伺服器端如下。這裡的業務處理器使用了單例模式建立,因為這裡的業務處理是多執行緒安全的,可以在多個通道間共享使用,所以使用單例模式讓多個通道共享同一個例項,從而減少例項的建立,從而減少記憶體空間的浪費。
@ChannelHandler.Sharable這個註解是Netty中的註解,它是用於標註一個Handler例項可以被多個通道安全地共享,如果不加這個註解,直接共享的話將會丟擲異常。
public class NettyEchoServer {
private final static Logger log = LoggerFactory.getLogger(NettyEchoServer.class);
private final int port;
private ServerBootstrap serverBootstrap = new ServerBootstrap();
private NettyEchoServer(int port){
this.port = port;
}
private void runServer(){
// 建立父通道反應器執行緒組
EventLoopGroup boss = new NioEventLoopGroup(1);
// 建立子通道反應器執行緒組
EventLoopGroup workers = new NioEventLoopGroup();
try {
serverBootstrap.group(boss, workers)
.channel(NioServerSocketChannel.class)
.localAddress("127.0.0.1", port)
.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(NettyEchoServerHandler.getInstance());
}
});
ChannelFuture future = serverBootstrap.bind();
future.addListener((channelFuture) -> {
if (channelFuture.isSuccess()){
log.info("伺服器啟動成功,監聽地址: [{}]", future.channel().localAddress());
}else {
log.info("伺服器啟動失敗");
}
});
// 阻塞直到啟動成功
future.sync();
ChannelFuture close = future.channel().closeFuture();
// 阻塞直到伺服器關閉
close.sync();
}catch (Exception e){
e.printStackTrace();
}finally {
boss.shutdownGracefully();
workers.shutdownGracefully();
}
}
/**
* 使用單例模式實現處理器,使得多個通道可以使用同一個處理器例項
* ChannelHandler.Sharable這個註解表示處理器可以共享
*/
@ChannelHandler.Sharable
static class NettyEchoServerHandler extends ChannelInboundHandlerAdapter{
// 使用volatile修飾變數,保證instance在多執行緒下的可見性
volatile static NettyEchoServerHandler instance;
// 雙重檢查鎖實現單例模式
static NettyEchoServerHandler getInstance(){
if (instance == null){
synchronized (NettyEchoServerHandler.class){
if (instance == null){
instance = new NettyEchoServerHandler();
}
}
}
return instance;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf)msg;
log.info("msg type: [{}]" ,(buf.hasArray() ? "堆記憶體" : "直接記憶體"));
int len = buf.readableBytes();
// 用取得的位元組的長度來初始化位元組陣列
byte[] array = new byte[len];
// 將堆外直接記憶體上的資料讀到堆記憶體上的位元組陣列中
buf.getBytes(buf.readerIndex(), array);
log.info("server received:[{}]", new String(array, StandardCharsets.UTF_8));
log.info("寫回前的引用計數:[{}]", buf.refCnt());
// 寫回資料,非同步任務(寫完後會釋放引用)
ChannelFuture cl = ctx.writeAndFlush(msg);
// I/O操作完成後的操作
cl.addListener((ChannelFuture future) -> log.info("寫回後的引用計數:[{}]", ((ByteBuf) msg).refCnt()));
}
}
public static void main(String[] args){
new NettyEchoServer(66).runServer();
}
}
複製程式碼
回顯伺服器的客戶端:
public class NettyEchoClient {
private final static Logger log = LoggerFactory.getLogger(NettyEchoClient.class);
private int serverPort;
private String serverIp;
private Bootstrap bootstrap = new Bootstrap();
private NettyEchoClient(String ip, int port){
serverIp = ip;
serverPort = port;
}
private void runClient(){
// 建立反應器執行緒組
EventLoopGroup worker = new NioEventLoopGroup();
try{
bootstrap.group(worker)
.channel(NioSocketChannel.class)
.remoteAddress(serverIp, serverPort)
.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(NettyEchoClientHandler.getInstance());
}
});
ChannelFuture future = bootstrap.connect();
// 客戶端連線的非同步通知
future.addListener((channelFuture) -> {
if (channelFuture.isSuccess()){
log.info("客戶端連線成功");
}else {
log.info("客戶端連線失敗");
}
});
// 阻塞直到連線成功
future.sync();
Channel channel = future.channel();
Scanner scanner = new Scanner(System.in);
System.out.println("請輸入要傳送的內容: ");
while (scanner.hasNext()){
// 獲取輸入的內容
String text = scanner.next();
byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
// 分配一個直接記憶體的緩衝區
ByteBuf buf = channel.alloc().buffer();
// 將位元組陣列寫入緩衝區
buf.writeBytes(bytes);
// 將緩衝區的資料寫入到通道中並重新整理通道
channel.writeAndFlush(buf);
System.out.println("請輸入要傳送的內容: ");
}
}catch (Exception e){
e.printStackTrace();
}finally {
worker.shutdownGracefully();
}
}
@ChannelHandler.Sharable
private static class NettyEchoClientHandler extends ChannelInboundHandlerAdapter{
static volatile NettyEchoClientHandler instance;
static NettyEchoClientHandler getInstance(){
if (instance == null){
synchronized (NettyEchoClientHandler.class){
if (instance == null){
instance = new NettyEchoClientHandler();
}
}
}
return instance;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
ByteBuf buf = (ByteBuf)msg;
int len = buf.readableBytes();
byte[] array = new byte[len];
// 將資料讀到位元組陣列中,這中讀不會改變讀指標
buf.getBytes(buf.readerIndex(), array);
log.info("客戶端回顯的資料:[{}]", new String(array, StandardCharsets.UTF_8));
// 手動釋放引用
buf.release();
}
}
public static void main(String[] args){
new NettyEchoClient("127.0.0.1", 66).runClient();
}
}
複製程式碼
執行結果:
客戶端:
15:12:49.677 [nioEventLoopGroup-2-1] INFO echoserver.NettyEchoClient - 客戶端連線成功
請輸入要傳送的內容:
哈哈哈,伺服器你好
15:12:59.821 [nioEventLoopGroup-2-1] INFO echoserver.NettyEchoClient - 客戶端回顯的資料:[哈哈哈,伺服器你好]
複製程式碼
伺服器端:
15:12:40.747 [nioEventLoopGroup-2-1] INFO echoserver.NettyEchoServer - 伺服器啟動成功,監聽地址: [/127.0.0.1:66]
15:12:59.816 [nioEventLoopGroup-3-1] INFO echoserver.NettyEchoServer - msg type: [直接記憶體]
15:12:59.818 [nioEventLoopGroup-3-1] INFO echoserver.NettyEchoServer - server received:[哈哈哈,伺服器你好]
15:12:59.818 [nioEventLoopGroup-3-1] INFO echoserver.NettyEchoServer - 寫回前的引用計數:[1]
15:12:59.821 [nioEventLoopGroup-3-1] INFO echoserver.NettyEchoServer - 寫回後的引用計數:[0]
複製程式碼