首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java 套接字程式設計(上):TCP 流套接字基礎

     編輯:Java 相關知識

用 Java 開發網路軟體非常方便強大,Java 此力量源於獨有強大網路 API,一系列類與介面位於 java.net 與 javax.net 套件。本文介紹套接字概念,以實例說明如何使用 Network API 操縱套接字,完成後可編寫網路低層通訊軟體。

什麼是套接字?

Network API 典型用於基於 TCP/IP 網路 Java 程式與其他程式通訊,依賴 Socket 通訊。Socket 可視為兩程式通訊連接端點,一程式將資訊寫入 Socket,Socket 將資訊發送給另一 Socket,使資訊傳送到其他程式。如圖 1。

分析圖 1:Host A 程式 A 將資訊寫入 Socket,Socket 內容被 Host A 網路管理軟體訪問,透過網路介面卡發送到 Host B,Host B 網路卡接收後傳給網路管理軟體,軟體將資訊保存 Host B Socket,程式 B 才能在 Socket 閱讀資訊。

假設網路加入第三主機 Host C,Host A 怎知資訊正確傳送到 Host B 而非 Host C?基於 TCP/IP 網路每主機賦予唯一 IP 位址,IP 位址為 32 位無號整數,通常以小數點分隔,如 198.163.227.6。IP 位址由四部分組成,每部分範圍 0-255 表示 8 位位址。

值得注意的是 IP 位址皆 32 位,為 IPv4 規定。因 IPv4 位址近耗盡,IPv6 位址正逐漸取代,IPv6 為 128 位無號整數。

假設第二程式加入 Host B,Host A 傳來資訊如何正確傳給程式 B 而非新程式?因每基於 TCP/IP 網路通訊程式賦予唯一埠與埠號。埠為資訊緩衝區,保留 Socket 輸入/輸出資訊;埠號為 16 位無號整數,範圍 0-65535,區別主機上每程式(埠號如房屋房間號)。256 以下埠號保留給標準應用,如 POP3 埠號 110。每套接字組合 IP 位址、埠、埠號,形成整體區別每套接字。下面談兩種套接字:流套接字與資料報資料套接字。

流套接字

無論何時,兩網路應用程式間發送接收資訊需建立可靠連接。流套接字依賴 TCP 協定保證資訊正確到達。實際上 IP 包可能在網路遺失或傳送過程發生錯誤,任何情況發生,作為接收方 TCP 將聯繫發送方 TCP 重新發送 IP 包。這就是所謂兩流套接字間建立可靠連接。

流套接字在 C/S 程式扮演必要角色。客戶機程式(需訪問某些服務網路應用程式)建立扮演服務器程式主機 IP 位址與服務器程式(為客戶端應用提供服務網路應用程式)埠號的流套接字物件。

客戶端流套接字初始化代碼將 IP 位址與埠號傳遞給客戶端主機網路管理軟體,管理軟體將 IP 位址與埠號透過 NIC 傳遞給服務器主機;服務器主機讀到經 NIC 傳遞資料,查看服務器程式是否處於監聽狀態,此監聽仍透過套接字與埠進行;若服務器程式處於監聽狀態,服務器網路管理軟體向客戶機網路管理軟體發出積極回應信號,接收回應後,客戶端流套接字初始化代碼給客戶程式建立埠號,將埠號傳遞給服務器程式套接字(服務器程式將用此埠號識別傳來資訊是否屬於客戶程式)同時完成流套接字初始化。

若服務器程式未處於監聽狀態,服務器網路管理軟體將給客戶端傳遞消極信號,收到消極信號後,客戶程式流套接字初始化代碼拋出異常物件,不建立通訊連接,不創建流套接字物件。這情形像打電話:有人時通訊建立,否則電話掛起。

此工作包含三相關類:InetAddress、Socket、ServerSocket。InetAddress 描述 32 位或 128 位 IP 位址,Socket 代表客戶程式流套接字,ServerSocket 代表服務程式流套接字,三類均位於 java.net。

InetAddress 類

InetAddress 在網路 API 套接字程式設計扮演重要角色。參數傳遞給流套接字類與資料報套接字類建構器或非建構器方法。InetAddress 描述 32 位或 64 位 IP 位址,主要依賴 Inet4Address、Inet6Address 兩支援類,三類為繼承關係,InetAddress 為父類。

因 InetAddress 僅一建構函數且不可傳參數,不能直接創建物件,如 InetAddress ia = new InetAddress(); 錯誤。但可透過 5 個工廠方法創建 InetAddress 物件或陣列:

  • getAllByName(String host):回傳 InetAddress 物件參考陣列,每物件包含對應主機名單獨 IP 位址。若指定主機無 IP 位址拋 UnknownHostException。
  • getByAddress(byte[] addr):回傳含 IPv4(4 位元組陣列)或 IPv6(16 位元組陣列)位址物件。若陣列非 4 或 16 位元組拋異常。
  • getByAddress(String host, byte[] addr):回傳含由 host 與 4 或 16 位元組陣列指定 IP 位址物件。
  • getByName(String host):回傳含與 host 指定主機對應 IP 位址物件。若主機無 IP 位址拋異常。
  • getLocalHost():回傳含本機 IP 位址物件。為避免混淆,稱客戶程式主機為客戶主機,服務器程式主機為服務器主機。

上述方法均提到回傳一或多 InetAddress 物件參考,實際每方法回傳一或多 Inet4Address/Inet6Address 物件參考。調用者不需知道引用子類型,可用回傳參考調用 InetAddress 非靜態方法,含子類型多態確保重載方法被調用。

InetAddress 與子類型物件處理主機名到 IPv4/IPv6 位址轉換,需使用域名系統。下列代碼示範調用 getByName(String host) 取得 InetAddress 子類物件參考:

InetAddress ia = InetAddress.getByName("www.bianceng.cn");

取得參考後可調用 InetAddress 方法取得 IP 位址資訊:getCanonicalHostName() 從域名服務取標準主機名、getHostAddress() 取 IP 位址、getHostName() 取主機名、isLoopbackAddress() 判斷是否迴環位址。

清單 1 為示範代碼 InetAddressDemo:

// InetAddressDemo.java
import java.net.*;
class InetAddressDemo { public static void main(String[] args) throws UnknownHostException {
String host = "localhost";
if (args.length == 1) host = args[0];
InetAddress ia = InetAddress.getByName(host);
System.out.println("Canonical Host Name = " + ia.getCanonicalHostName());
System.out.println("Host Address = " + ia.getHostAddress());
System.out.println("Host Name = " + ia.getHostName());
System.out.println("Is Loopback Address = " + ia.isLoopbackAddress());
} }

無命令列參數時輸出類似:

Canonical Host Name = localhost
Host Address = 127.0.0.1
Host Name = localhost
Is Loopback Address = true

InetAddressDemo 給指定主機名作命令列參數選擇,未指定用 localhost。透過 getByName 取得子類物件參考,輸出標準主機名、主機位址、主機名、是否迴環位址。

Socket 類

客戶程式需與服務器通訊時,在客戶機建立 Socket 物件。Socket 有幾建構函數,兩常用:Socket(InetAddress addr, int port)、Socket(String host, int port)。兩者都建立基於 Socket 連接服務器流套接字的流套接字。第一個透過 addr 參數取得服務器主機 IP 位址,第二個 host 參數被分配到 InetAddress 物件,若無 IP 位址匹配拋 UnknownHostException。兩函數都透過 port 取得服務器埠號。假設已建立連接,網路 API 將在客戶端基於 Socket 流套接字中綁定客戶程式 IP 位址與任意埠號,否則拋 IOException。

若建立 Socket 物件,可調用 getInputStream() 從服務程式取得輸入流讀傳來資訊,或 getOutputStream() 取得輸出流發送訊息。讀寫完成後調用 close() 關閉流與流套接字。下列代碼建立服務程式主機位址 198.163.227.6、埠號 13 的 Socket,讀取輸入流再關閉:

Socket s = new Socket("198.163.227.6", 13);
InputStream is = s.getInputStream();
is.close();
s.close();

接下來示範流套接字客戶程式,建立 Socket 訪問指定主機埠 10000 服務程式,成功則發送一系列命令並列印回應。清單 2 為 SSClient 原始碼:

// SSClient.java
import java.io.*; import java.net.*;
class SSClient { public static void main(String[] args) {
String host = "localhost";
if (args.length == 1) host = args[0];
BufferedReader br = null; PrintWriter pw = null; Socket s = null;
try {
s = new Socket(host, 10000);
InputStreamReader isr = new InputStreamReader(s.getInputStream());
br = new BufferedReader(isr);
pw = new PrintWriter(s.getOutputStream(), true);
pw.println("DATE");
System.out.println(br.readLine());
pw.println("PAUSE");
pw.println("DOW");
System.out.println(br.readLine());
pw.println("DOM");
System.out.println(br.readLine());
pw.println("DOY");
System.out.println(br.readLine());
} catch (IOException e) { System.out.println(e.toString()); }
finally { try { if (br != null) br.close(); if (pw != null) pw.close(); if (s != null) s.close(); } catch (IOException e) {} }
} }

執行結果:

Tue Jan 29 18:11:51 CST 2002
TUESDAY
29
29

SSClient 建立 Socket 聯繫主機埠 10000 服務程式,主機 IP 由 host 變數確定。取得 Socket 輸入輸出流,圍繞 BufferedReader、PrintWriter 對字串讀寫變容易。SSClient 向服務程式發送各種 date/time 命令獲取回應,每回應列印。最後回應列印後執行 finally 關閉 BufferedReader、PrintWriter 再關閉 Socket。

