基於AndroidPN搭建Android的推送平臺 - 離線消息的推送

一. 原理圖java

  1. 沒實現離線消息推送功能前,項目的架構圖以下android

  2. 實現離線消息推送功能後,項目的架構圖以下 - 版本一 - 適用於新聞等app工做環境,對離線消息的到達率要求不高的環境spring

  3. 實現離線消息推送功能後,項目的架構圖以下 - 版本二 - 適用於IM等app工做環境,對離線消息的到達率要求99.99%數據庫

二. 版本一的實現apache

  1. 建立數據表 - Notificationapi

    a. 建立實體類服務器

package org.androidpn.server.model;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "notification")
public class Notification {
	
	@Id
     @GeneratedValue(strategy = GenerationType.AUTO)
	private long id;
	
	@Column(name = "api_key", length = 64)
	private String apiKey;
	
	@Column(name = "username", nullable = false, length = 64)
	private String username;
	
	@Column(name = "title", nullable = false, length = 64)
	private String title;
	
	@Column(name = "message", nullable = false, length = 1024)
	private String message;
	
	@Column(name = "uri", length = 256)
	private String uri;
	
	public Notification() {
		
	}

	public long getId() {
		return id;
	}

	public void setId(long id) {
		this.id = id;
	}

	public String getApiKey() {
		return apiKey;
	}

	public void setApiKey(String apiKey) {
		this.apiKey = apiKey;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

	public String getUri() {
		return uri;
	}

	public void setUri(String uri) {
		this.uri = uri;
	}

}

    b. 修改Hibernate的配置文件hibernate.cfg.xmlsession

<!-- Mapping Files -->
<mapping class="org.androidpn.server.model.User" />
<!-- 消息映射 -->
<mapping class="org.androidpn.server.model.Notification" />

  2. Dao層封裝架構

    a. 新建一個NotificationDao接口及它的實現NotificationDaoHibernate.javaapp

package org.androidpn.server.dao;

import java.util.List;

import org.androidpn.server.model.Notification;

public interface NotificationDao {
	
	void saveNotification(Notification notification);
	
	List<Notification> findNotificationsByUsername(String username);
	
	void deleteNotification(Notification notification);
}

  

package org.androidpn.server.dao.hibernate;

import java.util.List;

import org.androidpn.server.dao.NotificationDao;
import org.androidpn.server.model.Notification;
import org.springframework.orm.hibernate3.support.HibernateDaoSupport;

public class NotificationDaoHibernate extends HibernateDaoSupport implements
		NotificationDao {

	public void saveNotification(Notification notification) {
		getHibernateTemplate().saveOrUpdate(notification);
		getHibernateTemplate().flush();
	}

	public void deleteNotification(Notification notification) {
		// TODO Auto-generated method stub
		getHibernateTemplate().delete(notification);
	}

	@SuppressWarnings("unchecked")
	public List<Notification> findNotificationsByUsername(String username) {
		// TODO Auto-generated method stub
		List<Notification> list = getHibernateTemplate().find("from Notification where username=?", username);
		if(list != null && list.size()>0) {
			return list;
		}
		return null;
	}

}

    b. 修改Spring的配置文件-spring-config.xml

<!-- =============================================================== -->
<!-- Data Access Objects -->
<!-- =============================================================== -->

<bean id="userDao" class="org.androidpn.server.dao.hibernate.UserDaoHibernate">
	<property name="sessionFactory" ref="sessionFactory" />
</bean>
	
<!-- 消息 -->
<bean id="notificationDao" class="org.androidpn.server.dao.hibernate.NotificationDaoHibernate">
	<property name="sessionFactory" ref="sessionFactory" />
</bean>

  3. Service層封裝

    a. 新建一個NotificationService接口及它的實現NotificationServiceImpl.java

package org.androidpn.server.service;

import java.util.List;

import org.androidpn.server.model.Notification;

public interface NotificationService {
	void saveNotification(Notification notification);

	List<Notification> findNotificationsByUsername(String username);

