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