一. 原理圖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推送平臺的離線功能就已經所有實現了