講述netty之前,先看下netty的一個整體結構如下:
從上述可以看出,netty核心部分主要有基於可擴展性的事件驅動設計模型實現,通用的通信API(支持的網絡協議比較豐富)以及基於ByteBuffer實現的零拷貝機制,同時從web的安全性考慮,netty支持SSL/TLS完整協議.
分析Netty的原理之前,我們先看看netty的核心組件有哪些.
Netty核心組件
netty一個簡單示例
public class NettyServer {
private static final String IP = "127.0.0.1";
private static final int port = 8080;
private static final int BIZGROUPSIZE = Runtime.getRuntime().availableProcessors() * 2;
private static final int BIZTHREADSIZE = 100;
private static final EventLoopGroup bossGroup = new NioEventLoopGroup(BIZGROUPSIZE);
private static final EventLoopGroup workGroup = new NioEventLoopGroup(BIZTHREADSIZE);
public static void main(String[] args) throws Exception {
NettyServer.start();
}
public static void start() throws Exception {
ServerBootstrap serverBootstrap = initServerBootstrap();
ChannelFuture channelFuture = serverBootstrap.bind(IP, port).sync();
channelFuture.channel().closeFuture().sync();
}
private static ServerBootstrap initServerBootstrap() {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE, 0, 4, 0, 4));
pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8));
pipeline.addLast(new TcpServerHandler());
}
});
return serverBootstrap;
}
}
public class TcpServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) {
System.out.println("get new client connection ");
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
// 使用之後需要進行釋放回到內存池中方便回收,以便於下次使用的時候可重複利用
// 如果不釋放將會產生新的ByteBuff
((ByteBuf) msg).release();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
ctx.close();
}
}
通過上述的一個簡單示例,可以看到Netty核心組件主要有:
- 啓動類ServerBoostrap
- 事件輪詢類EventLoop以及EventLoopGroup
- Netty自身實現的Channel,如上述的NioServerSocketChannel,作爲數據傳輸的載體
- Netty自身實現異步操作的ChannelFuture
- 事件(入站與出站事件)與ChannelHandler
- 責任鏈ChannelPipeline與上下文ChannelHandlerContext
- ByteBuff組件
啓動類ServerBoostrap
通過上述應用程序可以看到,使用到啓動類主要有以下方法:
// ServerBoostrap類的定義
public class ServerBootstrap extends AbstractBootstrap<ServerBootstrap, ServerChannel>{
// 添加boss事件輪詢以及worker事件輪詢
ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup);
// 添加事件源,監聽客戶端連接
B channel(Class<? extends C> channelClass);
// 添加監聽事件變化後對事件響應的處理器
ServerBootstrap childHandler(ChannelHandler childHandler);
// 綁定服務IP以及端口
ChannelFuture bind(String inetHost, int inetPort);
}
ServerBoostrap的類圖結構爲:
根據上述可知,ServerBoostrap類依賴於Netty自身實現的Channel,需要藉助Channel來實現數據傳輸,Channel可以是一個網絡輸入以及輸出的載體通道.
Netty的事件輪詢器EventLoop以及輪詢器組EventLoopGroup
基於併發知識經驗,有線程與線程組之分,因而可以推測到EvenntLoopGroup的作用主要爲了在程序運行過程中可以管理和控制一組處理相同業務操作的EventLoop事件,對此先查看EventLoop的組成結構設計.
EventLoop定義如下:
// EventLoop是具備有序性,是一個有序事件執行器
public interface EventLoop extends OrderedEventExecutor, EventLoopGroup {
@Override
EventLoopGroup parent();
}
// EventLoopGroup 是一個事件執行器組,在原有的基礎上增強事件功能方法
public interface EventLoopGroup extends EventExecutorGroup{}
// 對於OrderedEventExecutor,通過查看開發工具查看父類最終也是一個事件執行器組,在原有的基礎上增強功能,即EventExecutor
// OrderedEventExecutor -> EventExecutor -> EventExecutorGroup
// 事件執行器具備兩個功能:一個是迭代器功能,一個是計劃任務的線程池
interface EventExecutorGroup extends ScheduledExecutorService, Iterable<EventExecutor>{}
通過上述可知,EventLoop以及EventLoopGroup具備線程池以及迭代器功能,其簡要類圖如下:
結合Reactor模式以及上述的netty代碼示例,我們從宏觀上看Channel
,EventLoop
,EventLoopGroup
以及Thread
之間的關係如下(摘錄Netty實戰):
對此,可以對EventLoop
以及EventLoopGroup
的理解如下:
- EventLoopGroup包含多個EventLoop,每個EventLoop的生命週期綁定一個線程,相當於
Thread
與ThreadGroup
之間的關係,也就是不論是註冊還是事件IO完成響應都將會在專有的線程上進行處理. - 每個
Channel
進行註冊操作都只能註冊於一個EventLoop
,但是由於一個EventLoopGroup
存在多個EventLoop
,因而一個EventLoop
可以分配給一個或者多個Channel
進行IO相關的事件處理.
對於Netty的實現是使用多線程方式來實現一個異步程序上的非阻塞式IO網絡編程,同時將IO相關操作與Handler操作分離,真正實現IO操作與業務處理邏輯上的分離,並通過多線程異步的方式來回調喚醒執行IO事件完成操作的handler方法.
Netty自定義的Channel(重點)
Channel是Java的NIO的基本構造,Netty在原有的基礎上進行方法增強,主要有:
- 對於用戶而言具備以下三方面
- 提供了channel的當前狀態,即連接是open還是connected狀態
- channel相關的屬性配置ChannelConfig,比如channel接收的buffer大小配置
- channel支持讀取,寫出,連接以及綁定的IO操作,與ChannelPipeline一同協作完成所有和Channel相關的IO事件操作,其中ChannelPipeline以責任鏈的方式連接所有IO事件與Channel相關的請求事件的完成處理器.
- Channel是異步的
在netty中所有的IO操作都是以多線程的方式進行異步回調,是屬於應用程序上的多線程異步操作,而本質上是使用非阻塞式IO的方式進行調用,在Reactor同步IO操作的基礎上更改爲異步完成處理操作的方式.類似於Proactor模式,但仍有不同,區分在於Netty的異步調用是在程序中進行回調將事件結果傳遞給響應的Handler,而Proactor模式是在內核中執行異步操作,異步茶走哦的實現需要藉助ChannelFuture
組件來通知異步操作的狀態(成功/失敗/取消).
- Channel是劃分層次的
在Netty實現中,客戶端SocketChannel
是通過服務端的ServerSocketChannel
創建的,因此對於SocketChannel的上一個層級父類是ServerSocketChannel
,也就是說通過ScoektChannel
的parent()
方法能夠獲取到ServerSocketChannel
,這個是屬於語義上的層次劃分
- 支持向下轉換以便於於滿足特殊傳輸協議的一些特有的方法
比如可以向下轉換爲DatagramChannel
來調用對應的join或者leave方法
- 釋放資源
很重要一點就是當socket在對應的channel通過完成讀寫操作時,需要釋放所有的資源以便於能夠被系統回收.
Netty自定義的ChannelFuture(重點)
在併發線程庫中,Future類提供了異步操作的實現,通過調用方法返回Future,以異步的方式處理完某個操作之後通知到當前的程序執行位置,由於jdk實現需要手動檢測,即future.get()
,此時如果操作完成就會返回結果,如果未完成將會同步阻塞於線程等待完成,Netty爲了解決這個繁瑣的操作,自定義實現了ChannelFuture類.
// 與ChannelFuture相關的
interface ChannelFuture extends Future<Void>{}
// 特殊的ChannelFuture,表示具備可寫特徵,即客戶端獲取當前類之後是具備可更改屬性操作的.
interface Promise<V> extends Future<V>{} // Future爲java併發庫下
interface ChannelPromise extends ChannelFuture, Promise<Void>{}
- ChannelFuture使用示例
Channel channel = new NioSocketChannel();
// 以異步的方式與遠程服務建立連接
ChannelFuture future = channel.connect(new InetSocketAddress("192.168.10.110", 8080));
future.addListener(new ChannelFutureListener(){
void operationComplete(ChannelFuture future) throws Exception{
if(future.future.isSuccess()){
// 建立連接成功
}else{
//建立連接失敗
}
}
});
- Channel的生命週期(摘錄Netty實戰)
ChannelUnregistered: Channel已將被創建,但是還未被註冊到EventLoop組件上
ChannelRegistered: Channel已經被註冊到EventLoop組件上
ChannelActive: Channel已經與服務端建立連接狀態,處於活躍狀態,可以進行發送和寫出數據
ChannelInActive: Channel與服務端沒有建立連接狀態
事件與ChannelHandler(重點)
Netty事件
Netty網絡框架中的事件是按照網絡數據流的相關性質來定義區分,主要有入站事件以及出站事件
-
Netty入站事件: 主要是入站數據或者是相關狀態發生更改而觸發的事件,就是socket有新連接或者新請求
- 已經建立連接或者連接失效/超時
- 數據讀取
- 用戶事件,即應用程序給予事件響應完成的處理程序
- 異常錯誤事件
-
Netty出站事件: 未來將會觸發某個動作的結果,即程序主動向socket底層發起操作
- 打開或者關閉socket遠程節點的連接
- 將數據寫出或者刷新到socket緩衝區中
ChannelHandler
在netty中對事件的響應最終會分發給ChannelHandler進行處理,每個ChannelHandler會通過一個pipeline鏈的方式連接起來,以下是展示ChannelHandler鏈式處理入站與出戰事件的簡要流程.
ChannelHandler在netty源碼中主要有包含以下幾個內容:
- 交由具體子接口定義出入站事件處理方法
// 1. 交由具體子接口定義出入站事件處理方法
// ChannelHandler並沒有提供很多的方法聲明,同時通過上述的入站和出站事件處理,我們也很容易想到ChannelHandler存在處理入站事件的ChannelInboundHandler以及出站事件的ChannelOutboundHandler,同時在netty中默認有對應實現方式
// ChannelInboundHandlerAdapter實現入站事件的處理
// ChannelOutboundHandlerAdapter實現出站事件的處理
// ChannelDuplexHandler能夠同時處理入站和出站事件
ChannelHandler類圖組成結構如下:
- 上下文對象,ChannelHandlerContext
// 2. 上下文對象,ChannelHandlerContext
// 要想在上述ChannelHandler的鏈式事件處理流程,就必須滿足兩個條件,一個是如何在每個單獨ChannelHandler的處理器傳遞事件,二是每個ChannelHandler是如何通過鏈式綁定關聯的
// ChannelHandler通過ChannelHandlerContext爲每個對應的Handler傳遞事件,因此ChannelHandler必然存在一個上下文對象負責事件傳遞,類似於EDA的事件通道
// netty中事件觸發就會創建響應事件的ChannelHandler,並添加到ChannelPipeline中,通過鏈表的數據結構來維護每個Handler之間關聯,同時將ChannelPipeline存儲在上下文中,可以通過上下文對象獲取管道對象
ChannelHandler,ChannelHandlerContext以及ChannelPipeline之間的關聯如下:
- 能夠存儲並管理有狀態的信息
如果在ChannelHandler定義專屬於某一個連接的成員變量數據,即連接需要保持有狀態數據,爲了防止數據競爭產生數據不一致的問題,必須爲每一個連接請求的處理操作創建一個新的handler對象去單獨處理,保證業務的數據一致性.比如下面一個例子
// 現定義一個需求:需要進行登錄才能夠獲取數據,於是就有了以下的定義
interface Message{
// methods
}
class DataServerHandler extends SimpleChannelInboundHandler<Message> {
// 存儲有狀態數據
private boolean isLogined;
public void channelRead0(ChannelHandlerContext ctx, Message msg){
if(message instanceof LoginMessage){
auth((LoginMessage)message);
isLogined = true;
}else if(message instanceof GetDataMessage){
if(isLogined){
ctx.writeAndFlush(fetchSecret((GetDataMessage) message));
}else{
fail();
}
}
}
// 爲了避免數據競爭產生數據不一致問題,避免上述需求中的非法登錄用戶獲取到登錄數據,必須爲每個請求處理連接在提交給handler處理前保證handler是針對當前的連接是1:1的處理方式,即一個連接對應一個channel處理器,對此,需要在添加channel時通過以下的方式進行添加
serverBootstrap.childHandler(new ChannelInitializer<Channel>(){
public void initChannel(Channel channel){
channel.pipeline().addLast(new DataServerHandler());
// 如果一個連接需要用到多個handler協同處理,則只需要調用addLast添加即可
// 這樣每次有事件發生的時候,對應的一個請求處理連接的事件響應處理都會重新創建handler,即保證每個請求連接的事件響應handler都是新創建的,避免了數據競爭產生的數據不一致問題
}
});
// 如果要保證handler只創建一次,那麼就只需要進行調用childHandler添加對應實現的具體Handler實例
- 使用AttributeKey來存儲信息
儘管對於存儲有狀態的信息需要新創建channel去處理它,但是並不是所有情況都是需要創建一個新的handler去處理不同的連接,比如對於通用的共享數據,不存在於不同連接的狀態變化,但是爲了能夠保證共享數據是安全的,爲此可以使用AttribuiteKey存儲這類數據信息,同時在每個handler中都會有一個上下文對象,而當前的AttributeKey能夠通過上下文對象獲取到,因此對於AttributeKey的獲取在不同handler中可以通過上下文對象來獲取,並且爲對應的handler添加註解@Sharable能夠保證線程是安全的,比如下面例子.
interface Message{
// methods
}
@Sharable
class DataServerHandler extends SimpleChannelInboundHandler<Message> {
// 存儲有狀態數據
private boolean sharedObject;
public void channelRead0(ChannelHandlerContext ctx, Message msg){
// ...
}
}
serverBootstrap.childHandler(new ChannelInitializer<Channel>(){
private static final DataServerHandler SHARED = new DataServerHandler();
public void initChannel(Channel channel){
// 這樣保證在處理不同連接的處理鏈pipeline中存儲的handler都是相同的,並且是線程安全的
channel.pipeline().addLast(SHARED);
}
});
- 註解@Sharable
根據上述的AttributeKey分析可知,對於註解爲Sharable的說明如下:
- 如果定義的Handler添加註解爲@Sharable,則表示Handler在整個處理鏈pipeline中都是屬於無競爭的環境,數據不存在線程安全問題,同時如果是無狀態的數據可以通過只創建一次handler實例來完成整個事件處理鏈pipeline的handler處理.
- 如果沒有添加註解說明,那麼爲了保證每個handler的共享數據是屬於線程安全的,就必須爲每次親戚連接的操作創建新的handler,也就是說不同的連接的事件處理鏈pipeline存儲的handler都是連接特有的,屬於連接與handler的1:1模式,這樣才能保證數據是線程安全.
- ChannelHandler的方法回調機制
Netty設計實現是基於Reactor模式實現,在上文已經模擬Reactor模式的實現原理,可以知道在事件觸發之後就會回調執行ChannelHandler實現類中的方法執行響應事件的處理邏輯,即主要回調有以下方法:
根據上述的類圖結構,netty默認實現有出站與入站的HandlerAdaper實現,以及含有出站和入站實現的ChannelDuplexHandler
,因此在實際使用中可以繼承上述相關類來重寫部分方法以實現目標業務邏輯的處理程序.
- ChannelHanlder的生命週期
// handlerAdded: 當ChannelHandler添加到pipeline被調用
// handlerRemoved: 當ChannelHandler從pipeline中移除時被調用
// exceptionCaught: 在處理過程中ChannelPipeline發生異常時被調用
責任鏈ChannelPipeline與處理器上下文ChannelHandlerContext(重點)
在講述一個責任鏈與上下文對象前,先根據上述的Channel事件處理鏈pipeline演示一個責任鏈設計模式的代碼實現原理.
- 僞代碼實現
/**
* 責任鏈設計:
* channel1 -> channel2 -> channel3 -> ...
*/
// channel1處理完之後的結果將會傳遞給channel2進行處理,然後將channel2的處理結果傳遞給channel3再進行處理,那麼什麼時候結束呢?
//要麼遍歷到沒有下一個channel節點爲止就結束
//要麼就是直接在當前節點中斷不進行往下傳播事件
// 根據netty的channel可知,pipeline需要依賴到一個上下文對象,通過上下文對象來實現責任鏈的數據傳輸,於是就有以下的定義.
class Main{
public static void main(String[] args){
// 類比netty添加方式
HandlerPipeline pipeline = new HandlerPipeline();
pipeline.addLast(new HandlerTest());
pipeline.addLast(new HandlerTest());
}
}
// 基於鏈表結構存儲handler
class HandlerPipeline {
private HandlerContext head = new HandlerContext(new Handler(){
void doHandler(HandlerContext context, Object val){
context.nextRun(val);
}
});
public void addLast(Handler handler){
HandlerContext ctx = head;
while(ctx.next != null){
ctx = ctx.next;
}
ctx.next = new HandlerContext(handler);
}
}
// 通過上下文保存當前handler並傳遞數據到下一個context進行處理
class HandlerContext {
private HandlerContext next;
private Handler handler;
HandlerContext(Handler handler){
this.handler = handler;
}
void nextRun(Object val){
if(this.next != null){
this.next.handler(val);
}
}
void handler(Object val){
this.handler.doHandler(this, val);
}
}
// handler處理器
interface Handler{
void doHandler(HandlerContext context, Object val);
}
class HandlerTest implements Handler{
public void doHandler(HandlerContext context, Object val){
// 傳播給下一個handler
context.nextRun(val);
// 如果不傳播,在當前handler停止,就無需調用nextRun方法
}
}
-
Netty責任鏈運作流程
責任鏈流程(摘錄源碼)
* +---------------------------------------------------+---------------+
* | ChannelPipeline | |
* | \|/ |
* | +---------------------+ +-----------+----------+ |
* | | Inbound Handler N | | Outbound Handler 1 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* | | \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler N-1 | | Outbound Handler 2 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ . |
* | . . |
* | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()|
* | [ method call] [method call] |
* | . . |
* | . \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler 2 | | Outbound Handler M-1 | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* | | \|/ |
* | +----------+----------+ +-----------+----------+ |
* | | Inbound Handler 1 | | Outbound Handler M | |
* | +----------+----------+ +-----------+----------+ |
* | /|\ | |
* +---------------+-----------------------------------+---------------+
* | \|/
* +---------------+-----------------------------------+---------------+
* | | | |
* | [ Socket.read() ] [ Socket.write() ] |
* | |
* | Netty Internal I/O Threads (Transport Implementation) |
* +-------------------------------------------------------------------+
連接與事件處理鏈協作流程簡要流程圖
- Netty之ChannelPipeline源碼分析
// 1.pipeline的創建
// 每個Channel都有自己的pipeline,並且當一個新的channel被創建的時候會自動創建
// 2. 責任鏈流程 -- 見上述責任鏈流程
// 3. pipeline在上下文進行事件傳播的方法
// 入站事件與出站事件
/* <ul>
* <li>Inbound event propagation methods:
* <ul>
* <li>{@link ChannelHandlerContext#fireChannelRegistered()}</li>
* <li>{@link ChannelHandlerContext#fireChannelActive()}</li>
* <li>{@link ChannelHandlerContext#fireChannelRead(Object)}</li>
* <li>{@link ChannelHandlerContext#fireChannelReadComplete()}</li>
* <li>{@link ChannelHandlerContext#fireExceptionCaught(Throwable)}</li>
* <li>{@link ChannelHandlerContext#fireUserEventTriggered(Object)}</li>
* <li>{@link ChannelHandlerContext#fireChannelWritabilityChanged()}</li>
* <li>{@link ChannelHandlerContext#fireChannelInactive()}</li>
* <li>{@link ChannelHandlerContext#fireChannelUnregistered()}</li>
* </ul>
* </li>
* <li>Outbound event propagation methods:
* <ul>
* <li>{@link ChannelHandlerContext#bind(SocketAddress, ChannelPromise)}</li>
* <li>{@link ChannelHandlerContext#connect(SocketAddress, SocketAddress, ChannelPromise)}</li>
* <li>{@link ChannelHandlerContext#write(Object, ChannelPromise)}</li>
* <li>{@link ChannelHandlerContext#flush()}</li>
* <li>{@link ChannelHandlerContext#read()}</li>
* <li>{@link ChannelHandlerContext#disconnect(ChannelPromise)}</li>
* <li>{@link ChannelHandlerContext#close(ChannelPromise)}</li>
* <li>{@link ChannelHandlerContext#deregister(ChannelPromise)}</li>
* </ul>
* </li>
* </ul>
*/
// 4. 構建一個ChannelPipeline
// 當我們的web服務爲每個請求處理對應的decode-process-encode時,對於執行比較耗時的操作需要將線程隔離處理,也就是需要有針對Group對process進行處理,而其他線程仍然可以處理decode以及encode非耗時邏輯,可以通過以下的方式:
static final EventExecutorGroup group = new DefaultEventExecutorGroup();
pipeline = ch.pipeline();
pipeline.addLast("decoder", new MyProtocolDecoder());
pipeline.addLast("encoder", new MyProtocolEncoder());
// 這個時候的process處理會單獨放在以下的group進行處理
pipeline.addLast(group, "handler", new MyBusinessLogicHandler());
// 5. ChannelPipeline是屬於線程安全類
- Netty之ChannelHandlerContext源碼分析
// 1. 通過喚醒回調後在pipeline流程鏈中向不同的handler傳遞信息
// 2. 上下文存儲的數據可以實現事件觸發執行傳遞到不同的handler方法中,甚至可以是在不同線程中實現數據的共享,比如以下代碼:
public class MyHandler extends ChannelDuplexHandler{
private ChannelHandlerContext ctx;
public void beforeAdd(ChannelHandlerContext ctx){
// 可以在添加到pipeline之前保存ctx信息
this.ctx = ctx;
}
public void login(String username, password) {
// 將保存的ctx存儲登錄信息並將登錄信息傳遞到責任鏈pipeline下後續的handler獲取使用
ctx.write(new LoginMessage(username, password));
}
}
// 3. 存儲有狀態的信息,詳細可以查看上述的ChannelHandler使用
// 4. 一個handler可以擁有多個context信息,因爲一個handler可以添加到一個或者多個pipeline中,而每個pipeline都會對應着一個context,因而一個handler是可以擁有一個或者多個context,比如計算handler被添加到pipeline的次數
public class FactorialHandler extends ChannelInboundHandlerAdapter{
private final AttributeKey<Integer> counter = AttributeKey.valueOf("counter");
public void channelRead(ChannelHandlerContext ctx, Object msg){
Integer a = ctx.attr(counter).get();
if (a == null) {
a = 1;
}
attr.set(a * (Integer)msg);
}
}
// 下面將會進行4次計數器的計算,也就是一個handler實例添加到不同或者相同的active的pipeline中,其上下文對象是不一樣的
ChannelPipeline p1 = channel.pipeline();
p1.addLast("f1", fh);
p1.addLast("f2", fh);
ChannelPipeline p2 = channel.pipeline();
p1.addLast("f3", fh);
p1.addLast("f4", fh);
ByteBuf組件(重點)
支持的API
- 可以被用戶自定義的緩衝區擴展
- 通過內置的複合緩衝區實現透明的零拷貝
- 容量可以按需增長
- 擁有readerIndex與writeIndex,因而在讀寫之間不需要像NIO的ByteBuffer通過flip()方法進行切換
- 支持方法的鏈式調用
- 支持引用計數
- 使用池化技術
工作原理
存在兩個索引,一個是讀取索引,一個寫入索引,當使用ByteBuf調用read方法的時候,readIndex將會向前移動,即readIndex+1,同樣地,對於寫入數據的時候,對應的writeIndex也會增加,當readIndex==writeIndex
也就意味着讀取數據達到數組的末尾,再次進行讀取時會發生IndexOutOfBoundsException,而對於writeIndex==Capacity
即ByteBuf的容量大小時也會發生下標越界異常,名稱以read或者write將會推進對應的索引數值,而名稱以set或者get的方法調用時將不會對讀取或者寫入索引進行遞增操作.
字節級源碼分析
- 隨機訪問:
ByteBuf
類似於一個字節數組,於數組索引具備相同的特徵,即下標從0開始,以capacity - 1
的下標爲末尾索引,可以按照數組的方式對其通過下標隨機訪問,此時對於擁有readerIndex以及writeIndex值是不變,但可以通過調用readerIndex(index)
或者writeIndex(index)
來更改對應的索引值
for(int i=0; i<buffer.capacity(); i++){
byte b = buffer.getByte(i);
logger.info("char s is " + (char)b);
}
- 順序訪問: 主要是通過
readerIndex
以及writeIndex
兩個索引值指針實現順序訪問,對於ByteBuf
的順序訪問存在丟棄字節,可讀字節以及可寫字節的概念,對此摘錄源碼分析如下:
/*
* +-------------------+------------------+------------------+
* | discardable bytes | readable bytes | writable bytes |
* | | (CONTENT) | |
* +-------------------+------------------+------------------+
* | | | |
* 0 <= readerIndex <= writerIndex <= capacity
*.
*/
// 可讀字節區域: 代表ByteBuf還未讀取到ByteBuf的數據區域,在netty的ByteBuf中以read或者skip開頭的讀取數據的方法都會在指針readerIndex實現計數的自增加操作,對於可讀指針取值範圍: 0<=readerIndex<=writeIndex
// 可寫字節區域: 代表從writeIndex - capacity之間的區域爲空閒區域,能夠繼續存儲數據來填充區域,如果滿足writeIndex < capacity代表該ByteBuf擁有足夠的區域進行寫入數據.
// 丟棄的字節區域: 表示已經被讀取過的ByteBuf片段區域,初始化狀態的時候,區域大小爲0,如果數據一直在被讀取,那麼對應的區域大小會增加到writeIndex,也就是對於該區域滿足: 0<= size <= writeIndex.
// 調用discardReadBytes()方法的區域變化:
/*
* BEFORE discardReadBytes()
*
* +-------------------+------------------+------------------+
* | discardable bytes | readable bytes | writable bytes |
* +-------------------+------------------+------------------+
* | | | |
* 0 <= readerIndex <= writerIndex <= capacity
*
*
* AFTER discardReadBytes()
*
* +------------------+--------------------------------------+
* | readable bytes | writable bytes (got more space) |
* +------------------+--------------------------------------+
* | | |
* readerIndex (0) <= writerIndex (decreased) <= capacity
*/
- 字節清除方法
clear
// 關於清除方法直接通過查看區域的變化即可
/*
* <pre>
* BEFORE clear()
*
* +-------------------+------------------+------------------+
* | discardable bytes | readable bytes | writable bytes |
* +-------------------+------------------+------------------+
* | | | |
* 0 <= readerIndex <= writerIndex <= capacity
*
*
* AFTER clear()
*
* +---------------------------------------------------------+
* | writable bytes (got more space) |
* +---------------------------------------------------------+
* | |
* 0 = readerIndex = writerIndex <= capacity
* </pre>
*/
- 搜索操作
// 對於一些普通字節的查詢,可以調用indexOf(int, int, byte)或者是bytesBefore(int, int, byte)完成,bytesBefore這個方法尤其對於以一些特殊字符結尾的字符串尤其有用.
// 對於更爲複雜的操作,比較存在不同系統的特殊符號查詢,可以通過ByteProcessor接口指定查詢特定的符號內容,調用forEachByte(int, int, ByteProcessor)方法完成字節的搜索
- Derived Buffers(派生緩衝區)
// 可以通過以下的方法創建一個新的ByteBuf緩衝區,每個派生出來的緩衝區都擁有自己的readerIndex,writeIndex以及標記索引,與NIO的buffer議案具備數據共享,因而當派生的緩衝區數據發生變化的時候,對應的源緩衝區也會發生變化.
// 如果要讓數據不具備共享,可以通過使用copy()方法來實現一個新的數據副本,這個時候與派生的數據緩衝區區分在於copy()擁有獨立的數據副本信息,可以通過以下的圖示來分析,假設現在申請的一塊源bytebuf是使用堆外內存存儲數據的方式(堆內內存也是同理),這個時候派生的緩衝與copy的緩衝內存區域分佈如下:
通過上述可知,在JVM堆內存中創建ByteBuf對象,分別指向對應數據存儲的區域,對於Java程序而言,派生緩衝區對象在JVM中創建ByteBuf對象指向原有存儲數據的內存區域,因而對於派生的緩衝區的數據如果發生變化,對應的源緩衝區數據也會發生變化,相比使用copy()的申請數據一份新的數據副本方式,減少了數據內存的複製操作,避免更多的內存佔用冗餘的數據.
- Netty的ByteBuf轉換爲JDK存在的類型
// 1. 轉換爲byte[]
if(byteBuff.hasArray()){
byte[] byteArr = byteBuff.array();
}
// 2. 轉換爲NIO的Buffers
if(byteBuff.nioBufferCount() > 0){
ByteBuffer nioByteBuffer = byteBuff.nioBuffer();
}
// 3. 轉換爲String
String str = byteBuff.toString(Charset.forName("utf8"));
// 4. 轉換爲IO的字節流
ByteBufInputStream in = new ByteBufInputStream(byteBuff);
ByteBufOutputStream out = new ByteBufOutputStream(byteBuff);
ByteBuff使用模式
- 使用堆緩衝區
將數據存儲在JVM堆內存中,也就是從JVM的內存中申請內存區域來存放ByteBuff數據,這種模式稱爲支撐數組(Backing Array)
通過上述可知,在Java程序進行網絡IO數據傳輸的時候,對於存儲在堆內存的緩衝數據,不論是讀取還是寫入數據,都需要經過JVM堆內緩衝,然後將數據複製到操作系統內存的一塊區域中,最後在刷新到網卡設備的時候,需要將內存數據複製到socket緩衝區再進行數據刷新.
- 使用堆外緩衝區
直接從操作系統中申請內存區域來存儲ByteBuff數據,但是分配和釋放內存相對會更爲昂貴,同時如果這個時候不知道時候用於數組相關的數據操作而使用堆外內存的緩衝數據,那麼這個時候就需要進行一次數據內存的複製,相比堆內存操作更爲複雜.
// 現在有一數組數據
byte[] arr = [1,2,3,4,5];
// 這個時候要發送數據出去,可考慮使用堆外內存數據緩衝,避免數據緩衝多一次內存複製,將數據發送到網卡中
// 接收網卡數據的時候,如果這個時候知道接收的數據爲數組且需要堆數組數據進行遍歷,那麼這個時候直接使用堆外內存存儲,還需要再進行一次數據內容的複製,而如果使用堆內內存存儲,直接可以通過直接獲取到數據轉換爲數組進行操作
- 複合緩衝區(零拷貝機制)
通過上述可知,複合緩衝區是將不同存儲物理位置的緩衝區數據合併爲單個緩衝區的虛擬表示,屬於邏輯上述的緩衝區的數據合併,由此可知,如果程序中需要將一塊有關聯但存儲物理位置不同的緩衝區數據進行一起操作的話,可以使用複合緩衝區方式將多個緩衝區數據進行合併,這個時候不存在數據的複製,即零拷貝機制,實現多個緩衝區數據合併爲單個緩衝區,可以視爲單個緩衝區進行操作.
ByteBuff核心類圖
通過上述類圖結構可知,ByteBuff劃分爲三個維度,即
- 存儲方面: 使用堆內與堆外內存存儲
- 資源利用技術: 使用非池化與池化技術,池化技術與內存管理將在講netty高性能會詳細說明
- 直接通過底層操作內存: Unsafe操作
使用ByteBufHolder接口
在一個web服務程序中,如果能夠將一個http請求(請求頭/請求體/狀態碼/cookie等信息)都封裝一起以包的形式進行接收或者發送,那麼對程序開發者而言將會帶來很多的便利,對此,在netty框架提供了ByteBufHolder接口來存儲ByteBuf之外的屬性信息,同樣提供了緩衝池化,底層訪問數據以及引用計數的方法.對於網絡編程中實現自定義的消息協議,可以採用ByteBufHolder接口來有效承載消息的信息.
通過上述可知,ByteBuf具備引用計數(後續在高性能會寫到)特性,能夠在資源不被對象所持有時釋放來優化內存使用和性能的技術.
ByteBuf內存分配策略
- 按需分配:ByteBufAllocator接口
主要實現類有池化與非池化技術實現UnpooledByteBufAllocator以及PooledByteBufAllocator類,netty默認使用使用PooledByteBufAllocator實現池化的ByteBuf,但是對於默認也存在以下的規則:
// 默認創建池化且爲堆外內存存儲的ByteBuf,如果當前環境支持Unsafe底層操作,那麼默認就會使用Unsafe+堆外存儲+池化技術方式來創建ByteBuf
// 如果顯示使用PreferHeapByteBufAllocator方式進行分配,則會創建堆內數據存儲的ByteBuf
ByteBufAllocator的類圖如下:
- Unpooled分配ByteBuf工具類
對上述的ByteBufAllocator進一步封裝,對外提供操作的工具類,關於核心API直接摘錄《Netty實戰》書籍:
- ByteBufUtil操作ByteBuf工具類
// 核心方法
// 1. hexDump: 以16進制形式打印ByteBuf內容
// ByteBuf內容爲: [0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
// 16進制打印出來的數據爲: 00 00 00 01 02 03 04 05 06 07 08 09 0a 0b
// (空格是手動加的,實際爲: 0000000102030405060708090a0b)
// 2. 比較兩個ByteBuf的存儲的數據是否一致(內存 + 數據大小)