JAVA Socket 底层是怎样基于TCP/IP 实现的???
首先必須明確:TCP/IP模型中有四層結構:
????? 應用層(Application Layer)、傳輸層(Transport? Layer)、網絡層(Internet Layer? )、鏈路層(LinkLayer)
?其中Ip協議(Internet Protocol)是位于網絡層的,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套接字編程的嵌套異常捕獲和一個超時例子程序的抓包示例。
1 socket讀寫緩沖區
? 一旦創建了一個套接字實例,操作系統就會為其分配緩沖區以存放接收和要發送的數據。
?
?
? 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將放棄這個連接請求,連接建立就超時了。
? JAVA Socket連接建立超時和TCP是相同的,如果TCP建立連接時三次握手超時,那么導致Socket連接建立也就超時了。可以設置Socket連接建立的超時時間-
connect(SocketAddress endpoint, int timeout)
如果在timeout內,連接沒有建立成功,在TimeoutException異常被拋出。如果timeout的值小于三次握手的時間,那么Socket連接永遠也不會建立。
? 不同的應用層有不同的連接建立過程,Socket的連接建立和TCP一樣-僅僅需要三次握手就完成連接,但有些應用程序需要交互很多信息后才能成功建立連接,比如Telnet協議,在TCP三次握手完成后,需要進行選項協商之后,Telnet連接才建立完成。
3 socket讀超時
? 如果輸入緩沖隊列RecvQ中沒有數據,read操作會一直阻塞而掛起線程,直到有新的數據到來或者有異常產生。調用setSoTimeout(int timeout)可以設置超時時間,如果到了超時時間仍沒有數據,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方法來釋放資源(必須保證構造失敗 后不會留下任何需要清除的資源),因為這時套接字內部資源沒有被成功分配。如果構造成功,必須進入一個try finally語句塊里調用close釋放套接字。請參照下面例子程序。
?
import java.net.*;
import java.io.*;
public class SocketClientTest
{
? public static final int PORT = 8088;
? public static void main( String[] args ) throws Exception
? {
??? InetAddress addr = InetAddress.getByName( "127.0.0.1" );
??? Socket socket = 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 ) throws IOException
? {
??? ServerSocket server = 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 to read
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
??? at java.net.SocketOutputStream.socketWrite0( Native Method )
??? at java.net.SocketOutputStream.socketWrite( SocketOutputStream.java:92 )
??? at java.net.SocketOutputStream.write( SocketOutputStream.java:136 )
??? at sun.nio.cs.StreamEncoder.writeBytes( StreamEncoder.java:202 )
??? at sun.nio.cs.StreamEncoder.implFlushBuffer( StreamEncoder.java:272 )
??? at sun.nio.cs.StreamEncoder.implFlush( StreamEncoder.java:276 )
??? at sun.nio.cs.StreamEncoder.flush( StreamEncoder.java:122 )
??? at java.io.OutputStreamWriter.flush( OutputStreamWriter.java:212 )
??? at java.io.BufferedWriter.flush( BufferedWriter.java:236 )
??? at com.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 {
??? ??? // Passing null to getByName() produces the
??? ??? // special "Local Loopback" IP address, for
??? ??? // testing on one machine w/o a network:
??? ??? InetAddress addr =
??? ????? InetAddress.getByName("127.0.0.1");
??? ??? // 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 KeyIn = new BufferedReader(new InputStreamReader(System.in));
??? ????? 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");
??? ????? String str =null;
??? ????? while(true)
??? ????? {
??? ??? ??? ? str = KeyIn.readLine();
??? ??? ??? ? if("END".equals(str))
??? ??? ??? ??? ? break;
??? ??? ??? ? out.println(str);
??? ??? ??? ? System.out.println("Server:"+in.readLine());
??? ??? ??? ?
??? ??? ??? ?
??? ????? }
??? ??? } finally {
??? ????? System.out.println("closing...");
??? ????? socket.close();
??? ??? }
??? ? }
??? } ///:~
public class JabberServer {?
??? ? // Choose a port outside of the range 1-1024:
??? ? public static final int PORT = 8088;
??? ? public static void main(String[] args)
??? ????? throws IOException {
??? ??? ServerSocket s = new ServerSocket(PORT);
??? ??? System.out.println("Started: " + s);
??? ??? try {
??? ????? Socket socket = s.accept();
??? ????? try {
??? ??????? System.out.println(
??? ????????? "Connection accepted: "+ socket);
??? ??????? BufferedReader KeyIn = new BufferedReader(new InputStreamReader(System.in));
??? ???????
??? ??????? 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("Client: " + str);
??? ????????? out.println(KeyIn.readLine());
??? ?????????
??? ??????? }
??? ????? // Always close the two sockets...
??? ????? } finally {
??? ??????? System.out.println("closing...");
??? ??????? socket.close();
??? ????? }
??? ??? } finally {
??? ????? s.close();
??? ??? }
??? ? }
??? } ///:~
注意:一般在傳輸字符信息(例如txt文件,聊天信息)使用bufferedRead,printWrite? ,但是在傳送一些文件時一定要使用字節輸入輸出流socket.getInputStream?? 和scoket.getOutputStream(但是 使用上面的字符流傳輸文件時,當文件傳送完畢時打開會報錯!!!)
轉載于:https://www.cnblogs.com/kabi/p/5182798.html
總結
以上是生活随笔為你收集整理的JAVA Socket 底层是怎样基于TCP/IP 实现的???的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [leetcode]@python 85
- 下一篇: 微信不绑卡能收转账吗