There is a handy daemon for sending push notifications to iOS-based mobile clients via
Apple's Push Notification Service; it is called
pyapns. It is implemented in python but, since it runs as a standalone XML-RPC server process, that fact is largely irrelevant. The important facts are that:
- It properly and fully implements the client interface to APNs, including the requirement for maintaining a persistent connection with Apple's servers rather than repeatedly setting-up and tearing-down SSL connections.
- It includes client libraries for communicating with the pyapns daemon from python and ruby, although any language that can speak XML-RPC (including C) will work too.
The way it works is: you first start the pyapns daemon process. This process acts as a XML-RPC server handling requests from your application(s), packing them into Apple's binary APNS protocol, and sending them to Apple to deliver to the iPhone, iPad, or iPod.
In order for your applications to send a push notification request, though, they must first tell pyapns which client certificate it should use to authenticate with the APNS servers.
Here is a decent guide on how to obtain a client certificate. Once you have a certificate, you have to use the pyapns client library's
configure
and
provision
APIs to tell the pyapns daemon process to use your certificate.
If you are implementing your application in django, you can accomplish the configuration and provisioning directly from your django
settings.py file like so:
# Configuration for connecting to the local pyapns daemon,
# including our certificate for pushing notifications to
# mobile terminals via APNS.
PYAPNS_CONFIG = {
'HOST': 'http://localhost:7077/',
'TIMEOUT': 15,
'INITIAL': [
('MyAppName', 'path/to/cert/apns_sandbox.pem', 'sandbox'),
]
}
The pyapns python client library will automatically configure and provision itself from these settings. So, assuming you know the APNS device token of the mobile device you want to send a notification to, all you need to do to send a push notification is to call the
pyapns.client.notify()
function.
If only it were so easy. One complication arises in that the pyapns provisioning and configuration state is split between the client library and the pyapns daemon process. As a result, there are two scenarios to be wary of:
- The django application is restarted. In this case, the client library, which is part of your django application, loses its state and tries to re-configure and re-provision itself from your django settings. Luckily, since the client library will re-read the configuration and provisioning settings from settings.py and seamlessly resume communication with the pyapns daemon.
However, as noted in the pyapns documentation, "attempts to provision the same application id multiple times are ignored." As a result, if you change pyapns configuration in the settings.py file and restart django, you need to restart the pyapns daemon too for the new settings to take effect. Otherwise, if the settings are unchanged, the client library will seamlessly resume communication with the pyapns daemon.
- The pyapns daemon is restarted. In this case, the client library thinks it has already configured and provisioned the daemon, but the daemon has lost this configuration due to restart. As a result, any attempt to send a push notification will fail as the daemon does not know how to establish the connection with Apple's Push Notification service.
As I mentioned above, the first scenario isn't a big deal. If you have to restart your web application or the web server for some reason, the connection between the pyapns client library and the daemon process will automatically resume right where it left off. In the rare case that you changed the pyapns settings in your django
settings.py file, you need to restart both the django application and the pyapns daemon process for the new settings to take effect.
The latter scenario, though, is a bigger problem because it is impossible to detect until it is too late: that is, it doesn't manifest itself until you try to send a push notification and fail. Luckily, however, we can catch the failure condition and resolve the problem automatically. Specifically, if the pyapns client library fails to send a push notification to the daemon process due to the daemon process not being configured or provisioned, we can force the client library to re-configure and re-provision and retry.
So here you go, a wrapper around the pyapns client library to automatically recover when the backend pyapns daemon has been restarted:
"""
Wrappers for the pyapns client to simplify sending APNS
notifications, including support for re-configuring the
pyapns daemon after a restart.
"""
import pyapns.client
import time
import logging
log = logging.getLogger('APNS')
def notify(apns_token, message, badge=None, sound=None):
"""Push notification to device with the given message
@param apns_token - The device's APNS-issued unique token
@param message - The message to display in the
notification window
"""
notification = {'aps': {'alert': message}}
if badge is not None:
notification['aps']['badge'] = int(badge)
if sound is not None:
notification['aps']['sound'] = str(sound)
for attempt in range(4):
try:
pyapns.client.notify('MyAppId', apns_token,
notification)
break
except (pyapns.client.UnknownAppID,
pyapns.client.APNSNotConfigured):
# This can happen if the pyapns server has been
# restarted since django started running. In
# that case, we need to clear the client's
# configured flag so we can reconfigure it from
# our settings.py PYAPNS_CONFIG settings.
if attempt == 3:
log.exception()
pyapns.client.OPTIONS['CONFIGURED'] = False
pyapns.client.configure({})
time.sleep(0.5)
Since I glossed over it in this post, I'll cover how to get the APNS device token for a mobile device in my next post. The device token acts as an address, telling Apple's Push Notification service which mobile device it should deliver your notification message to.