11.3 I/O類使用
由于在IO操作中,需要使用的數據源有很多,作為一個IO技術的初學者,從讀寫文件開始學習IO技術是一個比較好的選擇。因為文件是一種常見的數據源,而且讀寫文件也是程序員進行IO編程的一個基本能力。本章IO類的使用就從讀寫文件開始。
11.3.1 文件操作
文件(File)是 最常見的數據源之一,在程序中經常需要將數據存儲到文件中,例如圖片文件、聲音文件等數據文件,也經常需要根據需要從指定的文件中進行數據的讀取。當然, 在實際使用時,文件都包含一個的格式,這個格式需要程序員根據需要進行設計,讀取已有的文件時也需要熟悉對應的文件格式,才能把數據從文件中正確的讀取出 來。
文件的存儲介質有很多,例如硬盤、光盤和U盤等,由于IO類設計時,從數據源轉換為流對象的操作由API實現了,所以存儲介質的不同對于程序員來說是透明的,和實際編寫代碼無關。
11.3.1.1 文件的概念
文件是計算機中一種基本的數據存儲形式,在實際存儲數據時,如果對于數據的讀寫速度要求不是很高,存儲的數據量不是很大時,使用文件作為一種持久數據存儲的方式是比較好的選擇。
存儲在文件內部的數據和內存中的數據不同,存儲在文件中的數據是一種“持久存儲”,也就是當程序退出或計算機關機以后,數據還是存在的,而內存內部的數據在程序退出或計算機關機以后,數據就丟失了。
在不同的存儲介質中,文件中的數據都是以一定的順序依次存儲起來,在實際讀取時由硬件以及操作系統完成對于數據的控制,保證程序讀取到的數據和存儲的順序保持一致。
每個文件以一個文件路徑和文件名稱進行表示,在需要訪問該文件的時,只需要知道該文件的路徑以及文件的全名即可。在不同的操作系統環境下,文件路徑的表示形式是不一樣的,例如在Windows操作系統中一般的表示形式為C:\windows\system,而Unix上的表示形式為/user/my。所以如果需要讓Java程序能夠在不同的操作系統下運行,書寫文件路徑時還需要比較注意。
11.3.1.1.1 絕對路徑和相對路徑
絕對路徑是指書寫文件的完整路徑,例如d:\java\Hello.java,該路徑中包含文件的完整路徑d:\java以及文件的全名Hello.java。使用該路徑可以唯一的找到一個文件,不會產生歧義。但是使用絕對路徑在表示文件時,受到的限制很大,且不能在不同的操作系統下運行,因為不同操作系統下絕對路徑的表達形式存在不同。
相對路徑是指書寫文件的部分路徑,例如\test\Hello.java,該路徑中只包含文件的部分路徑\test和文件的全名Hello.java,部分路徑是指當前路徑下的子路徑,例如當前程序在d:\abc下運行,則該文件的完整路徑就是d:\abc\test。使用這種形式,可以更加通用的代表文件的位置,使得文件路徑產生一定的靈活性。
在Eclipse項目中運行程序時,當前路徑是項目的根目錄,例如工作空間存儲在d:\javaproject,當前項目名稱是Test,則當前路徑是:d:\javaproject\Test。在控制臺下面運行程序時,當前路徑是class文件所在的目錄,如果class文件包含包名,則以該class文件最頂層的包名作為當前路徑。
另外在Java語言的代碼內部書寫文件路徑時,需要注意大小寫,大小寫需要保持一致,路徑中的文件夾名稱區分大小寫。由于’\’是Java語言中的特殊字符,所以在代碼內部書寫文件路徑時,例如代表“c:\test\java\Hello.java”時,需要書寫成“c:\\test\\java\\Hello.java”或“c:/test/java/Hello.java”,這些都需要在代碼中注意。
11.3.1.1.2 文件名稱
文件名稱一般采用“文件名.后綴名”的形式進行命名,其中“文件名”用來表示文件的作用,而使用后綴名來表示文件的類型,這是當前操作系統中常見的一種形式,例如“readme.txt”文件,其中readme代表該文件時說明文件,而txt后綴名代表文件時文本文件類型,在操作系統中,還會自動將特定格式的后綴名和對應的程序關聯,在雙擊該文件時使用特定的程序打開。
其實在文件名稱只是一個標示,和實際存儲的文件內容沒有必然的聯系,只是使用這種方式方便文件的使用。在程序中需要存儲數據時,如果自己設計了特定的文件格式,則可以自定義文件的后綴名,來標示自己的文件類型。
和文件路徑一樣,在Java代碼內部書寫文件名稱時也區分大小寫,文件名稱的大小寫必須和操作系統中的大小寫保持一致。
另外,在書寫文件名稱時不要忘記書寫文件的后綴名。
11.3.1.2 File類
為了很方便的代表文件的概念,以及存儲一些對于文件的基本操作,在java.io包中設計了一個專門的類——File類。
在File類中包含了大部分和文件操作的功能方法,該類的對象可以代表一個具體的文件或文件夾,所以以前曾有人建議將該類的類名修改成FilePath,因為該類也可以代表一個文件夾,更準確的說是可以代表一個文件路徑。
下面介紹一下File類的基本使用。
1、File對象代表文件路徑
File類的對象可以代表一個具體的文件路徑,在實際代表時,可以使用絕對路徑也可以使用相對路徑。
下面是創建的文件對象示例。
public File(String pathname)
該示例中使用一個文件路徑表示一個File類的對象,例如:
File f1 = new File(“d:\\test\\1.txt”);
File f2 = new File(“1.txt”);
File f3 = new File(“e:\\abc”);
這里的f1和f2對象分別代表一個文件,f1是絕對路徑,而f2是相對路徑,f3則代表一個文件夾,文件夾也是文件路徑的一種。
public File(String parent, String child)
也可以使用父路徑和子路徑結合,實現代表文件路徑,例如:
File f4 = new File(“d:\\test\\”,”1.txt”);
這樣代表的文件路徑是:d:\test\1.txt。
2、File類常用方法
File類中包含了很多獲得文件或文件夾屬性的方法,使用起來比較方便,下面將常見的方法介紹如下:
a、createNewFile方法
public boolean createNewFile() throws IOException
該方法的作用是創建指定的文件。該方法只能用于創建文件,不能用于創建文件夾,且文件路徑中包含的文件夾必須存在。
b、delect方法
public boolean delete()
該方法的作用是刪除當前文件或文件夾。如果刪除的是文件夾,則該文件夾必須為空。如果需要刪除一個非空的文件夾,則需要首先刪除該文件夾內部的每個文件和文件夾,然后在可以刪除,這個需要書寫一定的邏輯代碼實現。
c、exists方法
public boolean exists()
該方法的作用是判斷當前文件或文件夾是否存在。
d、getAbsolutePath方法
public String getAbsolutePath()
該方法的作用是獲得當前文件或文件夾的絕對路徑。例如c:\test\1.t則返回c:\test\1.t。
e、getName方法
public String getName()
該方法的作用是獲得當前文件或文件夾的名稱。例如c:\test\1.t,則返回1.t。
f、getParent方法
public String getParent()
該方法的作用是獲得當前路徑中的父路徑。例如c:\test\1.t則返回c:\test。
g、isDirectory方法
public boolean isDirectory()
該方法的作用是判斷當前File對象是否是目錄。
h、isFile方法
public boolean isFile()
該方法的作用是判斷當前File對象是否是文件。
i、length方法
public long length()
該方法的作用是返回文件存儲時占用的字節數。該數值獲得的是文件的實際大小,而不是文件在存儲時占用的空間數。
j、list方法
public String[] list()
該方法的作用是返回當前文件夾下所有的文件名和文件夾名稱。說明,該名稱不是絕對路徑。
k、listFiles方法
public File[] listFiles()
該方法的作用是返回當前文件夾下所有的文件對象。
l、mkdir方法
public boolean mkdir()
該方法的作用是創建當前文件文件夾,而不創建該路徑中的其它文件夾。假設d盤下只有一個test文件夾,則創建d:\test\abc文件夾則成功,如果創建d:\a\b文件夾則創建失敗,因為該路徑中d:\a文件夾不存在。如果創建成功則返回true,否則返回false。
m、mkdirs方法
public boolean mkdirs()
該方法的作用是創建文件夾,如果當前路徑中包含的父目錄不存在時,也會自動根據需要創建。
n、renameTo方法
public boolean renameTo(File dest)
該方法的作用是修改文件名。在修改文件名時不能改變文件路徑,如果該路徑下已有該文件,則會修改失敗。
o、setReadOnly方法
public boolean setReadOnly()
該方法的作用是設置當前文件或文件夾為只讀。
3、File類基本示例
以上各方法實現的測試代碼如下:
import java.io.File;
/**
* File類使用示例
*/
public class FileDemo {
public static void main(String[] args) {
//創建File對象
File f1 = new File("d:\\test");
File f2 = new File("1.txt");
File f3 = new File("e:\\file.txt");
File f4 = new File("d:\\","1.txt");
//創建文件
try{
boolean b = f3.createNewFile();
}catch(Exception e){
e.printStackTrace();
}
//判斷文件是否存在
System.out.println(f4.exists());
//獲得文件的絕對路徑
System.out.println(f3.getAbsolutePath());
//獲得文件名
System.out.println(f3.getName());
//獲得父路徑
System.out.println(f3.getParent());
//判斷是否是目錄
System.out.println(f1.isDirectory());
//判斷是否是文件
System.out.println(f3.isFile());
//獲得文件長度
System.out.println(f3.length());
//獲得當前文件夾下所有文件和文件夾名稱
String[] s = f1.list();
for(int i = 0;i < s.length;i++){
System.out.println(s[i]);
}
//獲得文件對象
File[] f5 = f1.listFiles();
for(int i = 0;i < f5.length;i++){
System.out.println(f5[i]);
}
//創建文件夾
File f6 = new File("e:\\test\\abc");
boolean b1 = f6.mkdir();
System.out.println(b1);
b1 = f6.mkdirs();
System.out.println(b1);
//修改文件名
File f7 = new File("e:\\a.txt");
boolean b2 = f3.renameTo(f7);
System.out.println(b2);
//設置文件為只讀
f7.setReadOnly();
}
}
4、File類綜合示例
下面以兩個示例演示File類的綜合使用。第一個示例是顯示某個文件夾下的所有文件和文件夾,原理是輸出當前名稱,然后判斷當前File對 象是文件還是文件夾,如果則獲得該文件夾下的所有子文件和子文件夾,并遞歸調用該方法實現。第二個示例是刪除某個文件夾下的所有文件和文件夾,原理是判斷 是否是文件,如果是文件則直接刪除,如果是文件夾,則獲得該文件夾下所有的子文件和子文件夾,然后遞歸調用該方法處理所有子文件和子文件夾,然后將空文件 夾刪除。則測試時謹慎使用第二個方法,以免刪除自己有用的數據文件。示例代碼如下:
import java.io.File;
/**
* 文件綜合使用示例
*/
public class AdvanceFileDemo {
public static void main(String[] args) {
File f = new File("e:\\Book");
printAllFile(f);
File f1 = new File("e:\\test");
deleteAll(f1);
}
/**
* 打印f路徑下所有的文件和文件夾
* @param f 文件對象
*/
public static void printAllFile(File f){
//打印當前文件名
System.out.println(f.getName());
//是否是文件夾
if(f.isDirectory()){
//獲得該文件夾下所有子文件和子文件夾
File[] f1 = f.listFiles();
//循環處理每個對象
int len = f1.length;
for(int i = 0;i < len;i++){
//遞歸調用,處理每個文件對象
printAllFile(f1[i]);
}
}
}
/**
* 刪除對象f下的所有文件和文件夾
* @param f 文件路徑
*/
public static void deleteAll(File f){
//文件
if(f.isFile()){
f.delete();
}else{ //文件夾
//獲得當前文件夾下的所有子文件和子文件夾
File f1[] = f.listFiles();
//循環處理每個對象
int len = f1.length;
for(int i = 0;i < len;i++){
//遞歸調用,處理每個文件對象
deleteAll(f1[i]);
}
//刪除當前文件夾
f.delete();
}
}
}
關于File類的使用就介紹這么多,其它的方法和使用時需要注意的問題還需要多進行練習和實際使用。
11.3.1.3 讀取文件
雖然前面介紹了流的概念,但是這個概念對于初學者來說,還是比較抽象的,下面以實際的讀取文件為例子,介紹流的概念,以及輸入流的基本使用。
按照前面介紹的知識,將文件中的數據讀入程序,是將程序外部的數據傳入程序中,應該使用輸入流——InputStream或Reader。而由于讀取的是特定的數據源——文件,則可以使用輸入對應的子類FileInputStream或FileReader實現。
在實際書寫代碼時,需要首先熟悉讀取文件在程序中實現的過程。在Java語言的IO編程中,讀取文件是分兩個步驟:1、將文件中的數據轉換為流,2、讀取流內部的數據。其中第一個步驟由系統完成,只需要創建對應的流對象即可,對象創建完成以后步驟1就完成了,第二個步驟使用輸入流對象中的read方法即可實現了。
使用輸入流進行編程時,代碼一般分為3個部分:1、創建流對象,2、讀取流對象內部的數據,3、關閉流對象。下面以讀取文件的代碼示例:
import java.io.*;
/**
* 使用FileInputStream讀取文件
*/
public class ReadFile1 {
public static void main(String[] args) {
//聲明流對象
FileInputStream fis = null;
try{
//創建流對象
fis = new FileInputStream("e:\\a.txt");
//讀取數據,并將讀取到的數據存儲到數組中
byte[] data = new byte[1024]; //數據存儲的數組
int i = 0; //當前下標
//讀取流中的第一個字節數據
int n = fis.read();
//依次讀取后續的數據
while(n != -1){ //未到達流的末尾
//將有效數據存儲到數組中
data[i] = (byte)n;
//下標增加
i++;
//讀取下一個字節的數據
n = fis.read();
}
//解析數據
String s = new String(data,0,i);
//輸出字符串
System.out.println(s);
}catch(Exception e){
e.printStackTrace();
}finally{
try{
//關閉流,釋放資源
fis.close();
}catch(Exception e){}
}
}
}
在該示例代碼中,首先創建一個FileInputStream類型的對象fis:
fis = new FileInputStream("e:\\a.txt");
這樣建立了一個連接到數據源e:\a.txt的流,并將該數據源中的數據轉換為流對象fis,以后程序讀取數據源中的數據,只需要從流對象fis中讀取即可。
讀取流fis中的數據,需要使用read方法,該方法是從InputStream類中繼承過來的方法,該方法的作用是每次讀取流中的一個字節,如果需要讀取流中的所有數據,需要使用循環讀取,當到達流的末尾時,read方法的返回值是-1。
在該示例中,首先讀取流中的第一個字節:
int n = fis.read();
并將讀取的值賦值給int值n,如果流fis為空,則n的值是-1,否則n中的最后一個字節包含的時流fis中的第一個字節,該字節被讀取以后,將被從流fis中刪除。
然后循環讀取流中的其它數據,如果讀取到的數據不是-1,則將已經讀取到的數據n強制轉換為byte,即取n中的有效數據——最后一個字節,并存儲到數組data中,然后調用流對象fis中的read方法繼續讀取流中的下一個字節的數據。一直這樣循環下去,直到讀取到的數據是-1,也就是讀取到流的末尾則循環結束。
這里的數組長度是1024,所以要求流中的數據長度不能超過1024,所以該示例代碼在這里具有一定的局限性。如果流的數據個數比較多,則可以將1024擴大到合適的個數即可。
經過上面的循環以后,就可以將流中的數據依次存儲到data數組中,存儲到data數組中有效數據的個數是i個,即循環次數。
其實截至到這里,IO操作中的讀取數據已經完成,然后再按照數據源中的數據格式,這里是文件的格式,解析讀取出的byte數組即可。
該示例代碼中的解析,只是將從流對象中讀取到的有效的數據,也就是data數組中的前n個數據,轉換為字符串,然后進行輸出。
在該示例代碼中,只是在catch語句中輸出異常的信息,便于代碼的調試,在實際的程序中,需要根據情況進行一定的邏輯處理,例如給出提示信息等。
最后在finally語句塊中,關閉流對象fis,釋放流對象占用的資源,關閉數據源,實現流操作的結束工作。
上面詳細介紹了讀取文件的過程,其實在實際讀取流數據時,還可以使用其它的read方法,下面的示例代碼是使用另外一個read方法實現讀取的代碼:
import java.io.FileInputStream;
/**
* 使用FileInputStream讀取文件
*/
public class ReadFile2 {
public static void main(String[] args) {
//聲明流對象
FileInputStream fis = null;
try{
//創建流對象
fis = new FileInputStream("e:\\a.txt");
//讀取數據,并將讀取到的數據存儲到數組中
byte[] data = new byte[1024]; //數據存儲的數組
int i = fis.read(data);
//解析數據
String s = new String(data,0,i);
//輸出字符串
System.out.println(s);
}catch(Exception e){
e.printStackTrace();
}finally{
try{
//關閉流,釋放資源
fis.close();
}catch(Exception e){}
}
}
}
該示例代碼中,只使用一行代碼:
int i = fis.read(data);
就實現了將流對象fis中的數據讀取到字節數組data中。該行代碼的作用是將fis流中的數據讀取出來,并依次存儲到數組data中,返回值為實際讀取的有效數據的個數。
使用該中方式在進行讀取時,可以簡化讀取的代碼。
當然,在讀取文件時,也可以使用Reader類的子類FileReader進行實現,在編寫代碼時,只需要將上面示例代碼中的byte數組替換成char數組即可。
使用FileReader讀取文件時,是按照char為單位進行讀取的,所以更適合于文本文件的讀取,而對于二進制文件或自定義格式的文件來說,還是使用FileInputStream進行讀取,方便對于讀取到的數據進行解析和操作。
讀取其它數據源的操作和讀取文件類似,最大的區別在于建立流對象時選擇的類不同,而流對象一旦建立,則基本的讀取方法是一樣,如果只使用最基本的read方法進行讀取,則使用基本上是一致的。這也是IO類設計的初衷,使得對于流對象的操作保持一致,簡化IO類使用的難度。
程。
基本的輸出流包含OutputStream和Writer兩個,區別是OutputStream體系中的類(也就是OutputStream的子類)是按照字節寫入的,而Writer體系中的類(也就是Writer的子類)是按照字符寫入的。
使用輸出流進行編程的步驟是:
1、建立輸出流
建立對應的輸出流對象,也就是完成由流對象到外部數據源之間的轉換。
2、向流中寫入數據
將需要輸出的數據,調用對應的write方法寫入到流對象中。
3、關閉輸出流
在寫入完畢以后,調用流對象的close方法關閉輸出流,釋放資源。
在使用輸出流向外部輸出數據時,程序員只需要將數據寫入流對象即可,底層的API實現將流對象中的內容寫入外部數據源,這個寫入的過程對于程序員來說是透明的,不需要專門書寫代碼實現。
在向文件中輸出數據,也就是寫文件時,使用對應的文件輸出流,包括FileOutputStream和FileWriter兩個類,下面以FileOutputStream為例子說明輸出流的使用。示例代碼如下:
import java.io.*;
/**
* 使用FileOutputStream寫文件示例
*/
public class WriteFile1 {
public static void main(String[] args) {
String s = "Java語言";
int n = 100;
//聲明流對象
FileOutputStream fos = null;
try{
//創建流對象
fos = new FileOutputStream("e:\\out.txt");
//轉換為byte數組
byte[] b1 = s.getBytes();
//換行符
byte[] b2 = "\r\n".getBytes();
byte[] b3 = String.valueOf(n).getBytes();
//依次寫入文件
fos.write(b1);
fos.write(b2);
fos.write(b3);
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
fos.close();
}catch(Exception e){}
}
}
}
該示例代碼寫入的文件使用記事本打開以后,內容為:
Java語言
100
在該示例代碼中,演示了將一個字符串和一個int類型的值依次寫入到同一個文件中。在寫入文件時,首先創建了一個文件輸出流對象fos:
fos = new FileOutputStream("e:\\out.txt");
該對象創建以后,就實現了從流到外部數據源e:\out.txt的連接。說明:當外部文件不存在時,系統會自動創建該文件,但是如果文件路徑中包含未創建的目錄時將出現異常。這里書寫的文件路徑可以是絕對路徑也可以是相對路徑。
在 實際寫入文件時,有兩種寫入文件的方式:覆蓋和追加。其中“覆蓋”是指清除原文件的內容,寫入新的內容,默認采用該種形式寫文件,“追加”是指在已有文件 的末尾寫入內容,保留原來的文件內容,例如寫日志文件時,一般采用追加。在實際使用時可以根據需要采用適合的形式,可以使用:
public FileOutputStream(String name, boolean append) throws FileNotFoundException
只需要使用該構造方法在構造FileOutputStream對象時,將第二個參數append的值設置為true即可。
流對象創建完成以后,就可以使用OutputStream中提供的wirte方法向流中依次寫入數據了。最基本的寫入方法只支持byte數組格式的數據,所以如果需要將內容寫入文件,則需要把對應的內容首先轉換為byte數組。
這里以如下格式寫入數據:首先寫入字符串s,使用String類的getBytes方法將該字符串轉換為byte數組,然后寫入字符串“\r\n”,轉換方式同上,該字符串的作用是實現文本文件的換行顯示,最后寫入int數據n,首先將n轉換為字符串,再轉換為byte數組。這種寫入數據的順序以及轉換為byte數組的方式就是流的數據格式,也就是該文件的格式。因為這里寫的都是文本文件,所以寫入的內容以明文的形式顯示出來,也可以根據自己需要存儲的數據設定特定的文件格式。
其實,所有的數據文件,包括圖片文件、聲音文件等等,都是以一定的數據格式存儲數據的,在保存該文件時,將需要保存的數據按照該文件的數據格式依次寫入即可,而在打開該文件時,將讀取到的數據按照該文件的格式解析成對應的邏輯即可。
最后,在數據寫入到流內部以后,如果需要立即將寫入流內部的數據強制輸出到外部的數據源,則可以使用流對象的flush方法實現。如果不需要強制輸出,則只需要在寫入結束以后,關閉流對象即可。在關閉流對象時,系統首先將流中未輸出到數據源中的數據強制輸出,然后再釋放該流對象占用的內存空間。
使用FileWriter寫入文件時,步驟和創建流對象的操作都和該示例代碼一致,只是在轉換數據時,需要將寫入的數據轉換為char數組,對于字符串來說,可以使用String中的toCharArray方法實現轉換,然后按照文件格式寫入數據即可。
對于其它類型的字節輸出流/字符輸出流來說,只是在邏輯上連接不同的數據源,在創建對象的代碼上會存在一定的不同,但是一旦流對象創建完成以后,基本的寫入方法都是write方法,也需要首先將需要寫入的數據按照一定的格式轉換為對應的byte數組/char數組,然后依次寫入即可。
所以IO類的這種設計形式,只需要熟悉該體系中的某一個類的使用以后,就可以觸類旁通的學會其它相同類型的類的使用,從而簡化程序員的學習,使得使用時保持統一。

序言
本指南對Netty 進行了介紹并指出其意義所在。
1. 問題
現在,我們使用適合一般用途的應用或組件來和彼此通信。例如,我們常常使用一個HTTP客戶端從遠程服務器獲取信息或者通過web services進行遠程方法的調用。
然而,一個適合普通目的的協議或其實現并不具備其規模上的擴展性。例如,我們無法使用一個普通的HTTP服務器進行大型文件,電郵信息的交互,或者處理金 融信息和多人游戲數據那種要求準實時消息傳遞的應用場景。因此,這些都要求使用一個適用于特殊目的并經過高度優化的協議實現。例如,你可能想要實現一個對 基于AJAX的聊天應用,媒體流或大文件傳輸進行過特殊優化的HTTP服務器。你甚至可能想去設計和實現一個全新的,特定于你的需求的通信協議。
另一種無法避免的場景是你可能不得不使用一種專有的協議和原有系統交互。在這種情況下,你需要考慮的是如何能夠快速的開發出這個協議的實現并且同時還沒有犧牲最終應用的性能和穩定性。
2. 方案
Netty 是一個異步的,事件驅動的網絡編程框架和工具,使用Netty 可以快速開發出可維護的,高性能、高擴展能力的協議服務及其客戶端應用。
也就是說,Netty 是一個基于NIO的客戶,服務器端編程框架,使用Netty 可以確保你快速和簡單的開發出一個網絡應用,例如實現了某種協議的客戶,服務端應用。Netty相當簡化和流線化了網絡應用的編程開發過程,例如,TCP和UDP的socket服務開發。
“快速”和“簡單”并不意味著會讓你的最終應用產生維護性或性能上的問題。Netty 是一個吸收了多種協議的實現經驗,這些協議包括FTP,SMPT,HTTP,各種二進制,文本協議,并經過相當精心設計的項目,最終,Netty 成功的找到了一種方式,在保證易于開發的同時還保證了其應用的性能,穩定性和伸縮性。
一些用戶可能找到了某些同樣聲稱具有這些特性的編程框架,因此你們可能想問Netty 又有什么不一樣的地方。這個問題的答案是Netty 項目的設計哲學。從創立之初,無論是在API還是在其實現上Netty 都致力于為你提供最為舒適的使用體驗。雖然這并不是顯而易見的,但你終將會認識到這種設計哲學將令你在閱讀本指南和使用Netty 時變得更加得輕松和容易。
第一章. 開始
這一章節將圍繞Netty的核心結構展開,同時通過一些簡單的例子可以讓你更快的了解Netty的使用。當你讀完本章,你將有能力使用Netty完成客戶端和服務端的開發。
如果你更喜歡自上而下式的學習方式,你可以首先完成 第二章:架構總覽 的學習,然后再回到這里。
1.1. 開始之前
運行本章示例程序的兩個最低要求是:最新版本的Netty程序以及JDK 1.5或更高版本。最新版本的Netty程序可在項目下載頁 下載。下載正確版本的JDK,請到你偏好的JDK站點下載。
這就已經足夠了嗎?實際上你會發現,這兩個條件已經足夠你完成任何協議的開發了。如果不是這樣,請聯系Netty項目社區 ,讓我們知道還缺少了什么。
最終但不是至少,當你想了解本章所介紹的類的更多信息時請參考API手冊。為方便你的使用,這篇文檔中所有的類名均連接至在線API手冊。此外,如果本篇文檔中有任何錯誤信息,無論是語法錯誤,還是打印排版錯誤或者你有更好的建議,請不要顧慮,立即聯系Netty項目社區 。
1.2. 拋棄協議服務
在這個世界上最簡化的協議不是“Hello,world!”而是拋棄協議 。這是一種丟棄接收到的任何數據并不做任何回應的協議。
實現拋棄協議(DISCARD protocol),你僅需要忽略接受到的任何數據即可。讓我們直接從處理器(handler)實現開始,這個處理器處理Netty的所有I/O事件。
Java代碼
package org.jboss.netty.example.discard;
@ChannelPipelineCoverage("all")1
public class DiscardServerHandler extends SimpleChannelHandler {2
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {3
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {4
e.getCause().printStackTrace();
Channel ch = e.getChannel();
ch.close();
}
}
代碼說明
1)ChannelPipelineCoverage注解了一種處理器類型,這個注解標示了一個處理器是 否可被多個Channel通道共享(同時關聯著ChannelPipeline)。DiscardServerHandler沒有處理任何有狀態的信息, 因此這里的注解是“all”。
2)DiscardServerHandler繼承了SimpleChannelHandler,這也是一個ChannelHandler 的實現。SimpleChannelHandler提供了多種你可以重寫的事件處理方法。目前直接繼承SimpleChannelHandler已經足夠 了,并不需要你完成一個自己的處理器接口。
3)我們這里重寫了messageReceived事件處理方法。這個方法由一個接收了客戶端傳送數據的MessageEvent事件調用。在這個例子中,我們忽略接收到的任何數據,并以此來實現一個拋棄協議(DISCARD protocol)。
4)exceptionCaught 事件處理方法由一個ExceptionEvent異常事件調用,這個異常事件起因于Netty的I/O異常或一個處理器實現的內部異常。多數情況下,捕捉 到的異常應當被記錄下來,并在這個方法中關閉這個channel通道。當然處理這種異常情況的方法實現可能因你的實際需求而有所不同,例如,在關閉這個連 接之前你可能會發送一個包含了錯誤碼的響應消息。
目前進展不錯,我們已經完成了拋棄協議服務器的一半開發工作。下面要做的是完成一個可以啟動這個包含DiscardServerHandler處理器服務的主方法。
Java代碼
package org.jboss.netty.example.discard;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
public class DiscardServer {
public static void main(String[] args) throws Exception {
ChannelFactory factory =
new NioServerSocketChannelFactory (
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
ServerBootstrap bootstrap = new ServerBootstrap (factory);
DiscardServerHandler handler = new DiscardServerHandler();
ChannelPipeline pipeline = bootstrap.getPipeline();
pipeline.addLast("handler", handler);
bootstrap.setOption("child.tcpNoDelay", true);
bootstrap.setOption("child.keepAlive", true);
bootstrap.bind(new InetSocketAddress(8080));
}
}
代碼說明
1)ChannelFactory 是一個創建和管理Channel通道及其相關資源的工廠接口,它處理所有的I/O請求并產生相應的I/O ChannelEvent通道事件。Netty 提供了多種 ChannelFactory 實現。這里我們需要實現一個服務端的例子,因此我們使用NioServerSocketChannelFactory實現。另一件需要注意的事情是這個工 廠并自己不負責創建I/O線程。你應當在其構造器中指定該工廠使用的線程池,這樣做的好處是你獲得了更高的控制力來管理你的應用環境中使用的線程,例如一 個包含了安全管理的應用服務。
2)ServerBootstrap 是一個設置服務的幫助類。你甚至可以在這個服務中直接設置一個Channel通道。然而請注意,這是一個繁瑣的過程,大多數情況下并不需要這樣做。
3)這里,我們將DiscardServerHandler處理器添加至默認的ChannelPipeline通道。任何時候當服務器接收到一個新的連 接,一個新的ChannelPipeline管道對象將被創建,并且所有在這里添加的ChannelHandler對象將被添加至這個新的 ChannelPipeline管道對象。這很像是一種淺拷貝操作(a shallow-copy operation);所有的Channel通道以及其對應的ChannelPipeline實例將分享相同的DiscardServerHandler 實例。
4)你也可以設置我們在這里指定的這個通道實現的配置參數。我們正在寫的是一個TCP/IP服務,因此我們運行設定一些socket選項,例如 tcpNoDelay和keepAlive。請注意我們在配置選項里添加的"child."前綴。這意味著這個配置項僅適用于我們接收到的通道實例,而不 是ServerSocketChannel實例。因此,你可以這樣給一個ServerSocketChannel設定參數:
bootstrap.setOption("reuseAddress", true);
5)我們繼續。剩下要做的是綁定這個服務使用的端口并且啟動這個服務。這里,我們綁定本機所有網卡(NICs,network interface cards)上的8080端口。當然,你現在也可以對應不同的綁定地址多次調用綁定操作。
大功告成!現在你已經完成你的第一個基于Netty的服務端程序。
1.3. 查看接收到的數據
現在你已經完成了你的第一個服務端程序,我們需要測試它是否可以真正的工作。最簡單的方法是使用telnet 命令。例如,你可以在命令行中輸入“telnet localhost 8080 ”或其他類型參數。
然而,我們可以認為服務器在正常工作嗎?由于這是一個丟球協議服務,所以實際上我們無法真正的知道。你最終將收不到任何回應。為了證明它在真正的工作,讓我們修改代碼打印其接收到的數據。
我們已經知道當完成數據的接收后將產生MessageEvent消息事件,并且也會觸發messageReceived處理方法。所以讓我在DiscardServerHandler處理器的messageReceived方法內增加一些代碼。
Java代碼
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
ChannelBuffer buf = (ChannelBuffer) e.getMessage();
while(buf.readable()) {
System.out.println((char) buf.readByte());
}
}
代碼說明
1) 基本上我們可以假定在socket的傳輸中消息類型總是ChannelBuffer。ChannelBuffer是Netty的一個基本數據結構,這個數 據結構存儲了一個字節序列。ChannelBuffer類似于NIO的ByteBuffer,但是前者卻更加的靈活和易于使用。例如,Netty允許你創 建一個由多個ChannelBuffer構建的復合ChannelBuffer類型,這樣就可以減少不必要的內存拷貝次數。
2) 雖然ChannelBuffer有些類似于NIO的ByteBuffer,但強烈建議你參考Netty的API手冊。學會如何正確的使用ChannelBuffer是無障礙使用Netty的關鍵一步。
如果你再次運行telnet命令,你將會看到你所接收到的數據。
拋棄協議服務的所有源代碼均存放在在分發版的org.jboss.netty.example.discard包下。
1.4. 響應協議服務
目前,我們雖然使用了數據,但最終卻未作任何回應。然而一般情況下,一個服務都需要回應一個請求。讓我們實現ECHO協議 來學習如何完成一個客戶請求的回應消息,ECHO協議規定要返回任何接收到的數據。
與我們上一節實現的拋棄協議服務唯一不同的地方是,這里需要返回所有的接收數據而不是僅僅打印在控制臺之上。因此我們再次修改messageReceived方法就足夠了。
Java代碼
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
Channel ch = e.getChannel();
ch.write(e.getMessage());
}
代碼說明
1) 一個ChannelEvent通道事件對象自身存有一個和其關聯的Channel對象引用。這個返回的Channel通道對象代表了這個接收 MessageEvent消息事件的連接(connection)。因此,我們可以通過調用這個Channel通道對象的write方法向遠程節點寫入返 回數據。
現在如果你再次運行telnet命令,你將會看到服務器返回的你所發送的任何數據。
相應服務的所有源代碼存放在分發版的org.jboss.netty.example.echo包下。
1.5. 時間協議服務
這一節需要實現的協議是TIME協議 。這是一個與先前所介紹的不同的例子。這個例子里,服務端返回一個32位的整數消息,我們不接受請求中包含的任何數據并且當消息返回完畢后立即關閉連接。通過這個例子你將學會如何構建和發送消息,以及當完成處理后如何主動關閉連接。
因為我們會忽略接收到的任何數據而只是返回消息,這應當在建立連接后就立即開始。因此這次我們不再使用messageReceived方法,取而代之的是使用channelConnected方法。下面是具體的實現:
Java代碼
package org.jboss.netty.example.time;
@ChannelPipelineCoverage("all")
public class TimeServerHandler extends SimpleChannelHandler {
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
Channel ch = e.getChannel();
ChannelBuffer time = ChannelBuffers.buffer(4);
time.writeInt(System.currentTimeMillis() / 1000);
ChannelFuture f = ch.write(time);
f.addListener(new ChannelFutureListener() {
public void operationComplete(ChannelFuture future) {
Channel ch = future.getChannel();
ch.close();
}
});
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
e.getCause().printStackTrace();
e.getChannel().close();
}
}
代碼說明
1) 正如我們解釋過的,channelConnected方法將在一個連接建立后立即觸發。因此讓我們在這個方法里完成一個代表當前時間(秒)的32位整數消息的構建工作。
2) 為了發送一個消息,我們需要分配一個包含了這個消息的buffer緩沖。因為我們將要寫入一個32位的整數,因此我們需要一個4字節的 ChannelBuffer。ChannelBuffers是一個可以創建buffer緩沖的幫助類。除了這個buffer方 法,ChannelBuffers還提供了很多和ChannelBuffer相關的實用方法。更多信息請參考API手冊。
另外,一個很不錯的方法是使用靜態的導入方式:
import static org.jboss.netty.buffer.ChannelBuffers.*;
...
ChannelBuffer dynamicBuf = dynamicBuffer(256);
ChannelBuffer ordinaryBuf = buffer(1024);
3) 像通常一樣,我們需要自己構造消息。
但是打住,flip在哪?過去我們在使用NIO發送消息時不是常常需要調用 ByteBuffer.flip()方法嗎?實際上ChannelBuffer之所以不需要這個方法是因為 ChannelBuffer有兩個指針;一個對應讀操作,一個對應寫操作。當你向一個 ChannelBuffer寫入數據的時候寫指針的索引值便會增加,但與此同時讀指針的索引值不會有任何變化。讀寫指針的索引值分別代表了這個消息的開 始、結束位置。
與之相應的是,NIO的buffer緩沖沒有為我們提供如此簡潔的一種方法,除非你調用它的flip方法。因此,當你忘記調用flip方法而引起發送錯誤 時,你便會陷入困境。這樣的錯誤不會再Netty中發生,因為我們對應不同的操作類型有不同的指針。你會發現就像你已習慣的這樣過程變得更加容易—一種沒 有flippling的體驗!
另一點需要注意的是這個寫方法返回了一個ChannelFuture對象。一個ChannelFuture 對象代表了一個尚未發生的I/O操作。這意味著,任何已請求的操作都可能是沒有被立即執行的,因為在Netty內部所有的操作都是異步的。例如,下面的代 碼可能會關閉一 個連接,這個操作甚至會發生在消息發送之前:
Channel ch = ...;
ch.write(message);
ch.close();
因此,你需要這個write方法返回的ChannelFuture對象,close方法需要等待寫操作異步完成之后的ChannelFuture通知/監聽觸發。需要注意的是,關閉方法仍舊不是立即關閉一個連接,它同樣也是返回了一個ChannelFuture對象。
4) 在寫操作完成之后我們又如何得到通知?這個只需要簡單的為這個返回的ChannelFuture對象增加一個ChannelFutureListener 即可。在這里我們創建了一個匿名ChannelFutureListener對象,在這個ChannelFutureListener對象內部我們處理了 異步操作完成之后的關閉操作。
另外,你也可以通過使用一個預定義的監聽類來簡化代碼。
f.addListener(ChannelFutureListener.CLOSE);
1.6. 時間協議服務客戶端
不同于DISCARD和ECHO協議服務,我們需要一個時間協議服務的客戶端,因為人們無法直接將一個32位的二進制數據轉換一個日歷時間。在這一節我們將學習如何確保服務器端工作正常,以及如何使用Netty完成客戶端的開發。
使用Netty開發服務器端和客戶端代碼最大的不同是要求使用不同的Bootstrap及ChannelFactory。請參照以下的代碼:
Java代碼
package org.jboss.netty.example.time;
import java.net.InetSocketAddress;
import java.util.concurrent.Executors;
public class TimeClient {
public static void main(String[] args) throws Exception {
String host = args[0];
int port = Integer.parseInt(args[1]);
ChannelFactory factory =
new NioClientSocketChannelFactory (
Executors.newCachedThreadPool(),
Executors.newCachedThreadPool());
ClientBootstrap bootstrap = new ClientBootstrap (factory);
TimeClientHandler handler = new TimeClientHandler();
bootstrap.getPipeline().addLast("handler", handler);
bootstrap.setOption("tcpNoDelay" , true);
bootstrap.setOption("keepAlive", true);
bootstrap.connect (new InetSocketAddress(host, port));
}
}
代碼說明
1) 使用NioClientSocketChannelFactory而不是NioServerSocketChannelFactory來創建客戶端的Channel通道對象。
2) 客戶端的ClientBootstrap對應ServerBootstrap。
3) 請注意,這里不存在使用“child.”前綴的配置項,客戶端的SocketChannel實例不存在父級Channel對象。
4) 我們應當調用connect連接方法,而不是之前的bind綁定方法。
正如你所看到的,這與服務端的啟動過程是完全不一樣的。ChannelHandler又該如何實現呢?它應當負責接收一個32位的整數,將其轉換為可讀的格式后,打印輸出時間,并關閉這個連接。
Java代碼
package org.jboss.netty.example.time;
import java.util.Date;
@ChannelPipelineCoverage("all")
public class TimeClientHandler extends SimpleChannelHandler {
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
ChannelBuffer buf = (ChannelBuffer) e.getMessage();
long currentTimeMillis = buf.readInt() * 1000L;
System.out.println(new Date(currentTimeMillis));
e.getChannel().close();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
e.getCause().printStackTrace();
e.getChannel().close();
}
}
這看起來很是簡單,與服務端的實現也并未有什么不同。然而,這個處理器卻時常會因為拋出IndexOutOfBoundsException異常而拒絕工作。我們將在下一節討論這個問題產生的原因。
1.7. 流數據的傳輸處理
1.7.1. Socket Buffer的缺陷
對于例如TCP/IP這種基于流的傳輸協議實現,接收到的數據會被存儲在socket的接受緩沖區內。不幸的是,這種基于流的傳輸緩沖區并不是一個包隊 列,而是一個字節隊列。這意味著,即使你以兩個數據包的形式發送了兩條消息,操作系統卻不會把它們看成是兩條消息,而僅僅是一個批次的字節序列。因此,在 這種情況下我們就無法保證收到的數據恰好就是遠程節點所發送的數據。例如,讓我們假設一個操作系統的TCP/IP堆棧收到了三個數據包:
+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+
由于這種流傳輸協議的普遍性質,在你的應用中有較高的可能會把這些數據讀取為另外一種形式:
+----+-------+---+---+
| AB | CDEFG | H | I |
+----+-------+---+---+
因此對于數據的接收方,不管是服務端還是客戶端,應當重構這些接收到的數據,讓其變成一種可讓你的應用邏輯易于理解的更有意義的數據結構。在上面所述的這個例子中,接收到的數據應當重構為下面的形式:
+-----+-----+-----+
| ABC | DEF | GHI |
+-----+-----+-----+
1.7.2. 第一種方案
現在讓我們回到時間協議服務客戶端的例子中。我們在這里遇到了同樣的問題。一個32位的整數是一個非常小的數據量,因此它常常不會被切分在不同的數據段內。然而,問題是它確實可以被切分在不同的數據段內,并且這種可能性隨著流量的增加而提高。
最簡單的方案是在程序內部創建一個可準確接收4字節數據的累積性緩沖。下面的代碼是修復了這個問題后的TimeClientHandler實現。
Java代碼
package org.jboss.netty.example.time;
import static org.jboss.netty.buffer.ChannelBuffers.*;
import java.util.Date;
@ChannelPipelineCoverage("one")
public class TimeClientHandler extends SimpleChannelHandler {
private final ChannelBuffer buf = dynamicBuffer();
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
ChannelBuffer m = (ChannelBuffer) e.getMessage();
buf.writeBytes(m);
if (buf.readableBytes() >= 4) {
long currentTimeMillis = buf.readInt() * 1000L;
System.out.println(new Date(currentTimeMillis));
e.getChannel().close();
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) {
e.getCause().printStackTrace();
e.getChannel().close();
}
}
代碼說明
1) 這一次我們使用“one”做為ChannelPipelineCoverage的注解值。這是由于這個修改后的TimeClientHandler不在不 在內部保持一個buffer緩沖,因此這個TimeClientHandler實例不可以再被多個Channel通道或ChannelPipeline共 享。否則這個內部的buffer緩沖將無法緩沖正確的數據內容。
2) 動態的buffer緩沖也是ChannelBuffer的一種實現,其擁有動態增加緩沖容量的能力。當你無法預估消息的數據長度時,動態的buffer緩沖是一種很有用的緩沖結構。
3) 首先,所有的數據將會被累積的緩沖至buf容器。
4) 之后,這個處理器將會檢查是否收到了足夠的數據然后再進行真實的業務邏輯處理,在這個例子中需要接收4字節數據。否則,Netty將重復調用messageReceived方法,直至4字節數據接收完成。
這里還有另一個地方需要進行修改。你是否還記得我們把TimeClientHandler實例添加到了這個ClientBootstrap實例的默 認ChannelPipeline管道里?這意味著同一個TimeClientHandler實例將被多個Channel通道共享,因此接受的數據也將受 到破壞。為了給每一個Channel通道創建一個新的TimeClientHandler實例,我們需要實現一個 ChannelPipelineFactory管道工廠:
Java代碼
package org.jboss.netty.example.time;
public class TimeClientPipelineFactory implements ChannelPipelineFactory {
public ChannelPipeline getPipeline() {
ChannelPipeline pipeline = Channels.pipeline();
pipeline.addLast("handler", new TimeClientHandler());
return pipeline;
}
}
現在,我們需要把TimeClient下面的代碼片段:
Java代碼
TimeClientHandler handler = new TimeClientHandler();
bootstrap.getPipeline().addLast("handler", handler);
替換為:
Java代碼
bootstrap.setPipelineFactory(new TimeClientPipelineFactory());
雖然這看上去有些復雜,并且由于在TimeClient內部我們只創建了一個連接(connection),因此我們在這里確實沒必要引入TimeClientPipelineFactory實例。
然而,當你的應用變得越來越復雜,你就總會需要實現自己的ChannelPipelineFactory,這個管道工廠將會令你的管道配置變得更加具有靈活性。
1.7.3. 第二種方案
雖然第二種方案解決了時間協議客戶端遇到的問題,但是這個修改后的處理器實現看上去卻不再那么簡潔。設想一種更為復雜的,由多個可變長度字段組成的協議。你的ChannelHandler實現將變得越來越難以維護。
正如你已注意到的,你可以為一個ChannelPipeline添加多個ChannelHandler,因此,為了減小應用的復雜性,你可以把這個臃腫的 ChannelHandler切分為多個獨立的模塊單元。例如,你可以把TimeClientHandler切分為兩個獨立的處理器:
TimeDecoder,解決數據分段的問題。
TimeClientHandler,原始版本的實現。
幸運的是,Netty提供了一個可擴展的類,這個類可以直接拿過來使用幫你完成TimeDecoder的開發:
Java代碼
package org.jboss.netty.example.time;
public class TimeDecoder extends FrameDecoder {
@Override
protected Object decode(
ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) {
if (buffer.readableBytes() < 4) {
return null;
}
return buffer.readBytes(4);
}
}
代碼說明
1) 這里不再需要使用ChannelPipelineCoverage的注解,因為FrameDecoder總是被注解為“one”。
2) 當接收到新的數據后,FrameDecoder會調用decode方法,同時傳入一個FrameDecoder內部持有的累積型buffer緩沖。
3) 如果decode返回null值,這意味著還沒有接收到足夠的數據。當有足夠數量的數據后FrameDecoder會再次調用decode方法。
4) 如果decode方法返回一個非空值,這意味著decode方法已經成功完成一條信息的解碼。FrameDecoder將丟棄這個內部的累計型緩沖。請注 意你不需要對多條消息進行解碼,FrameDecoder將保持對decode方法的調用,直到decode方法返回非空對象。
如果你是一個勇于嘗試的人,你或許應當使用ReplayingDecoder,ReplayingDecoder更加簡化了解碼的過程。為此你需要查看API手冊獲得更多的幫助信息。
Java代碼
package org.jboss.netty.example.time;
public class TimeDecoder extends ReplayingDecoder<VoidEnum> {
@Override
protected Object decode(
ChannelHandlerContext ctx, Channel channel,
ChannelBuffer buffer, VoidEnum state) {
return buffer.readBytes(4);
}
}
此外,Netty還為你提供了一些可以直接使用的decoder實現,這些decoder實現不僅可以讓你非常容易的實現大多數協議,并且還會幫你避免某些臃腫、難以維護的處理器實現。請參考下面的代碼包獲得更加詳細的實例:
org.jboss.netty.example.factorial for a binary protocol, and
org.jboss.netty.example.telnet for a text line-based protocol
1.8. 使用POJO代替ChannelBuffer
目前為止所有的實例程序都是使用ChannelBuffer做為協議消息的原始數據結構。在這一節,我們將改進時間協議服務的客戶/服務端實現,使用POJO 而不是ChannelBuffer做為協議消息的原始數據結構。
在你的ChannelHandler實現中使用POJO的優勢是很明顯的;從你的ChannelHandler實現中分離從ChannelBuffer獲 取數據的代碼,將有助于提高你的ChannelHandler實現的可維護性和可重用性。在時間協議服務的客戶/服務端代碼中,直接使用 ChannelBuffer讀取一個32位的整數并不是一個主要的問題。然而,你會發現,當你試圖實現一個真實的協議的時候,這種代碼上的分離是很有必要 的。
首先,讓我們定義一個稱之為UnixTime的新類型。
Java代碼
package org.jboss.netty.example.time;
import java.util.Date;
public class UnixTime {
private final int value;
public UnixTime(int value) {
this.value = value;
}
public int getValue() {
return value;
}
@Override
public String toString() {
return new Date(value * 1000L).toString();
}
}
現在讓我們重新修改TimeDecoder實現,讓其返回一個UnixTime,而不是一個ChannelBuffer。
Java代碼
@Override
protected Object decode(
ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer) {
if (buffer.readableBytes() < 4) {
return null;
}
return new UnixTime(buffer.readInt());
}
FrameDecoder和ReplayingDecoder允許你返回一個任何類型的對象。如果它們僅允許返回一個ChannelBuffer類 型的對象,我們將不得不插入另一個可以從ChannelBuffer對象轉換 為UnixTime對象的ChannelHandler實現。
有了這個修改后的decoder實現,這個TimeClientHandler便不會再依賴ChannelBuffer。
Java代碼
@Override
public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) {
UnixTime m = (UnixTime) e.getMessage();
System.out.println(m);
e.getChannel().close();
}
更加簡單優雅了,不是嗎?同樣的技巧也可以應用在服務端,讓我們現在更新TimeServerHandler的實現:
Java代碼
@Override
public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) {
UnixTime time = new UnixTime(System.currentTimeMillis() / 1000);
ChannelFuture f = e.getChannel().write(time);
f.addListener(ChannelFutureListener.CLOSE);
}
現在剩下的唯一需要修改的部分是這個ChannelHandler實現,這個ChannelHandler實現需要把一個UnixTime對象重新 轉換為一個ChannelBuffer。但這卻已是相當簡單了,因為當你對消息進行編碼的時候你不再需要處理數據包的拆分及組裝。
Java代碼
package org.jboss.netty.example.time;
import static org.jboss.netty.buffer.ChannelBuffers.*;
@ChannelPipelineCoverage("all")
public class TimeEncoder extends SimpleChannelHandler {
public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) {
UnixTime time = (UnixTime) e.getMessage();
ChannelBuffer buf = buffer(4);
buf.writeInt(time.getValue());
Channels.write(ctx, e.getFuture(), buf);
}
}
代碼說明
1) 因為這個encoder是無狀態的,所以其使用的ChannelPipelineCoverage注解值是“all”。實際上,大多數encoder實現都是無狀態的。
2) 一個encoder通過重寫writeRequested方法來實現對寫操作請求的攔截。不過請注意雖然這個writeRequested方法使用了和 messageReceived方法一樣的MessageEvent參數,但是它們卻分別對應了不同的解釋。一個ChannelEvent事件可以既是一 個上升流事件(upstream event)也可以是一個下降流事件(downstream event),這取決于事件流的方向。例如:一個MessageEvent消息事件可以作為一個上升流事件(upstream event)被messageReceived方法調用,也可以作為一個下降流事件(downstream event)被writeRequested方法調用。請參考API手冊獲得上升流事件(upstream event)和下降流事件(downstream event)的更多信息。
3) 一旦完成了POJO和ChannelBuffer轉換,你應當確保把這個新的buffer緩沖轉發至先前的 ChannelDownstreamHandler處理,這個下降通道的處理器由某個ChannelPipeline管理。Channels提供了多個可 以創建和發送ChannelEvent事件的幫助方法。在這個例子中,Channels.write(...)方法創建了一個新的 MessageEvent事件,并把這個事件發送給了先前的處于某個ChannelPipeline內的 ChannelDownstreamHandler處理器。
另外,一個很不錯的方法是使用靜態的方式導入Channels類:
import static org.jboss.netty.channel.Channels.*;
...
ChannelPipeline pipeline = pipeline();
write(ctx, e.getFuture(), buf);
fireChannelDisconnected(ctx);
最后的任務是把這個TimeEncoder插入服務端的ChannelPipeline,這是一個很簡單的步驟。
1.9. 關閉你的應用
如果你運行了TimeClient,你肯定可以注意到,這個應用并沒有自動退出而只是在那里保持著無意義的運行。跟蹤堆棧記錄你可以發現,這里有一些運行 狀態的I/O線程。為了關閉這些I/O線程并讓應用優雅的退出,你需要釋放這些由ChannelFactory分配的資源。
一個典型的網絡應用的關閉過程由以下三步組成:
關閉負責接收所有請求的server socket。
關閉所有客戶端socket或服務端為響應某個請求而創建的socket。
釋放ChannelFactory使用的所有資源。
為了讓TimeClient執行這三步,你需要在TimeClient.main()方法內關閉唯一的客戶連接以及ChannelFactory使用的所有資源,這樣做便可以優雅的關閉這個應用。
Java代碼
package org.jboss.netty.example.time;
public class TimeClient {
public static void main(String[] args) throws Exception {
...
ChannelFactory factory = ...;
ClientBootstrap bootstrap = ...;
...
ChannelFuture future = bootstrap.connect(...);
future.awaitUninterruptible();
if (!future.isSuccess()) {
future.getCause().printStackTrace();
}
future.getChannel().getCloseFuture().awaitUninterruptibly();
factory.releaseExternalResources();
}
}
代碼說明
1) ClientBootstrap對象的connect方法返回一個ChannelFuture對象,這個ChannelFuture對象將告知這個連接操 作的成功或失敗狀態。同時這個ChannelFuture對象也保存了一個代表這個連接操作的Channel對象引用。
2) 阻塞式的等待,直到ChannelFuture對象返回這個連接操作的成功或失敗狀態。
3) 如果連接失敗,我們將打印連接失敗的原因。如果連接操作沒有成功或者被取消,ChannelFuture對象的getCause()方法將返回連接失敗的原因。
4) 現在,連接操作結束,我們需要等待并且一直到這個Channel通道返回的closeFuture關閉這個連接。每一個Channel都可獲得自己的closeFuture對象,因此我們可以收到通知并在這個關閉時間點執行某種操作。
并且即使這個連接操作失敗,這個closeFuture仍舊會收到通知,因為這個代表連接的 Channel對象將會在連接操作失敗后自動關閉。
5) 在這個時間點,所有的連接已被關閉。剩下的唯一工作是釋放ChannelFactory通道工廠使用的資源。這一步僅需要調用 releaseExternalResources()方法即可。包括NIO Secector和線程池在內的所有資源將被自動的關閉和終止。
關閉一個客戶端應用是很簡單的,但又該如何關閉一個服務端應用呢?你需要釋放其綁定的端口并關閉所有接受和打開的連接。為了做到這一點,你需要使用一種數據結構記錄所有的活動連接,但這卻并不是一件容易的事。幸運的是,這里有一種解決方案,ChannelGroup。
ChannelGroup是Java 集合 API的一個特有擴展,ChannelGroup內部持有所有打開狀態的Channel通道。如果一個Channel通道對象被加入到 ChannelGroup,如果這個Channel通道被關閉,ChannelGroup將自動移除這個關閉的Channel通道對象。此外,你還可以對 一個ChannelGroup對象內部的所有Channel通道對象執行相同的操作。例如,當你關閉服務端應用時你可以關閉一個ChannelGroup 內部的所有Channel通道對象。
為了記錄所有打開的socket,你需要修改你的TimeServerHandler實現,將一個打開的Channel通道加入全局的ChannelGroup對象,TimeServer.allChannels:
Java代碼
@Override
public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) {
TimeServer.allChannels.add(e.getChannel());
}
代碼說明
是的,ChannelGroup是線程安全的。
現在,所有活動的Channel通道將被自動的維護,關閉一個服務端應用有如關閉一個客戶端應用一樣簡單。
Java代碼
package org.jboss.netty.example.time;
public class TimeServer {
static final ChannelGroup allChannels = new DefaultChannelGroup("time-server" );
public static void main(String[] args) throws Exception {
...
ChannelFactory factory = ...;
ServerBootstrap bootstrap = ...;
...
Channel channel = bootstrap.bind(...);
allChannels.add(channel);
waitForShutdownCommand();
ChannelGroupFuture future = allChannels.close();
future.awaitUninterruptibly();
factory.releaseExternalResources();
}
}
代碼說明
1) DefaultChannelGroup需要一個組名作為其構造器參數。這個組名僅是區分每個ChannelGroup的一個標示。
2) ServerBootstrap對象的bind方法返回了一個綁定了本地地址的服務端Channel通道對象。調用這個Channel通道的close()方法將釋放這個Channel通道綁定的本地地址。
3) 不管這個Channel對象屬于服務端,客戶端,還是為響應某一個請求創建,任何一種類型的Channel對象都會被加入ChannelGroup。因此,你盡可在關閉服務時關閉所有的Channel對象。
4) waitForShutdownCommand()是一個想象中等待關閉信號的方法。你可以在這里等待某個客戶端的關閉信號或者JVM的關閉回調命令。
5) 你可以對ChannelGroup管理的所有Channel對象執行相同的操作。在這個例子里,我們將關閉所有的通道,這意味著綁定在服務端特定地址的 Channel通道將解除綁定,所有已建立的連接也將異步關閉。為了獲得成功關閉所有連接的通知,close()方法將返回一個 ChannelGroupFuture對象,這是一個類似ChannelFuture的對象。
1.10. 總述
在這一章節,我們快速瀏覽并示范了如何使用Netty開發網絡應用。下一章節將涉及更多的問題。同時請記住,為了幫助你以及能夠讓Netty基于你的回饋得到持續的改進和提高,Netty社區 將永遠歡迎你的問題及建議。
第二章. 架構總覽
在這個章節,我們將闡述Netty提供的核心功能以及在此基礎之上如何構建一個完備的網絡應用。
2.1. 豐富的緩沖實現
Netty使用自建的buffer API,而不是使用NIO的ByteBuffer來代表一個連續的字節序列。與ByteBuffer相比這種方式擁有明顯的優勢。Netty使用新的 buffer類型ChannelBuffer,ChannelBuffer被設計為一個可從底層解決ByteBuffer問題,并可滿足日常網絡應用開發 需要的緩沖類型。這些很酷的特性包括:
如果需要,允許使用自定義的緩沖類型。
復合緩沖類型中內置的透明的零拷貝實現。
開箱即用的動態緩沖類型,具有像StringBuffer一樣的動態緩沖能力。
不再需要調用的flip()方法。
正常情況下具有比ByteBuffer更快的響應速度。
更多信息請參考:org.jboss.netty.buffer package description
2.2. 統一的異步 I/O API
傳統的Java I/O API在應對不同的傳輸協議時需要使用不同的類型和方法。例如:java.net.Socket 和 java.net.DatagramSocket它們并不具有相同的超類型,因此,這就需要使用不同的調用方式執行socket操作。
這種模式上的不匹配使得在更換一個網絡應用的傳輸協議時變得繁雜和困難。由于(Java I/O API)缺乏協議間的移植性,當你試圖在不修改網絡傳輸層的前提下增加多種協議的支持,這時便會產生問題。并且理論上講,多種應用層協議可運行在多種傳輸 層協議之上例如TCP/IP,UDP/IP,SCTP和串口通信。
讓這種情況變得更糟的是,Java新的I/O(NIO)API與原有的阻塞式的I/O(OIO)API并不兼容,NIO.2(AIO)也是如此。由于所有的API無論是在其設計上還是性能上的特性都與彼此不同,在進入開發階段,你常常會被迫的選擇一種你需要的API。
例如,在用戶數較小的時候你可能會選擇使用傳統的OIO(Old I/O) API,畢竟與NIO相比使用OIO將更加容易一些。然而,當你的業務呈指數增長并且服務器需要同時處理成千上萬的客戶連接時你便會遇到問題。這種情況下 你可能會嘗試使用NIO,但是復雜的NIO Selector編程接口又會耗費你大量時間并最終會阻礙你的快速開發。
Netty有一個叫做Channel的統一的異步I/O編程接口,這個編程接口抽象了所有點對點的通信操作。也就是說,如果你的應用是基于Netty的某 一種傳輸實現,那么同樣的,你的應用也可以運行在Netty的另一種傳輸實現上。Netty提供了幾種擁有相同編程接口的基本傳輸實現:
NIO-based TCP/IP transport (See org.jboss.netty.channel.socket.nio),
OIO-based TCP/IP transport (See org.jboss.netty.channel.socket.oio),
OIO-based UDP/IP transport, and
Local transport (See org.jboss.netty.channel.local).
切換不同的傳輸實現通常只需對代碼進行幾行的修改調整,例如選擇一個不同的ChannelFactory實現。
此外,你甚至可以利用新的傳輸實現沒有寫入的優勢,只需替換一些構造器的調用方法即可,例如串口通信。而且由于核心API具有高度的可擴展性,你還可以完成自己的傳輸實現。
2.3. 基于攔截鏈模式的事件模型
一個定義良好并具有擴展能力的事件模型是事件驅動開發的必要條件。Netty具有定義良好的I/O事件模型。由于嚴格的層次結構區分了不同的事件類型,因 此Netty也允許你在不破壞現有代碼的情況下實現自己的事件類型。這是與其他框架相比另一個不同的地方。很多NIO框架沒有或者僅有有限的事件模型概 念;在你試圖添加一個新的事件類型的時候常常需要修改已有的代碼,或者根本就不允許你進行這種擴展。
在一個ChannelPipeline內部一個ChannelEvent被一組ChannelHandler處理。這個管道是攔截過濾器 模式的一種高級形式的實現,因此對于一個事件如何被處理以及管道內部處理器間的交互過程,你都將擁有絕對的控制力。例如,你可以定義一個從socket讀取到數據后的操作:
Java代碼
public class MyReadHandler implements SimpleChannelHandler {
public void messageReceived(ChannelHandlerContext ctx, MessageEvent evt) {
Object message = evt.getMessage();
// Do something with the received message.
...
// And forward the event to the next handler.
ctx.sendUpstream(evt);
}
}
同時你也可以定義一種操作響應其他處理器的寫操作請求:
Java代碼
public class MyWriteHandler implements SimpleChannelHandler {
public void writeRequested(ChannelHandlerContext ctx, MessageEvent evt) {
Object message = evt.getMessage();
// Do something with the message to be written.
...
// And forward the event to the next handler.
ctx.sendDownstream(evt);
}
}
有關事件模型的更多信息,請參考API文檔ChannelEvent和ChannelPipeline部分。
2.4. 適用快速開發的高級組件
上述所提及的核心組件已經足夠實現各種類型的網絡應用,除此之外,Netty也提供了一系列的高級組件來加速你的開發過程。
2.4.1. Codec框架
就像“1.8. 使用POJO代替ChannelBuffer”一節所展示的那樣,從業務邏輯代碼中分離協議處理部分總是一個很不錯的想法。然而如果一切從零開始便會遭遇 到實現上的復雜性。你不得不處理分段的消息。一些協議是多層的(例如構建在其他低層協議之上的協議)。一些協議過于復雜以致難以在一臺主機(single state machine)上實現。
因此,一個好的網絡應用框架應該提供一種可擴展,可重用,可單元測試并且是多層的codec框架,為用戶提供易維護的codec代碼。
Netty提供了一組構建在其核心模塊之上的codec實現,這些簡單的或者高級的codec實現幫你解決了大部分在你進行協議處理開發過程會遇到的問題,無論這些協議是簡單的還是復雜的,二進制的或是簡單文本的。
2.4.2. SSL / TLS 支持
不同于傳統阻塞式的I/O實現,在NIO模式下支持SSL功能是一個艱難的工作。你不能只是簡單的包裝一下流數據并進行加密或解密工作,你不得不借助于 javax.net.ssl.SSLEngine,SSLEngine是一個有狀態的實現,其復雜性不亞于SSL自身。你必須管理所有可能的狀態,例如密 碼套件,密鑰協商(或重新協商),證書交換以及認證等。此外,與通常期望情況相反的是SSLEngine甚至不是一個絕對的線程安全實現。
在Netty內部,SslHandler封裝了所有艱難的細節以及使用SSLEngine可能帶來的陷阱。你所做的僅是配置并將該SslHandler插入到你的ChannelPipeline中。同樣Netty也允許你實現像StartTlS 那樣所擁有的高級特性,這很容易。
2.4.3. HTTP實現
HTTP無疑是互聯網上最受歡迎的協議,并且已經有了一些例如Servlet容器這樣的HTTP實現。因此,為什么Netty還要在其核心模塊之上構建一套HTTP實現?
與現有的HTTP實現相比Netty的HTTP實現是相當與眾不同的。在HTTP消息的低層交互過程中你將擁有絕對的控制力。這是因為Netty的 HTTP實現只是一些HTTP codec和HTTP消息類的簡單組合,這里不存在任何限制——例如那種被迫選擇的線程模型。你可以隨心所欲的編寫那種可以完全按照你期望的工作方式工作 的客戶端或服務器端代碼。這包括線程模型,連接生命期,快編碼,以及所有HTTP協議允許你做的,所有的一切,你都將擁有絕對的控制力。
由于這種高度可定制化的特性,你可以開發一個非常高效的HTTP服務器,例如:
要求持久化鏈接以及服務器端推送技術的聊天服務(e.g. Comet )
需要保持鏈接直至整個文件下載完成的媒體流服務(e.g. 2小時長的電影)
需要上傳大文件并且沒有內存壓力的文件服務(e.g. 上傳1GB文件的請求)
支持大規模mash-up應用以及數以萬計連接的第三方web services異步處理平臺
2.4.4. Google Protocol Buffer 整合
Google Protocol Buffers 是快速實現一個高效的二進制協議的理想方案。通過使用ProtobufEncoder和ProtobufDecoder,你可以把Google Protocol Buffers 編譯器 (protoc)生成的消息類放入到Netty的codec實現中。請參考“LocalTime ”實例,這個例子也同時顯示出開發一個由簡單協議定義 的客戶及服務端是多么的容易。
2.5. 總述
在這一章節,我們從功能特性的角度回顧了Netty的整體架構。Netty有一個簡單卻不失強大的架構。這個架構由三部分組成——緩沖(buffer), 通道(channel),事件模型(event model)——所有的高級特性都構建在這三個核心組件之上。一旦你理解了它們之間的工作原理,你便不難理解在本章簡要提及的更多高級特性。
你可能對Netty的整體架構以及每一部分的工作原理仍舊存有疑問。如果是這樣,最好的方式是告訴我們 應該如何改進這份指南