1. 什麼是TCP/ IP協議?
2. TCP/IP有哪兩種傳輸協議,各有什麼特點?
3. 什麼是URL?
4. URL和IP地址有什麼樣的關係?
5. 什麼叫套接字(Socket)?
6. 套接字(Socket)和TCP/IP協議的關係?
7. URL和套接字(Socket)的關係?
8.1 網絡編程基本概念,TCP/IP協議簡介
目前較爲流行的網絡編程模型是客戶機/服務器(C/S)結構。即通信雙方一方作爲服務器等待客戶提出請求並予以響應。客戶則在需要服務時向服務器提出申請。服務器一般作爲守護進程始終運行,監聽網絡端口,一旦有客戶請求,就會啓動一個服務進程來響應該客戶,同時自己繼續監聽服務端口,使後來的客戶也能及時得到服務。
儘管TCP/IP協議的名稱中只有TCP這個協議名,但是在TCP/IP的傳輸層同時存在TCP和UDP兩個協議。
UDP是User Datagram Protocol的簡稱,是一種無連接的協議,每個數據報都是一個獨立的信息,包括完整的源地址或目的地址,它在網絡上以任何可能的路徑傳往目的地,因此能否到達目的地,到達目的地的時間以及內容的正確性都是不能被保證的。
下面我們對這兩種協議做簡單比較:
使用UDP時,每個數據報中都給出了完整的地址信息,因此無需要建立發送方和接收方的連接。對於TCP協議,由於它是一個面向連接的協議,在socket之間進行數據傳輸之前必然要建立連接,所以在TCP中多了一個連接建立的時間。
使用UDP傳輸數據時是有大小限制的,每個被傳輸的數據報必須限定在64KB之內。而TCP沒有這方面的限制,一旦連接建立起來,雙方的socket就可以按統一的格式傳輸大量的數據。UDP是一個不可靠的協議,發送方所發送的數據報並不一定以相同的次序到達接收方。而TCP是一個可靠的協議,它確保接收方完全正確地獲取發送方所發送的全部數據。
總之,TCP在網絡通信上有極強的生命力,例如遠程連接(Telnet)和文件傳輸(FTP)都需要不定長度的數據被可靠地傳輸。相比之下UDP操作簡單,而且僅需要較少的監護,因此通常用於局域網高可靠性的分散系統中client/server應用程序。
讀者可能要問,既然有了保證可靠傳輸的TCP協議,爲什麼還要非可靠傳輸的UDP協議呢?主要的原因有兩個。一是可靠的傳輸是要付出代價的,對數據內容正確性的檢驗必然佔用計算機的處理時間和網絡的帶寬,因此TCP傳輸的效率不如UDP高。二是在許多應用中並不需要保證嚴格的傳輸可靠性,比如視頻會議系統,並不要求音頻視頻數據絕對的正確,只要保證連貫性就可以了,這種情況下顯然使用UDP會更合理一些。
協議名(protocol)指明獲取資源所使用的傳輸協議,如http、ftp、gopher、file等,資源名(resourceName)則應該是資源的完整地址,包括主機名、端口號、文件名或文件內部的一個引用。例如:
http://www.sun.com/ 協議名://主機名
http://home.netscape.com/home/welcome.html 協議名://機器名+文件名
http://www.gamelan.com:80/Gamelan/network.html#BOTTOM 協議名://機器名+端口號+文件名+內部引用.
(1) public URL (String spec);
通過一個表示URL地址的字符串可以構造一個URL對象。
URL urlBase=new URL("http://www. 263.net/")
(2) public URL(URL context, String spec);
通過基URL和相對URL構造一個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 myURL= new URL(…)
}catch (MalformedURLException e){
… }
public String getProtocol() 獲取該URL的協議名。
public String getHost() 獲取該URL的主機名。
public int getPort() 獲取該URL的端口號,如果沒有設置端口,返回-1。
public String getFile() 獲取該URL的文件名。
public String getRef() 獲取該URL在文件中的相對位置。
public String getQuery() 獲取該URL的查詢信息。
public String getPath() 獲取該URL的路徑
public String getAuthority() 獲取該URL的權限信息
public String getUserInfo() 獲得使用者的信息
public String getRef() 獲得該URL的錨
InputStream openStream();
方法openSteam()與指定的URL建立連接並返回InputStream類的對象以從這一連接中讀取數據。
public class URLReader {
public static void main(String[] args) throws Exception {
//聲明拋出所有例外
URL tirc = new URL("http://www.tirc1.cs.tsinghua.edu.cn/");
//構建一URL對象
BufferedReader in = new BufferedReader(new InputStreamReader(tirc.openStream()));
//使用openStream得到一輸入流並由此構造一個BufferedReader對象
String inputLine;
while ((inputLine = in.readLine()) != null)
//從輸入流不斷的讀數據,直到讀完爲止
System.out.println(inputLine); //把讀入的數據打印到屏幕上
in.close(); //關閉輸入流
}
}
類URLConnection也在包java.net中定義,它表示Java程序和URL在網絡上的通信連接。當與一個URL建立連接時,首先要在一個URL對象上通過方法openConnection()生成對應的URLConnection對象。例如下面的程序段首先生成一個指向地址http://edu.chinaren.com/index.shtml的對象,然後用openConnection()打開該URL對象上的一個連接,返回一個URLConnection對象。如果連接過程失敗,將產生IOException.
Try{
URL netchinaren = new URL ("http://edu.chinaren.com/index.shtml");
URLConnectonn tc = netchinaren.openConnection();
}catch(MalformedURLException e){ //創建URL()對象失敗
…
}catch (IOException e){ //openConnection()失敗
…
}
類URLConnection提供了很多方法來設置或獲取連接參數,程序設計時最常使用的是getInputStream()和getOurputStream(),其定義爲:
InputSteram getInputSteram();
OutputSteram getOutputStream();
通過返回的輸入/輸出流我們可以與遠程對象進行通信。看下面的例子:
URL url =new URL ("http://www.javasoft.com/cgi-bin/backwards");
//創建一URL對象
URLConnectin con=url.openConnection();
//由URL對象獲取URLConnection對象
DataInputStream dis=new DataInputStream (con.getInputSteam());
//由URLConnection獲取輸入流,並構造DataInputStream對象
PrintStream ps=new PrintSteam(con.getOutupSteam());
//由URLConnection獲取輸出流,並構造PrintStream對象
String line=dis.readLine(); //從服務器讀入一行
ps.println("client…"); //向服務器寫出字符串 "client…"
其中backwards爲服務器端的CGI程序。實際上,類URL的方法openSteam()是通過URLConnection來實現的。它等價於
openConnection().getInputStream();
基於URL的網絡編程在底層其實還是基於下面要講的Socket接口的。WWW,FTP等標準化的網絡服務都是基於TCP協議的,所以本質上講URL編程也是基於TCP的一種應用.
在傳統的UNIX環境下可以操作TCP/IP協議的接口不止Socket一個,Socket所支持的協議種類也不光TCP/IP一種,因此兩者之間是沒有必然聯繫的。在Java環境下,Socket編程主要是指基於TCP/IP協議的網絡編程。
(1)創建Socket;
(2)打開連接到Socket的輸入/出流;
(3)按照一定的協議對Socket進行讀/寫操作;
(4)關閉Socket.
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地址、主機名和端口號,stream指明socket是流socket還是數據報socket,localPort表示本地主機的端口號,localAddr和bindAddr是本地機器的地址(ServerSocket的主機地址),impl是socket的父類,既可以用來創建serverSocket又可以用來創建Socket。count則表示服務端所能支持的最大連接數。例如:
Socket client = new Socket("127.0.01.", 80);
ServerSocket server = new ServerSocket(80);
注意,在選擇端口時,必須小心。每一個端口提供一種特定的服務,只有給出正確的端口,才能獲得相應的服務。0~1023的端口號爲系統所保留,例如http服務的端口號爲80,telnet服務的端口號爲21,ftp服務的端口號爲23, 所以我們在選擇端口號時,最好選擇一個大於1023的數以防止發生衝突。
在創建socket時如果發生錯誤,將產生IOException,在程序中必須對之作出處理。所以在創建Socket或ServerSocket是必須捕獲或拋出例外。
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);
//向本機的4700端口發出客戶請求
BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
//由系統標準輸入設備構造BufferedReader對象
PrintWriter os=new PrintWriter(socket.getOutputStream());
//由Socket對象得到輸出流,並構造PrintWriter對象
BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//由Socket對象得到輸入流,並構造相應的BufferedReader對象
String readline;
readline=sin.readLine(); //從系統標準輸入讀入一字符串
while(!readline.equals("bye")){
//若從標準輸入讀入的字符串爲 "bye"則停止循環
os.println(readline);
//將從系統標準輸入讀入的字符串輸出到Server
os.flush();
//刷新輸出流,使Server馬上收到該字符串
System.out.println("Client:"+readline);
//在系統標準輸出上打印讀入的字符串
System.out.println("Server:"+is.readLine());
//從Server讀入一字符串,並打印到標準輸出上
readline=sin.readLine(); //從系統標準輸入讀入一字符串
} //繼續循環
os.close(); //關閉Socket輸出流
is.close(); //關閉Socket輸入流
socket.close(); //關閉Socket
}catch(Exception e) {
System.out.println("Error"+e); //出錯,則打印出錯信息
}
}
}
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);
//創建一個ServerSocket在端口4700監聽客戶請求
}catch(Exception e) {
System.out.println("can not listen to:"+e);
//出錯,打印出錯信息
}
try{
socket=server.accept();
//使用accept()阻塞等待客戶請求,有客戶
//請求到來則產生一個Socket對象,並繼續執行
}catch(Exception e) {
System.out.println("Error."+e);
//出錯,打印出錯信息
}
String line;
BufferedReader is=new BufferedReader(new InputStreamReader(socket.getInputStream()));
//由Socket對象得到輸入流,並構造相應的BufferedReader對象
PrintWriter os=newPrintWriter(socket.getOutputStream());
//由Socket對象得到輸出流,並構造PrintWriter對象
BufferedReader sin=new BufferedReader(new InputStreamReader(System.in));
//由系統標準輸入設備構造BufferedReader對象
//在標準輸出上打印從客戶端讀入的字符串
line=sin.readLine();
//從標準輸入讀入一字符串
while(!line.equals("bye")){
//如果該字符串爲 "bye",則停止循環
os.println(line);
//向客戶端輸出該字符串
os.flush();
//刷新輸出流,使Client馬上收到該字符串
System.out.println("Server:"+line);
//在系統標準輸出上打印讀入的字符串
System.out.println("Client:"+is.readLine());
//從Client讀入一字符串,並打印到標準輸出上
line=sin.readLine();
//從系統標準輸入讀入一字符串
} //繼續循環
os.close(); //關閉Socket輸出流
is.close(); //關閉Socket輸入流
socket.close(); //關閉Socket
server.close(); //關閉ServerSocket
}catch(Exception e){
System.out.println("Error:"+e);
//出錯,打印出錯信息
}
}
}
boolean listening=true;
try{
serverSocket=new ServerSocket(4700);
//創建一個ServerSocket在端口4700監聽客戶請求
}catch(IOException e) { }
while(listening){ //永遠循環監聽
new ServerThread(serverSocket.accept(),clientnum).start();
//監聽到客戶請求,根據得到的Socket對象和
客戶計數創建服務線程,並啓動之
clientnum++; //增加客戶計數
}
serverSocket.close(); //關閉ServerSocket
Socket socket=null; //保存與本線程相關的Socket對象
int clientnum; //保存本進程的客戶計數
public ServerThread(Socket socket,int num) { //構造函數
this.socket=socket; //初始化socket變量
clientnum=num+1; //初始化clientnum變量
}
public void run() { //線程主體
try{//在這裏實現數據的接受和發送}
TCP,可靠,傳輸大小無限制,但是需要連接建立時間,差錯控制開銷大。
UDP,不可靠,差錯控制開銷較小,傳輸大小限制在64K以下,不需要建立連接。
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地址,然後再獲取相應的地址信息。
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;
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(); //關閉廣播套接字
}
}
public class MulticastServer{
public static void main(String args[]) throws java.io.IOException
{
new MulticastServerThread().start();
//啓動一個服務器線程
}
}
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的構造函數
}
{
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( ); //打印錯誤棧
}
}
socket.close( ); //關閉廣播套接口
}
}
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Date;
public class QuoteServerThread extends Thread {
protected DatagramSocket socket = null;
protected BufferedReader in = null;
protected boolean moreQuotes = true;
// 無參數的?造函數
this("QuoteServerThread");
// 以QuoteServerThread?默???用?參數的?造函數
}
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.");
// 打印出?信息
}
}
{
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 = getNextQuotes();
// 否??用成?函數從文件中?出字符串
buf = dString.getBytes();
// 把String??成字?數?,以便?送
// 從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(); // ??數據?套接字
}
// 成?函數,從文件中?數據
String returnValue = null;
try {
if ((returnValue = in.readLine()) == null) {
// 從文件中?一行,如果?到了文件尾
in.close(); // ???入流
moreQuotes = false;
// ?志?量置false,以?束循?
returnValue = "No more quotes. Goodbye.";
// 置返回?
} // 否?返回字符串即?從文件?出的字符串
} catch (IOException e) { // ?常?理
returnValue = "IOException occurred in server";
// 置?常返回?
}
return returnValue; // 返回字符串
}
}
後續的內容分爲兩大塊,一塊是以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在網絡編程這方面已經做的夠"傻瓜"了,但是網絡編程在其他環境下的卻是一件極爲頭痛的事情,再"傻瓜"還是有一定的難度),也是功能最爲強大的一部分,讀者應該好好研究,領悟其中的思想。