Java網(wǎng)絡(luò)socket編程詳解

7.2 面向套接字編程
我們已經(jīng)通過(guò)了解Socket的接口,知其所以然,下面我們就將通過(guò)具體的案例,來(lái)熟悉Socket的具體工作方式

7.2.1使用套接字實(shí)現(xiàn)基于TCP協(xié)議的服務(wù)器和客戶機(jī)程序
依據(jù)TCP協(xié)議,在C/S架構(gòu)的通訊過(guò)程中,客戶端和服務(wù)器的Socket動(dòng)作如下:

客戶端:

1.用服務(wù)器的IP地址和端口號(hào)實(shí)例化Socket對(duì)象。

2.調(diào)用connect方法,連接到服務(wù)器上。

3.將發(fā)送到服務(wù)器的IO流填充到IO對(duì)象里,比如BufferedReader/PrintWriter。

4.利用Socket提供的getInputStream和getOutputStream方法,通過(guò)IO流對(duì)象,向服務(wù)器發(fā)送數(shù)據(jù)流。

  1. 通訊完成后,關(guān)閉打開的IO對(duì)象和Socket。

服務(wù)器:

  1. 在服務(wù)器,用一個(gè)端口來(lái)實(shí)例化一個(gè) ServerSocket對(duì)象。此時(shí),服務(wù)器就可以這個(gè)端口時(shí)刻監(jiān)聽從客戶端發(fā)來(lái)的連接請(qǐng)求。

2.調(diào)用ServerSocket的accept方法,開始監(jiān)聽連接從端口上發(fā)來(lái)的連接請(qǐng)求。

3.利用accept方法返回的客戶端的Socket對(duì)象,進(jìn)行讀寫IO的操作

通訊完成后,關(guān)閉打開的流和Socket對(duì)象。

7.2.1.1 開發(fā)客戶端代碼
根據(jù)上面描述的通訊流程,我們可以按如下的步驟設(shè)計(jì)服務(wù)器端的代碼。

第一步,依次點(diǎn)擊Eclipse環(huán)境里的“文件”|“新建”|“項(xiàng)目”選項(xiàng),進(jìn)入“新建項(xiàng)目”的向?qū)?duì)話框,在其中選中“Java項(xiàng)目”,點(diǎn)擊“下一步”按鈕,在隨后彈出的對(duì)話框里,在其中的“項(xiàng)目名”一欄里,輸入項(xiàng)目名“TCPSocket”,其它的選項(xiàng)目

選擇系統(tǒng)默認(rèn)值,再按“完成”按鈕,結(jié)束創(chuàng)建Java項(xiàng)目的動(dòng)作。

第二步,完成創(chuàng)建項(xiàng)目后,選中集成開發(fā)環(huán)境左側(cè)的項(xiàng)目名“TCPSocket”,點(diǎn)擊右鍵,在隨后彈出的菜單里依次選擇“新建”!“類”的選項(xiàng),創(chuàng)建服務(wù)器類的代碼。

在隨后彈出的“新建Java類”的對(duì)話框里,輸入包名“tcp”,輸入文件名“ServerCode”,請(qǐng)注意大小寫,在“修飾符”里選中“公用”,在“想要?jiǎng)?chuàng)建哪些方法存根”下,選中“public static void main(String[] args )”單選框,同時(shí)把其它兩項(xiàng)目取消掉,再按“完成”按鈕,可以生成代碼。

第三步,在生成的代碼里,編寫引入Java包的代碼,只有當(dāng)我們引入這些包后,我們才能調(diào)用這些包里提供的IO和Socket類的方法。

package tcp;

import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.OutputStreamWriter;

import java.io.PrintWriter;

import java.net.ServerSocket;

import java.net.Socket;

第四步,編寫服務(wù)器端的主體代碼,如下所示。

public class ServerCode

{
// 設(shè)置端口號(hào)
public static int portNo = 3333;
public static void main(String[] args) throws IOException
{
ServerSocket s = new ServerSocket(portNo);

          System.out.println("The Server is start: " + s);

          // 阻塞,直到有客戶端連接

    Socket socket = s.accept();

try

{

                 System.out.println("Accept the Client: " + socket);                   

                 //設(shè)置IO句柄

BufferedReader in = new BufferedReader(new InputStreamReader(socket

                               .getInputStream()));

PrintWriter out = new PrintWriter(new BufferedWriter(

                new OutputStreamWriter(socket.getOutputStream())), true);                    

                 while (true)

{

                        String str = in.readLine();

                        if (str.equals("byebye"))

            {

                               break;

                        }

                        System.out.println("In Server reveived the info: " + str);

                        out.println(str);

                 }

          } 

    finally 

{

                 System.out.println("close the Server socket and the io.");

                 socket.close();

                 s.close();

          }

}

}

