點選上方“占小狼的部落格”,選擇“設為星标
最近熱文
- 瘋了吧!面試官問一個 TCP 連接配接可以發多少個 HTTP 請求?
- 不重新開機JVM,替換掉已經加載的類,偷天換日?
- 螞蟻面試:字元串在JVM中如何存放?
在開始了解Netty是什麼之前,我們先來回顧一下,如果我們需要實作一個用戶端與服務端通信的程式,使用傳統的IO程式設計,應該如何來實作?
IO程式設計
我們簡化下場景:用戶端每隔兩秒發送一個帶有時間戳的"hello world"給服務端,服務端收到之後列印。
為了友善示範,下面例子中,服務端和用戶端各一個類,把這兩個類拷貝到你的IDE中,先後運作 IOServer.java和 IOClient.java可看到效果。
下面是傳統的IO程式設計中服務端實作
IOServer.java
public class IOServer {
public static void main(String[] args) throws Exception {
ServerSocket serverSocket = new ServerSocket(8000);
// (1) 接收新連接配接線程
new Thread(() -> {
while (true) {
try {
// (1) 阻塞方法擷取新的連接配接
Socket socket = serverSocket.accept();
// (2) 每一個新的連接配接都建立一個線程,負責讀取資料
new Thread(() -> {
try {
byte[] data = new byte[1024];
InputStream inputStream = socket.getInputStream();
while (true) {
int len;
// (3) 按位元組流方式讀取資料
while ((len = inputStream.read(data)) != -1) {
System.out.println(new String(data, 0, len));
}
}
} catch (IOException e) {
}
}).start();
} catch (IOException e) {
}
}
}).start();
}
}
server端首先建立了一個 serverSocket來監聽8000端口,然後建立一個線程,線程裡面不斷調用阻塞方法 serversocket.accept();擷取新的連接配接,見(1),當擷取到新的連接配接之後,給每條連接配接建立一個新的線程,這個線程負責從該連接配接中讀取資料,見(2),然後讀取資料是以位元組流的方式,見(3)。
下面是傳統的IO程式設計中用戶端實作
IOClient.java
public class IOClient {
public static void main(String[] args) {
new Thread(() -> {
try {
Socket socket = new Socket("127.0.0.1", 8000);
while (true) {
try {
socket.getOutputStream().write((new Date() + ": hello world").getBytes());
socket.getOutputStream().flush();
Thread.sleep(2000);
} catch (Exception e) {
}
}
} catch (IOException e) {
}
}).start();
}
}
用戶端的代碼相對簡單,連接配接上服務端8000端口之後,每隔2秒,我們向服務端寫一個帶有時間戳的 "hello world"。
IO程式設計模型在用戶端較少的情況下運作良好,但是對于用戶端比較多的業務來說,單機服務端可能需要支撐成千上萬的連接配接,IO模型可能就不太合适了,我們來分析一下原因。
上面的demo,從服務端代碼中我們可以看到,在傳統的IO模型中,每個連接配接建立成功之後都需要一個線程來維護,每個線程包含一個while死循環,那麼1w個連接配接對應1w個線程,繼而1w個while死循環,這就帶來如下幾個問題:
- 線程資源受限:線程是作業系統中非常寶貴的資源,同一時刻有大量的線程處于阻塞狀态是非常嚴重的資源浪費,作業系統耗不起
- 線程切換效率低下:單機cpu核數固定,線程爆炸之後作業系統頻繁進行線程切換,應用性能急劇下降。
- 除了以上兩個問題,IO程式設計中,我們看到資料讀寫是以位元組流為機關,效率不高。
為了解決這三個問題,JDK在1.4之後提出了NIO。
NIO程式設計
關于NIO相關的文章網上也有很多,這裡不打算詳細深入分析,下面簡單描述一下NIO是如何解決以上三個問題的。
線程資源受限
NIO程式設計模型中,新來一個連接配接不再建立一個新的線程,而是可以把這條連接配接直接綁定到某個固定的線程,然後這條連接配接所有的讀寫都由這個線程來負責,那麼他是怎麼做到的?我們用一幅圖來對比一下IO與NIO

