废话少说,公司没事干,组长叫我写Demo,我狂晕....
先把先说说这个什么聊天室吧,服务器就别说了,典型的拿来主义。用的是成品的openfire。说道smack,这个是一个jar文件,库,是用在javae的。要想在android端使用这个,就要用到asmack包,这个可以在官网下。其实里边跟smack包没什么区别,具体差异目前我没发现。
可能有的朋友说这个smack是什么用的,我个人一句话,一个已经包装了XMPP协议的架包。提供了API的操作...好了进入正题
1,首先想进入聊天室,那必须要先知道聊天室所在的会议服务(下图,用spark截图)
- /**
- * 初始化聊服务会议列表
- */
- private void initHostRoom() {
- Collection<HostedRoom> hostrooms;
- try {
- hostrooms = MultiUserChat.getHostedRooms(Constants.conn,
- Constants.conn.getServiceName());
- for (HostedRoom entry : hostrooms) {
- roominfos.add(entry);
- Log.i(TAG, "名字:" + entry.getName() + " - ID:" + entry.getJid());
- }
- Log.i(TAG, "服务会议数量:" + roominfos.size());
- } catch (XMPPException e) {
- e.printStackTrace();
- }
- }
2,获取了那个服务的JID后才能进入会议列表:
- /**
- * 初始化房间列表
- */
- public void init() {
- listDiscoverItems = new ArrayList<DiscoverItems.Item>();
- // 获得与XMPPConnection相关的ServiceDiscoveryManager
- ServiceDiscoveryManager discoManager = ServiceDiscoveryManager
- .getInstanceFor(Constants.conn);
- // 获得指定XMPP实体的项目
- // 这个例子获得与在线目录服务相关的项目
- DiscoverItems discoItems;
- try {
- discoItems = discoManager.discoverItems(jid);
- // 获得被查询的XMPP实体的要查看的项目
- Iterator it = discoItems.getItems();
- // 显示远端XMPP实体的项目
- while (it.hasNext()) {
- DiscoverItems.Item item = (DiscoverItems.Item) it.next();
- System.out.println(item.getEntityID());
- System.out.println(item.getName());
- listDiscoverItems.add(item);
- }
- } catch (XMPPException e) {
- e.printStackTrace();
- }
- }
3,同样,要进入会议房间(聊天室)也要获取它的ID,才能进入
- // 后面服务名称必需是创建房间的那个服务
- jid = getIntent().getStringExtra("jid");
- try {
- muc = new MultiUserChat(Constants.conn, jid);
- // 创建聊天室,进入房间后的nickname(昵称)
- muc.join("痞子测试");
- Log.v(TAG, "join success");
- } catch (XMPPException e) {
- e.printStackTrace();
- }
简单吧!
上一篇呢说了怎么进入这个聊天室,这次呢,咱就说聊天室里的功能吧,聊天信息、成员变动什么的。还是少说废话,正题:
1,说要聊天呢,简单就是一个文本信息,当然我们不能时时去服务器获取信息。要充分发挥即时推送嘛。
(1)首先要添加一个监听,muc.addMessageListener(chatListener); 要是muc不知道是什么请看上一篇吧,这就不多解释。chatListener就是我们的监听器,看代码,其实下边的代码有些啰嗦。我只是懒得改了。最近有点烦躁。这里我们主要获取的就是Packet,这个是一个XMPP包装好的XML流,里边有你想要的东西。有兴趣深入的朋友可以上XMPP中文翻译组去看看,挺犀利的。
- /**
- * PacketListener 通过一个规定的过滤器提供一个机制来监听数据包
- *
- * @author liaonaibo
- *
- */
- class ChatPacketListener implements PacketListener {
- private String _number;
- private Date _lastDate;
- private MultiUserChat _muc;
- private String _roomName;
- public ChatPacketListener(MultiUserChat muc) {
- _number = "0";
- _lastDate = new Date(0);
- _muc = muc;
- _roomName = muc.getRoom();
- }
- @Override
- public void processPacket(Packet packet) {
- Message message = (Message) packet;
- String from = message.getFrom();
- if (message.getBody() != null) {
- DelayInformation inf = (DelayInformation) message.getExtension(
- "x", "jabber:x:delay");
- Date sentDate;
- if (inf != null) {
- sentDate = inf.getStamp();
- } else {
- sentDate = new Date();
- }
- Log.i(TAG, "Receive old message: date="
- + sentDate.toLocaleString() + " ; message="
- + message.getBody());
- android.os.Message msg = new android.os.Message();
- msg.what = RECEIVE;
- Bundle bd = new Bundle();
- bd.putString("from", from);
- bd.putString("body", message.getBody());
- msg.setData(bd);
- handler.sendMessage(msg);
- }
- }
- }
2,下边就是成员了,一个聊天室没成员多离谱的事情啊,我们主要做的是把成员列表逃出来。其实有几种方法可以弄出来的,我只是简单的获取成员的昵称。可能有朋友会问为什么不获取成员的信息呢。这个下一篇我会告诉你。
- /**
- * 获取聊天室的所有成员
- */
- private void getAllMember() {
- Log.i(TAG, "获取聊天室的所有成员");
- affiliates.clear();
- new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- Iterator<String> it = muc.getOccupants();
- while (it.hasNext()) {
- String name = it.next();
- name = name.substring(name.indexOf("/") + 1);
- affiliates.add(name);
- Log.i(TAG, "成员名字;" + name);
- }
- } catch (Exception e) {
- e.printStackTrace();
- }
- android.os.Message msg = new android.os.Message();
- msg.what = MEMBER;
- handler.sendMessage(msg);
- }
- }).start();
- }
这篇结束,下篇写写聊天室的一些权限的变更、成员的变更、主题变更什么的。
- <message
- from='darkcave@chat.shakespeare.lit/firstwitch'
- to='hecate@shakespeare.lit/broom'
- type='groupchat'>
- <body>Thrice the brinded cat hath mew'd.</body>
- <delay xmlns='urn:xmpp:delay'
- from='crone1@shakespeare.lit/desktop'
- stamp='2002-10-13T23:58:37Z'/>
- </message>
- <message
- from='darkcave@chat.shakespeare.lit/secondwitch'
- to='hecate@shakespeare.lit/broom'
- type='groupchat'>
- <body>Thrice and once the hedge-pig whined.</body>
- <delay xmlns='urn:xmpp:delay'
- from='wiccarocks@shakespeare.lit/laptop'
- stamp='2002-10-13T23:58:43Z'/>
- </message>
- <message
- from='darkcave@chat.shakespeare.lit/thirdwitch'
- to='hecate@shakespeare.lit/broom'
- type='groupchat'>
- <body>Harpier cries 'Tis time, 'tis time.</body>
- <delay xmlns='urn:xmpp:delay'
- from='hag66@shakespeare.lit/pda'
- stamp='2002-10-13T23:58:49Z'/>
- </message>
上边这段是一个聊天室返回的XML数据,是历史消息。就在这里要告诉大家的是,如果是新消息的话就没有<delay>节点。所以我们以这个为根据来判断。下边贴些我的代码,因为服务器被我们给改了,所以返回的有些出入。但八九不离十。原理一样。
- DelayInformation inf = (DelayInformation) message.getExtension(
- "x", "jabber:x:delay");
- System.out.println("判断消息");
- if (inf == null && count >= 1) {
- System.out.println("新消息来了");
- isHistory = true;
- } else {
- System.out.println("这是旧的消息");
- }
这段代码写哪里呢,就是写在消息的监听里头
- /**
- * PacketListener 通过一个规定的过滤器提供一个机制来监听数据包
- *
- * @author liaonaibo
- *
- */
- class ChatPacketListener implements PacketListener
- @Override
- public void processPacket(Packet packet)
好了,下边讲踢人吧
2,踢人:
踢人很简单,只要你是房主,或管理员什么的,主持人也行。这些高权限的才能有踢人的权限。
- // 踢人
- try {
- String nickName = affiliates.get(id);
- //得到的昵称包含的房间名和服务名的一段很长数据。好像是吧。哈哈,忘记了。所以截取吧
- muc.kickParticipant(nickName
- .substring(nickName.indexOf("]") + 1), "看你不爽就 踢了你");
- android.os.Message msg = new android.os.Message();
- msg.what = MEMBER;
- handler.sendMessage(msg);
- Toast.makeText(this, "哈哈,踹了你", Toast.LENGTH_LONG).show();
- } catch (XMPPException e) {
- e.printStackTrace();
- Toast.makeText(this, "你没有权利踢人", Toast.LENGTH_LONG).show();
- }
有的哥们会疑惑,说如果根据昵称来踢人,那会不会把重名的人都一起踢了。这个问题我也想过,这个也是测试的一个弊端。但是如果通过spark来看,即便你用同样的名字加入了房间,要么会报错,要么就是系统自动在你的名字后边加个2,呵呵,够2吧。如果有人坚持要用自己的昵称,这个可以的,具体怎么样做,下次再阐述。
这篇就主要讲创建房间和报文吧。
1,创建房间:
其实创建房间很简单,两句话就搞定了。但你要知道,简单的同时,服务器可没帮你配置房间的信息什么的。所以一般用下边的方法来创建房间别人是进不去的。
- // 使用XMPPConnection创建一个MultiUserChat
- MultiUserChat muc = new MultiUserChat(conn1, "myroom@conference.jabber.org");
- //创建聊天室
- muc.create("testbot");
- // 发送一个空表单配置这显示我们想要一个instant room
- muc.sendConfigurationForm(new Form(Form.TYPE_SUBMIT));
所以咱们还是手动配置一些信息吧,免得留后患。不过在这里要给朋友们提个醒,我觉得不论是哪种方法都很狗屎,为什么狗屎。你看看那MUC,本来就是一个聊天室了,还要让你在聊天室创建另个聊天室,老妈生孩子,然后孩子和老妈并排。狗屎啊,不过不排除我理解错误。希望高人能给与指点。
- try {
- // 创建聊天室
- muc.create(Constants.vCard.getNickName().toString());
- // 获得聊天室的配置表单
- Form form = muc.getConfigurationForm();
- // 根据原始表单创建一个要提交的新表单。
- Form submitForm = form.createAnswerForm();
- // 向要提交的表单添加默认答复
- for (Iterator fields = form.getFields(); fields.hasNext();) {
- FormField field = (FormField) fields.next();
- if (!FormField.TYPE_HIDDEN.equals(field.getType())
- && field.getVariable() != null) {
- // 设置默认值作为答复
- submitForm.setDefaultAnswer(field.getVariable());
- }
- }
- // 设置聊天室的新拥有者
- // List owners = new ArrayList();
- // owners.add("liaonaibo2\\40slook.cc");
- // owners.add("liaonaibo1\\40slook.cc");
- // submitForm.setAnswer("muc#roomconfig_roomowners", owners);
- // 设置聊天室是持久聊天室,即将要被保存下来
- submitForm.setAnswer("muc#roomconfig_persistentroom", true);
- // 房间仅对成员开放
- submitForm.setAnswer("muc#roomconfig_membersonly", false);
- // 允许占有者邀请其他人
- submitForm.setAnswer("muc#roomconfig_allowinvites", true);
- // 能够发现占有者真实 JID 的角色
- // submitForm.setAnswer("muc#roomconfig_whois", "anyone");
- // 登录房间对话
- submitForm.setAnswer("muc#roomconfig_enablelogging", true);
- // 仅允许注册的昵称登录
- submitForm.setAnswer("x-muc#roomconfig_reservednick", true);
- // 允许使用者修改昵称
- submitForm.setAnswer("x-muc#roomconfig_canchangenick", false);
- // 允许用户注册房间
- submitForm.setAnswer("x-muc#roomconfig_registration", false);
- // 发送已完成的表单(有默认值)到服务器来配置聊天室
- muc.sendConfigurationForm(submitForm);
- } catch (XMPPException e) {
- e.printStackTrace();
- }
还有个细节,有朋友估计看到我注释的那里有\\40的代码。原因是在XMPP中的唯一标示JID不允许出现@,所以要转义。OK?
2,报文
有朋友说报文干嘛,其实我开始也觉得有点多余,但想想类似QQ群那样,人下线了,头像名字变灰色。如果是openfire服务器的话,你下线了就直接把你去除。所以我们要给服务器发送个广播。具体处理那是以后的是了。看看这段报文:
- <iq id="902U0-48" type="set">
- <query xmlns="jabber:iq:private">
- <storage xmlns="storage:bookmarks">
- <conference name="ccc" autojoin="true" jid="ccc@conference.xmpp.chaoboo.com" ></conference>
- </storage>
- </query>
- </iq>
报文可以根据需要来改服务器的插件。
下边发送的报文:
- /**
- * 用户加入时向服务器发送的报文
- * @return
- */
- public IQ joinXml(){
- IQ iq = new IQ() {
- public String getChildElementXML() {
- StringBuilder buf = new StringBuilder();
- buf.append("<query xmlns=\"jabber:iq:private\">");
- buf.append("<storage xmlns=\"storage:bookmarks\">");
- buf.append("<").append("conference").append(" name=\"ccc\"").append(" autojoin=\"false\"").append("");
- buf.append("</storage>");
- buf.append("</query>");
- return buf.toString();
- }
- };
- iq.setType(IQ.Type.SET);
- //方法如名,这里是设置这份报文来至那个JID,后边的/smack是这段信息来至哪个端,如spark端就是/spark,android就是/Smack
- iq.setFrom(Constants.USERNAME+"@naibo.liao.com/Smack");
- return iq;
- }
怎么发送呢?可以通过连接,就是XMPPCONNECTION来发送:
- Constants.conn.sendPacket(leaveXml());
OK,停一会,手酸了
1,邀请:
很简单的一句话,邀请的时候要传入被邀请者的JID,邀请信息可以为空。
- muc.invite(userjid, "进来我们聊妹子");
简单的同时我们要知道它发生了什么,当这句话执行会向服务器发送一段报文,如下:
- <message
- from='crone1@shakespeare.lit/desktop'
- to='darkcave@chat.shakespeare.lit'>
- <x xmlns='http://jabber.org/protocol/muc#user'>
- <invite to='hecate@shakespeare.lit'>
- <reason>
- 进来我们聊妹子
- </reason>
- </invite>
- </x>
- </message>
2,被邀请
被邀请呢,我们可以开个监听吧,要不要进房间先不判断了,我这里如果有邀请就直接进房间。有需要的朋友可以自己琢磨:
- MultiUserChat.addInvitationListener(Constants.conn,
- new InvitationListener() {
- // 对应参数:连接、 房间JID、房间名、附带内容、密码、消息
- @Override
- public void invitationReceived(Connection conn,
- String room, String inviter, String reason,
- String password, Message message) {
- Log.i(TAG, "收到来自 " + inviter + " 的聊天室邀请。邀请附带内容:"
- + reason);
- Intent intent = new Intent(MucService.this,
- ActivityMultiRoom.class);
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- intent.putExtra("jid", room);
- intent.putExtra("action", "join");
- startActivity(intent);
- }
- });
1,加入房间错误,通常一个就是密码错误。此时服务器会返回以下报文:
- <presence
- from='darkcave@chat.shakespeare.lit'
- to='hag66@shakespeare.lit/pda'
- type='error'>
- <x xmlns='http://jabber.org/protocol/muc'/>
- <error type='auth'>
- <not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-stanzas'/>
- </error>
- </presence>
官网意思:如果房间要求密码验证而用户不能提供(或密码错误), 服务必须 MUST 拒绝访问这个房间并且通知该用户它们是未被授权的; 具体方法是返回一个类型为"error"的出席信息节并标明 <not-authorized/> 错误
解决如下:
- // 使用XMPPConnection创建一个MultiUserChat
- MultiUserChat muc2 = new MultiUserChat(conn1,
- "myroom@chat.shakespeare.lit/thirdwitch");
- // 用户2使用密码加入新聊天室
- // 聊天室服务将会决定要发送的历史记录数量
- muc2.join("testbot2", "password");
报文如下:
- <presence
- from='hag66@shakespeare.lit/pda'
- to='myroom@chat.shakespeare.lit/thirdwitch'>
- <x xmlns='http://jabber.org/protocol/muc'>
- <password>password</password>
- </x>
- </presence>
2,权限错误:
这个没文本,当然如果你加入一个房间后,要注意的是,如果第一次加入别人的房间,没有被授予成员权限的时候,你仅仅是一个游客。除了说话。也不能检索聊天室成员的信息,仅可以获取成员的昵称,除此别无其他。通常不注意会用这样贸然去获取成员信息是,会照成错误,返回402(进入房间后获取超越权限错误:权限不足)等信息。以下是其他错误信息
401 | Error | Presence | 进入一个房间 | 通知用户需要密码 |
403 | Error | Presence | 进入一个房间 | 通知用户他或她被房间禁止了 |
404 | Error | Presence | 进入一个房间 | 通知用户房间不存在 |
405 | Error | Presence | 进入一个房间 | 通知用户限制创建房间 |
406 | Error | Presence | 进入一个房间 | 通知用户必须使用保留的房间昵称 |
407 | Error | Presence | 进入一个房间 | 通知用户他或她不在成员列表中 |
409 | Error | Presence | 进入一个房间 | 通知用户他或她的房间昵称正在使用或被别的用户注册了 |
503 | Error | Presence | 进入一个房间 | 通知用户已经达到最大用户数 |
用户聊天的使用一个 <status/> 元素(特指, <status/> 元素的的 'code' 属性 ) 来传达关于用户在一个房间里的状态的信息.
1,聊天是成员的监听,加到MUC里边就行
- /**
- * 聊天室成员的监听器
- *
- * @author 廖乃波
- *
- */
- class MyParticipantStatusListener implements ParticipantStatusListener {
- @Override
- public void adminGranted(String arg0) {
- Log.i(TAG, "授予管理员权限" + arg0);
- }
- @Override
- public void adminRevoked(String arg0) {
- Log.i(TAG, "移除管理员权限" + arg0);
- }
- @Override
- public void banned(String arg0, String arg1, String arg2) {
- Log.i(TAG, "禁止加入房间(拉黑,不知道怎么理解,呵呵)" + arg0);
- }
- @Override
- public void joined(String arg0) {
- Log.i(TAG, "执行了joined方法:" + arg0 + "加入了房间");
- // 更新成员
- getAllMember();
- android.os.Message msg = new android.os.Message();
- msg.what = MEMBER;
- handler.sendMessage(msg);
- }
- @Override
- public void kicked(String arg0, String arg1, String arg2) {
- Log.i(TAG, "踢人" + arg0 + "被踢出房间");
- }
- @Override
- public void left(String arg0) {
- String lefter = arg0.substring(arg0.indexOf("/") + 1);
- Log.i(TAG, "执行了left方法:" + lefter + "离开的房间");
- // 更新成员
- getAllMember();
- android.os.Message msg = new android.os.Message();
- msg.what = MEMBER;
- handler.sendMessage(msg);
- }
- @Override
- public void membershipGranted(String arg0) {
- Log.i(TAG, "授予成员权限" + arg0);
- }
- @Override
- public void membershipRevoked(String arg0) {
- Log.i(TAG, "成员权限被移除" + arg0);
- }
- @Override
- public void moderatorGranted(String arg0) {
- Log.i(TAG, "授予主持人权限" + arg0);
- }
- @Override
- public void moderatorRevoked(String arg0) {
- Log.i(TAG, "移除主持人权限" + arg0);
- }
- @Override
- public void nicknameChanged(String arg0, String arg1) {
- Log.i(TAG, "昵称改变了" + arg0);
- }
- @Override
- public void ownershipGranted(String arg0) {
- Log.i(TAG, "授予所有者权限" + arg0);
- }
- @Override
- public void ownershipRevoked(String arg0) {
- Log.i(TAG, "移除所有者权限" + arg0);
- }
- @Override
- public void voiceGranted(String arg0) {
- Log.i(TAG, "给" + arg0+"授权发言");
- }
- @Override
- public void voiceRevoked(String arg0) {
- Log.i(TAG, "禁止" + arg0+"发言");
- }
- }
2,自身状态监听
- class MyUserStatusListener implements UserStatusListener {
- /*这里实现默认方法即可*/
- }