	void deleteNotification(Notification notification);
}

  

package org.androidpn.server.service.impl;

import java.util.List;

import org.androidpn.server.dao.NotificationDao;
import org.androidpn.server.model.Notification;
import org.androidpn.server.service.NotificationService;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class NotificationServiceImpl implements NotificationService {
	
	protected final Log log = LogFactory.getLog(getClass());
	
	private NotificationDao notificationDao;
	
	public NotificationDao getNotificationDao() {
		return notificationDao;
	}

	public void setNotificationDao(NotificationDao notificationDao) {
		this.notificationDao = notificationDao;
	}

	public void saveNotification(Notification notification) {
		// TODO Auto-generated method stub
		notificationDao.saveNotification(notification);
	}

	public List<Notification> findNotificationsByUsername(String username) {
		// TODO Auto-generated method stub
		return notificationDao.findNotificationsByUsername(username);
	}

	public void deleteNotification(Notification notification) {
		// TODO Auto-generated method stub
		notificationDao.deleteNotification(notification);
	}

}

    b. 修改Spring的配置文件-spring-config.xml

<!-- =============================================================== -->
<!-- Services -->
<!-- =============================================================== -->

<bean id="userService" class="org.androidpn.server.service.impl.UserServiceImpl">
	<property name="userDao" ref="userDao" />
</bean>
	
<bean id="notificationService" class="org.androidpn.server.service.impl.NotificationServiceImpl">
	<property name="notificationDao" ref="notificationDao" />
</bean>

    c. 修改ServiceLocator.java,提供對外調用的應用

package org.androidpn.server.service;

import org.androidpn.server.xmpp.XmppServer;

/** 
 * This is a helper class to look up service objects.
 *
 * @author Sehwan Noh (devnoh@gmail.com)
 */
public class ServiceLocator {

    public static String USER_SERVICE = "userService";
    
    public static String NOTIFICATION_SERVICE = "notificationService";

    /**
     * Generic method to obtain a service object for a given name. 
     * 
     * @param name the service bean name
     * @return
     */
    public static Object getService(String name) {
        return XmppServer.getInstance().getBean(name);
    }

    /**
     * Obtains the user service.
     * 
     * @return the user service
     */
    public static UserService getUserService() {
        return (UserService) XmppServer.getInstance().getBean(USER_SERVICE);
    }
    
    /**
     * Obtains the notification service.
     * 
     * @return the notification service
     */
    public static NotificationService getNotificationService() {
        return (NotificationService) XmppServer.getInstance().getBean(NOTIFICATION_SERVICE);
    }

}

  4. 業務邏輯層實現

    a. 修改NotificationManager.java,添加一個存儲消息Notification的方法

/**
* 存儲推送消息
* @param apiKey
* @param username
* @param title
* @param message
* @param uri
*/
private void saveNotification(String apiKey, String username, String title,
			String message, String uri) {
	Notification notification = new Notification();
	notification.setApiKey(apiKey);
	notification.setUri(uri);
	notification.setUsername(username);
	notification.setTitle(title);
	notification.setMessage(message);
	// ServiceLocator.getNotificationService().saveNotification(notification);
		notificationService.saveNotification(notification);
}

