Netty In Action中文版 - 第一章:Netty介紹


        本章介紹

  • Netty介紹
  • 爲什麼要使用non-blocking IO(NIO)
  • 阻塞IO(blocking IO)和非阻塞IO(non-blocking IO)對比
  • Java NIO的問題和在Netty中的解決方案
        Netty是基於Java NIO的網絡應用框架,如果你是Java網絡方面的新手,那麼本章將是你學習Java網絡應用的開始;對於有經驗的開發者來說,學習本章內容也是很好的複習。如果你熟悉NIO和NIO2,你可以隨時跳過本章直接從第二章開始學習。在你的機器上運行第二章編寫的Netty服務器和客戶端。

        Netty是一個NIO client-server(客戶端服務器)框架,使用Netty可以快速開發網絡應用,例如服務器和客戶端協議。Netty提供了一種新的方式來使開發網絡應用程序,這種新的方式使得它很容易使用和有很強的擴展性。Netty的內部實現時很複雜的,但是Netty提供了簡單易用的api從網絡處理代碼中解耦業務邏輯。Netty是完全基於NIO實現的,所以整個Netty都是異步的。

        網絡應用程序通常需要有較高的可擴展性,無論是Netty還是其他的基於Java NIO的框架,都會提供可擴展性的解決方案。Netty中一個關鍵組成部分是它的異步特性,本章將討論同步(阻塞)和異步(非阻塞)的IO來說明爲什麼使用異步代碼來解決擴展性問題以及如何使用異步。

        對於那些初學網絡變成的讀者,本章將幫助您對網絡應用的理解,以及Netty是如何實現他們的。它說明了如何使用基本的Java網絡API,探討Java網絡API的優點和缺點並闡述Netty是如何解決Java中的問題的,比如Eploo錯誤或內存泄露問題。

        在本章的結尾,你會明白什麼是Netty以及Netty提供了什麼,你會理解Java NIO和異步處理機制,並通過本書的其他章節加強理解。

1.1 爲什麼使用Netty?

        David John Wheeler說過“在計算機科學中的所有問題都可以通過間接的方法解決。”作爲一個NIO client-server框架,Netty提供了這樣的一個間接的解決方法。Netty提供了高層次的抽象來簡化TCP和UDP服務器的編程,但是你仍然可以使用底層地API。
        (David John Wheeler有一句名言“計算機科學中的任何問題都可以通過加上一層邏輯層來解決”,這個原則在計算機各技術領域被廣泛應用)

1.1.1 不是所有的網絡框架都是一樣的

        Netty的“quick and easy(高性能和簡單易用)”並不意味着編寫的程序的性能和可維護性會受到影響。從Netty中實現的協議如FTP,SMTP,HTTP,WebSocket,SPDY以及各種二進制和基於文本的傳統協議中獲得的經驗導致Netty的創始人要非常小心它的設計。Netty成功的提供了易於開發,高性能和高穩定性,以及較強的擴展性。
        高調的公司和開源項目有RedHat, Twitter, Infinispan, and HornetQ, Vert.x, Finagle, Akka, Apache Cassandra, Elasticsearch,以及其他人的使用有助於Netty的發展,Netty的一些特性也是這些項目的需要所致。多年來,Netty變的更廣爲人知,它是Java網絡的首選框架,在一些開源或非開源的項目中可以體現。並且,Netty在2011年獲得Duke's Choice Award(Duke's Choice獎)。
        此外,在2011年,Netty的創始人Trustion Lee離開RedHat後加入Twitter,在這一點上,Netty項目獎會成爲一個獨立的項目組織。RedHat和Twitter都使用Netty,所以它毫不奇怪。在撰寫本書時RedHat和Twitter這兩家公司是最大的貢獻者。使用Netty的項目越來越多,Netty的用戶羣體和項目以及Netty社區都是非常活躍的。

1.1.2 Netty的功能非常豐富

        通過本書可以學習Netty豐富的功能。下圖是Netty框架的組成

        Netty除了提供傳輸和協議,在其他各領域都有發展。Netty爲開發者提供了一套完整的工具,看下面表格:

Development Area Netty Features
Design(設計)
  • 各種傳輸類型,阻塞和非阻塞套接字統一的API
  • 使用靈活
  • 簡單但功能強大的線程模型
  • 無連接的DatagramSocket支持
  • 鏈邏輯,易於重用
