閑來無事,把之前寫的一個游戲服務器框架(《一個java頁游服務器框架》),部署到阿里雲服務器上,測試運行了下,結果看到后台log中打印出了“Connection reset by peer”。出於好奇疑問就查了一下相關資料,網上說一般有這幾種:
①:服務器的並發連接數超過了其承載量,服務器會將其中一些連接Down掉;
②:客戶關掉了瀏覽器,而服務器還在給客戶端發送數據;
③:瀏覽器端按了Stop
但是這幾種都可以排除,剛搭建的服務器,就我測試連接了下不可能超過負載,而且我這是tcp長連接與瀏覽器沒半點關系。后來問了一些大神才知道,一個連接如果長時間不用防火牆或者路由器就會給你斷開,於是就會打印如上異常。現實中的解決方法就是,客戶端定時向服務器發送心跳包。才知道心跳包原來還有這個作用,說來甚是慚愧啊。
說到了心跳包自然又想到了另一個功能。客戶端斷電等一些列強制連接斷開的情況,可以通過心跳包機制,讓服務器端及時知道鏈接斷開,從而及時清除這些無法使用的鏈接屍體。反過來說,如果沒有心跳包,那么客戶端強制斷開,服務器是無法及時捕獲到鏈接斷開的事件的。然而在我測試下,強制關閉客戶端,服務器端是會拋出異常的(我的這個框架使用了mina,客戶端強制斷開后,會調用對應的exceptionCaught方法拋出異常,然后又調用sessionClosed關閉連接)。既然服務器會拋出異常,自然我們不用通過心跳包,服務器端也能及時知道客戶端連接斷開的情況。
懷着強烈的好奇心就做了如下測試,由於java網絡編程有bio和nio以及最新的aio,對於AIO,成產環境中我還沒有接觸到又用這個的,所以暫不做測試,就針對BIO和NIO做測試
(以下代碼比較簡單,就不做解釋了。僅僅用於測試我的疑惑,某些不合理的地方也就不必在意了)
BIO測試程序:
服務器端測試代碼:
public class BIOServer { public static void main(String[] args) throws UnsupportedEncodingException { byte[] datas="hello".getBytes("UTF-8"); try { ServerSocket ss = new ServerSocket(810); while(true){ Socket s = ss.accept(); OutputStream os = s.getOutputStream(); os.write(datas); } } catch (IOException e) { e.printStackTrace(); } } }
客戶端程序:
public class Client { public static void main(String[] args) { try { Socket s = new Socket("localhost", 810); InputStream is = s.getInputStream(); byte[] buf = new byte[1024]; int len=0; while((len=is.read(buf))!=-1){ System.out.println(new String(buf,0,len,"utf-8")); } System.in.read(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
先運行服務器端,然后運行客戶端收到服務器端發來的hello,然后強制關閉客戶端,服務器端沒有任何反應,這說明BIO下強制關閉客戶端,服務器端是沒法及時捕獲的。當然在連接斷開的情況下,服務器端對此連接進行讀寫,就會拋出異常。
NIO測試程序:
服務器端:
public class NioServer { public static void main(String[] args)throws Exception { Selector selector = Selector.open(); ServerSocketChannel serverChannel = ServerSocketChannel.open(); serverChannel.configureBlocking(false); serverChannel.socket().bind(new InetSocketAddress(810)); serverChannel.register(selector, SelectionKey.OP_ACCEPT); while(true){ int l = selector.select(); if(l==0){ continue; } Set<SelectionKey> sets = selector.selectedKeys(); for(SelectionKey key :sets){ if(key.isAcceptable()){ ServerSocketChannel tChannel = (ServerSocketChannel) key.channel(); SocketChannel clientChannel = tChannel.accept(); clientChannel.configureBlocking(false); clientChannel.register(selector, SelectionKey.OP_READ); }else if(key.isReadable()){ SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer buf = ByteBuffer.allocate(1024); int len = clientChannel.read(buf); if(len==-1){ System.out.println("客戶端斷開了"); }else{ buf.flip(); System.out.println(new String(buf.array(),0,len,"utf-8")); } } sets.remove(key); } } } }
客戶端:
public class Client { public static void main(String[] args) { try { Socket s = new Socket("localhost", 810); OutputStream os = s.getOutputStream(); byte[] datas="hello".getBytes("UTF-8"); os.write(datas); System.in.read(); } catch (UnknownHostException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } }
運行服務器端,然后運行客戶端,可以看到服務器端打印出客戶端發來的“hello”,然后強制關閉客戶端,服務器端就回拋出如下異常:
通過以上測試,可以知道:BIO下,客戶端強制斷開,服務器端是沒法及時知道的;NIO下,客戶端強制斷開,服務器端可以及時知道並拋出異常。有此差異肯定是因為BIO和nio使用了不同的網絡模型。NIO有一個selector,我想也就是這個selector的底層對每一個連接會有檢查,所以能及時知道客戶端連接斷開事件。我的有服務器框架就是基於mina,而mina使用的nio,所以對於強制關閉客戶端,我的服務器能及時捕獲到並拋出異常這個疑惑,也就算徹底明確了。