如上圖所示,IO模型中,一個連接配接來了,會建立一個線程,對應一個while死循環,死循環的目的就是不斷監測這條連接配接上是否有資料可以讀,大多數情況下,1w個連接配接裡面同一時刻隻有少量的連接配接有資料可讀,是以,很多個while死循環都白白浪費掉了,因為讀不出啥資料。
而在NIO模型中,他把這麼多while死循環變成一個死循環,這個死循環由一個線程控制,那麼他又是如何做到一個線程,一個while死循環就能監測1w個連接配接是否有資料可讀的呢? 這就是NIO模型中selector的作用,一條連接配接來了之後,現在不建立一個while死循環去監聽是否有資料可讀了,而是直接把這條連接配接注冊到selector上,然後,通過檢查這個selector,就可以批量監測出有資料可讀的連接配接,進而讀取資料,下面我再舉個非常簡單的生活中的例子說明IO與NIO的差別。
在一家幼稚園裡,小朋友有上廁所的需求,小朋友都太小以至于你要問他要不要上廁所,他才會告訴你。幼稚園一共有100個小朋友,有兩種方案可以解決小朋友上廁所的問題:
- 每個小朋友配一個老師。每個老師隔段時間詢問小朋友是否要上廁所,如果要上,就領他去廁所,100個小朋友就需要100個老師來詢問,并且每個小朋友上廁所的時候都需要一個老師領着他去上,這就是IO模型,一個連接配接對應一個線程。
- 所有的小朋友都配同一個老師。這個老師隔段時間詢問所有的小朋友是否有人要上廁所,然後每一時刻把所有要上廁所的小朋友批量領到廁所,這就是NIO模型,所有小朋友都注冊到同一個老師,對應的就是所有的連接配接都注冊到一個線程,然後批量輪詢。
這就是NIO模型解決線程資源受限的方案,實際開發過程中,我們會開多個線程,每個線程都管理着一批連接配接,相對于IO模型中一個線程管理一條連接配接,消耗的線程資源大幅減少
線程切換效率低下
由于NIO模型中線程數量大大降低,線程切換效率是以也大幅度提高
IO讀寫以位元組為機關
NIO解決這個問題的方式是資料讀寫不再以位元組為機關,而是以位元組塊為機關。IO模型中,每次都是從作業系統底層一個位元組一個位元組地讀取資料,而NIO維護一個緩沖區,每次可以從這個緩沖區裡面讀取一塊的資料, 這就好比一盤美味的豆子放在你面前,你用筷子一個個夾(每次一個),肯定不如要勺子挖着吃(每次一批)效率來得高。
簡單講完了JDK NIO的解決方案之後,我們接下來使用NIO的方案替換掉IO的方案,我們先來看看,如果用JDK原生的NIO來實作服務端,該怎麼做
前方高能預警:以下代碼可能會讓你感覺極度不适,如有不适,請跳過
NIOServer.java
public class NIOServer {
public static void main(String[] args) throws IOException {
Selector serverSelector = Selector.open();
Selector clientSelector = Selector.open();
new Thread(() -> {
try {
// 對應IO程式設計中服務端啟動
ServerSocketChannel listenerChannel = ServerSocketChannel.open();
listenerChannel.socket().bind(new InetSocketAddress(8000));
listenerChannel.configureBlocking(false);
listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) {
// 監測是否有新的連接配接,這裡的1指的是阻塞的時間為1ms
if (serverSelector.select(1) > 0) {
Set<SelectionKey> set = serverSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
try {
// (1) 每來一個新連接配接,不需要建立一個線程,而是直接注冊到clientSelector
SocketChannel clientChannel = ((ServerSocketChannel) key.channel()).accept();
clientChannel.configureBlocking(false);
clientChannel.register(clientSelector, SelectionKey.OP_READ);
} finally {
keyIterator.remove();
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
new Thread(() -> {
try {
while (true) {
// (2) 批量輪詢是否有哪些連接配接有資料可讀,這裡的1指的是阻塞的時間為1ms
if (clientSelector.select(1) > 0) {
Set<SelectionKey> set = clientSelector.selectedKeys();
Iterator<SelectionKey> keyIterator = set.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
try {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
// (3) 讀取資料以塊為機關批量讀取
clientChannel.read(byteBuffer);
byteBuffer.flip();
System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer)
.toString());
} finally {
keyIterator.remove();
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
}
} catch (IOException ignored) {
}
}).start();
}
}
相信大部分沒有接觸過NIO的同學應該會直接跳過代碼來到這一行:原來使用JDK原生NIO的API實作一個簡單的服務端通信程式是如此複雜!
複雜得我都沒耐心解釋這一坨代碼的執行邏輯(開個玩笑),我們還是先對照NIO來解釋一下幾個核心思路
- NIO模型中通常會有兩個線程,每個線程綁定一個輪詢器selector,在我們這個例子中
負責輪詢是否有新的連接配接,serverSelector
負責輪詢連接配接是否有資料可讀clientSelector
- 服務端監測到新的連接配接之後,不再建立一個新的線程,而是直接将新連接配接綁定到
上,這樣就不用IO模型中1w個while循環在死等,參見(1)clientSelector
-
被一個while死循環包裹着,如果在某一時刻有多條連接配接有資料可讀,那麼通過clientSelector
方法可以輪詢出來,進而批量處理,參見(2)clientSelector.select(1)
- 資料的讀寫以記憶體塊為機關,參見(3)
其他的細節部分,我不願意多講,因為實在是太複雜,你也不用對代碼的細節深究到底。總之,強烈不建議直接基于JDK原生NIO來進行網絡開發,下面是我總結的原因
1、JDK的NIO程式設計需要了解很多的概念,程式設計複雜,對NIO入門非常不友好,程式設計模型不友好,ByteBuffer的api簡直反人類 2、對NIO程式設計來說,一個比較合适的線程模型能充分發揮它的優勢,而JDK沒有給你實作,你需要自己實作,就連簡單的自定義協定拆包都要你自己實作 3、JDK的NIO底層由epoll實作,該實作飽受诟病的空輪訓bug會導緻cpu飙升100% 4、項目龐大之後,自行實作的NIO很容易出現各類bug,維護成本較高,上面這一坨代碼我都不能保證沒有bug
正因為如此,我用戶端代碼都懶得寫給你看了==!,你可以直接使用 IOClient.java與 NIOServer.java通信
JDK的NIO猶如帶刺的玫瑰,雖然美好,讓人向往,但是使用不當會讓你抓耳撓腮,痛不欲生,正因為如此,Netty橫空出世!
Netty程式設計
那麼Netty到底是何方神聖? 用一句簡單的話來說就是:Netty封裝了JDK的NIO,讓你用得更爽,你不用再寫一大堆複雜的代碼了。 用官方正式的話來說就是:Netty是一個異步事件驅動的網絡應用架構,用于快速開發可維護的高性能伺服器和用戶端。
下面是我總結的使用Netty不使用JDK原生NIO的原因
- 使用JDK自帶的NIO需要了解太多的概念,程式設計複雜,一不小心bug橫飛
- Netty底層IO模型随意切換,而這一切隻需要做微小的改動,改改參數,Netty可以直接從NIO模型變身為IO模型
- Netty自帶的拆包解包,異常檢測等機制讓你從NIO的繁重細節中脫離出來,讓你隻需要關心業務邏輯
- Netty解決了JDK的很多包括空輪詢在内的bug
- Netty底層對線程,selector做了很多細小的優化,精心設計的reactor線程模型做到非常高效的并發處理
- 自帶各種協定棧讓你處理任何一種通用協定都幾乎不用親自動手
- Netty社群活躍,遇到問題随時郵件清單或者issue
- Netty已經曆各大rpc架構,消息中間件,分布式通信中間件線上的廣泛驗證,健壯性無比強大
看不懂沒有關系,這些我們在後續的課程中我們都可以學到,接下來我們用Netty的版本來重新實作一下本文開篇的功能吧
首先,引入Maven依賴
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
<version>4.1.6.Finalversion>
dependency>
然後,下面是服務端實作部分
NettyServer.java
public class NettyServer {
public static void main(String[] args) {
ServerBootstrap serverBootstrap = new ServerBootstrap();
NioEventLoopGroup boos = new NioEventLoopGroup();
NioEventLoopGroup worker = new NioEventLoopGroup();
serverBootstrap
.group(boos, worker)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ch.Pipeline().addLast(new StringDecoder());
ch.Pipeline().addLast(new SimpleChannelInboundHandler<String>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) {
System.out.println(msg);
}
});
}
})
.bind(8000);
}
}
這麼一小段代碼就實作了我們前面NIO程式設計中的所有的功能,包括服務端啟動,接受新連接配接,列印用戶端傳來的資料,怎麼樣,是不是比JDK原生的NIO程式設計優雅許多?
初學Netty的時候,由于大部分人對NIO程式設計缺乏經驗,是以,将Netty裡面的概念與IO模型結合起來可能更好了解
1.
boos
對應了
IOServer.java
中的接收新連接配接線程,主要負責建立新連接配接 2.
worker
對應
IOClient.java
中的負責讀取資料的線程,主要用于讀取資料以及業務邏輯處理
然後剩下的邏輯我在後面的系列文章中會詳細分析,你可以先把這段代碼拷貝到你的IDE裡面,然後運作main函數
然後下面是用戶端NIO的實作部分
NettyClient.java
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
Bootstrap bootstrap = new Bootstrap();
NioEventLoopGroup group = new NioEventLoopGroup();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<Channel>() {
@Override
protected void initChannel(Channel ch) {
ch.Pipeline().addLast(new StringEncoder());
}
});
Channel channel = bootstrap.connect("127.0.0.1", 8000).channel();
while (true) {
channel.writeAndFlush(new Date() + ": hello world!");
Thread.sleep(2000);
}
}
}
在用戶端程式中,
group
對應了我們
IOClient.java
中main函數起的線程,剩下的邏輯我在後面的文章中會詳細分析,現在你要做的事情就是把這段代碼拷貝到你的IDE裡面,然後運作main函數,最後回到
NettyIOServer.java
的控制台,你會看到效果。
使用Netty之後是不是覺得整個世界都美好了,一方面Netty對NIO封裝得如此完美,寫出來的代碼非常優雅,另外一方面,使用Netty之後,網絡通信這塊的性能問題幾乎不用操心,盡情地讓Netty榨幹你的CPU吧。
最後分享一份面試寶典《Java核心知識點整理.pdf》,覆寫了JVM、鎖、高并發、反射、Spring原理、微服務、Zookeeper、資料庫、資料結構等等。
擷取方式:關注公衆号并回複 666 領取,更多内容陸續奉上。
明天見(。・ω・。)ノ♡