此文章主要记录本人在学习IM时的一些历程。主要包含以下内容(对TCP/IP协议这里不做介绍):
Socket 是 TCP 层的封装,通过 socket,我们就能进行 TCP 通信。
在 Java 的 SDK 中,socket 的共有两个接口:用于监听客户连接的 ServerSocket 和用于通信的 Socket。
使用 Socket 的步骤如下:
public class EchoServer {
private final ServerSocket mServerSocket;
public EchoServer(int port) throws IOException {
// 1. 创建一个 ServerSocket 并监听端口 port
mServerSocket = new ServerSocket(port);
}
public void run() throws IOException {
// 2. 开始接受客户连接
Socket client = mServerSocket.accept();
handleClient(client);
}
private void handleClient(Socket socket) throws IOException {
// 3. 使用 socket 进行通信 ...
InputStream in = socket.getInputStream();
OutputStream out = socket.getOutputStream();
byte[] buffer = new byte[1024];
int n;
while ((n = in.read(buffer)) > 0) {
out.write(buffer, 0, n);
}
}
public static void main(String[] argv) {
try {
EchoServer server = new EchoServer(9877);
server.run();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class EchoClient {
private final Socket mSocket;
public EchoClient(String host, int port) throws IOException {
// 创建 socket 并连接服务器
mSocket = new Socket(host, port);
}
public void run() throws IOException {
// 和服务端进行通信
//客户端会稍微复杂一点点,在读取用户输入的同时,我们又想读取服务器的响应。所以,这里创建了一个线程来读服务器的响应。
Thread readerThread = new Thread(this::readResponse);
readerThread.start();
OutputStream out = mSocket.getOutputStream();
byte[] buffer = new byte[1024];
int n;
while ((n = System.in.read(buffer)) > 0) {
out.write(buffer, 0, n);
}
}
private void readResponse() {
try {
InputStream in = mSocket.getInputStream();
byte[] buffer = new byte[1024];
int n;
while ((n = in.read(buffer)) > 0) {
System.out.write(buffer, 0, n);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] argv) {
try {
// 由于服务端运行在同一主机,这里我们使用 localhost
EchoClient client = new EchoClient("localhost", 9877);
client.run();
} catch (IOException e) {
e.printStackTrace();
}
}
}
接下来在idea中分别运行server和client,运行效果如下:
最后需要注意的有几点
Socket 长连接,指的是在客户和服务端之间保持一个 socket 连接长时间不断开。大家知道Socket有这样一个 API:
socket.setKeepAlive(true);
keep-alive:让 TCP 不断开的意思。那么,我们要实现一个 socket 的长连接是否只需要这一个调用即可?
遗憾的是,生活并不总是那么美好。Socket 的这个 keep-alive 选项如果打开并且两个小时内没有通信,那么底层会发一个心跳,看看对方是不是还活着。
注意:两个小时才会发一次。也就是说,在没有实际数据通信的时候,我把网线拔了,你的应用程序要经过两个小时才会知道。
假定现在有一对已经连接的 socket,在以下情况发生时候,socket 将不再可用:
在上面的几种情形中,去读、写 socket,只要 socket 连接不正常,就能够知道。基于这一点,实现一个 socket 长连接,要做的就是不断地给对方写数据,然后读取对方的数据即心跳。只要心还在跳,socket 就是活的。心跳数据的间隔,根据实际需求来决定就好。
心跳包不是实际的业务数据,根据通信协议的不同,需要做不同的处理。比如:我们使用 JSON 进行通信,为协议包加一个 messageType 字段,区分心跳和业务数据
{
"messageType": 0, // 0 表示心跳
}
鉴于初学这里只实现一个长连接的 Android echo 客户端。重连的等待时间、读写、连接的 timeout、消息分割(定长、分隔符)均未涉及。
public final class LongLifeSocket {
private static final String TAG = LongLifeSocket.class.getSimpleName();
private static final long RETRY_INTERVAL_MILLIS = 3 * 1000;
private static final long HEART_BEAT_INTERVAL_MILLIS = 5 * 1000;
private static final long HEART_BEAT_TIMEOUT_MILLIS = 2 * 1000;
/**
* 错误回调
*/
public interface ErrorCallback {
/**
* 如果需要重连,返回 true
*/
boolean onError();
}
/**
* 读数据回调
*/
public interface DataCallback {
void onData(byte[] data, int offset, int len);
}
/**
* 写数据回调
*/
public interface WritingCallback {
void onSuccess();
void onFail(byte[] data, int offset, int len);
}
private final String mHost;
private final int mPort;
private final DataCallback mDataCallback;
private final ErrorCallback mErrorCallback;
private final HandlerThread mWriterThread;
private final Handler mWriterHandler;
private final Handler mUIHandler = new Handler(Looper.getMainLooper());
private final Object mLock = new Object();
private Socket mSocket; // guarded by mLock
private boolean mClosed; // guarded by mLock
private final Runnable mHeartBeatTask = new Runnable() {
private byte[] mHeartBeat = new byte[0];
@Override
public void run() {
// 我们使用长度为 0 的数据作为 heart beat
write(mHeartBeat, new WritingCallback() {
@Override
public void onSuccess() {
// 每隔 HEART_BEAT_INTERVAL_MILLIS 发送一次
mWriterHandler.postDelayed(mHeartBeatTask, HEART_BEAT_INTERVAL_MILLIS);
mUIHandler.postDelayed(mHeartBeatTimeoutTask, HEART_BEAT_TIMEOUT_MILLIS);
}
@Override
public void onFail(byte[] data, int offset, int len) {
// nop
// write() 方法会处理失败
}
});
}
};
private final Runnable mHeartBeatTimeoutTask = new Runnable() {
@Override
public void run() {
Log.e(TAG, "mHeartBeatTimeoutTask#run: heart beat timeout");
closeSocket();
}
};
public LongLifeSocket(String host, int port,
DataCallback dataCallback, ErrorCallback errorCallback) {
mHost = host;
mPort = port;
mDataCallback = dataCallback;
mErrorCallback = errorCallback;
mWriterThread = new HandlerThread("socket-writer");
mWriterThread.start();
mWriterHandler = new Handler(mWriterThread.getLooper());
mWriterHandler.post(new Runnable() {
@Override
public void run() {
initSocket();
}
});
}
private void initSocket() {
while (true) {
if (closed()) return;
try {
Socket socket = new Socket(mHost, mPort);
synchronized (mLock) {
// 在我们创建 socket 的时候,客户可能就调用了 close()
if (mClosed) {
silentlyClose(socket);
return;
}
mSocket = socket;
// 每次创建新的 socket,会开一个线程来读数据
Thread reader = new Thread(new ReaderTask(socket), "socket-reader");
reader.start();
mWriterHandler.post(mHeartBeatTask);
}
break;
} catch (IOException e) {
Log.e(TAG, "initSocket: ", e);
if (closed() || !mErrorCallback.onError()) {
break;
}
try {
TimeUnit.MILLISECONDS.sleep(RETRY_INTERVAL_MILLIS);
} catch (InterruptedException e1) {
// interrupt writer-thread to quit
break;
}
}
}
}
public void write(byte[] data, WritingCallback callback) {
write(data, 0, data.length, callback);
}
public void write(final byte[] data, final int offset, final int len, final WritingCallback callback) {
mWriterHandler.post(new Runnable() {
@Override
public void run() {
Socket socket = getSocket();
if (socket == null) {
// initSocket 失败而客户说不需要重连,但客户又叫我们给他发送数据
throw new IllegalStateException("Socket not initialized");
}
try {
OutputStream outputStream = socket.getOutputStream();
DataOutputStream out = new DataOutputStream(outputStream);
out.writeInt(len);
out.write(data, offset, len);
callback.onSuccess();
} catch (IOException e) {
Log.e(TAG, "write: ", e);
closeSocket();
callback.onFail(data, offset, len);
if (!closed() && mErrorCallback.onError()) {
initSocket();
}
}
}
});
}
private boolean closed() {
synchronized (mLock) {
return mClosed;
}
}
private Socket getSocket() {
synchronized (mLock) {
return mSocket;
}
}
private void closeSocket() {
synchronized (mLock) {
closeSocketLocked();
}
}
private void closeSocketLocked() {
if (mSocket == null) return;
silentlyClose(mSocket);
mSocket = null;
mWriterHandler.removeCallbacks(mHeartBeatTask);
}
public void close() {
if (Looper.getMainLooper() == Looper.myLooper()) {
new Thread() {
@Override
public void run() {
doClose();
}
}.start();
} else {
doClose();
}
}
private void doClose() {
synchronized (mLock) {
mClosed = true;
// 关闭 socket,从而使得阻塞在 socket 上的线程返回
closeSocketLocked();
}
mWriterThread.quit();
// 在重连的时候,有个 sleep
mWriterThread.interrupt();
}
private static void silentlyClose(Socket closeable) {
if (closeable != null) {
try {
closeable.close();
} catch (IOException e) {
Log.e(TAG, "silentlyClose: ", e);
// error ignored
}
}
}
private class ReaderTask implements Runnable {
private final Socket mSocket;
public ReaderTask(Socket socket) {
mSocket = socket;
}
@Override
public void run() {
try {
readResponse();
} catch (IOException e) {
Log.e(TAG, "ReaderTask#run: ", e);
}
}
private void readResponse() throws IOException {
// For simplicity, assume that a msg will not exceed 1024-byte
byte[] buffer = new byte[1024];
InputStream inputStream = mSocket.getInputStream();
DataInputStream in = new DataInputStream(inputStream);
while (true) {
int nbyte = in.readInt();
if (nbyte == 0) {
Log.i(TAG, "readResponse: heart beat received");
mUIHandler.removeCallbacks(mHeartBeatTimeoutTask);
continue;
}
if (nbyte > buffer.length) {
throw new IllegalStateException("Receive message with len " + nbyte +
" which exceeds limit " + buffer.length);
}
if (readn(in, buffer, nbyte) != 0) {
// Socket might be closed twice but it does no harm
silentlyClose(mSocket);
// Socket will be re-connected by writer-thread if you want
break;
}
mDataCallback.onData(buffer, 0, nbyte);
}
}
private int readn(InputStream in, byte[] buffer, int n) throws IOException {
int offset = 0;
while (n > 0) {
int readBytes = in.read(buffer, offset, n);
if (readBytes < 0) {
// EoF
break;
}
n -= readBytes;
offset += readBytes;
}
return n;
}
}
}
public class EchoClient {
private static final String TAG = EchoClient.class.getSimpleName();
private final LongLifeSocket mLongLiveSocket;
public EchoClient(String host, int port) {
mLongLiveSocket = new LongLifeSocket(
host, port,
new LongLifeSocket.DataCallback() {
@Override
public void onData(byte[] data, int offset, int len) {
Log.i(TAG, "EchoClient: received: " + new String(data, offset, len));
}
},
// 返回 true,所以只要出错,就会一直重连
new LongLifeSocket.ErrorCallback() {
@Override
public boolean onError() {
return true;
}
});
}
public void send(String msg) {
mLongLiveSocket.write(msg.getBytes(), new LongLifeSocket.WritingCallback() {
@Override
public void onSuccess() {
Log.d(TAG, "onSuccess: ");
}
@Override
public void onFail(byte[] data, int offset, int len) {
Log.w(TAG, "onFail: fail to write: " + new String(data, offset, len));
// 连接成功后,还会发送这个消息
mLongLiveSocket.write(data, offset, len, this);
}
});
}
}
首先启动SocketServer程序
然后启动android demo
本文只对长连接做了简单实现,IM中重连、质量保证等机制未涉及
源码地址:
Server demo
Client demo