這段代碼的主要業(yè)務(wù)邏輯是:

  1.     在上述代碼里的main函數(shù)前,我們?cè)O(shè)置了通訊所用到的端口號(hào),為3333。
    
  2.     在main函數(shù)里,根據(jù)給定3333端口號(hào),初始化一個(gè)ServerSocket對(duì)象s,該對(duì)象用來(lái)承擔(dān)服務(wù)器端監(jiān)聽連接和提供通訊服務(wù)的功能。
    
  3.     調(diào)用ServerSocket對(duì)象的accept方法,監(jiān)聽從客戶端的連接請(qǐng)求。當(dāng)完成調(diào)用accept方法后,整段服務(wù)器端代碼將回阻塞在這里,直到客戶端發(fā)來(lái)connect請(qǐng)求。
    
  4.     當(dāng)客戶端發(fā)來(lái)connect請(qǐng)求,或是通過(guò)構(gòu)造函數(shù)直接把客戶端的Socket對(duì)象連接到服務(wù)器端后,阻塞于此的代碼將會(huì)繼續(xù)運(yùn)行。此時(shí)服務(wù)器端將會(huì)根據(jù)accept方法的執(zhí)行結(jié)果,用一個(gè)Socket對(duì)象來(lái)描述客戶端的連接句柄。
    
  5.     創(chuàng)建兩個(gè)名為in和out的對(duì)象,用來(lái)傳輸和接收通訊時(shí)的數(shù)據(jù)流。
    
  6.     創(chuàng)建一個(gè)while(true)的死循環(huán),在這個(gè)循環(huán)里,通過(guò)in.readLine()方法,讀取從客戶端發(fā)送來(lái)的IO流(字符串),并打印出來(lái)。如果讀到的字符串是“byebye”,那么退出while循環(huán)。
    
  7.     在try…catch…finally語(yǔ)句段里,不論在try語(yǔ)句段里是否發(fā)生異常,并且不論這些異常的種類,finally從句都將會(huì)被執(zhí)行到。在finally從句里,將關(guān)閉描述客戶端的連接句柄socket對(duì)象和ServerSocket類型的s對(duì)象。
    

7.2.1.2 開發(fā)客戶端代碼
我們可以按以下的步驟,開發(fā)客戶端的代碼。

第一,在TCPSocket項(xiàng)目下的tcp包下,創(chuàng)建一個(gè)名為ClientCode.java的文件。在其中編寫引入Java包的代碼,如下所示:

package tcp;

import java.io.BufferedReader;

import java.io.BufferedWriter;

import java.io.IOException;

import java.io.InputStreamReader;

import java.io.OutputStreamWriter;

import java.io.PrintWriter;

import java.net.InetAddress;

import java.net.Socket;

第二,編寫客戶端的主體代碼,如下所示:

public class ClientCode

{

   static String clientName = "Mike";

   //端口號(hào)

public static int portNo = 3333;

   public static void main(String[] args) throws IOException

{

          // 設(shè)置連接地址類,連接本地

          InetAddress addr = InetAddress.getByName("localhost");        

          //要對(duì)應(yīng)服務(wù)器端的3333端口號(hào)

          Socket socket = new Socket(addr, portNo);

          try

{

        System.out.println("socket = " + socket);

                 // 設(shè)置IO句柄

BufferedReader in = new BufferedReader(new InputStreamReader(socket

                               .getInputStream()));

PrintWrite out = new PrintWriter(BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);

                 out.println("Hello Server,I am " + clientName);

                 String str = in.readLine();

                 System.out.println(str);

                 out.println("byebye");

          }

finally

{

                 System.out.println("close the Client socket and the io.");

                 socket.close();

    }

   }

}

上述客戶端代碼的主要業(yè)務(wù)邏輯是:

  1.     同樣定義了通訊端口號(hào),這里給出的端口號(hào)必須要和服務(wù)器端的一致。
    
  2.     在main函數(shù)里,根據(jù)地址信息“l(fā)ocalhost”,創(chuàng)建一個(gè)InetAddress類型的對(duì)象addr。這里,因?yàn)槲覀儼芽蛻舳撕头?wù)器端的代碼都放在本機(jī)運(yùn)行,所以同樣可以用“127.0.0.1”字符串,來(lái)創(chuàng)建InetAddress對(duì)象。
    
  3.     根據(jù)addr和端口號(hào)信息,創(chuàng)建一個(gè)Socket類型對(duì)象,該對(duì)象用來(lái)同服務(wù)器端的ServerSocket類型對(duì)象交互,共同完成C/S通訊流程。
    
  4.     同樣地創(chuàng)建in和out兩類IO句柄,用來(lái)向服務(wù)器端發(fā)送和接收數(shù)據(jù)流。
    
  5.     通過(guò)out對(duì)象,向服務(wù)器端發(fā)送"Hello Server,I am …"的字符串。發(fā)送后,同樣可以用in句柄,接收從服務(wù)器端的消息。
    
  6.     利用out對(duì)象,發(fā)送”byebye”字符串,用以告之服務(wù)器端,本次通訊結(jié)束。
    
  7.     在finally從句里,關(guān)閉Socket對(duì)象,斷開同服務(wù)器端的連接。
    

7.2.1.3 運(yùn)行效果演示
在上述兩部分里,我們分別講述了C/S通訊過(guò)程中服務(wù)器端和客戶端代碼的業(yè)務(wù)邏輯,下面我們將在集成開發(fā)環(huán)境里,演示這里通訊流程。

第一步,選中ServerCode.java代碼,在eclipse的“運(yùn)行”菜單里,選中“運(yùn)行方式”|“1 Java應(yīng)用程序”的菜單,開啟服務(wù)器端的程序。

開啟服務(wù)端程序后,會(huì)在eclipse環(huán)境下方的控制臺(tái)里顯示如下的內(nèi)容:

The Server is start: ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=3333]

