課前思考
1. 什么是TCP/ IP協(xié)議?
2. TCP/IP有哪兩種傳輸協(xié)議,各有什么特點(diǎn)?
3. 什么是URL?
4. URL和IP地址有什么樣的關(guān)系?
5. 什么叫套接字(Socket)?
6. 套接字(Socket)和TCP/IP協(xié)議的關(guān)系?
7. URL和套接字(Socket)的關(guān)系?
8.1 網(wǎng)絡(luò)編程基本概念,TCP/IP協(xié)議簡介
8.1.1 網(wǎng)絡(luò)基礎(chǔ)知識
網(wǎng)絡(luò)編程的目的就是指直接或間接地通過網(wǎng)絡(luò)協(xié)議與其他計(jì)算機(jī)進(jìn)行通訊。網(wǎng)絡(luò)編程中有兩個(gè)主要的問題,一個(gè)是如何準(zhǔn)確的定位網(wǎng)絡(luò)上一臺或多臺主機(jī),另一個(gè)就是找到主機(jī)后如何可靠高效的進(jìn)行數(shù)據(jù)傳輸。在TCP/IP協(xié)議中IP層主要負(fù)責(zé)網(wǎng)絡(luò)主機(jī)的定位,數(shù)據(jù)傳輸?shù)穆酚桑?/span>IP地址可以唯一地確定Internet上的一臺主機(jī)。而TCP層則提供面向應(yīng)用的可靠的或非可靠的數(shù)據(jù)傳輸機(jī)制,這是網(wǎng)絡(luò)編程的主要對象,一般不需要關(guān)心IP層是如何處理數(shù)據(jù)的。
目前較為流行的網(wǎng)絡(luò)編程模型是客戶機(jī)/服務(wù)器(C/S)結(jié)構(gòu)。即通信雙方一方作為服務(wù)器等待客戶提出請求并予以響應(yīng)。客戶則在需要服務(wù)時(shí)向服務(wù)器提出申請。服務(wù)器一般作為守護(hù)進(jìn)程始終運(yùn)行,監(jiān)聽網(wǎng)絡(luò)端口,一旦有客戶請求,就會啟動一個(gè)服務(wù)進(jìn)程來響應(yīng)該客戶,同時(shí)自己繼續(xù)監(jiān)聽服務(wù)端口,使后來的客戶也能及時(shí)得到服務(wù)。
8.1.3兩類傳輸協(xié)議:TCP;UDP
盡管TCP/IP協(xié)議的名稱中只有TCP這個(gè)協(xié)議名,但是在TCP/IP的傳輸層同時(shí)存在TCP和UDP兩個(gè)協(xié)議。
TCP是Tranfer Control Protocol的簡稱,是一種面向連接的保證可靠傳輸?shù)膮f(xié)議。通過TCP協(xié)議傳輸,得到的是一個(gè)順序的無差錯(cuò)的數(shù)據(jù)流。發(fā)送方和接收方的成對的兩個(gè)socket之間必須建立連接,以便在TCP協(xié)議的基礎(chǔ)上進(jìn)行通信,當(dāng)一個(gè)socket(通常都是server socket)等待建立連接時(shí),另一個(gè)socket可以要求進(jìn)行連接,一旦這兩個(gè)socket連接起來,它們就可以進(jìn)行雙向數(shù)據(jù)傳輸,雙方都可以進(jìn)行發(fā)送或接收操作。
UDP是User Datagram Protocol的簡稱,是一種無連接的協(xié)議,每個(gè)數(shù)據(jù)報(bào)都是一個(gè)獨(dú)立的信息,包括完整的源地址或目的地址,它在網(wǎng)絡(luò)上以任何可能的路徑傳往目的地,因此能否到達(dá)目的地,到達(dá)目的地的時(shí)間以及內(nèi)容的正確性都是不能被保證的。
下面我們對這兩種協(xié)議做簡單比較:
使用UDP時(shí),每個(gè)數(shù)據(jù)報(bào)中都給出了完整的地址信息,因此無需要建立發(fā)送方和接收方的連接。對于TCP協(xié)議,由于它是一個(gè)面向連接的協(xié)議,在socket之間進(jìn)行數(shù)據(jù)傳輸之前必然要建立連接,所以在TCP中多了一個(gè)連接建立的時(shí)間。
使用UDP傳輸數(shù)據(jù)時(shí)是有大小限制的,每個(gè)被傳輸?shù)臄?shù)據(jù)報(bào)必須限定在64KB之內(nèi)。而TCP沒有這方面的限制,一旦連接建立起來,雙方的socket就可以按統(tǒng)一的格式傳輸大量的數(shù)據(jù)。UDP是一個(gè)不可靠的協(xié)議,發(fā)送方所發(fā)送的數(shù)據(jù)報(bào)并不一定以相同的次序到達(dá)接收方。而TCP是一個(gè)可靠的協(xié)議,它確保接收方完全正確地獲取發(fā)送方所發(fā)送的全部數(shù)據(jù)。
總之,TCP在網(wǎng)絡(luò)通信上有極強(qiáng)的生命力,例如遠(yuǎn)程連接(Telnet)和文件傳輸(FTP)都需要不定長度的數(shù)據(jù)被可靠地傳輸。相比之下UDP操作簡單,而且僅需要較少的監(jiān)護(hù),因此通常用于局域網(wǎng)高可靠性的分散系統(tǒng)中client/server應(yīng)用程序。
讀者可能要問,既然有了保證可靠傳輸?shù)?/span>TCP協(xié)議,為什么還要非可靠傳輸?shù)?/span>UDP協(xié)議呢?主要的原因有兩個(gè)。一是可靠的傳輸是要付出代價(jià)的,對數(shù)據(jù)內(nèi)容正確性的檢驗(yàn)必然占用計(jì)算機(jī)的處理時(shí)間和網(wǎng)絡(luò)的帶寬,因此TCP傳輸?shù)男什蝗?/span>UDP高。二是在許多應(yīng)用中并不需要保證嚴(yán)格的傳輸可靠性,比如視頻會議系統(tǒng),并不要求音頻視頻數(shù)據(jù)絕對的正確,只要保證連貫性就可以了,這種情況下顯然使用UDP會更合理一些。
8.2 基于URL的高層次Java網(wǎng)絡(luò)編程
8.2.1一致資源定位器URL
URL(Uniform Resource Locator)是一致資源定位器的簡稱,它表示Internet上某一資源的地址。通過URL我們可以訪問Internet上的各種網(wǎng)絡(luò)資源,比如最常見的WWW,FTP站點(diǎn)。瀏覽器通過解析給定的URL可以在網(wǎng)絡(luò)上查找相應(yīng)的文件或其他資源。
8.2.2 URL的組成
protocol://resourceName
協(xié)議名(protocol)指明獲取資源所使用的傳輸協(xié)議,如http、ftp、gopher、file等,資源名(resourceName)則應(yīng)該是資源的完整地址,包括主機(jī)名、端口號、文件名或文件內(nèi)部的一個(gè)引用。例如:
http://www.sun.com/ 協(xié)議名://主機(jī)名
http://home.netscape.com/home/welcome.html 協(xié)議名://機(jī)器名+文件名
http://www.gamelan.com:80/Gamelan/network.html#BOTTOM 協(xié)議名://機(jī)器名+端口號+文件名+內(nèi)部引用.
8.2.3 創(chuàng)建一個(gè)URL
為了表示URL, java.net中實(shí)現(xiàn)了類URL。我們可以通過下面的構(gòu)造方法來初始化一個(gè)URL對象:
(1) public URL (String spec);
通過一個(gè)表示URL地址的字符串可以構(gòu)造一個(gè)URL對象。
URL urlBase=new URL("http://www. 263.net/")
(2) public URL(URL context, String spec);
通過基URL和相對URL構(gòu)造一個(gè)URL對象。
URL net263=new URL ("http://www.263.net/");
URL index263=new URL(net263, "index.html")
(3) public URL(String protocol, String host, String file);
new URL("http", "www.gamelan.com", "/pages/Gamelan.net. html");
(4) public URL(String protocol, String host, int port, String file);
URL gamelan=new URL("http", "www.gamelan.com", 80, "Pages/Gamelan.network.html");
注意:類URL的構(gòu)造方法都聲明拋棄非運(yùn)行時(shí)例外(MalformedURLException),因此生成URL對象時(shí),我們必須要對這一例外進(jìn)行處理,通常是用try-catch語句進(jìn)行捕獲。格式如下:
try{
URL myURL= new URL(…)
}catch (MalformedURLException e){
… }
8.2.4 解析一個(gè)URL
一個(gè)URL對象生成后,其屬性是不能被改變的,但是我們可以通過類URL所提供的方法來獲取這些屬性:
public String getProtocol() 獲取該URL的協(xié)議名。
public String getHost() 獲取該URL的主機(jī)名。
public int getPort() 獲取該URL的端口號,如果沒有設(shè)置端口,返回-1。
public String getFile() 獲取該URL的文件名。
public String getRef() 獲取該URL在文件中的相對位置。
public String getQuery() 獲取該URL的查詢信息。
public String getPath() 獲取該URL的路徑
public String getAuthority() 獲取該URL的權(quán)限信息
public String getUserInfo() 獲得使用者的信息
public String getRef() 獲得該URL的錨
8.2.5 從URL讀取WWW網(wǎng)絡(luò)資源
當(dāng)我們得到一個(gè)URL對象后,就可以通過它讀取指定的WWW資源。這時(shí)我們將使用URL的方法openStream(),其定義為:
InputStream openStream();
方法openSteam()與指定的URL建立連接并返回InputStream類的對象以從這一連接中讀取數(shù)據(jù)。
public class URLReader {
public static void main(String[] args) throws Exception {
//聲明拋出所有例外
URL tirc = new URL("http://www.tirc1.cs.tsinghua.edu.cn/");
//構(gòu)建一URL對象
BufferedReader in = new BufferedReader(new InputStreamReader(tirc.openStream()));
//使用openStream得到一輸入流并由此構(gòu)造一個(gè)BufferedReader對象
String inputLine;
while ((inputLine = in.readLine()) != null)
//從輸入流不斷的讀數(shù)據(jù),直到讀完為止
System.out.println(inputLine); //把讀入的數(shù)據(jù)打印到屏幕上
in.close(); //關(guān)閉輸入流
}
}
8.2.6 通過URLConnetction連接WWW
通過URL的方法openStream(),我們只能從網(wǎng)絡(luò)上讀取數(shù)據(jù),如果我們同時(shí)還想輸出數(shù)據(jù),例如向服務(wù)器端的CGI程序發(fā)送一些數(shù)據(jù),我們必須先與URL建立連接,然后才能對其進(jìn)行讀寫,這時(shí)就要用到類URLConnection了。CGI是公共網(wǎng)關(guān)接口(Common Gateway Interface)的簡稱,它是用戶瀏覽器和服務(wù)器端的應(yīng)用程序進(jìn)行連接的接口,有關(guān)CGI程序設(shè)計(jì),請讀者參考有關(guān)書籍。
類URLConnection也在包java.net中定義,它表示Java程序和URL在網(wǎng)絡(luò)上的通信連接。當(dāng)與一個(gè)URL建立連接時(shí),首先要在一個(gè)URL對象上通過方法openConnection()生成對應(yīng)的URLConnection對象。例如下面的程序段首先生成一個(gè)指向地址http://edu.chinaren.com/index.shtml的對象,然后用openConnection()打開該URL對象上的一個(gè)連接,返回一個(gè)URLConnection對象。如果連接過程失敗,將產(chǎn)生IOException.
Try{
URL netchinaren = new URL ("http://edu.chinaren.com/index.shtml");
URLConnectonn tc = netchinaren.openConnection();
}catch(MalformedURLException e){ //創(chuàng)建URL()對象失敗
…
}catch (IOException e){ //openConnection()失敗
…
}
類URLConnection提供了很多方法來設(shè)置或獲取連接參數(shù),程序設(shè)計(jì)時(shí)最常使用的是getInputStream()和getOurputStream(),其定義為:
InputSteram getInputSteram();
OutputSteram getOutputStream();
通過返回的輸入/輸出流我們可以與遠(yuǎn)程對象進(jìn)行通信。看下面的例子:
URL url =new URL ("http://www.javasoft.com/cgi-bin/backwards");
//創(chuàng)建一URL對象
URLConnectin con=url.openConnection();
//由URL對象獲取URLConnection對象
DataInputStream dis=new DataInputStream (con.getInputSteam());
//由URLConnection獲取輸入流,并構(gòu)造DataInputStream對象
PrintStream ps=new PrintSteam(con.getOutupSteam());
//由URLConnection獲取輸出流,并構(gòu)造PrintStream對象
String line=dis.readLine(); //從服務(wù)器讀入一行
ps.println("client…"); //向服務(wù)器寫出字符串 "client…"
其中backwards為服務(wù)器端的CGI程序。實(shí)際上,類URL的方法openSteam()是通過URLConnection來實(shí)現(xiàn)的。它等價(jià)于
openConnection().getInputStream();
基于URL的網(wǎng)絡(luò)編程在底層其實(shí)還是基于下面要講的Socket接口的。WWW,FTP等標(biāo)準(zhǔn)化的網(wǎng)絡(luò)服務(wù)都是基于TCP協(xié)議的,所以本質(zhì)上講URL編程也是基于TCP的一種應(yīng)用.
8.3 基于Socket的低層次Java網(wǎng)絡(luò)編程
8.3.1 Socket通訊
網(wǎng)絡(luò)上的兩個(gè)程序通過一個(gè)雙向的通訊連接實(shí)現(xiàn)數(shù)據(jù)的交換,這個(gè)雙向鏈路的一端稱為一個(gè)Socket。Socket通常用來實(shí)現(xiàn)客戶方和服務(wù)方的連接。Socket是TCP/IP協(xié)議的一個(gè)十分流行的編程界面,一個(gè)Socket由一個(gè)IP地址和一個(gè)端口號唯一確定。
在傳統(tǒng)的UNIX環(huán)境下可以操作TCP/IP協(xié)議的接口不止Socket一個(gè),Socket所支持的協(xié)議種類也不光TCP/IP一種,因此兩者之間是沒有必然聯(lián)系的。在Java環(huán)境下,Socket編程主要是指基于TCP/IP協(xié)議的網(wǎng)絡(luò)編程。
8.3.2 Socket通訊的一般過程
使用Socket進(jìn)行Client/Server程序設(shè)計(jì)的一般連接過程是這樣的:Server端Listen(監(jiān)聽)某個(gè)端口是否有連接請求,Client端向Server端發(fā)出Connect(連接)請求,Server端向Client端發(fā)回Accept(接受)消息。一個(gè)連接就建立起來了。Server端和Client端都可以通過Send,Write等方法與對方通信。
對于一個(gè)功能齊全的Socket,都要包含以下基本結(jié)構(gòu),其工作過程包含以下四個(gè)基本的步驟:
(1) 創(chuàng)建Socket;
(2) 打開連接到Socket的輸入/出流;
(3) 按照一定的協(xié)議對Socket進(jìn)行讀/寫操作;
(4) 關(guān)閉Socket.
8.3.3 創(chuàng)建Socket
java在包java.net中提供了兩個(gè)類Socket和ServerSocket,分別用來表示雙向連接的客戶端和服務(wù)端。這是兩個(gè)封裝得非常好的類,使用很方便。其構(gòu)造方法如下:
Socket(InetAddress address, int port);
Socket(InetAddress address, int port, boolean stream);
Socket(String host, int prot);
Socket(String host, int prot, boolean stream);
Socket(SocketImpl impl)
Socket(String host, int port, InetAddress localAddr, int localPort)
Socket(InetAddress address, int port, InetAddress localAddr, int localPort)
ServerSocket(int port);
ServerSocket(int port, int backlog);
ServerSocket(int port, int backlog, InetAddress bindAddr)
其中address、host和port分別是雙向連接中另一方的IP地址、主機(jī)名和端口號,stream指明socket是流socket還是數(shù)據(jù)報(bào)socket,localPort表示本地主機(jī)的端口號,localAddr和bindAddr是本地機(jī)器的地址(ServerSocket的主機(jī)地址),impl是socket的父類,既可以用來創(chuàng)建serverSocket又可以用來創(chuàng)建Socket。count則表示服務(wù)端所能支持的最大連接數(shù)。例如:
Socket client = new Socket("127.0.01.", 80);
ServerSocket server = new ServerSocket(80);
注意,在選擇端口時(shí),必須小心。每一個(gè)端口提供一種特定的服務(wù),只有給出正確的端口,才能獲得相應(yīng)的服務(wù)。0~1023的端口號為系統(tǒng)所保留,例如http服務(wù)的端口號為80,telnet服務(wù)的端口號為21,ftp服務(wù)的端口號為23, 所以我們在選擇端口號時(shí),最好選擇一個(gè)大于1023的數(shù)以防止發(fā)生沖突。
在創(chuàng)建socket時(shí)如果發(fā)生錯(cuò)誤,將產(chǎn)生IOException,在程序中必須對之作出處理。所以在創(chuàng)建Socket或ServerSocket是必須捕獲或拋出例外。
8.3.8 簡單的Client/Server程序設(shè)計(jì)
下面我們給出一個(gè)用Socket實(shí)現(xiàn)的客戶和服務(wù)器交互的典型的C/S結(jié)構(gòu)的演示程序,讀者通過仔細(xì)閱讀該程序,會對前面所討論的各個(gè)概念有更深刻的認(rèn)識。程序的意義請參考注釋。
1. 客戶端程序
import java.io.*;
import java.net.*;
public class TalkClient {
public static void main(String args[]) {
try{
Socket socket=new Socket("127.0.0.1",4700);
//向本機(jī)的4700端口發(fā)出客戶請求
BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
//由系統(tǒng)標(biāo)準(zhǔn)輸入設(shè)備構(gòu)造BufferedReader對象
PrintWriter os=new PrintWriter(socket.getOutputStream());
//由Socket對象得到輸出流,并構(gòu)造PrintWriter對象
BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//由Socket對象得到輸入流,并構(gòu)造相應(yīng)的BufferedReader對象
String readline;
readline=sin.readLine(); //從系統(tǒng)標(biāo)準(zhǔn)輸入讀入一字符串
while(!readline.equals("bye")){
//若從標(biāo)準(zhǔn)輸入讀入的字符串為 "bye"則停止循環(huán)
os.println(readline);
//將從系統(tǒng)標(biāo)準(zhǔn)輸入讀入的字符串輸出到Server
os.flush();
//刷新輸出流,使Server馬上收到該字符串
System.out.println("Client:"+readline);
//在系統(tǒng)標(biāo)準(zhǔn)輸出上打印讀入的字符串
System.out.println("Server:"+is.readLine());
//從Server讀入一字符串,并打印到標(biāo)準(zhǔn)輸出上
readline=sin.readLine(); //從系統(tǒng)標(biāo)準(zhǔn)輸入讀入一字符串
} //繼續(xù)循環(huán)
os.close(); //關(guān)閉Socket輸出流
is.close(); //關(guān)閉Socket輸入流
socket.close(); //關(guān)閉Socket
}catch(Exception e) {
System.out.println("Error"+e); //出錯(cuò),則打印出錯(cuò)信息
}
}
}
2. 服務(wù)器端程序
import java.io.*;
import java.net.*;
import java.applet.Applet;
public class TalkServer{
public static void main(String args[]) {
try{
ServerSocket server=null;
try{
server=new ServerSocket(4700);
//創(chuàng)建一個(gè)ServerSocket在端口4700監(jiān)聽客戶請求
}catch(Exception e) {
System.out.println("can not listen to:"+e);
//出錯(cuò),打印出錯(cuò)信息
}
Socket socket=null;
try{
socket=server.accept();
//使用accept()阻塞等待客戶請求,有客戶
//請求到來則產(chǎn)生一個(gè)Socket對象,并繼續(xù)執(zhí)行
}catch(Exception e) {
System.out.println("Error."+e);
//出錯(cuò),打印出錯(cuò)信息
}
String line;
BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//由Socket對象得到輸入流,并構(gòu)造相應(yīng)的BufferedReader對象
PrintWriter os=newPrintWriter(socket.getOutputStream());
//由Socket對象得到輸出流,并構(gòu)造PrintWriter對象
BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
//由系統(tǒng)標(biāo)準(zhǔn)輸入設(shè)備構(gòu)造BufferedReader對象
System.out.println("Client:"+is.readLine());
//在標(biāo)準(zhǔn)輸出上打印從客戶端讀入的字符串
line=sin.readLine();
//從標(biāo)準(zhǔn)輸入讀入一字符串
while(!line.equals("bye")){
//如果該字符串為 "bye",則停止循環(huán)
os.println(line);
//向客戶端輸出該字符串
os.flush();
//刷新輸出流,使Client馬上收到該字符串
System.out.println("Server:"+line);
//在系統(tǒng)標(biāo)準(zhǔn)輸出上打印讀入的字符串
System.out.println("Client:"+is.readLine());
//從Client讀入一字符串,并打印到標(biāo)準(zhǔn)輸出上
line=sin.readLine();
//從系統(tǒng)標(biāo)準(zhǔn)輸入讀入一字符串
} //繼續(xù)循環(huán)
os.close(); //關(guān)閉Socket輸出流
is.close(); //關(guān)閉Socket輸入流
socket.close(); //關(guān)閉Socket
server.close(); //關(guān)閉ServerSocket
}catch(Exception e){
System.out.println("Error:"+e);
//出錯(cuò),打印出錯(cuò)信息
}
}
}
8.3.9 支持多客戶的client/server程序設(shè)計(jì)
前面提供的Client/Server程序只能實(shí)現(xiàn)Server和一個(gè)客戶的對話。在實(shí)際應(yīng)用中,往往是在服務(wù)器上運(yùn)行一個(gè)永久的程序,它可以接收來自其他多個(gè)客戶端的請求,提供相應(yīng)的服務(wù)。為了實(shí)現(xiàn)在服務(wù)器方給多個(gè)客戶提供服務(wù)的功能,需要對上面的程序進(jìn)行改造,利用多線程實(shí)現(xiàn)多客戶機(jī)制。服務(wù)器總是在指定的端口上監(jiān)聽是否有客戶請求,一旦監(jiān)聽到客戶請求,服務(wù)器就會啟動一個(gè)專門的服務(wù)線程來響應(yīng)該客戶的請求,而服務(wù)器本身在啟動完線程之后馬上又進(jìn)入監(jiān)聽狀態(tài),等待下一個(gè)客戶的到來。
ServerSocket serverSocket=null;
boolean listening=true;
try{
serverSocket=new ServerSocket(4700);
//創(chuàng)建一個(gè)ServerSocket在端口4700監(jiān)聽客戶請求
}catch(IOException e) { }
while(listening){ //永遠(yuǎn)循環(huán)監(jiān)聽
new ServerThread(serverSocket.accept(),clientnum).start();
//監(jiān)聽到客戶請求,根據(jù)得到的Socket對象和
客戶計(jì)數(shù)創(chuàng)建服務(wù)線程,并啟動之
clientnum++; //增加客戶計(jì)數(shù)
}
serverSocket.close(); //關(guān)閉ServerSocket
設(shè)計(jì)ServerThread類
public class ServerThread extends Thread{
Socket socket=null; //保存與本線程相關(guān)的Socket對象
int clientnum; //保存本進(jìn)程的客戶計(jì)數(shù)
public ServerThread(Socket socket,int num) { //構(gòu)造函數(shù)
this.socket=socket; //初始化socket變量
clientnum=num+1; //初始化clientnum變量
}
public void run() { //線程主體
try{//在這里實(shí)現(xiàn)數(shù)據(jù)的接受和發(fā)送}
8.3.10 據(jù)報(bào)Datagram通訊
前面在介紹TCP/IP協(xié)議的時(shí)候,我們已經(jīng)提到,在TCP/IP協(xié)議的傳輸層除了TCP協(xié)議之外還有一個(gè)UDP協(xié)議,相比而言UDP的應(yīng)用不如TCP廣泛,幾個(gè)標(biāo)準(zhǔn)的應(yīng)用層協(xié)議HTTP,FTP,SMTP…使用的都是TCP協(xié)議。但是,隨著計(jì)算機(jī)網(wǎng)絡(luò)的發(fā)展,UDP協(xié)議正越來越來顯示出其威力,尤其是在需要很強(qiáng)的實(shí)時(shí)交互性的場合,如網(wǎng)絡(luò)游戲,視頻會議等,UDP更是顯示出極強(qiáng)的威力,下面我們就介紹一下Java環(huán)境下如何實(shí)現(xiàn)UDP網(wǎng)絡(luò)傳輸。
8.3.11 什么是Datagram
所謂數(shù)據(jù)報(bào)(Datagram)就跟日常生活中的郵件系統(tǒng)一樣,是不能保證可靠的寄到的,而面向鏈接的TCP就好比電話,雙方能肯定對方接受到了信息。在本章前面,我們已經(jīng)對UDP和TCP進(jìn)行了比較,在這里再稍作小節(jié):
TCP,可靠,傳輸大小無限制,但是需要連接建立時(shí)間,差錯(cuò)控制開銷大。
UDP,不可靠,差錯(cuò)控制開銷較小,傳輸大小限制在64K以下,不需要建立連接。
8.3.12 Datagram通訊的表示方法:DatagramSocket;DatagramPacket
包java.net中提供了兩個(gè)類DatagramSocket和DatagramPacket用來支持?jǐn)?shù)據(jù)報(bào)通信,DatagramSocket用于在程序之間建立傳送數(shù)據(jù)報(bào)的通信連接, DatagramPacket則用來表示一個(gè)數(shù)據(jù)報(bào)。先來看一下DatagramSocket的構(gòu)造方法:
DatagramSocket();
DatagramSocket(int prot);
DatagramSocket(int port, InetAddress laddr)
其中,port指明socket所使用的端口號,如果未指明端口號,則把socket連接到本地主機(jī)上一個(gè)可用的端口。laddr指明一個(gè)可用的本地地址。給出端口號時(shí)要保證不發(fā)生端口沖突,否則會生成SocketException類例外。注意:上述的兩個(gè)構(gòu)造方法都聲明拋棄非運(yùn)行時(shí)例外SocketException,程序中必須進(jìn)行處理,或者捕獲、或者聲明拋棄。
用數(shù)據(jù)報(bào)方式編寫client/server程序時(shí),無論在客戶方還是服務(wù)方,首先都要建立一個(gè)DatagramSocket對象,用來接收或發(fā)送數(shù)據(jù)報(bào),然后使用DatagramPacket類對象作為傳輸數(shù)據(jù)的載體。下面看一下DatagramPacket的構(gòu)造方法 :
DatagramPacket(byte buf[],int length);
DatagramPacket(byte buf[], int length, InetAddress addr, int port);
DatagramPacket(byte[] buf, int offset, int length);
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port);
其中,buf中存放數(shù)據(jù)報(bào)數(shù)據(jù),length為數(shù)據(jù)報(bào)中數(shù)據(jù)的長度,addr和port旨明目的地址,offset指明了數(shù)據(jù)報(bào)的位移量。
在接收數(shù)據(jù)前,應(yīng)該采用上面的第一種方法生成一個(gè)DatagramPacket對象,給出接收數(shù)據(jù)的緩沖區(qū)及其長度。然后調(diào)用DatagramSocket 的方法receive()等待數(shù)據(jù)報(bào)的到來,receive()將一直等待,直到收到一個(gè)數(shù)據(jù)報(bào)為止。
DatagramPacket packet=new DatagramPacket(buf, 256);
Socket.receive (packet);
發(fā)送數(shù)據(jù)前,也要先生成一個(gè)新的DatagramPacket對象,這時(shí)要使用上面的第二種構(gòu)造方法,在給出存放發(fā)送數(shù)據(jù)的緩沖區(qū)的同時(shí),還要給出完整的目的地址,包括IP地址和端口號。發(fā)送數(shù)據(jù)是通過DatagramSocket的方法send()實(shí)現(xiàn)的,send()根據(jù)數(shù)據(jù)報(bào)的目的地址來尋徑,以傳遞數(shù)據(jù)報(bào)。
DatagramPacket packet=new DatagramPacket(buf, length, address, port);
Socket.send(packet);
在構(gòu)造數(shù)據(jù)報(bào)時(shí),要給出InetAddress類參數(shù)。類InetAddress在包java.net中定義,用來表示一個(gè)Internet地址,我們可以通過它提供的類方法getByName()從一個(gè)表示主機(jī)名的字符串獲取該主機(jī)的IP地址,然后再獲取相應(yīng)的地址信息。
8.3.14 用數(shù)據(jù)報(bào)進(jìn)行廣播通訊
DatagramSocket只允許數(shù)據(jù)報(bào)發(fā)送一個(gè)目的地址,java.net包中提供了一個(gè)類MulticastSocket,允許數(shù)據(jù)報(bào)以廣播方式發(fā)送到該端口的所有客戶。MulticastSocket用在客戶端,監(jiān)聽服務(wù)器廣播來的數(shù)據(jù)。
1. 客戶方程序:MulticastClient.java
import java.io.*;
import java.net.*;
import java.util.*;
public class MulticastClient {
public static void main(String args[]) throws IOException
{
MulticastSocket socket=new MulticastSocket(4446);
//創(chuàng)建4446端口的廣播套接字
InetAddress address=InetAddress.getByName("230.0.0.1");
//得到230.0.0.1的地址信息
socket.joinGroup(address);
//使用joinGroup()將廣播套接字綁定到地址上
DatagramPacket packet;
for(int i=0;i<5;i++) {
byte[] buf=new byte[256];
//創(chuàng)建緩沖區(qū)
packet=new DatagramPacket(buf,buf.length);
//創(chuàng)建接收數(shù)據(jù)報(bào)
socket.receive(packet); //接收
String received=new String(packet.getData());
//由接收到的數(shù)據(jù)報(bào)得到字節(jié)數(shù)組,
//并由此構(gòu)造一個(gè)String對象
System.out.println("Quote of theMoment:"+received);
//打印得到的字符串
} //循環(huán)5次
socket.leaveGroup(address);
//把廣播套接字從地址上解除綁定
socket.close(); //關(guān)閉廣播套接字
}
}
2. 服務(wù)器方程序:MulticastServer.java
public class MulticastServer{
public static void main(String args[]) throws java.io.IOException
{
new MulticastServerThread().start();
//啟動一個(gè)服務(wù)器線程
}
}
3. 程序MulticastServerThread.java
import java.io.*;
import java.net.*;
import java.util.*;
public class MulticastServerThread extends QuoteServerThread
//從QuoteServerThread繼承得到新的服務(wù)器線程類MulticastServerThread
{
Private long FIVE_SECOND=5000; //定義常量,5秒鐘
public MulticastServerThread(String name) throws IOException
{
super("MulticastServerThread");
//調(diào)用父類,也就是QuoteServerThread的構(gòu)造函數(shù)
}
public void run() //重寫父類的線程主體
{
while(moreQuotes) {
//根據(jù)標(biāo)志變量判斷是否繼續(xù)循環(huán)
try{
byte[] buf=new byte[256];
//創(chuàng)建緩沖區(qū)
String dString=null;
if(in==null) dString=new Date().toString();
//如果初始化的時(shí)候打開文件失敗了,
//則使用日期作為要傳送的字符串
else dString=getNextQuote();
//否則調(diào)用成員函數(shù)從文件中讀出字符串
buf=dString.getByte();
//把String轉(zhuǎn)換成字節(jié)數(shù)組,以便傳送send it
InetAddress group=InetAddress.getByName("230.0.0.1");
//得到230.0.0.1的地址信息
DatagramPacket packet=new DatagramPacket(buf,buf.length,group,4446);
//根據(jù)緩沖區(qū),廣播地址,和端口號創(chuàng)建DatagramPacket對象
socket.send(packet); //發(fā)送該P(yáng)acket
try{
sleep((long)(Math.random()*FIVE_SECONDS));
//隨機(jī)等待一段時(shí)間,0~5秒之間
}catch(InterruptedException e) { } //異常處理
}catch(IOException e){ //異常處理
e.printStackTrace( ); //打印錯(cuò)誤棧
moreQuotes=false; //置結(jié)束循環(huán)標(biāo)志
}
}
socket.close( ); //關(guān)閉廣播套接口
}
}
【本講小結(jié)】
本講主要講解了Java環(huán)境下的網(wǎng)絡(luò)編程。因?yàn)?/span>TCP/IP協(xié)議是Java網(wǎng)絡(luò)編程的基礎(chǔ)知識,本講開篇重點(diǎn)介紹了TCP/IP協(xié)議中的一些概念,TCP/IP協(xié)議本身是一個(gè)十分龐大的系統(tǒng),用幾個(gè)小節(jié)是不可能講清楚的。所以我們只是聯(lián)系實(shí)際,講解了一些最基本的概念,幫助學(xué)生理解后面的相關(guān)內(nèi)容。重點(diǎn)有一下幾個(gè)概念:主機(jī)名,IP,端口,服務(wù)類型,TCP,UDP。
后續(xù)的內(nèi)容分為兩大塊,一塊是以URL為主線,講解如何通過URL類和URLConnection類訪問WWW網(wǎng)絡(luò)資源,由于使用URL十分方便直觀,盡管功能不是很強(qiáng),還是值得推薦的一種網(wǎng)絡(luò)編程方法,尤其是對于初學(xué)者特別容易接受。本質(zhì)上講,URL網(wǎng)絡(luò)編程在傳輸層使用的還是TCP協(xié)議。
另一塊是以Socket接口和C/S網(wǎng)絡(luò)編程模型為主線,依次講解了如何用Java實(shí)現(xiàn)基于TCP的C/S結(jié)構(gòu),主要用到的類有Socket,ServerSocket。以及如何用Java實(shí)現(xiàn)基于UDP的C/S結(jié)構(gòu),還討論了一種特殊的傳輸方式,廣播方式,這種方式是UDP所特有的,主要用到的類有DatagramSocket , DatagramPacket, MulticastSocket。這一塊在Java網(wǎng)絡(luò)編程中相對而言是最難的(盡管Java在網(wǎng)絡(luò)編程這方面已經(jīng)做的夠"傻瓜"了,但是網(wǎng)絡(luò)編程在其他環(huán)境下的卻是一件極為頭痛的事情,再"傻瓜"還是有一定的難度),也是功能最為強(qiáng)大的一部分,讀者應(yīng)該好好研究,領(lǐng)悟其中的思想。
最后要強(qiáng)調(diào)的是要學(xué)好Java網(wǎng)絡(luò)編程,Java語言,最重要的還是在于多多練習(xí)!