2013年10月12日

Smack 开发笔记(1)

上个月接到一个任务——开发一个XMPP客户端,准确地说来,是用XMPP协议实现在两台计算机之间互发消息。我建议用Java,因为我知道大Java不管什么东西,总有5个以上的开源解决方案。于是选了Openfire作服务器,Smack 3.3来开发客户端。
国庆参考了几篇博客文章[1][2][3],写了个控制台下的客户端,可以实现简单地收发消息。
综合这几篇文章,为了实现一个XMPP客户端的消息收发功能,我封装了一个Client.java类,其成员变量及方法如下(省略了具体实现代码,后面会详细说明):


package edu.ncepu.smack.sample;

import java.util.Scanner;
import org.jivesoftware.smack.*;
import org.jivesoftware.smack.packet.*;
import org.jivesoftware.smack.packet.Message.Type;

/**
 * 
 * Copyright © 2013 NCEPU. All rights reserved.
 * 
 * @ClassName: Client
 * @Description: TODO
 * @author: Reiji
 * @date: 2013年10月5日 下午6:54:57
 * @version: V1.0
 */
public class Client {
 private String username = null;
 private String password = null;
 private XMPPConnection connection;
 private ConnectionConfiguration config;
 private String server = "reiji-think";
 private String domain = "@reiji-think";
 private ChatManager chatManager;

 /**
  * 
  * @Title: username
  * @Description: 用户名
  * @return
  */
 public String username() {
  return username;
 }

 /**
  * 
  * @Title: password
  * @Description:密码 
  * @return String
  */
 public String password() {
  return password;
 }

 /**
  * 
  * @Title:Client
  * @Description:初始化各种连接配置,与服务器建立连接
  * @param server 服务器地址或计算机名
  */
 public Client(String server) {
  this.server = server;
  init();
 }

 /**
  * 
  * @Title: init
  * @Description:初始化各种连接配置,与服务器建立连接
  */
 private void init() {}

 /**
  * 
  * @Title: login
  * @Description: 用户登陆,资源绑定为Ncepu.Smack,并设优先级为1 
  * @param username 用户名
  * @param password 密码
  */
 public void login(String username, String password) {}

 /**
  * 
  * @Title: send
  * @Description: 以数据包形式发送消息(另一种方式为创建一个Chat,通过Chat发送)
  * @param to 消息的接受者,若非完整JID则默认发给reiji-think域下优先级最高者
  * @param message 发送的消息
  */
 public void send(String to, String message) {}

 /**
  * 
  * @Title: setListener
  * @Description: 设置消息监听器
  */
 public void setListener() {
  chatManager.addChatListener(new SimpleChatManagerListener());
 }

 /**
  * 资源回收
  */
 public void finalize() {}

}
其行为分为如下步骤:

  1. 与服务器建立连接
  2. 这一步,需要实例化一个XMPPConection,虽然XMPPConection提供new XMPPConection(String server)的构造方法,但我们这里使用new XMPPConection(ConnectionConfiguration config)这种定制性更强的构造方法。 代码如下:
    /**
     * 
     * @Title: init
     * @Description:初始化各种连接配置,与服务器建立连接
     */
    private void init() {
     try {
      /*
       * 5222是openfire服务器默认的通信端口,可以登录http://%server%:9090/
       * 到管理员控制台查看客户端到服务器端口
       */
      config = new ConnectionConfiguration(server, 5222);
      // 是否启用压缩
      config.setCompressionEnabled(true);
      // 是否启用安全验证
      config.setSASLAuthenticationEnabled(true);
      // 是否启用调试
      config.setDebuggerEnabled(false);
      // 创建connection链接
      connection = new XMPPConnection(config);
      connection.connect();
      System.out.println("Connection established.");
     }
     catch (XMPPException e) {
      System.out.println("Fatal Error,cannot connect to server. Application terminated.");
      e.printStackTrace();
      System.out.println("Press any key to exit.");
      Scanner sc = new Scanner(System.in);
      sc.nextLine();
      sc.close();
      System.exit(1);
     }  
    }
    
  3. 以某一用户登陆
  4. 即login(String username, String password)方法中的具体实现,代码如下:
    /**
     * 
     * @Title: login
     * @Description: 用户登陆,资源绑定为Ncepu.Smack,并设优先级为1 
     * @param username 用户名
     * @param password 密码
     */
    public void login(String username, String password) {
     try {
      connection.login(username, password, "Ncepu.Smack");
      // 当前登陆用户的聊天管理器
      chatManager = connection.getChatManager();
      Presence pre = new Presence(Presence.Type.available);
      // 设置优先级
      pre.setPriority(1);
      connection.sendPacket(pre);
      System.out.println("Login successfully.");
     }
     catch (XMPPException e) {
      System.out.println("Fatal Error,cannot login. Application terminated.");
      e.printStackTrace();
      System.out.println("Press any key to exit.");
      Scanner sc = new Scanner(System.in);
      sc.nextLine();
      sc.close();
      System.exit(1);
     }
    }
    
    注意这里登陆后向服务器发送了一个Presence,将自己的状态设为在线(available)。更重要的是,设置了该客户端的消息优先级,此处为1。这个优先级在一个账号多处登陆时将决定发给该用户的消息到底由哪一个客户端接收,优先级高的接收,低的则接收不到;若优先级相同,则先登陆的先接收。我还没有测试过从Openfire服务器给所有用户广播的情况。
  5. 获取好友列表(可省略,Smack、Spark等客户端都允许直接给陌生人发消息)
  6. 好吧,我略过了这一步。
  7. 添加消息/聊天监听器
  8. 即类中的setListener()方法,代码如下:
    /**
     * 
     * @Title: setListener
     * @Description: 设置消息监听器
     */
    public void setListener() {
     chatManager.addChatListener(new SimpleChatManagerListener());
    }
    
    这里涉及到我自己写的SimpleChatManagerListener类(还有一个SimpleMessageListener类也使用到了),这两个类分别继承ChatManagerListener和MessageListener,实现代码都很简单,这里不赘述,代码会附在最后。
  9. 向用户发送消息
  10. 即send(String message)方法,代码如下:
    /**
     * 
     * @Title: send
     * @Description: 以数据包形式发送消息(另一种方式为创建一个Chat,通过Chat发送)
     * @param to 消息的接受者,若非完整JID则默认发给reiji-think域下优先级最高者
     * @param message 发送的消息
     */
    public void send(String to, String message) {
     if (connection.isConnected()) {
      if (!to.contains("@")) {
       to = to.concat(domain);
      }
      Message msg = new Message(to, Type.chat);
      msg.setBody(message);
      msg.setLanguage("zh");
      connection.sendPacket(msg);
     }
     else {
      System.out.println("Error! Not connect to a server. Application is terminating.");
      System.exit(1);
     }
    }
    
    为了保证健壮性,在发送之前先判断一下连接是否还连着。当然这里可以自动重连,后再发送。但由于我在login()方法里没有把username和password保存下来,在send方法里重连是无法做到的。 另外我使用了Message.setLanguage()方法,纯粹是为了尝试一下这个方法,不设置这个Language也是可以的。这个Language的合法列表Smack的文档里没有给出,它在RFCXXX某一个标准里,可以搜xml:lang找到 发送消息直接在connection级别发送,而没有使用Chat.send()方法,因为Chat需要由登陆用户的ChatManager来创建,而ChatManager.createChat(String to, MessageListener listener)需要两个参数。这个listener只能监听特定用户的消息,不能接收所有发给当前用户的消息,虽然这里可以传个null,但这样很别扭,还不如直接在connection编码。
  11. 结束,断开连接
  12. 控制台下这个其实没太大必要,一关闭,自动回收资源了。但还是写写吧:
    /**
     * 资源回收
     */
    public void finalize() {
     if (connection != null && connection.isConnected()) {
      connection.disconnect();
      connection = null;
     }
    }
    
