https://www.jianshu.com/p/e58674eb4c7a
1. Netty - 異步和事件驅動
1. Netty 能夠幫助搭建允許系統能夠擴展到支持150000名併發用戶。
2. Netty 設計關鍵: 異步 + 事件驅動
1.1 Java網絡編程(BIO)
典型的BIO服務端:
1. 一個主線程在某個port監聽,等待客戶端連接。
2. 當接收到客戶端發起的連接時,創建一個新的線程去處理客戶端請求。
3. 主線程重新回到port監聽,等待下一個客戶端連接。
缺點:
1. 每個新的客戶端Socket都需要創建一個新的Thread處理,將會導致大量的線程處於休眠狀態。
2. 每個線程都有調用棧的內存分配,連接數非常多時,耗費較多內存。
3. 連接數比較多時,創建大量線程,上下文切換所帶來的開銷較大。
代碼:
- public void serve(int port) throws IOException {
- // 創建Socket
- ServerSocket serverSocket = new ServerSocket(port);
- // 等待客戶端連接
- Socket clientSocket = serverSocket.accept();
- // 創建輸入流
- BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
- PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
- String request, response;
- while((request = in.readLine()) != null) {
- if("Done".equals(request)) {
- break;
- }
- response = processRequest(request);
- out.println(response);
- }
- }
1.2 Java NIO
1. 使用Selector來實現Java的非阻塞I/O操作。將多個Socket的讀寫狀態綁定到Selector上,允許在任何時間檢查任意的讀操作/寫操作的完成狀態。
2. 允許單個線程處理多個併發的連接。
1.3 Netty的核心組件
Netty的主要構件塊:
1. Channel
2. 回調
3. Future
4. 事件和ChannelHandler
1.3.1 Channel
Channel是傳入(入站)或者傳出(出站)數據的載體(如一個文件、一個Socket或一個硬件設備)。可以被打開或者被關閉,連接或斷開連接。
1.3.2 回調
回調只是:先寫一段代碼,該段代碼在將來某個適當的時候會被執行。Netty大量使用了回調,比如:某ChannelHandler中的channelActive()方法則是一個回調,表示連接建立時,請執行該段回調代碼。
1.3.3 Future
異步操作佔位符。在操作完成時,提供結果的訪問。
JDK提供的Future和ChannelFuture對比:
1. JDK提供的Future需要手動檢查對應的操作是否完成,或一直阻塞直到它完成
2. ChannelFuture能夠註冊Listener監聽器,監聽器的回調函數operationComplete()能異步的在操作完成時被調用。
代碼:
- public static void connect() {
- Channel channel = CHANNEL_FROM_SOMEWHERE;
-
- ChannelFuture future = channel.connect(new InetSocketAddress("127.0.0.1", 9080));
- future.addListener(new ChannelFutureListener() {
- @Override
- public void operationComplete(ChannelFuture future) throws Exception {
- if(future.isSuccess()) {
- ByteBuf buf = Unpooled.copiedBuffer("hello", Charset.defaultCharset());
- ChannelFuture wf = future.channel().writeAndFlush(buf);
- // ...
- } else {
- // 失敗後可嘗試重連/切換鏈路
- future.cause().printStackTrace();
- }
- }
- })
- }
1.3.4 事件和ChannelHandler
1. 事件:發生某種事件觸發適當的動作。比如入站觸發事件: 鏈路激活(channelActive)/數據可讀(channelRead)/發生異常(exceptionCaught)/...
2. Channelhandler:一組爲了響應特定事件而被執行的回調函數。如:ChannelInboundHanderAdapter.java是一個入站事件
1.3.5 Channel和EventLoop關係:
Channel和EventLoop都是Netty核心概念,而且有一些約定俗成的規定,能幫助編程和理解:
1. 單個Channel只會映射到單個EventLoop
2. 單個EventLoop可以處理多個Channel(1:n關係)
3. 一個EventLoop在其生命週期內只能綁定到一個線程上4. 由於單個Channel在其生命週期中只會有一個I/O線程,所以ChannelPipeline中多個ChannelHandler無需關心同步互斥問題
2. 第一款Netty應用程序
1. ChannelHandler用於構建應用業務邏輯。往往封裝了爲響應特定事件而編寫的回調函數
2. 本節主要講解一個超級簡單的Netty應用程序,回顯服務: 客戶端建立連接後,發送一個或多個消息。服務端收到後,將消息返回。
2.3 編寫Echo服務器
Netty服務端至少需要兩個部分: 一個ChannelHandler + 引導(Bootstrap)
2.3.1 ChannelHandler和業務邏輯
繼承ChannelInboundHandlerAdapter類,感興趣的入站方法:
1. channelRead() - 對於每個傳入的消息都要調用
2. channelReadComplete() - 當前批量讀取中的最後一條數據
3. exceptionCaught() - 讀取操作期間,有異常拋出時調用
代碼:
- @ChannelHandler.Sharable
- public class EchoServerHandler extends ChannelInboundHandlerAdapter{
-
- /**
- * 每次傳入的消息都要調用
- */
- @Override
- public void channelRead(ChannelHandlerContext ctx, Object msg) {
- ByteBuf in = (ByteBuf) msg;
- System.out.println(
- "Server received: " + in.toString(CharsetUtil.UTF_8));
- ctx.write(in);
- }
-
- /**
- * 讀完當前批量中的最後一條數據後,觸發channelReadComplete(...)方法
- */
- @Override
- public void channelReadComplete(ChannelHandlerContext ctx)
- throws Exception {
- ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
- .addListener(ChannelFutureListener.CLOSE);
- }
-
- /**
- * 異常捕獲
- */
- @Override
- public void exceptionCaught(ChannelHandlerContext ctx,
- Throwable cause) {
- cause.printStackTrace();
- ctx.close();
- }
- }
解釋:
1. channelRead和channelReadComplete理解:當批量消息後最後一條數據被channelRead(...)後觸發channelReadComplete事件。
2. ctx.write(...)只是將消息暫時存放在ChannelOutboundBuffer中,等待flush(...)操作
3. @Sharable註解:本質是聲明該ChannelHandler全局單例。可被多個Channel安全的共享。標註了@Sharable註解的ChannelHandler請注意不能有對應的狀態
4. 完整代碼地址
2.3.2 引導服務器
1. 引導服務器主要打開Netty的Channel。並分配對應的EventLoop和ChannelPipeline。
2. 一個Channel只有一個ChannelPipeline。ChannelPipeline是由一組ChannelHandler組成的責任鏈。
代碼:
- EventLoopGroup group = new NioEventLoopGroup();
- try {
- ServerBootstrap b = new ServerBootstrap();
- b.group(group)
- .channel(NioServerSocketChannel.class)
- .localAddress(new InetSocketAddress(port))
- .childHandler(new ChannelInitializer<SocketChannel>() {
- @Override
- public void initChannel(SocketChannel ch) throws Exception {
- ch.pipeline().addLast(new EchoServerHandler());
- }
- });
- } finally {
- group.shutdownGracefully().sync();
- }
2.4 編寫Echo客戶端
客戶端將會:
1. 建立連接
2. 發送消息
3. 關閉連接
2.4.1 ChannelHandler客戶端邏輯
1. Java是通過GC可達性分析來實現垃圾回收。對於Netty傳輸中的ByteBuf,使用的是引用計數算法。也就是說:如果你使用了Netty,需要你親自考慮是否需要手動釋放對象。判斷方法,後文將會給出
2. 擴展SimpleChannelInboundHandler類處理任務的Handler,無需手動釋放對象。SimpleChannelInboundHandler.java中方法channelRead()中會負責釋放引用。
3. 客戶端發送消息條數和服務端接收的消息條數是不對應的。除非處理了TCP的粘包黏包。
代碼:
- // SimpleChannelInboundHandler<T>中channelRead方法負責釋放對象msg引用
- public abstract class SimpleChannelInboundHandler<I> ...{
- public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
- boolean release = true;
- try {
- // ...
- } finally {
- if (autoRelease && release) {
- // 減少對象msg引用計數
- ReferenceCountUtil.release(msg);
- }
- }
- }
- }
問:ChannelHandler中何時需要主動釋放引用?
1. 擴展的類不是: SimpleChannelInboundHandler,且該對象msg不會傳給下一個ChannelHandler
2. 擴展的類不是: SimpleChannelInboundHandler,且該對象msg不會被ctx.write(...)
2.4.2 引導客戶端
給出引導客戶端關鍵代碼,完整代碼請參考地址
代碼:
- EventLoopGroup group = new NioEventLoopGroup();
- try {
- Bootstrap b = new Bootstrap();
- b.group(group)
- .channel(NioSocketChannel.class)
- .remoteAddress(new InetSocketAddress(host, port))
- .handler(new ChannelInitializer<SocketChannel>() {
- @Override
- public void initChannel(SocketChannel ch)
- throws Exception {
- ch.pipeline().addLast(
- new EchoClientHandler());
- }
- });
- // 下面兩行代碼可以刪除
- ChannelFuture f = b.connect().sync();
- f.channel().closeFuture().sync();
- } finally {
- group.shutdownGracefully().sync();
- }
附錄
1.完整代碼地址