    b. 修改發送推送消息邏輯

public void sendBroadcast(String apiKey, String title, String message,
			String uri) {
	log.debug("sendBroadcast()...");
	IQ notificationIQ = createNotificationIQ(apiKey, title, message, uri);
		
	// 經過遍歷數據庫的用戶,發送推送消息
	List<User> allUser = userService.getUsers();
	for(User user : allUser) {
			ClientSession session = sessionManager.getSession(user.getUsername());
		if(session != null && session.getPresence().isAvailable()) {
			notificationIQ.setTo(session.getAddress());
			session.deliver(notificationIQ);
		} else {
			saveNotification(apiKey, user.getUsername(), title, message, uri);
		}
	}
		
	// 僅僅遍歷在線用戶
//	for (ClientSession session : sessionManager.getSessions()) {
//		if (session.getPresence().isAvailable()) {
//			notificationIQ.setTo(session.getAddress());
//			session.deliver(notificationIQ);
//		}
//	}
}

public void sendNotifcationToUser(String apiKey, String username,
			String title, String message, String uri) {
	log.debug("sendNotifcationToUser()...");
	IQ notificationIQ = createNotificationIQ(apiKey, title, message, uri);
	ClientSession session = sessionManager.getSession(username);
	if (session != null) {
		if (session.getPresence().isAvailable()) {
			notificationIQ.setTo(session.getAddress());
			session.deliver(notificationIQ);
		} 
		// 若是用戶在線但不可用時,則保存推送消息到數據庫中
		else {
			saveNotification(apiKey, username, title, message, uri);
		}
	} 
	// 若是用戶不在線但不可用時,則保存推送消息到數據庫中
	else {
		User user;
		try {
			// 經過用戶名發送推送消息,在保存消息時,驗證該用戶名是否存儲,防止存儲無用數據
			user = userService.getUserByUsername(username);
			if (user != null) {
				saveNotification(apiKey, username, title, message, uri);
			}
		} catch (UserNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

    c. 用戶從新上線後,向其推送以前的存儲在數據庫的離線消息 - 修改PresenceUpdateHandler.java中的process()方法

public void process(Packet packet) {
	ClientSession session = sessionManager.getSession(packet.getFrom());

	try {
		Presence presence = (Presence) packet;
		Presence.Type type = presence.getType();

		if (type == null) { // null == available
			if (session != null
						&& session.getStatus() == Session.STATUS_CLOSED) {
				log.warn("Rejected available presence: " + presence + " - "
							+ session);
				return;
			}

			// 用戶從新上線
			if (session != null) {
				session.setPresence(presence);
				if (!session.isInitialized()) {
					// initSession(session);
					session.setInitialized(true);
				}

				// 遍歷消息數據庫
				List<Notification> list = notificationService
							.findNotificationsByUsername(session.getUsername());
				if (list != null && list.size() > 0) {
					for(Notification notification : list) {
						String apiKey = notification.getApiKey();
						String title = notification.getTitle();
						String message = notification.getMessage();
						String uri = notification.getUri();
							notificationManager.sendNotifcationToUser(apiKey, session.getUsername(), title, message, uri);
						// 發送後將該消息從數據庫表中刪除
							notificationService.deleteNotification(notification);
					}
				}

			}

		} else if (Presence.Type.unavailable == type) {

			if (session != null) {
				session.setPresence(presence);
			}

		} else {
			presence = presence.createCopy();
			if (session != null) {
				presence.setFrom(new JID(null, session.getServerName(),
							null, true));
				presence.setTo(session.getAddress());
			} else {
				JID sender = presence.getFrom();
				presence.setFrom(presence.getTo());
				presence.setTo(sender);
			}
			presence.setError(PacketError.Condition.bad_request);
			PacketDeliverer.deliver(presence);
		}

	} catch (Exception e) {
		log.error("Internal server error. Triggered by packet: " + packet,
					e);
	}
}

三. 版本二的實現 - 版本一的加強版

  1. 爲Notification添加一個UUID字段(回執的消息體就是該消息的uuid)

    @Column(name = "uuid", length = 64, nullable = false, unique = true)

    private String uuid;

    // setter - getter

  2. 爲Dao及其實現層和Server層及其實現層添加一個根據uuid刪除消息的方法

    void deleteNotificationByUUID(String uuid); -> 實現

@SuppressWarnings("unchecked")
public void deleteNotificationByUUID(String uuid) {
	// TODO Auto-generated method stub
	List<Notification> list = getHibernateTemplate().find(
				"from Notification where uuid=?", uuid);
	if (list != null && list.size() > 0) {
		Notification notification = list.get(0);
		deleteNotification(notification);
	}
}

  3. 修改相關業務邏輯

    a. 修改NotificationManager相關方法

package org.androidpn.server.xmpp.push;

import java.util.List;
import java.util.Random;

import org.androidpn.server.model.Notification;
import org.androidpn.server.model.User;
import org.androidpn.server.service.NotificationService;
import org.androidpn.server.service.ServiceLocator;
import org.androidpn.server.service.UserNotFoundException;
import org.androidpn.server.service.UserService;
import org.androidpn.server.xmpp.session.ClientSession;
import org.androidpn.server.xmpp.session.SessionManager;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.xmpp.packet.IQ;

/**
 * This class is to manage sending the notifcations to the users.
 *
 * @author Sehwan Noh (devnoh@gmail.com)
 */
public class NotificationManager {

	private static final String NOTIFICATION_NAMESPACE = "androidpn:iq:notification";

	private final Log log = LogFactory.getLog(getClass());

	private SessionManager sessionManager;

	private NotificationService notificationService;

	private UserService userService;

	/**
	 * Constructor.
	 */
	public NotificationManager() {
		sessionManager = SessionManager.getInstance();
		notificationService = ServiceLocator.getNotificationService();
		userService = ServiceLocator.getUserService();
	}

	/**
	 * Broadcasts a newly created notification message to all connected users.
	 * 
	 * @param apiKey
	 *            the API key
	 * @param title
	 *            the title
	 * @param message
	 *            the message details
	 * @param uri
	 *            the uri
	 */
	public void sendBroadcast(String apiKey, String title, String message,
			String uri) {
		log.debug("sendBroadcast()...");
		// IQ notificationIQ = createNotificationIQ(id, apiKey, title, message,
		// uri);
		// 經過遍歷數據庫的用戶,發送推送消息
		List<User> allUser = userService.getUsers();
		for (User user : allUser) {
			Random random = new Random();
			String id = Integer.toHexString(random.nextInt());
			IQ notificationIQ = createNotificationIQ(id, apiKey, title,
					message, uri);
			ClientSession session = sessionManager.getSession(user
					.getUsername());
			if (session != null && session.getPresence().isAvailable()) {
				notificationIQ.setTo(session.getAddress());
				session.deliver(notificationIQ);
			} /* else { */
			saveNotification(id, apiKey, user.getUsername(), title, message,
					uri);
			// }
		}

		// 僅僅遍歷在線用戶
		// for (ClientSession session : sessionManager.getSessions()) {
		// if (session.getPresence().isAvailable()) {
		// notificationIQ.setTo(session.getAddress());
		// session.deliver(notificationIQ);
		// }
		// }
	}

	/**
	 * Sends a newly created notification message to the specific user.
	 * 
	 * @param apiKey
	 *            the API key
	 * @param title
	 *            the title
	 * @param message
	 *            the message details
	 * @param uri
	 *            the uri
	 */
	public void sendNotifcationToUser(String apiKey, String username,
			String title, String message, String uri) {
		log.debug("sendNotifcationToUser()...");
		Random random = new Random();
		String id = Integer.toHexString(random.nextInt());
		IQ notificationIQ = createNotificationIQ(id, apiKey, title, message,
				uri);
		ClientSession session = sessionManager.getSession(username);
		if (session != null) {
			if (session.getPresence().isAvailable()) {
				notificationIQ.setTo(session.getAddress());
				session.deliver(notificationIQ);
			}
			// // 若是用戶在線但不可用時,則保存推送消息到數據庫中
			// else {
			// saveNotification(id, apiKey, username, title, message, uri);
			// }
		}
		// // 若是用戶不在線但不可用時,則保存推送消息到數據庫中
		// else {
		User user;
		try {
			// 經過用戶名發送推送消息,在保存消息時,驗證該用戶名是否存儲,防止存儲無用數據
			user = userService.getUserByUsername(username);
			if (user != null) {
				saveNotification(id, apiKey, username, title, message, uri);
			}
		} catch (UserNotFoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		// }
	}

	/**
	 * 存儲推送消息
	 * 
	 * @param apiKey
	 * @param username
	 * @param title
	 * @param message
	 * @param uri
	 */
	private void saveNotification(String uuid, String apiKey, String username,
			String title, String message, String uri) {
		Notification notification = new Notification();
		notification.setUuid(uuid);
		notification.setApiKey(apiKey);
		notification.setUri(uri);
		notification.setUsername(username);
		notification.setTitle(title);
		notification.setMessage(message);
		// ServiceLocator.getNotificationService().saveNotification(notification);
		notificationService.saveNotification(notification);
	}

	/**
	 * Creates a new notification IQ and returns it.
	 */
	private IQ createNotificationIQ(String id, String apiKey, String title,
			String message, String uri) {
		// String id = String.valueOf(System.currentTimeMillis());
		/**
		 * uuid生成策略
		 */
		// Random random = new Random();
		// String id = Integer.toHexString(random.nextInt());
		Element notification = DocumentHelper.createElement(QName.get(
				"notification", NOTIFICATION_NAMESPACE));
		notification.addElement("id").setText(id);
		notification.addElement("apiKey").setText(apiKey);
		notification.addElement("title").setText(title);
		notification.addElement("message").setText(message);
		notification.addElement("uri").setText(uri);

		IQ iq = new IQ();
		iq.setType(IQ.Type.set);
		iq.setChildElement(notification);

		return iq;
	}

}

    b. 客戶端向服務器發送消息回執

      1. 新建一個消息回執IQ -> DeliverConfirmIQ.java

package org.androidpn.client;

import org.jivesoftware.smack.packet.IQ;

public class DeliverConfirmIQ extends IQ {
	private String uuid;

	@Override
	public String getChildElementXML() {
		// TODO Auto-generated method stub
		StringBuilder buf = new StringBuilder();
		buf.append("<").append("deliverconfirm").append(" xmlns=\"")
				.append("androidpn:iq:deliverconfirm").append("\">");
		if (uuid != null) {
			buf.append("<uuid>").append(uuid).append("</uuid>");
		}
		buf.append("</").append("deliverconfirm").append("> ");
		return buf.toString();
	}

	public String getUuid() {
		return uuid;
	}

	public void setUuid(String uuid) {
		this.uuid = uuid;
	}

}

      2. 在客戶端收到推送消息對外發送廣播後,向服務器發送回執消息,修改NotificationPacketListener的processPacket()方法

@Override
public void processPacket(Packet packet) {
    Log.d(LOGTAG, "NotificationPacketListener.processPacket()...");
    Log.d(LOGTAG, "packet.toXML()=" + packet.toXML());

    if (packet instanceof NotificationIQ) {
        	// ...
            xmppManager.getContext().sendBroadcast(intent);
            
            // 向服務器發送消息回執
            DeliverConfirmIQ deliverConfirmIQ = new DeliverConfirmIQ();
            deliverConfirmIQ.setUuid(notificationId);
            deliverConfirmIQ.setType(IQ.Type.SET);
            xmppManager.getConnection().sendPacket(deliverConfirmIQ);
        }
    }

}

    c. 服務器處理來自客戶端的消息回執

      1. 建立一個IQDeliverConfirmHandler處理從客戶端發送過來的IQ

package org.androidpn.server.xmpp.handler;

import org.androidpn.server.service.NotificationService;
import org.androidpn.server.service.ServiceLocator;
import org.androidpn.server.xmpp.UnauthorizedException;
import org.androidpn.server.xmpp.session.ClientSession;
import org.androidpn.server.xmpp.session.Session;
import org.xmpp.packet.IQ;
import org.xmpp.packet.PacketError;
import org.dom4j.Element;

public class IQDeliverConfirmHandler extends IQHandler {
	private static final String NAMESPACE = "androidpn:iq:deliverconfirm";

	private NotificationService notificationService;

	public IQDeliverConfirmHandler() {
		// TODO Auto-generated constructor stub
		notificationService = ServiceLocator.getNotificationService();
	}

	@Override
	public IQ handleIQ(IQ packet) throws UnauthorizedException {
		// TODO Auto-generated method stub
		ClientSession session = sessionManager.getSession(packet.getFrom());
		IQ reply;
		if (session == null) {
			log.error("Session not found for key " + packet.getFrom());
			reply = IQ.createResultIQ(packet);
			reply.setChildElement(packet.getChildElement().createCopy());
			reply.setError(PacketError.Condition.internal_server_error);
			return reply;
		}
		if (session.getStatus() == Session.STATUS_AUTHENTICATED) {
			if (IQ.Type.set.equals(packet.getType())) {
				Element element = packet.getChildElement();
				String uuid = element.elementText("uuid");
				notificationService.deleteNotificationByUUID(uuid);
			}
		}

		return null;
	}

	@Override
	public String getNamespace() {
		// TODO Auto-generated method stub
		return NAMESPACE;
	}

}

      2. 修改IQRouter的構造方法,將IQDeliverConfirmHandler添加到IQHandler集合中

public IQRouter() {
        sessionManager = SessionManager.getInstance();
        iqHandlers.add(new IQAuthHandler());
        iqHandlers.add(new IQRegisterHandler());
        iqHandlers.add(new IQRosterHandler());
        
        // 將處理消息回執添加到IQHandler集合中
        iqHandlers.add(new IQDeliverConfirmHandler());
   }

      3. 修改NotificationManager中的sendNotificationToUser()方法 - 在PresenceUpdateHandler調用該方法時傳入false,同時在其餘類調該方法時傳入true,

// 添加一個shouldSave標誌位,防止在PresenceUpdateHandler中調用時刪除Notification數據時重複保存該消息,致使消息沒有獲得刪除
public void sendNotifcationToUser(String apiKey, String username,
			String title, String message, String uri, boolean shouldSave) {
	log.debug("sendNotifcationToUser()...");
	Random random = new Random();
	String id = Integer.toHexString(random.nextInt());
	IQ notificationIQ = createNotificationIQ(id, apiKey, title, message,
				uri);
	ClientSession session = sessionManager.getSession(username);
	if (session != null) {
		if (session.getPresence().isAvailable()) {
			notificationIQ.setTo(session.getAddress());
			session.deliver(notificationIQ);
		}
		// // 若是用戶在線但不可用時,則保存推送消息到數據庫中
		// else {
		// saveNotification(id, apiKey, username, title, message, uri);
		// }
	}
	// // 若是用戶不在線但不可用時,則保存推送消息到數據庫中
	// else {
	User user;
	try {
		// 經過用戶名發送推送消息,在保存消息時,驗證該用戶名是否存儲,防止存儲無用數據
		user = userService.getUserByUsername(username);
		if (user != null && shouldSave) {
			saveNotification(id, apiKey, username, title, message, uri);
		}
	} catch (UserNotFoundException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
	// }
}

  4. 在版本二的基礎上修復離線發送邏輯

    1. 去掉NotificationManager中的sendNotificationToUser()方法中的shouldSave參數

    2. 在NotificationManager中添加一個發送離線消息給從新上線的用戶

/**
* 從數據庫中取出已有的離線消息推送給從新上線的用戶
* 
* @param uuid
* @param apiKey
* @param username
* @param title
* @param message
* @param uri
*/
public void sendOfflineNotifcationToUser(String uuid, String apiKey, String username, String title, String message,
			String uri) {
	log.debug("sendNotifcationToUser()...");
	IQ notificationIQ = createNotificationIQ(uuid, apiKey, title, message, uri);
	ClientSession session = sessionManager.getSession(username);
	if (session != null) {
		if (session.getPresence().isAvailable()) {
			notificationIQ.setTo(session.getAddress());
			session.deliver(notificationIQ);
		}
	}
}

    3. 在PresenceUpdateHandler中修改process(Packet)方法的相關代碼

if (session != null) {
	session.setPresence(presence);
	if (!session.isInitialized()) {
		// initSession(session);
		session.setInitialized(true);
	}

	// 遍歷消息數據庫
	List<Notification> list = notificationService.findNotificationsByUsername(session.getUsername());
	if (list != null && list.size() > 0) {
		for (Notification notification : list) {
			String apiKey = notification.getApiKey();
			String title = notification.getTitle();
			String message = notification.getMessage();
			String uri = notification.getUri();
			String uuid = notification.getUuid();
			notificationManager.sendOfflineNotifcationToUser(uuid, apiKey, session.getUsername(), title,
					message, uri);
		}
	}

}

-----------------------------------------華麗麗的分割線-----------------------------------------

 至此,基於Androidpn推送平臺的離線功能就已經所有實現了

相關文章
相關標籤/搜索