自尋址套接字(Datagram Sockets)
,因為使用流套接字的每個連接均要花費一定的時間,要減少這種開銷,網絡API提供了第二種套接字:自尋址套接字(datagram socket),自尋址使用UDP發送尋址信息(從客戶程序到服務程序或從服務程序到客戶程序),不同的是可以通過自尋址套接字發送多IP信息包,自尋址信息包含在自尋址包中,此外自尋址包又包含在IP包內,這就將尋址信息長度限制在60000字節內。圖2顯示了位於IP包內的自尋址包的自尋址信息。
與TCP保證信息到達信息目的地的方式不同,UDP提供了另外一種方法,如果自尋址信息包沒有到達目的地,,那麼UDP也不會請求發送者重新發送自尋址包,這是因為UDP在每一個自尋址包中包含了錯誤檢測信息,在每個自尋址包到達目的地之後UDP只進行簡單的錯誤檢查,如果檢測失敗,UDP將拋棄這個自尋址包,也不會從發送者那裡重新請求替代者,這與通過郵局發送信件相似,發信人在發信之前不需要與收信人建立連接,同樣也不能保證信件能到達收信人那裡
自尋址套接字工作包括下面三個類:DatagramPacket, DatagramSocket,和 MulticastSocket。DatagramPacket對象描繪了自尋址包的地址信息,DatagramSocket表示客戶程序和服務程序自尋址套接字,MulticastSocket描繪了能進行多點傳送的自尋址套接字,這三個類均位於java.net包內。
DatagramPacket類
在使用自尋址包之前,你需要首先熟悉DatagramPacket類,地址信息和自尋址包以字節數組的方式同時壓縮入這個類創建的對象中
DatagramPacket有數個構造函數,即使這些構造函數的形式不同,但通常情況下他們都有兩個共同的參數:byte [] buffer 和 int length,buffer參數包含了一個對保存自尋址數據包信息的字節數組的引用,length表示字節數組的長度。
最簡單的構造函數是DatagramPacket(byte [] buffer, int length),這個構造函數確定了自尋址數據包數組和數組的長度,但沒有任何自尋址數據包的地址和端口信息,這些信息可以後面通過調用方法setAddress(InetAddress addr)和setPort(int port)添加上,下面的代碼示范了這些函數和方法。
byte [] buffer = new byte [100];
DatagramPacket dgp = new DatagramPacket (buffer, buffer.length);
InetAddress ia = InetAddress.getByName ("www.disney.com");
dgp.setAddress (ia);
dgp.setPort (6000); // Send datagram packet to port 6000.
如果你更喜歡在調用構造函數的時候同時包括地址和端口號,可以使用DatagramPacket(byte [] buffer, int length, InetAddress addr, int port)函數,下面的代碼示范了另外一種選擇。
byte [] buffer = new byte [100];
InetAddress ia = InetAddress.getByName ("www.disney.com");
DatagramPacket dgp = new DatagramPacket (buffer, buffer.length, ia,
6000);
有時候在創建了DatagramPacket對象後想改變字節數組和他的長度,這時可以通過調用setData(byte [] buffer) 和 setLength(int length)方法來實現。在任何時候都可以通過調用getData() 來得到字節數組的引用,通過調用getLength()來獲得字節數組的長度。下面的代碼示范了這些方法:
byte [] buffer2 = new byte [256];
dgp.setData (buffer2);
dgp.setLength (buffer2.length);
關於DatagramPacket的更多信息請參考SDK文檔。
DatagramSocket類
DatagramSocket類在客戶端創建自尋址套接字與服務器端進行通信連接,並發送和接受自尋址套接字。雖然有多個構造函數可供選擇,但我發現創建客戶端自尋址套接字最便利的選擇是DatagramSocket()函數,而服務器端則是DatagramSocket(int port)函數,如果未能創建自尋址套接字或綁定自尋址套接字到本地端口,那麼這兩個函數都將拋出一個SocketException對象,一旦程序創建了DatagramSocket對象,那麼程序分別調用send(DatagramPacket dgp)和 receive(DatagramPacket dgp)來發送和接收自尋址數據包,
List4顯示的DGSClient源代碼示范了如何創建自尋址套接字以及如何通過套接字處理發送和接收信息
Listing 4: DGSClient.java
// DGSClient.java
import java.io.*;
import java.net.*;
class DGSClient
{
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 == 1)
host = args [0];
DatagramSocket s = null;
try
{
// Create a datagram socket bound to an arbitrary port.
s = new DatagramSocket ();
// Create a byte array that will hold the data portion of a
// datagram packet's message. That message originates as a
// String object, which gets converted to a sequence of
// bytes when String's getBytes() method is called. The
// conversion uses the platform's default character set.
byte [] buffer;
buffer = new String ("Send me a datagram").getBytes ();
// Convert the name of the host to an InetAddress object.
// That object contains the IP address of the host and is
// used by DatagramPacket.
InetAddress ia = InetAddress.getByName (host);
// Create a DatagramPacket object that encapsulates a
// reference to the byte array and destination address
// information. The destination address consists of the
// host's IP address (as stored in the InetAddress object)
// and port number 10000 -- the port on which the server
// program listens.
DatagramPacket dgp = new DatagramPacket (buffer,
buffer.length,
ia,
10000);
// Send the datagram packet over the socket.
s.send (dgp);
// Create a byte array to hold the response from the server.
// program.
byte [] buffer2 = new byte [100];
// Create a DatagramPacket object that specifies a buffer
// to hold the server program's response, the IP address of
// the server program's computer, and port number 10000.
dgp = new DatagramPacket (buffer2,
buffer.length,
ia,
10000);
// Receive a datagram packet over the socket.
s.receive (dgp);
// Print the data returned from the server program and stored
// in the datagram packet.
System.out.println (new String (dgp.getData ()));
}
catch (IOException e)
{
System.out.println (e.toString ());
}
finally
{
if (s != null)
s.close ();
}
}
}
DGSClient由創建一個綁定任意本地(客戶端)端口好的DatagramSocket對象開始,然後裝入帶有文本信息的數組buffer和描述服務器主機IP地址的InetAddress子類對象的引用,接下來,DGSClient創建了一個DatagramPacket對象,該對象加入了帶文本信息的緩沖器的引用,InetAddress子類對象的引用,以及服務端口號10000, DatagramPacket的自尋址數據包通過方法sent()發送給服務器程序,於是一個包含服務程序響應的新的DatagramPacket對象被創建,receive()得到響應的自尋址數據包,然後自尋址數據包的getData()方法返回該自尋址數據包的一個引用,最後關閉DatagramSocket。
DGSServer服務程序補充了DGSClient的不足,List5是DGSServer的源代碼:
Listing 5: DGSServer.java
// DGSServer.java
import java.io.*;
import java.net.*;
class DGSServer
{
public static void main (String [] args) throws IOException
{
System.out.println ("Server starting ...\n");
// Create a datagram socket bound to port 10000. Datagram
// packets sent from client programs arrive at this port.
DatagramSocket s = new DatagramSocket (10000);
// Create a byte array to hold data contents of datagram
// packet.
byte [] data = new byte [100];
// Create a DatagramPacket object that encapsulates a reference
// to the byte array and destination address information. The
// DatagramPacket object is not initialized to an address
// because it obtains that address from the client program.
DatagramPacket dgp = new DatagramPacket (data, data.length);
// Enter an infinite loop. Press Ctrl+C to terminate program.
while (true)
{
// Receive a datagram packet from the client program.
s.receive (dgp);
// Display contents of datagram packet.
System.out.println (new String (data));
// Echo datagram packet back to client program.
s.send (dgp);
}
}
}
DGSServer創建了一個綁定端口10000的自尋址套接字,然後創建一個字節數組容納自尋址信息,並創建自尋址包,下一步,DGSServer進入一個無限循環中以接收自尋址數據包、顯示內容並將響應返回客戶端,自尋址套接沒有關閉,因為循環是無限的。
在編譯DGSServer 和DGSClient的源代碼後,由輸入java DGSServer開始運行DGSServer,然後在同一主機上輸入Java DGSClient開始運行DGSClient,如果DGSServer與DGSClient運行於不同主機,在輸入時注意要在命令行加上服務程序的主機名或IP地址,如:java DGSClient www.yesky.com
多點傳送和MulticastSocket類
前面的例子顯示了服務器程序線程發送單一的消息(通過流套接字或自尋址套接字)給唯一的客戶端程序,這種行為被稱為單點傳送(unicasting),多數情況都不適合於單點傳送,比如,搖滾歌手舉辦一場音樂會將通過互聯網進行播放,畫面和聲音的質量依賴於傳輸速度,服務器程序要傳送大約10億字節的數據給客戶端程序,使用單點傳送,那麼每個客戶程序都要要復制一份數據,如果,互聯網上有10000個客戶端要收看這個音樂會,那麼服務器程序通過Internet要傳送10000G的數據,這必然導致網絡阻塞,降低網絡的傳輸速度。
如果服務器程序要將同一信息發送給多個客戶端,那麼服務器程序和客戶程序可以利用多點傳送(multicasting)方式進行通信。多點傳送就是服務程序對專用的多點傳送組的IP地址和端口發送一系列自尋址數據包,通過加入操作IP地址被多點傳送Socket注冊,通過這個點客戶程序可以接收發送給組的自尋址包(同樣客戶程序也可以給這個組發送自尋址包),一旦客戶程序讀完所有要讀的自尋址數據包,那麼可以通過離開組操作離開多點傳送組。
注意:IP地址224.0.0.1 到 239.255.255.255(包括)均為保留的多點傳送組地址。
網絡API通過MulticastSocket類和MulticastSocket,以及一些輔助類(比如NetworkInterface)支持多點傳送,當一個客戶程序要加入多點傳送組時,就創建一個MulticastSocket對象。MulticastSocket(int port)構造函數允許應用程序指定端口(通過port參數)接收自尋址包,端口必須與服務程序的端口號相匹配,要加入多點傳送組,客戶程序調用兩個joinGroup()方法中的一個,同樣要離開傳送組,也要調用兩個leaveGroup()方法中的一個。
由於MulticastSocket擴展了DatagramSocket類,一個MulticastSocket對象就有權訪問DatagramSocket方法。
List6是MCClient的源代碼,這段代碼示范了一個客戶端加入多點傳送組的例子。
Listing 6: MCClient.java
// MCClient.java
import java.io.*;
import java.net.*;
class MCClient
{
public static void main (String [] args) throws IOException
{
// Create a MulticastSocket bound to local port 10000. All
// multicast packets from the server program are received
// on that port.
MulticastSocket s = new MulticastSocket (10000);
// Obtain an InetAddress object that contains the multicast
// group address 231.0.0.1. The InetAddress object is used by
// DatagramPacket.
InetAddress group = InetAddress.getByName ("231.0.0.1");
// Join the multicast group so that datagram packets can be
// received.
s.joinGroup (group);
// Read several datagram packets from the server program.
for (int i = 0; i < 10; i++)
{
// No line will exceed 256 bytes.
byte [] buffer = new byte [256];
// The DatagramPacket object needs no addressing
// information because the socket contains the address.
DatagramPacket dgp = new DatagramPacket (buffer,
buffer.length);
// Receive a datagram packet.
s.receive (dgp);
// Create a second byte array with a length that matches
// the length of the sent data.
byte [] buffer2 = new byte [dgp.getLength ()];
// Copy the sent data to the second byte array.
System.arraycopy (dgp.getData (),
0,
buffer2,
0,
dgp.getLength ());
// Print the contents of the second byte array. (Try
// printing the contents of buffer. You will soon see why
// buffer2 is used.)
System.out.println (new String (buffer2));
}
// Leave the multicast group.
s.leaveGroup (group);
// Close the socket.
s.close ();
}
}
MCClient創建了一個綁定端口號10000的MulticastSocket對象,接下來他獲得了一個InetAddress子類對象,該子類對象包含多點傳送組的IP地址231.0.0.0,然後通過joinGroup(InetAddress addr)方法加入多點傳送組中,接下來MCClient接收10個自尋址包,同時輸出他們的內容,然後使用leaveGroup(InetAddress addr)方法離開傳送組,最後關閉套接字。
也許你對使用兩個字節數組buffer 和 buffer2感到奇怪,當接收到一個自尋址包後,getData()方法返回一個引用,自尋址包的長度是256個字節,如果要輸出所有數據,在輸出完實際數據後會有很多空格,這顯然是不合理的,所以我們必須去掉這些空格,因此我們創建一個小的字節數組buffer2,buffer2的實際長度就是數據的實際長度,通過調用DatagramPacket's getLength()方法來得到這個長度。從buffer 到 buffer2快速復制getLength()的長度的方法是調用System.arraycopy()方法。
List7 MCServer的源代碼顯示了服務程序是怎樣工作的。
Listing 7: MCServer.java
// MCServer.java
import java.io.*;
import java.net.*;
class MCServer
{
public static void main (String[] args) throws IOException
{
System.out.println ("Server starting...\n");
// Create a MulticastSocket not bound to any port.
MulticastSocket s = new MulticastSocket ();
// Because MulticastSocket subclasses DatagramSocket, it is
// legal to replace MulticastSocket s = new MulticastSocket ();
// with the following line.
// DatagramSocket s = new DatagramSocket ();
// Obtain an InetAddress object that contains the multicast
// group address 231.0.0.1. The InetAddress object is used by
// DatagramPacket.
InetAddress group = InetAddress.getByName ("231.0.0.1");
// Create a DatagramPacket object that encapsulates a reference
// to a byte array (later) and destination address
// information. The destination address consists of the
// multicast group address (as stored in the InetAddress object)
// and port number 10000 -- the port to which multicast datagram
// packets are sent. (Note: The dummy array is used to prevent a
// NullPointerException object being thrown from the
// DatagramPacket constructor.)
byte [] dummy = new byte [0];
DatagramPacket dgp = new DatagramPacket (dummy,
0,
group,
10000);
// Send 30000 Strings to the port.
for (int i = 0; i < 30000; i++)
{
// Create an array of bytes from a String. The platform's
// default character set is used to convert from Unicode
// characters to bytes.
byte [] buffer = ("Video line " + i).getBytes ();
// Establish the byte array as the datagram packet's
// buffer.
dgp.setData (buffer);
// Establish the byte array's length as the length of the
// datagram packet's buffer.
dgp.setLength (buffer.length);
// Send the datagram to all members of the multicast group
// that listen on port 10000.
s.send (dgp);
}
// Close the socket.
s.close ();
}
}
MCServer創建了一個MulticastSocket對象,由於他是DatagramPacket對象的一部分,所以他沒有綁定端口號,DatagramPacket有多點傳送組的IP地址(231.0.0.0),一旦創建DatagramPacket對象,MCServer就進入一個發送30000條的文本的循環中,對文本的每一行均要創建一個字節數組,他們的引用均存儲在前面創建的DatagramPacket對象中,通過send()方法,自尋址包發送給所有的組成員。
在編譯了MCServer 和 MCClient後,通過輸入java MCServer開始運行MCServer,最後再運行一個或多個MCClient。
結論
本文通過研究套接字揭示了Java的網絡API的應用方法,我們介紹了套接自的慨念和套接字的組成,以及流套接字和自尋址套接字,以及如何使用InetAddress, Socket, ServerSocket, DatagramPacket, DatagramSocket和MulticastSocket類。在完成本文後就可以編寫基本的底層通訊程序。