在這里,由于ServerSocket對(duì)象并沒(méi)監(jiān)聽到客戶端的請(qǐng)求,所以addr和后面的port值都是初始值。

第二步,按同樣的方法,打開ClientCode.java程序,啟動(dòng)客戶端。啟動(dòng)以后,將在客戶端的控制臺(tái)里看到如下的信息:

socket = Socket[addr=localhost/127.0.0.1,port=3333,localport=1326]

Hello Server,I am Mike

close the Client socket and the io.

從中可以看到,在第一行里,顯示客戶端Socket對(duì)象連接的IP地址和端口號(hào),在第二行里,可以到到客戶端向服務(wù)器端發(fā)送的字符串,而在第三行里,可以看到通訊結(jié)束后,客戶端關(guān)閉連接Socket和IO對(duì)象的提示語(yǔ)句。

第三步,在eclipse下方的控制臺(tái)里,切換到ServerCode服務(wù)端的控制臺(tái)提示信息里,我們可以看到服務(wù)器端在接收到客戶端連接請(qǐng)求后的響應(yīng)信息。

響應(yīng)的信息如下所示:

The Server is start: ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=3333]

Accept the Client: Socket[addr=/127.0.0.1,port=1327,localport=3333]

In Server reveived the info: Hello Server,I am Mike

close the Server socket and the io.

其中,第一行是啟動(dòng)服務(wù)器程序后顯示的信息。在第二行里,顯示從客戶端發(fā)送的連接請(qǐng)求的各項(xiàng)參數(shù)。在第三行里,顯示了從客戶端發(fā)送過(guò)來(lái)的字符串。在第四行里,顯示了關(guān)閉服務(wù)器端ServerSocket和IO對(duì)象的提示信息。從中我們可以看出在服務(wù)器端里accept阻塞和繼續(xù)運(yùn)行的這個(gè)過(guò)程。

通過(guò)上述的操作,我們可以詳細(xì)地觀察到C/S通訊的全部流程,請(qǐng)大家務(wù)必要注意:一定要先開啟服務(wù)器端的程序再開啟客戶端,如果這個(gè)步驟做反的話,客戶端程序會(huì)應(yīng)找不到服務(wù)器端而報(bào)異常。

7.2.2使用套接字連接多個(gè)客戶機(jī)
在7.1的代碼里,客戶端和服務(wù)器之間只有一個(gè)通訊線程,所以它們之間只有一條Socket信道。

如果我們?cè)谕ㄟ^(guò)程序里引入多線程的機(jī)制,可讓一個(gè)服務(wù)器端同時(shí)監(jiān)聽并接收多個(gè)客戶端的請(qǐng)求,并同步地為它們提供通訊服務(wù)。

基于多線程的通訊方式,將大大地提高服務(wù)器端的利用效率,并能使服務(wù)器端能具備完善的服務(wù)功能。

7.2.2.1 開發(fā)客戶端代碼
我們可以按以下的步驟開發(fā)基于多線程的服務(wù)器端的代碼。

第一步,在3.2里創(chuàng)建的“TCPSocket”項(xiàng)目里,新建一個(gè)名為ThreadServer.java的代碼文件,創(chuàng)建文件的方式大家可以參照3.2部分的描述。首先編寫package和import部分的代碼,用來(lái)打包和引入包文件,如下所示:

package tcp;

import java.io.*;

import java.net.*;

第二步,由于我們?cè)诜?wù)器端引入線程機(jī)制,所以我們要編寫線程代碼的主體執(zhí)行類ServerThreadCode,這個(gè)類的代碼如下所示:

class ServerThreadCode extends Thread

{

   //客戶端的socket

   private Socket clientSocket;

   //IO句柄

   private BufferedReader sin;

   private PrintWriter sout;    

   //默認(rèn)的構(gòu)造函數(shù)

   public ServerThreadCode()

   {}  

   public ServerThreadCode(Socket s) throws IOException 

   {

          clientSocket = s;            

          //初始化sin和sout的句柄

          sin = new BufferedReader(new InputStreamReader(clientSocket

                        .getInputStream()));

    sout = new PrintWriter(new BufferedWriter(new OutputStreamWriter(

                        clientSocket.getOutputStream())), true);             

          //開啟線程

          start(); 

   }

   //線程執(zhí)行的主體函數(shù)

   public void run() 

   {

          try 

          {

                 //用循環(huán)來(lái)監(jiān)聽通訊內(nèi)容

                 for(;;) 

                 {

            String str = sin.readLine();

                        //如果接收到的是byebye,退出本次通訊

                        if (str.equals("byebye"))

                        {     

                               break;

                        }     

                        System.out.println("In Server reveived the info: " + str);

                        sout.println(str);

                 }

                 System.out.println("closing the server socket!");

          } 

    catch (IOException e) 

          {

                 e.printStackTrace();

          } 

          finally 

          {

                 System.out.println("close the Server socket and the io.");

                 try 

        {

                        clientSocket.close();

                 } 

                 catch (IOException e) 

                 {

                        e.printStackTrace();

                 }

          }

   }

}

這個(gè)類的業(yè)務(wù)邏輯說(shuō)明如下:

  1.     這個(gè)類通過(guò)繼承Thread類來(lái)實(shí)現(xiàn)線程的功能,也就是說(shuō),在其中的run方法里,定義了該線程啟動(dòng)后要執(zhí)行的業(yè)務(wù)動(dòng)作。
    
  2.     這個(gè)類提供了兩種類型的重載函數(shù)。在參數(shù)類型為Socket的構(gòu)造函數(shù)里, 通過(guò)參數(shù),初始化了本類里的Socket對(duì)象,同時(shí)實(shí)例化了兩類IO對(duì)象。在此基礎(chǔ)上,通過(guò)start方法,啟動(dòng)定義在run方法內(nèi)的本線程的業(yè)務(wù)邏輯。
    
  3.     在定義線程主體動(dòng)作的run方法里,通過(guò)一個(gè)for(;;)類型的循環(huán),根據(jù)IO句柄,讀取從Socket信道上傳輸過(guò)來(lái)的客戶端發(fā)送的通訊信息。如果得到的信息為“byebye”,則表明本次通訊結(jié)束,退出for循環(huán)。
    
  4.     catch從句將處理在try語(yǔ)句里遇到的IO錯(cuò)誤等異常,而在finally從句里,將在通訊結(jié)束后關(guān)閉客戶端的Socket句柄。
    

上述的線程主體代碼將會(huì)在ThreadServer類里被調(diào)用。

第三步,編寫服務(wù)器端的主體類ThreadServer,代碼如下所示:

public class ThreadServer

{

   //端口號(hào)

   static final int portNo = 3333;

   public static void main(String[] args) throws IOException 

   {

          //服務(wù)器端的socket

          ServerSocket s = new ServerSocket(portNo);

          System.out.println("The Server is start: " + s);      

          try 

          {

                 for(;;)                          

                 {

              //阻塞,直到有客戶端連接

                        Socket socket = s.accept();

                        //通過(guò)構(gòu)造函數(shù),啟動(dòng)線程

                    new ServerThreadCode(socket);

                 }

          }

       finally 

          {

                 s.close();

          }

   }

}

這段代碼的主要業(yè)務(wù)邏輯說(shuō)明如下:

  1.     首先定義了通訊所用的端口號(hào),為3333。
    
  2.     在main函數(shù)里,根據(jù)端口號(hào),創(chuàng)建一個(gè)ServerSocket類型的服務(wù)器端的Socket,用來(lái)同客戶端通訊。
    
  3.     在for(;;)的循環(huán)里,調(diào)用accept方法,監(jiān)聽從客戶端請(qǐng)求過(guò)來(lái)的socket,請(qǐng)注意這里又是一個(gè)阻塞。當(dāng)客戶端有請(qǐng)求過(guò)來(lái)時(shí),將通過(guò)ServerThreadCode的構(gòu)造函數(shù),創(chuàng)建一個(gè)線程類,用來(lái)接收客戶端發(fā)送來(lái)的字符串。在這里我們可以再一次觀察ServerThreadCode類,在其中,這個(gè)類通過(guò)構(gòu)造函數(shù)里的start方法,開啟run方法,而在run方法里,是通過(guò)sin對(duì)象來(lái)接收字符串,通過(guò)sout對(duì)象來(lái)輸出。
    
  4.     在finally從句里,關(guān)閉服務(wù)器端的Socket,從而結(jié)束本次通訊。
    

7.2.2.2 開發(fā)客戶端代碼
我們可以按以下的步驟,編寫的基于多線程的客戶端代碼。

第一步,在 “TCPSocket”項(xiàng)目里,新建一個(gè)名為ThreadClient.java的代碼文件。同樣是編寫package和import部分的代碼,用來(lái)打包和引入包文件,如下所示:

package tcp;

import java.net.*;

import java.io.*;

第二步,編寫線程執(zhí)行主體的ClientThreadCode類,同樣,這個(gè)類通過(guò)繼承Thread來(lái)實(shí)現(xiàn)線程的功能。

class ClientThreadCode extends Thread

{

//客戶端的socket

private Socket socket;

//線程統(tǒng)計(jì)數(shù),用來(lái)給線程編號(hào)

private static int cnt = 0;

private int clientId = cnt++;

private BufferedReader in;

private PrintWriter out;

//構(gòu)造函數(shù)

public ClientThreadCode(InetAddress addr)

{

try 

{

  socket = new Socket(addr, 3333);

}

catch(IOException e) 

{

      e.printStackTrace();

}

//實(shí)例化IO對(duì)象

try

{    

  in = new BufferedReader(

         new InputStreamReader(socket.getInputStream()));    

   out = new PrintWriter(

           new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);

    //開啟線程

    start();

 } 

 catch(IOException e) 

 {

    //出現(xiàn)異常,關(guān)閉socket 

      try 

      {

        socket.close();

    } 

      catch(IOException e2) 

      {

          e2.printStackTrace();       

      }

 }

}

//線程主體方法

public void run()

{

try 

{

  out.println("Hello Server,My id is " + clientId );

  String str = in.readLine();

  System.out.println(str);

  out.println("byebye");

} 

catch(IOException e) 

{

   e.printStackTrace();  

}

finally 

{

  try 

  {

    socket.close();

  } 

  catch(IOException e) 

  {

          e.printStackTrace();

  }    

}

}

}

這個(gè)類的主要業(yè)務(wù)邏輯是:

  1.     在構(gòu)造函數(shù)里, 通過(guò)參數(shù)類型為InetAddress類型參數(shù)和3333,初始化了本類里的Socket對(duì)象,隨后實(shí)例化了兩類IO對(duì)象,并通過(guò)start方法,啟動(dòng)定義在run方法內(nèi)的本線程的業(yè)務(wù)邏輯。
    
  2.     在定義線程主體動(dòng)作的run方法里,通過(guò)IO句柄,向Socket信道上傳輸本客戶端的ID號(hào),發(fā)送完畢后,傳輸”byebye”字符串,向服務(wù)器端表示本線程的通訊結(jié)束。
    
  3.     同樣地,catch從句將處理在try語(yǔ)句里遇到的IO錯(cuò)誤等異常,而在finally從句里,將在通訊結(jié)束后關(guān)閉客戶端的Socket句柄。
    

第三步,編寫客戶端的主體代碼,在這段代碼里,將通過(guò)for循環(huán),根據(jù)指定的待創(chuàng)建的線程數(shù)量,通過(guò)ClientThreadCode的構(gòu)造函數(shù),創(chuàng)建若干個(gè)客戶端線程,同步地和服務(wù)器端通訊。

public class ThreadClient

{

public static void main(String[] args)

  throws IOException, InterruptedException 

{

int threadNo = 0;

   InetAddress addr = 

   InetAddress.getByName("localhost");

for(threadNo = 0;threadNo<3;threadNo++)

{

   new ClientThreadCode(addr);

}

}

}

這段代碼執(zhí)行以后,在客戶端將會(huì)有3個(gè)通訊線程,每個(gè)線程首先將先向服務(wù)器端發(fā)送"Hello Server,My id is "的字符串,然后發(fā)送”byebye”,終止該線程的通訊。

7.2.2.3 運(yùn)行效果演示
接下來(lái),我們來(lái)觀察一下基于多線程的C/S架構(gòu)的運(yùn)行效果。

第一步,我們先要啟動(dòng)服務(wù)器端的ThreadServer代碼,啟動(dòng)后,在控制臺(tái)里會(huì)出現(xiàn)如下的提示信息:

The Server is start: ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=3333]

上述的提示信息里,我們同樣可以看到,服務(wù)器在開啟服務(wù)后,會(huì)阻塞在accept這里,直到有客戶端請(qǐng)求過(guò)來(lái)。

第二步,我們?cè)趩?dòng)完服務(wù)器后,運(yùn)行客戶端的ThreadClient.java代碼,運(yùn)行后,我們觀察服務(wù)器端的控制臺(tái),會(huì)出現(xiàn)如下的信息:

The Server is start: ServerSocket[addr=0.0.0.0/0.0.0.0,port=0,localport=3333]

In Server reveived the info: Hello Server,My id is 0

In Server reveived the info: Hello Server,My id is 1

In Server reveived the info: Hello Server,My id is 2

closing the server socket!

close the Server socket and the io.

closing the server socket!

close the Server socket and the io.

closing the server socket!

close the Server socket and the io.

其中,第一行是原來(lái)就有,在后面的幾行里,首先將會(huì)輸出了從客戶端過(guò)來(lái)的線程請(qǐng)求信息,比如

In Server reveived the info: Hello Server,My id is 0

接下來(lái)則會(huì)顯示關(guān)閉Server端的IO和Socket的提示信息。

這里,請(qǐng)大家注意,由于線程運(yùn)行的不確定性,從第二行開始的打印輸出語(yǔ)句的次序是不確定的。但是,不論輸出語(yǔ)句的次序如何變化,我們都可以從中看到,客戶端有三個(gè)線程請(qǐng)求過(guò)來(lái),并且,服務(wù)器端在處理完請(qǐng)求后,會(huì)關(guān)閉Socker和IO。

第三步,當(dāng)我們運(yùn)行完ThreadClient.java的代碼后,并切換到ThreadClient.java的控制臺(tái),我們可以看到如下的輸出:

Hello Server,My id is 0

Hello Server,My id is 2

Hello Server,My id is 1

這說(shuō)明在客戶端開啟了3個(gè)線程,并利用這3個(gè)線程,向服務(wù)器端發(fā)送字符串。

而在服務(wù)器端,用accept方法分別監(jiān)聽到了這3個(gè)線程,并與之對(duì)應(yīng)地也開了3個(gè)線程與之通訊。

7.2.3 UDP協(xié)議與傳輸數(shù)據(jù)報(bào)文
UDP協(xié)議一般應(yīng)用在 “群發(fā)信息”的場(chǎng)合,所以它更可以利用多線程的機(jī)制,實(shí)現(xiàn)多信息的同步發(fā)送。

為了改善代碼的架構(gòu),我們更可以把一些業(yè)務(wù)邏輯的動(dòng)作抽象成方法,并封裝成類,這樣,基于UDP功能的類就可以在其它應(yīng)用項(xiàng)目里被輕易地重用。

7.2.3.1 開發(fā)客戶端代碼
如果我們把客戶端的所有代碼都寫在一個(gè)文件中,那么代碼的功能很有可能都聚集在一個(gè)方法力,代碼的可維護(hù)性將會(huì)變得很差。

所以我們專門設(shè)計(jì)了ClientBean類,在其中封裝了客戶端通訊的一些功能方法,在此基礎(chǔ)上,通過(guò)UDPClient.java文件,實(shí)現(xiàn)UDP客戶端的功能。

