
做Netty源碼分析繞不開兩個最基礎(chǔ)也最容易被混為一談的操作客戶端的Connect服務(wù)端的Bind。我最初啃源碼時也有個錯覺這兩個操作不都是“把channel往某個地址上一掛”么一個連遠(yuǎn)程一個綁本地好像只是參數(shù)不同。真把調(diào)用鏈一路追下去才發(fā)現(xiàn)從入口到NIO底層的興趣事件處理它們完完全全是兩套邏輯只是中段共用了一段注冊代碼導(dǎo)致表面看著很像。這篇Connect與Bind對比總結(jié)就把這兩條鏈路從入口到NIO事件循環(huán)徹底拆開講順便把backlog、connectTimeoutMillis、OP_ACCEPT和OP_CONNECT這幾個大家經(jīng)常面試被問、排查時又搞不清的細(xì)節(jié)一并說透。無論你是在準(zhǔn)備Netty相關(guān)面試還是排查線上連接不上、端口占用這類問題這篇都能當(dāng)一份源碼級的速查手冊用。1. 先說結(jié)論Connect與Bind招式像內(nèi)功完全兩套1.1 從一段最常見的使用代碼說起先用最典型的兩段代碼把場景立住。服務(wù)端是這么寫的ServerBootstrap serverBootstrap new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new ServerHandler()); } }); serverBootstrap.bind(8080).sync();客戶端是這么寫的Bootstrap bootstrap new Bootstrap(); bootstrap.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializerSocketChannel() { Override protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new ClientHandler()); } }); bootstrap.connect(127.0.0.1, 8080).sync();從使用者的角度兩個操作都返回ChannelFuture都可以.sync()阻塞等待失敗都能拿到.cause()寫起來體感高度一致。但它們背后的channel類型都不同一個是NioServerSocketChannel一個是NioSocketChannel這已經(jīng)暗示了后續(xù)走向完全不同。接下來從入口開始一追到底。1.2 兩條鏈路的“形似”與“神離”我把兩條調(diào)用鏈從入口到最終落到底層NIO的路徑壓縮成一句話bind鏈路ServerBootstrap.bind()→AbstractBootstrap.doBind()→initAndRegister()→doBind0()→channel.bind()→pipeline.fireChannelBind()→NioServerSocketChannel.doBind()→ServerSocketChannel.bind(port, backlog)connect鏈路Bootstrap.connect()→doResolveAndConnect()→initAndRegister()→channel.connect()→pipeline.fireChannelConnect()→NioSocketChannel.doConnect()→SocketChannel.connect(remoteAddress)兩條鏈都在initAndRegister()這里匯合完成channel創(chuàng)建和注冊到NioEventLoop的selector之后立刻分道揚(yáng)鑣。后面會看到真正的格局差異在NioEventLoop處理就緒事件時才暴露出來bind之后注冊O(shè)P_ACCEPT等新連接connect之后先注冊O(shè)P_CONNECT等連接完成完成之后再轉(zhuǎn)向OP_READ讀寫數(shù)據(jù)。1.3 這篇總結(jié)適合誰讀完能帶走什么這篇總結(jié)適合這么幾類人正準(zhǔn)備Netty源碼面試需要把啟動流程講清楚的人線上遇到“偶爾連不上”、“大量TIME_WAIT”、“端口明明沒占用卻bind失敗”這類問題想從原理層面找切入點(diǎn)的開發(fā)以及剛開始看Netty源碼被doBind0、initAndRegister這些方法名繞暈的自學(xué)者。讀完你至少能帶走四樣?xùn)|西第一Connect和Bind在傳播路徑上的真正分水嶺第二OP_ACCEPT與OP_CONNECT在NioEventLoop里是怎么被區(qū)別處理的第三backlog、connectTimeoutMillis這些參數(shù)到底作用在哪一層第四遇到bind失敗、connect超時這些常見錯誤時一套可復(fù)用的排查思路。2. 入口分叉Bootstrap.connect與ServerBootstrap.bind在AbstractBootstrap里怎么走2.1 bind()的調(diào)用鏈從validate到doBind0ServerBootstrap本身沒有重寫bind方法它直接繼承了AbstractBootstrap的bind(SocketAddress localAddress)。我們跟進(jìn)去看核心的doBindprivate ChannelFuture doBind(final SocketAddress localAddress) { final ChannelFuture regFuture initAndRegister(); final Channel channel regFuture.channel(); if (regFuture.cause() ! null) { return regFuture; } if (regFuture.isDone()) { ChannelPromise promise channel.newPromise(); doBind0(regFuture, channel, localAddress, promise); return promise; } else { final PendingRegistrationPromise promise new PendingRegistrationPromise(channel); regFuture.addListener(new ChannelFutureListener() { Override public void operationComplete(ChannelFuture future) throws Exception { Throwable cause future.cause(); if (cause ! null) { promise.setFailure(cause); } else { promise.registered(); doBind0(regFuture, channel, localAddress, promise); } } }); return promise; } }這段代碼的關(guān)鍵邏輯在于initAndRegister()是異步的channel注冊到EventLoop上不一定已經(jīng)完成所以這里分兩種情況處理——注冊已完成就直接doBind0未完成就掛一個監(jiān)聽器等注冊完成后再執(zhí)行doBind0。這是Netty異步編程的一個典型設(shè)計后續(xù)動作不一定立刻執(zhí)行但它一定會在正確的時間點(diǎn)被觸發(fā)。繼續(xù)看doBind0這里有個值得一提的小細(xì)節(jié)private static void doBind0( final ChannelFuture regFuture, final Channel channel, final SocketAddress localAddress, final ChannelPromise promise) { channel.eventLoop().execute(new Runnable() { Override public void run() { if (regFuture.isSuccess()) { channel.bind(localAddress, promise).addListener(ChannelFutureListener.CLOSE_ON_FAILURE); } else { promise.setFailure(regFuture.cause()); } } }); }注意兩個點(diǎn)。第一channel.bind()被放進(jìn)了eventLoop().execute()里執(zhí)行保證了channel操作一定在它所屬的IO線程上串行發(fā)生從根源上避免了多線程并發(fā)寫channel的問題。第二這里加了一個CLOSE_ON_FAILURE監(jiān)聽器bind一旦失敗channel會被自動關(guān)閉這是個很容易被忽略的兜底機(jī)制——所以線上bind失敗后即使你不手動close這個channel也不會泄漏。2.2 connect()的調(diào)用鏈doResolveAndConnect先把地址解析交給線程池Bootstrap重寫了connect但真正的實(shí)現(xiàn)不在connect里而是轉(zhuǎn)到了doResolveAndConnectprivate ChannelFuture doResolveAndConnect( final SocketAddress remoteAddress, final SocketAddress localAddress) { final ChannelFuture regFuture initAndRegister(); final Channel channel regFuture.channel(); if (regFuture.isDone()) { if (!regFuture.isSuccess()) { return regFuture; } return doResolveAndConnect0(channel, remoteAddress, localAddress, channel.newPromise()); } else { final PendingRegistrationPromise promise new PendingRegistrationPromise(channel); regFuture.addListener(new ChannelFutureListener() { Override public void operationComplete(ChannelFuture future) throws Exception { Throwable cause future.cause(); if (cause ! null) { promise.setFailure(cause); } else { promise.registered(); doResolveAndConnect0(channel, remoteAddress, localAddress, promise); } } }); return promise; } }結(jié)構(gòu)與doBind幾乎一致也都是等注冊完成后再發(fā)起真正的連接動作。接下來看doResolveAndConnect0這里就出現(xiàn)了和bind的第一個顯著差異private ChannelFuture doResolveAndConnect0( final Channel channel, SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) { try { EventLoop eventLoop channel.eventLoop(); AddressResolverSocketAddress resolver this.resolver.getResolver(eventLoop); if (!resolver.isSupported(remoteAddress) || resolver.isResolved(remoteAddress)) { channel.connect(remoteAddress, localAddress, promise); return promise; } FutureSocketAddress resolveFuture resolver.resolve(remoteAddress); ... } }如果遠(yuǎn)程目標(biāo)是域名而不是IPNetty會先用AddressResolver異步解析DNS解析完成后再真正發(fā)起channel.connect()。DNS解析默認(rèn)走的是DefaultAddressResolverGroup底層用Netty自己封裝的一套異步DNS客戶端實(shí)現(xiàn)。2.3 為什么connect要異步解析地址bind卻不用這個問題我當(dāng)初也想過bind拿到的是本地地址本機(jī)網(wǎng)卡信息是確定性的不存在“需要查DNS”的場景所以bind鏈路不需要resolver這一層。connect則完全相反你寫connect(some-service.example.com, 8080)時底層需要先把域名解析成IP而DNS查詢在網(wǎng)絡(luò)環(huán)境里可能耗時幾十毫秒甚至數(shù)秒如果阻塞在IO線程上整個EventLoop上的其他channel都會被拖累。Netty把這一步也異步化了讓DNS解析發(fā)生在獨(dú)立的Resolver線程組里解析完成后由回調(diào)把結(jié)果帶回EventLoop再執(zhí)行真正的connect。這一點(diǎn)對比能幫我們在源碼層面理解一個宏觀設(shè)計原則任何可能阻塞IO線程的操作Netty都會想方設(shè)法移出去或者以異步回調(diào)的方式回來。bind是純本地系統(tǒng)調(diào)用最快路徑就是直接在EventLoop里同步執(zhí)行connect涉及域名解析就要額外走一層。3. 源碼逐行看bind與connect在Channel層真正做了什么3.1 bind從pipeline一路摸到NioServerSocketChannel.doBindchannel.bind(localAddress, promise)會從DefaultChannelPipeline進(jìn)入。Netty的pipeline傳播規(guī)則是出站事件從Tail開始往前找直到某個ChannelOutboundHandler處理它入站事件從Head開始往后傳播。bind是典型的出站事件所以它會倒著走pipelineTailContext - 用戶OutboundHandler - ... - HeadContextHeadContext本身就實(shí)現(xiàn)了ChannelOutboundHandler它的bind方法最終調(diào)用了unsafe.bind。unsafe是Netty對JDK底層channel操作的封裝層全名叫Channel.Unsafe名字聽著危險其實(shí)是完成真正臟活累活的地方。再往下就到了AbstractChannel.bindOverride public final void bind(final SocketAddress localAddress, final ChannelPromise promise) { ... boolean wasActive isActive(); doBind(localAddress); if (!wasActive isActive()) { pipeline.fireChannelActive(); } promise.setSuccess(); }這里有兩個關(guān)鍵判斷bind之前channel處于inactive狀態(tài)bind成功之后channel變成active于是觸發(fā)fireChannelActive()。這個事件很重要——服務(wù)端會在doBeginRead里根據(jù)isActive狀態(tài)注冊O(shè)P_ACCEPT。真正落到NIO層的是NioServerSocketChannel.doBindOverride protected void doBind(SocketAddress localAddress) throws Exception { if (PlatformDependent.javaVersion() 7) { javaChannel().bind(localAddress, config.getBacklog()); } else { javaChannel().socket().bind(localAddress, config.getBacklog()); } active true; }這里直接調(diào)用了JDK NIO的ServerSocketChannel.bind(localAddress, backlog)。注意第二個參數(shù)config.getBacklog()這個值就是從ChannelOption.SO_BACKLOG讀出來的傳給操作系統(tǒng)控制連接隊列深度的關(guān)鍵參數(shù)。如果這個端口已經(jīng)被別的進(jìn)程占用這一行會拋出SocketException(Address already in use)一路向上傳播最終體現(xiàn)在你bind().sync()拿到的異常里。3.2 connect從pipeline一路摸到NioSocketChannel.doConnectconnect同樣是出站事件同樣從Tail倒著走到HeadContext.connect然后進(jìn)入unsafe.connect最終到達(dá)NioSocketChannel.doConnectOverride 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); } success true; return connected; } finally { if (!success) { doClose(); } } }這段代碼是理解非阻塞connect的關(guān)鍵。JDK NIO的SocketChannel.connect在非阻塞模式下并不會阻塞到連接建立完成而是立即返回一個布爾值如果返回true說明連接已經(jīng)立刻建立比如連本機(jī)回環(huán)地址如果返回false說明連接還在進(jìn)行中。此時Netty立刻把興趣事件設(shè)置為OP_CONNECT把“等待連接完成”這件事交給了Selector——相當(dāng)于告訴操作系統(tǒng)這個socket正在連接中等它連好了你通知我。finally塊里的doClose()是一個很嚴(yán)謹(jǐn)?shù)亩档兹绻谠O(shè)置興趣事件之前出了任何異常說明連接沒正常發(fā)起必須關(guān)閉channel避免半初始化狀態(tài)的socket泄漏。當(dāng)操作系統(tǒng)通知連接完成時NioSocketChannel.doFinishConnect會被調(diào)用Override protected void doFinishConnect() throws Exception { if (!javaChannel().finishConnect()) { throw new ConnectException(finishConnect() returned false); } }finishConnect()會確認(rèn)連接狀態(tài)如果連接已經(jīng)建立返回true這條鏈路就通了如果連接失敗比如對端拒絕這里會直接拋出ConnectException——這就是你在客戶端sync()時拿到Connection refused異常的最底層來源。3.3 NioEventLoop里OP_ACCEPT與OP_CONNECT的分叉處理服務(wù)端和客戶端的命運(yùn)在NioEventLoop的processSelectedKey里真正分道揚(yáng)鑣??催@段核心代碼if ((readyOps (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) ! 0 || readyOps 0) { unsafe.read(); } if ((readyOps SelectionKey.OP_WRITE) ! 0) { ch.unsafe().forceFlush(); } if ((readyOps SelectionKey.OP_CONNECT) ! 0) { int ops k.interestOps(); ops ~SelectionKey.OP_CONNECT; k.interestOps(ops); unsafe.finishConnect(); }OP_ACCEPT被歸到和OP_READ同一個分支走的是unsafe.read()。但這時的“讀”不是讀數(shù)據(jù)而是讀新連接。NioMessageUnsafe.read()內(nèi)部會調(diào)用doReadMessages把已經(jīng)完成三次握手的連接逐個accept()出來封裝成NioSocketChannel然后觸發(fā)pipeline.fireChannelRead。服務(wù)端的pipeline里有一個關(guān)鍵handler叫ServerBootstrapAcceptor它會在channelRead里把新accept出來的channel注冊到workerGroup上完成從boss線程到worker線程的交接。OP_CONNECT則是獨(dú)立的第三個分支處理邏輯更簡單先清理掉OP_CONNECT興趣位然后調(diào)用finishConnect確認(rèn)連接結(jié)果。確認(rèn)成功之后客戶端channel進(jìn)入active狀態(tài)觸發(fā)fireChannelActive隨后開始注冊O(shè)P_READ進(jìn)入正常的讀寫工作狀態(tài)。這里可以梳理成一張對比表對比維度Bind鏈路Connect鏈路入口方法ServerBootstrap.bindBootstrap.connect監(jiān)聽事件OP_ACCEPT等待新連接OP_CONNECT先等連接完成就緒事件分支與OP_READ同分支走unsafe.read獨(dú)立分支走unsafe.finishConnect完成后動作accept出NioSocketChannel交給workerchannelActive后轉(zhuǎn)OP_READ底層NIO操作ServerSocketChannel.bindSocketChannel.connect/finishConnect失敗典型異常Address already in useConnection refused / connect timed out4. 參數(shù)、超時與狀態(tài)機(jī)三個容易忽略但決定成敗的細(xì)節(jié)4.1 backlog如何影響bind本地地址與臨時端口問題backlog這個參數(shù)在面試?yán)锍霈F(xiàn)頻率很高但很多人只知道“設(shè)大一點(diǎn)可以提高并發(fā)”說不清它到底管什么。在TCP協(xié)議里服務(wù)端socket的listen隊列由兩部分組成未完成三次握手的半連接隊列SYN Queue和已完成握手的全連接隊列Accept Queue。backlog主要控制全連接隊列的長度。當(dāng)并發(fā)連接瞬間涌入accept處理速度跟不上連接建立速度時隊列就是緩沖區(qū)。隊列滿了新的連接請求會被內(nèi)核直接丟棄或拒絕。Netty的NioServerSocketChannelConfig在設(shè)置默認(rèn)backlog時有個細(xì)節(jié)優(yōu)先讀操作系統(tǒng)的net.core.somaxconn配置讀不到才用默認(rèn)值1024。也就是說你ServerBootstrap里不顯式設(shè)置SO_BACKLOG時實(shí)際生效值不一定是你以為的默認(rèn)值。我在實(shí)測中遇到過sysctl net.core.somaxconn為128的服務(wù)器不設(shè)置SO_BACKLOG時并發(fā)一大就出現(xiàn)大量連接被重置最后顯式設(shè)置SO_BACKLOG為1024才解決——這種問題從現(xiàn)象上很難想到根因在這里。再提一個connect鏈路上的端口細(xì)節(jié)connect方法還有一個重載可以傳localAddress但絕大多數(shù)場景不需要。如果你不指定操作系統(tǒng)會從臨時端口范圍內(nèi)自動選擇一個作為客戶端端口如果指定了底層NioSocketChannel.doConnect里會先對本地地址做一次bind。指定本地端口時要注意端口沖突概率很高尤其是當(dāng)你部署多個客戶端實(shí)例在同一臺機(jī)器上時指定固定本地端口容易遇到Address already in use。4.2 connectTimeoutMillis背后的定時任務(wù)邏輯ChannelOption.CONNECT_TIMEOUT_MILLIS是客戶端連接超時配置默認(rèn)值是30秒。這個超時時間的實(shí)現(xiàn)也值得講一下它不是在doConnect里同步等待而是通過EventLoop的schedule注冊了一個延遲任務(wù)在指定時間后檢查連接是否還在進(jìn)行中如果是則關(guān)閉channel并讓promise失敗。有一個容易被誤解的點(diǎn)這個超時是從調(diào)用connect到連接事件就緒的總超時。如果系統(tǒng)內(nèi)核在更早的時候返回了ECONNREFUSED那你拿到的會是ConnectException而不是超時異常兩者產(chǎn)生原因完全不同排查方向也不同。我在實(shí)際調(diào)線上接口時發(fā)現(xiàn)很多“莫名其妙連接超時”其實(shí)是因為目標(biāo)端口被防火墻默默丟棄了請求包內(nèi)核收不到任何響應(yīng)才硬生生等到超時。這種情況從源碼角度很好理解connect發(fā)出了但沒有任何ACK或RST回來NIO事件一直不觸發(fā)只能靠定時任務(wù)兜底。4.3 從狀態(tài)機(jī)再看兩件事的區(qū)別Netty的channel狀態(tài)遷移是理解兩個操作底層差異的一個很好的視角。在bind鏈路上NioServerSocketChannel創(chuàng)建時是inactivebind成功后變active于是注冊O(shè)P_ACCEPT之后一直穩(wěn)定在“接受新連接”的狀態(tài)不再有大的狀態(tài)遷移。在connect鏈路上NioSocketChannel創(chuàng)建時同樣是inactiveconnect發(fā)起后進(jìn)入“連接中”的中間態(tài)finishConnect成功后才active然后注冊O(shè)P_READ開始讀寫。這里藏著一個經(jīng)常被忽視的點(diǎn)客戶端channel在register階段并不會注冊任何IO興趣事件。它得等connect完成之后通過fireChannelActive事件觸發(fā)doBeginRead才注冊O(shè)P_READ。如果你在handlerAdded里就嘗試讀數(shù)據(jù)會發(fā)現(xiàn)讀不到任何東西——不是沒數(shù)據(jù)而是事件還沒注冊上。這個時序問題導(dǎo)致很多新手在寫客戶端ChannelInitializer時把“連接建立后主動發(fā)請求”的邏輯放錯了地方正確做法是放到channelActive回調(diào)里。5. 實(shí)戰(zhàn)排查遇到連接類問題怎么用源碼思維定位5.1 先分清是bind還是connect階段出錯排查連接問題第一步永遠(yuǎn)是定位錯誤發(fā)生在bind階段還是connect階段這兩個階段的異常類完全不一樣但現(xiàn)象可能很接近。bind階段最常見的錯誤是Address already in use也就是端口占用。但要注意這個異常不一定只出現(xiàn)在bind那一刻有些時候出現(xiàn)在ServerBootstrapAcceptor處理新連接時——如果你把childGroup的線程數(shù)配得太小新連接accept出來之后沒法及時注冊到worker也可能出現(xiàn)資源相關(guān)的異常但這就不是bind本身的問題了。判斷方法很簡單服務(wù)端啟動時bind().sync()拋出的異?;揪褪莃ind階段問題啟動成功后運(yùn)行期間報的異?;径家鵤ccept和worker線程方向去查。connect階段的錯誤類型更豐富異?;颥F(xiàn)象可能原因排查方向Connection refused目標(biāo)端口未監(jiān)聽或連接被RSTss -lnt看端口狀態(tài)Connection timed out防火墻丟包、目標(biāo)IP不可達(dá)檢查路由、防火墻策略、安全組Address already in use客戶端本地端口被占用檢查是否顯式指定了localAddress大量TIME_WAIT導(dǎo)致端口耗盡短連接過多連接未復(fù)用考慮連接池、調(diào)整端口范圍No buffer space available本地端口或文件句柄耗盡sysctl查看端口范圍、ulimit5.2 常見連接失敗問題速查表結(jié)合我自己踩過的坑整理一份更貼近現(xiàn)場的速查表服務(wù)端端口看著沒被占用但bind一直失敗先查netstat -tlnp確認(rèn)是TCP端口而不是UDP端口占用沖突再查是否設(shè)置了SO_REUSEADDR。Linux下端口處于TIME_WAIT狀態(tài)時如果沒開SO_REUSEADDRbind同樣會失敗Netty默認(rèn)SO_REUSEADDR是false所以高并發(fā)短連接場景下服務(wù)端重啟很容易遇到這個問題建議在option里顯式打開??蛻舳诉B接拒絕但服務(wù)端確實(shí)在監(jiān)聽確認(rèn)你連的不是回環(huán)地址。如果服務(wù)端監(jiān)聽在127.0.0.1你拿內(nèi)網(wǎng)IP去連是連不上的反過來監(jiān)聽在0.0.0.0或具體內(nèi)網(wǎng)IP用127.0.0.1連也可能失敗取決于防火墻規(guī)則。連接超時但三五秒后又恢復(fù)先懷疑半連接隊列溢出。看ss -s里SynCookies相關(guān)的統(tǒng)計或netstat -s里的listen queue overflow計數(shù)如果一直在增長說明backlog不夠或者accept處理不過來。connect失敗但服務(wù)端沒收到任何信息基本可以確定流量根本沒到服務(wù)端從防火墻、安全組、網(wǎng)絡(luò)策略方向排查不要繼續(xù)在應(yīng)用代碼里浪費(fèi)時間。5.3 我常用的斷點(diǎn)調(diào)試與日志技巧源碼分析不能只看不調(diào)我自己的經(jīng)驗是準(zhǔn)備一個最小復(fù)現(xiàn)工程一個只打印channelActive的客戶端ChannelInitializer一個帶LoggingHandler的服務(wù)端ChannelInitializer。然后打兩組斷點(diǎn)——第一組在NioSocketChannel.doConnect和NioServerSocketChannel.doBind確認(rèn)底層NIO調(diào)用參數(shù)第二組在NioEventLoop.processSelectedKey確認(rèn)事件就緒后走的是哪個分支。日志方面Netty自帶的LoggingHandler能打印出pipeline上每個事件的傳播過程包括REGISTERED、ACTIVE、READ等這些日志可以把時序問題可視化比你在業(yè)務(wù)handler里打日志全面得多。我排查一個客戶端偶發(fā)連接失敗問題時就是靠LoggingHandler發(fā)現(xiàn)channelActive和channelRead之間出現(xiàn)了接近兩秒的間隔才定位到DNS解析耗時的根因——不是連接慢是域名解析慢。connectTimeoutMillis這個參數(shù)也要在測試環(huán)境里刻意壓出超時場景來驗證行為把超時設(shè)為1毫秒連一個不存在且被防火墻靜默丟棄的IP觀察channel的關(guān)閉時機(jī)和異常類型。這樣你能準(zhǔn)確區(qū)分“內(nèi)核立即告訴我們連不上”和“等超時任務(wù)把我們踢下線”兩種模式線上遇到問題時的第一反應(yīng)就會完全不一樣。6. 一點(diǎn)個人體會源碼拆到這一步我自己最大的收獲不是記住了哪幾個方法名而是形成了一個判斷網(wǎng)絡(luò)問題的“分層反射”先看錯誤發(fā)生在bind還是connect再看是內(nèi)核直接反饋還是靠超時兜底最后才鉆進(jìn)業(yè)務(wù)代碼找原因。Netty把Connect和Bind設(shè)計成這樣兩張各有節(jié)奏的流程圖本質(zhì)上是為了在同一套非阻塞模型下同時安頓好“等待連接建立”和“等待新連接到來”這兩種完全不同性質(zhì)的等待。理解了這一層再看Netty源碼分析里其他和連接生命線相關(guān)的代碼比如重連、斷線檢測、優(yōu)雅停機(jī)都會順暢很多。最后再分享一個小技巧把這兩條鏈路的源碼各讀三遍之后試著關(guān)掉IDE自己在紙上畫出從Bootstrap.connect到內(nèi)核connect()的每一跳再畫出從ServerBootstrap.bind到OP_ACCEPT就緒的每一跳。畫得出來的這部分源碼你就真的吃透了。