程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 一個簡單的服務器和客戶機程序

一個簡單的服務器和客戶機程序

編輯:關於JAVA

這個例子將以最簡單的方式運用套接字對服務器和客戶機進行操作。服務器的全部工作就是等候建立一個連接,然後用那個連接產生的Socket創建一個InputStream以及一個OutputStream。在這之後,它從InputStream讀入的所有東西都會反饋給OutputStream,直到接收到行中止(END)為止,最後關閉連接。
客戶機連接與服務器的連接,然後創建一個OutputStream。文本行通過OutputStream發送。客戶機也會創建一個InputStream,用它收聽服務器說些什麼(本例只不過是反饋回來的同樣的字句)。
服務器與客戶機(程序)都使用同樣的端口號,而且客戶機利用本地主機地址連接位於同一台機器中的服務器(程序),所以不必在一個物理性的網絡裡完成測試(在某些配置環境中,可能需要同真正的網絡建立連接,否則程序不能工作——盡管實際並不通過那個網絡通信)。
下面是服務器程序:

 

//: JabberServer.java
// Very simple server that just
// echoes whatever the client sends.
import java.io.*;
import java.net.*;

public class JabberServer {  
  // Choose a port outside of the range 1-1024:
  public static final int PORT = 8080;
  public static void main(String[] args) 
      throws IOException {
    ServerSocket s = new ServerSocket(PORT);
    System.out.println("Started: " + s);
    try {
      // Blocks until a connection occurs:
      Socket socket = s.accept();
      try {
        System.out.println(
          "Connection accepted: "+ socket);
        BufferedReader in = 
          new BufferedReader(
            new InputStreamReader(
              socket.getInputStream()));
        // Output is automatically flushed
        // by PrintWriter:
        PrintWriter out = 
          new PrintWriter(
            new BufferedWriter(
              new OutputStreamWriter(
                socket.getOutputStream())),true);
        while (true) {  
          String str = in.readLine();
          if (str.equals("END")) break;
          System.out.println("Echoing: " + str);
          out.println(str);
        }
      // Always close the two sockets...
      } finally {
        System.out.println("closing...");
        socket.close();
      }
    } finally {
      s.close();
    }
  } 
} ///:~


可以看到,ServerSocket需要的只是一個端口編號,不需要IP地址(因為它就在這台機器上運行)。調用accept()時,方法會暫時陷入停頓狀態(堵塞),直到某個客戶嘗試同它建立連接。換言之,盡管它在那裡等候連接,但其他進程仍能正常運行(參考第14章)。建好一個連接以後,accept()就會返回一個Socket對象,它是那個連接的代表。
清除套接字的責任在這裡得到了很藝術的處理。假如ServerSocket構建器失敗,則程序簡單地退出(注意必須保證ServerSocket的構建器在失敗之後不會留下任何打開的網絡套接字)。針對這種情況,main()會“擲”出一個IOException違例,所以不必使用一個try塊。若ServerSocket構建器成功執行,則其他所有方法調用都必須到一個try-finally代碼塊裡尋求保護,以確保無論塊以什麼方式留下,ServerSocket都能正確地關閉。
同樣的道理也適用於由accept()返回的Socket。若accept()失敗,那麼我們必須保證Socket不再存在或者含有任何資源,以便不必清除它們。但假若執行成功,則後續的語句必須進入一個try-finally塊內,以保障在它們失敗的情況下,Socket仍能得到正確的清除。由於套接字使用了重要的非內存資源,所以在這裡必須特別謹慎,必須自己動手將它們清除(Java中沒有提供“破壞器”來幫助我們做這件事情)。
無論ServerSocket還是由accept()產生的Socket都打印到System.out裡。這意味著它們的toString方法會得到自動調用。這樣便產生了:

 

ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]
Socket[addr=127.0.0.1,PORT=1077,localport=8080]


大家不久就會看到它們如何與客戶程序做的事情配合。
程序的下一部分看來似乎僅僅是打開文件,以便讀取和寫入,只是InputStream和OutputStream是從Socket對象創建的。利用兩個“轉換器”類InputStreamReader和OutputStreamWriter,InputStream和OutputStream對象已經分別轉換成為Java 1.1的Reader和Writer對象。也可以直接使用Java1.0的InputStream和OutputStream類,但對輸出來說,使用Writer方式具有明顯的優勢。這一優勢是通過PrintWriter表現出來的,它有一個過載的構建器,能獲取第二個參數——一個布爾值標志,指向是否在每一次println()結束的時候自動刷新輸出(但不適用於print()語句)。每次寫入了輸出內容後(寫進out),它的緩沖區必須刷新,使信息能正式通過網絡傳遞出去。對目前這個例子來說,刷新顯得尤為重要,因為客戶和服務器在采取下一步操作之前都要等待一行文本內容的到達。若刷新沒有發生,那麼信息不會進入網絡,除非緩沖區滿(溢出),這會為本例帶來許多問題。
編寫網絡應用程序時,需要特別注意自動刷新機制的使用。每次刷新緩沖區時,必須創建和發出一個數據包(數據封)。就目前的情況來說,這正是我們所希望的,因為假如包內包含了還沒有發出的文本行,服務器和客戶機之間的相互“握手”就會停止。換句話說,一行的末尾就是一條消息的末尾。但在其他許多情況下,消息並不是用行分隔的,所以不如不用自動刷新機制,而用內建的緩沖區判決機制來決定何時發送一個數據包。這樣一來,我們可以發出較大的數據包,而且處理進程也能加快。
注意和我們打開的幾乎所有數據流一樣,它們都要進行緩沖處理。本章末尾有一個練習,清楚展現了假如我們不對數據流進行緩沖,那麼會得到什麼樣的後果(速度會變慢)。
無限while循環從BufferedReader in內讀取文本行,並將信息寫入System.out,然後寫入PrintWriter.out。注意這可以是任何數據流,它們只是在表面上同網絡連接。
客戶程序發出包含了"END"的行後,程序會中止循環,並關閉Socket。
下面是客戶程序的源碼:

 

//: JabberClient.java
// Very simple client that just sends
// lines to the server and reads lines
// that the server sends.
import java.net.*;
import java.io.*;

public class JabberClient {
  public static void main(String[] args) 
      throws IOException {
    // Passing null to getByName() produces the
    // special "Local Loopback" IP address, for
    // testing on one machine w/o a network:
    InetAddress addr = 
      InetAddress.getByName(null);
    // Alternatively, you can use 
    // the address or name:
    // InetAddress addr = 
    //    InetAddress.getByName("127.0.0.1");
    // InetAddress addr = 
    //    InetAddress.getByName("localhost");
    System.out.println("addr = " + addr);
    Socket socket = 
      new Socket(addr, JabberServer.PORT);
    // Guard everything in a try-finally to make
    // sure that the socket is closed:
    try {
      System.out.println("socket = " + socket);
      BufferedReader in =
        new BufferedReader(
          new InputStreamReader(
            socket.getInputStream()));
      // Output is automatically flushed
      // by PrintWriter:
      PrintWriter out =
        new PrintWriter(
          new BufferedWriter(
            new OutputStreamWriter(
              socket.getOutputStream())),true);
      for(int i = 0; i < 10; i ++) {
        out.println("howdy " + i);
        String str = in.readLine();
        System.out.println(str);
      }
      out.println("END");
    } finally {
      System.out.println("closing...");
      socket.close();
    }
  }
} ///:~


在main()中,大家可看到獲得本地主機IP地址的InetAddress的三種途徑:使用null,使用localhost,或者直接使用保留地址127.0.0.1。當然,如果想通過網絡同一台遠程主機連接,也可以換用那台機器的IP地址。打印出InetAddress addr後(通過對toString()方法的自動調用),結果如下:
localhost/127.0.0.1
通過向getByName()傳遞一個null,它會默認尋找localhost,並生成特殊的保留地址127.0.0.1。注意在名為socket的套接字創建時,同時使用了InetAddress以及端口號。打印這樣的某個Socket對象時,為了真正理解它的含義,請記住一次獨一無二的因特網連接是用下述四種數據標識的:clientHost(客戶主機)、clientPortNumber(客戶端口號)、serverHost(服務主機)以及serverPortNumber(服務端口號)。服務程序啟動後,會在本地主機(127.0.0.1)上建立為它分配的端口(8080)。一旦客戶程序發出請求,機器上下一個可用的端口就會分配給它(這種情況下是1077),這一行動也在與服務程序相同的機器(127.0.0.1)上進行。現在,為了使數據能在客戶及服務程序之間來回傳送,每一端都需要知道把數據發到哪裡。所以在同一個“已知”服務程序連接的時候,客戶會發出一個“返回地址”,使服務器程序知道將自己的數據發到哪兒。我們在服務器端的示范輸出中可以體會到這一情況:
Socket[addr=127.0.0.1,port=1077,localport=8080]
這意味著服務器剛才已接受了來自127.0.0.1這台機器的端口1077的連接,同時監聽自己的本地端口(8080)。而在客戶端:
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]
這意味著客戶已用自己的本地端口1077與127.0.0.1機器上的端口8080建立了 連接。
大家會注意到每次重新啟動客戶程序的時候,本地端口的編號都會增加。這個編號從1025(剛好在系統保留的1-1024之外)開始,並會一直增加下去,除非我們重啟機器。若重新啟動機器,端口號仍然會從1025開始增值(在Unix機器中,一旦超過保留的套按字范圍,數字就會再次從最小的可用數字開始)。
創建好Socket對象後,將其轉換成BufferedReader和PrintWriter的過程便與在服務器中相同(同樣地,兩種情況下都要從一個Socket開始)。在這裡,客戶通過發出字串"howdy",並在後面跟隨一個數字,從而初始化通信。注意緩沖區必須再次刷新(這是自動發生的,通過傳遞給PrintWriter構建器的第二個參數)。若緩沖區沒有刷新,那麼整個會話(通信)都會被掛起,因為用於初始化的“howdy”永遠不會發送出去(緩沖區不夠滿,不足以造成發送動作的自動進行)。從服務器返回的每一行都會寫入System.out,以驗證一切都在正常運轉。為中止會話,需要發出一個"END"。若客戶程序簡單地掛起,那麼服務器會“擲”出一個違例。
大家在這裡可以看到我們采用了同樣的措施來確保由Socket代表的網絡資源得到正確的清除,這是用一個try-finally塊實現的。
套接字建立了一個“專用”連接,它會一直持續到明確斷開連接為止(專用連接也可能間接性地斷開,前提是某一端或者中間的某條鏈路出現故障而崩潰)。這意味著參與連接的雙方都被鎖定在通信中,而且無論是否有數據傳遞,連接都會連續處於開放狀態。從表面看,這似乎是一種合理的連網方式。然而,它也為網絡帶來了額外的開銷。本章後面會介紹進行連網的另一種方式。采用那種方式,連接的建立只是暫時的。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved