JAVA Socket 底層是怎樣基於TCP/IP實現的???

OneCode2World發表於2015-08-05

JAVA Socket 底層是怎樣基於TCP/IP 實現的???


首先必須明確:TCP/IP模型中有四層結構:
     應用層(Application Layer)、傳輸層(Transport Layer)、網路層(Internet Layer  )、鏈路層(LinkLayer)
 其中Ip協議(InternetProtocol)是位於網路層的,TCP協議時位於傳輸層的。通過Ip協議可以使可以使兩臺計算機使用同一種語言,從而允許Internet上連線不同型別的計算機和不同作業系統的網路。Ip協議只保證計算機能夠接收和傳送分組資料。當計算機要和遠端的計算機建立連線時,TCP協議會讓他們建立連線:用於傳送和接收資料的虛擬電路。



在JAVA中,我們用 ServerSocket、Socket類建立一個套接字連線,從套接字得到的結果是一個InputStream以及OutputStream物件,以便將連線作為一個IO流物件對待。通過IO流可以從流中讀取資料或者寫資料到流中,讀寫IO流會有異常IOException產生。

套接字
或插座(socket)是一種軟體形式的抽象,用於表達兩臺機器間一個連線的“終端”。針對一個特定的連線,每臺機器上都有一個“套接字”,可以想象它們之間有一條虛擬的“線纜”。JAVA有兩個基於資料流的套接字類:ServerSocket,伺服器用它“偵聽”進入的連線;Socket,客戶端用它初始一次連線。偵聽套接字只能接收新的連線請求,不能接收實際的資料包,即ServerSocket不能接收實際的資料包。

  套接字是基於TCP/IP實現的,它是用來提供一個訪問TCP的服務介面,或者說套接字socket是TCP的應用程式設計介面API,通過應用層就可以訪問TCP提供的服務。

在JAVA中,我們用ServerSocket、Socket類建立一個套接字連線,從套接字得到的結果是一個InputStream以及OutputStream物件,以便將連線作為一個IO流物件對待。通過IO流可以從流中讀取資料或者寫資料到流中,讀寫IO流會有異常IOException產生。

 套接字底層是基於TCP的,所以socket的超時和TCP超時是相同的。下面先討論套接字讀寫緩衝區,接著討論連線建立超時、讀寫超時以及JAVA套接字程式設計的巢狀異常捕獲和一個超時例子程式的抓包示例。

1socket讀寫緩衝區

 一旦建立了一個套接字例項,作業系統就會為其分配緩衝區以存放接收和要傳送的資料。

 

 

 JAVA可以設定讀寫緩衝區的大小-setReceiveBufferSize(int size),setSendBufferSize(int size)。

 向輸出流寫資料並不意味著資料實際上已經被髮送,它們只是被複制到了傳送緩衝區佇列SendQ,就是在Socket的OutputStream上呼叫flush()方法,也不能保證資料能夠立即傳送到網路。真正的資料傳送是由作業系統的TCP協議棧模組從緩衝區中取資料傳送到網路來完成的。

 當有資料從網路來到時,TCP協議棧模組接收資料並放入接收緩衝區佇列RecvQ,輸入流InputStream通過read方法從RecvQ中取出資料。

2 socket連線建立超時

 socket連線建立是基於TCP的連線建立過程。TCP的連線需要通過3次握手報文來完成,開始建立TCP連線時需要傳送同步SYN報文,然後等待確認報文SYN+ACK,最後再傳送確認報文ACK。TCP連線的關閉通過4次揮手來完成,主動關閉TCP連線的一方傳送FIN報文,等待對方的確認報文;被動關閉的一方也傳送FIN報文,然等待確認報文。


 正在等待TCP連線請求的一端有一個固定長度的連線佇列,該佇列中的連線已經被TCP接受(即三次握手已經完成),但還沒有被應用層所接受TCP接受一個連線是將其放入這個連線佇列,而應用層接受連線是將其從該佇列中移出。應用層可以通過設定backlog變數來指明該連線佇列的最大長度,即已被TCP接受而等待應用層接受的最大連線數。

 當一個連線請求SYN到達時,TCP確定是否接受這個連線。如果佇列中還有空間,TCP模組將對SYN進行確認並完成連線的建立。但應用層只有在三次握手中的第三個報文收到後才會知道這個新連線。如果佇列沒有空間,TCP將不理會收到的SYN。

 如果應用層不能及時接受已被TCP接受的連線,這些連線可能佔滿整個連線佇列,新的連線請求可能不被響應而會超時。如果一個連線請求SYN傳送後,一段時間後沒有收到確認SYN+ACK,TCP會重傳這個連線請求SYN兩次,每次重傳的時間間隔加倍,在規定的時間內仍沒有收到SYN+ACK,TCP將放棄這個連線請求,連線建立就超時了。

  JAVASocket連線建立超時和TCP是相同的,如果TCP建立連線時三次握手超時,那麼導致Socket連線建立也就超時了。可以設定Socket連線建立的超時時間-

