XMPP协议(Extensible Messaging and PresenceProtocol,可扩展消息处理现场协议)是一种基于XML的协议,目的是为了解决及时通信标准而提出来的,最早是在Jabber上实现的。Android_IM 中sdk多采用ASmack实现,第三方IM通信SDK环信号称可以支持千万级用户,就是采用ASmack作为基础实现的。
一、服务器端的设计方案。
服务器在Openfire的基础上做插件开发。按客户端和服务器数据库数据交互的路径可分为三类(见下图):
1.通过自己写的插件直接操作数据库。
2.通过自己写的插件调用openfire的接口完成数据的交互。
3.直接通过openfire的接口完成数据的交互。
二、客户端服务器交互方式
客户端获取数据的方式有两种,一种是推送,一种是拉取。在我们的应用设计中对应Message和IQ。
IQ指info/Query。是一问一答的机制,当发送一个set或者get类型的IQ到服务器,服务器必须返回一个result或者error类型的IQ给客户端。
使用http进行数据交互的时候,可以通过地址和参数来区分不同的请求。长连接的数据交互都是以包为单位,区分不同的请求是通过包信息识别的。对应xmpp的设计中就是IQ的命名空间和其拓展数据。按xmpp的规则自定义IQ的话略显复杂,所以我们采用了填充json的方式来扩展数据。比如我们通过IQ发送一条微博:
[html] view plain copy print?<iq from='zhangsan@example.com' type='set'>
<data xmlns='com.example.test.weibo' action="add"/>
{"content":"今天天气不错",
"time":"2014-11-28 14:00:01",
"images":["/image/test1.png",
"/image/test2.png",
"/image/test3.png"
]
}
</data>
</iq>
三、IM软件要解决的几个基本问题
1.消息类
.不同类型的消息:图片、声音、文字等。离线消息
2.联系人(好友)
联系人的常规操作,添加、删除、分组
3.连接管理
包括数据包的监听,连接的创建、断开、重连
4.其他
数据推送,多终端数据同步
XMPP的所有数据包都是xml格式的数据,并且有自己的拓展规则。但不得不说的是xml格式的数据传输效率比较低,因为存在大量标签,所以数据包里的有效数据比例降低。特别是在移动网络的情况下,这种格式的数据包非常的浪费流量。我们采用的包结构式xml+json。一来提高了数据传输效率,二来一下子从繁琐的xmpp拓展协议解脱出来,换成了喜闻乐见的json。例如:
文本类型的消息:
[html] view plain copy print?<message
to='zhangsan@example.com'
from='lisi@example.com/android1.0'
type='chat'>
<body>
{"type": "text",
"data": "今天天气不错"
}
</body>
</message>
图片类型的消息:
[html] view plain copy print?<message
to='zhangsan@example.com'
from='lisi@example.com/android1.0'
type='chat'>
<body>
{"type":"image",
"data":{"url":"/image/test.png",
"thumbUrl":"/image/thumb/test.png",
"width":150,
"height":120
}
}
</body>
</message>
openfire已经完成了个人消息的离线处理。服务器会在你登陆后,将所有的离线消息推送给你,你在登陆后监听来自服务器的所有消息包即可。严格讲openfire现在的群聊应该叫做多人聊天室,openfire会自动记录群内的所有聊天内容并存储(数据库表ofmucconversationlog)。你需要做的是在每次退出登录时,记录一个时间,下次登录的时候 将聊天记录表中这个时间之后的所有消息推送给该用户(这里要注意,openfire的多人聊天记录,不会被立刻写入数据库,openfire会把记录写入缓存,待到达一定条数或者一定时间过后一起写入数据库。针对这个情况,我们现阶段用了个不好的方法,就是不使用缓存)。
在xmpp的设计中好友关系分为3种:双向好友,单向好友,没有关系。在好友关系变更的时候会有两项数据会改变,一个是花名册(roster),一个是订阅关系,两者是同事变更的。花名册说白了就是你的好友列表,而订阅关系就是指是否接收对方的在线状态等(比如好友上线了你是否能知道)。
连接管理
因为移动设备网络环境的不稳定性,连接的处理显的尤为重要。asmack中ConnectionListener定义了连接的各种事件,可以通过XMPPConnection的addConnectionListener方法添加连接监听。asmack也有个自动重连的类(ReconnectionManager),但是单纯的在配置中设置是无效的config.setReconnectionAllowed(true);这个类构造方法私有化了,可以通过反射机制去加载这个类使其生效Class.forName("org.jivesoftware.smack.ReconnectionManager");除此之外,长连接通常还设置心跳包,asmack也有对应的实现。如果有客户端发送ping包的话PingManager.getInstanceFor(conn).setPingInterval(20);(这里的单位是s...刚开始还以为是ms,困惑好久),添加ping的同时还可以添加ping失败的监听(registerPingFailedListener),来处理ping失败后的事项。尽管这些已经能保持连接的可靠性,但仍有问题的存在。在连接断开和知道连接断开这段时间内的Message包会丢失,这时候就要引入一个回执机制来确保发送的所有包都已到达,对应的xmpp协议XEP-0184。asmack中对应的实现类为DeliveryReceiptManager。
多终端、版本兼容问题
应用上线一段时间后,可能会有多个版本在流通,甚至是多平台多版本。比如Android1.5,Android2.0,IOS1.0等等。如果一开始设计没有很好地考虑这个问题,后期必将是个坑。xmpp在这方面已经有相应的解决方案。在用户登录的时候有个resource的参数,在这里我们可以传入当前应用的版本及平台信息。比如上面的例子,我们想要废弃Android2.0之前的版本,可以在登录的时候是否是Android平台版本号是否<2.0 。在多版本通信的时候如果数据结构不一致,高版本需要兼容低版本。从Message的from参数中可以看到消息来自哪个平台哪个版本的终端。
推送
对于IM软件来讲推送就变得非常简单了。其实就是发消息,根据不同需求发不同消息就可以了。项目中我把推送内容分为两类:一类是通知,服务器可以通过Message的from参数来控制消息发向哪个会话。一类是同步数据,目的就是为了保证服务器和客户端的数据一致性。
为了保证客户端和服务器数据的一致性通常有两种方案。
方案一,本地应用不保存数据,每次从服务器获取全量的数据。
方案二,本地保存数据,每次获取增量数据,保证与服务器的数据同步。
IM这种应用第二种方案最为适合。特别是移动应用,每次拉取全量的数据不仅浪费流量,而且应用也无法离线使用。
这里说说我对这种增量数据同步的一些想法。为了保证数据同步的实时性,如果应用离线,数据同步操作在登陆成功到进入应用这段时间完成,如果应用在线,应用后台服务器应直接处理。关于这个数据同步还有一些问题没有完美的解决方案。像这种增量的数据操作,错误都会累积,如果上次的数据没有能正确处理,可能导致下次数据同步时出错。我想了想大概有这么两种可能。一是丢包,从服务器推送的同步数据丢失,导致客户端的数据并没有和服务器保持一致。针对这种情况,可以设计一个反馈机制。如果客户端收到数据包的话,给服务器一个再发一个数据包,告知服务器数据已收到。二是本地数据处理异常。由于一些未知的问题导致数据未能正确存储到本地。数据的不一致可能导致应用发生一些不可预知的错误,这时候最好是强制关闭应用,同时保持一个数据同步失败的状态到本地,下次启动的时候检查到上次同步失败的状态的时候,向服务器请求全量的数据并更新到本地数据库。
应用场景,比如自己所在的群有个成员被删除了,这个时候需要更新群成员列表。
应用的启动流程
如上图,应用启动要处理的事情包括:身份验证,数据库、监听等的初始化,离线数据处理、同步数据处理,进入应用。
登陆之前要先建立匿名连接,然后在进行身份验证。
身份验证成功后,要做相应的初始化操作。如监听的初始化,数据库的初始化,用户配置的初始化。监听初始化,是为了下一步监听服务器发来的数据包。(之所以初始化在登陆成功后进行,是因为在登陆成功前你不知道要使用哪个配置文件、哪个数据库,比如一个手机切换多个账号登陆)
在初始化完成,这时候客户端已经准备就绪,发送一个presence包通知服务器可以接收数据了,服务器开始向客户端推送数据,推送的数据包括离线消息,同步数据。处理这些数据(通常是写入数据库)。完成这些就可以进入应用了。