以下是SimpleChatManagerListener.java
/**  
 * Copyright © 2013NCEPU. All rights reserved.
 *
 * @Title: SimpleChatManagerListener.java
 * @Prject: edu.ncepu.smack.sample
 * @Package: edu.ncepu.smack.sample
 * @ClassName: SimpleChatManagerListener
 * @Description: TODO
 * @author:Reiji
 * @date: 2013年10月6日 下午5:55:26
 * @version: V1.0  
 */
package edu.ncepu.smack.sample;

import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.ChatManagerListener;

public class SimpleChatManagerListener implements ChatManagerListener {

 /* (非 Javadoc)
  * @Title: chatCreated
  * @Description: TODO
  * @param chat
  * @param createdLocally
  * @see org.jivesoftware.smack.ChatManagerListener
  *     #chatCreated(org.jivesoftware.smack.Chat, boolean)
  */
 @Override
 public void chatCreated(Chat chat, boolean createdLocally) {
  String to=chat.getParticipant();
  chat.addMessageListener(new SimpleMessageListener(to));
 }

}
以下是SimpleMessageListener.java
/**  
 * Copyright © 2013NCEPU. All rights reserved.
 *
 * @Title: SimpleMessageListener.java
 * @Prject: edu.ncepu.smack.sample
 * @Package: edu.ncepu.smack.sample
 * @ClassName: SimpleMessageListener
 * @Description: TODO
 * @author: Reiji 
 * @date: 2013年10月6日 下午4:25:54
 * @version: V1.0  
 */
package edu.ncepu.smack.sample;

import java.util.Collection;

import org.jivesoftware.smack.Chat;
import org.jivesoftware.smack.MessageListener;
import org.jivesoftware.smack.packet.Message;
import org.jivesoftware.smack.packet.Message.Body;

public class SimpleMessageListener implements MessageListener {

 private String to;
 
 public SimpleMessageListener(){
  
 }
 
 public SimpleMessageListener(String to){
  this.to=to;
 }
 
 
 /* (非 Javadoc)
  * @Title: processMessage
  * @Description: TODO
  * @param chat
  * @param message
  * @see org.jivesoftware.smack.MessageListener
        #processMessage(org.jivesoftware.smack.Chat, org.jivesoftware.smack.packet.Message)
  */
 @Override
 public void processMessage(Chat chat, Message message) {
  Collection bodies=message.getBodies();
  for(Body i :bodies){
   System.out.println("\nMessage received!\n"+chat.getParticipant()+":"+i.getMessage());
  }
  //For DEBUG
  //System.out.println("\nMessage received!\n"+to+":"+message.getBody());
  //System.out.println(message.toXML());
  System.out.print("Enter the message you want to send to "+to+":");
 }

}

没有评论:

发表评论