connect(SocketAddress endpoint, inttimeout)

如果在timeout內,連線沒有建立成功,在TimeoutException異常被丟擲。如果timeout的值小於三次握手的時間,那麼Socket連線永遠也不會建立。

 不同的應用層有不同的連線建立過程,Socket的連線建立和TCP一樣-僅僅需要三次握手就完成連線,但有些應用程式需要互動很多資訊後才能成功建立連線,比如Telnet協議,在TCP三次握手完成後,需要進行選項協商之後,Telnet連線才建立完成。

3 socket讀超時

 如果輸入緩衝佇列RecvQ中沒有資料,read操作會一直阻塞而掛起執行緒,直到有新的資料到來或者有異常產生。呼叫setSoTimeout(inttimeout)可以設定超時時間,如果到了超時時間仍沒有資料,read會丟擲一個SocketTimeoutException,程式需要捕獲這個異常,但是當前的socket連線仍然是有效的。

 如果對方程式崩潰、對方機器突然重啟、網路斷開,本端的read會一直阻塞下去(由前面可知:雙方要關閉連線需要四次揮手.對方機重啟或斷開只是對方機的TCP連線關閉,本端的TCP連線還沒關閉所以本端機會一直阻塞),這時設定超時時間是非常重要的,否則呼叫read的執行緒會一直掛起。

 TCP模組把接收到的資料放入RecvQ中,直到應用層呼叫輸入流的read方法來讀取。如果RecvQ佇列被填滿了,這時TCP會根據滑動視窗機制通知對方不要繼續傳送資料,本端停止接收從對端傳送來的資料,直到接收者應用程式呼叫輸入流的read方法後騰出了空間。

4 socket寫超時

 socket的寫超時是基於TCP的超時重傳。超時重傳是TCP保證資料可靠性傳輸的一個重要機制,其原理是在傳送一個資料包文後就開啟一個計時器,在一定時間內如果沒有得到傳送報文的確認ACK,那麼就重新傳送報文。如果重新傳送多次之後,仍沒有確認報文,就傳送一個復位報文RST,然後關閉TCP連線。首次資料包文傳送與復位報文傳輸之間的時間差大約為9分鐘,也就是說如果9分鐘內沒有得到確認報文,就關閉連線。但是這個值是根據不同的TCP協議棧實現而不同。

 如果傳送端呼叫write持續地寫出資料,直到SendQ佇列被填滿。如果在SendQ佇列已滿時呼叫write方法,則write將被阻塞,直到SendQ有新的空閒空間為止,也就是說直到一些位元組傳輸到了接收者套接字的RecvQ中。如果此時RecvQ佇列也已經被填滿,所有操作都將停止,直到接收端呼叫read方法將一些位元組傳輸到應用程式。

 當Socket的write傳送資料時,如果網線斷開、對端程式崩潰或者對端機器重啟動,由前面可知:雙方要關閉連線需要四次揮手.對端程式崩潰或者對端機器重啟動只是對方機的TCP連線關閉,本端的TCP連線還沒關閉所以本端機會一直阻塞TCP模組會重傳資料,最後超時而關閉連線。下次如再呼叫write會導致一個異常而退出。

 Socket寫超時是基於TCP協議棧的超時重傳機制,一般不需要設定write的超時時間,也沒有提供這種方法。

5 雙重巢狀異常捕獲

  如果ServerSocket、Socket構造失敗,只需要僅僅捕獲這個構造失敗異常而不需要呼叫套接字的close方法來釋放資源(必須保證構造失敗後不會留下任何需要清除的資源),因為這時套接字內部資源沒有被成功分配。如果構造成功,必須進入一個tryfinally語句塊裡呼叫close釋放套接字。請參照下面例子程式。


import java.net.*;
import java.io.*;
public class SocketClientTest
{
  public static final int PORT = 8088;
  public static void main( String[] args ) throwsException
  {
    InetAddressaddr = InetAddress.getByName( "127.0.0.1" );
    Socketsocket = new Socket();
    try
    {
     socket.connect( new InetSocketAddress( addr, PORT ), 30000 );
     socket.setSendBufferSize(100);
     
     BufferedWriter out = new BufferedWriter( new OutputStreamWriter(socket.getOutputStream() ) );
     int i = 0;
     
     while( true )
     {
       System.out.println( "client sent --- hello *** " + i++ );
       out.write( "client sent --- hello *** " + i );
       out.flush();
       
       Thread.sleep( 1000 );
     }
    }
   finally
    {
     socket.close();
    }
  }
}

