為什么80%的碼農都做不了架構師?>>> ??
1.服務端啟動流程
這一小節,我們來學習一下如何使用 Netty 來啟動一個服務端應用程序,以下是服務端啟動的一個非常精簡的 Demo:
NettyServer.java
public class NettyServer {public static void main(String[] args) {NioEventLoopGroup bossGroup = new NioEventLoopGroup();NioEventLoopGroup workerGroup = new NioEventLoopGroup();ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class).childHandler(new ChannelInitializer<NioSocketChannel>() {protected void initChannel(NioSocketChannel ch) {}});serverBootstrap.bind(8000);}
}
- 首先看到,我們創建了兩個
NioEventLoopGroup
,這兩個對象可以看做是傳統IO編程模型的兩大線程組,bossGroup
表示監聽端口,accept
新連接的線程組,workerGroup
表示處理每一條連接的數據讀寫的線程組。用生活中的例子來講就是,一個工廠要運作,必然要有一個老板負責從外面接活,然后有很多員工,負責具體干活,老板就是bossGroup
,員工們就是workerGroup
,bossGroup
接收完連接,扔給workerGroup
去處理。 - 接下來 我們創建了一個引導類
ServerBootstrap
,這個類將引導我們進行服務端的啟動工作,直接new
出來開搞。 - 我們通過
.group(bossGroup, workerGroup)
給引導類配置兩大線程組,這個引導類的線程模型也就定型了。 - 然后,我們指定我們服務端的 IO 模型為NIO,我們通過
.channel(NioServerSocketChannel.class)
來指定 IO 模型,當然,這里也有其他的選擇,如果你想指定 IO 模型為 BIO,那么這里配置上OioServerSocketChannel.class
類型即可,當然通常我們也不會這么做,因為Netty的優勢就在于NIO。 - 接著,我們調用
childHandler()
方法,給這個引導類創建一個ChannelInitializer
,這里主要就是定義后續每條連接的數據讀寫,業務處理邏輯,不理解沒關系,在后面我們會詳細分析。ChannelInitializer
這個類中,我們注意到有一個泛型參數NioSocketChannel
,這個類呢,就是 Netty 對 NIO 類型的連接的抽象,而我們前面NioServerSocketChannel
也是對 NIO 類型的連接的抽象,NioServerSocketChannel
和NioSocketChannel
的概念可以和 BIO 編程模型中的ServerSocket
以及Socket
兩個概念對應上
我們的最小化參數配置到這里就完成了,我們總結一下就是,要啟動一個Netty服務端,必須要指定三類屬性,分別是線程模型、IO 模型、連接讀寫處理邏輯,有了這三者,之后在調用bind(8000),我們就可以在本地綁定一個 8000 端口啟動起來,以上這段代碼讀者可以直接拷貝到你的 IDE 中運行。
2.自動綁定遞增端口
在上面代碼中我們綁定了 8000 端口,接下來我們實現一個稍微復雜一點的邏輯,我們指定一個起始端口號,比如 1000,然后呢,我們從1000號端口往上找一個端口,直到這個端口能夠綁定成功,比如 1000 端口不可用,我們就嘗試綁定 1001,然后 1002,依次類推。
serverBootstrap.bind(8000)
;這個方法呢,它是一個異步的方法,調用之后是立即返回的,他的返回值是一個ChannelFuture
,我們可以給這個ChannelFuture
添加一個監聽器GenericFutureListener
,然后我們在GenericFutureListener
的operationComplete
方法里面,我們可以監聽端口是否綁定成功,接下來是監測端口是否綁定成功的代碼片段
serverBootstrap.bind(8000).addListener(new GenericFutureListener<Future<? super Void>>() {public void operationComplete(Future<? super Void> future) {if (future.isSuccess()) {System.out.println("端口綁定成功!");} else {System.err.println("端口綁定失敗!");}}
});
我們接下來從 1000 端口號,開始往上找端口號,直到端口綁定成功,我們要做的就是在 if (future.isSuccess())
的else
邏輯里面重新綁定一個遞增的端口號,接下來,我們把這段綁定邏輯抽取出一個bind
方法
private static void bind(final ServerBootstrap serverBootstrap, final int port) {serverBootstrap.bind(port).addListener(new GenericFutureListener<Future<? super Void>>() {public void operationComplete(Future<? super Void> future) {if (future.isSuccess()) {System.out.println("端口[" + port + "]綁定成功!");} else {System.err.println("端口[" + port + "]綁定失敗!");bind(serverBootstrap, port + 1);}}});
}
然后呢,以上代碼中最關鍵的就是在端口綁定失敗之后,重新調用自身方法,并且把端口號加一,然后,在我們的主流程里面,我們就可以直接調用
bind(serverBootstrap, 1000)
端口成功綁定了在1024,從 1000 開始到 1023,端口均綁定失敗了,這是因為在我的 MAC 系統下,1023 以下的端口號都是被系統保留了,需要 ROOT 權限才能綁定。
以上就是自動綁定遞增端口的邏輯,接下來,我們來一起學習一下,服務端啟動,我們的引導類ServerBootstrap
除了指定線程模型,IO 模型,連接讀寫處理邏輯之外,他還可以干哪些事情?
3.服務端啟動其他方法
handler() 方法
serverBootstrap.handler(new ChannelInitializer<NioServerSocketChannel>() {protected void initChannel(NioServerSocketChannel ch) {System.out.println("服務端啟動中");}
})
handler()
方法呢,可以和我們前面分析的childHandler()
方法對應起來,childHandler()
用于指定處理新連接數據的讀寫處理邏輯,handler()
用于指定在服務端啟動過程中的一些邏輯,通常情況下呢,我們用不著這個方法。
attr() 方法
serverBootstrap.attr(AttributeKey.newInstance("serverName"), "nettyServer")
attr()
方法可以給服務端的 channel
,也就是NioServerSocketChannel
指定一些自定義屬性,然后我們可以通過channel.attr()
取出這個屬性,比如,上面的代碼我們指定我們服務端channel
的一個serverName
屬性,屬性值為nettyServer
,其實說白了就是給NioServerSocketChannel
維護一個map而已,通常情況下,我們也用不上這個方法。
那么,當然,除了可以給服務端 channel NioServerSocketChannel
指定一些自定義屬性之外,我們還可以給每一條連接指定自定義屬性
childAttr() 方法
serverBootstrap.childAttr(AttributeKey.newInstance("clientKey"), "clientValue")
上面的childAttr可以給每一條連接指定自定義屬性,然后后續我們可以通過channel.attr()
取出該屬性。
childOption() 方法
serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true).childOption(ChannelOption.TCP_NODELAY, true)
childOption()
可以給每條連接設置一些TCP底層相關的屬性,比如上面,我們設置了兩種TCP屬性,其中
ChannelOption.SO_KEEPALIVE
表示是否開啟TCP底層心跳機制,true為開啟ChannelOption.TCP_NODELAY
表示是否開始Nagle算法,true表示關閉,false表示開啟,通俗地說,如果要求高實時性,有數據發送時就馬上發送,就關閉,如果需要減少發送次數減少網絡交互,就開啟。
其他的參數這里就不一一講解,有興趣的同學可以去這個類里面自行研究。
option() 方法
除了給每個連接設置這一系列屬性之外,我們還可以給服務端channel
設置一些屬性,最常見的就是so_backlog
,如下設置
serverBootstrap.option(ChannelOption.SO_BACKLOG, 1024)
表示系統用于臨時存放已完成三次握手的請求的隊列的最大長度,如果連接建立頻繁,服務器處理創建新連接較慢,可以適當調大這個參數
4.總結
- 本文中,我們首先學習了 Netty 服務端啟動的流程,一句話來說就是:創建一個引導類,然后給他指定線程模型,IO模型,連接讀寫處理邏輯,綁定端口之后,服務端就啟動起來了。
- 然后,我們學習到 bind 方法是異步的,我們可以通過這個異步機制來實現端口遞增綁定。
- 最后呢,我們討論了 Netty 服務端啟動額外的參數,主要包括給服務端 Channel 或者客戶端 Channel 設置屬性值,設置底層 TCP 參數。
以上內容來源于掘金小冊《Netty 入門與實戰:仿寫微信 IM 即時通訊系統》,若想獲得更多,更詳細的內容,請用微信掃碼訂閱: