JAVA Socket超時(shí)淺析
套接字或插座(socket)是一種軟件形式的抽象,用于表達兩臺機器間一個(gè)連接的“終端”。針對一個(gè)特定的連接,每臺機器上都有一個(gè)“套接字”,可以想象它們之間有一條虛擬的“線(xiàn)纜”。JAVA有兩個(gè)基于數據流的套接字類(lèi):ServerSocket,服務(wù)器用它“偵聽(tīng)”進(jìn)入的連接;Socket,客戶(hù)端用它初始一次連接。偵聽(tīng)套接字只能接收新的連接請求,不能接收實(shí)際的數據包。
套接字是基于TCP/IP實(shí)現的,它是用來(lái)提供一個(gè)訪(fǎng)問(wèn)TCP的服務(wù)接口,或者說(shuō)套接字socket是TCP的應用編程接口A(yíng)PI,通過(guò)它應用層就可以訪(fǎng)問(wèn)TCP提供的服務(wù)。
在JAVA中,我們用ServerSocket、Socket類(lèi)創(chuàng )建一個(gè)套接字連接,從套接字得到的結果是一個(gè)InputStream以及OutputStream對象,以便將連接作為一個(gè)IO流對象對待。通過(guò)IO流可以從流中讀取數據或者寫(xiě)數據到流中,讀寫(xiě)IO流會(huì )有異常IOException產(chǎn)生。
套接字底層是基于TCP的,所以socket的超時(shí)和TCP超時(shí)是相同的。下面先討論套接字讀寫(xiě)緩沖區,接著(zhù)討論連接建立超時(shí)、讀寫(xiě)超時(shí)以及JAVA套接字編程的嵌套異常捕獲和一個(gè)超時(shí)例子程序的抓包示例。
1 socket讀寫(xiě)緩沖區
一旦創(chuàng )建了一個(gè)套接字實(shí)例,操作系統就會(huì )為其分配緩沖區以存放接收和要發(fā)送的數據。
JAVA可以設置讀寫(xiě)緩沖區的大小-setReceiveBufferSize(int size), setSendBufferSize(int size)。
向輸出流寫(xiě)數據并不意味著(zhù)數據實(shí)際上已經(jīng)被發(fā)送,它們只是被復制到了發(fā)送緩沖區隊列SendQ,就是在Socket的OutputStream上調用flush()方法,也不能保證數據能夠立即發(fā)送到網(wǎng)絡(luò )。真正的數據發(fā)送是由操作系統的TCP協(xié)議棧模塊從緩沖區中取數據發(fā)送到網(wǎng)絡(luò )來(lái)完成的。
當有數據從網(wǎng)絡(luò )來(lái)到時(shí),TCP協(xié)議棧模塊接收數據并放入接收緩沖區隊列RecvQ,輸入流InputStream通過(guò)read方法從RecvQ中取出數據。
2 socket連接建立超時(shí)
socket連接建立是基于TCP的連接建立過(guò)程。TCP的連接需要通過(guò)3次握手報文來(lái)完成,開(kāi)始建立TCP連接時(shí)需要發(fā)送同步SYN報文,然后等待確認報文SYN+ACK,最后再發(fā)送確認報文ACK。TCP連接的關(guān)閉通過(guò)4次揮手來(lái)完成,主動(dòng)關(guān)閉TCP連接的一方發(fā)送FIN報文,等待對方的確認報文;被動(dòng)關(guān)閉的一方也發(fā)送FIN報文,然等待確認報文。
正在等待TCP連接請求的一端有一個(gè)固定長(cháng)度的連接隊列,該隊列中的連接已經(jīng)被TCP接受(即三次握手已經(jīng)完成),但還沒(méi)有被應用層所接受。TCP接受一個(gè)連接是將其放入這個(gè)連接隊列,而應用層接受連接是將其從該隊列中移出。應用層可以通過(guò)設置backlog變量來(lái)指明該連接隊列的最大長(cháng)度,即已被TCP接受而等待應用層接受的最大連接數。
當一個(gè)連接請求SYN到達時(shí),TCP確定是否接受這個(gè)連接。如果隊列中還有空間,TCP模塊將對SYN進(jìn)行確認并完成連接的建立。但應用層只有在三次握手中的第三個(gè)報文收到后才會(huì )知道這個(gè)新連接。如果隊列沒(méi)有空間,TCP將不理會(huì )收到的SYN。
如果應用層不能及時(shí)接受已被TCP接受的連接,這些連接可能占滿(mǎn)整個(gè)連接隊列,新的連接請求可能不被響應而會(huì )超時(shí)。如果一個(gè)連接請求SYN發(fā)送后,一段時(shí)間后沒(méi)有收到確認SYN+ACK,TCP會(huì )重傳這個(gè)連接請求SYN兩次,每次重傳的時(shí)間間隔加倍,在規定的時(shí)間內仍沒(méi)有收到SYN+ACK,TCP將放棄這個(gè)連接請求,連接建立就超時(shí)了。
JAVA Socket連接建立超時(shí)和TCP是相同的,如果TCP建立連接時(shí)三次握手超時(shí),那么導致Socket連接建立也就超時(shí)了??梢栽O置Socket連接建立的超時(shí)時(shí)間-
connect(SocketAddress endpoint, int timeout)
如果在timeout內,連接沒(méi)有建立成功,在TimeoutException異常被拋出。如果timeout的值小于三次握手的時(shí)間,那么Socket連接永遠也不會(huì )建立。
不同的應用層有不同的連接建立過(guò)程,Socket的連接建立和TCP一樣-僅僅需要三次握手就完成連接,但有些應用程序需要交互很多信息后才能成功建立連接,比如Telnet協(xié)議,在TCP三次握手完成后,需要進(jìn)行選項協(xié)商之后,Telnet連接才建立完成。
3 socket讀超時(shí)
如果輸入緩沖隊列RecvQ中沒(méi)有數據,read操作會(huì )一直阻塞而掛起線(xiàn)程,直到有新的數據到來(lái)或者有異常產(chǎn)生。調用setSoTimeout(int timeout)可以設置超時(shí)時(shí)間,如果到了超時(shí)時(shí)間仍沒(méi)有數據,read會(huì )拋出一個(gè)SocketTimeoutException,程序需要捕獲這個(gè)異常,但是當前的socket連接仍然是有效的。
如果對方進(jìn)程崩潰、對方機器突然重啟、網(wǎng)絡(luò )斷開(kāi),本端的read會(huì )一直阻塞下去,這時(shí)設置超時(shí)時(shí)間是非常重要的,否則調用read的線(xiàn)程會(huì )一直掛起。
TCP模塊把接收到的數據放入RecvQ中,直到應用層調用輸入流的read方法來(lái)讀取。如果RecvQ隊列被填滿(mǎn)了,這時(shí)TCP會(huì )根據滑動(dòng)窗口機制通知對方不要繼續發(fā)送數據,本端停止接收從對端發(fā)送來(lái)的數據,直到接收者應用程序調用輸入流的read方法后騰出了空間。
4 socket寫(xiě)超時(shí)
socket的寫(xiě)超時(shí)是基于TCP的超時(shí)重傳。超時(shí)重傳是TCP保證數據可靠性傳輸的一個(gè)重要機制,其原理是在發(fā)送一個(gè)數據報文后就開(kāi)啟一個(gè)計時(shí)器,在一定時(shí)間內如果沒(méi)有得到發(fā)送報文的確認ACK,那么就重新發(fā)送報文。如果重新發(fā)送多次之后,仍沒(méi)有確認報文,就發(fā)送一個(gè)復位報文RST,然后關(guān)閉TCP連接。首次數據報文發(fā)送與復位報文傳輸之間的時(shí)間差大約為9分鐘,也就是說(shuō)如果9分鐘內沒(méi)有得到確認報文,就關(guān)閉連接。但是這個(gè)值是根據不同的TCP協(xié)議棧實(shí)現而不同。
如果發(fā)送端調用write持續地寫(xiě)出數據,直到SendQ隊列被填滿(mǎn)。如果在SendQ隊列已滿(mǎn)時(shí)調用write方法,則write將被阻塞,直到SendQ有新的空閑空間為止,也就是說(shuō)直到一些字節傳輸到了接收者套接字的RecvQ中。如果此時(shí)RecvQ隊列也已經(jīng)被填滿(mǎn),所有操作都將停止,直到接收端調用read方法將一些字節傳輸到應用程序。
當Socket的write發(fā)送數據時(shí),如果網(wǎng)線(xiàn)斷開(kāi)、對端進(jìn)程崩潰或者對端機器重啟動(dòng),TCP模塊會(huì )重傳數據,最后超時(shí)而關(guān)閉連接。下次如再調用write會(huì )導致一個(gè)異常而退出。
Socket寫(xiě)超時(shí)是基于TCP協(xié)議棧的超時(shí)重傳機制,一般不需要設置write的超時(shí)時(shí)間,也沒(méi)有提供這種方法。
5 雙重嵌套異常捕獲
如果ServerSocket、Socket構造失敗,只需要僅僅捕獲這個(gè)構造失敗異常而不需要調用套接字的close方法來(lái)釋放資源(必須保證構造失敗后不會(huì )留下任何需要清除的資源),因為這時(shí)套接字內部資源沒(méi)有被成功分配。如果構造成功,必須進(jìn)入一個(gè)try finally語(yǔ)句塊里調用close釋放套接字。請參照下面例子程序。
[java:nogutter]
view plaincopyimport 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();
}
}
}
[java:nogutter]
view plaincopyimport 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();
}
}
}
執行上面的程序,在程序運行一會(huì )兒之后,斷開(kāi)client和server之間的網(wǎng)絡(luò )連接,在機器上輸出如下:
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
---->> 斷開(kāi)了網(wǎng)絡(luò )連接之后沒(méi)有數據輸出
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
---->> 斷開(kāi)網(wǎng)絡(luò )連接后客戶(hù)端進(jìn)程掛起
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被發(fā)送到server端后,網(wǎng)絡(luò )連接被斷開(kāi),這時(shí)server端不能接收任何數據而掛起。client端仍然繼續發(fā)送數據,實(shí)際上hello7、hello8、hello9、hello10都被復制到SendQ隊列中,write方法立即返回。當client的SendQ隊列被填滿(mǎn)之后,write方法就被阻塞。TCP模塊在發(fā)送報文hello7之后,沒(méi)有收到確認而超時(shí)重傳,再重傳幾次之后關(guān)閉了TCP連接,同時(shí)導致被阻塞的write方法異常返回。
通過(guò)抓包工具,我們可以看到超時(shí)重傳的報文。