IOS离线推送
场景:
如果您有IOS端的APP,在会话聊天的时候,用户登陆了但可能会退出了界面。这时候其他终端给目标端发送消息时候,消息可以发送到IOS的推送服务器。用过QQ的都知道,你会有哦一条消息在您的主屏上展示。这个就是利用了IOS的推送服务器呢。那么openfire只需要判断用户不在线的时候将消息推送给IOS端。
苹果服务器的消息推送都需要手机的唯一标志,也就是唯一的终端设备号。那么IOS端在登陆的时候需要将该手机的设备号传递给OF服务器。这个传递很简单,您可以自定义发送IQ消息。也可以在登陆后绑定资料的时候添加JID属性来绑定设备。(建议用绑定资源的形式,这样在服务端判断的时候可以很方便的根据JID的属性值来决定是都推送)
服务端实现ios消息推送所需2个证书(附件):测试推送证书.p12、正式推送正式.p12,密码都为123456.
2个证书的区别在于一个是用于开发测试的推送证书,一个是用于产品正式上线的推送证书。2个证书获取到的终端token是不一样的。这2个证书用于JAVA后台连接APNS的服务器地址也是不同的,测试推送证书对应服务器地址是:gateway.sandbox.push.apple.com , 正式推送证书对应的服务器地址是:gateway.push.apple.com .
具体怎么做呢:
1、安装IOS推送服务需要的证书到本地,这个在网上有很多中方法
2、IOS终端登陆发送设备消息给服务器,或者以绑定资源的形式。
3、OF服务端接收该设备ID,并保存起来。
4、当有消息推送时候,根据JID属性push消息。
接下来具体看看源码了。
源码
OfflinePushPlugin
public class OfflinePushPlugin implements Component, Plugin, PropertyEventListener, PacketInterceptor{ private static final Logger Log = LoggerFactory.getLogger(OfflinePushPlugin.class); public static final String NAMESPACE_JABBER_IQ_TOKEN_BIND= "jabber:iq:token:bind"; public static final String NAMESPACE_JABBER_IQ_TOKEN_UNBUND= "jabber:iq:token:unbund"; public static final String SERVICENAME = "plugin.offlinepush.serviceName"; public static final String SERVICEENABLED = "plugin.offlinepush.serviceEnabled"; private ComponentManager componentManager; private PluginManager pluginManager; private String serviceName; private boolean serviceEnabled; //证书安装的目录 private static String dcpath = System.getProperty("openfireHome") + "\\conf\\"; private String dcName; private String dcPassword; private boolean enabled; private static Map<String, String> map = new ConcurrentHashMap<String, String>(20); private static Map<String, Integer> count = new ConcurrentHashMap<String, Integer>(20); private static AppleNotificationServer appleServer = null; private static List<PayloadPerDevice> list ; public String getDcName() { return dcName; } public void setDcName(String dcName) { JiveGlobals.setProperty("plugin.offlinepush.dcName", dcName); this.dcName = dcName; } public String getDcPassword() { return dcPassword; } public void setDcPassword(String dcPassword) { JiveGlobals.setProperty("plugin.offlinepush.password", dcPassword); this.dcPassword = dcPassword; } public boolean getEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; JiveGlobals.setProperty("plugin.offlinepush.enabled", enabled ? "true" : "false"); } public OfflinePushPlugin () { serviceName = JiveGlobals.getProperty(SERVICENAME, "offlinepush"); serviceEnabled = JiveGlobals.getBooleanProperty(SERVICEENABLED, true); } @Override public void xmlPropertySet(String property, Map<String, Object> params) { } @Override public void xmlPropertyDeleted(String property, Map<String, Object> params) { } @Override public void initializePlugin(PluginManager manager, File pluginDirectory) { dcName = JiveGlobals.getProperty("plugin.offlinepush.dcName", ""); // If no secret key has been assigned to the user service yet, assign a random one. if (dcName.equals("")){ dcName = "delementtest.p12"; setDcName(dcName); } dcpath += dcName; dcPassword = JiveGlobals.getProperty("plugin.offlinepush.password", ""); if (dcPassword.equals("")){ dcPassword = "123456"; setDcPassword(dcPassword); } enabled = JiveGlobals.getBooleanProperty("plugin.offlinepush.enabled"); setEnabled(enabled); Log.info("dcpath: " + dcpath); Log.info("dcPassword: " + dcPassword); Log.info("enabled: " + enabled); try { appleServer = new AppleNotificationServerBasicImpl(dcpath, dcPassword, enabled ); if (list == null ) { list = new ArrayList<PayloadPerDevice>(); } } catch (KeystoreException e1) { Log.error("KeystoreException: " + e1.getMessage()); } pluginManager = manager; componentManager = ComponentManagerFactory.getComponentManager(); try { componentManager.addComponent(serviceName, this); } catch (ComponentException e) { Log.error(e.getMessage(), e); } InterceptorManager.getInstance().addInterceptor(this); PropertyEventDispatcher.addListener(this); } @Override public void destroyPlugin() { InterceptorManager.getInstance().removeInterceptor(this); PropertyEventDispatcher.removeListener(this); pluginManager = null; try { componentManager.removeComponent(serviceName); componentManager = null; } catch (Exception e) { if (componentManager != null) { Log.error(e.getMessage(), e); } } serviceName = null; } @Override public String getName() { return pluginManager.getName(this); } @Override public String getDescription() { return pluginManager.getDescription(this); } @Override public void processPacket(Packet p) { if (!(p instanceof IQ)) { return; } final IQ packet = (IQ) p; if (packet.getType().equals(IQ.Type.error) || packet.getType().equals(IQ.Type.result)) { return; } final IQ replyPacket = handleIQRequest(packet); try { componentManager.sendPacket(this, replyPacket); } catch (ComponentException e) { Log.error(e.getMessage(), e); } } private IQ handleIQRequest(IQ iq) { final IQ replyPacket; // 'final' to ensure that it is set. if (iq == null) { throw new IllegalArgumentException("Argument 'iq' cannot be null."); } final IQ.Type type = iq.getType(); if (type != IQ.Type.get && type != IQ.Type.set) { throw new IllegalArgumentException( "Argument 'iq' must be of type 'get' or 'set'"); } final Element childElement = iq.getChildElement(); if (childElement == null) { replyPacket = IQ.createResultIQ(iq); replyPacket .setError(new PacketError( Condition.bad_request, org.xmpp.packet.PacketError.Type.modify, "IQ stanzas of type 'get' and 'set' MUST contain one and only one child element (RFC 3920 section 9.2.3).")); return replyPacket; } final String namespace = childElement.getNamespaceURI(); if (namespace == null) { replyPacket = IQ.createResultIQ(iq); replyPacket.setError(Condition.feature_not_implemented); return replyPacket; } if (namespace.equals(NAMESPACE_JABBER_IQ_TOKEN_BIND)) { replyPacket = processSetUUID(iq, true); } else if (namespace.equals(NAMESPACE_JABBER_IQ_TOKEN_UNBUND)) { replyPacket = processSetUUID(iq, false); } else if (namespace.equals(IQDiscoInfoHandler.NAMESPACE_DISCO_INFO)) { replyPacket = handleDiscoInfo(iq); } else { // don't known what to do with this. replyPacket = IQ.createResultIQ(iq); replyPacket.setError(Condition.feature_not_implemented); } return replyPacket; } private static IQ handleDiscoInfo(IQ iq) { if (iq == null) { throw new IllegalArgumentException("Argument 'iq' cannot be null."); } if (!iq.getChildElement().getNamespaceURI().equals( IQDiscoInfoHandler.NAMESPACE_DISCO_INFO) || iq.getType() != Type.get) { throw new IllegalArgumentException( "This is not a valid disco#info request."); } final IQ replyPacket = IQ.createResultIQ(iq); final Element responseElement = replyPacket.setChildElement("query", IQDiscoInfoHandler.NAMESPACE_DISCO_INFO); responseElement.addElement("identity").addAttribute("category", "directory").addAttribute("type", "user").addAttribute("name", "Offline Push"); responseElement.addElement("feature").addAttribute("var", NAMESPACE_JABBER_IQ_TOKEN_BIND); responseElement.addElement("feature").addAttribute("var", IQDiscoInfoHandler.NAMESPACE_DISCO_INFO); responseElement.addElement("feature").addAttribute("var", ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT); return replyPacket; } private IQ processSetUUID(IQ packet, boolean isSet) { Element rsmElement = null; if (!packet.getType().equals(IQ.Type.set)) { throw new IllegalArgumentException( "This method only accepts 'set' typed IQ stanzas as an argument."); } final IQ resultIQ; final Element incomingForm = packet.getChildElement(); rsmElement = incomingForm.element(QName.get("info", NAMESPACE_JABBER_IQ_TOKEN_UNBUND)); if(rsmElement == null) { rsmElement = incomingForm.element(QName.get("info", NAMESPACE_JABBER_IQ_TOKEN_BIND)); } resultIQ = IQ.createResultIQ(packet); if (rsmElement != null) { String osElement = rsmElement.attributeValue("os"); String jidElement = rsmElement.attributeValue("jid"); String username = new JID(jidElement).getNode(); if (osElement == null || jidElement == null) { resultIQ.setError(Condition.bad_request); return resultIQ; } if (isSet) { String tokenElement = rsmElement.attributeValue("token"); map.put(username, tokenElement); count.put(username, 0); Log.info("set token,username:" + username + " ,token:" + tokenElement); } else { map.remove(username); count.remove(username); Log.info("remove token,username:" + username ); } } else{ resultIQ.setError(Condition.bad_request); } return resultIQ; } public String getServiceName() { return serviceName; } public void setServiceName(String name) { JiveGlobals.setProperty(SERVICENAME, name); } public boolean getServiceEnabled() { return serviceEnabled; } public void setServiceEnabled(boolean enabled) { serviceEnabled = enabled; JiveGlobals.setProperty(SERVICEENABLED, enabled ? "true" : "false"); } public void propertySet(String property, Map<String, Object> params) { if (property.equals(SERVICEENABLED)) { this.serviceEnabled = Boolean.parseBoolean((String)params.get("value")); } if (property.equals("plugin.offlinepush.dcName")) { this.dcName = (String)params.get("value"); } else if (property.equals("plugin.offlinepush.enabled")) { this.enabled = Boolean.parseBoolean((String)params.get("value")); } else if (property.equals("plugin.offlinepush.password")) { this.dcPassword = (String)params.get("value"); } } /* * (non-Javadoc) * * @see org.jivesoftware.util.PropertyEventListener#propertyDeleted(java.lang.String, * java.util.Map) */ public void propertyDeleted(String property, Map<String, Object> params) { if (property.equals(SERVICEENABLED)) { this.serviceEnabled = true; } if (property.equals("plugin.offlinepush.dcName")) { this.dcName = "delementtest.p12"; } else if (property.equals("plugin.offlinepush.enabled")) { this.enabled = false; } else if (property.equals("plugin.offlinepush.password")) { this.dcPassword = "123456"; } } @Override public void initialize(JID jid, ComponentManager componentManager) throws ComponentException { // TODO Auto-generated method stub } @Override public void start() { // TODO Auto-generated method stub } @Override public void shutdown() { // TODO Auto-generated method stub } @Override public void interceptPacket(Packet packet, Session session, boolean incoming, boolean processed) throws PacketRejectedException { if (processed && incoming) { if (packet instanceof Message) { if (((Message) packet).getBody() == null) { return; } JID jid = packet.getTo(); //获取用户的设备标志id String uuid = map.get(jid.getNode()); if (uuid != null && !"".equals(uuid)) { User user = null; try { user = XMPPServer.getInstance().getUserManager().getUser(jid.getNode()); } catch (UserNotFoundException e2) { e2.printStackTrace(); } PresenceManager presenceManager = XMPPServer.getInstance().getPresenceManager(); org.xmpp.packet.Presence presence = presenceManager.getPresence(user); if (presence == null) { String body = ((Message) packet).getBody(); JSONObject jb = null; String msgType = "10015"; try { jb = new JSONObject(body); msgType = jb.getString("msgType"); if ("10012".equals(msgType) || "10001".equals(msgType) || "10002".equals(msgType)) { return; } } catch (JSONException e) { try { //根据不同的消息类型,发送不通的提示语 msgType = jb.getInt("msgType")+""; if ("10012".equals(msgType) || "10001".equals(msgType) || "10002".equals(msgType)) { return; } } catch (JSONException e1) { msgType = "10015"; } } if (msgType != null) { //msgType = "offlinepush." + msgType; String pushCont = LocaleUtils.getLocalizedString("offlinepush.10015", "offlinepush"); if (!"10000".equals(msgType)) { msgType = "offlinepush." + msgType; pushCont = LocaleUtils.getLocalizedString(msgType, "offlinepush"); } else { pushCont = LocaleUtils.getLocalizedString("offlinepush.10000", "offlinepush"); String cont = LocaleUtils.getLocalizedString("offlinepush.other", "offlinepush");; String mtype = ""; try { mtype = jb.getString("mtype"); } catch (JSONException e) { try { mtype = jb.getInt("mtype") + ""; } catch (JSONException e1) { msgType = "10015"; } } if ("0".equals(mtype)) { try { cont = jb.getString("Cnt"); if (cont.length() > 20) { cont = cont.substring(0, 20); cont += "..."; } } catch (JSONException e) { } } else if ("1".equals(mtype)) { cont = LocaleUtils.getLocalizedString("offlinepush.image", "offlinepush"); } else if ("2".equals(mtype)) { cont = LocaleUtils.getLocalizedString("offlinepush.audio", "offlinepush"); } else if ("4".equals(mtype)) { cont = LocaleUtils.getLocalizedString("offlinepush.file", "offlinepush"); } else if ("3".equals(mtype)) { cont = LocaleUtils.getLocalizedString("offlinepush.location", "offlinepush"); } else if ("6".equals(mtype)) { cont = LocaleUtils.getLocalizedString("offlinepush.video", "offlinepush"); } pushCont += cont; } pushOfflineMsg(uuid, pushCont, jid); } } } } } } private void pushOfflineMsg(String token, String pushCont, JID jid) { NotificationThreads work = null; try { Integer size = count.get(jid.getNode()) + 1; if (size <= 1000) count.put(jid.getNode(), size); List<PayloadPerDevice> list = new ArrayList<PayloadPerDevice>(); PushNotificationPayload payload = new PushNotificationPayload(); payload.addAlert(pushCont); payload.addSound("default"); payload.addBadge(size); payload.addCustomDictionary("jid", jid.toString()); PayloadPerDevice pay = new PayloadPerDevice(payload, token); list.add(pay); work = new NotificationThreads(appleServer,list,1); work.setListener(DEBUGGING_PROGRESS_LISTENER); work.start(); } catch (JSONException e) { Log.error("JSONException:" + e.getMessage()); } catch (InvalidDeviceTokenFormatException e) { Log.error("InvalidDeviceTokenFormatException:" + e.getMessage()); }finally{ work.destroy(); Log.info("push to apple: username: " + jid.getNode() + " ,context" + pushCont); } } public Runnable createTask(final String token, final String msgType, final JID jid) { return new Runnable() { @Override public void run() { PushNotificationPayload payload = new PushNotificationPayload(); try { String pushCont = LocaleUtils.getLocalizedString(msgType, "offlinepush"); List<PayloadPerDevice> list = new ArrayList<PayloadPerDevice>(); payload.addAlert(pushCont); payload.addSound("default"); payload.addBadge(1); payload.addCustomDictionary("jid", jid.toString()); PayloadPerDevice pay = new PayloadPerDevice(payload, token); list.add(pay); NotificationThreads work = new NotificationThreads(appleServer,list,1); work.setListener(DEBUGGING_PROGRESS_LISTENER); work.start(); } catch (JSONException e) { Log.error("JSONException:" + e.getMessage()); } catch (InvalidDeviceTokenFormatException e) { Log.error("InvalidDeviceTokenFormatException:" + e.getMessage()); } } }; } public static final NotificationProgressListener DEBUGGING_PROGRESS_LISTENER = new NotificationProgressListener() { public void eventThreadStarted(NotificationThread notificationThread) { System.out.println(" [EVENT]: thread #" + notificationThread.getThreadNumber() + " started with " + " devices beginning at message id #" + notificationThread.getFirstMessageIdentifier()); } public void eventThreadFinished(NotificationThread thread) { System.out.println(" [EVENT]: thread #" + thread.getThreadNumber() + " finished: pushed messages #" + thread.getFirstMessageIdentifier() + " to " + thread.getLastMessageIdentifier() + " toward "+ " devices"); } public void eventConnectionRestarted(NotificationThread thread) { System.out.println(" [EVENT]: connection restarted in thread #" + thread.getThreadNumber() + " because it reached " + thread.getMaxNotificationsPerConnection() + " notifications per connection"); } public void eventAllThreadsStarted(NotificationThreads notificationThreads) { System.out.println(" [EVENT]: all threads started: " + notificationThreads.getThreads().size()); } public void eventAllThreadsFinished(NotificationThreads notificationThreads) { System.out.println(" [EVENT]: all threads finished: " + notificationThreads.getThreads().size()); } public void eventCriticalException(NotificationThread notificationThread, Exception exception) { System.out.println(" [EVENT]: critical exception occurred: " + exception); } }; }
Plugin.xml
<?xml version="1.0" encoding="UTF-8"?> <plugin> <class>com.....offlinepush.plugin.OfflinePushPlugin</class> <name>offlinepush</name> <description>.......</description> <author>huwenfeng</author> <version>1.5.1</version> <date>1/2/2014</date> <minServerVersion>3.7.0</minServerVersion> </plugin>
资源文件:offlinepush_i18n_zh_CN.properties
offlinepush.10000=\u65B0\u6D88\u606F\uFF1A offlinepush.10001=\u7528\u6237\u64CD\u4F5C offlinepush.image=[\u56FE\u7247] offlinepush.audio=[\u8BED\u97F3] offlinepush.file=[\u6587\u4EF6] offlinepush.other=[\u5176\u4ED6] offlinepush.location=[\u4F4D\u7F6E] offlinepush.video=[\u89C6\u9891] ......
需要的jar包。
OK啦。
注意:IOS的推送服务器有两种模式都是免费,一种是测试的还一种是正式使用的。
所以这里最好将推送服务的使用模式在OF的管理台做配置。
本人在控制台配置了三个属性值: