跟我一起開發(fā)商業(yè)級IM(2)—— 接口定義及封裝

image

寫在前面

在上一篇文章跟我一起開發(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)樓:

Kula

本文只講述接口的定義及封裝,至于實現(xiàn)會在后續(xù)的文章中會分篇講解。

分析一下,我們的IM Service(下文簡稱IMS)應(yīng)該有如下接口:

  • 初始化
  • 連接
  • 重連
  • 斷開連接
  • 發(fā)送消息
  • 釋放資源

那我們來開始封裝吧。

接口定義

這一步比較簡單,先定義一個IMSInterface,在其中編寫一些接口方法,然后分別實現(xiàn)NettyTCPIMSNettyWebSocketIMS。

/**
 * @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);
}

其中,由于同時支持ProtobufJson傳輸協(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)NettyTCPIMSNettyWebSocket即可,至于具體實現(xiàn),在后續(xù)文章會分篇講解。

Java服務(wù)端代碼

Java服務(wù)端代碼基本上大同小異,考慮到篇幅等原因,代碼就不貼了,已上傳到kulachat-server,有需要的同學(xué)可以跳轉(zhuǎn)Github查看。

貼一下Java服務(wù)端的代碼結(jié)構(gòu)吧:

服務(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

FreddyChen的微信公眾號

下篇文章見,古德拜~ ~ ~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容