另外,在這里以及以后的代碼里,我們不再詳細(xì)講述用Eclipse開發(fā)和運(yùn)行Java程序的方法,而是重點(diǎn)講述Java代碼的業(yè)務(wù)邏輯和主要工作流程。

首先,我們可以按如下的步驟,設(shè)計(jì)ClientBean這個(gè)類。通過(guò)import語(yǔ)句,引入所用到的類庫(kù),代碼如下所示。

import java.io.IOException;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.InetAddress;

import java.net.SocketException;

import java.net.UnknownHostException;

第二,定義ClientBean所用到的變量,并給出針對(duì)這些變量操作的get和set類型的方法,代碼如下所示。

//描述UDP通訊的DatagramSocket對(duì)象

private DatagramSocket ds;

//用來(lái)封裝通訊字符串

private byte buffer[];

//客戶端的端口號(hào)

private int clientport ;

//服務(wù)器端的端口號(hào)

private int serverport;

//通訊內(nèi)容

private String content;

//描述通訊地址

private InetAddress ia;

//以下是各屬性的Get和Set類型方法

public byte[] getBuffer()

{

   return buffer;

}

public void setBuffer(byte[] buffer)

{

   this.buffer = buffer;

}

public int getClientport()

{

return clientport;

}

public void setClientport(int clientport)

{

   this.clientport = clientport;

}

public String getContent()

{

   return content;

}

public void setContent(String content)

{

   this.content = content;

}

public DatagramSocket getDs()

{

   return ds;

}

public void setDs(DatagramSocket ds)

{

   this.ds = ds;

}

public InetAddress getIa()

{

   return ia;

}

public void setIa(InetAddress ia)

{

   this.ia = ia;

}

public int getServerport()

{

   return serverport;

}

public void setServerport(int serverport)

{

this.serverport = serverport;

}

在上述的代碼里,我們定義了描述用來(lái)實(shí)現(xiàn)UDP通訊的DatagramSocket類型對(duì)象ds,描述客戶端和服務(wù)器端的端口號(hào)clientport和serverport,用于描述通訊信息的buffer和content對(duì)象,其中,buffer對(duì)象是byte數(shù)組類型的,可通過(guò)UDP的數(shù)據(jù)報(bào)文傳輸,而content是String類型的,在應(yīng)用層面表示用戶之間的通訊內(nèi)容,另外還定義了InetAddress類型的ia變量,用來(lái)封裝通訊地址信息。

在隨后定義的一系列g(shù)et和set方法里,給出了設(shè)置和獲取上述變量的方法。

第三,編寫該類的構(gòu)造函數(shù),代碼如下所示。

public ClientBean() throws SocketException, UnknownHostException

{

   buffer = new byte[1024];

   clientport = 1985;

   serverport = 1986;

   content = "";

   ds = new DatagramSocket(clientport);

   ia = InetAddress.getByName("localhost");

}

在這個(gè)構(gòu)造函數(shù)里,我們給各變量賦予了初始值,其中分別設(shè)置了客戶端和服務(wù)器端的端口號(hào)分別為1985和1986,設(shè)置了通訊連接地址為本地,并根據(jù)客戶端的端口號(hào)初始化了DatagramSocket對(duì)象。

當(dāng)程序員初始化ClientBean類時(shí),這段構(gòu)造函數(shù)會(huì)自動(dòng)執(zhí)行,完成設(shè)置通訊各參數(shù)等工作。

第四,編寫向服務(wù)器端發(fā)送消息的sendToServer方法,代碼如下所示。

public void sendToServer() throws IOException

{

   buffer = content.getBytes();

   ds.send(new DatagramPacket(buffer,content.length(),ia,serverport));

}

在這段代碼里,根據(jù)String類型的表示通訊信息的content變量,初始化UDP數(shù)據(jù)報(bào)文,即DatagramPacket對(duì)象,并通過(guò)調(diào)用DatagramSocket類型對(duì)象的send方法,發(fā)送該UDP報(bào)文。

縱觀ClientBean類,我們可以發(fā)現(xiàn)在其中封裝了諸如通訊端口、通訊內(nèi)容和通訊報(bào)文等對(duì)象以及以UDP方式發(fā)送信息的sendToServer方法。所以,在UDPClient類里,可以直接調(diào)用其中的接口,方便地實(shí)現(xiàn)通訊功能。

其次,我們可以按如下的步驟,設(shè)計(jì)UDPClient這個(gè)類。

第一步,通過(guò)import語(yǔ)句,引入所用到的類庫(kù),代碼如下所示。

import java.io.BufferedReader;

import java.io.IOException;

import java.io.InputStreamReader;

第二步,編寫線程相關(guān)的代碼。

由于我們要在UDP客戶端里通過(guò)多線程的機(jī)制,同時(shí)開多個(gè)客戶端,向服務(wù)器端發(fā)送通訊內(nèi)容,所以我們的UDPClient類必須要實(shí)現(xiàn)Runnable接口,并在其中覆蓋掉Runnable接口里的run方法。定義類和實(shí)現(xiàn)run方法的代碼如下所示。

public class UDPClient implements Runnable

{

public static String content;

public static ClientBean client;

public void run()

{

   try

{

          client.setContent(content);

          client.sendToServer();

   }

catch(Exception ex)

{

          System.err.println(ex.getMessage());

   }

}//end of run

//main 方法

//…

}

在上述代碼的run方法里,我們主要通過(guò)了ClientBean類里封裝的方法,設(shè)置了content內(nèi)容,并通過(guò)了sentToServer方法,將content內(nèi)容以數(shù)據(jù)報(bào)文的形式發(fā)送到服務(wù)器端。

一旦線程被開啟,系統(tǒng)會(huì)自動(dòng)執(zhí)行定義在run方法里的動(dòng)作。

第三步,編寫主方法。在步驟(2)里的//main方法注釋的位置,我們可以插入U(xiǎn)DPClient類的main方法代碼,具體如下所示。

public static void main(String args[]) throws IOException

{

   BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

   client = new ClientBean();

   System.out.println("客戶端啟動(dòng)...");

   while(true)

{

          //接收用戶輸入

content = br.readLine();

          //如果是end或空,退出循環(huán)

if(content==null||content.equalsIgnoreCase("end")||content.equalsIgnoreCase(""))

{

                 break;

          }

          //開啟新線程,發(fā)送消息

new Thread(new UDPClient()).start();

   }            

}

這段代碼的主要業(yè)務(wù)邏輯是,首先初始化了BufferedReader類型的br對(duì)象,該對(duì)象可以接收從鍵盤輸入的字符串。隨后啟動(dòng)一個(gè)while(true)的循環(huán),在這個(gè)循環(huán)體里,接收用戶從鍵盤的輸入,如果用戶輸入的字符串不是“end”,或不是為空,則開啟一個(gè)UDPClient類型的線程,并通過(guò)定義在run方法里的線程主體動(dòng)作,發(fā)送接收到的消息。如果在循環(huán)體里,接收到“end”或空字符,則通過(guò)break語(yǔ)句,退出循環(huán)。

從上述代碼里,我們可以看出,對(duì)于每次UDP發(fā)送請(qǐng)求,UDPClient類都將會(huì)啟動(dòng)一個(gè)線程來(lái)發(fā)送消息。

7.2.3.2 開發(fā)客戶端代碼
同樣,我們把服務(wù)器端所需要的一些通用方法以類的形式封裝,而在UDP的服務(wù)器端,通過(guò)調(diào)用封裝在ServerBean類里的方法來(lái)完成信息的接收工作。

首先,我們可以按如下的步驟,設(shè)計(jì)ServerBean類的代碼。

第一步,通過(guò)import語(yǔ)句,引入所用到的類庫(kù),代碼如下所示。

import java.io.IOException;

import java.net.DatagramPacket;

import java.net.DatagramSocket;

import java.net.InetAddress;

import java.net.SocketException;

import java.net.UnknownHostException;

第二步,同樣定義ServerBean類里用到的變量,并給出針對(duì)這些變量操作的get和set類型的方法。由于這里的代碼和ClientBean類里的非常相似,所以不再贅述,代碼部分大家可以參考光盤上。

第三步,編寫該類的構(gòu)造函數(shù),在這個(gè)構(gòu)造函數(shù)里,給該類里的一些重要屬性賦了初值,代碼如下所示。

public ServerBean() throws SocketException, UnknownHostException

{

   buffer = new byte[1024];

   clientport = 1985;

   serverport = 1986;

   content = "";

   ds = new DatagramSocket(serverport);

   ia = InetAddress.getByName("localhost");

}

從中我們可以看到,在UDP的服務(wù)端里,為了同客戶端對(duì)應(yīng),所以同樣把clientport和serverport值設(shè)置為1985和1986,同時(shí)初始化了DatagramSocket對(duì)象,并把服務(wù)器的地址也設(shè)置成本地。

第四,編寫實(shí)現(xiàn)監(jiān)聽客戶端請(qǐng)求的listenClient方法,代碼如下所示。

public void listenClient() throws IOException

{

   //在循環(huán)體里接收消息

while(true)

{

    //初始化DatagramPacket類型的變量

DatagramPacket dp = new DatagramPacket(buffer,buffer.length);

          //接收消息,并把消息通過(guò)dp參數(shù)返回

ds.receive(dp);

          content = new String(dp.getData(),0,dp.getLength());

          //打印消息

print();

   }

}

在這個(gè)方法里,構(gòu)造了一個(gè)while(true)的循環(huán),在這個(gè)循環(huán)體內(nèi)部,調(diào)用了封裝在DatagramSocket類型里的receive方法,接收客戶端發(fā)送過(guò)來(lái)的UDP報(bào)文,并通過(guò)print方法,把報(bào)文內(nèi)容打印出來(lái)。

而print方法的代碼比較簡(jiǎn)單,只是通過(guò)輸出語(yǔ)句,打印報(bào)文里的字符串。

public void print()

{

   System.out.println(content);

}

而UDP通訊的服務(wù)器端代碼相對(duì)簡(jiǎn)單,以下是UDPServer類的全部代碼。

import java.io.IOException;

public class UDPServer

{

   public static void main(String args[]) throws IOException

{

          System.out.println("服務(wù)器端啟動(dòng)...");

          //初始化ServerBean對(duì)象

ServerBean server = new ServerBean();

          //開啟監(jiān)聽程序

server.listenClient();

   }

}

