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