編譯後輸入 java SSClient 執行。若合適程式在不同主機,採用主機名/IP 參數輸入,如 java SSClient www.sina.com.cn。

技巧

Socket 類含許多有用方法。getLocalAddress() 回傳含客戶程式 IP 位址 InetAddress 子類物件參考;getLocalPort() 回傳客戶程式埠號;getInetAddress() 回傳含服務器 IP 位址 InetAddress 子類參考;getPort() 回傳服務程式埠號。

ServerSocket 類

因 SSClient 用流套接字,服務程式也用流套接字。建立 ServerSocket 物件,ServerSocket 有幾建構函數,最簡單 ServerSocket(int port)。使用時 port 傳遞埠號,此埠為服務器監聽連接請求埠。若出錯拋 IOException,否則創建物件並準備接收請求。

服務程式進入無限迴圈,從調用 accept() 開始,調用後導致調用執行緒阻塞直到連接建立。建立連接後 accept() 回傳最近創建 Socket 物件,綁定客戶程式 IP 位址或埠號。

因存在單服務程式與多客戶程式通訊可能,服務程式回應客戶程式不應花很多時間,否則客戶程式得服務前可能花很多時間等待。但服務程式與客戶程式會話可能很長(似電話),為加快對客戶程式連接請求回應,典型方法是服務器主機運行後台執行緒處理通訊。

為示範概念並完成 SSClient,建立 SSServer 程式,創建 ServerSocket 監聽埠 10000 連接請求,成功則等待連接輸入、啟動執行緒處理連接、回應客戶命令。清單 3 為代碼:

// SSServer.java
import java.io.*; import java.net.*; import java.util.*;
class SSServer { public static void main(String[] args) throws IOException {
System.out.println("Server starting...\n");
ServerSocket server = new ServerSocket(10000);
while (true) {
Socket s = server.accept();
System.out.println("Accepting Connection...\n");
new ServerThread(s).start();
} } }
class ServerThread extends Thread {
private Socket s;
ServerThread(Socket s) { this.s = s; }
public void run() {
BufferedReader br = null; PrintWriter pw = null;
try {
InputStreamReader isr = new InputStreamReader(s.getInputStream());
br = new BufferedReader(isr);
pw = new PrintWriter(s.getOutputStream(), true);
Calendar c = Calendar.getInstance();
do {
String cmd = br.readLine();
if (cmd == null) break;
cmd = cmd.toUpperCase();
if (cmd.startsWith("BYE")) break;
if (cmd.startsWith("DATE") || cmd.startsWith("TIME")) pw.println(c.getTime().toString());
if (cmd.startsWith("DOM")) pw.println("" + c.get(Calendar.DAY_OF_MONTH));
if (cmd.startsWith("DOW"))
switch (c.get(Calendar.DAY_OF_WEEK)) {
case Calendar.SUNDAY: pw.println("SUNDAY"); break;
case Calendar.MONDAY: pw.println("MONDAY"); break;
case Calendar.TUESDAY: pw.println("TUESDAY"); break;
case Calendar.WEDNESDAY: pw.println("WEDNESDAY"); break;
case Calendar.THURSDAY: pw.println("THURSDAY"); break;
case Calendar.FRIDAY: pw.println("FRIDAY"); break;
case Calendar.SATURDAY: pw.println("SATURDAY");
}
if (cmd.startsWith("DOY")) pw.println("" + c.get(Calendar.DAY_OF_YEAR));
if (cmd.startsWith("PAUSE"))
try { Thread.sleep(3000); } catch (InterruptedException e) {}
} while (true);
} catch (IOException e) { System.out.println(e.toString()); }
finally {
System.out.println("Closing Connection...\n");
try { if (br != null) br.close(); if (pw != null) pw.close(); if (s != null) s.close(); } catch (IOException e) {}
}
} }

執行輸出:

Server starting...
Accepting Connection...
Closing Connection...

SSServer 宣告兩類:SSServer、ServerThread。main() 創建 ServerSocket 監聽埠 10000,成功進入無限迴圈,交替調用 accept() 等待請求、啟動後台執行緒處理。執行緒由 ServerThread 繼承 start() 開始,執行 run() 代碼。

run() 運行後,執行緒創建 BufferedReader、PrintWriter、Calendar 進入迴圈,由讀取客戶程式一行文本開始,文本(命令)存儲在 cmd 字串。若客戶程式過早關閉輸出流,cmd 得不到賦值。注意必須考慮此情況:服務程式正讀輸入流時客戶程式關閉輸出流,若無處理將產生異常。

編譯後輸入 java SSServer 執行,啟動後可運行一或多個 SSClient。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved