熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> Java編程 >> Java核心技術 >> 正文

Java套接字實現網絡編程之基礎篇

2013-11-23 18:51:24  來源: Java核心技術 
用Java開發網絡軟件非常方便和強大Java的這種力量來源於他獨有的一套強大的用於網絡的 API這些API是一系列的類和接口均位於包和中在這篇文章中我們將介紹套接字(Socket)慨念同時以實例說明如何使用Network API操縱套接字在完成本文後你將具備編寫網絡低端通訊軟件的能力

  什麼是套接字(Socket)?

  Network API是典型的用於基於TCP/IP網絡Java程序與其他程序通訊Network API依靠Socket進行通訊Socket可以看成在兩個程序進行通訊連接中的一個端點一個程序將一段信息寫入Socket中該Socket將這段信息發送給另外一個Socket中使這段信息能傳送到其他程序中如圖


  我們來分析一下圖Host A上的程序A將一段信息寫入Socket中Socket的內容被Host A的網絡管理軟件訪問並將這段信息通過Host A的網絡接口卡發送到Host BHost B的網絡接口卡接收到這段信息後傳送給Host B的網絡管理軟件網絡管理軟件將這段信息保存在Host B的Socket中然後程序B才能在Socket中閱讀這段信息

  假設在圖的網絡中添加第三個主機Host C那麼Host A怎麼知道信息被正確傳送到Host B而不是被傳送到Host C中了呢?基於TCP/IP網絡中的每一個主機均被賦予了一個唯一的IP地址IP地址是一個位的無符號整數由於沒有轉變成二進制因此通常以小數點分隔正如所見IP地址均由四個部分組成每個部分的范圍都是以表示位地址

  值得注意的是IP地址都是位地址這是IP協議版本(簡稱Ipv)規定的目前由於IPv地址已近耗盡所以IPv地址正逐漸代替Ipv地址Ipv地址則是位無符號整數

  假設第二個程序被加入圖的網絡的Host B中那麼由Host A傳來的信息如何能被正確的傳給程序B而不是傳給新加入的程序呢?這是因為每一個基於TCP/IP網絡通訊的程序都被賦予了唯一的端口和端口號端口是一個信息緩沖區用於保留Socket中的輸入/輸出信息端口號是一個位無符號整數范圍是以區別主機上的每一個程序(端口號就像房屋中的房間號)低於的短口號保留給標准應用程序比如pop的端口號就是每一個套接字都組合進了IP地址端口端口號這樣形成的整體就可以區別每一個套接字t下面我們就來談談兩種套接字流套接字和自尋址數據套接字

  流套接字(Stream Socket)

  無論何時在兩個網絡應用程序之間發送和接收信息時都需要建立一個可靠的連接流套接字依靠TCP協議來保證信息正確到達目的地實際上IP包有可能在網絡中丟失或者在傳送過程中發生錯誤任何一種情況發生作為接受方的 TCP將聯系發送方TCP重新發送這個IP包這就是所謂的在兩個流套接字之間建立可靠的連接

  流套接字在C/S程序中扮演一個必需的角色客戶機程序(需要訪問某些服務的網絡應用程序)創建一個扮演服務器程序的主機的IP地址和服務器程序(為客戶端應用程序提供服務的網絡應用程序)的端口號的流套接字對象

  客戶端流套接字的初始化代碼將IP地址和端口號傳遞給客戶端主機的網絡管理軟件管理軟件將IP地址和端口號通過NIC傳遞給服務器端主機服務器端主機讀到經過NIC傳遞來的數據然後查看服務器程序是否處於監聽狀態這種監聽依然是通過套接字和端口來進行的如果服務器程序處於監聽狀態那麼服務器端網絡管理軟件就向客戶機網絡管理軟件發出一個積極的響應信號接收到響應信號後客戶端流套接字初始化代碼就給客戶程序建立一個端口號並將這個端口號傳遞給服務器程序的套接字(服務器程序將使用這個端口號識別傳來的信息是否是屬於客戶程序)同時完成流套接字的初始化

  如果服務器程序沒有處於監聽狀態那麼服務器端網絡管理軟件將給客戶端傳遞一個消極信號收到這個消極信號後客戶程序的流套接字初始化代碼將拋出一個異常對象並且不建立通訊連接也不創建流套接字對象這種情形就像打電話一樣當有人的時候通訊建立否則電話將被掛起

  這部分的工作包括了相關聯的三個類InetAddress Socket 和 ServerSocket InetAddress對象描繪了位或位IP地址Socket對象代表了客戶程序流套接字ServerSocket代表了服務程序流套接字所有這三個類均位於包中

  InetAddress類

  InetAddress類在網絡API套接字編程中扮演了一個重要角色參數傳遞給流套接字類和自尋址套接字類構造器或非構造器方法InetAddress描述了位或位IP地址要完成這個功能InetAddress類主要依靠兩個支持類InetAddress 和 InetAddress這三個類是繼承關系InetAddrress是父類InetAddress 和 InetAddress是子類

  由於InetAddress類只有一個構造函數而且不能傳遞參數所以不能直接創建InetAddress對象比如下面的做法就是錯誤的

InetAddress ia = new InetAddress ();
  但我們可以通過下面的個工廠方法創建來創建一個InetAddress對象或InetAddress數組

    getAllByName(String host)方法返回一個InetAddress對象的引用每個對象包含一個表示相應主機名的單獨的IP地址這個IP地址是通過host參數傳遞的對於指定的主機如果沒有IP地址存在那麼這個方法將拋出一個UnknownHostException 異常對象

    getByAddress(byte [] addr)方法返回一個InetAddress對象的引用這個對象包含了一個Ipv地址或Ipv地址Ipv地址是一個字節數組Ipv地址是一個字節地址數組如果返回的數組既不是字節的也不是字節的那麼方法將會拋出一個UnknownHostException異常對象

    getByAddress(String host byte [] addr)方法返回一個InetAddress對象的引用這個InetAddress對象包含了一個由host和字節的addr數組指定的IP地址或者是host和字節的addr數組指定的IP地址如果這個數組既不是字節的也不是位字節的那麼該方法將拋出一個UnknownHostException異常對象

    getByName(String host)方法返回一個InetAddress對象該對象包含了一個與host參數指定的主機相對應的IP地址對於指定的主機如果沒有IP地址存在那麼方法將拋出一個UnknownHostException異常對象

    getLocalHost()方法返回一個InetAddress對象這個對象包含了本地機的IP地址考慮到本地主機既是客戶程序主機又是服務器程序主機為避免混亂我們將客戶程序主機稱為客戶主機將服務器程序主機稱為服務器主機

  上面講到的方法均提到返回一個或多個InetAddress對象的引用實際上每一個方法都要返回一個或多個InetAddress/InetAddress對象的引用調用者不需要知道引用的子類型相反調用者可以使用返回的引用調用InetAddress對象的非靜態方法包括子類型的多態以確保重載方法被調用

  InetAddress和它的子類型對象處理主機名到主機IPv或IPv地址的轉換要完成這個轉換需要使用域名系統下面的代碼示范了如何通過調用getByName(String host)方法獲得InetAddress子類對象的方法這個對象包含了與host參數相對應的IP地址

InetAddress ia = InetAddressgetByName ());
  一但獲得了InetAddress子類對象的引用就可以調用InetAddress的各種方法來獲得InetAddress子類對象中的IP地址信息比如可以通過調用getCanonicalHostName()從域名服務中獲得標准的主機名getHostAddress()獲得IP地址getHostName()獲得主機名isLoopbackAddress()判斷IP地址是否是一個loopback地址

  List 是一段示范代碼InetAddressDemo

// InetAddressDemojava

import *;

class InetAddressDemo
{
 public static void main (String [] args) throws UnknownHostException
 {
  String host = localhost;

  if (argslength == )
   host = args [];

  InetAddress ia = InetAddressgetByName (host);

  Systemoutprintln (Canonical Host Name = +
        iagetCanonicalHostName ());
  Systemoutprintln (Host Address = +
        iagetHostAddress ());
  Systemoutprintln (Host Name = +
        iagetHostName ());
  Systemoutprintln (Is Loopback Address = +
        iaisLoopbackAddress ());
 }
}
  當無命令行參數時代碼輸出類似下面的結果

Canonical Host Name = localhost
Host Address =
Host Name = localhost
Is Loopback Address = true
  InetAddressDemo給了你一個指定主機名作為命令行參數的選擇如果沒有主機名被指定那麼將使用localhost(客戶機的)InetAddressDemo通過調用getByName(String host)方法獲得一個InetAddress子類對象的引用通過這個引用獲得了標准主機名主機地址主機名以及IP地址是否是loopback地址的輸出
  Socket類

  當客戶程序需要與服務器程序通訊的時候客戶程序在客戶機創建一個socket對象Socket類有幾個構造函數兩個常用的構造函數是 Socket(InetAddress addr int port) 和 Socket(String host int port)兩個構造函數都創建了一個基於Socket的連接服務器端流套接字的流套接字對於第一個InetAddress子類對象通過addr參數獲得服務器主機的IP地址對於第二個函數host參數包被分配到InetAddress對象中如果沒有IP地址與host參數相一致那麼將拋出UnknownHostException異常對象兩個函數都通過參數port獲得服務器的端口號假設已經建立連接了網絡API將在客戶端基於Socket的流套接字中捆綁客戶程序的IP地址和任意一個端口號否則兩個函數都會拋出一個IOException對象

  如果創建了一個Socket對象那麼它可能通過調用Socket的 getInputStream()方法從服務程序獲得輸入流讀傳送來的信息也可能通過調用Socket的 getOutputStream()方法獲得輸出流來發送消息在讀寫活動完成之後客戶程序調用close()方法關閉流和流套接字下面的代碼創建了一個服務程序主機地址為端口號為的Socket對象然後從這個新創建的Socket對象中讀取輸入流然後再關閉流和Socket對象

Socket s = new Socket ( );
InputStream is = sgetInputStream ();
// Read from the stream
isclose ();
sclose ();
  接下面我們將示范一個流套接字的客戶程序這個程序將創建一個Socket對象Socket將訪問運行在指定主機端口上的服務程序如果訪問成功客戶程序將給服務程序發送一系列命令並打印服務程序的響應List使我們創建的程序SSClient的源代碼

  Listing : SSClientjava

// SSClientjava

import javaio*;
import *;

class SSClient
{
 public static void main (String [] args)
 {
  String host = localhost;

  // If user specifies a commandline argument that argument
  // represents the host name

  if (argslength == )
   host = args [];

  BufferedReader br = null;
  PrintWriter pw = null;
  Socket s = null;

  try
  {
   // Create a socket that attempts to connect to the server
   // program on the host at port

   s = new Socket (host );

   // Create an input stream reader that chains to the sockets
   // byteoriented input stream The input stream reader
   // converts bytes read from the socket to characters The
   // conversion is based on the platforms default character
   // set

   InputStreamReader isr;
   isr = new InputStreamReader (sgetInputStream ());

   // Create a buffered reader that chains to the input stream
   // reader The buffered reader supplies a convenient method
   // for reading entire lines of text

   br = new BufferedReader (isr);

   // Create a print writer that chains to the sockets byte
   // oriented output stream The print writer creates an
   // intermediate output stream writer that converts
   // characters sent to the socket to bytes The conversion
   // is based on the platforms default character set

   pw = new PrintWriter (sgetOutputStream () true);

   // Send the DATE command to the server

   pwprintln (DATE);

   // Obtain and print the current date/time

   Systemoutprintln (brreadLine ());
   // Send the PAUSE command to the server This allows several
   // clients to start and verifies that the server is spawning
   // multiple threads

   pwprintln (PAUSE);
   // Send the DOW command to the server

   pwprintln (DOW);

   // Obtain and print the current day of week

   Systemoutprintln (brreadLine ());

   // Send the DOM command to the server
 
   pwprintln (DOM);

   // Obtain and print the current day of month

   Systemoutprintln (brreadLine ());

   // Send the DOY command to the server

   pwprintln (DOY);

   // Obtain and print the current day of year

   Systemoutprintln (brreadLine ());
  }
  catch (IOException e)
  {
   Systemoutprintln (etoString ());
  }
  finally
  {
   try
   {
    if (br != null)
     brclose ();

    if (pw != null)
     pwclose ();

    if (s != null)
     sclose ();
   }
   catch (IOException e)
   {
    }
  }
 }
}
  運行這段程序將會得到下面的結果

Tue Jan :: CST
TUESDAY


  SSClient創建了一個Socket對象與運行在主機端口的服務程序聯系主機的IP地址由host變量確定SSClient將獲得Socket的輸入輸出流圍繞BufferedReader的輸入流和PrintWriter的輸出流對字符串進行讀寫操作就變得非常容易SSClient個服務程序發出各種date/time命令並得到響應每個響應均被打印一旦最後一個響應被打印將執行Try/Catch/Finally結構的Finally子串Finally子串將在關閉Socket之前關閉BufferedReader 和 PrintWriter

  在SSClient源代碼編譯完成後可以輸入java SSClient 來執行這段程序如果有合適的程序運行在不同的主機上采用主機名/IP地址為參數的輸入方式比如是運行服務器程序的主機那麼輸入方式就是java SSClient

  技巧

  Socket類包含了許多有用的方法比如getLocalAddress()將返回一個包含客戶程序IP地址的InetAddress子類對象的引用;getLocalPort()將返回客戶程序的端口號;getInetAddress()將返回一個包含服務器IP地址的InetAddress子類對象的引用getPort()將返回服務程序的端口號
  ServerSocket類

  由於SSClient使用了流套接字所以服務程序也要使用流套接字這就要創建一個ServerSocket對象ServerSocket有幾個構造函數最簡單的是ServerSocket(int port)當使用ServerSocket(int port)創建一個ServerSocket對象port參數傳遞端口號這個端口就是服務器監聽連接請求的端口如果在這時出現錯誤將拋出IOException異常對象否則將創建ServerSocket對象並開始准備接收連接請求

  接下來服務程序進入無限循環之中無限循環從調用ServerSocket的accept()方法開始在調用開始後accept()方法將導致調用線程阻塞直到連接建立在建立連接後accept()返回一個最近創建的Socket對象該Socket對象綁定了客戶程序的IP地址或端口號

  由於存在單個服務程序與多個客戶程序通訊的可能所以服務程序響應客戶程序不應該花很多時間否則客戶程序在得到服務前有可能花很多時間來等待通訊的建立然而服務程序和客戶程序的會話有可能是很長的(這與電話類似)因此為加快對客戶程序連接請求的響應典型的方法是服務器主機運行一個後台線程這個後台線程處理服務程序和客戶程序的通訊

  為了示范我們在上面談到的慨念並完成SSClient程序下面我們創建一個SSServer程序程序將創建一個ServerSocket對象來監聽端口的連接請求如果成功服務程序將等待連接輸入開始一個線程處理連接並響應來自客戶程序的命令下面就是這段程序的代碼

  Listing : SSServerjava

// SSServerjava

import javaio*;
import *;
import javautil*;

class SSServer
{
 public static void main (String [] args) throws IOException
 { 
  Systemoutprintln (Server starting\n);

  // Create a server socket that listens for incoming connection
  // requests on port

  ServerSocket server = new ServerSocket ();

  while (true)
  {
   // Listen for incoming connection requests from client
   // programs establish a connection and return a Socket
   // object that represents this connection

   Socket s = serveraccept ();

   Systemoutprintln (Accepting Connection\n);

   // Start a thread to handle the connection

   new ServerThread (s)start ();
  }
 }
}

class ServerThread extends Thread
{
 private Socket s;

 ServerThread (Socket s)
 {
  thiss = s;
 }

 public void run ()
 {
  BufferedReader br = null;
  PrintWriter pw = null;

  try
  {
   // Create an input stream reader that chains to the sockets
   // byteoriented input stream The input stream reader
   // converts bytes read from the socket to characters The
   // conversion is based on the platforms default character
   // set

   InputStreamReader isr;
   isr = new InputStreamReader (sgetInputStream ());

   // Create a buffered reader that chains to the input stream
   // reader The buffered reader supplies a convenient method
   // for reading entire lines of text

   br = new BufferedReader (isr);

   // Create a print writer that chains to the sockets byte
   // oriented output stream The print writer creates an
   // intermediate output stream writer that converts
   // characters sent to the socket to bytes The conversion
   // is based on the platforms default character set

   pw = new PrintWriter (sgetOutputStream () true);

   // Create a calendar that makes it possible to obtain date
   // and time information

   Calendar c = CalendargetInstance ();

   // Because the client program may send multiple commands a
   // loop is required Keep looping until the client either
   // explicitly requests termination by sending a command
   // beginning with letters BYE or implicitly requests
   // termination by closing its output stream

   do
   {
    // Obtain the client programs next command

    String cmd = brreadLine ();

    // Exit if client program has closed its output stream

    if (cmd == null)
     break;
  
    // Convert command to uppercase for ease of comparison

    cmd = cmdtoUpperCase ();

    // If client program sends BYE command terminate

    if (cmdstartsWith (BYE))
     break;

    // If client program sends DATE or TIME command return
    // current date/time to the client program

    if (cmdstartsWith (DATE) || cmdstartsWith (TIME))
     pwprintln (cgetTime ()toString ());

    // If client program sends DOM (Day Of Month) command
    // return current day of month to the client program

    if (cmdstartsWith (DOM))
     pwprintln ( + cget (CalendarDAY_OF_MONTH));

    // If client program sends DOW (Day Of Week) command
    // return current weekday (as a string) to the client
    // program

    if (cmdstartsWith (DOW))
     switch (cget (CalendarDAY_OF_WEEK))
    {
     case CalendarSUNDAY : pwprintln (SUNDAY);
      break;

     case CalendarMONDAY : pwprintln (MONDAY);
      break;

     case CalendarTUESDAY : pwprintln (TUESDAY);
      break;

     case CalendarWEDNESDAY: pwprintln (WEDNESDAY);
      break;

     case CalendarTHURSDAY : pwprintln (THURSDAY);
      break;

     case CalendarFRIDAY : pwprintln (FRIDAY);
      break;

     case CalendarSATURDAY : pwprintln (SATURDAY);
    }

    // If client program sends DOY (Day of Year) command
    // return current day of year to the client program

    if (cmdstartsWith (DOY))
     pwprintln ( + cget (CalendarDAY_OF_YEAR));

     // If client program sends PAUSE command sleep for three
     // seconds
 
    if (cmdstartsWith (PAUSE))
    try
    {
     Threadsleep ();
    }
    catch (InterruptedException e)
    {
    }
   }
   while (true);
   {
   catch (IOException e)
   {
    Systemoutprintln (etoString ());
   }
   finally
   {
    Systemoutprintln (Closing Connection\n);

    try
    {
     if (br != null)
      brclose ();

      if (pw != null)
       pwclose ();

      if (s != null)
       sclose ();
    }
    catch (IOException e)
    {
    }
   }
  }
}
  運行這段程序將得到下面的輸出

Server starting
Accepting Connection
Closing Connection
  SSServer的源代碼聲明了一對類SSServer 和ServerThreadSSServer的main()方法創建了一個ServerSocket對象來監聽端口上的連接請求如果成功 SSServer進入一個無限循環中交替調用ServerSocket的 accept() 方法來等待連接請求同時啟動後台線程處理連接(accept()返回的請求)線程由ServerThread繼承的start()方法開始並執行ServerThread的run()方法中的代碼

  一旦run()方法運行線程將創建BufferedReader PrintWriter和 Calendar對象並進入一個循環這個循環由讀(通過BufferedReader的 readLine())來自客戶程序的一行文本開始文本(命令)存儲在cmd引用的string對象中如果客戶程序過早的關閉輸出流會發生什麼呢?答案是cmd將得不到賦值

  注意必須考慮到這種情況在服務程序正在讀輸入流時客戶程序關閉了輸出流如果沒有對這種情況進行處理那麼程序將產生異常

  一旦編譯了SSServer的源代碼通過輸入Java SSServer來運行程序在開始運行SSServer後就可以運行一個或多個SSClient程序
From:http://tw.wingwit.com/Article/program/Java/hx/201311/25867.html
  • 上一篇文章:

  • 下一篇文章:
  • 推薦文章
    Copyright © 2005-2013 電腦知識網 Computer Knowledge   All rights reserved.