Ease of Use(易於使用)
  • 提供大量的文檔和例子
  • 除了依賴jdk1.6+,沒有額外的依賴關係。某些功能依賴jdk1.7+,其他特性可能有相關依賴,但都是可選的。
Performance(性能)
  • 比Java APIS更好的吞吐量和更低的延遲
  • 因爲線程池和重用所有消耗較少的資源
  • 儘量減少不必要的內存拷貝
Robustness(魯棒性) 魯棒性,可以理解爲健壯性
  • 鏈接快或慢或超載不會導致更多的OutOfMemoryError
  • 在高速的網絡程序中不會有不公平的read/write
Security(安全性)
  • 完整的SSL/TLS和StartTLS支持
  • 可以在如Applet或OSGI這些受限制的環境中運行
Community(社區)
  • 版本發佈頻繁
  • 社區活躍
        除了列出的功能外,Netty爲Java NIO中的bug和限制也提供瞭解決方案。我們需要深刻理解Netty的功能以及它的異步處理機制和它的架構。NIO和Netty都大量使用了異步代碼,並且封裝的很好,我們無需瞭解底層的事件選擇機制。下面我們來看看爲什麼需要異步APIS。

1.2 異步設計

        整個Netty的API都是異步的,異步處理不是一個新的機制,這個機制出來已經有一些時間了。對網絡應用來說,IO一般是性能的瓶頸,使用異步IO可以較大程度上提高程序性能,因爲異步變的越來越重要。但是它是如何工作的呢?以及有哪些不同的模式可用呢?
        異步處理提倡更有效的使用資源,它允許你創建一個任務,當有事件發生時將獲得通知並等待事件完成。這樣就不會阻塞,不管事件完成與否都會及時返回,資源利用率更高,程序可以利用剩餘的資源做一些其他的事情。
        本節將說明一起工作或實現異步API的兩個最常用的方法,並討論這些技術之間的差異。

1.2.1 Callbacks(回調)

        回調一般是異步處理的一種技術。一個回調是被傳遞到並且執行完該方法。你可能認爲這種模式來自JavaScript,在Javascript中,回調是它的核心。下面的代碼顯示瞭如何使用這種技術來獲取數據。下面代碼是一個簡單的回調
package netty.in.action;

public class Worker {

	public void doWork() {
		Fetcher fetcher = new MyFetcher(new Data(1, 0));
		fetcher.fetchData(new FetcherCallback() {
			@Override
			public void onError(Throwable cause) {
				System.out.println("An error accour: " + cause.getMessage());
			}

			@Override
			public void onData(Data data) {
				System.out.println("Data received: " + data);
			}
		});
	}

	public static void main(String[] args) {
		Worker w = new Worker();
		w.doWork();
	}

}
package netty.in.action;

public interface Fetcher {
	void fetchData(FetcherCallback callback);
}
package netty.in.action;

public class MyFetcher implements Fetcher {
	
	final Data data;
	
	public MyFetcher(Data data){
		this.data = data;
	}

	@Override
	public void fetchData(FetcherCallback callback) {
		try {
			callback.onData(data);
		} catch (Exception e) {
			callback.onError(e);
		}
	}

}
package netty.in.action;

public interface FetcherCallback {
	void onData(Data data) throws Exception;
	void onError(Throwable cause);
}
package netty.in.action;

public class Data {
	
	private int n;
	private int m;

	public Data(int n,int m){
		this.n = n;
		this.m = m;
	}

	@Override
	public String toString() {
		int r = n/m;
		return n + "/" + m +" = " + r;
	}
}
        上面的例子只是一個簡單的模擬回調,要明白其所表達的含義。Fetcher.fetchData()方法需傳遞一個FetcherCallback類型的參數,當獲得數據或發生錯誤時被回調。對於每種情況都提供了同意的方法:
  • FetcherCallback.onData(),將接收數據時被調用
  • FetcherCallback.onError(),發生錯誤時被調用
        因爲可以將這些方法的執行從"caller"線程移動到其他的線程執行;但也不會保證FetcherCallback的每個方法都會被執行。回調過程有個問題就是當你使用鏈式調用
很多不同的方法會導致線性代碼;有些人認爲這種鏈式調用方法會導致代碼難以閱讀,但是我認爲這是一種風格和習慣問題。例如,基於Javascript的Node.js越來越受歡迎,它使用了大量的回調,許多人都認爲它的這種方式利於閱讀和編寫。

1.2.2 Futures

        第二種技術是使用Futures。Futures是一個抽象的概念,它表示一個值,該值可能在某一點變得可用。一個Future要麼獲得計算完的結果,要麼獲得計算失敗後的異常。Java在java.util.concurrent包中附帶了Future接口,它使用Executor異步執行。例如下面的代碼,每傳遞一個Runnable對象到ExecutorService.submit()方法就會得到一個回調的Future,你能使用它檢測是否執行完成。
package netty.in.action;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class FutureExample {
	
	public static void main(String[] args) throws Exception {
		ExecutorService executor = Executors.newCachedThreadPool();
		Runnable task1 = new Runnable() {
			@Override
			public void run() {
				//do something
				System.out.println("i am task1.....");
			}
		};
		Callable<Integer> task2 = new Callable<Integer>() {
			@Override
			public Integer call() throws Exception {
				//do something
				return new Integer(100);
			}
		};
		Future<?> f1 = executor.submit(task1);
		Future<Integer> f2 = executor.submit(task2);
		System.out.println("task1 is completed? " + f1.isDone());
		System.out.println("task2 is completed? " + f2.isDone());
		//waiting task1 completed
		while(f1.isDone()){
			System.out.println("task1 completed.");
			break;
		}
		//waiting task2 completed
		while(f2.isDone()){
			System.out.println("return value by task2: " + f2.get());
			break;
		}
	}

}
        有時候使用Future感覺很醜陋,因爲你需要間隔檢查Future是否已完成,而使用回調會直接收到返回通知。看完這兩個常用的異步執行技術後,你可能想知道使用哪個最好?這裏沒有明確的答案。事實上,Netty兩者都使用,提供兩全其美的方案。下一節將在JVM上首先使用阻塞,然後再使用NIO和NIO2寫一個網絡程序。這些是本書後續章節必不可少的基礎知識,如果你熟悉Java網絡AIPs,你可以快速翻閱即可。

1.3 Java中的Blocking和non-blocking IO對比

        本節主要講解Java的IO和NIO的差異,這裏不過多贅述,網絡已有很多相關文章。

1.4 NIO的問題和Netty中是如何解決這些問題的

        本節中將介紹Netty是如何解決NIO中的一些問題和限制。Java的NIO相對老的IO APIs有着非常大的進步,但是使用NIO是受限制的。這些問題往往是設計的問題,有些是缺陷知道的。

1.4.1 跨平臺和兼容性問題

        NIO是一個比較底層的APIs,它依賴於操作系統的IO APIs。Java實現了統一的接口來操作IO,其在所有操作系統中的工作行爲是一樣的,這是很偉大的。使用NIO會經常發現代碼在Linux上正常運行,但在Windows上就會出現問題。我建議你如果使用NIO編寫程序,就應該在所有的操作系統上進行測試來支持,使程序可以在任何操作系統上正常運行;即使在所有的Linux系統上都測試通過了,也要在其他的操作系統上進行測試;你若不驗證,以後就可能會出問題。
        NIO2看起來很理想,但是NIO2只支持Jdk1.7+,若你的程序在Java1.6上運行,則無法使用NIO2。另外,Java7的NIO2中沒有提供DatagramSocket的支持,所以NIO2只支持TCP程序,不支持UDP程序。
        Netty提供一個統一的接口,同一語義無論在Java6還是Java7的環境下都是可以運行的,開發者無需關心底層APIs就可以輕鬆實現相關功能。

1.4.2 擴展ByteBuffer

        ByteBuffer是一個數據容器,但是可惜的是JDK沒有開發ByteBuffer實現的源碼;ByteBuffer允許包裝一個byte[]來獲得一個實例,如果你希望儘量減少內存拷貝,那麼這種方式是非常有用的。若果你想將ByteBuffer重新實現,那麼不要浪費你的時間了,ByteBuffer的構造函數是私有的,所以它不能被擴展。Netty提供了自己的ByteBuffer實現,Netty通過一些簡單的APIs對ByteBuffer進行構造、使用和操作,以此來解決NIO中的一些限制。

