8.3.10 據報Datagram通訊
前面在介紹TCP/IP協議的時候,我們已經提到,在TCP/IP協議的傳輸層除了TCP協議之外還有一個UDP協議,相比而言UDP的應用不如TCP廣泛,幾個標準的應用層協議HTTP,FTP,SMTP…使用的都是TCP協議。但是,隨着計算機網絡的發展,UDP協議正越來越來顯示出其威力,尤其是在需要很強的實時交互性的場合,如網絡遊戲,視頻會議等,UDP更是顯示出極強的威力,下面我們就介紹一下Java環境下如何實現UDP網絡傳輸。
8.3.11 什麼是Datagram
所謂數據報(Datagram)就跟日常生活中的郵件系統一樣,是不能保證可靠的寄到的,而面向鏈接的TCP就好比電話,雙方能肯定對方接受到了信息。在本章前面,我們已經對UDP和TCP進行了比較,在這裏再稍作小節:
TCP,可靠,傳輸大小無限制,但是需要連接建立時間,差錯控制開銷大。
UDP,不可靠,差錯控制開銷較小,傳輸大小限制在64K以下,不需要建立連接。
總之,這兩種協議各有特點,應用的場合也不同,是完全互補的兩個協議,在TCP/IP協議中佔有同樣重要的地位,要學好網絡編程,兩者缺一不可。
8.3.12 Datagram通訊的表示方法:DatagramSocket;DatagramPacket
包java.net中提供了兩個類DatagramSocket和DatagramPacket用來支持數據報通信,DatagramSocket用於在程序之間建立傳送數據報的通信連接, DatagramPacket則用來表示一個數據報。先來看一下DatagramSocket的構造方法:
DatagramSocket();
DatagramSocket(int prot);
DatagramSocket(int port, InetAddress laddr)
其中,port指明socket所使用的端口號,如果未指明端口號,則把socket連接到本地主機上一個可用的端口。laddr指明一個可用的本地地址。給出端口號時要保證不發生端口衝突,否則會生成SocketException類例外。注意:上述的兩個構造方法都聲明拋棄非運行時例外SocketException,程序中必須進行處理,或者捕獲、或者聲明拋棄。
用數據報方式編寫client/server程序時,無論在客戶方還是服務方,首先都要建立一個DatagramSocket對象,用來接收或發送數據報,然後使用DatagramPacket類對象作爲傳輸數據的載體。下面看一下DatagramPacket的構造方法 :
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中存放數據報數據,length爲數據報中數據的長度,addr和port旨明目的地址,offset指明瞭數據報的位移量。
在接收數據前,應該採用上面的第一種方法生成一個DatagramPacket對象,給出接收數據的緩衝區及其長度。然後調用DatagramSocket 的方法receive()等待數據報的到來,receive()將一直等待,直到收到一個數據報爲止。
DatagramPacket packet=new DatagramPacket(buf, 256);
Socket.receive (packet);
發送數據前,也要先生成一個新的DatagramPacket對象,這時要使用上面的第二種構造方法,在給出存放發送數據的緩衝區的同時,還要給出完整的目的地址,包括IP地址和端口號。發送數據是通過DatagramSocket的方法send()實現的,send()根據數據報的目的地址來尋徑,以傳遞數據報。
DatagramPacket packet=new DatagramPacket(buf, length, address, port);
Socket.send(packet);
在構造數據報時,要給出InetAddress類參數。類InetAddress在包java.net中定義,用來表示一個Internet地址,我們可以通過它提供的類方法getByName()從一個表示主機名的字符串獲取該主機的IP地址,然後再獲取相應的地址信息。
8.3.13 基於UDP的簡單的Client/Server程序設計
有了上面的知識,我們就可以來構件一個基於UDP的C/S 網絡傳輸模型
1. 客戶方程序 QuoteClient.java
import java.io.*;
import java.net.*;
import java.util.*;
public class QuoteClient {
public static void main(String[] args) throws IOException
{
if(args.length!=1) {
//如果啓動的時候沒有給出Server的名字,那麼出錯退出
System.out.println("Usage:java QuoteClient <hostname>");
//打印出錯信息
return; //返回
}
DatagramSocket socket=new DatagramSocklet();
//創建數據報套接字
Byte[] buf=new byte[256]; //創建緩衝區
InetAddress address=InetAddress.getByName(args [0]);
//由命令行給出的第一個參數默認爲Server的名字,通過它得到Server的IP信息
DatagramPacket packet=new DatagramPacket (buf, buf.length, address, 4445);
//創建DatagramPacket對象
socket.send(packet); //發送
packet=new DatagramPacket(buf,buf.length);
//創建新的DatagramPacket對象,用來接收數據報
socket.receive(packet); //接收
String received=new String(packet.getData());
//根據接收到的字節數組生成相應的字符串
System.out.println("Quote of the Moment:"+received );
//打印生成的字符串
socket.close(); //關閉套接口
}
}
2. 服務器方程序:QuoteServer.java
public class QuoteServer{
public static void main(String args[]) throws java.io.IOException
{
new QuoteServerThread().start();
//啓動一個QuoteServerThread線程
}
}
3. 程序QuoteServerThread.java
import java.io.*;
import java.net.*;
import java.util.*;
//服務器線程
public class QuoteServerThread extends Thread
{
protected DatagramSocket socket=null;
//記錄和本對象相關聯的DatagramSocket對象
protected BufferedReader in=null;
//用來讀文件的一個Reader
protected boolean moreQuotes=true;
//標誌變量,是否繼續操作
public QuoteServerThread() throws IOException {
//無參數的構造函數
this("QuoteServerThread");
//以QuoteServerThread爲默認值調用帶參數的構造函數
}
public QuoteServerThread(String name) throws IOException {
super(name); //調用父類的構造函數
socket=new DatagramSocket(4445);
//在端口4445創建數據報套接字
try{
in= new BufferedReader(new FileReader(" one-liners.txt"));
//打開一個文件,構造相應的BufferReader對象
}catch(FileNotFoundException e) { //異常處理
System.err.println("Could not open quote file. Serving time instead.");
//打印出錯信息
}
}
public void run() //線程主體
{
while(moreQuotes) {
try{
byte[] buf=new byte[256]; //創建緩衝區
DatagramPacket packet=new DatagramPacket(buf,buf.length);
//由緩衝區構造DatagramPacket對象
socket.receive(packet); //接收數據報
String dString=null;
if(in= =null) dString=new Date().toString();
//如果初始化的時候打開文件失敗了,
//則使用日期作爲要傳送的字符串
else dString=getNextQuote();
//否則調用成員函數從文件中讀出字符串
buf=dString.getByte();
//把String轉換成字節數組,以便傳送
InetAddress address=packet.getAddress();
//從Client端傳來的Packet中得到Client地址
int port=packet.getPort(); //和端口號
packet=new DatagramPacket(buf,buf.length,address,port);
//根據客戶端信息構建DatagramPacket
socket.send(packet); //發送數據報
}catch(IOException e) { //異常處理
e.printStackTrace(); //打印錯誤棧
moreQuotes=false; //標誌變量置false,以結束循環
}
}
socket.close(); //關閉數據報套接字
}
protected String getNextQuotes(){
//成員函數,從文件中讀數據
String returnValue=null;
try {
if((returnValue=in.readLine())= =null) {
//從文件中讀一行,如果讀到了文件尾
in.close( ); //關閉輸入流
moreQuotes=false;
//標誌變量置false,以結束循環
returnValue="No more quotes. Goodbye.";
//置返回值
} //否則返回字符串即爲從文件讀出的字符串
}catch(IOEception e) { //異常處理
returnValue="IOException occurred in server";
//置異常返回值
}
return returnValue; //返回字符串
}
}
可以看出使用UDP和使用TCP在程序上還是有很大的區別的。一個比較明顯的區別是,UDP的Socket編程是不提供監聽功能的,也就是說通信雙方更爲平等,面對的接口是完全一樣的。但是爲了用UDP實現C/S結構,在使用UDP時可以使用DatagramSocket.receive()來實現類似於監聽的功能。因爲receive()是阻塞的函數,當它返回時,緩衝區裏已經填滿了接受到的一個數據報,並且可以從該數據報得到發送方的各種信息,這一點跟accept()是很相象的,因而可以根據讀入的數據報來決定下一步的動作,這就達到了跟網絡監聽相似的效果。
8.3.14 用數據報進行廣播通訊
DatagramSocket只允許數據報發送一個目的地址,java.net包中提供了一個類MulticastSocket,允許數據報以廣播方式發送到該端口的所有客戶。MulticastSocket用在客戶端,監聽服務器廣播來的數據。
我們對上面的程序作一些修改,利用MulticastSocket實現廣播通信。新程序完成的功能是使同時運行的多個客戶程序能夠接收到服務器發送來的相同的信息,顯示在各自的屏幕上。
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);
//創建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];
//創建緩衝區
packet=new DatagramPacket(buf,buf.length);
//創建接收數據報
socket.receive(packet); //接收
String received=new String(packet.getData());
//由接收到的數據報得到字節數組,
//並由此構造一個String對象
System.out.println("Quote of theMoment:"+received);
//打印得到的字符串
} //循環5次
socket.leaveGroup(address);
//把廣播套接字從地址上解除綁定
socket.close(); //關閉廣播套接字
}
}
2. 服務器方程序:MulticastServer.java
public class MulticastServer{
public static void main(String args[]) throws java.io.IOException
{
new MulticastServerThread().start();
//啓動一個服務器線程
}
}
3. 程序MulticastServerThread.java
import java.io.*;
import java.net.*;
import java.util.*;
public class MulticastServerThread extends QuoteServerThread
//從QuoteServerThread繼承得到新的服務器線程類MulticastServerThread
{
Private long FIVE_SECOND=5000; //定義常量,5秒鐘
public MulticastServerThread(String name) throws IOException
{
super("MulticastServerThread");
//調用父類,也就是QuoteServerThread的構造函數
}
public void run() //重寫父類的線程主體
{
while(moreQuotes) {
//根據標誌變量判斷是否繼續循環
try{
byte[] buf=new byte[256];
//創建緩衝區
String dString=null;
if(in==null) dString=new Date().toString();
//如果初始化的時候打開文件失敗了,
//則使用日期作爲要傳送的字符串
else dString=getNextQuote();
//否則調用成員函數從文件中讀出字符串
buf=dString.getByte();
//把String轉換成字節數組,以便傳送send it
InetAddress group=InetAddress.getByName("230.0.0.1");
//得到230.0.0.1的地址信息
DatagramPacket packet=new DatagramPacket(buf,buf.length,group,4446);
//根據緩衝區,廣播地址,和端口號創建DatagramPacket對象
socket.send(packet); //發送該Packet
try{
sleep((long)(Math.random()*FIVE_SECONDS));
//隨機等待一段時間,0~5秒之間
}catch(InterruptedException e) { } //異常處理
}catch(IOException e){ //異常處理
e.printStackTrace( ); //打印錯誤棧
moreQuotes=false; //置結束循環標誌
}
}
socket.close( ); //關閉廣播套接口
}
}
至此,Java網絡編程這一章已經講解完畢。讀者通過學習,應該對網絡編程有了一個清晰的認識,可能對某些概念還不是十分的清楚,還是需要更多的實踐來進一步掌握。編程語言的學習不同於一般的學習,及其強調實踐的重要性。讀者應該對URL網絡編程,Socket中的TCP,UDP編程進行大量的練習才能更好的掌握本章中所提到的一些概念,才能真正學到Java網絡編程的精髓!
最後幾個小節所舉的例子,讀者務必要親自試驗一下,如果遇到問題,想辦法解決之。最好能根據自己的意圖加以改進。這樣才能更好的理解這幾個程序,理解其中所包含的編程思想。
【本講小結】
本講主要講解了Java環境下的網絡編程。因爲TCP/IP協議是Java網絡編程的基礎知識,本講開篇重點介紹了TCP/IP協議中的一些概念,TCP/IP協議本身是一個十分龐大的系統,用幾個小節是不可能講清楚的。所以我們只是聯繫實際,講解了一些最基本的概念,幫助學生理解後面的相關內容。重點有一下幾個概念:主機名,IP,端口,服務類型,TCP,UDP。
後續的內容分爲兩大塊,一塊是以URL爲主線,講解如何通過URL類和URLConnection類訪問WWW網絡資源,由於使用URL十分方便直觀,儘管功能不是很強,還是值得推薦的一種網絡編程方法,尤其是對於初學者特別容易接受。本質上講,URL網絡編程在傳輸層使用的還是TCP協議。
另一塊是以Socket接口和C/S網絡編程模型爲主線,依次講解了如何用Java實現基於TCP的C/S結構,主要用到的類有Socket,ServerSocket。以及如何用Java實現基於UDP的C/S結構,還討論了一種特殊的傳輸方式,廣播方式,這種方式是UDP所特有的,主要用到的類有DatagramSocket , DatagramPacket, MulticastSocket。這一塊在Java網絡編程中相對而言是最難的(儘管Java在網絡編程這方面已經做的夠"傻瓜"了,但是網絡編程在其他環境下的卻是一件極爲頭痛的事情,再"傻瓜"還是有一定的難度),也是功能最爲強大的一部分,讀者應該好好研究,領悟其中的思想。
最後要強調的是要學好Java網絡編程,Java語言,最重要的還是在於多多練習!