本文分享以下內容
- Unsafe概況
- AbstractUnsafe 分析
- AbstractNioUnsafe分析
- NioByteUnsafe分析
- NioMessageUnsafe分析
Unsafe概況
unsafe是Channel的內部邏輯實現,實際的IO操作都是由unsafe完成的。
unsafe調用原生的NIO API處理IO操作,使用Pipeline傳遞觸發的channel事件,使用EventLoop進行無鎖化處理。
unsafe調用原生Nio執行bind(),register(),觸發各種事件 ,更新網絡標識位。
類繼承圖如下
NioByteUnsafe用於客戶端,NioMessageUnsafe用於服務端
AbstractUnsafe 分析
舉例一個典型方法register
@Override
public final void register(final ChannelPromise promise) {
if (eventLoop.inEventLoop()) {//根據 eventLoop 屬性判斷 是否 使用 線程池執行
register0(promise);
} else {
try {
eventLoop.execute(new Runnable() {
@Override
public void run() {
register0(promise);
}
});
} catch (Throwable t) {
closeForcibly();// 調用原生 NIO 關閉
closeFuture.setClosed();//設置關閉
promise.setFailure(t);// save Exception
}
}
}
private void register0(ChannelPromise promise) {
try {
if (!ensureOpen(promise)) {
return;
}
doRegister();//由子類實現
registered = true;
promise.setSuccess();
pipeline.fireChannelRegistered();//使用pipeline 傳遞 註冊事件
if (isActive()) {
pipeline.fireChannelActive();
}
} catch (Throwable t) {
//...
}
}
}
處理邏輯: 先看是否在eventLoop的線程池中執行 ,執行完IO處理,pipeline觸發Registered事件.
而 bind,disconnect,close,write 不需要線程控制和觸發事件參與。
具體的IO 處理都由子類實現。
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
if (!ensureOpen(promise)) {
return;
}
boolean wasActive = isActive();
try {
doBind(localAddress);//調用原生NIO API 執行bind
} catch (Throwable t) {
promise.setFailure(t);
closeIfClosed();
return;
}
if (!wasActive && isActive()) {
invokeLater(new Runnable() { //提交任務執行bind()
@Override
public void run() {//
pipeline.fireChannelActive();
}
});
}
promise.setSuccess();
}
bind 實現比較簡單 沒有EventLoop介入。
doBind有2個實現
NioServerSocketChannel-doBind
protected void doBind(SocketAddress localAddress) throws Exception {
javaChannel().socket().bind(localAddress, config.getBacklog());
}
NioSocketChannel-doBind
protected void doBind(SocketAddress localAddress) throws Exception {
javaChannel().socket().bind(localAddress);
}
NioSocketChannel是客戶端,這個bind沒有啥用。
protected void doDeregister() throws Exception {
//取消當前eventLoop中SelectionKey的所有註冊的channel。
eventLoop().cancel(selectionKey());
}
public void beginRead() {
if (!isActive()) {
return;
}
try {
doBeginRead();
} catch (final Exception e) {
//...
}
}
doBeginRead由AbstractNioChannel實現 用於修改網絡標識位,NioServerSocketChannel 修改成SelectionKey.OP_ACCEPT,NioSocketChannel 修改成SelectionKey.OP_READ
write和flush方法在寫數據有詳細分析。
小結:AbstractUnsafe 定義了原生NIO,EventLoop,Pipeline協作處理IO 的框架的多個方法,是Netty IO的操作的模板類。
AbstractNioUnsafe分析
public interface NioUnsafe extends Unsafe {
/** underlying :底層的,可理解爲原生NIO的SelectableChannel
* Return underlying {@link SelectableChannel} 返回底層的SelectableChannel
*/
SelectableChannel ch();//獲取原生channel
/**
* Finish connect 完成連接
*/
void finishConnect();
/**
* Read from underlying {@link SelectableChannel} //從原生NIO的SelectableChannel讀取數據
*/
void read();
void forceFlush();//調用flush0
}
Unsafe 中沒定義 read, NioUnsafe 定義了 read 不知道爲啥這樣搞
AbstractNioUnsafe
實現了 返回原生SelectableChannel的方法
實現了connect的框架
先分析一下 doConnect方法
@Override
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
if (localAddress != null) {
javaChannel().socket().bind(localAddress);
}
boolean success = false;
try {
boolean connected = javaChannel().connect(remoteAddress);
if (!connected) {
selectionKey().interestOps(SelectionKey.OP_CONNECT);//line A
}
success = true;
return connected;
} finally {
if (!success) {
doClose();
}
}
}
當SocketChannel設置非阻塞時 connect()不會等待服務端應答,會立即返回false, 當服務應答或連接被拒絕(服務端掛了,或網絡不通)會觸發 OP_CONNECT事件。netty 將SocketChannel默認設置成非阻塞。
OP_CONNECT事件觸發執行finishConnect() ;
服務端接受連接後,客戶端需要執行finishConnect()。
@Override
public void finishConnect() {
// Note this method is invoked by the event loop only if the connection attempt was
// neither cancelled nor timed out.
assert eventLoop().inEventLoop();
assert connectPromise != null;
try {
boolean wasActive = isActive();
doFinishConnect();
fulfillConnectPromise(connectPromise, wasActive);
} catch (Throwable t) {
if (t instanceof ConnectException) {
Throwable newT = new ConnectException(t.getMessage() + ": " + requestedRemoteAddress);
newT.setStackTrace(t.getStackTrace());
t = newT;
}
// Use tryFailure() instead of setFailure() to avoid the race against cancel().
connectPromise.tryFailure(t);
closeIfClosed();
} finally {
// Check for null as the connectTimeoutFuture is only created if a connectTimeoutMillis > 0 is used
// See https://github.com/netty/netty/issues/1770
if (connectTimeoutFuture != null) {
connectTimeoutFuture.cancel(false);
}
connectPromise = null;
}
}
處理邏輯:驗證連接是否成功,如果失敗 記錄異常信息,嘗試執行channel關閉。無論成功失敗都執行取消定時任務,將connectPromise賦值爲null。
connect是AbstractNioUnsafe中最重要的方法,用於客戶端連接服務端。
@Override
public void connect(
final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
if (!ensureOpen(promise)) {// 校驗SocketChannel isOpen
return;
}
try {
if (connectPromise != null) {//校驗 connection是否已經執行
throw new IllegalStateException("connection attempt already made");
}
boolean wasActive = isActive();//記錄活動狀態
if (doConnect(remoteAddress, localAddress)) {//NioSocketChannel實現,使用原生NIO連接服務端
fulfillConnectPromise(promise, wasActive);//填充執行結果
} else {//還未成功連接 // line 1
connectPromise = promise;
requestedRemoteAddress = remoteAddress;
// Schedule connect timeout.
int connectTimeoutMillis = config().getConnectTimeoutMillis();//默認值30000
if (connectTimeoutMillis > 0) {//添加一個延遲任務,connectTimeoutMillis 毫秒後只執行1次
connectTimeoutFuture = eventLoop().schedule(new Runnable() {{// line 2
@Override//這個任務處理連接超時,記錄異常, 執行關閉 將channel從selector上卸載。
public void run() {
ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
ConnectTimeoutException cause =
new ConnectTimeoutException("connection timed out: " + remoteAddress);
if (connectPromise != null && connectPromise.tryFailure(cause))
close(voidPromise()); //line 3
}
}
}, connectTimeoutMillis, TimeUnit.MILLISECONDS);
}
promise.addListener(new ChannelFutureListener() {// line 4
@Override
public void operationComplete(ChannelFuture future) throws Exception {
if (future.isCancelled()) {// line5
if (connectTimeoutFuture != null) {
connectTimeoutFuture.cancel(false);
}
connectPromise = null;
close(voidPromise());
}
}
});
}
} catch (Throwable t) {
if (t instanceof ConnectException) {
Throwable newT = new ConnectException(t.getMessage() + ": " + remoteAddress);
newT.setStackTrace(t.getStackTrace());
t = newT;
}
promise.tryFailure(t);
closeIfClosed();
}
}
line 2 添加一個延遲任務處理連接超時,簡稱 "任務A" ,它用於處理連接超時,記錄異常, 執行關閉 將channel從selector上卸載。
line 3 執行關閉 將channel從selector上卸載。
結合 doConnect() 的分析,通常會進入line1。如果超時時間大於0,添加任務A。注意當指定完doConnect() 後,當服務應答或連接被拒絕會觸發事件調用finishConnect(),只要執行了finishConnect()後續都不會觸發定時任務。推測當服務器長時間沒有應答纔有可能觸發任務A。
line4 添加一個listener 執行connectPromise.tryFailure(cause) 後被觸發。
line5 代碼塊內 取消定時任務,記錄異常,執行關閉 將channel從selector上卸載。
future 類型是 DefaultChannelPromise future.isCancelled()==true 什麼時候?
DefaultPromise
private static final CauseHolder CANCELLATION_CAUSE_HOLDER = new CauseHolder(new CancellationException());
public boolean cancel(boolean mayInterruptIfRunning) {
Object result = this.result;
if (isDone0(result) || result == UNCANCELLABLE) {
return false;// 操作執行執行完成 (result==SUCCESS) 或 不能取消
}
synchronized (this) {
// Allow only once.
result = this.result;
if (isDone0(result) || result == UNCANCELLABLE) {
return false;再次檢查
}
this.result = CANCELLATION_CAUSE_HOLDER;//設置 被取消
if (hasWaiters()) {
notifyAll();//解除阻塞
}
}
notifyListeners();//觸發監聽器
return true;
}
DefaultPromise 繼承了Future,實現了其 cancel(boolean b)方法,也就是DefaultPromise和它的子類調用了cancel(boolean b)方法,future.isCancelled()==true 才能成立
綜上所述:當 promise執行取消後,line5 代碼塊執行了收尾工作,也就是說 promise.addListener() 是爲了執行收尾工作。
填充連接用到的ChannelPromise
private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
// trySuccess() will return false if a user cancelled the connection attempt.
//trySuccess() 將返回false 如果用戶試圖取消連接。
boolean promiseSet = promise.trySuccess();//給promise填充 執行成功
// Regardless if the connection attempt was cancelled, channelActive() event should be triggered,不管連接是否被取消,channelActive事件讀應該被觸發
// because what happened is what happened. 因爲 已經發生的就是已經發生的
if (!wasActive && isActive()) {//成功的 connect ,觸發 ChannelActive事件。
pipeline().fireChannelActive();
}
// If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
if (!promiseSet) {//用戶取消連接,執行關閉SocketChannel處理。
close(voidPromise());
}
}
// 真正寫數據的地方將緩存好的數據
@Override
protected void flush0() {
// Flush immediately only when there's no pending flush.
// If there's a pending flush operation, event loop will call forceFlush() later,
// and thus there's no need to call it now.
if (isFlushPending()) {
return;
}
super.flush0();
}
@Override
public void forceFlush() {
// directly call super.flush0() to force a flush now
super.flush0();
}
forceFlush 依賴 super.flush0(),在寫事件觸發時調用。
flush系列的詳細分析請看 《寫數據》篇
//檢查是否是寫事件觸發的flush
private boolean isFlushPending() {
SelectionKey selectionKey = selectionKey();
return selectionKey.isValid() && (selectionKey.interestOps() & SelectionKey.OP_WRITE) != 0;
}
小結:AbstractNioUnsafe實現了 客戶端連接系列方法,重寫flush,如果是事件觸發則不執行。
NioByteUnsafe分析
重要的方法 只有一個 read()
讀數據分析。
@Override
public void read() {
final ChannelConfig config = config();
final ChannelPipeline pipeline = pipeline();
final ByteBufAllocator allocator = config.getAllocator();//默認UnpooledByteBufAllocator
final int maxMessagesPerRead = config.getMaxMessagesPerRead();//默認 16
RecvByteBufAllocator.Handle allocHandle = this.allocHandle;
if (allocHandle == null) {
this.allocHandle = allocHandle = config.getRecvByteBufAllocator().newHandle();
}
if (!config.isAutoRead()) {
removeReadOp();
}
ByteBuf byteBuf = null;
int messages = 0;
boolean close = false;
try {
//allocHandle type is AdaptiveRecvByteBufAllocator$HandleImpl 用於容量預估
int byteBufCapacity = allocHandle.guess();////容量預估
int totalReadAmount = 0;//統計讀取的總字節數,用於容量預估的根據。
do {
byteBuf = allocator.ioBuffer(byteBufCapacity);//創建ByteBuf type is UnpooledUnsafeDirectByteBuf
int writable = byteBuf.writableBytes();
//會出現一次只能讀取部分數據的情況麼??問題A
int localReadAmount = doReadBytes(byteBuf);
if (localReadAmount <= 0) {//沒讀到數據或channel關閉
// not was read release the buffer
byteBuf.release();
close = localReadAmount < 0;//channel 關閉 localReadAmount ==-1
break;
}
pipeline.fireChannelRead(byteBuf);//觸發channelRead
byteBuf = null;
if (totalReadAmount >= Integer.MAX_VALUE - localReadAmount) {
// Avoid overflow. 避免 totalReadAmount 數值越界
totalReadAmount = Integer.MAX_VALUE;
break;
}
totalReadAmount += localReadAmount;
if (localReadAmount < writable) {//讀取的總數比buf可寫數小,說明可讀的數據已經讀完,跳出循環即可。
// Read less than what the buffer can hold,
// which might mean we drained the recv buffer completely.
break;
}
} while (++ messages < maxMessagesPerRead);//循環超過maxMessagesPerRead次數停止。
pipeline.fireChannelReadComplete();//觸發 ChannelReadComplete事件
allocHandle.record(totalReadAmount);//調整容量。
if (close) {// 需要關閉 ,執行關閉流程
closeOnRead(pipeline);
close = false;
}
} catch (Throwable t) {//處理異常
handleReadException(pipeline, byteBuf, t, close);
}
}
}
問題A有能讀取到部分數據, 因爲只要select() 一次獲取到數據 就會全部讀取。
NioServerSocketChannel-doReadBytes
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
return byteBuf.writeBytes(javaChannel(), byteBuf.writableBytes());
}
從SocketChannel中讀取數據到byteBuf中去
容量分配器AdaptiveRecvByteBufAllocator分析
這段代碼獲取了容量分配器
this.allocHandle = allocHandle = config.getRecvByteBufAllocator().newHandle();
跟蹤代碼
public RecvByteBufAllocator getRecvByteBufAllocator() {
return rcvBufAllocator;
}
DefaultChannelConfig
private volatile RecvByteBufAllocator rcvBufAllocator = DEFAULT_RCVBUF_ALLOCATOR;
private static final RecvByteBufAllocator DEFAULT_RCVBUF_ALLOCATOR = AdaptiveRecvByteBufAllocator.DEFAULT;
AdaptiveRecvByteBufAllocator
public static final AdaptiveRecvByteBufAllocator DEFAULT = new AdaptiveRecvByteBufAllocator();
private AdaptiveRecvByteBufAllocator() {
this(DEFAULT_MINIMUM, DEFAULT_INITIAL, DEFAULT_MAXIMUM);//this(64,1024,65536)
}
static {
List<Integer> sizeTable = new ArrayList<Integer>();
for (int i = 16; i < 512; i += 16) {
sizeTable.add(i);
}
for (int i = 512; i > 0; i <<= 1) {
sizeTable.add(i);
}
SIZE_TABLE = new int[sizeTable.size()];
for (int i = 0; i < SIZE_TABLE.length; i ++) {
SIZE_TABLE[i] = sizeTable.get(i);
}
}
SIZE_TABLE數據:[16,32,48,64,80,96,112,128,144,160,176,192,208,224,240,256,272,288,304,320,336,352,368,384,400,416,432,448,464,480,496,512,1024,2048,4096,8192,16384,32768,65536,131072,262144,524288,1048576,2097152,4194304,8388608,16777216,33554432,67108864,134217728,268435456,536870912,1073741824]
SIZE_TABLE 是一個容量預估的數組,每個元素都是一個字節數, 512 之前 從16開始,每次遞增16,512後每次 2倍。
public AdaptiveRecvByteBufAllocator(int minimum, int initial, int maximum) {
//...
int minIndex = getSizeTableIndex(minimum);//二分查找獲取minimum的位置
if (SIZE_TABLE[minIndex] < minimum) {//調整預估值,使預估值一定大於等於minimum
this.minIndex = minIndex + 1;
} else {
this.minIndex = minIndex;
}//minIndex 最後等於3
int maxIndex = getSizeTableIndex(maximum);
if (SIZE_TABLE[maxIndex] > maximum) {
this.maxIndex = maxIndex - 1;
} else {
this.maxIndex = maxIndex;//
}//minIndex 最後等於38
this.initial = initial;//1024
}
其內部類創建
public Handle newHandle() {
return new HandleImpl(minIndex, maxIndex, initial);// minIndex==3,maxIndex==38,initial==1024
}
HandleImpl(int minIndex, int maxIndex, int initial) {
this.minIndex = minIndex;
this.maxIndex = maxIndex;
index = getSizeTableIndex(initial);//二分查找獲取initial的index
nextReceiveBufferSize = SIZE_TABLE[index];
}
@Override
public void record(int actualReadBytes) {
//實際讀取bytes數小於等於較小的容量數進縮容
if (actualReadBytes <= SIZE_TABLE[Math.max(0, index - INDEX_DECREMENT - 1)]) {
if (decreaseNow) {//是否立即縮容
index = Math.max(index - INDEX_DECREMENT, minIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
} else {
decreaseNow = true;
}
//實際讀取bytes數大於等於當前的容量數進行擴容
} else if (actualReadBytes >= nextReceiveBufferSize) {
index = Math.min(index + INDEX_INCREMENT, maxIndex);
nextReceiveBufferSize = SIZE_TABLE[index];
decreaseNow = false;
}
}
}
record實現了數組的容量調整
小結:AdaptiveRecvByteBufAllocator通過容量增長經驗定義一個 容量預估的SIZE_TABLE ,實現了 容量預估,和容量調整的功能。
NioMessageUnsafe分析
服務端用到的Unsafe
private final List<Object> readBuf = new ArrayList<Object>();
NioMessageUnsafe中最重要的就是read方法了,這個方法用於初始化NioSocketChannel。
詳情參照《NioSocketChanel初始化》篇
下面總結一下NioByteUnsafe和NioMessageUnsafe的區別
它們主要是read方法的區別,下面根據功能,使用頻率,重要度區分。
NioByteUnsafe:read方法讀取channel數據 常用 重要
NioMessageUnsafe:read方法初始化 NioSocketChannel 常用 重要
write方法也很重要,但不是NioByteUnsafe和NioMessageUnsafe的主要實現,詳細見《寫數據》篇。
總結:本文分析了Unsafe體系的各個類以及其重要的方法,AbstractUnsafe實現IO操作處理的框架,AbstractNioUnsafe實現了客戶端連接系列方法強制刷新系列方法,NioByteUnsafe實現客戶端讀取數據的read方法,NioMessageUnsafe實現了 初始化 NioSocketChanne的read方法。