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

3,037

欢迎转载,转载请注明出处:juejin.cn/post/685844…

写在前面

在上一篇文章跟我一起开发商业级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的微信公众号

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