隨筆 - 312, 文章 - 14, 評論 - 1393, 引用 - 0
          數據加載中……

          Java網絡編程從入門到精通(18):Socket類的getter和setter方法(2)

          本文為原創,如需轉載,請注明作者和出處,謝謝!


          上一篇:Java網絡編程從入門到精通(17):Socket類的getter和setter方法(1)

          二、
          用于獲得和設置Socket選項的gettersetter方法

          Socket選擇可以指定Socket類發送和接受數據的方式。在JDK1.4中共有8Socket選擇可以設置。這8個選項都定義在java.net.SocketOptions接口中。定義如下:


              public final static int TCP_NODELAY = 0x0001;

              public final static int SO_REUSEADDR = 0x04;

              public final static int SO_LINGER = 0x0080;

              
          public final static int SO_TIMEOUT = 0x1006;

              
          public final static int SO_SNDBUF = 0x1001;

              
          public final static int SO_RCVBUF = 0x1002;

              
          public final static int SO_KEEPALIVE = 0x0008;

              
          public final static int SO_OOBINLINE = 0x1003;

              有趣的是,這8個選項除了第一個沒在SO前綴外,其他7個選項都以SO作為前綴。其實這個SO就是Socket Option的縮寫;因此,在Java中約定所有以SO為前綴的常量都表示Socket選項;當然,也有例外,如TCP_NODELAY。在Socket類中為每一個選項提供了一對getset方法,分別用來獲得和設置這些選項。

          1. TCP_NODELAY

          public boolean getTcpNoDelay() throws SocketException
          public void setTcpNoDelay(boolean on) throws SocketException

          在默認情況下,客戶端向服務器發送數據時,會根據數據包的大小決定是否立即發送。當數據包中的數據很少時,如只有1個字節,而數據包的頭卻有幾十個字節(IP+TCP頭)時,系統會在發送之前先將較小的包合并到軟大的包后,一起將數據發送出去。在發送下一個數據包時,系統會等待服務器對前一個數據包的響應,當收到服務器的響應后,再發送下一個數據包,這就是所謂的Nagle算法;在默認情況下,Nagle算法是開啟的。

          這種算法雖然可以有效地改善網絡傳輸的效率,但對于網絡速度比較慢,而且對實現性的要求比較高的情況下(如游戲、Telnet等),使用這種方式傳輸數據會使得客戶端有明顯的停頓現象。因此,最好的解決方案就是需要Nagle算法時就使用它,不需要時就關閉它。而使用setTcpToDelay正好可以滿足這個需求。當使用setTcpNoDelay(true)Nagle算法關閉后,客戶端每發送一次數據,無論數據包的大小都會將這些數據發送出去。

          2.  SO_REUSEADDR

          public boolean getReuseAddress() throws SocketException           
          public void setReuseAddress(boolean on) throws SocketException

          通過這個選項,可以使多個Socket對象綁定在同一個端口上。其實這樣做并沒有多大意義,但當使用close方法關閉Socket連接后,Socket對象所綁定的端口并不一定馬上釋放;系統有時在Socket連接關閉才會再確認一下是否有因為延遲面未到達的數據包,這完全是在底層處理的,也就是說對用戶是透明的;因此,在使用Socket類時完全不會感覺到。

          這種處理機制對于隨機綁定端口的Socket對象沒有什么影響,但對于綁定在固定端口的Socket對象就可能會拋出“Address already in use: JVM_Bind”例外。因此,使用這個選項可以避免個例外的發生。

          package mynet;

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

          public class Test
          {
              
          public static void main(String[] args)
              {
                  Socket socket1 
          = new Socket();
                  Socket socket2 
          = new Socket();
                  
          try
                  {
                      socket1.setReuseAddress(
          true);
                      socket1.bind(
          new InetSocketAddress("127.0.0.1"88));
                      System.out.println(
          "socket1.getReuseAddress():"
                              
          + socket1.getReuseAddress());
                      socket2.bind(
          new InetSocketAddress("127.0.0.1"88));
                  }
                  
          catch (Exception e)
                  {
                      System.out.println(
          "error:" + e.getMessage());
                      
          try
                      {
                          socket2.setReuseAddress(
          true);
                          socket2.bind(
          new InetSocketAddress("127.0.0.1"88));
                          System.out.println(
          "socket2.getReuseAddress():"
                                  
          + socket2.getReuseAddress());
                          System.out.println(
          "端口88第二次綁定成功!");
                      }
                      
          catch (Exception e1)
                      {
                          System.out.println(e.getMessage());
                      }
                  }
              }
          }

          上面的代碼的運行結果如下:

          socket1.getReuseAddress():true
          error:Address already in use: JVM_Bind
          socket2.getReuseAddress():true
          端口88第二次綁定成功!

          使用SO_REUSEADDR選項時有兩點需要注意:
          1.  必須在調用bind方法之前使用setReuseAddress方法來打開SO_REUSEADDR選項。因此,要想使用SO_REUSEADDR選項,就不能通過Socket類的構造方法來綁定端口。
          2.  必須將綁定同一個端口的所有的Socket對象的SO_REUSEADDR選項都打開才能起作用。如在例程4-12中,socket1和socket2都使用了setReuseAddress方法打開了各自的SO_REUSEADDR選項。

          3.  SO_LINGER

          public int getSoLinger() throws SocketException
          public void setSoLinger(boolean on, int linger) throws SocketException

          這個Socket選項可以影響close方法的行為。在默認情況下,當調用close方法后,將立即返回;如果這時仍然有未被送出的數據包,那么這些數據包將被丟棄。如果將linger參數設為一個正整數n(n的值最大是65,535),在調用close方法后,將最多被阻塞n秒。在這n秒內,系統將盡量將未送出的數據包發送出去;如果超過了n秒,如果還有未發送的數據包,這些數據包將全部被丟棄;而close方法會立即返回。如果將linger設為0,和關閉SO_LINGER選項的作用是一樣的。

          如果底層的Socket實現不支持SO_LINGER都會拋出SocketException例外。當給linger參數傳遞負數值時,setSoLinger還會拋出一個IllegalArgumentException例外。可以通過getSoLinger方法得到延遲關閉的時間,如果返回-1,則表明SO_LINGER是關閉的。例如,下面的代碼將延遲關閉的時間設為1分鐘:

          if(socket.getSoLinger() == -1) socket.setSoLinger(true60);

          4.  SO_TIMEOUT

          public int getSoTimeout() throws SocketException
          public void setSoTimeout(int timeout) throws SocketException

          這個Socket選項在前面已經討論過。可以通過這個選項來設置讀取數據超時。當輸入流的read方法被阻塞時,如果設置timeouttimeout的單位是毫秒),那么系統在等待了timeout毫秒后會拋出一個InterruptedIOException例外。在拋出例外后,輸入流并未關閉,你可以繼續通過read方法讀取數據。

          如果將timeout設為0,就意味著read將會無限等待下去,直到服務端程序關閉這個Socket。這也是timeout的默認值。如下面的語句將讀取數據超時設為30秒:

          socket1.setSoTimeout(30 * 1000);

          當底層的Socket實現不支持SO_TIMEOUT選項時,這兩個方法將拋出SocketException例外。不能將timeout設為負數,否則setSoTimeout方法將拋出IllegalArgumentException例外。

          5.  SO_SNDBUF

          public int getSendBufferSize() throws SocketException
          public void setSendBufferSize(int size) throws SocketException

          在默認情況下,輸出流的發送緩沖區是8096個字節(8K)。這個值是Java所建議的輸出緩沖區的大小。如果這個默認值不能滿足要求,可以用setSendBufferSize方法來重新設置緩沖區的大小。但最好不要將輸出緩沖區設得太小,否則會導致傳輸數據過于頻繁,從而降低網絡傳輸的效率。

          如果底層的Socket實現不支持SO_SENDBUF選項,這兩個方法將會拋出SocketException例外。必須將size設為正整數,否則setSendBufferedSize方法將拋出IllegalArgumentException例外。

          6.  SO_RCVBUF


          public int getReceiveBufferSize() throws SocketException
          public void setReceiveBufferSize(int size) throws SocketException

          在默認情況下,輸入流的接收緩沖區是8096個字節(8K)。這個值是Java所建議的輸入緩沖區的大小。如果這個默認值不能滿足要求,可以用setReceiveBufferSize方法來重新設置緩沖區的大小。但最好不要將輸入緩沖區設得太小,否則會導致傳輸數據過于頻繁,從而降低網絡傳輸的效率。

          如果底層的Socket實現不支持SO_RCVBUF選項,這兩個方法將會拋出SocketException例外。必須將size設為正整數,否則setReceiveBufferSize方法將拋出IllegalArgumentException例外。

          7.  SO_KEEPALIVE

          public boolean getKeepAlive() throws SocketException
          public void setKeepAlive(boolean on) throws SocketException

          如果將這個Socket選項打開,客戶端Socket每隔段的時間(大約兩個小時)就會利用空閑的連接向服務器發送一個數據包。這個數據包并沒有其它的作用,只是為了檢測一下服務器是否仍處于活動狀態。如果服務器未響應這個數據包,在大約11分鐘后,客戶端Socket再發送一個數據包,如果在12分鐘內,服務器還沒響應,那么客戶端Socket將關閉。如果將Socket選項關閉,客戶端Socket在服務器無效的情況下可能會長時間不會關閉。SO_KEEPALIVE選項在默認情況下是關閉的,可以使用如下的語句將這個SO_KEEPALIVE選項打開:

          socket1.setKeepAlive(true);

          8.  SO_OOBINLINE

           public boolean getOOBInline() throws SocketException
           
          public void setOOBInline(boolean on) throws SocketException

          如果這個Socket選項打開,可以通過Socket類的sendUrgentData方法向服務器發送一個單字節的數據。這個單字節數據并不經過輸出緩沖區,而是立即發出。雖然在客戶端并不是使用OutputStream向服務器發送數據,但在服務端程序中這個單字節的數據是和其它的普通數據混在一起的。因此,在服務端程序中并不知道由客戶端發過來的數據是由OutputStream還是由sendUrgentData發過來的。下面是sendUrgentData方法的聲明:

          public void sendUrgentData(int data) throws IOException

          雖然sendUrgentData的參數dataint類型,但只有這個int類型的低字節被發送,其它的三個字節被忽略。下面的代碼演示了如何使用SO_OOBINLINE選項來發送單字節數據。

          package mynet;

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

          class Server
          {
              
          public static void main(String[] args) throws Exception
              {
                  ServerSocket serverSocket 
          = new ServerSocket(1234);
                  System.out.println(
          "服務器已經啟動,端口號:1234");
                  
          while (true)
                  {
                      Socket socket 
          = serverSocket.accept();
                      socket.setOOBInline(
          true);
                      InputStream in 
          = socket.getInputStream();
                      InputStreamReader inReader 
          = new InputStreamReader(in);
                      BufferedReader bReader 
          = new BufferedReader(inReader);
                      System.out.println(bReader.readLine());
                      System.out.println(bReader.readLine());
                      socket.close();
                  }
              }
          }
          public class Client
          {
              
          public static void main(String[] args) throws Exception
              {
                  Socket socket 
          = new Socket("127.0.0.1"1234);
                  socket.setOOBInline(
          true);
                  OutputStream out 
          = socket.getOutputStream();
                  OutputStreamWriter outWriter 
          = new OutputStreamWriter(out);
                  outWriter.write(
          67);              // 向服務器發送字符"C"
                  outWriter.write("hello world\r\n");
                  socket.sendUrgentData(
          65);        // 向服務器發送字符"A"
                  socket.sendUrgentData(322);        // 向服務器發送字符"B"
                  outWriter.flush();
                  socket.sendUrgentData(
          214);       // 向服務器發送漢字”中”
                  socket.sendUrgentData(208);
                  socket.sendUrgentData(
          185);       // 向服務器發送漢字”國”
                  socket.sendUrgentData(250);
                  socket.close();
              }
          }

          由于運行上面的代碼需要一個服務器類,因此,在加了一個類名為Server的服務器類,關于服務端套接字的使用方法將會在后面的文章中詳細討論。在類Server類中只使用了ServerSocket類的accept方法接收客戶端的請求。并從客戶端傳來的數據中讀取兩行字符串,并顯示在控制臺上。

          測試

          由于本例使用了127.0.0.1,因ServerClient類必須在同一臺機器上運行。

          運行Server

          java mynet.Server

          運行Client

          java mynet.Client

          在服務端控制臺的輸出結果

          服務器已經啟動,端口號:1234
          ABChello world
          中國

          在ClienT類中使用了sendUrgentData方法向服務器發送了字符'A'(65)'B'(66)。但發送'B'時實際發送的是322,由于sendUrgentData只發送整型數的低字節。因此,實際發送的是66。十進制整型322的二進制形式如圖1所示。

           

          圖1  十進制整型322的二進制形式

               從圖1可以看出,雖然322分布在了兩個字節上,但它的低字節仍然是66。

          在Client類中使用flush將緩沖區中的數據發送到服務器。我們可以從輸出結果發現一個問題,在Client類中先后向服務器發送了'C'"hello world"r"n"'A''B'。而在服務端程序的控制臺上顯示的卻是ABChello world。這種現象說明使用sendUrgentData方法發送數據后,系統會立即將這些數據發送出去;而使用write發送數據,必須要使用flush方法才會真正發送數據。

          在Client類中向服務器發送"中國"字符串。由于""是由214208兩個字節組成的;而""是由185250兩個字節組成的;因此,可分別發送這四個字節來傳送"中國"字符串。

          注意:在使用setOOBInline方法打開SO_OOBINLINE選項時要注意是必須在客戶端和服務端程序同時使用setOOBInline方法打開這個選項,否則無法命名用sendUrgentData來發送數據。

          下一篇:Java網絡編程從入門到精通(19):套接字(Socket)的異常





          Android開發完全講義(第2版)(本書版權已輸出到臺灣)

          http://product.dangdang.com/product.aspx?product_id=22741502



          Android高薪之路:Android程序員面試寶典 http://book.360buy.com/10970314.html


          新浪微博:http://t.sina.com.cn/androidguy   昵稱:李寧_Lining

          posted on 2009-06-01 17:31 銀河使者 閱讀(3739) 評論(0)  編輯  收藏 所屬分類: java 原創網絡編程

          主站蜘蛛池模板: 海林市| 九台市| 全南县| 荆州市| 岳阳县| 泉州市| 玛纳斯县| 富锦市| 鹤岗市| 定陶县| 酉阳| 施甸县| 花垣县| 蚌埠市| 车致| 元阳县| 锡林浩特市| 呼和浩特市| 宜兰市| 朝阳县| 深圳市| 泾川县| 昌黎县| 海宁市| 马山县| 拉孜县| 油尖旺区| 扎鲁特旗| 祁东县| 开江县| 南投市| 日喀则市| 石家庄市| 太仆寺旗| 浦东新区| 运城市| 秦皇岛市| 江口县| 光山县| 搜索| 屏山县|