認識Netty之 hello world

最近一直在看與IM相關的東西說到這個java中應該要說到netty,下面來認識一下netty。

開始瞭解netty之前我們先了解下傳統的IO程式設計。

IO程式設計

我們來做一個比較簡單的demo:客戶端每隔5秒傳送一條訊息到服務端,服務端收到訊息打印出來。

服務端實現

Server 端首先建立了一個

serverSocket

來監聽 8000 埠,然後建立一個執行緒,執行緒裡面不斷呼叫阻塞方法

serversocket。accept();

獲取新的連線,當獲取到新的連線之後,給每條連線建立一個新的執行緒,這個執行緒負責從該連線中讀取資料,然後讀取資料是以位元組流的方式。

package com。pine。springbootdemo01。netty;import java。io。IOException;import java。io。InputStream;import java。net。ServerSocket;import java。net。Socket;/** * @author anziyang * @version V1。0 * @date 2022/3/1 11:53 上午 **/public class IOServer { public static void main(String[] args) throws Exception { ServerSocket serverSocket = new ServerSocket(8000); // 接收新連線執行緒 new Thread(() -> { while (true) { try { // 阻塞方法獲取新的連線 Socket socket = serverSocket。accept(); // 每一個新的連線都建立一個執行緒,負責讀取資料 new Thread(() -> { try { int len; byte[] data = new byte[1024]; InputStream inputStream = socket。getInputStream(); // 按位元組流方式讀取資料 while ((len = inputStream。read(data)) != -1) { System。out。println(new String(data, 0, len)); } } catch (IOException e) { System。err。println(e); } })。start(); } catch (IOException e) { System。err。println(e); } } })。start(); }}

客戶端實現

啟動一個執行緒連線到服務端,迴圈中每次睡眠五秒也就是每隔五秒傳送一次訊息。

package com。pine。springbootdemo01。netty;import java。io。IOException;import java。net。Socket;import java。util。Date;/** * @author anziyang * @version V1。0 * @date 2022/3/1 11:54 上午 **/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 socket”)。getBytes()); // 睡眠5秒 Thread。sleep(5000); } catch (Exception e) { System。err。println(e); } } } catch (IOException e) { System。err。println(e); } })。start(); }}

先啟動server端然後啟動client可以看到server收到請求如下:

認識Netty之 hello world

上面的 demo,從服務端程式碼中我們可以看到,在傳統的 IO 模型中,每個連線建立成功之後都需要一個執行緒來維護,每個執行緒包含一個 while true 死迴圈,那麼 10w 個連線對應 10w 個執行緒,繼而 10w 個 while 死迴圈,這就帶來如下幾個問題:

執行緒資源受限:執行緒是作業系統中非常寶貴的資源,同一時刻有大量的執行緒處於阻塞狀態是非常嚴重的資源浪費,作業系統耗不起

執行緒切換效率低下:單機 CPU 核數固定,執行緒爆炸之後作業系統頻繁進行執行緒切換,應用效能急劇下降。

除了以上兩個問題,IO 程式設計中,我們看到資料讀寫是以位元組流為單位。

為了解決這三個問題,JDK 在 1。4 之後提出了 NIO。

NIO(Non-blocking I/O,在Java領域,也稱為New I/O),是一種同步非阻塞的I/O模型,也是I/O多路複用的基礎,已經被越來越多地應用到大型應用伺服器,成為解決高併發與大量連線、I/O處理問題的有效方式。

NIO程式設計

java對於非堵塞I/O的支援是在2002年引入的,位於JDK1。4的java。nio包中。

IO和NIO的區別:

IO是面向位元組流和字元流的,而NIO是面向緩衝區的。

IO是阻塞模式的,NIO是非阻塞模式的

NIO新增了選擇器的概念,可以透過選擇器監聽多個通道。

NIO用的是事件機制。它可以用一個執行緒把Accept,讀,寫操作,請求處理的邏輯全乾了。如果什麼事都沒得做,它也不會死迴圈,它會將執行緒休眠起來,直到下一個事件來了再繼續幹活,這樣的一個執行緒稱之為NIO執行緒。

