1. 程式人生 > >網絡編程的基本概念,TCP/IP協議簡介

網絡編程的基本概念,TCP/IP協議簡介

cli 面向 red 展示 應用程序 隨著 完全 welcome 底層


8.1.1 網絡基礎知識

  計算機網絡形式多樣,內容繁雜。網絡上的計算機要互相通信,必須遵循一定的協議。目前使用最廣泛的網絡協議是Internet上所使用的TCP/IP協議。

網絡編程的目的就是指直接或間接地通過網絡協議與其他計算機進行通訊。網絡編程中有兩個主要的問題,一個是如何準確的定位網絡上一臺或多臺主機,另一個就是找到主機後如何可靠高效的進行數據傳輸。在TCP/IP協議中IP層主要負責網絡主機的定位,數據傳輸的路由,由IP地址可以唯一地確定Internet上的一臺主機。而TCP層則提供面向應用的可靠的或非可靠的數據傳輸機制,這是網絡編程的主要對象,一般不需要關心IP層是如何處理數據的。
 
  目前較為流行的網絡編程模型是客戶機/服務器(C/S)結構。即通信雙方一方作為服務器等待客戶提出請求並予以響應。客戶則在需要服務時向服務器提出申請。服務器一般作為守護進程始終運行,監聽網絡端口,一旦有客戶請求,就會啟動一個服務進程來響應該客戶,同時自己繼續監聽服務端口,使後來的客戶也能及時得到服務。

8.1.2網絡基本概念
IP地址:標識計算機等網絡設備的網絡地址,由四個8位的二進制數組成,中間以小數點分隔。
    如:166.111.136.3, 166.111.52.80

  主機名(hostname):網絡地址的助記名,按照域名進行分級管理。
    如:www.tsinghua.edu.cn
      www.fanso.com
  
  端口號(portnumber):網絡通信時同一機器上的不同進程的標識。
    如:80,21,23,25,其中1~1024為系統保留的端口號
  
  服務類型(service):網絡的各種服務。
    http,telnet, ftp, smtp
 
  我們可以用以下的一幅圖來描述這裏我們所提到的幾個概念:


  在Internet上IP地址和主機名是一一對應的,通過域名解析可以由主機名得到機器的IP,由於機器名更接近自然語言,容易記憶,所以使用比IP地址廣泛,但是對機器而言只有IP地址才是有效的標識符。

  通常一臺主機上總是有很多個進程需要網絡資源進行網絡通訊。網絡通訊的對象準確的講不是主機,而應該是主機中運行的進程。這時候光有主機名或IP地址來標識這麽多個進程顯然是不夠的。端口號就是為了在一臺主機上提供更多的網絡資源而采取得一種手段,也是TCP層提供的一種機制。只有通過主機名或IP地址和端口號的組合才能唯一的確定網絡通訊中的對象:進程。

服務類型是在TCP層上面的應用層的概念。基於TCP/IP協議可以構建出各種復雜的應用,服務類型是那些已經被標準化了的應用,一般都是網絡服務器(軟件)。讀者可以編寫自己的基於網絡的服務器,但都不能被稱作標準的服務類型。

8.1.3兩類傳輸協議:TCP;UDP


  盡管TCP/IP協議的名稱中只有TCP這個協議名,但是在TCP/IP的傳輸層同時存在TCP和UDP兩個協議。
TCP是Tranfer Control Protocol的簡稱,是一種面向連接的保證可靠傳輸的協議。通過TCP協議傳輸,得到的是一個順序的無差錯的數據流。發送方和接收方的成對的兩個socket之間必須建立連接,以便在TCP協議的基礎上進行通信,當一個socket(通常都是server socket)等待建立連接時,另一個socket可以要求進行連接,一旦這兩個socket連接起來,它們就可以進行雙向數據傳輸,雙方都可以進行發送或接收操作。

  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會更合理一些。

8.2 基於URL的高層次Java網絡編程

8.2.1一致資源定位器URL

 URL(Uniform Resource Locator)是一致資源定位器的簡稱,它表示Internet上某一資源的地址。通過URL我們可以訪問Internet上的各種網絡資源,比如最常見的WWW,FTP站點。瀏覽器通過解析給定的URL可以在網絡上查找相應的文件或其他資源。

  URL是最為直觀的一種網絡定位方法。使用URL符合人們的語言習慣,容易記憶,所以應用十分廣泛。而且在目前使用最為廣泛的TCP/IP中對於URL中主機名的解析也是協議的一個標準,即所謂的域名解析服務。使用URL進行網絡編程,不需要對協議本身有太多的了解,功能也比較弱,相對而言是比較簡單的,所以在這裏我們先介紹在Java中如何使用URL進行網絡編程來引導讀者入門。


8.2.2URL的組成

protocol://resourceName
  協議名(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協議名://機器名+端口號+文件名+內部引用
  
  端口號是和Socket編程相關的一個概念,初學者不必在此深究,在後面會有詳細講解。內部引用是HTML中的標記,有興趣的讀者可以參考有關HTML的書籍。

8.2.3創建一個URL

為了表示URL,java.net中實現了類URL。我們可以通過下面的構造方法來初始化一個URL對象:
  (1) public URL (String spec);
     通過一個表示URL地址的字符串可以構造一個URL對象。
     URL urlBase=newURL("http://www. 263.net/")

  (2) public URL(URL context, String spec);
     通過基URL和相對URL構造一個URL對象。
     URL net263=newURL ("http://www.263.net/");
     URL index263=newURL(net263, "index.html")

  (3) public URL(String protocol, String host,String file);
     newURL("http", "www.gamelan.com", "/pages/Gamelan.net.html");

  (4) public URL(String protocol, String host,int port, String file);
     URL gamelan=newURL("http", "www.gamelan.com", 80,"Pages/Gamelan.network.html");

  註意:類URL的構造方法都聲明拋棄非運行時例外(MalformedURLException),因此生成URL對象時,我們必須要對這一例外進行處理,通常是用try-catch語句進行捕獲。格式如下:

try{
     URL myURL= newURL(…)
  }catch(MalformedURLException e){
  …
  //exception handlercode here
  …
  }


8.2.4解析一個URL

一個URL對象生成後,其屬性是不能被改變的,但是我們可以通過類URL所提供的方法來獲取這些屬性:
   public StringgetProtocol() 獲取該URL的協議名。
   public String getHost()獲取該URL的主機名。
   public int getPort() 獲取該URL的端口號,如果沒有設置端口,返回-1。
   public String getFile()獲取該URL的文件名。
   public String getRef() 獲取該URL在文件中的相對位置。
   public StringgetQuery() 獲取該URL的查詢信息。
   public String getPath()獲取該URL的路徑
   public StringgetAuthority() 獲取該URL的權限信息
   public StringgetUserInfo() 獲得使用者的信息
   public String getRef() 獲得該URL的錨


  下面的例子中,我們生成一個URL對象,並獲取它的各個屬性。

  import java.net.*;
  import java.io.*;

  public class ParseURL{
  public static void main(String [] args) throws Exception{

  URL Aurl=new URL("http://java.sun.com:80/docs/books/");
  URL tuto=newURL(Aurl,"tutorial.intro.html#DOWNLOADING");
  System.out.println("protocol="+tuto.getProtocol());
  System.out.println("host="+ tuto.getHost());
  System.out.println("filename="+tuto.getFile());
  System.out.println("port="+tuto.getPort());
  System.out.println("ref="+tuto.getRef());
  System.out.println("query="+tuto.getQuery());
  System.out.println("path="+tuto.getPath());
  System.out.println("UserInfo="+tuto.getUserInfo());
  System.out.println("Authority="+tuto.getAuthority());
  }
  }

  執行結果為:
   protocol=http host=java.sun.com filename=/docs/books/tutorial.intro.html
   port=80
   ref=DOWNLOADING
   query=null
   path=/docs/books/tutorial.intro.html
   UserInfo=null
   Authority=java.sun.com:80

8.2.5 從URL讀取WWW網絡資源

當我們得到一個URL對象後,就可以通過它讀取指定的WWW資源。這時我們將使用URL的方法openStream(),其定義為:
         InputStreamopenStream();
  
  方法openSteam()與指定的URL建立連接並返回InputStream類的對象以從這一連接中讀取數據。
  public classURLReader {
  publicstatic void main(String[] args) throws Exception {
//聲明拋出所有例外
    URL tirc =new URL("http://www.tirc1.cs.tsinghua.edu.cn/");
//構建一URL對象
    BufferedReaderin = new BufferedReader(new InputStreamReader(tirc.openStream()));
    //使用openStream得到一輸入流並由此構造一個BufferedReader對象
    StringinputLine;
    while((inputLine = in.readLine()) != null)
//從輸入流不斷的讀數據,直到讀完為止
       System.out.println(inputLine);//把讀入的數據打印到屏幕上
    in.close();//關閉輸入流
  }
  }

8.2.6通過URLConnetction連接WWW

通過URL的方法openStream(),我們只能從網絡上讀取數據,如果我們同時還想輸出數據,例如向服務器端的CGI程序發送一些數據,我們必須先與URL建立連接,然後才能對其進行讀寫,這時就要用到類URLConnection了。CGI是公共網關接口(CommonGateway Interface)的簡稱,它是用戶瀏覽器和服務器端的應用程序進行連接的接口,有關CGI程序設計,請讀者參考有關書籍。

  類URLConnection也在包java.net中定義,它表示Java程序和URL在網絡上的通信連接。當與一個URL建立連接時,首先要在一個URL對象上通過方法openConnection()生成對應的URLConnection對象。例如下面的程序段首先生成一個指向地址http://edu.chinaren.com/index.shtml的對象,然後用openConnection()打開該URL對象上的一個連接,返回一個URLConnection對象。如果連接過程失敗,將產生IOException.

  Try{
    URLnetchinaren = new URL ("http://edu.chinaren.com/index.shtml");
    URLConnectonntc = netchinaren.openConnection();
  }catch(MalformedURLExceptione){ //創建URL()對象失敗
  …
  }catch(IOException e){ //openConnection()失敗
  …
  }

  類URLConnection提供了很多方法來設置或獲取連接參數,程序設計時最常使用的是getInputStream()和getOurputStream(),其定義為:
     InputSteramgetInputSteram();
     OutputSteramgetOutputStream();

  通過返回的輸入/輸出流我們可以與遠程對象進行通信。看下面的例子:
  URL url =newURL ("http://www.javasoft.com/cgi-bin/backwards");
  //創建一URL對象
  URLConnectincon=url.openConnection();
  //由URL對象獲取URLConnection對象
  DataInputStreamdis=new DataInputStream (con.getInputSteam());
  //由URLConnection獲取輸入流,並構造DataInputStream對象
  PrintStreamps=new PrintSteam(con.getOutupSteam());
  //由URLConnection獲取輸出流,並構造PrintStream對象
  Stringline=dis.readLine(); //從服務器讀入一行
  ps.println("client…");//向服務器寫出字符串 "client…"
  
  其中backwards為服務器端的CGI程序。實際上,類URL的方法openSteam()是通過URLConnection來實現的。它等價於
    openConnection().getInputStream();
  
  基於URL的網絡編程在底層其實還是基於下面要講的Socket接口的。WWW,FTP等標準化的網絡服務都是基於TCP協議的,所以本質上講URL編程也是基於TCP的一種應用。

8.3 基於Socket(套接字)的低層次Java網絡編程

 8.3.1 Socket通訊

網絡上的兩個程序通過一個雙向的通訊連接實現數據的交換,這個雙向鏈路的一端稱為一個Socket。Socket通常用來實現客戶方和服務方的連接。Socket是TCP/IP協議的一個十分流行的編程界面,一個Socket由一個IP地址和一個端口號唯一確定。

  在傳統的UNIX環境下可以操作TCP/IP協議的接口不止Socket一個,Socket所支持的協議種類也不光TCP/IP一種,因此兩者之間是沒有必然聯系的。在Java環境下,Socket編程主要是指基於TCP/IP協議的網絡編程。

  說Socket編程是低層次網絡編程並不等於它功能不強大,恰恰相反,正因為層次低,Socket編程比基於URL的網絡編程提供了更強大的功能和更靈活的控制,但是卻要更復雜一些。由於Java本身的特殊性,Socket編程在Java中可能已經是層次最低的網絡編程接口,在Java中要直接操作協議中更低的層次,需要使用Java的本地方法調用(JNI),在這裏就不予討論了。

8.3.2 Socket通訊的一般過程

前面已經提到Socket通常用來實現C/S結構。

  使用Socket進行Client/Server程序設計的一般連接過程是這樣的:Server端Listen(監聽)某個端口是否有連接請求,Client端向Server端發出Connect(連接)請求,Server端向Client端發回Accept(接受)消息。一個連接就建立起來了。Server端和Client端都可以通過Send,Write等方法與對方通信。
  
  對於一個功能齊全的Socket,都要包含以下基本結構,其工作過程包含以下四個基本的步驟:
  (1) 創建Socket;
  (2) 打開連接到Socket的輸入/出流;
  (3) 按照一定的協議對Socket進行讀/寫操作;
  (4) 關閉Socket.

  第三步是程序員用來調用Socket和實現程序功能的關鍵步驟,其他三步在各種程序中基本相同。

  以上4個步驟是針對TCP傳輸而言的,使用UDP進行傳輸時略有不同,在後面會有具體講解。

8.3.3 創建Socket

java在包java.net中提供了兩個類Socket和ServerSocket,分別用來表示雙向連接的客戶端和服務端。這是兩個封裝得非常好的類,使用很方便。其構造方法如下:
  Socket(InetAddressaddress, int port);
  Socket(InetAddressaddress, int port, boolean stream);
  Socket(Stringhost, int prot);
  Socket(Stringhost, int prot, boolean stream);
  Socket(SocketImplimpl)
  Socket(Stringhost, int port, InetAddress localAddr, int localPort)
  Socket(InetAddressaddress, int port, InetAddress localAddr, int localPort)
  ServerSocket(intport);
  ServerSocket(intport, int backlog);
  ServerSocket(intport, 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);
  ServerSocketserver = new ServerSocket(80);

  註意,在選擇端口時,必須小心。每一個端口提供一種特定的服務,只有給出正確的端口,才能獲得相應的服務。0~1023的端口號為系統所保留,例如http服務的端口號為80,telnet服務的端口號為21,ftp服務的端口號為23, 所以我們在選擇端口號時,最好選擇一個大於1023的數以防止發生沖突。

  在創建socket時如果發生錯誤,將產生IOException,在程序中必須對之作出處理。所以在創建Socket或ServerSocket是必須捕獲或拋出例外。

8.3.4客戶端的Socket

下面是一個典型的創建客戶端Socket的過程。
   try{
     Socketsocket=new Socket("127.0.0.1",4700);
     //127.0.0.1是TCP/IP協議中默認的本機地址
   }catch(IOExceptione){
     System.out.println("Error:"+e);
   }

  這是最簡單的在客戶端創建一個Socket的一個小程序段,也是使用Socket進行網絡通訊的第一步,程序相當簡單,在這裏不作過多解釋了。在後面的程序中會用到該小程序段。

8.3.5服務器端的ServerSocket

下面是一個典型的創建Server端ServerSocket的過程。
  ServerSocketserver=null;
  try {
     server=newServerSocket(4700);
     //創建一個ServerSocket在端口4700監聽客戶請求
  }catch(IOExceptione){
     System.out.println("cannot listen to :"+e);
  }
  Socketsocket=null;
  try {
    socket=server.accept();
    //accept()是一個阻塞的方法,一旦有客戶請求,它就會返回一個Socket對象用於同客戶進行交互
  }catch(IOExceptione){
    System.out.println("Error:"+e);
  }

  以上的程序是Server的典型工作模式,只不過在這裏Server只能接收一個請求,接受完後Server就退出了。實際的應用中總是讓它不停的循環接收,一旦有客戶請求,Server總是會創建一個服務線程來服務新來的客戶,而自己繼續監聽。程序中accept()是一個阻塞函數,所謂阻塞性方法就是說該方法被調用後,將等待客戶的請求,直到有一個客戶啟動並請求連接到相同的端口,然後accept()返回一個對應於客戶的socket。這時,客戶方和服務方都建立了用於通信的socket,接下來就是由各個socket分別打開各自的輸入/輸出流。

8.3.6打開輸入/出流

類Socket提供了方法getInputStream ()和getOutStream()來得到對應的輸入/輸出流以進行讀/寫操作,這兩個方法分別返回InputStream和OutputSteam類對象。為了便於讀/寫數據,我們可以在返回的輸入/輸出流對象上建立過濾流,如DataInputStream、DataOutputStream或PrintStream類對象,對於文本方式流對象,可以采用InputStreamReader和OutputStreamWriter、PrintWirter等處理。

  例如:
  PrintStreamos=new PrintStream(new BufferedOutputStreem(socket.getOutputStream()));
  DataInputStreamis=new DataInputStream(socket.getInputStream());
  PrintWriterout=new PrintWriter(socket.getOutStream(),true);
  BufferedReaderin=new ButfferedReader(new InputSteramReader(Socket.getInputStream()));

  輸入輸出流是網絡編程的實質性部分,具體如何構造所需要的過濾流,要根據需要而定,能否運用自如主要看讀者對Java中輸入輸出部分掌握如何。

8.3.7 關閉Socket

每一個Socket存在時,都將占用一定的資源,在Socket對象使用完畢時,要其關閉。關閉Socket可以調用Socket的Close()方法。在關閉Socket之前,應將與Socket相關的所有的輸入/輸出流全部關閉,以釋放所有的資源。而且要註意關閉的順序,與Socket相關的所有的輸入/輸出該首先關閉,然後再關閉Socket。
  os.close();
  is.close();
  socket.close();

  盡管Java有自動回收機制,網絡資源最終是會被釋放的。但是為了有效的利用資源,建議讀者按照合理的順序主動釋放資源。


8.3.8簡單的Client/Server程序設計

下面我們給出一個用Socket實現的客戶和服務器交互的典型的C/S結構的演示程序,讀者通過仔細閱讀該程序,會對前面所討論的各個概念有更深刻的認識。程序的意義請參考註釋。

1. 客戶端程序

  import java.io.*;
  import java.net.*;
  public class TalkClient{
    public staticvoid main(String args[]) {
      try{
        Socket socket=newSocket("127.0.0.1",4700);
        //向本機的4700端口發出客戶請求
        BufferedReadersin=new BufferedReader(new InputStreamReader(System.in));
        //由系統標準輸入設備構造BufferedReader對象
        PrintWriteros=new PrintWriter(socket.getOutputStream());
        //由Socket對象得到輸出流,並構造PrintWriter對象
        BufferedReader is=newBufferedReader(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(Exceptione) {
        System.out.println("Error"+e);//出錯,則打印出錯信息
      }
  }
}

 2. 服務器端程序

  import java.io.*;
  import java.net.*;
  importjava.applet.Applet;
  public classTalkServer{
    public staticvoid main(String args[]) {
      try{
        ServerSocketserver=null;
        try{
          server=newServerSocket(4700);
        //創建一個ServerSocket在端口4700監聽客戶請求
        }catch(Exceptione) {
          System.out.println("cannot listen to:"+e);
        //出錯,打印出錯信息
        }

        Socket socket=null;
        try{
          socket=server.accept();
          //使用accept()阻塞等待客戶請求,有客戶
//請求到來則產生一個Socket對象,並繼續執行
        }catch(Exceptione) {
          System.out.println("Error."+e);
          //出錯,打印出錯信息
        }
        String line;
        BufferedReaderis=new BufferedReader(new InputStreamReader(socket.getInputStream()));
         //由Socket對象得到輸入流,並構造相應的BufferedReader對象
        PrintWriteros=newPrintWriter(socket.getOutputStream());
         //由Socket對象得到輸出流,並構造PrintWriter對象
        BufferedReadersin=new BufferedReader(new InputStreamReader(System.in));
         //由系統標準輸入設備構造BufferedReader對象

        System.out.println("Client:"+is.readLine());
        //在標準輸出上打印從客戶端讀入的字符串
        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(Exceptione){
        System.out.println("Error:"+e);
        //出錯,打印出錯信息
      }
    }
  }

  從上面的兩個程序中我們可以看到,socket四個步驟的使用過程。讀者可以分別將Socket使用的四個步驟的對應程序段選擇出來,這樣便於讀者對socket的使用有進一步的了解。

  讀者可以在單機上試驗該程序,最好是能在真正的網絡環境下試驗該程序,這樣更容易分辨輸出的內容和客戶機,服務器的對應關系。同時也可以修改該程序,提供更為強大的功能,或更加滿足讀者的意圖。

8.3.9 支持多客戶的client/server程序設計

  前面提供的Client/Server程序只能實現Server和一個客戶的對話。在實際應用中,往往是在服務器上運行一個永久的程序,它可以接收來自其他多個客戶端的請求,提供相應的服務。為了實現在服務器方給多個客戶提供服務的功能,需要對上面的程序進行改造,利用多線程實現多客戶機制。服務器總是在指定的端口上監聽是否有客戶請求,一旦監聽到客戶請求,服務器就會啟動一個專門的服務線程來響應該客戶的請求,而服務器本身在啟動完線程之後馬上又進入監聽狀態,等待下一個客戶的到來。

  客戶端的程序和上面程序是完全一樣的,讀者如果仔細閱讀過上面的程序,可以跳過不讀,把主要精力集中在Server端的程序上。


1. 客戶端程序:MultiTalkClient.java

  import java.io.*;
  import java.net.*;
  public class MultiTalkClient {
   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); //出錯,則打印出錯信息
    }
  }
}

 2. 服務器端程序: MultiTalkServer.java

  import java.io.*;
  import java.net.*;
  import ServerThread;
  public class MultiTalkServer{
   static int clientnum=0; //靜態成員變量,記錄當前客戶的個數
   public static void main(String args[]) throws IOException {
    ServerSocket serverSocket=null;
    boolean listening=true;
    try{
      serverSocket=new ServerSocket(4700);
      //創建一個ServerSocket在端口4700監聽客戶請求
    }catch(IOException e) {
      System.out.println("Could not listen on port:4700.");
      //出錯,打印出錯信息
      System.exit(-1); //退出
    }
    while(listening){ //永遠循環監聽
      new ServerThread(serverSocket.accept(),clientnum).start();
      //監聽到客戶請求,根據得到的Socket對象和
       客戶計數創建服務線程,並啟動之
      clientnum++; //增加客戶計數
    }
    serverSocket.close(); //關閉ServerSocket
  }
}

 3. 程序ServerThread.java

  import java.io.*;
  import java.net.*;
  public class ServerThread extends Thread{
   Socket socket=null; //保存與本線程相關的Socket對象
   int clientnum; //保存本進程的客戶計數
   public ServerThread(Socket socket,int num) { //構造函數
    this.socket=socket; //初始化socket變量
    clientnum=num+1; //初始化clientnum變量
   }
   public void run() { //線程主體
    try{
      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對象
      System.out.println("Client:"+ clientnum +is.readLine());
      //在標準輸出上打印從客戶端讀入的字符串
      line=sin.readLine();
      //從標準輸入讀入一字符串
      while(!line.equals("bye")){
      //如果該字符串為 "bye",則停止循環
        os.println(line);
        //向客戶端輸出該字符串
        os.flush();
        //刷新輸出流,使Client馬上收到該字符串
        System.out.println("Server:"+line);
        //在系統標準輸出上打印該字符串
        System.out.println("Client:"+ clientnum +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);
      //出錯,打印出錯信息
     }
   }
 }

  這個程序向讀者展示了網絡應用中最為典型的C/S結構,我們可以用下面的圖來描述這樣一種模型:


  通過以上的學習,讀者應該對Java的面向流的網絡編程有了一個比較全面的認識,這些都是基於TCP的應用,後面我們將介紹基於UDP的Socket編程。

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(intport, InetAddress laddr)
  
  其中,port指明socket所使用的端口號,如果未指明端口號,則把socket連接到本地主機上一個可用的端口。laddr指明一個可用的本地地址。給出端口號時要保證不發生端口沖突,否則會生成SocketException類例外。註意:上述的兩個構造方法都聲明拋棄非運行時例外SocketException,程序中必須進行處理,或者捕獲、或者聲明拋棄。

用數據報方式編寫client/server程序時,無論在客戶方還是服務方,首先都要建立一個DatagramSocket對象,用來接收或發送數據報,然後使用DatagramPacket類對象作為傳輸數據的載體。下面看一下DatagramPacket的構造方法 :
   DatagramPacket(byte buf[],int length);
   DatagramPacket(bytebuf[], 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()將一直等待,直到收到一個數據報為止。
  DatagramPacketpacket=new DatagramPacket(buf, 256);
  Socket.receive(packet);

  發送數據前,也要先生成一個新的DatagramPacket對象,這時要使用上面的第二種構造方法,在給出存放發送數據的緩沖區的同時,還要給出完整的目的地址,包括IP地址和端口號。發送數據是通過DatagramSocket的方法send()實現的,send()根據數據報的目的地址來尋徑,以傳遞數據報。
  DatagramPacketpacket=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 classQuoteClient {
   public static voidmain(String[] args) throws IOException
   {
    if(args.length!=1){
    //如果啟動的時候沒有給出Server的名字,那麽出錯退出
     System.out.println("Usage:javaQuoteClient <hostname>");
     //打印出錯信息
     return; //返回
    }

    DatagramSocket socket=new DatagramSocklet();
    //創建數據報套接字

    Byte[] buf=new byte[256]; //創建緩沖區
    InetAddressaddress=InetAddress.getByName(args [0]);
//由命令行給出的第一個參數默認為Server的名字,通過它得到Server的IP信息
    DatagramPacketpacket=new DatagramPacket (buf, buf.length, address, 4445);
    //創建DatagramPacket對象
    socket.send(packet);//發送
    packet=newDatagramPacket(buf,buf.length);
    //創建新的DatagramPacket對象,用來接收數據報
    socket.receive(packet);//接收
    Stringreceived=new String(packet.getData());
    //根據接收到的字節數組生成相應的字符串
    System.out.println("Quoteof the Moment:"+received );
    //打印生成的字符串

    socket.close(); //關閉套接口
   }
 }

 2. 服務器方程序:QuoteServer.java

  public classQuoteServer{
   public static voidmain(String args[]) throws java.io.IOException
   {
    newQuoteServerThread().start();
    //啟動一個QuoteServerThread線程
   }
  }

 3. 程序QuoteServerThread.java

  import java.io.*;
  import java.net.*;
  import java.util.*;
  //服務器線程
  public classQuoteServerThread extends Thread
  {
  protectedDatagramSocket socket=null;
  //記錄和本對象相關聯的DatagramSocket對象
  protectedBufferedReader in=null;
  //用來讀文件的一個Reader
  protected booleanmoreQuotes=true;
  //標誌變量,是否繼續操作

  public QuoteServerThread() throws IOException {
  //無參數的構造函數
    this("QuoteServerThread");
    //以QuoteServerThread為默認值調用帶參數的構造函數
  }
  publicQuoteServerThread(String name) throws IOException {
    super(name); //調用父類的構造函數
    socket=newDatagramSocket(4445);
    //在端口4445創建數據報套接字
    try{
      in= newBufferedReader(new FileReader(" one-liners.txt"));
      //打開一個文件,構造相應的BufferReader對象
    }catch(FileNotFoundExceptione) { //異常處理
      System.err.println("Couldnot open quote file. Serving time instead.");
       //打印出錯信息
    }
  }
  public void run() //線程主體
  {
    while(moreQuotes){
     try{
       byte[] buf=newbyte[256]; //創建緩沖區
       DatagramPacketpacket=new DatagramPacket(buf,buf.length);
       //由緩沖區構造DatagramPacket對象
       socket.receive(packet);//接收數據報
       StringdString=null;
       if(in= =null)dString=new Date().toString();
       //如果初始化的時候打開文件失敗了,
//則使用日期作為要傳送的字符串
       elsedString=getNextQuote();
       //否則調用成員函數從文件中讀出字符串
       buf=dString.getByte();
       //把String轉換成字節數組,以便傳送

       InetAddress address=packet.getAddress();
       //從Client端傳來的Packet中得到Client地址
       intport=packet.getPort(); //和端口號
       packet=newDatagramPacket(buf,buf.length,address,port);
       //根據客戶端信息構建DatagramPacket
       socket.send(packet);//發送數據報
      }catch(IOExceptione) { //異常處理
       e.printStackTrace();//打印錯誤棧
       moreQuotes=false;//標誌變量置false,以結束循環
      }
    }
    socket.close(); //關閉數據報套接字
  }

  protected String getNextQuotes(){
  //成員函數,從文件中讀數據
    StringreturnValue=null;
    try {
       if((returnValue=in.readLine())==null) {
       //從文件中讀一行,如果讀到了文件尾
       in.close( ); //關閉輸入流
       moreQuotes=false;
       //標誌變量置false,以結束循環
       returnValue="Nomore quotes. Goodbye.";
       //置返回值
       } //否則返回字符串即為從文件讀出的字符串
    }catch(IOEceptione) { //異常處理
       returnValue="IOExceptionoccurred in server";
       //置異常返回值
    }
    returnreturnValue; //返回字符串
  }
}

  可以看出使用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 classMulticastClient {
    public staticvoid main(String args[]) throws IOException
    {
     MulticastSocketsocket=new MulticastSocket(4446);
     //創建4446端口的廣播套接字
     InetAddressaddress=InetAddress.getByName("230.0.0.1");
     //得到230.0.0.1的地址信息
     socket.joinGroup(address);
     //使用joinGroup()將廣播套接字綁定到地址上
     DatagramPacketpacket;

     for(int i=0;i<5;i++) {
       byte[] buf=newbyte[256];
       //創建緩沖區
       packet=newDatagramPacket(buf,buf.length);
       //創建接收數據報
       socket.receive(packet);//接收
       Stringreceived=new String(packet.getData());
       //由接收到的數據報得到字節數組,
       //並由此構造一個String對象
       System.out.println("Quoteof theMoment:"+received);
       //打印得到的字符串
     } //循環5次
     socket.leaveGroup(address);
     //把廣播套接字從地址上解除綁定
     socket.close(); //關閉廣播套接字
   }
 }

 2. 服務器方程序:MulticastServer.java

  public classMulticastServer{
    public staticvoid main(String args[]) throws java.io.IOException
    {
      new MulticastServerThread().start();
      //啟動一個服務器線程
    }
  }

 3. 程序MulticastServerThread.java

  import java.io.*;
  import java.net.*;
  import java.util.*;
  public classMulticastServerThread extends QuoteServerThread
  //從QuoteServerThread繼承得到新的服務器線程類MulticastServerThread
  {
    Private longFIVE_SECOND=5000; //定義常量,5秒鐘
    publicMulticastServerThread(String name) throws IOException
    {
      super("MulticastServerThread");
      //調用父類,也就是QuoteServerThread的構造函數
    }

    public void run() //重寫父類的線程主體
    {
     while(moreQuotes){
     //根據標誌變量判斷是否繼續循環
      try{
        byte[] buf=newbyte[256];
        //創建緩沖區
        StringdString=null;
        if(in==null)dString=new Date().toString();
        //如果初始化的時候打開文件失敗了,
//則使用日期作為要傳送的字符串
        else dString=getNextQuote();
        //否則調用成員函數從文件中讀出字符串
        buf=dString.getByte();
        //把String轉換成字節數組,以便傳送send it
        InetAddressgroup=InetAddress.getByName("230.0.0.1");
        //得到230.0.0.1的地址信息
        DatagramPacketpacket=new DatagramPacket(buf,buf.length,group,4446);
        //根據緩沖區,廣播地址,和端口號創建DatagramPacket對象
        socket.send(packet);//發送該Packet
        try{
          sleep((long)(Math.random()*FIVE_SECONDS));
          //隨機等待一段時間,0~5秒之間
        }catch(InterruptedExceptione) { } //異常處理
      }catch(IOExceptione){ //異常處理
        e.printStackTrace(); //打印錯誤棧

        moreQuotes=false; //置結束循環標誌
      }
    }
    socket.close( ); //關閉廣播套接口
   }
 }

  至此,Java網絡編程這一章已經講解完畢。讀者通過學習,應該對網絡編程有了一個清晰的認識,可能對某些概念還不是十分的清楚,還是需要更多的實踐來進一步掌握。編程語言的學習不同於一般的學習,及其強調實踐的重要性。讀者應該對URL網絡編程,Socket中的TCP,UDP編程進行大量的練習才能更好的掌握本章中所提到的一些概念,才能真正學到Java網絡編程的精髓!

網絡編程的基本概念,TCP/IP協議簡介