美文网首页
跟我一起开发商业级IM(2)—— 接口定义及封装

跟我一起开发商业级IM(2)—— 接口定义及封装

作者: FreddyChen | 来源:发表于2020-08-27 11:39 被阅读0次
    image

    写在前面

    在上一篇文章跟我一起开发商业级IM(1)—— 技术选型及协议定义中,我们完成了技术选型,回顾一下:

    通信协议
    • TCP
    • WebSocket
    传输协议
    • Protobuf
    • Json
    通信框架
    • Netty

    接下来,我们基于上述的协议与框架,分别来实现Android客户端Java服务端的接口定义及封装,在这个阶段,只需要定义接口及适当封装即可,暂不需要具体实现。

    由于篇幅原因,只能贴出核心部分的代码。在后续的文章中,也是以文字+部分核心代码的方式讲解,如果需要完整代码,请移步Github

    贴个Kula高清图镇楼:

    Kula

    本文只讲述接口的定义及封装,至于实现会在后续的文章中会分篇讲解。

    分析一下,我们的IM Service(下文简称IMS)应该有如下接口:

    • 初始化
    • 连接
    • 重连
    • 断开连接
    • 发送消息
    • 释放资源

    那我们来开始封装吧。

    接口定义

    这一步比较简单,先定义一个IMSInterface,在其中编写一些接口方法,然后分别实现NettyTCPIMSNettyWebSocketIMS

    /**
     * @author FreddyChen
     * @name IMS抽象接口,不同的客户端协议实现此接口即可
     */
    public interface IMSInterface {
    }
    
    /**
     * @author FreddyChen
     * @name Netty TCP IM Service,基于Netty实现的TCP协议客户端
     */
    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实现的WebSocket协议客户端
     */
    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();
        }
    }
    

    如上,接口定义完成,接下来我们来分别定义具体的方法(方法实现在后续文章会讲解)。

    初始化

    一款优秀的SDK应该具备可配置、易扩展等特性,分析一下,我们不难发现IMS应该需要支持大量的参数配置,比如:

    • 通信协议(TCP/WebSocket)
    • 传输协议(Protobuf/Json)
    • 连接超时时间
    • 重连间隔时间
    • 服务器地址
    • 心跳前后台间隔时间
    • 是否自动重发消息
    • 消息最大重发次数
    • 消息重发间隔时间

    等,以上参数都不应该在IMS内部固定,IMS可以提供默认值,同时支持应用层(调用方)去配置。可见支持配置的参数非常多,如果都单独作为参数传递过来,那可读性会非常差,这种情况我们可以利用“Builder模式(构建者模式,也可称为建造者模式)”来优化一下,所以初始化的接口方法可以定义为:

    /**
     * 初始化
     *
     * @param context
     * @param options               IMS初始化配置
     * @param connectStatusListener IMS连接状态监听
     * @param msgReceivedListener   IMS消息接收监听
     */
    void init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener);
    
    /**
     * @author FreddyChen
     * @name IMS初始化配置项
     */
    public class IMSOptions {
    
        private CommunicationProtocol communicationProtocol;// 通信协议
        private TransportProtocol transportProtocol;// 传输协议
        private int connectTimeout;// 连接超时时间,单位:毫秒
        private int reconnectInterval;// 重连间隔时间,单位:毫秒
        private int reconnectCount;// 单个地址一个周期最大重连次数
        private int foregroundHeartbeatInterval;// 应用在前台时心跳间隔时间,单位:毫秒
        private int backgroundHeartbeatInterval;// 应用在后台时心跳间隔时间,单位:毫秒
        private boolean autoResend;// 是否自动重发消息
        private int resendInterval;// 自动重发间隔时间,单位:毫秒
        private int resendCount;// 消息最大重发次数
        private List<String> serverList;// 服务器地址列表
    
        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;// 通信协议
            private TransportProtocol transportProtocol;// 传输协议
            private int connectTimeout;// 连接超时时间,单位:毫秒
            private int reconnectInterval;// 重连间隔时间,单位:毫秒
            private int reconnectCount;// 单个地址一个周期最大重连次数
            private int foregroundHeartbeatInterval;// 应用在前台时心跳间隔时间,单位:毫秒
            private int backgroundHeartbeatInterval;// 应用在后台时心跳间隔时间,单位:毫秒
            private boolean autoResend;// 是否自动重发消息
            private int resendInterval;// 自动重发间隔时间,单位:毫秒
            private int resendCount;// 消息最大重发次数
            private List<String> serverList;// 服务器地址列表
    
            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;// 单个地址一个周期最大重连次数
        public static final int FOREGROUND_HEARTBEAT_INTERVAL = 8 * 1000;// 应用在前台时心跳间隔时间,单位:毫秒
        public static final int BACKGROUND_HEARTBEAT_INTERVAL = 30 * 1000;// 应用在后台时心跳间隔时间,单位:毫秒
        public static final boolean AUTO_RESEND = true;// 是否自动重发消息
        public static final int RESEND_INTERVAL = 3 * 1000;// 自动重发间隔时间,单位:毫秒
    }
    
    /**
     * @author FreddyChen
     * @name 通讯协议
     */
    public enum CommunicationProtocol {
        TCP,
        WebSocket
    }
    
    /**
     * @author FreddyChen
     * @name 传输协议
     */
    public enum TransportProtocol {
        Protobuf,
        Json
    }
    
    /**
     * @author FreddyChen
     * @name IMS连接状态监听器
     */
    public interface IMSConnectStatusListener {
        void onUnconnected();
        void onConnecting();
        void onConnected();
        void onConnectFailed();
    }
    
    /**
     * @author FreddyChen
     * @name IMS消息接收监听器
     */
    public interface IMSMsgReceivedListener {
        void onMsgReceived(IMSMsg msg);
    }
    

    其中,由于同时支持ProtobufJson传输协议,所以需要自己封装一个IMSMsg实现兼容:

    /**
     * @author FreddyChen
     * @name IMS消息,通用的消息格式定义,可转换成json或protobuf传输
     */
    public class IMSMsg {
        private String msgId;// 消息唯一标识
        private int msgType; // 消息类型
        private String sender;// 发送者标识
        private String receiver;// 接收者标识
        private long timestamp;// 消息发送时间,单位:毫秒
        private int report;// 消息发送状态报告
        private String content;// 消息内容
        private int contentType;// 消息内容类型
        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;// 消息唯一标识
            private int msgType; // 消息类型
            private String sender;// 发送者标识
            private String receiver;// 接收者标识
            private long timestamp;// 消息发送时间,单位:毫秒
            private int report;// 消息发送状态报告
            private String content;// 消息内容
            private int contentType;// 消息内容类型
            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();
    

    首次连接也可认为是重连,所以调用connect()方法的时候,可以直接调用reconnect(true)。

    重连

    /**
     * 重连
     *
     * @param isFirstConnect 是否首次连接
     */
    void reconnect(boolean isFirstConnect);
    

    重连时,根据isFirstConnect参数判断是否为首次连接,如果是首次连接,直接去连接即可,否则可以进行延时一段时间再去连接(因为如果为非首次连接的重连,意味着上一次连接失败,有可能是网络环境不好,延时一段时间再去连接,有利于在下一次连接时提升成功率并且避免太频繁去进行连接,节约资源)。

    断开连接

    /**
     * 断开连接
     */
    void disconnect();
    

    断开连接只是把长连接断开,并不释放资源。在下次进行连接的时候,无需重新调用init()方法初始化。

    发送消息

    发送消息,提供多种方式,一种是直接发送,不关注消息发送状态。另一种是加入消息发送状态监听器,方便应用层感知。另外,支持加入消息重发定时器,如果加入,则消息在发送超时后,会自动重发指定的最大重发次数,超时次数达到最大重发次数时,才认为消息发送失败:

    /**
     * 发送消息
     *
     * @param msg
     */
    void sendMsg(IMSMsg msg);
    
    /**
     * 发送消息
     * 重载
     *
     * @param msg
     * @param listener 消息发送状态监听器
     */
    void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener);
    
    /**
     * 发送消息
     * 重载
     *
     * @param msg
     * @param isJoinResendManager 是否加入消息重发管理器
     */
    void sendMsg(IMSMsg msg, boolean isJoinResendManager);
    
    /**
     * 发送消息
     * 重载
     *
     * @param msg
     * @param listener 消息发送状态监听器
     * @param isJoinResendManager 是否加入消息重发管理器
     */
    void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener, boolean isJoinResendManager);
    

    消息发送状态监听器定义如下:

    /**
     * @author FreddyChen
     * @name IMS消息发送状态监听器
     */
    public interface IMSMsgSentStatusListener {
    
        /**
         * 消息发送成功
         */
        void onSendSucceed(IMSMsg msg);
    
        /**
         * 消息发送失败
         */
        void onSendFailed(IMSMsg msg, String errMsg);
    }
    

    注:消息发送成功是指客户端A发送的消息已到达服务端并且收到服务端的回执,并不一定已到达另一个客户端B。对于客户端A来说,消息只要到达服务端,即可认为消息发送成功。

    释放资源

    /**
     * 释放资源
     */
    void release();
    

    释放资源代表断开长连接并释放所有资源,在下次需要进行连接的时候,需要重新调用init()初始化再进行连接。

    贴上最终的IMSInterface

    /**
     * @author FreddyChen
     * @name IMS抽象接口
     * @desc 不同的客户端协议实现此接口即可:
     */
    public interface IMSInterface {
    
        /**
         * 初始化
         *
         * @param context
         * @param options               IMS初始化配置
         * @param connectStatusListener IMS连接状态监听
         * @param msgReceivedListener   IMS消息接收监听
         */
        IMSInterface init(Context context, IMSOptions options, IMSConnectStatusListener connectStatusListener, IMSMsgReceivedListener msgReceivedListener);
    
        /**
         * 连接
         */
        void connect();
    
        /**
         * 重连
         *
         * @param isFirstConnect 是否首次连接
         */
        void reconnect(boolean isFirstConnect);
    
        /**
         * 断开连接
         */
        void disconnect();
    
        /**
         * 发送消息
         *
         * @param msg
         */
        void sendMsg(IMSMsg msg);
    
        /**
         * 发送消息
         * 重载
         *
         * @param msg
         * @param listener 消息发送状态监听器
         */
        void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener);
    
        /**
         * 发送消息
         * 重载
         *
         * @param msg
         * @param isJoinResendManager 是否加入消息重发管理器
         */
        void sendMsg(IMSMsg msg, boolean isJoinResendManager);
    
        /**
         * 发送消息
         * 重载
         *
         * @param msg
         * @param listener 消息发送状态监听器
         * @param isJoinResendManager 是否加入消息重发管理器
         */
        void sendMsg(IMSMsg msg, IMSMsgSentStatusListener listener, boolean isJoinResendManager);
    
        /**
         * 释放资源
         */
        void release();
    }
    

    然后分别实现NettyTCPIMSNettyWebSocket即可,至于具体实现,在后续文章会分篇讲解。

    Java服务端代码

    Java服务端代码基本上大同小异,考虑到篇幅等原因,代码就不贴了,已上传到kulachat-server,有需要的同学可以跳转Github查看。

    贴一下Java服务端的代码结构吧:

    服务端代码结构

    注意:Java服务端的msg.proto,我这边是直接复制之前在Android客户端编写的文件,并且建议大家尽量保持protobuf的版本一致,否则可能会有协议兼容性的问题。

    写在最后

    到此为止,我们已经把IMS的基础接口定义完毕,后续在接口定义的基础上实现即可。由于是边写文章边写项目,所以难免有考虑不周的地方,希望大家能给我指出来,共同完善。

    在下一篇文章中,我将会讲解连接及重连部分,详细分析什么情况下该重连、怎么去执行重连的逻辑等,长连接稳定是我们关注的重点,只有长连接稳定了,才能继续开发其它功能。

    由于边写文章边写项目实在太耗精力,同时要兼顾Android客户端和Java服务端,平时工作也忙,所以进度会稍慢,大概十天一篇文章那样,所以大家不要着急,很多东西学透了,才是自己的。

    PS:新开的公众号不能留言,如果大家有不同的意见或建议,可以到掘金上评论或者加到QQ群:1015178804,如果群满人的话,也可以在公众号给我私信,谢谢。

    贴上公众号:
    FreddyChen

    FreddyChen的微信公众号

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

    相关文章

      网友评论

          本文标题:跟我一起开发商业级IM(2)—— 接口定义及封装

          本文链接:https://www.haomeiwen.com/subject/tdrcsktx.html