package com。pine。springbootdemo01。netty;import java。io。IOException;import java。net。InetSocketAddress;import java。nio。ByteBuffer;import java。nio。channels。SelectionKey;import java。nio。channels。Selector;import java。nio。channels。ServerSocketChannel;import java。nio。channels。SocketChannel;import java。nio。charset。Charset;import java。util。Iterator;import java。util。Set;/** * @author anziyang * @version V1。0 * @date 2022/3/1 2:29 下午 **/public class NIOServer { public static void main(String[] args) throws IOException { Selector serverSelector = Selector。open(); Selector clientSelector = Selector。open(); // 啟動一個執行緒繫結select 用於監聽是否有新的連線 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 set = serverSelector。selectedKeys(); Iterator 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 e) { System。err。println(e); } })。start(); // 啟動一個執行緒繫結select 用於監聽哪些連線有資料可讀 new Thread(() -> { try { while (true) { // (2) 批次輪詢是否有哪些連線有資料可讀,這裡的1指的是阻塞的時間為 1ms if (clientSelector。select(1) > 0) { Set set = clientSelector。selectedKeys(); Iterator 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) 面向 Buffer clientChannel。read(byteBuffer); byteBuffer。flip(); System。out。println(Charset。defaultCharset()。newDecoder()。decode(byteBuffer) 。toString()); } finally { keyIterator。remove(); key。interestOps(SelectionKey。OP_READ); } } } } } } catch (IOException e) { System。err。println(e); } })。start(); }}

從上面的程式碼中我們可以看到

NIO 模型中通常會有兩個執行緒,每個執行緒繫結一個輪詢器 selector ,在我們這個例子中

serverSelector

負責輪詢是否有新的連線,

clientSelector

負責輪詢連線是否有資料可讀

服務端監測到新的連線之後,不再建立一個新的執行緒,而是直接將新連線繫結到

clientSelector

上,這樣就不用 IO 模型中 1w 個 while 迴圈在死等,參見(1)

clientSelector

被一個 while 死迴圈包裹著,如果在某一時刻有多條連線有資料可讀,那麼透過

clientSelector。select(1)

方法可以輪詢出來,進而批次處理,參見(2)

資料的讀寫面向 Buffer,參見(3)

啟動NIOServer main方法然後啟動之前的client可以看到控制列印

認識Netty之 hello world

Netty程式設計

首先看下netty如何實現服務端的:

package com。pine。springbootdemo01。netty;import io。netty。bootstrap。ServerBootstrap;import io。netty。channel。ChannelHandlerContext;import io。netty。channel。ChannelInitializer;import io。netty。channel。SimpleChannelInboundHandler;import io。netty。channel。nio。NioEventLoopGroup;import io。netty。channel。socket。nio。NioServerSocketChannel;import io。netty。channel。socket。nio。NioSocketChannel;import io。netty。handler。codec。string。StringDecoder;/** * @author anziyang * @version V1。0 * @date 2022/3/1 2:41 下午 **/public class NettyServer { public static void main(String[] args) { ServerBootstrap serverBootstrap = new ServerBootstrap(); NioEventLoopGroup boss = new NioEventLoopGroup(); NioEventLoopGroup worker = new NioEventLoopGroup(); serverBootstrap。group(boss, worker)。channel(NioServerSocketChannel。class) 。childHandler(new ChannelInitializer() { protected void initChannel(NioSocketChannel ch) { ch。pipeline()。addLast(new StringDecoder()); ch。pipeline()。addLast(new SimpleChannelInboundHandler() { @Override protected void channelRead0(ChannelHandlerContext ctx, String msg) { System。out。println(msg); } }); } })。bind(8000); }}

上面程式碼明顯比前面的NIO少很多程式碼,優雅了不少。

可以看到netty服務端輸出內容:

認識Netty之 hello world

本文就先聊這麼多,主要是熟悉下netty程式設計,看下經典的hello world。

感覺有收穫點個贊,轉發下哦

參考文件:

https://juejin。cn/book/6844733738119593991/section/6844733738270588942

https://www。jianshu。com/p/432d5557a19e

https://www。cnblogs。com/flyingeagle/articles/11031163。html