import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class SocketServerTest
{
  public static final int PORT = 8088;
  public static final int BACKLOG = 2;
  public static void main( String[] args ) throwsIOException
  {
    ServerSocketserver = new ServerSocket( PORT, BACKLOG );
   System.out.println("started: " + server);
    try
    {
     Socket socket = server.accept();
     try
     {
       BufferedReader in = new BufferedReader( new InputStreamReader(socket.getInputStream() ) );
       String info = null;
       
       while( ( info = in.readLine() ) != null )
       {
         System.out.println( info );
       }
     }
     finally
     {
       socket.close();
     }
    }
   finally
    {
     server.close();
    }
  }
}

執行上面的程式,在程式執行一會兒之後,斷開client和server之間的網路連線,在機器上輸出如下:

 

Server上的輸出:

Echoing:client sent-----hello0

Echoing:client sent-----hello1

Echoing:client sent-----hello2

Echoing:client sent-----hello3

Echoing:client sent-----hello4

Echoing:client sent-----hello5

Echoing:client sent-----hello6

 

---->>斷開了網路連線之後沒有資料輸出

 

Client上的輸出:

socket default timeout= 0

socket =Socket[addr=/10.15.9.99,port=8088,localport=4691]

begin toread

client sent --- hello*** 0

client sent --- hello*** 1

client sent --- hello*** 2

client sent --- hello*** 3

client sent --- hello*** 4

client sent --- hello*** 5

client sent --- hello*** 6

client sent --- hello*** 7

client sent --- hello*** 8  

client sent --- hello*** 9

client sent --- hello*** 10

 

 ---->> 斷開網路連線後客戶端程式掛起

 

java.net.SocketException : Connection reset by peer: socket write error

    atjava.net.SocketOutputStream.socketWrite0( Native Method )

    atjava.net.SocketOutputStream.socketWrite( SocketOutputStream.java:92 )

    atjava.net.SocketOutputStream.write( SocketOutputStream.java:136 )

    atsun.nio.cs.StreamEncoder.writeBytes( StreamEncoder.java:202 )

    atsun.nio.cs.StreamEncoder.implFlushBuffer( StreamEncoder.java:272 )

    atsun.nio.cs.StreamEncoder.implFlush( StreamEncoder.java:276 )

    atsun.nio.cs.StreamEncoder.flush( StreamEncoder.java:122 )

    atjava.io.OutputStreamWriter.flush( OutputStreamWriter.java:212 )

    atjava.io.BufferedWriter.flush( BufferedWriter.java:236 )

    atcom.xtera.view.SocketClientTest.main( SocketClientTest.java:99 )

 

 當hello6被髮送到server端後,網路連線被斷開,這時server端不能接收任何資料而掛起。client端仍然繼續傳送資料,實際上hello7、hello8、hello9、hello10都被複制到SendQ佇列中,write方法立即返回。當client的SendQ佇列被填滿之後,write方法就被阻塞。TCP模組在傳送報文hello7之後,沒有收到確認而超時重傳,再重傳幾次之後關閉了TCP連線,同時導致被阻塞的write方法異常返回。

 通過抓包工具,我們可以看到超時重傳的報文。

 

 

 

 

 

 




下面是規範程式碼例項:(服務端和客戶端實現雙向(可通過鍵盤輸入)互動通訊)

public class JabberClient {
     public static void main(String[] args)
        throws IOException {
       // Passingnull to getByName() produces the
       // special"Local Loopback" IP address, for
       // testingon one machine w/o a network:
       InetAddressaddr =
        InetAddress.getByName("127.0.0.1");
       //Alternatively, you can use
       // theaddress or name:
       //InetAddress addr =
      //   InetAddress.getByName("127.0.0.1");
       //InetAddress addr =
      //   InetAddress.getByName("localhost");
      System.out.println("addr = " + addr);
       Socketsocket =
        new Socket(addr, JabberServer.PORT);
      // Guard everything in a try-finally to make
       // sure thatthe socket is closed:

       try {
        System.out.println("socket = " + socket);
        BufferedReader KeyIn = new BufferedReader(newInputStreamReader(System.in));
        BufferedReader in =
          new BufferedReader(
            new InputStreamReader(
              socket.getInputStream()));
        // Output isautomatically 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");
        String str =null;
     

相關文章