用Java開發網絡軟件非常方便和強大
Java的這種力量來源於他獨有的一套強大的用於網絡的 API
這些API是一系列的類和接口
均位於包和中
在這篇文章中我們將介紹套接字(Socket)慨念
同時以實例說明如何使用Network API操縱套接字
在完成本文後
你將具備編寫網絡低端通訊軟件的能力
什麼是套接字(Socket)? Network API是典型的用於基於TCP/IP網絡Java程序與其他程序通訊
Network API依靠Socket進行通訊
Socket可以看成在兩個程序進行通訊連接中的一個端點
一個程序將一段信息寫入Socket中
該Socket將這段信息發送給另外一個Socket中
使這段信息能傳送到其他程序中
如圖
我們來分析一下圖
Host A上的程序A將一段信息寫入Socket中
Socket的內容被Host A的網絡管理軟件訪問
並將這段信息通過Host A的網絡接口卡發送到Host B
Host B的網絡接口卡接收到這段信息後
傳送給Host B的網絡管理軟件
網絡管理軟件將這段信息保存在Host B的Socket中
然後程序B才能在Socket中閱讀這段信息
假設在圖
的網絡中添加第三個主機Host C
那麼Host A怎麼知道信息被正確傳送到Host B而不是被傳送到Host C中了呢?基於TCP/IP網絡中的每一個主機均被賦予了一個唯一的IP地址
IP地址是一個
位的無符號整數
由於沒有轉變成二進制
因此通常以小數點分隔
如
正如所見IP地址均由四個部分組成
每個部分的范圍都是
以表示
位地址
值得注意的是IP地址都是
位地址
這是IP協議版本
(簡稱Ipv
)規定的
目前由於IPv
地址已近耗盡
所以IPv
地址正逐漸代替Ipv
地址
Ipv
地址則是
位無符號整數
假設第二個程序被加入圖
的網絡的Host B中
那麼由Host A傳來的信息如何能被正確的傳給程序B而不是傳給新加入的程序呢?這是因為每一個基於TCP/IP網絡通訊的程序都被賦予了唯一的端口和端口號
端口是一個信息緩沖區
用於保留Socket中的輸入/輸出信息
端口號是一個
位無符號整數
范圍是
以區別主機上的每一個程序(端口號就像房屋中的房間號)
低於
的短口號保留給標准應用程序
比如pop
的端口號就是
每一個套接字都組合進了IP地址
端口
端口號
這樣形成的整體就可以區別每一個套接字t
下面我們就來談談兩種套接字
流套接字和自尋址數據套接字
流套接字(Stream Socket) 無論何時
在兩個網絡應用程序之間發送和接收信息時都需要建立一個可靠的連接
流套接字依靠TCP協議來保證信息正確到達目的地
實際上
IP包有可能在網絡中丟失或者在傳送過程中發生錯誤
任何一種情況發生
作為接受方的 TCP將聯系發送方TCP重新發送這個IP包
這就是所謂的在兩個流套接字之間建立可靠的連接
流套接字在C/S程序中扮演一個必需的角色
客戶機程序(需要訪問某些服務的網絡應用程序)創建一個扮演服務器程序的主機的IP地址和服務器程序(為客戶端應用程序提供服務的網絡應用程序)的端口號的流套接字對象
客戶端流套接字的初始化代碼將IP地址和端口號傳遞給客戶端主機的網絡管理軟件
管理軟件將IP地址和端口號通過NIC傳遞給服務器端主機
服務器端主機讀到經過NIC傳遞來的數據
然後查看服務器程序是否處於監聽狀態
這種監聽依然是通過套接字和端口來進行的
如果服務器程序處於監聽狀態
那麼服務器端網絡管理軟件就向客戶機網絡管理軟件發出一個積極的響應信號
接收到響應信號後
客戶端流套接字初始化代碼就給客戶程序建立一個端口號
並將這個端口號傳遞給服務器程序的套接字(服務器程序將使用這個端口號識別傳來的信息是否是屬於客戶程序)同時完成流套接字的初始化
如果服務器程序沒有處於監聽狀態
那麼服務器端網絡管理軟件將給客戶端傳遞一個消極信號
收到這個消極信號後
客戶程序的流套接字初始化代碼將拋出一個異常對象並且不建立通訊連接
也不創建流套接字對象
這種情形就像打電話一樣
當有人的時候通訊建立
否則電話將被掛起
這部分的工作包括了相關聯的三個類
InetAddress
Socket
和 ServerSocket
InetAddress對象描繪了
位或
位IP地址
Socket對象代表了客戶程序流套接字
ServerSocket代表了服務程序流套接字
所有這三個類均位於包中
InetAddress類 InetAddress類在網絡API套接字編程中扮演了一個重要角色
參數傳遞給流套接字類和自尋址套接字類構造器或非構造器方法
InetAddress描述了
位或
位IP地址
要完成這個功能
InetAddress類主要依靠兩個支持類Inet
Address 和 Inet
Address
這三個類是繼承關系
InetAddrress是父類
Inet
Address 和 Inet
Address是子類
由於InetAddress類只有一個構造函數
而且不能傳遞參數
所以不能直接創建InetAddress對象
比如下面的做法就是錯誤的
InetAddress ia = new InetAddress ();
但我們可以通過下面的
個工廠方法創建來創建一個InetAddress對象或InetAddress數組
getAllByName(String host)方法返回一個InetAddress對象的引用
每個對象包含一個表示相應主機名的單獨的IP地址
這個IP地址是通過host參數傳遞的
對於指定的主機如果沒有IP地址存在那麼這個方法將拋出一個UnknownHostException 異常對象
getByAddress(byte [] addr)方法返回一個InetAddress對象的引用
這個對象包含了一個Ipv
地址或Ipv
地址
Ipv
地址是一個
字節數組
Ipv
地址是一個
字節地址數組
如果返回的數組既不是
字節的也不是
字節的
那麼方法將會拋出一個UnknownHostException異常對象
getByAddress(String host
byte [] addr)方法返回一個InetAddress對象的引用
這個InetAddress對象包含了一個由host和
字節的addr數組指定的IP地址
或者是host和
字節的addr數組指定的IP地址
如果這個數組既不是
字節的也不是
位字節的
那麼該方法將拋出一個UnknownHostException異常對象
getByName(String host)方法返回一個InetAddress對象
該對象包含了一個與host參數指定的主機相對應的IP地址
對於指定的主機如果沒有IP地址存在
那麼方法將拋出一個UnknownHostException異常對象
getLocalHost()方法返回一個InetAddress對象
這個對象包含了本地機的IP地址
考慮到本地主機既是客戶程序主機又是服務器程序主機
為避免混亂
我們將客戶程序主機稱為客戶主機
將服務器程序主機稱為服務器主機
上面講到的方法均提到返回一個或多個InetAddress對象的引用
實際上每一個方法都要返回一個或多個Inet
Address/Inet
Address對象的引用
調用者不需要知道引用的子類型
相反調用者可以使用返回的引用調用InetAddress對象的非靜態方法
包括子類型的多態以確保重載方法被調用
InetAddress和它的子類型對象處理主機名到主機IPv
或IPv
地址的轉換
要完成這個轉換需要使用域名系統
下面的代碼示范了如何通過調用getByName(String host)方法獲得InetAddress子類對象的方法
這個對象包含了與host參數相對應的IP地址
InetAddress ia = InetAddress
getByName (
));
一但獲得了InetAddress子類對象的引用就可以調用InetAddress的各種方法來獲得InetAddress子類對象中的IP地址信息
比如
可以通過調用getCanonicalHostName()從域名服務中獲得標准的主機名
getHostAddress()獲得IP地址
getHostName()獲得主機名
isLoopbackAddress()判斷IP地址是否是一個loopback地址
List
是一段示范代碼
InetAddressDemo
// InetAddressDemo
java
import
*;
class InetAddressDemo
{
public static void main (String [] args) throws UnknownHostException
{
String host =
localhost
;
if (args
length ==
)
host = args [
];
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 =
Host Name = localhost
Is Loopback Address = true
InetAddressDemo給了你一個指定主機名作為命令行參數的選擇
如果沒有主機名被指定
那麼將使用localhost(客戶機的)
InetAddressDemo通過調用getByName(String host)方法獲得一個InetAddress子類對象的引用
通過這個引用獲得了標准主機名
主機地址
主機名以及IP地址是否是loopback地址的輸出
Socket類 當客戶程序需要與服務器程序通訊的時候
客戶程序在客戶機創建一個socket對象
Socket類有幾個構造函數
兩個常用的構造函數是 Socket(InetAddress addr
int port) 和 Socket(String host
int port)
兩個構造函數都創建了一個基於Socket的連接服務器端流套接字的流套接字
對於第一個InetAddress子類對象通過addr參數獲得服務器主機的IP地址
對於第二個函數host參數包被分配到InetAddress對象中
如果沒有IP地址與host參數相一致
那麼將拋出UnknownHostException異常對象
兩個函數都通過參數port獲得服務器的端口號
假設已經建立連接了
網絡API將在客戶端基於Socket的流套接字中捆綁客戶程序的IP地址和任意一個端口號
否則兩個函數都會拋出一個IOException對象
如果創建了一個Socket對象
那麼它可能通過調用Socket的 getInputStream()方法從服務程序獲得輸入流讀傳送來的信息
也可能通過調用Socket的 getOutputStream()方法獲得輸出流來發送消息
在讀寫活動完成之後
客戶程序調用close()方法關閉流和流套接字
下面的代碼創建了一個服務程序主機地址為
端口號為
的Socket對象
然後從這個新創建的Socket對象中讀取輸入流
然後再關閉流和Socket對象
Socket s = new Socket (
);
InputStream is = s
getInputStream ();
// Read from the stream
is
close ();
s
close ();
接下面我們將示范一個流套接字的客戶程序
這個程序將創建一個Socket對象
Socket將訪問運行在指定主機端口
上的服務程序
如果訪問成功客戶程序將給服務程序發送一系列命令並打印服務程序的響應
List
使我們創建的程序SSClient的源代碼
Listing
: SSClient
java
// SSClient
java
import java
io
*;
import
*;
class SSClient
{
public static void main (String [] args)
{
String host =
localhost
;
// If user specifies a command
line argument
that argument
// represents the host name
if (args
length ==
)
host = args [
];
BufferedReader br = null;
PrintWriter pw = null;
Socket s = null;
try
{
// Create a socket that attempts to connect to the server
// program on the host at port
s = new Socket (host
);
// Create an input stream reader that chains to the socket
s
// byte
oriented input stream
The input stream reader
// converts bytes read from the socket to characters
The
// conversion is based on the platform
s default character
// set
InputStreamReader isr;
isr = new InputStreamReader (s
getInputStream ());
// Create a buffered reader that chains to the input stream
// reader
The buffered reader supplies a convenient method
// for reading entire lines of text
br = new BufferedReader (isr);
// Create a print writer that chains to the socket
s byte
// oriented output stream
The print writer creates an
// intermediate output stream writer that converts
// characters sent to the socket to bytes
The conversion
// is based on the platform
s default character set
pw = new PrintWriter (s
getOutputStream ()
true);
// Send the DATE command to the server
pw
println (
DATE
);
// Obtain and print the current date/time
System
out
println (br
readLine ());
// Send the PAUSE command to the server
This allows several
// clients to start and verifies that the server is spawning
// multiple threads
pw
println (
PAUSE
);
// Send the DOW command to the server
pw
println (
DOW
);
// Obtain and print the current day of week
System
out
println (br
readLine ());
// Send the DOM command to the server
pw
println (
DOM
);
// Obtain and print the current day of month
System
out
println (br
readLine ());
// Send the DOY command to the server
pw
println (
DOY
);
// Obtain and print the current day of year
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
:
:
CST
TUESDAY
SSClient創建了一個Socket對象與運行在主機端口
的服務程序聯系
主機的IP地址由host變量確定
SSClient將獲得Socket的輸入輸出流
圍繞BufferedReader的輸入流和PrintWriter的輸出流對字符串進行讀寫操作就變得非常容易
SSClient個服務程序發出各種date/time命令並得到響應
每個響應均被打印
一旦最後一個響應被打印
將執行Try/Catch/Finally結構的Finally子串
Finally子串將在關閉Socket之前關閉BufferedReader 和 PrintWriter
在SSClient源代碼編譯完成後
可以輸入java SSClient 來執行這段程序
如果有合適的程序運行在不同的主機上
采用主機名/IP地址為參數的輸入方式
比如是運行服務器程序的主機
那麼輸入方式就是java SSClient
技巧 Socket類包含了許多有用的方法
比如getLocalAddress()將返回一個包含客戶程序IP地址的InetAddress子類對象的引用;getLocalPort()將返回客戶程序的端口號;getInetAddress()將返回一個包含服務器IP地址的InetAddress子類對象的引用
getPort()將返回服務程序的端口號
ServerSocket類 由於SSClient使用了流套接字
所以服務程序也要使用流套接字
這就要創建一個ServerSocket對象
ServerSocket有幾個構造函數
最簡單的是ServerSocket(int port)
當使用ServerSocket(int port)創建一個ServerSocket對象
port參數傳遞端口號
這個端口就是服務器監聽連接請求的端口
如果在這時出現錯誤將拋出IOException異常對象
否則將創建ServerSocket對象並開始准備接收連接請求
接下來服務程序進入無限循環之中
無限循環從調用ServerSocket的accept()方法開始
在調用開始後accept()方法將導致調用線程阻塞直到連接建立
在建立連接後accept()返回一個最近創建的Socket對象
該Socket對象綁定了客戶程序的IP地址或端口號
由於存在單個服務程序與多個客戶程序通訊的可能
所以服務程序響應客戶程序不應該花很多時間
否則客戶程序在得到服務前有可能花很多時間來等待通訊的建立
然而服務程序和客戶程序的會話有可能是很長的(這與電話類似)
因此為加快對客戶程序連接請求的響應
典型的方法是服務器主機運行一個後台線程
這個後台線程處理服務程序和客戶程序的通訊
為了示范我們在上面談到的慨念並完成SSClient程序
下面我們創建一個SSServer程序
程序將創建一個ServerSocket對象來監聽端口
的連接請求
如果成功服務程序將等待連接輸入
開始一個線程處理連接
並響應來自客戶程序的命令
下面就是這段程序的代碼
Listing
: SSServer
java
// SSServer
java
import java
io
*;
import
*;
import java
util
*;
class SSServer
{
public static void main (String [] args) throws IOException
{
System
out
println (
Server starting
\n
);
// Create a server socket that listens for incoming connection
// requests on port
ServerSocket server = new ServerSocket (
);
while (true)
{
// Listen for incoming connection requests from client
// programs
establish a connection
and return a Socket
// object that represents this connection
Socket s = server
accept ();
System
out
println (
Accepting Connection
\n
);
// Start a thread to handle the connection
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
{
// Create an input stream reader that chains to the socket
s
// byte
oriented input stream
The input stream reader
// converts bytes read from the socket to characters
The
// conversion is based on the platform
s default character
// set
InputStreamReader isr;
isr = new InputStreamReader (s
getInputStream ());
// Create a buffered reader that chains to the input stream
// reader
The buffered reader supplies a convenient method
// for reading entire lines of text
br = new BufferedReader (isr);
// Create a print writer that chains to the socket
s byte
// oriented output stream
The print writer creates an
// intermediate output stream writer that converts
// characters sent to the socket to bytes
The conversion
// is based on the platform
s default character set
pw = new PrintWriter (s
getOutputStream ()
true);
// Create a calendar that makes it possible to obtain date
// and time information
Calendar c = Calendar
getInstance ();
// Because the client program may send multiple commands
a
// loop is required
Keep looping until the client either
// explicitly requests termination by sending a command
// beginning with letters BYE or implicitly requests
// termination by closing its output stream
do
{
// Obtain the client program
s next command
String cmd = br
readLine ();
// Exit if client program has closed its output stream
if (cmd == null)
break;
// Convert command to uppercase
for ease of comparison
cmd = cmd
toUpperCase ();
// If client program sends BYE command
terminate
if (cmd
startsWith (
BYE
))
break;
// If client program sends DATE or TIME command
return
// current date/time to the client program
if (cmd
startsWith (
DATE
) || cmd
startsWith (
TIME
))
pw
println (c
getTime ()
toString ());
// If client program sends DOM (Day Of Month) command
// return current day of month to the client program
if (cmd
startsWith (
DOM
))
pw
println (
+ c
get (Calendar
DAY_OF_MONTH));
// If client program sends DOW (Day Of Week) command
// return current weekday (as a string) to the client
// program
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 client program sends DOY (Day of Year) command
// return current day of year to the client program
if (cmd
startsWith (
DOY
))
pw
println (
+ c
get (Calendar
DAY_OF_YEAR));
// If client program sends PAUSE command
sleep for three
// seconds
if (cmd
startsWith (
PAUSE
))
try
{
Thread
sleep (
);
}
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
SSServer的main()方法創建了一個ServerSocket對象來監聽端口
上的連接請求
如果成功
SSServer進入一個無限循環中
交替調用ServerSocket的 accept() 方法來等待連接請求
同時啟動後台線程處理連接(accept()返回的請求)
線程由ServerThread繼承的start()方法開始
並執行ServerThread的run()方法中的代碼
一旦run()方法運行
線程將創建BufferedReader
PrintWriter和 Calendar對象並進入一個循環
這個循環由讀(通過BufferedReader的 readLine())來自客戶程序的一行文本開始
文本(命令)存儲在cmd引用的string對象中
如果客戶程序過早的關閉輸出流
會發生什麼呢?答案是
cmd將得不到賦值
注意必須考慮到這種情況
在服務程序正在讀輸入流時
客戶程序關閉了輸出流
如果沒有對這種情況進行處理
那麼程序將產生異常
一旦編譯了SSServer的源代碼
通過輸入Java SSServer來運行程序
在開始運行SSServer後
就可以運行一個或多個SSClient程序
From:http://tw.wingwit.com/Article/program/Java/hx/201311/25867.html