13.2.3 TCP編程
按照前面的介紹,網絡通訊的方式有TCP和UDP兩種,其中TCP方式的網絡通訊是指在通訊的過程中保持連接,有點類似於打電話,只需要撥打一次號碼(建立一次網絡連接),就可以多次通話(多次傳輸數據)。這樣方式在實際的網絡編程中,由於傳輸可靠,類似於打電話,如果甲給乙打電話,乙說沒有聽清楚讓甲重復一遍,直到乙聽清楚為止,實際的網絡傳輸也是這樣,如果發送的一方發送的數據接收方覺得有問題,則網絡底層會自動要求發送方重發,直到接收方收到為止。
在Java語言中,對於TCP方式的網絡編程提供了良好的支持,在實際實現時,以java.net.Socket類代表客戶端連接,以java.net.ServerSocket類代表服務器端連接。在進行網絡編程時,底層網絡通訊的細節已經實現了比較高的封裝,所以在程序員實際編程時,只需要指定IP地址和端口號碼就可以建立連接了。正是由於這種高度的封裝,一方面簡化了Java語言網絡編程的難度,另外也使得使用Java語言進行網絡編程時無法深入到網絡的底層,所以使用Java語言進行網絡底層系統編程很困難,具體點說,Java語言無法實現底層的網絡嗅探以及獲得IP包結構等信息。但是由於Java語言的網絡編程比較簡單,所以還是獲得了廣泛的使用。
在使用TCP方式進行網絡編程時,需要按照前面介紹的網絡編程的步驟進行,下面分別介紹一下在Java語言中客戶端和服務器端的實現步驟。
在客戶端網絡編程中,首先需要建立連接,在Java API中以java.net.Socket類的對象代表網絡連接,所以建立客戶端網絡連接,也就是創建Socket類型的對象,該對象代表網絡連接,示例如下:
Socket socket1 = new Socket(“192.168.1.103”,10000);
Socket socket2 = new Socket(“www.sohu.com”,80);
上面的代碼中,socket1實現的是連接到IP地址是192.168.1.103的計算機的10000號端口,而socket2實現的是連接到域名是www.sohu.com的計算機的80號端口,至於底層網絡如何實現建立連接,對於程序員來說是完全透明的。如果建立連接時,本機網絡不通,或服務器端程序未開啟,則會拋出異常。
連接一旦建立,則完成了客戶端編程的第一步,緊接著的步驟就是按照“請求-響應”模型進行網絡數據交換,在Java語言中,數據傳輸功能由Java IO實現,也就是說只需要從連接中獲得輸入流和輸出流即可,然後將需要發送的數據寫入連接對象的輸出流中,在發送完成以後從輸入流中讀取數據即可。示例代碼如下:
OutputStream os = socket1.getOutputStream(); //獲得輸出流
InputStream is = socket1.getInputStream(); //獲得輸入流
上面的代碼中,分別從socket1這個連接對象獲得了輸出流和輸入流對象,在整個網絡編程中,後續的數據交換就變成了IO操作,也就是遵循“請求-響應”模型的規定,先向輸出流中寫入數據,這些數據會被系統發送出去,然後在從輸入流中讀取服務器端的反饋信息,這樣就完成了一次數據交換過程,當然這個數據交換過程可以多次進行。
這裡獲得的只是最基本的輸出流和輸入流對象,還可以根據前面學習到的IO知識,使用流的嵌套將這些獲得到的基本流對象轉換成需要的裝飾流對象,從而方便數據的操作。
最後當數據交換完成以後,關閉網絡連接,釋放網絡連接占用的系統端口和內存等資源,完成網絡操作,示例代碼如下:
socket1.close();
這就是最基本的網絡編程功能介紹。下面是一個簡單的網絡客戶端程序示例,該程序的作用是向服務器端發送一個字符串“Hello”,並將服務器端的反饋顯示到控制台,數據交換只進行一次,當數據交換進行完成以後關閉網絡連接,程序結束。實現的代碼如下:
package tcp;
import java.io.*;
import java.net.*;
/**
* 簡單的Socket客戶端
* 功能為:發送字符串“Hello”到服務器端,並打印出服務器端的反饋
*/
public class SimpleSocketClient {
public static void main(String[] args) {
Socket socket = null;
InputStream is = null;
OutputStream os = null;
//服務器端IP地址
String serverIP = "127.0.0.1";
//服務器端端口號
int port = 10000;
//發送內容
String data = "Hello";
try {
//建立連接
socket = new Socket(serverIP,port);
//發送數據
os = socket.getOutputStream();
os.write(data.getBytes());
//接收數據
is = socket.getInputStream();
byte[] b = new byte[1024];
int n = is.read(b);
//輸出反饋數據
System.out.println("服務器反饋:" + new String(b,0,n));
} catch (Exception e) {
e.printStackTrace(); //打印異常信息
}finally{
try {
//關閉流和連接
is.close();
os.close();
socket.close();
} catch (Exception e2) {}
}
}
}
在該示例代碼中建立了一個連接到IP地址為127.0.0.1,端口號碼為10000的TCP類型的網絡連接,然後獲得連接的輸出流對象,將需要發送的字符串“Hello”轉換為byte數組寫入到輸出流中,由系統自動完成將輸出流中的數據發送出去,如果需要強制發送,可以調用輸出流對象中的flush方法實現。在數據發送出去以後,從連接對象的輸入流中讀取服務器端的反饋信息,讀取時可以使用IO中的各種讀取方法進行讀取,這裡使用最簡單的方法進行讀取,從輸入流中讀取到的內容就是服務器端的反饋,並將讀取到的內容在客戶端的控制台進行輸出,最後依次關閉打開的流對象和網絡連接對象。
這是一個簡單的功能示例,在該示例中演示了TCP類型的網絡客戶端基本方法的使用,該代碼只起演示目的,還無法達到實用的級別。
如果需要在控制台下面編譯和運行該代碼,需要首先在控制台下切換到源代碼所在的目錄,然後依次輸入編譯和運行命令:
javac -d . SimpleSocketClient.java
java tcp.SimpleSocketClient
和下面將要介紹的SimpleSocketServer服務器端組合運行時,程序的輸出結果為:
服務器反饋:Hello
介紹完一個簡單的客戶端編程的示例,下面接著介紹一下TCP類型的服務器端的編寫。首先需要說明的是,客戶端的步驟和服務器端的編寫步驟不同,所以在學習服務器端編程時注意不要和客戶端混淆起來。
在服務器端程序編程中,由於服務器端實現的是被動等待連接,所以服務器端編程的第一個步驟是監聽端口,也就是監聽是否有客戶端連接到達。實現服務器端監聽的代碼為:
ServerSocket ss = new ServerSocket(10000);
該代碼實現的功能是監聽當前計算機的10000號端口,如果在執行該代碼時,10000號端口已經被別的程序占用,那麼將拋出異常。否則將實現監聽。
服務器端編程的第二個步驟是獲得連接。該步驟的作用是當有客戶端連接到達時,建立一個和客戶端連接對應的Socket連接對象,從而釋放客戶端連接對於服務器端端口的占用。實現功能就像公司的前台一樣,當一個客戶到達公司時,會告訴前台我找某某某,然後前台就通知某某某,然後就可以繼續接待其它客戶了。通過獲得連接,使得客戶端的連接在服務器端獲得了保持,另外使得服務器端的端口釋放出來,可以繼續等待其它的客戶端連接。實現獲得連接的代碼是:
Socket socket = ss.accept();
該代碼實現的功能是獲得當前連接到服務器端的客戶端連接。需要說明的是accept和前面IO部分介紹的read方法一樣,都是一個阻塞方法,也就是當無連接時,該方法將阻塞程序的執行,直到連接到達時才執行該行代碼。另外獲得到的連接將使用服務器端其它未使用的端口號和客戶端進行連接,使得服務器端端口不會被一直占用。由於每個獲得的連接都會占用服務器端的一個端口號,這樣使用TCP類型的網絡進行編程時,同時可以支持的最大連接數量受到端口數量的限制,最大為65535個。
連接獲得以後,後續的編程就和客戶端的網絡編程類似了,這裡獲得的Socket類型的連接就和客戶端的網絡連接一樣了,只是服務器端需要首先讀取發送過來的數據,然後進行邏輯處理以後再發送給客戶端,也就是交換數據的順序和客戶端交換數據的步驟剛好相反。這部分的內容和客戶端很類似,所以就不重復了,如果還不熟悉,可以參看下面的示例代碼。
最後,在服務器端通信完成以後,關閉服務器端連接。實現的代碼為:
ss.close();
這就是基本的TCP類型的服務器端編程步驟。下面以一個簡單的echo服務實現為例子,介紹綜合使用示例。echo的意思就是“回聲”,echo服務器端實現的功能就是將客戶端發送的內容再原封不動的反饋給客戶端。實現的代碼如下:
package tcp;
import java.io.*;
import java.net.*;
/**
* echo服務器
* 功能:將客戶端發送的內容反饋給客戶端
*/
public class SimpleSocketServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
Socket socket = null;
OutputStream os = null;
InputStream is = null;
//監聽端口號
int port = 10000;
try {
//建立連接
serverSocket = new ServerSocket(port);
//獲得連接
socket = serverSocket.accept();
//接收客戶端發送內容
is = socket.getInputStream();
byte[] b = new byte[1024];
int n = is.read(b);
//輸出
System.out.println("客戶端發送內容為:" + new String(b,0,n));
//向客戶端發送反饋內容
os = socket.getOutputStream();
os.write(b, 0, n);
} catch (Exception e) {
e.printStackTrace();
}finally{
try{
//關閉流和連接
os.close();
is.close();
socket.close();
serverSocket.close();
}catch(Exception e){}
}
}
}
在該示例代碼中建立了一個監聽當前計算機10000號端口的服務器端Socket連接,然後獲得客戶端發送過來的連接,如果有連接到達時,讀取連接中發送過來的內容,並將發送的內容在控制台進行輸出,輸出完成以後將客戶端發送的內容再反饋給客戶端。最後關閉流和連接對象,結束程序。
在控制台下面編譯和運行該程序的命令和客戶端部分的類似。
這樣,就以一個很簡單的示例演示了TCP類型的網絡編程在Java語言中的基本實現,這個示例只是演示了網絡編程的基本步驟以及各個功能方法的基本使用,只是為網絡編程打下了一個基礎,下面將就幾個問題來深入介紹網絡編程深層次的一些知識。