http://chwshuang.iteye.com/blog/2028951
定義:這裏討論的Mina 斷線重連是指使用mina作爲客戶端軟件,連接其他提供Socket通訊服務的服務器端。Socket服務器可以是Mina提供的服務器,也可以是C++提供的服務器。
一、斷線重連的方式;
1. 在創建Mina客戶端時增加一個監聽器,或者增加一個攔截器,當檢測到Session關閉時,自動進行重連。
2. 在第1種方式的基礎上,增加客戶端的讀寫通道空閒檢查,當發生Session關閉或者讀寫空閒時,進行重連。
第一種方式比較傳統,優點是簡單方便,適合網絡穩定、數據量不大(1M帶寬以下)的環境;不過缺點是不能對系統級的連接斷開阻塞進行捕獲。
第二種方式更加精細,基本上能捕獲到應用、網絡、系統級的斷連。
二、重連目的:
在使用Mina做爲客戶端時,往往因爲網絡、服務器、應用程序出現問題而導致連接斷開,而自動重連,就是解決連接斷開的唯一方式。如果網線斷開、服務器宕機、應用程序掛了,都是斷線的原因,這個時候,通過增加一個監聽器或者攔截器,就能實現重連。但是生產環境中,斷線的原因可能更復雜:網絡不穩定、延時、服務器負載高、服務器或者應用程序的發送或者接收緩衝區滿等等問題都可能導致數據傳輸過程出現類似於斷線的情況,這個時候,光檢測Session關閉是遠遠不夠的,這個時候就需要一種重連機制,比如讀寫空閒超過30秒,就進行重連。對於數據不間斷、實時性高、數據量大的應用場景,更是實用。
三、實例:
第一種:監聽器方式
創建一個監聽器實現mina的IoServiceListener接口,裏面的方法可以不用寫實現
import org.apache.mina.core.service.IoServiceListener;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
public class IoListener implements IoServiceListener{
@Override
public void serviceActivated(IoService arg0) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void serviceDeactivated(IoService arg0) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void serviceIdle(IoService arg0, IdleStatus arg1) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void sessionCreated(IoSession arg0) throws Exception {
// TODO Auto-generated method stub
}
@Override
public void sessionDestroyed(IoSession arg0) throws Exception {
// TODO Auto-generated method stub
}
再創建客戶端時加入監聽
NioSocketConnector connector = new NioSocketConnector(); //創建連接客戶端
connector.setConnectTimeoutMillis(30000); //設置連接超時
connector.getSessionConfig().setReceiveBufferSize(10240); // 設置接收緩衝區的大小
connector.getSessionConfig().setSendBufferSize(10240);// 設置輸出緩衝區的大小
// 加入解碼器
TextLineCodecFactory factory = new TextLineCodecFactory(Charset.forName("GBK"), LineDelimiter.WINDOWS.getValue(), LineDelimiter.WINDOWS.getValue());
factory.setDecoderMaxLineLength(10240);
factory.setEncoderMaxLineLength(10240);
connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(factory));
connector.setDefaultRemoteAddress(new InetSocketAddress(host, port));// 設置默認訪問地址
//添加處理器
connector.setHandler(new IoHandler());
// 添加重連監聽
connector.addListener(new IoListener() {
@Override
public void sessionDestroyed(IoSession arg0) throws Exception {
for (;;) {
try {
Thread.sleep(3000);
ConnectFuture future = connector.connect();
future.awaitUninterruptibly();// 等待連接創建成功
session = future.getSession();// 獲取會話
if (session.isConnected()) {
logger.info("斷線重連[" + connector.getDefaultRemoteAddress().getHostName() + ":" + connector.getDefaultRemoteAddress().getPort() + "]成功");
break;
}
} catch (Exception ex) {
logger.info("重連服務器登錄失敗,3秒再連接一次:" + ex.getMessage());
}
}
}
});
for (;;) {
try {
ConnectFuture future = connector.connect();
future.awaitUninterruptibly(); // 等待連接創建成功
session = future.getSession(); // 獲取會話
logger.info("連接服務端" + host + ":" + port + "[成功]" + ",,時間:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
break;
} catch (RuntimeIoException e) {
logger.error("連接服務端" + host + ":" + port + "失敗" + ",,時間:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + ", 連接MSG異常,請檢查MSG端口、IP是否正確,MSG服務是否啓動,異常內容:" + e.getMessage(), e);
Thread.sleep(5000);// 連接失敗後,重連間隔5s
}
}
第一種:攔截器方式
connector = new NioSocketConnector(); //創建連接客戶端
connector.setConnectTimeoutMillis(30000); //設置連接超時
// 斷線重連回調攔截器
connector.getFilterChain().addFirst("reconnection", new IoFilterAdapter() {
@Override
public void sessionClosed(NextFilter nextFilter, IoSession ioSession) throws Exception {
for(;;){
try{
Thread.sleep(3000);
ConnectFuture future = connector.connect();
future.awaitUninterruptibly();// 等待連接創建成功
session = future.getSession();// 獲取會話
if(session.isConnected()){
logger.info("斷線重連["+ connector.getDefaultRemoteAddress().getHostName() +":"+ connector.getDefaultRemoteAddress().getPort()+"]成功");
break;
}
}catch(Exception ex){
logger.info("重連服務器登錄失敗,3秒再連接一次:" + ex.getMessage());
}
}
}
});
TextLineCodecFactory factory = new TextLineCodecFactory(Charset.forName(encoding), LineDelimiter.WINDOWS.getValue(), LineDelimiter.WINDOWS.getValue());
factory.setDecoderMaxLineLength(10240);
factory.setEncoderMaxLineLength(10240);
//加入解碼器
connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(factory));
//添加處理器
connector.setHandler(new IoHandler());
connector.getSessionConfig().setReceiveBufferSize(10240); // 設置接收緩衝區的大小
connector.getSessionConfig().setSendBufferSize(10240); // 設置輸出緩衝區的大小
connector.setDefaultRemoteAddress(new InetSocketAddress(host, port));// 設置默認訪問地址
for (;;) {
try {
ConnectFuture future = connector.connect();
// 等待連接創建成功
future.awaitUninterruptibly();
// 獲取會話
session = future.getSession();
logger.error("連接服務端" + host + ":" + port + "[成功]" + ",,時間:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
break;
} catch (RuntimeIoException e) {
logger.error("連接服務端" + host + ":" + port + "失敗" + ",,時間:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + ", 連接MSG異常,請檢查MSG端口、IP是否正確,MSG服務是否啓動,異常內容:" + e.getMessage(), e);
Thread.sleep(5000);// 連接失敗後,重連間隔5s
}
}
第二種:加入空閒檢測機制
空閒檢測機制需要在創建客戶端時,加入空閒超時,然後在處理器handler端的sessionIdle方法中加入一個預關閉連接的方法。讓Session關閉傳遞到監聽器或者攔截器的sessionClose方法中實現重連。
以攔截器方式爲例,在創建客戶端時,加入讀寫通道空閒檢查超時機制。
connector = new NioSocketConnector(); //創建連接客戶端
connector.setConnectTimeoutMillis(30000); //設置連接超時
// 斷線重連回調攔截器
connector.getFilterChain().addFirst("reconnection", new IoFilterAdapter() {
@Override
public void sessionClosed(NextFilter nextFilter, IoSession ioSession) throws Exception {
for(;;){
try{
Thread.sleep(3000);
ConnectFuture future = connector.connect();
future.awaitUninterruptibly();// 等待連接創建成功
session = future.getSession();// 獲取會話
if(session.isConnected()){
logger.info("斷線重連["+ connector.getDefaultRemoteAddress().getHostName() +":"+ connector.getDefaultRemoteAddress().getPort()+"]成功");
break;
}
}catch(Exception ex){
logger.info("重連服務器登錄失敗,3秒再連接一次:" + ex.getMessage());
}
}
}
});
connector.getFilterChain().addLast("mdc", new MdcInjectionFilter());
TextLineCodecFactory factory = new TextLineCodecFactory(Charset.forName(encoding), LineDelimiter.WINDOWS.getValue(), LineDelimiter.WINDOWS.getValue());
factory.setDecoderMaxLineLength(10240);
factory.setEncoderMaxLineLength(10240);
//加入解碼器
connector.getFilterChain().addLast("codec", new ProtocolCodecFilter(factory));
connector.getSessionConfig().setReceiveBufferSize(10240); // 設置接收緩衝區的大小
connector.getSessionConfig().setSendBufferSize(10240);// 設置輸出緩衝區的大小
connector.getSessionConfig().setIdleTime(IdleStatus.BOTH_IDLE, 30000); //讀寫都空閒時間:30秒
connector.getSessionConfig().setIdleTime(IdleStatus.READER_IDLE, 40000);//讀(接收通道)空閒時間:40秒
connector.getSessionConfig().setIdleTime(IdleStatus.WRITER_IDLE, 50000);//寫(發送通道)空閒時間:50秒
//添加處理器
connector.setHandler(new IoHandler());
connector.setDefaultRemoteAddress(new InetSocketAddress(host, port));// 設置默認訪問地址
for (;;) {
try {
ConnectFuture future = connector.connect();
// 等待連接創建成功
future.awaitUninterruptibly();
// 獲取會話
session = future.getSession();
logger.error("連接服務端" + host + ":" + port + "[成功]" + ",,時間:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
break;
} catch (RuntimeIoException e) {
System.out.println("連接服務端" + host + ":" + port + "失敗" + ",,時間:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + ", 連接MSG異常,請檢查MSG端口、IP是否正確,MSG服務是否啓動,異常內容:" + e.getMessage());
logger.error("連接服務端" + host + ":" + port + "失敗" + ",,時間:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + ", 連接MSG異常,請檢查MSG端口、IP是否正確,MSG服務是否啓動,異常內容:" + e.getMessage(), e);
Thread.sleep(5000);// 連接失敗後,重連10次,間隔30s
}
}
然後在數據處理器IoHandler中sessionIdle方法中加入Session會話關閉的代碼,這樣session關閉就能傳遞到攔截器或者監聽器中,然後實現重連。
import org.apache.mina.core.service.IoHandlerAdapter;
import org.apache.mina.core.session.IdleStatus;
import org.apache.mina.core.session.IoSession;
public class IoHandler extends IoHandlerAdapter {
//部分代碼忽略...
@Override
public void sessionIdle(IoSession session, IdleStatus status) throws Exception {
logger.info("-客戶端與服務端連接[空閒] - " + status.toString());
if(session != null){
session.close(true);
}
}
//部分代碼忽略...
}
總結-最佳實踐:
以上兩種方式我個人認爲最好是使用第二種。在實際的生產環境,對於數據量比較少的情況下,需要加一個線程專門發送心跳信息,然後在服務器端進行迴應心跳,這樣就保證讀寫通道不出現空閒。如果數據量比較大,大到24小時都有數據,那麼就不需要心跳線程,可以直接在IoHandler處理器端中messageReceived方法中定時發送心跳到服務器。由於讀寫監控還可以處理服務器、網絡、應用等等方面的不確定因素,所以建議使用第二種方式。