1.4.3 NIO對緩衝區的聚合和分散操作可能會操作內存泄露

        很多Channel的實現支持Gather和Scatter。這個功能允許從從多個ByteBuffer中讀入或寫入到過個ByteBuffer,這樣做可以提供性能。操作系統底層知道如何處理這些被寫入/讀出,並且能以最有效的方式處理。如果要分割的數據再多個不同的ByteBuffer中,使用Gather/Scatter是比較好的方式。
        例如,你可能希望header在一個ByteBuffer中,而body在另外的ByteBuffer中;
        下圖顯示的是Scatter(分散),將ScatteringByteBuffer中的數據分散讀取到多個ByteBuffer中:

        下圖顯示的是Gather(聚合),將多個ByteBuffer的數據寫入到GatheringByteChannel:

        可惜Gather/Scatter功能會導致內存泄露,知道Java7才解決內存泄露問題。使用這個功能必須小心編碼和Java版本。

1.4.4 Squashing the famous epoll bug

        壓碎著名的epoll缺陷。
        On Linux-like OSs the selector makes use of the epoll- IO event notification facility. This is a high-performance technique in which the OS works asynchronously with the networking stack.Unfortunately,  even  today  the "famous" epoll- bug  can  lead  to  an "invalid" state  in  the selector, resulting in 100% CPU-usage and spinning. The only way to recover is to recycle the old  selector  and  transfer  the  previously  registered  Channel  instances  to  the  newly  created Selector. 
        Linux-like OSs的選擇器使用的是epoll-IO事件通知工具。這是一個在操作系統以異步方式工作的網絡stack.Unfortunately,即使是現在,著名的epoll-bug也可能會導致無效的狀態的選擇和100%的CPU利用率。要解決epoll-bug的唯一方法是回收舊的選擇器,將先前註冊的通道實例轉移到新創建的選擇器上。
        What  happens  here  is  that  the Selector.select() method  stops  to  block  and  returns immediately-even  if  there  are  no  selected  SelectionKeys  present.  This  is  against  the contract,  which  is  in  the  Javadocs  of  the  Selector.select()  method:Selector.select() must not unblock if nothing is selected.
        這裏發生的是,不管有沒有已選擇的SelectionKey,Selector.select()方法總是不會阻塞並且會立刻返回。這違反了Javadoc中對Selector.select()方法的描述,Javadoc中的描述:Selector.select() must not unblock if nothing is selected. (Selector.select()方法若未選中任何事件將會阻塞。)
        The range of solutions to this epoll- problem is limited, but Netty attempts to automatically detect and prevent it. The following listing is an example of the epoll- bug.
        NIO中對epoll問題的解決方案是有限制的,Netty提供了更好的解決方案。下面是epoll-bug的一個例子:
...
while (true) {
int selected = selector.select();
Set<SelectedKeys> readyKeys = selector.selectedKeys();
Iterator iterator = readyKeys.iterator();
while (iterator.hasNext()) {
...
... 
}
}
...
        The effect of this code is that the while loop eats CPU:
        這段代碼的作用是while循環消耗CPU:
...
while (true) {
}
...
        The value will never be false, and the code keeps your CPU spinning and eats resources. This can have some undesirable side effects as it can consume all of your CPU, preventing any other CPU-bound work.
        該值將永遠是假的,代碼將持續消耗你的CPU資源。這會有一些副作用,因爲CPU消耗完了就無法再去做其他任何的工作。
        These  are  only  a  few  of  the  possible  problems  you  may  see  while  using  non-blocking  IO. Unfortunately, even  after years of development  in  this area,  issues still need  to be  resolved; thankfully, Netty addresses them for you. 
        這些僅僅是在使用NIO時可能會出現的一些問題。不幸的是,雖然在這個領域發展了多年,問題依然存在;幸運的是,Netty給了你解決方案。

1.5 Summary

This  chapter  provided  an  overview  of  Netty's  features,  design  and  benefits.  I  discussed  the difference  between  blocking  and  non-blocking  processing  to  give  you  a  fundamental understanding of the reasons to use a non-blocking framework. You  learned  how  to  use  the  JDK  API  to  write  network  code  in  both  blocking  and  non-blocking modes. This included the new non-blocking API, which comes with JDK 7. After seeing the NIO APIs in action, it was also important to understand some of the known issues that you may run into. In fact, this is why so many people use Netty: to take care of workarounds and other JVM quirks. In the next chapter, you'll learn the basics of the Netty API and programming model, and, finally, use Netty to write some useful code.
發佈了32 篇原創文章 · 獲贊 11 · 訪問量 36萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章