從上述代碼里,我們可以看到,在UDP的服務(wù)器端里,主要通過(guò)ServerBean類里提供的listenClient方法,監(jiān)聽從客戶端發(fā)送過(guò)來(lái)的UDP報(bào)文,并通過(guò)解析得到其中包含的字符串,隨后輸出。

7.3.2.3 開發(fā)客戶端代碼
由于我們已經(jīng)講述過(guò)通過(guò)Eclipse查看代碼運(yùn)行結(jié)果的詳細(xì)步驟,所以這里我們將直接通過(guò)命令行的方式,通過(guò)javac和java等命令,查看基于多線程UDP通訊的演示效果。

  1.     首先我們把剛才編寫好的四段java代碼(即ClientBean.java、UDPClient.java、ServerBean.java和UDPServer.java)放到D盤下的work目錄下(如果沒(méi)有則新建)。
    
  2.     點(diǎn)擊“開始菜單”|“運(yùn)行”選項(xiàng),并在“運(yùn)行程序”的對(duì)話框里輸入”cmd”命令,進(jìn)入DOS命令界面,并進(jìn)入到D:\work這個(gè)目錄里。
    
  3.     如果大家已經(jīng)按照第一章的說(shuō)明,成功地配置好關(guān)于java的path和classpath環(huán)境變量,在這里可以直接運(yùn)行javac *.java命令,編譯這四個(gè).java文件,編譯后,會(huì)在D:\work目錄下產(chǎn)生同四個(gè)java文件相對(duì)應(yīng)的.class文件。
    
  4.     在這個(gè)命令窗口里運(yùn)行java UDPServer命令,通過(guò)運(yùn)行UDPServer代碼,開啟UDP服務(wù)器端程序,開啟后,會(huì)出現(xiàn)如圖7-3所示的信息。
    

圖7-3啟動(dòng)UDP服務(wù)端后的效果

  1.     在出現(xiàn)上圖的效果后,別關(guān)閉這個(gè)命令窗口,按步驟(2)里說(shuō)明的流程,新開啟一個(gè)DOS命令窗口,并同樣進(jìn)入到D:\work這個(gè)目錄下。
    
  2.     在新窗口里輸入java UDPClient,開啟UDP客戶端程序。開啟后,可通過(guò)鍵盤向服務(wù)器端輸入通訊字符串,這些字符串將會(huì)以數(shù)據(jù)報(bào)文的形式發(fā)送到服務(wù)器端。
    

在圖7-4里,演示了UDP客戶端向服務(wù)器端發(fā)送消息的效果。

圖7-4 UDP客戶端發(fā)送消息的效果

每當(dāng)我們?cè)诳蛻舳税l(fā)送一條消息,服務(wù)器端會(huì)收到并輸出這條消息,從代碼里我們可以得知,每條消息是通過(guò)為之新開啟的線程發(fā)送到服務(wù)器端的。

如果我們?cè)诳蛻舳溯斎搿眅nd”或空字符串,客戶端的UDPClient代碼會(huì)退出。在圖7-5里演示了UDP服務(wù)器端接收并輸出通訊字符串的效果。

圖7-5 UDP服務(wù)器端接收到消息的效果

  1.     由于UDPServer.java代碼里,我們通過(guò)一個(gè)while(true)的循環(huán)來(lái)監(jiān)聽客戶端的請(qǐng)求,所以當(dāng)程序運(yùn)行結(jié)束后,可通過(guò)Ctrl+C的快捷鍵的方式退出這段程序。
    
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,401評(píng)論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,069評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評(píng)論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,635評(píng)論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,128評(píng)論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評(píng)論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,365評(píng)論 0 288
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,881評(píng)論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,733評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,935評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評(píng)論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,172評(píng)論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評(píng)論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評(píng)論 1 282
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,595評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,908評(píng)論 2 372

推薦閱讀更多精彩內(nèi)容

  • 計(jì)算機(jī)網(wǎng)絡(luò)概述 網(wǎng)絡(luò)編程的實(shí)質(zhì)就是兩個(gè)(或多個(gè))設(shè)備(例如計(jì)算機(jī))之間的數(shù)據(jù)傳輸。 按照計(jì)算機(jī)網(wǎng)絡(luò)的定義,通過(guò)一定...
    蛋炒飯_By閱讀 1,235評(píng)論 0 10
  • 網(wǎng)絡(luò)編程 網(wǎng)絡(luò)編程對(duì)于很多的初學(xué)者來(lái)說(shuō),都是很向往的一種編程技能,但是很多的初學(xué)者卻因?yàn)楹荛L(zhǎng)一段時(shí)間無(wú)法進(jìn)入網(wǎng)絡(luò)編...
    程序員歐陽(yáng)閱讀 2,032評(píng)論 1 37
  • 1 近期,自己的記憶力明顯下降。 昨天,同一個(gè)水杯,我丟了兩次,雖然幾經(jīng)波折,最終還是順利的丟了。 這兩天,我莫名...
    安和然閱讀 212評(píng)論 1 1
  • 到底溫煦和溫馨有什么區(qū)別? 最近老是想著怎么把閨女喂的水靈?今天晚上我就給她做了西米水果撈,成本可...
    落雪小依閱讀 184評(píng)論 0 0
  • 學(xué)會(huì)一直向前看,日子會(huì)越過(guò)越好,加油!晚安!
    稻城禾歡閱讀 80評(píng)論 0 0