
寫在前面
在上一篇文章跟我一起開發(fā)商業(yè)級IM(1)—— 技術(shù)選型及協(xié)議定義中,我們完成了技術(shù)選型,回顧一下:
通信協(xié)議
- TCP
- WebSocket
傳輸協(xié)議
- Protobuf
- Json
通信框架
- Netty
接下來,我們基于上述的協(xié)議與框架,分別來實現(xiàn)Android客戶端與Java服務(wù)端的接口定義及封裝,在這個階段,只需要定義接口及適當(dāng)封裝即可,暫不需要具體實現(xiàn)。
由于篇幅原因,只能貼出核心部分的代碼。在后續(xù)的文章中,也是以文字+部分核心代碼的方式講解,如果需要完整代碼,請移步Github。
貼個Kula高清圖鎮(zhèn)樓:

本文只講述接口的定義及封裝,至于實現(xiàn)會在后續(xù)的文章中會分篇講解。
分析一下,我們的IM Service(下文簡稱IMS)應(yīng)該有如下接口:
- 初始化
- 連接
- 重連
- 斷開連接
- 發(fā)送消息
- 釋放資源
那我們來開始封裝吧。
接口定義
這一步比較簡單,先定義一個IMSInterface,在其中編寫一些接口方法,然后分別實現(xiàn)NettyTCPIMS和NettyWebSocketIMS。
/**
* @author FreddyChen
* @name IMS抽象接口,不同的客戶端協(xié)議實現(xiàn)此接口即可
*/
public interface IMSInterface {
}
/**
* @author FreddyChen
* @name Netty TCP IM Service,基于Netty實現(xiàn)的TCP協(xié)議客戶端
*/
public class NettyTCPIMS implements IMSInterface {
private NettyTCPIMS() { }
public static NettyTCPIMS getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final NettyTCPIMS INSTANCE = new NettyTCPIMS();
}
}
/**
* @author FreddyChen
* @name Netty WebSocket IM Service,基于Netty實現(xiàn)的WebSocket協(xié)議客戶端
*/
public class NettyWebSocketIMS implements IMSInterface {
private NettyWebSocketIMS() { }
public static NettyWebSocketIMS getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final NettyWebSocketIMS INSTANCE = new NettyWebSocketIMS();
}
}
如上,接口定義完成,接下來我們來分別定義具體的方法(方法實現(xiàn)在后續(xù)文章會講解)。
初始化
一款優(yōu)秀的SDK應(yīng)該具備可配置、易擴展等特性,分析一下,我們不難發(fā)現(xiàn)IMS應(yīng)該需要支持大量的參數(shù)配置,比如:
- 通信協(xié)議(TCP/WebSocket)
- 傳輸協(xié)議(Protobuf/Json)
- 連接超時時間
- 重連間隔時間
- 服務(wù)器地址
- 心跳前后臺間隔時間
- 是否自動重發(fā)消息
- 消息最大重發(fā)次數(shù)
- 消息重發(fā)間隔時間
等,以上參數(shù)都不應(yīng)該在IMS內(nèi)部固定,IMS可以提供默認值,同時支持應(yīng)用層(調(diào)用方)去配置??梢娭С峙渲玫膮?shù)非常多,如果都單獨作為參數(shù)傳遞過來,那可讀性會非常差,這種情況我們可以利用“Builder模式(構(gòu)建者模式,也可稱為建造者模式)”來優(yōu)化一下,所以初始化的接口方法可以定義為:
/**
* 初始化
*
* @param context
* @param options IMS初始化配置
* @param connectStatusListener IMS連接狀態(tài)監(jiān)聽
* @param msgReceivedListener IMS消息接收監(jiān)聽
*/
void init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener);
/**
* @author FreddyChen
* @name IMS初始化配置項
*/
public class IMSOptions {
private CommunicationProtocol communicationProtocol;// 通信協(xié)議
private TransportProtocol transportProtocol;// 傳輸協(xié)議
private int connectTimeout;// 連接超時時間,單位:毫秒
private int reconnectInterval;// 重連間隔時間,單位:毫秒
private int reconnectCount;// 單個地址一個周期最大重連次數(shù)
private int foregroundHeartbeatInterval;// 應(yīng)用在前臺時心跳間隔時間,單位:毫秒
private int backgroundHeartbeatInterval;// 應(yīng)用在后臺時心跳間隔時間,單位:毫秒
private boolean autoResend;// 是否自動重發(fā)消息
private int resendInterval;// 自動重發(fā)間隔時間,單位:毫秒
private int resendCount;// 消息最大重發(fā)次數(shù)
private List<String> serverList;// 服務(wù)器地址列表
private IMSOptions(Builder builder) {
if (builder == null) return;
this.communicationProtocol = builder.communicationProtocol;
this.transportProtocol = builder.transportProtocol;
this.connectTimeout = builder.connectTimeout;
this.reconnectInterval = builder.reconnectInterval;
this.reconnectCount = builder.reconnectCount;
this.foregroundHeartbeatInterval = builder.foregroundHeartbeatInterval;
this.backgroundHeartbeatInterval = builder.backgroundHeartbeatInterval;
this.autoResend = builder.autoResend;
this.resendInterval = builder.resendInterval;
this.resendCount = builder.resendCount;
this.serverList = builder.serverList;
}
public CommunicationProtocol getCommunicationProtocol() {
return communicationProtocol;
}
public TransportProtocol getTransportProtocol() {
return transportProtocol;
}
public int getConnectTimeout() {
return connectTimeout;
}
public int getReconnectInterval() {
return reconnectInterval;
}
public int getReconnectCount() {
return reconnectCount;
}
public int getForegroundHeartbeatInterval() {
return foregroundHeartbeatInterval;
}
public int getBackgroundHeartbeatInterval() {
return backgroundHeartbeatInterval;
}
public boolean isAutoResend() {
return autoResend;
}
public int getResendInterval() {
return resendInterval;
}
public int getResendCount() {
return resendCount;
}
public List<String> getServerList() {
return serverList;
}
public static class Builder {
private CommunicationProtocol communicationProtocol;// 通信協(xié)議
private TransportProtocol transportProtocol;// 傳輸協(xié)議
private int connectTimeout;// 連接超時時間,單位:毫秒
private int reconnectInterval;// 重連間隔時間,單位:毫秒
private int reconnectCount;// 單個地址一個周期最大重連次數(shù)
private int foregroundHeartbeatInterval;// 應(yīng)用在前臺時心跳間隔時間,單位:毫秒
private int backgroundHeartbeatInterval;// 應(yīng)用在后臺時心跳間隔時間,單位:毫秒
private boolean autoResend;// 是否自動重發(fā)消息
private int resendInterval;// 自動重發(fā)間隔時間,單位:毫秒
private int resendCount;// 消息最大重發(fā)次數(shù)
private List<String> serverList;// 服務(wù)器地址列表
public Builder() {
this.connectTimeout = IMSConfig.CONNECT_TIMEOUT;
this.reconnectInterval = IMSConfig.RECONNECT_INTERVAL;
this.reconnectCount = IMSConfig.RECONNECT_COUNT;
this.foregroundHeartbeatInterval = IMSConfig.FOREGROUND_HEARTBEAT_INTERVAL;
this.backgroundHeartbeatInterval = IMSConfig.BACKGROUND_HEARTBEAT_INTERVAL;
this.autoResend = IMSConfig.AUTO_RESEND;
this.resendInterval = IMSConfig.RESEND_INTERVAL;
this.resendCount = IMSConfig.RESEND_COUNT;
}
public Builder setCommunicationProtocol(CommunicationProtocol communicationProtocol) {
this.communicationProtocol = communicationProtocol;
return this;
}
public Builder setTransportProtocol(TransportProtocol transportProtocol) {
this.transportProtocol = transportProtocol;
return this;
}
public Builder setConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
return this;
}
public Builder setReconnectInterval(int reconnectInterval) {
this.reconnectInterval = reconnectInterval;
return this;
}
public Builder setReconnectCount(int reconnectCount) {
this.reconnectCount = reconnectCount;
return this;
}
public Builder setForegroundHeartbeatInterval(int foregroundHeartbeatInterval) {
this.foregroundHeartbeatInterval = foregroundHeartbeatInterval;
return this;
}
public Builder setBackgroundHeartbeatInterval(int backgroundHeartbeatInterval) {
this.backgroundHeartbeatInterval = backgroundHeartbeatInterval;
return this;
}
public Builder setAutoResend(boolean autoResend) {
this.autoResend = autoResend;
return this;
}
public Builder setResendInterval(int resendInterval) {
this.resendInterval = resendInterval;
return this;
}
public Builder setResendCount(int resendCount) {
this.resendCount = resendCount;
return this;
}
public Builder setServerList(List<String> serverList) {
this.serverList = serverList;
return this;
}
public IMSOptions build() {
return new IMSOptions(this);
}
}
}
/**
* IMS配置
*/
public class IMSConfig {
public static final int CONNECT_TIMEOUT = 10 * 1000;// 連接超時時間,單位:毫秒
public static final int RECONNECT_INTERVAL = 8 * 1000;// 重連間隔時間,單位:毫秒
public static final int RECONNECT_COUNT = 3;// 單個地址一個周期最大重連次數(shù)
public static final int FOREGROUND_HEARTBEAT_INTERVAL = 8 * 1000;// 應(yīng)用在前臺時心跳間隔時間,單位:毫秒
public static final int BACKGROUND_HEARTBEAT_INTERVAL = 30 * 1000;// 應(yīng)用在后臺時心跳間隔時間,單位:毫秒
public static final boolean AUTO_RESEND = true;// 是否自動重發(fā)消息
public static final int RESEND_INTERVAL = 3 * 1000;// 自動重發(fā)間隔時間,單位:毫秒
}
/**
* @author FreddyChen
* @name 通訊協(xié)議
*/
public enum CommunicationProtocol {
TCP,
WebSocket
}
/**
* @author FreddyChen
* @name 傳輸協(xié)議
*/
public enum TransportProtocol {
Protobuf,
Json
}
/**
* @author FreddyChen
* @name IMS連接狀態(tài)監(jiān)聽器
*/
public interface IMSConnectStatusListener {
void onUnconnected();
void onConnecting();
void onConnected();
void onConnectFailed();
}
/**
* @author FreddyChen
* @name IMS消息接收監(jiān)聽器
*/
public interface IMSMsgReceivedListener {
void onMsgReceived(IMSMsg msg);
}
其中,由于同時支持Protobuf和Json傳輸協(xié)議,所以需要自己封裝一個IMSMsg實現(xiàn)兼容:
/**
* @author FreddyChen
* @name IMS消息,通用的消息格式定義,可轉(zhuǎn)換成json或protobuf傳輸
*/
public class IMSMsg {
private String msgId;// 消息唯一標(biāo)識
private int msgType; // 消息類型
private String sender;// 發(fā)送者標(biāo)識
private String receiver;// 接收者標(biāo)識
private long timestamp;// 消息發(fā)送時間,單位:毫秒
private int report;// 消息發(fā)送狀態(tài)報告
private String content;// 消息內(nèi)容
private int contentType;// 消息內(nèi)容類型
private String data; // 擴展字段,以key/value形式存儲的json字符串
public IMSMsg(Builder builder) {
if(builder == null) {
return;
}
this.msgId = builder.msgId;
this.msgType = builder.msgType;
this.sender = builder.sender;
this.receiver = builder.receiver;
this.timestamp = builder.timestamp;
this.report = builder.report;
this.content = builder.content;
this.contentType = builder.contentType;
this.data = builder.data;
}
public String getMsgId() {
return msgId;
}
public int getMsgType() {
return msgType;
}
public String getSender() {
return sender;
}
public String getReceiver() {
return receiver;
}
public long getTimestamp() {
return timestamp;
}
public int getReport() {
return report;
}
public String getContent() {
return content;
}
public int getContentType() {
return contentType;
}
public String getData() {
return data;
}
public static class Builder {
private String msgId;// 消息唯一標(biāo)識
private int msgType; // 消息類型
private String sender;// 發(fā)送者標(biāo)識
private String receiver;// 接收者標(biāo)識
private long timestamp;// 消息發(fā)送時間,單位:毫秒
private int report;// 消息發(fā)送狀態(tài)報告
private String content;// 消息內(nèi)容
private int contentType;// 消息內(nèi)容類型
private String data; // 擴展字段,以key/value形式存儲的json字符串
public Builder() {
this.msgId = UUID.generateShortUuid();
}
public Builder setMsgType(int msgType) {
this.msgType = msgType;
return this;
}
public Builder setSender(String sender) {
this.sender = sender;
return this;
}
public Builder setReceiver(String receiver) {
this.receiver = receiver;
return this;
}
public Builder setTimestamp(long timestamp) {
this.timestamp = timestamp;
return this;
}
public Builder setReport(int report) {
this.report = report;
return this;
}
public Builder setContent(String content) {
this.content = content;
return this;
}
public Builder setContentType(int contentType) {
this.contentType = contentType;
return this;
}
public Builder setData(String data) {
this.data = data;
return this;
}
public IMSMsg build() {
return new IMSMsg(this);
}
}
}
連接
/**
* 連接
*/
void connect();
首次連接也可認為是重連,所以調(diào)用connect()方法的時候,可以直接調(diào)用reconnect(true)。
重連
/**
* 重連
*
* @param isFirstConnect 是否首次連接
*/
void reconnect(boolean isFirstConnect);
重連時,根據(jù)isFirstConnect參數(shù)判斷是否為首次連接,如果是首次連接,直接去連接即可,否則可以進行延時一段時間再去連接(因為如果為非首次連接的重連,意味著上一次連接失敗,有可能是網(wǎng)絡(luò)環(huán)境不好,延時一段時間再去連接,有利于在下一次連接時提升成功率并且避免太頻繁去進行連接,節(jié)約資源)。
斷開連接
/**
* 斷開連接
*/
void disconnect();
斷開連接只是把長連接斷開,并不釋放資源。在下次進行連接的時候,無需重新調(diào)用init()方法初始化。
發(fā)送消息
發(fā)送消息,提供多種方式,一種是直接發(fā)送,不關(guān)注消息發(fā)送狀態(tài)。另一種是加入消息發(fā)送狀態(tài)監(jiān)聽器,方便應(yīng)用層感知。另外,支持加入消息重發(fā)定時器,如果加入,則消息在發(fā)送超時后,會自動重發(fā)指定的最大重發(fā)次數(shù),超時次數(shù)達到最大重發(fā)次數(shù)時,才認為消息發(fā)送失?。?/p>
/**
* 發(fā)送消息
*
* @param msg
*/
void sendMsg(IMSMsg msg);
/**
* 發(fā)送消息
* 重載
*
* @param msg
* @param listener 消息發(fā)送狀態(tài)監(jiān)聽器
*/
void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener);
/**
* 發(fā)送消息
* 重載
*
* @param msg
* @param isJoinResendManager 是否加入消息重發(fā)管理器
*/
void sendMsg(IMSMsg msg, boolean isJoinResendManager);
/**
* 發(fā)送消息
* 重載
*
* @param msg
* @param listener 消息發(fā)送狀態(tài)監(jiān)聽器
* @param isJoinResendManager 是否加入消息重發(fā)管理器
*/
void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener, boolean isJoinResendManager);
消息發(fā)送狀態(tài)監(jiān)聽器定義如下:
/**
* @author FreddyChen
* @name IMS消息發(fā)送狀態(tài)監(jiān)聽器
*/
public interface IMSMsgSentStatusListener {
/**
* 消息發(fā)送成功
*/
void onSendSucceed(IMSMsg msg);
/**
* 消息發(fā)送失敗
*/
void onSendFailed(IMSMsg msg, String errMsg);
}
注:消息發(fā)送成功是指客戶端A發(fā)送的消息已到達服務(wù)端并且收到服務(wù)端的回執(zhí),并不一定已到達另一個客戶端B。對于客戶端A來說,消息只要到達服務(wù)端,即可認為消息發(fā)送成功。
釋放資源
/**
* 釋放資源
*/
void release();
釋放資源代表斷開長連接并釋放所有資源,在下次需要進行連接的時候,需要重新調(diào)用init()初始化再進行連接。
貼上最終的IMSInterface:
/**
* @author FreddyChen
* @name IMS抽象接口
* @desc 不同的客戶端協(xié)議實現(xiàn)此接口即可:
*/
public interface IMSInterface {
/**
* 初始化
*
* @param context
* @param options IMS初始化配置
* @param connectStatusListener IMS連接狀態(tài)監(jiān)聽
* @param msgReceivedListener IMS消息接收監(jiān)聽
*/
IMSInterface init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener);
/**
* 連接
*/
void connect();
/**
* 重連
*
* @param isFirstConnect 是否首次連接
*/
void reconnect(boolean isFirstConnect);
/**
* 斷開連接
*/
void disconnect();
/**
* 發(fā)送消息
*
* @param msg
*/
void sendMsg(IMSMsg msg);
/**
* 發(fā)送消息
* 重載
*
* @param msg
* @param listener 消息發(fā)送狀態(tài)監(jiān)聽器
*/
void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener);
/**
* 發(fā)送消息
* 重載
*
* @param msg
* @param isJoinResendManager 是否加入消息重發(fā)管理器
*/
void sendMsg(IMSMsg msg, boolean isJoinResendManager);
/**
* 發(fā)送消息
* 重載
*
* @param msg
* @param listener 消息發(fā)送狀態(tài)監(jiān)聽器
* @param isJoinResendManager 是否加入消息重發(fā)管理器
*/
void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener, boolean isJoinResendManager);
/**
* 釋放資源
*/
void release();
}
然后分別實現(xiàn)NettyTCPIMS和NettyWebSocket即可,至于具體實現(xiàn),在后續(xù)文章會分篇講解。
Java服務(wù)端代碼
Java服務(wù)端代碼基本上大同小異,考慮到篇幅等原因,代碼就不貼了,已上傳到kulachat-server,有需要的同學(xué)可以跳轉(zhuǎn)Github查看。
貼一下Java服務(wù)端的代碼結(jié)構(gòu)吧:

注意:Java服務(wù)端的msg.proto,我這邊是直接復(fù)制之前在Android客戶端編寫的文件,并且建議大家盡量保持protobuf的版本一致,否則可能會有協(xié)議兼容性的問題。
寫在最后
到此為止,我們已經(jīng)把IMS的基礎(chǔ)接口定義完畢,后續(xù)在接口定義的基礎(chǔ)上實現(xiàn)即可。由于是邊寫文章邊寫項目,所以難免有考慮不周的地方,希望大家能給我指出來,共同完善。
在下一篇文章中,我將會講解連接及重連部分,詳細分析什么情況下該重連、怎么去執(zhí)行重連的邏輯等,長連接穩(wěn)定是我們關(guān)注的重點,只有長連接穩(wěn)定了,才能繼續(xù)開發(fā)其它功能。
由于邊寫文章邊寫項目實在太耗精力,同時要兼顧Android客戶端和Java服務(wù)端,平時工作也忙,所以進度會稍慢,大概十天一篇文章那樣,所以大家不要著急,很多東西學(xué)透了,才是自己的。
PS:新開的公眾號不能留言,如果大家有不同的意見或建議,可以到掘金上評論或者加到QQ群:1015178804,如果群滿人的話,也可以在公眾號給我私信,謝謝。
貼上公眾號:
FreddyChen
下篇文章見,古德拜~ ~ ~