写在前面
在上一篇文章跟我一起开发商业级IM(1)—— 技术选型及协议定义中,我们完成了技术选型,回顾一下:
通信协议
- TCP
- WebSocket
传输协议
- Protobuf
- Json
通信框架
- Netty
接下来,我们基于上述的协议与框架,分别来实现Android客户端
与Java服务端
的接口定义及封装,在这个阶段,只需要定义接口及适当封装即可,暂不需要具体实现。
由于篇幅原因,只能贴出核心部分的代码。在后续的文章中,也是以文字+部分核心代码的方式讲解,如果需要完整代码,请移步Github。
贴个Kula高清图镇楼:
本文只讲述接口的定义及封装,至于实现会在后续的文章中会分篇讲解。
分析一下,我们的IM Service(下文简称IMS)应该有如下接口:
- 初始化
- 连接
- 重连
- 断开连接
- 发送消息
- 释放资源
那我们来开始封装吧。
接口定义
这一步比较简单,先定义一个IMSInterface
,在其中编写一些接口方法,然后分别实现NettyTCPIMS
和NettyWebSocketIMS
。
/**
* @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);
}
其中,由于同时支持Protobuf和Json传输协议,所以需要自己封装一个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();
}
然后分别实现NettyTCPIMS
和NettyWebSocket
即可,至于具体实现,在后续文章会分篇讲解。
Java服务端代码
Java服务端代码基本上大同小异,考虑到篇幅等原因,代码就不贴了,已上传到kulachat-server,有需要的同学可以跳转Github查看。
贴一下Java服务端的代码结构吧:
注意:Java服务端的msg.proto,我这边是直接复制之前在Android客户端编写的文件,并且建议大家尽量保持protobuf的版本一致,否则可能会有协议兼容性的问题。
写在最后
到此为止,我们已经把IMS的基础接口定义完毕,后续在接口定义的基础上实现即可。由于是边写文章边写项目,所以难免有考虑不周的地方,希望大家能给我指出来,共同完善。
在下一篇文章中,我将会讲解连接及重连部分,详细分析什么情况下该重连、怎么去执行重连的逻辑等,长连接稳定是我们关注的重点,只有长连接稳定了,才能继续开发其它功能。
由于边写文章边写项目实在太耗精力,同时要兼顾Android客户端和Java服务端,平时工作也忙,所以进度会稍慢,大概十天一篇文章那样,所以大家不要着急,很多东西学透了,才是自己的。
PS:新开的公众号不能留言,如果大家有不同的意见或建议,可以到掘金上评论或者加到QQ群:1015178804,如果群满人的话,也可以在公众号给我私信,谢谢。
贴上公众号:
FreddyChen
下篇文章见,古德拜~ ~ ~
网友评论