如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!
概念
Channel 是java nio的一個基本構造。
它代表一個到實體(如一個硬件設備,一個文件、一個網絡套接字或者一個能夠之行一個或者多個不同的I/O操作的程序組件)的開放鏈接,如讀操作和寫操作。
可以把Channel 看做是傳送(入站)或者傳出(出站)數據的載體。可以被打開或者被關閉,鏈接或者斷開連接。
其UML圖:
分類
Channel:是對網絡Socket的封裝,抽象了網絡I/O的讀、寫、連接與綁定。
AbstractChannel:實現了Channel接口的大部分功能,一次連接用到的Channel、ChannelId、eventLoop、pipelIne、unsafe都會保存在這裏。
AbstractNioChannel:通過select的方式對讀寫事件進行監聽。
客戶端Channel:主要註冊read與write事件,關注於具體數據的讀寫。
服務端Channel:主要註冊accept事件,關注於具體連接的接入,這也是與客戶端Channel的read事件最主要的區別。
所有方法,如圖:
Channel重要方法和參數
eventLoop: 返回分配給Channel 的EventLoop
pipeline: 返回分配給Channel 的ChannelPipeline
isActive: 如果Channel 是活動的,則返回true。活動的意義可能依賴於底層的傳輸。例如,一個Socket 傳輸一旦連接到了遠程節點便是活動的,而一個Datagram 傳輸一旦被打開便是活動的。
localAddress: 返回本地的SokcetAddress
remoteAddress: 返回遠程的SocketAddress
write: 將數據寫到遠程節點。這個數據將被傳遞給ChannelPipeline,並且排隊直到它被沖刷
flush: 將之前已寫的數據沖刷到底層傳輸,如一個Socket
writeAndFlush: 一個簡便的方法,等同於調用write()並接着調用flush()
生命週期狀態
ChannelUnregistered :Channel 已經被創建,但還未註冊到EventLoop
ChannelRegistered :Channel 已經被註冊到了EventLoop
ChannelActive :Channel 處於活動狀態(已經連接到它的遠程節點)。它現在可以接收和發送數據了
ChannelInactive :Channel 沒有連接到遠程節點
當這些狀態發生改變時,將會生成對應的事件。這些事件將會被轉發給ChannelPipeline 中的ChannelHandler,其可以隨後對它們做出響應。
狀態的切換:
相關源碼
1. AbstractChannel
public abstract class AbstractChannel extends DefaultAttributeMap implements Channel {
// 父 Channel(NioServerSocketChannel 是沒有父channel的)
private final Channel parent;
// Channel 唯一ID
private final ChannelId id;
// Unsafe 對象,封裝 ByteBuf 的讀寫操作
private final Unsafe unsafe;
// 關聯的 Pipeline 對象
private final DefaultChannelPipeline pipeline;
private final VoidChannelPromise unsafeVoidPromise = new VoidChannelPromise(this, false);
private final CloseFuture closeFuture = new CloseFuture(this);
// 本地地址
private volatile SocketAddress localAddress;
//遠端地址
private volatile SocketAddress remoteAddress;
// EventLoop 封裝的 Selector,channel所註冊的eventLoop
private volatile EventLoop eventLoop;
// 是否註冊
private volatile boolean registered;
private boolean closeInitiated;
/** Cache for the string representation of this channel */
private boolean strValActive;
private String strVal;
構造函數
protected AbstractChannel(Channel parent, ChannelId id) {
this.parent = parent;
this.id = id;
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
// Unsafe 實現交給子類實現
protected abstract AbstractUnsafe newUnsafe();
// 創建 DefaultChannelPipeline 對象
protected DefaultChannelPipeline newChannelPipeline() {
return new DefaultChannelPipeline(this);
}
說明
1.Unsafe類裏實現了具體的連接與寫數據。比如:網絡的讀,寫,鏈路關閉,發起連接等。之所以命名爲unsafe是不希望外部使用,並非是不安全的。
2.DefaultChannelPipeline 只是一個 Handler 的容器,也可以理解爲一個Handler鏈,具體的邏輯由Handler處理,而每個Handler都會分配一個EventLoop,最終的請求還是要EventLoop來執行,而EventLoop中又調用Channel中的內部類Unsafe對應的方法。
新建一個channel會自動創建一個ChannelPipeline。
3.這裏創建 DefaultChannelPipeline,構造中傳入當前的 Channel,而讀寫數據都是在 ChannelPipeline 中進行的,ChannelPipeline 進行讀寫數據又委託給 Channel 中的 Unsafe 進行操作。
2.AbstractNioChannel
public abstract class AbstractNioChannel extends AbstractChannel {
// 抽象了 SocketChannel 和 ServerSocketChannel 的公共的父類
//Socketchannle和ServerSocketChannel的公共操作類,用來設置SelectableChannel相關參數和IO操作
private final SelectableChannel ch;
// SelectionKey.OP_READ 讀事件
protected final int readInterestOp;
// 註冊到 selector 上返回的 selectorKey
volatile SelectionKey selectionKey;
// 是否還有未讀的數據
boolean readPending;
private final Runnable clearReadPendingRunnable = new Runnable() {
@Override
public void run() {
clearReadPending0();
}
};
/**
* The future of the current connection attempt. If not null, subsequent
* connection attempts will fail.
*/
// 連接操作的結果
private ChannelPromise connectPromise;
// 連接超時定時任務
private ScheduledFuture<?> connectTimeoutFuture;
// 客戶端地址
private SocketAddress requestedRemoteAddress;
....
核心方法:
//核心操作,註冊操作
//1) 如果當前註冊返回的selectionKey已經被取消,則拋出CancelledKeyException異常,捕獲該異常進行處理。
//2) 如果是第一次處理該異常,調用多路複用器的selectNow()方法將已經取消的selectionKey從多路複用器中刪除掉。操作成功之後,將selected置爲true, 說明之前失效的selectionKey已經被刪除掉。繼續發起下一次註冊操作,如果成功則退出,
//3) 如果仍然發生CancelledKeyException異常,說明我們無法刪除已經被取消的selectionKey,按照JDK的API說明,這種意外不應該發生。如果發生這種問題,則說明可能NIO的相關類庫存在不可恢復的BUG,直接拋出CancelledKeyException異常到上層進行統一處理。
protected void doRegister() throws Exception {
boolean selected = false;
for (;;) {
try {
selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
return;
} catch (CancelledKeyException e) {
if (!selected) {
// Force the Selector to select now as the "canceled" SelectionKey may still be
// cached and not removed because no Select.select(..) operation was called yet.
eventLoop().selectNow();
selected = true;
} else {
// We forced a select operation on the selector before but the SelectionKey is still cached
// for whatever reason. JDK bug ?
throw e;
}
}
}
}
}
SelectionKey 常量值
public abstract class SelectionKey {
public static final int OP_READ = 1 << 0; //讀操作位
public static final int OP_WRITE = 1 << 2; //寫操作位
public static final int OP_CONNECT = 1 << 3; //客戶端連接操作位
public static final int OP_ACCEPT = 1 << 4; //服務端接受連接操作位
//如果註冊的操作位爲0表示只是完成註冊功能,說明對任何事件都不感興趣
doBeginRead() 讀之前的準備
@Override
protected void doBeginRead() throws Exception {
// Channel.read() or ChannelHandlerContext.read() was called
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return; //key無效的話直接返回
}
readPending = true; //表示讀pending中
final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) { //表示當前沒有讀操作位
selectionKey.interestOps(interestOps | readInterestOp); //設置讀操作位
}
}
//SelectionKey中定義的是否可讀操作
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
}
3.AbstractNioByteChannel
doWrite操作
配置中設置循環次數是避免半包中數據量過大,IO線程一直嘗試寫操作,此時IO線程無法處理其他IO操作或者定時任務,比如新的消息或者定時任務,如果網絡IO慢或者對方讀取慢等造成IO線程假死的狀態.
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
int writeSpinCount = -1; // 寫自選次數
boolean setOpWrite = false; //寫操作位爲0
for (;;) {
Object msg = in.current();
if (msg == null) { //從環形數組ChannelOutboundBuffer彈出一條消息,如果爲null,表示消息已經發送完成,
// Wrote all messages.
clearOpWrite(); //清除寫標誌位,退出循環
// Directly return here so incompleteWrite(...) is not called.
return;
}
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
int readableBytes = buf.readableBytes();
if (readableBytes == 0) { //如果可讀字節爲0,則丟棄該消息,循環處理其他消息
in.remove();
continue;
}
boolean done = false; //消息是否全部發送完畢表示
long flushedAmount = 0; //發送的字節數量
if (writeSpinCount == -1) {
//如果爲-1的時候從配置中獲取寫循環次數
writeSpinCount = config().getWriteSpinCount();
}
for (int i = writeSpinCount - 1; i >= 0; i --) {
int localFlushedAmount = doWriteBytes(buf); //由子類實現寫
if (localFlushedAmount == 0) { //這裏表示本次發送字節爲0,發送TCP緩衝區滿了,所以此時爲了避免空循環一直髮送,這裏就將半包寫表示設置爲true並退出循環
setOpWrite = true;
break;
}
//發送成功就對發送的字節計數
flushedAmount += localFlushedAmount;
if (!buf.isReadable()) { //如果沒有可讀字節,表示已經發送完畢
done = true; //表示發送完成,並退出循環
break;
}
}
//通知promise當前寫的進度
in.progress(flushedAmount);
if (done) { //如果發送完成,移除緩衝的數據
in.remove();
} else {
如果沒有完成會調用incompleteWrite方法
// Break the loop and so incompleteWrite(...) is called.
break;
}
} else if (msg instanceof FileRegion) { //這個是文件傳輸和上面類似
FileRegion region = (FileRegion) msg;
boolean done = region.transferred() >= region.count();
if (!done) {
long flushedAmount = 0;
if (writeSpinCount == -1) {
writeSpinCount = config().getWriteSpinCount();
}
for (int i = writeSpinCount - 1; i >= 0; i--) {
long localFlushedAmount = doWriteFileRegion(region);
if (localFlushedAmount == 0) {
setOpWrite = true;
break;
}
flushedAmount += localFlushedAmount;
if (region.transferred() >= region.count()) {
done = true;
break;
}
}
in.progress(flushedAmount);
}
if (done) {
in.remove();
} else {
// Break the loop and so incompleteWrite(...) is called.
break;
}
} else {
// Should not reach here.
throw new Error();
}
}
//如果沒有完成寫看看需要做的事情
incompleteWrite(setOpWrite);
}
//未完成寫操作,看看操作
protected final void incompleteWrite(boolean setOpWrite) {
// Did not write completely.
if (setOpWrite) { //如果當前的寫操作位true,那麼當前多路複用器繼續輪詢處理
setOpWrite();
} else { //否則重新新建一個task任務,讓eventLoop後面點執行flush操作,這樣其他任務才能夠執行
// Schedule flush again later so other tasks can be picked up in the meantime
Runnable flushTask = this.flushTask;
if (flushTask == null) {
flushTask = this.flushTask = new Runnable() {
@Override
public void run() {
flush();
}
};
}
eventLoop().execute(flushTask);
}
}
4.AbstractNioMessageChannel
doWrite操作
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
final SelectionKey key = selectionKey();
final int interestOps = key.interestOps();
for (;;) {
Object msg = in.current();
if (msg == null) {
// Wrote all messages.
if ((interestOps & SelectionKey.OP_WRITE) != 0) {
key.interestOps(interestOps & ~SelectionKey.OP_WRITE);
}
break;
}
try {
boolean done = false;
for (int i = config().getWriteSpinCount() - 1; i >= 0; i--) {
if (doWriteMessage(msg, in)) {
done = true;
break;
}
}
if (done) {
in.remove();
} else {
// Did not write all messages.
if ((interestOps & SelectionKey.OP_WRITE) == 0) {
key.interestOps(interestOps | SelectionKey.OP_WRITE);
}
break;
}
} catch (Exception e) {
if (continueOnWriteError()) {
in.remove(e);
} else {
throw e;
}
}
}
}
AbstractNioMessageChannel 和AbstractNioByteChannel的消息發送實現比較相似,
不同之處在於:一個發送的是ByteBuf或者FileRegion,它們可以直接被髮送;另一個發送的則是POJO對象。
如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!