本篇文章主要从架构设计和源码的角度讲解客户端是如何一步一步建立长连接底层框架的。主要介绍如何优美的创建长连接,如何设计发送消息框架,如何设计回执框架,如何设计消息分发框架等内容。
长连接总架构
Client
负责对外提供框架能力。
-
Client:对外提供同步调用方法。比如connect,disConnect等方法。
-
AsyncClient:对外提供异步调用方法。
-
ICallback:是指长连接声明周期回调接口,供上层业务实现使用。比如connectLost(),messageArrived 等等。
-
IActionListener: 客户端请求回调接口,可以在具体请求方法中作为参数使用。
-
Token:客户端调用请求管理的令牌对象,业务可以拿该Token。
-
Message:统一对外的消息对象,所有请求和消息最终封装成该对象再发起请求。(每个业务不一样,这里通过一个统一的Message来代表消息对象)
ClientComms
负责长连接的建立和断开框架管理。
-
Socket:Socket网络管理。
-
SenderThread:发送消息线程 ReceiverThread:接收消息线程
-
CallbackThread:消息回调线程
ClientState
负责消息状态管理,比如消息发送,消息通知等管理。
-
Sender:消息发送管理
-
Notify:消息通知管理
-
Callback:消息回调
-
Save:存储(根据自身业务实际情况评估是否需要存储)
ClientOther
- SmartHB: 智能心跳。本次主要讲解框架搭建,不会介绍智能心跳细节。
接下来我们将逐个介绍上述模块的实现过程。
ClientComms
ClientComms模块主要负责长连接的建立和断开框架管理。
网络模块
-
NetWork: 网络模块对外接口。
-
LocalNetwork:本地网络实现。一般用于本地调试。
-
TCPNetwork:TCP协议网络连接实现。
-
SSLNetwork:基于TCP的SSL通道安全连接。
-
UdpNetwork:udp协议的连接。
这么设计可以让各种不同协议之间的连接建立业务互相解耦。下面我们来看看具体代码是如何一步一步演进的。
一般我们是通过操作Socket来建立长连接的,代码如下:
public class NetWork {
protected Socket mSocket;
private OutputStream mOutputStream;
private InputStream mInputStream;
public void start(String host, int port) {
//获取socketFactory 对象,创建Socket要用到
SocketFactory socketFactory = SocketFactory.getDefault();
//构建网络地址对象
SocketAddress sockaddr = new InetSocketAddress(host, port);
//创建Socket对象
mSocket = socketFactory.createSocket();
//建立Socket连接,连接服务器
mSocket.connect(sockaddr, 5 * 1000);
//获取OutputStream, 发送消息时写入数据
mOutputStream = mSocket.getOutputStream();
//获取InputStream, 接收消息时读取数据
mInputStream = mSocket.getInputStream();
}
}
通过上面代码,我们知道,建立socket连接需要下面3个必要信息:
-
建立连接的host 和 port,即服务器远程的ip和port
-
建立连接超时时间
-
建立连接的SocketFactory
那么我们可以将必要信息参数化,修改后如下:
public class NetWork {
protected Socket mSocket;
private OutputStream mOutputStream;
private InputStream mInputStream;
public void start(SocketFactory socketFactory, String host, int port, long timeout) {
//构建网络地址对象
SocketAddress sockaddr = new InetSocketAddress(host, port);
//创建Socket对象
mSocket = socketFactory.createSocket();
//建立Socket连接,连接服务器
mSocket.connect(sockaddr, timeout);
//获取OutputStream, 发送消息时写入数据
mOutputStream = mSocket.getOutputStream();
//获取InputStream, 接收消息时读取数据
mInputStream = mSocket.getInputStream();
}
}
至此,上述代码已经足够支撑我们建立长连接了。但是还是有如下几个问题?
-
如果建立连接时有多个业务参数也需要传递给服务器怎么办?
-
上面是实现了TCP普通模式下的建连任务,那么如果要使用SSL通道加密的长连接怎么办呢?(不要想着在start里面添加if-else 来分开处理,这个违背我们设计的开闭原则以及单一职责原则)
-
我们的读写数据也在这里实现吗?
解决多个参数问题
我们可以通过定义专门的对象来负责必要的参数管理。比如建立 ConnectOption 对象
public class ConnectOption {
private SocketFactory mSocketFactory;
private String mHost;
private int mPort;
private int mTimeout;
private String mUserName;
private String mPassword;
private String mSSLVersion;
......
......
}
解决不同方式建连场景
其实,我们可以将建连网络模块行为继续抽象一下,主要包括一下行为:
-
建立Socket连接
-
停止Socket连接
-
提供 ImputStream
-
提供 OutputStream
基于此,我们可以这么来设计
- 定义一个网络连接接口
public interface NetWork {
void start();
void stop();
InputStream getInputStream();
OutputStream getOutputStream();
}
定义TCP 网络连接模块
public class TCPNetWork implement NetWork {
protected Socket mSocket;
private OutputStream mOutputStream;
private InputStream mInputStream;
private SocketFactory mSocketFactory;
private String mHost;
private int mPort;
private long mTimeout;
public TCPNetWork(ConnectOption option) {
this(option.getSocketFactory(), option.getHost(), option.getPort(), option.getTimeout());
}
public TCPNetWork(SocketFactory factory, String host, int port, long timeout) {
mSocketFactory = factory;
mHost = host;
mPort = port;
mTimeout = timeout;
}
public void start() {
//构建网络地址对象
SocketAddress sockaddr = new InetSocketAddress(mHost, mPort);
//创建Socket对象
mSocket = mSocketFactory.createSocket();
//建立Socket连接,连接服务器
mSocket.connect(sockaddr, mTimeout);
//获取OutputStream, 发送消息时写入数据
mOutputStream = mSocket.getOutputStream();
//获取InputStream, 接收消息时读取数据
mInputStream = mSocket.getInputStream();
}
public void stop() throws IOException {
if (mSocket != null) {
mSocket.close();
}
}
public InputStream getInputStream() {
return mInputStream;
}
OutputStream getOutputStream() {
return mOutputStream;
}
}
note:
- 上述 TCPNetWork 中有2个构造方法,请问哪个构造方法更好,为什么?
- 为什么发送数据叫out, 接收数据叫input?
定义SSL 网络模块
public class SSLNetWork extends TCPNetWork {
protected Socket mSocket;
private OutputStream mOutputStream;
private InputStream mInputStream;
private SocketFactory mSocketFactory;
private String mHost;
private int mPort;
private long mTimeout;
public SSLNetWork(ConnectOption option) {
super(option);
}
public SSLNetWork(SocketFactory factory, String host, int port, long timeout) {
super(factory, host, port, timeout);
}
public void start() {
//复用父类的start 建连方法。
super.start();
//增加SSL 安全秘钥协商相关代码
((SSLSocket) mSocket).addHandshakeCompletedListener(“若上层需要监听握手协商完成的回调,在这里添加listener即可”);
//开始握手协商秘钥等动作。
((SSLSocket) mSocket).startHandshake();
}
}
至此,我们的长连接常用的两个建连模块就设计完成了。 这么做有什么好处呢?
- 稳定性(符合单一职责原则)。上层业务调用建连方法确定之后,后续针对连接模块的改动,几乎对上层业务都没有影响。
- 扩展性(符合开闭原则)。比如哪天要扩展一种新协议(比如quic协议或者自定义的udp协议),直接扩展一个 NetWork 实现类即可,而不会涉及到对已有代码的改动,这是if-else 解决不了的。
注意,因为SSL握手过程比较耗时,涉及到证书链传输和校验过程,此处我们可以暂时将Socket IO超时时间设为5s(具体时间根据实际业务来控制),防止建连过程一直被阻塞。
消息收发管理模块
Socket连接建立之后,我们需要基于InputStream 和 OutputStream 来进行数据处理,这就是我们消息收发模块需要完成的任务。具体我们要做下面几件事:
-
创建独立发送消息线程
-
创建独立接收消息线程
-
负责数据流的解析和消息封装(接收线程)
-
负责消息队列管理(发送线程)
创建线程
如果只是写个简单的demo,能够简单的收发消息的话,那么下面这段代码就可以满足要求了
public void connect(NetWork netWork, ConnectOption option) {
//建立连接
netWork.start();
new Thread(new Runnable() {
while(true) {
//处理接收数据业务
InputStream inputStream = netWork.getInputStream();
inputStream.read();
。。。
。。。
}
}, "receive").start();
new Thread(new Runnable() {
while(true) {
//处理发送数据业务
OutputStream os = netWork.getOutputStream();
os.write("123".getBytes(StandardCharsets.UTF_8), 0 , 123".length());
。。。
。。。
}
}, "send").start();
}
上面这段代码虽然也能跑起来,但是也是有比较大的问题的。
-
建连过程是在业务线程同步调用的,可能会阻塞上层的业务线程。
-
发送线程和接收线程 都是一个while 死循环。 没有退出循环,并且发送线程会一直循环调用,停不下来,会导致功耗问题。
-
消息线程没有体现出来消息的读取与解析过程。
-
发送线程没有体现出来发送队列的能力。
为了解决上面的问题,我们可以这么做。
异步建连
public void connect(NetWork netWork, ConnectOption option) {
//构建异步线程并启动
new ConnectBG().start();
}
class ConnectBG implements Runnable {
Thread mBg = null;
ConnectBG() {
mBg = new Thread(this, "connect");
}
public void start() {
mBg.start();
}
public void run() {
//建连前已经准备好了, 直接连接即可。
mNetwork.start();
new Thread(new Runnable() {
while(true) {
//处理接收数据业务
InputStream inputStream = netWork.getInputStream();
inputStream.read();
。。。
。。。
}
}, "receive").start();
new Thread(new Runnable() {
while(true) {
//处理发送数据业务
netWork.getOutputStream();
。。。
。。。
}
}, "send").start();
}
}
到这里我们就先完成了异步建连的功能,这里你可能会有疑问,直接new 一个Thread 操作一下不就可以了吗?这里示例有些功能并没有展示出来,其实在建连时还有一些其他业务需要处理,比如我们Socket建立成功之后,一般会有Connect的业务数据需要发送给服务器,这个过程其实也是在建连过程中的一部分,这里没有展示出来而已。
note:我们尽可能通过对象来封装相关功能,这点很重要。
接收线程优化
跟异步建连一样,因为消息接收业务是比较复杂的一个过程,所以直接使用一个Thread 的Runnable 囊括所有的业务处理不太现实(即使可以实现复杂度也会陡增)。所以这里我们首先定义一个消息接收的业务类,它需要包含下面几个能力:
-
循环接收消息
-
数据流转业务数据
-
需要有主动停止接收数据的能力
具体代码如下:
public void connect(NetWork netWork, ConnectOption option) {
//构建异步线程并启动
new ConnectBG().start();
}
class ConnectBG implements Runnable {
Thread mBg = null;
ConnectBG() {
mBg = new Thread(this, "connect");
}
public void start() {
mBg.start();
}
public void run() {
//建连前已经准备好了, 直接连接即可。
mNetwork.start();
//构造消息接收对象。
Receiver receiver = new Receiver(mNetwork.getInputStream);
receiver.start();
new Thread(new Runnable() {
while(true) {
//处理发送数据业务
netWork.getOutputStream();
。。。
。。。
}
}, "send").start();
}
}
class Receiver implements Runnable {
//业务自定义一个 InputStream,专门用于socket数据读取,这样业务更内聚,不然Receiver类中会充斥大量的数据解析相关业务代码
MyInputStream mMyInputStream;
//启动和停止 的锁对象。
private final Object mLifecycle = new Object();
//接收线程是否正在运行
private boolean mRunning = false;
//真正的运行线程对象
private Thread mRecThread = null;
Receiver(InputStream inputStream) {
mMyInputStream = new MyInputStream(inputStream);
}
public void start(String threadName) {
synchronized (mLifecycle) {
if (!mRunning) {
mRunning = true;
mRecThread = new Thread(this, threadName);
mRecThread.start();
}
}
}
public void stop() {
synchronized (mLifecycle) {
if (mRunning) {
mRunning = false;
if (!Thread.currentThread().equals(mRecThread)) {
try {
// Wait for the thread to finish.
mRecThread.join();
} catch (InterruptedException ex) {
}
}
}
}
//线程结束后将对象赋值为null,防止继续使用出错。
mRecThread = null;
}
public void run() {
while (mRunning && mMyInputStream != null ) {
try {
Message msg = mMyInputStream.readMessage();
if (如果是回执) {
//通知上层业务请求已经响应。
} else {
//不是回执信息,则通知分发业务收到了一条新消息
}
} catch (Exception e) {
mRunning = false;
//断开长连接
} catch (IOException ioe) {
mRunning = false;
if (当前不是断开中状态) {
//断开长连接
}
}
}//end while
}
}
至此,接收线程的基本框架已经成型了。此时我们已经完成了上面说的3个功能:
-
到循环接收消息。 我们通过 mRunning 状态来循环接收消息,并且通过阻塞式的read 方法来读取数据(可以使线程暂停,不至于一没数据的时候也一直跑)。
-
数据流转业务数据。我们通过readMessage() 方法将二进制数据流转为我们业务消息对象。具体转换过程这里省略了。
-
需要有主动停止接收数据的能力。 我们有提供stop的能力。
下面简单介绍下读取数据原理。 一般任何业务场景数据都是分为
固定头部 | 业务数据 |
---|
其中固定头部又一般包含下面的数据
魔数 | userName | password | 剩余长度 |
---|
所以上面的 readMessage() 方法大致执行流程如下:
-
读一个字节。判断是不是和魔数相等,如果不相等,则继续一直读一个字节,直到遇到魔数为止。
-
读固定长度的userName 数据。一般读取字符串是动态读取的,即一个字节一个字节的读,直到读取到的字节的最高一位为1时表示该字符串读完了。
-
读固定长度的 password 数据。这里读 userName 和 password 只是举个例子,具体业务以自己业务实际数据为准。
-
读取剩余长度。最多2个字节。字节的最高位为1则表示长度读完了。
-
最后一次性将所有剩余数据全部读出来,并且转为对应的业务消息对象即可。
下次用一篇独立的文章来专门分析数据设计和解析这部分内容。
发送线程优化
我们在进行发送之前,首先得像接收线程一样可以启动和主动停止 发送线程。其实这里我们还得明确一件事,即我们的发送线程只负责发送数据,它是符合单一职责原则的。 具体如下:
public void connect(NetWork netWork, ConnectOption option) {
//构建异步线程并启动
new ConnectBG().start();
}
class ConnectBG implements Runnable {
Thread mBg = null;
ConnectBG() {
mBg = new Thread(this, "connect");
}
public void start() {
mBg.start();
}
public void run() {
//建连前已经准备好了, 直接连接即可。
mNetwork.start();
//构造消息接收对象。
Receiver receiver = new Receiver(mNetwork.getInputStream);
receiver.start();
Sender sender = new Sender(mNetwork.getOutputStream);
sender.start();
}
}
class Sender implements Runnable {
//业务自定义一个 OutStream,专门用于socket数据写入,这样业务更内聚,
MyOutStream mMyOutStream;
//启动和停止 的锁对象。
private final Object mLifecycle = new Object();
//接收线程是否正在运行
private boolean mRunning = false;
//真正的运行线程对象
private Thread mSendThread = null;
Receiver(OutputStream outStream) {
mMyOutStream = new MyOutStream(outStream);
}
public void start(String threadName) {
synchronized (mLifecycle) {
if (!mRunning) {
mRunning = true;
mSendThread = new Thread(this, threadName);
mSendThread.start();
}
}
}
public void stop() {
synchronized (mLifecycle) {
if (mRunning) {
mRunning = false;
if (!Thread.currentThread().equals(mSendThread)) {
try {
//这里首先要唤醒mSendThread ,然后等待其执行完成。问题:为什么Receiver 线程不需要通知呢?
// Wait for the thread to finish.
mSendThread.join();
} catch (InterruptedException ex) {
}
}
}
}
//线程结束后将对象赋值为null,防止继续使用出错。
mRecThread = null;
}
public void run() {
while (mRunning) {
try {
//get 是封装好的一个方法,负责从队列中取出第一条消息发送到服务器
Message msg = get();
if (如果是回执) {
//二次封装的write 方法,内部需要处理每次写入的数据块大小,若数据超过单个数据块大小则需要分批写入
mMyOutStream.write(msg);
//将缓存数据立即写入到输出流的目的地(比如文件、网络连接等)
mMyOutStream.flush();
} else {
mMyOutStream.write(msg);
mMyOutStream.flush();
//需要通知该消息已经发送,更新相关状态。
notifySend(msg);
}
} catch (Exception e) {
mRunning = false;
//断开长连接
} catch (IOException ioe) {
mRunning = false;
if (当前不是断开中状态) {
//断开长连接
}
}
}//end while
}
}
至此,我们的Sender 线程任务已经完成了。发送消息流程如下:
-
首先从队列中取出首条消息
-
将消息数据写入到Socket连接中
-
如果不是回执消息,发送完成后需要更新相关状态。
由此可以看出,其实我们把每个类的功能明确之后,业务并不复杂。但是若是把所有细节都掺杂在一个类里面实现,那这个类就是一个大杂烩,变得异常复杂,维护起来不出问题才怪。
虽然发送线程实现了,但是我们的功能实现还并不完整,这里有几个问题需要考虑:
-
get() 获取消息逻辑具体是怎么样的?
-
sender线程中并没有阻塞线程的方法,难道sender 线程一直循环跑吗?
-
消息发送之后通知更新状态应用在什么场景?为什么需要存在?
接下来在消息队列设计中我们将找到答案。
ClientState
ClientState 负责消息发送与回执等状态管理。
消息发送管理
接下来我们看看我们的消息是如何一步一步发送出去的。
队列设计
首先我们得需要有一个消息队列,用于缓存所有的发送消息。其实消息队列设计也不复杂,当发送消息时将数据入队列, 然后唤醒Sender线程,sender线程此时会去读消息(此处的get方法)
public class ClientState {
private volatile Vector<Message> mPendingMessages = new Vector();
private final Object mQueueLock = new Object();
public void send(Message msg) {
synchronized (mQueueLock) {
mPendingMessages.addElement(message);
mQueueLock.notifyAll();
}
}
public void get() {
Message msg = null;
synchronized (mQueueLock) {
while(msg == null) {
//如果队列没有消息,则wait Sender线程
if(mPendingMessages.isEmpty()) {
mQueueLock.wait();
}
//sender 线程被唤醒后,取出消息队列首个元素,并返回
if(!mPendingMessages.isEmpty()) {
msg = mPendingMessages.elementAt(0);
mPendingMessages.removeElementAt(0);
}
} //end while
} //end synchronized
return msg;
}
//撤回消息
public void undo(Message msg) {
synchronized (mQueueLock) {
mPendingMessages.removeElement(message);
}
}
}
队列优先级
因为长连接一般会主动收到服务器的消息,需要给其发送回执,因为回执信息对时效性比较高,一般服务器都会等待该消息的回执,所以这里如果我们的队列已经积压了大量发送消息的话,就来不及响应回执的发送。 那我们可以怎么优化呢?是不是可以考虑将回执消息的发送单独申请一个队列呢?并且优先分发回执队列里面的消息。
代码可以做如下优化:
public class ClientState {
private volatile Vector<Message> mPendingMessages = new Vector();
private volatile Vector<Message> mAckMessages = new Vector();
private final Object mQueueLock = new Object();
public void send(Message msg) {
synchronized (mQueueLock) {
if (如果是回执信息) {
mAckMessages.addElement(message);
mQueueLock.notifyAll();
} else {
mPendingMessages.addElement(message);
mQueueLock.notifyAll();
}
}
}
public void get() {
Message msg = null;
synchronized (mQueueLock) {
while(msg == null) {
//如果队列没有消息,则wait Sender线程
if((mPendingMessages.isEmpty() && mAckMessages.isEmpty())) {
mQueueLock.wait();
}
//sender 线程被唤醒后,优先取回执消息发送,再取普通消息发送
if(!mAckMessages.isEmpty()) {
msg = mPendingMessages.elementAt(0);
mPendingMessages.removeElementAt(0);
} else if (!mPendingMessages.isEmpty()) {
msg = mPendingMessages.elementAt(0);
mPendingMessages.removeElementAt(0);
}
} //end while
} //end synchronized
return msg;
}
//撤回消息
public void undo(Message msg) {
synchronized (mQueueLock) {
mPendingMessages.removeElement(message);
}
}
}
经过上面优化,我们就做到了回执消息最先发送,再发送普通请求消息,即做了一个优先级处理。可以类比哈,如果业务上有其他多种业务消息需要发送,可以指定对应的优先级队列,优先分发高优先级队列消息。
限流设计
这里还有一个问题,若是业务高并发量级比较大,而服务器资源有限,那我们可能就需要限流(也有可能是系统网络框架数据缓冲区有限),那如何设计这个限流策略呢?首先限流不能影响我们的回执效率,但是又能限制普通消息的发送速率。
public class ClientState {
private volatile Vector<Message> mPendingMessages = new Vector();
private volatile Vector<Message> mAckMessages = new Vector();
private final Object mQueueLock = new Object();
//最多同时发送10条普通消息
private int mMaxInflight = 10;
//当前正在发送的消息量级
private int mActualInFlight = 0;
public void send(Message msg) {
synchronized (mQueueLock) {
if (如果是回执信息) {
mAckMessages.addElement(message);
mQueueLock.notifyAll();
} else {
mPendingMessages.addElement(message);
mQueueLock.notifyAll();
}
}
}
public void get() {
Message msg = null;
synchronized (mQueueLock) {
while(msg == null) {
//如果队列没有消息,则wait Sender线程. 当没有回执消息,但是普通消息正在发送量级大于最大量级也需要先暂停下。
if((mPendingMessages.isEmpty() && mAckMessages.isEmpty())
|| (mAckMessages.isEmpty() && mActualInFlight >= mMaxInflight)) {
mQueueLock.wait();
}
//sender 线程被唤醒后,优先取回执消息发送,再取普通消息发送
if(!mAckMessages.isEmpty()) {
msg = mPendingMessages.elementAt(0);
mPendingMessages.removeElementAt(0);
} else if(!mPendingMessages.isEmpty()) {
msg = mPendingMessages.elementAt(0);
mPendingMessages.removeElementAt(0);
mActualInFlight++;
}
} //end while
} //end synchronized
return msg;
}
//撤回消息
public void undo(Message msg) {
synchronized (mQueueLock) {
mPendingMessages.removeElement(message);
}
}
//消息发送之后,需要通知将
public void notifySend(msg) {
if (msg是普通消息) {
decrementInFlight();
}
}
private void decrementInFlight() {
synchronized (mQueueLock) {
//发送数量减1
mActualInFlight--;
//唤醒发送线程继续发送消息
mQueueLock.notifyAll();
}
}
}
首先我们可以定义一个最大并发发送量级,比如10条。当发送一条消息后将其+1, 然后当发送消息并发量级大于等于10条时,我们将发送线程阻塞住,当消息发送后将发送数量减一并唤醒Sender线程继续发送消息。
至此,我们的发送消息队列管理已经基本完成了。
消息回执管理
到目前为止,我们的消息是发送出去了,但是我们好像获取不到服务器的响应结果。此时我们需要考虑对应请求的回执管理了,那么怎么设计这个回执管理系统呢?首先我们得搞清楚,这个回执管理系统承担哪些必要的功能
-
请求id管理(内部管理,不需要面向业务)
-
请求等待与超时管理
-
负责请求回调处理,即支持上层业务传入回调Listener,同时回调该Listener
-
负责所有请求的回执调度管理
这里我们可以设计一个令牌机制,即当发起一个请求事件时,我们给这次请求生成一个令牌并返回给请求者,对方可以查询该请求的当前状态。令牌接口定义如下:
定义Token接口
public interface IToken {
//该令牌的唯一标识符
long getId();
//等待请求返回,中途可能发生Exception,所以我们需要捕获对应的异常
void waitForCompletion() throws Exception;
//等待请求返回,指定等待时长。
void waitForCompletion(long timeout) throws Exception;
//获取当前请求服务器返回对象。简单起见,这里就不定义具体的Response对象了,因为每个业务不一样。
Response getResponse();
//设置listener
void setListener(Listener listener);
//获取listener
Listener getListener();
}
定义Token实现
我们再来看具体的实现
public class Token implements IToken {
private long mId = 0;
private static final long SID = 0;
//等待回执时操作持锁
private final Object mResponseLock = new Object();
//是否完成请求,收到服务器返回数据即算完成。
private volatile boolean mCompleted = false;
//请求中发生异常。
private Exception mException;
private Rsponse mResponse;
private Listener mListener;
public Token() {
}
//该令牌的唯一标识符
public long setId(long id) {
mId = id;
}
//该令牌的唯一标识符
public long getId() {
return mId;
}
//等待请求返回,中途可能发生Exception,所以我们需要捕获对应的异常
public void waitForCompletion() throws Exception {
waitForCompletion(-1);
}
public void waitForCompletion(long timeout) throws Exception {
Rsponse resp = waitForResponse(timeout);
//构建超时错误
if (resp == null && !mCompleted) {
mException = new Exception(REASON_CODE_CLIENT_TIMEOUT);
}
}
//等待请求返回,指定等待时长。
private Response waitForResponse(long timeout) throws Exception{
synchronized (mResponseLock) {
while (!this.mCompleted) {
if (this.mException == null) {
try {
if (timeout <= 0) {
mResponseLock.wait();
} else {
mResponseLock.wait(timeout);
}
} catch (InterruptedException e) {
mException = e;
}
}
//发生异常
if (!this.mCompleted) {
if (this.mException != null) {
throw mException;
}
}
//超时,则退出等待。
if (timeout > 0) {
break;
}
}
}
return mResponse;
}
//获取当前请求服务器返回对象。简单起见,这里就不定义具体的Response对象了,因为每个业务不一样。
public Response getResponse() {
return mResponse;
}
//设置listener
public void setListener(Listener listener) {
mListener = listener;
}
//因为标记请求完成 之后,不一定会立即发出通知
public void markComplete(Response r, Exception e) {
synchronized (mResponseLock) {
mResponse = msg;
mException = ex;
}
}
//唤醒业务等待线程。
public void notifyComplete() {
synchronized (mResponseLock) {
mCompleted = true;
//唤醒请求等待阻塞线程。
mResponseLock.notifyAll();
}
}
}
我们定义好Token之后, 我们再回过头来看看消息发送过程。主要涉及到send 方法的改动, 此处我们需要定义一个Token容器类来负责统一管理所有的请求Token。
public class ClientState {
private volatile Vector<Message> mPendingMessages = new Vector();
private volatile Vector<Message> mAckMessages = new Vector();
private final Object mQueueLock = new Object();
//最多同时发送10条普通消息
private int mMaxInflight = 10;
//当前正在发送的消息量级
private int mActualInFlight = 0;
//可以把id生成策略独立出来,这里我只是起到一个说明,直接通过msg获取id,其实是在构造msg的时候生成的,这里省略了
public Token send(Message msg, Listener listener) {
Token token = new Token();
token..setListener(listener);
token.setId(msg.getId());
send(msg, token);
}
private void send(Message msg, Token token) {
synchronized (mQueueLock) {
if (如果是回执信息) {
mAckMessages.addElement(message);
mQueueLock.notifyAll();
} else {
//将token 加入到TokenStore 中。Token管理业务是内聚在框架内部的,不需要对外。
mTokenStore.saveToken(token, message);
mPendingMessages.addElement(message);
mQueueLock.notifyAll();
}
}
}
}
定义TokenStore
为什么要定义TokenStore, 因为客户端可以同时发起多个请求,若是没有一个TokenStore 统一管理所有的请求,那谁能区分哪条消息是谁发送的呢?该TokenStore 对象主要负责管理所有的Token信息。
我们再来看如何实现一个简易版的 TokenStore,具体如下: 比较简单,定义一个Hashtable 来缓存所有的Token信息。
public class TokenStore {
private final Hashtable<String, MqttToken> mTokens = Hashtable<>();
//存储Token
public void saveToken(Token token, Message message) {
synchronized (mTokens) {
String key = message.getId();
mTokens.put(key, token);
}
}
//通过message对象获取返回的token,注意这里需要服务器配合原样的将消息id 给返回回来,不然这个消息的key就变了,会导致Token获取不到
public Token getToken(Message message) {
synchronized (mTokens) {
return mTokens.get(key);
}
}
public removeToken(Message message) {
synchronized (mTokens) {
return mTokens.remove(message.getId());
}
}
}
这样一来我们的整个发送体系就完成了。当发起请求时我们向TokenStore 中添加Token信息,在收到服务器回执的时候我们从TokenStore 中取出Token信息。
分发消息模块
当收到消息或者回执后,这里我们统一以收到客户端请求回执来处理。 首先分发模块至少要满足下面几点:
-
不能阻塞发送线程
-
不能阻塞接收消息线程
-
如果收到服务器主动下发的消息需要发起回执
-
如果收到客户端发起请求的回执,则需要通知上层业务。
如果要满足不阻塞发送线程和接收线程,那分发消息就必须异步去执行,但是我们也要考虑到回执和通知回调的时序,这时我们就只能新开一个线程单独执行分发消息任务了。
创建消息分发线程
首先我们在发起建连时就可以新建一个分发消息线程。具体如下:
public void connect(NetWork netWork, ConnectOption option) {
//构建异步线程并启动
new ConnectBG().start();
}
class ConnectBG implements Runnable {
Thread mBg = null;
ConnectBG() {
mBg = new Thread(this, "connect");
}
public void start() {
mBg.start();
}
public void run() {
//建连前已经准备好了, 直接连接即可。
mNetwork.start();
//构造消息接收对象。
Receiver receiver = new Receiver(InputStream inputStream);
receiver.start();
//发送消息
Sender sender = new Sender(OutputStream outputStream);
sender.start();
//消息分发
Callback callback = new Callback();
callback.start();
}
}
class Callback implements Runnable {
//客户端主动请求完成队列
private final Vector mCompleteQueue = new Vector(10);
//收到服务器消息
private final Vector mMessageQueue = new Vector(10);
//线程生命周期锁
private final Object mLifecycle = new Object();
//线程锁,读取消息列表操作锁。
private final Object mWorkAvailable = new Object();
public boolean mRunning = false;
private Thread mCallbackThread;
public void start(String threadName) {
synchronized (mLifecycle) {
if (!mRunning) {
mMessageQueue.clear();
mCompleteQueue.clear();
mRunning = true;
mCallbackThread = new Thread(this, threadName);
mCallbackThread.start();
}
}
}
public void stop() {
synchronized (mLifecycle) {
if (mRunning) {
mRunning = false;
if (!Thread.currentThread().equals(mCallbackThread)) {
try {
synchronized (mWorkAvailable) {
mWorkAvailable.notifyAll();
}
mCallbackThread.join();
} catach (Exception e) {
}
}
}
}
mCallbackThread = null;
}
public void run() {
while (mRunning) {
try {
try {
synchronized (mWorkAvailable) {
if (mRunning && mMessageQueue.isEmpty()
&& mCompleteQueue.isEmpty()) {
mWorkAvailable.wait();
}
}
} catch (InterruptedException e) {
}
if (mRunning) {
MqttToken token = null;
synchronized (mCompleteQueue) {
if (!mCompleteQueue.isEmpty()) {
token = mCompleteQueue.elementAt(0);
mCompleteQueue.removeElementAt(0);
}
}
//优先处理第一条客户端请求回执
if (null != token) {
handleComplete(token);
}
Message message = null;
synchronized (mMessageQueue) {
if (!mMessageQueue.isEmpty()) {
message = mMessageQueue.elementAt(0);
mMessageQueue.removeElementAt(0);
}
}
//再处理第一条收到的消息
if (null != message) {
handleMessage(message);
}
}// if (mRunning)
} catch (Throwable ex) {
mRunning = false;
//停止长连接
}
}
}
//发送完成回调处理
private void handleComplete(Token token) throws Exception {
synchronized (token) {
token.notifyComplete();
//通知上层业务
if (!token.isNotified()) {
if (token.getListener() != null) {
if (token.getException() != null) {
token.getListener().onSucess(token);
} else {
token.getListener().onFailure(token, token.getException());
}
}
}
token.setNotified(true);
//发送队列数减一,移除token缓存,继续唤醒发送线程。
if (token.isComplete()) {
ClientState.notifyComplete(token);
}
}
}
private void handleMessage(Message msg) {
//这里处理业务消息, 建议一定要异步去处理,不然会阻塞其他消息的回执性能。
}
//通知收到回调了
public void asyncComplete(MqttToken token) {
if (mRunning) {
mCompleteQueue.addElement(token); // vector 本身也是线程安全的。
synchronized (mWorkAvailable) {
mWorkAvailable.notifyAll();
}
} else {
handleComplete(token);
}
}
//通知收到消息了
public void messageArrived(MqttPublish sendMessage) {
mMessageQueue.addElement(sendMessage);
synchronized (mWorkAvailable) {
mWorkAvailable.notifyAll();
}
}
}
我们首先定义了2个消息队列,一个是客户端主动请求的回执队列,另一个是服务器主动下发的消息队列。每次唤醒分发线程后都会对两个队列的首个元素进行处理。并且我们提供了收到消息和回执的通知处理方法,供Receiver去调用。
通知有消息到达
我们回过头来再看receiver 类,。注意,这里只是简单写了必要的回调处理而已,实际业务要复杂的多,也不是可以这么直接调用的。
class Receiver implements Runnable {
public void run() {
while (mRunning && mMyInputStream != null ) {
try {
Message msg = mMyInputStream.readMessage();
if (如果是回执) {
Token token = TokenStore.getToken(msg.getId());
//通知上层业务请求已经响应。
callback.asyncComplete(token);
} else {
//不是回执信息,则通知分发业务收到了一条新消息
callback.messageArrived(msg);
}
} catch (Exception e) {
mRunning = false;
//断开长连接
} catch (IOException ioe) {
mRunning = false;
if (当前不是断开中状态) {
//断开长连接
}
}
}
}
}
至此,我们从发送,到收到回执,到回调等链路就完成闭环了。
优美的关闭长连接
关闭长连接的必要步骤如下:
-
先发起断连业务请求,让服务器不要再发消息了,同时也释放相关资源(若是发生NAT超时,此时通信会失败)。
-
主动关闭socket。
-
同步更新连接状态(前面一直没说连接状态,这个也是很重要的一环)。
-
同时设置关闭中状态,不再收消息也不再发送消息。主要解决停止过程遇到发送消息的情况。
-
关闭Sender线程。
-
关闭Receiver线程。
-
关闭callback线程。
-
通知回调业务层长连接断开了。
ClientComms 中应该包含下面的断连方法。
public void shutDown(Token token, final Exception e) {
//1. 修改长连接状态
//2. 停止消息分发线程
mCallback.stop();
//3. 关闭Socket
netWork.stop();
//4. 关闭收消息线程
mReceiver.stop();
//5. 处理正在处理中的所有Tokens。
//6. 关闭发送线程
mSender.stop();
//7. 回调长连接断开
mCallback.connectionLost(reason);
}
总结
最后做下总结,回顾下本文的内容
- Socket的建连设计。通过简单的接口封装将不同类型的连接建立解耦开来。
- ClientCommons介绍。主要介绍了异步建连,发送和接收线程设计。
- ClientState介绍。主要介绍了消息发送,接收,回执,以及关闭长连接等内容。
最后本文只是起到一个抛砖引玉的作用,欢迎大家评论留言讨论。