1 超時
套接字底層是基於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連接永遠也不會建立。
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(); } } }
(3)socket 讀超時
如果輸入緩沖隊列RecvQ中沒有數據,read操作會一直阻塞而掛起線程,直到有新的數據到來或者有異常產生。調用setSoTimeout(int timeout)可以設置超時時間,如果到了超時時間仍沒有數據,read會拋出一個SocketTimeoutException,程序需要捕獲這個異常,但是當前的socket連接仍然是有效的。如果對方進程崩潰、對方機器突然重啟、網絡斷開,本端的read會一直阻塞下去,這時設置超時時間是非常重要的,否則調用read的線程會一直掛起。TCP模塊把接收到的數據放入RecvQ中,直到應用層調用輸入流的read方法來讀取。如果RecvQ隊列被填滿了,這時TCP會根據滑動窗口機制通知對方不要繼續發送數據,本端停止接收從對端發送來的數據,直到接收者應用程序調用輸入流的read方法后騰出了空間。
假設我們需要控制我們的客戶端在開始讀取數據10秒后還沒有讀到數據就中斷阻塞的話我們可以這樣做:
public class Client { public static void main(String args[]) throws Exception { //為了簡單起見,所有的異常都直接往外拋 String host = "127.0.0.1"; //要連接的服務端IP地址 int port = 8899; //要連接的服務端對應的監聽端口 //與服務端建立連接 Socket client = new Socket(host, port); //建立連接后就可以往服務端寫數據了 Writer writer = new OutputStreamWriter(client.getOutputStream()); writer.write("Hello Server."); writer.write("eof\n"); writer.flush(); //寫完以后進行讀操作 BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream())); //設置超時間為10秒 client.setSoTimeout(10*1000); StringBuffer sb = new StringBuffer(); String temp; int index; try { while ((temp=br.readLine()) != null) { if ((index = temp.indexOf("eof")) != -1) { sb.append(temp.substring(0, index)); break; } sb.append(temp); } } catch (SocketTimeoutException e) { System.out.println("數據讀取超時。"); } System.out.println("from server: " + sb); writer.close(); br.close(); client.close(); } }
2 斷開連接
TCP Socket連接是雙向的,通過四次揮手的方式斷開,雙方分別調用Socket.close()方法斷開連接。連接斷開的過程中,一般一方A先斷開連接,另一方B發現A斷開連接后,也斷開連接。為方便表述,將先斷開連接的一方A稱為“主動斷開連接”;后斷開的一方B,則為“被動斷開連接”。
在一方B阻塞執行in.readUTF()方法時,如果對方A主動斷開Socket連接,這個方法會拋出異常。從而在B處理異常時,可以被動的斷開這邊的連接。
為保證主動斷開連接的一方不會阻塞在in.readUTF()方法中,需要先執行socket.shutdownInput()。所以主動斷開連接的代碼如下。
socket.shutdownInput();
in.close();
socket.close();
被動斷開連接的一方,在捕獲到in.readUTF()的異常后,斷開Socket連接。
try { String s = in.readUTF(); } catch (IOException e) { // 連接被斷開(被動) try { in.close(); socket.close(); in = null; socket = null; } catch (IOException e) { e.printStackTrace(); } }
