从零学IM(二)——基于TCP的socket长连接实现

前言

此文章主要记录本人在学习IM时的一些历程。主要包含以下内容(对TCP/IP协议这里不做介绍):

  • Java Socket API
  • 简单实现echo服务器
  • 基于android实现客户端TCP长连接

1 Socket 基本用法

Socket 是 TCP 层的封装,通过 socket,我们就能进行 TCP 通信。
在 Java 的 SDK 中,socket 的共有两个接口:用于监听客户连接的 ServerSocket 和用于通信的 Socket。

使用 Socket 的步骤如下:

  1. 创建 ServerSocket 并监听客户连接;
  2. 使用 Socket 连接服务端;
  3. 通过 Socket.getInputStream()/getOutputStream() 获取输入输出流进行通信。

2 Echo示例实现

创建ServerSocket服务端并监听客户端连接

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,运行效果如下:
从零学IM(二)——基于TCP的socket长连接实现_第1张图片

最后

最后需要注意的有几点

  1. 上面的代码中,所有的异常都没有处理。实际应用中,在发生异常时,需要关闭 socket,并根据实际业务做一些错误处理工作;
  2. 在客户端,我们没有停止 readThread。实际应用中,我们可以通过关闭 socket 来让线程从阻塞读中返回;
  3. 服务端只处理了一个客户连接。如果需要同时处理多个客户端,可以创建线程来处理请求;

3 实现长连接

背景知识

Socket 长连接,指的是在客户和服务端之间保持一个 socket 连接长时间不断开。大家知道Socket有这样一个 API:

socket.setKeepAlive(true);

keep-alive:让 TCP 不断开的意思。那么,我们要实现一个 socket 的长连接是否只需要这一个调用即可?
遗憾的是,生活并不总是那么美好。Socket 的这个 keep-alive 选项如果打开并且两个小时内没有通信,那么底层会发一个心跳,看看对方是不是还活着。

注意:两个小时才会发一次。也就是说,在没有实际数据通信的时候,我把网线拔了,你的应用程序要经过两个小时才会知道。

假定现在有一对已经连接的 socket,在以下情况发生时候,socket 将不再可用:

  • 某一端socket关闭
  • 程序崩溃
  • 系统崩溃
  • 内存不足程序被杀死
  • 网络不在可用

在上面的几种情形中,去读、写 socket,只要 socket 连接不正常,就能够知道。基于这一点,实现一个 socket 长连接,要做的就是不断地给对方写数据,然后读取对方的数据即心跳。只要心还在跳,socket 就是活的。心跳数据的间隔,根据实际需求来决定就好。

心跳包不是实际的业务数据,根据通信协议的不同,需要做不同的处理。比如:我们使用 JSON 进行通信,为协议包加一个 messageType 字段,区分心跳和业务数据

{
    "messageType": 0,  // 0 表示心跳
}

代码实现

鉴于初学这里只实现一个长连接的 Android echo 客户端。重连的等待时间、读写、连接的 timeout、消息分割(定长、分隔符)均未涉及。

  • 首先定义LongLifeSocket负责处理心跳、消息的发送和接收
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;
        }
    }
}
  • 然后定义EchoClient进行业务处理
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(二)——基于TCP的socket长连接实现_第2张图片

写在最后

本文只对长连接做了简单实现,IM中重连、质量保证等机制未涉及

源码地址:
Server demo
Client demo

你可能感兴趣的:(长连接,server,android,im,socket,tcpip)