您的位置:首页 > 移动开发 > Android开发

基于AndroidPN搭建Android的推送平台 - 离线消息的推送

2015-12-10 03:37 609 查看
一. 原理图

  1. 没实现离线消息推送功能前,项目的架构图如下



  2. 实现离线消息推送功能后,项目的架构图如下 - 版本一 - 适用于新闻等app工作环境,对离线消息的到达率要求不高的环境



  3. 实现离线消息推送功能后,项目的架构图如下 - 版本二 - 适用于IM等app工作环境,对离线消息的到达率要求99.99%



二. 版本一的实现

  1. 创建数据表 - Notification

    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.xml

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


  2. Dao层封装

    a. 新建一个NotificationDao接口及它的实现NotificationDaoHibernate.java

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推送平台的离线功能就已经全部实现了
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: