Java Socket 連接狀態管理

Java socket 客戶端,需要對socket的連接狀態進行管理,以便在socket連接斷開的時候,可以實現自動重新連接;通過一系列的摸索,發現客戶端只有一個方法可以判斷socket連接處於斷開狀態,就是向socket寫數據。

因此客戶端需要定期向服務端寫數據,如果業務數據寫出不是那麼頻繁,則插入心跳包進行寫出操作,這樣才能做到比較及時的感知到socket的斷開(心跳包也不宜太頻繁,一般在30秒一次比較靠譜)。

 

類:連接管理

關鍵API:start, stop, appendOutput

public class IMConnectionSubject {

    private volatile Thread socketThread;
    private SocketRunnable socketRunnable;

    private final List<IMSocketEventListener> listeners = new ArrayList<>();
    private final BlockingQueue<byte[]> outputQueue = new ArrayBlockingQueue<>(5000);

    public void addListener(IMSocketEventListener listener) {
        synchronized (this) {
            listeners.add(listener);
        }

    }

    public void removeListener(IMSocketEventListener listener) {
        synchronized (this) {
            listeners.remove(listener);
        }
    }


    public void start(InetSocketAddress address) {
        if (socketThread == null) {
            synchronized (this) {
                if (socketThread == null) {
                    socketRunnable = new SocketRunnable(address);
                    socketThread = new Thread(socketRunnable);
                    socketThread.start();
                }
            }
        }
    }

    public void stop() {
        if (socketThread != null) {
            socketRunnable.notifyQuit();
            try {
                socketThread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }


    public void appendOutput(byte[] buffer) {
        outputQueue.add(buffer);
    }


    private class SocketRunnable implements Runnable {
        private final InetSocketAddress address;
        private final Object lock = new Object();
        private volatile boolean notified = false;

        public SocketRunnable(InetSocketAddress address) {
            this.address = address;
        }

        private List<IMSocketEventListener> getListenersSanpshot() {
            List<IMSocketEventListener> listenersSnapshot = new ArrayList<>();
            synchronized (IMConnectionSubject.this) {
                listenersSnapshot.addAll(listeners);
            }
            return listenersSnapshot;
        }

        public void notifyQuit() {
            synchronized (lock) {
                notified = true;
                lock.notify();
            }
        }

        @Override
        public void run() {
            Socket socket = new Socket();

            //
            // connect section
            //
            try {
                socket.connect(address);
            } catch (IOException e) {
                //e.printStackTrace();
                for (IMSocketEventListener listener : getListenersSanpshot()) {
                    listener.onConnectFailed(e.getMessage());
                }
                return;
            }

            for (IMSocketEventListener listener : getListenersSanpshot()) {
                listener.onConnected();
            }

            //
            // write & read section
            //
            try {
                while (true) {
                    synchronized (lock) {
                        lock.wait(100); // 100 ms
                        if (notified) {
                            break;
                        }
                    }

                    // 即使遠端關閉管道,這幾個屬性也不會變爲true
                    //System.out.println("isClosed:" + socket.isClosed()
                    //        + ", isInputShutdown:" + socket.isInputShutdown()
                    //        + ", isOutputShutdown:" + socket.isOutputShutdown());

                    byte[] polled = outputQueue.poll();
                    while (polled != null) {
                        // 如果遠端已關閉,則調用write將引發IOException
                        socket.getOutputStream().write(polled);
                        polled = outputQueue.poll();
                    }


                    // 即使遠端關閉,調用read也不會出任何異常,只會陷入無限等待
                    // int readed = socket.getInputStream().read(test, 0, 1);

                    int available = socket.getInputStream().available();
                    if (available > 0) {
                        byte buffer[] = new byte[available];
                        socket.getInputStream().read(buffer, 0, available);
                        for (IMSocketEventListener listener : getListenersSanpshot()) {
                            listener.onData(buffer);
                        }
                    }
                }


            } catch (IOException e) {
                e.printStackTrace();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 綜合上所述,如果客戶端需要檢測socket是否還活躍,只能通過定期發送數據
            // 來實現。因爲只有write纔會觸發IOException。
            try {
                socket.close();
                for (IMSocketEventListener listener : getListenersSanpshot()) {
                    listener.onDisconnected();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

Socket 事件偵聽接口定義:

public interface IMSocketEventListener {

    void onConnected();

    void onConnectFailed(String errorMessage);

    void onDisconnected();

    void onData(byte[] data);
}

 

測試類:

   在另外一個進程上,偵聽本地8080端口跑了一個Echo服務端。當檢測到客戶端發送過來的數據爲“quit”時,服務端將發回原始數據並關閉連接套接字。

 

public class IMConnectionSubjectTest implements IMSocketEventListener {

    private static final WaitCondition connectCompleteEvent = new WaitCondition();
    private static final WaitCondition disconnectedEvent = new WaitCondition();
    //
    // method defined in interface IMSocketEventListener
    // will called in socket thread.
    //

    @Override
    public void onConnected() {
        System.out.println("Connected");
        connectCompleteEvent.notifyResult(true);
    }

    @Override
    public void onConnectFailed(String errorMessage) {
        System.out.println("ConnectFailed: " + errorMessage);
        connectCompleteEvent.notifyResult(false);
    }

    @Override
    public void onDisconnected() {
        System.out.println("Disconnected");
        disconnectedEvent.notifyResult(true);
    }

    @Override
    public void onData(byte[] data) {
        System.out.println("Received " + new String(data));
    }

    public static void main(String[] args) {
        String testData[] = new String[] {
                "AAA",
                "BBB",
                "quit", // 服務端收到quit消息後會主動關閉套件字
                "MMM",
                "DDVS",
                "HHH",
                "III",
                "JJJ"
        };

        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8080);
        IMConnectionSubject connectionSubject = new IMConnectionSubject();
        IMConnectionSubjectTest appContainer = new IMConnectionSubjectTest();

        connectionSubject.addListener(appContainer);
        connectionSubject.start(address);

        // wait for connected or connect-failed
        boolean isConnectSucceed = connectCompleteEvent.waitResult();

        if (isConnectSucceed) {
            for (String data : testData) {
                boolean flag = disconnectedEvent.waitResult(2000);
                if (flag) {
                    break;
                }

                System.out.println("====>" + data);
                connectionSubject.appendOutput(data.getBytes());
            }

            connectionSubject.removeListener(appContainer);
            System.out.println("closing...");
            connectionSubject.stop();

        } else {
            System.out.println("Connect to remote failed");

        }
    }
}

以下是程序運行輸出內容,可以看到,在Received quit之後,其實服務端已經關閉了套件字管道;但是客戶端只有通過隨後的發包“MMM”才能得到IOException,從而感知到連接斷開:

Connected
====>AAA
Received AAA
====>BBB
Received BBB
====>quit
Received quit
====>MMM
====>DDVS
java.net.SocketException: Broken pipe (Write failed)
    at java.net.SocketOutputStream.socketWrite0(Native Method)
    at java.net.SocketOutputStream.socketWrite(SocketOutputStream.java:111)
    at java.net.SocketOutputStream.write(SocketOutputStream.java:143)
    at com.test.application.IMConnectionSubject$SocketRunnable.run(IMConnectionSubject.java:130)
    at java.lang.Thread.run(Thread.java:748)
Disconnected
closing...

Process finished with exit code 0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章