There are two methods for sending push notifications to Apple Push Notification Service (APNS). First one is to use only device token and notification in JSON format. This method is fairly simple and has many implementations. I'll describe the second one, enhanced message format.

The reasons why you should use the enhanced message format are:

  • when APNS server cancel your connection, and you want to get response error code from APNS server,
  • when you want to set expiration time for your push notification (i.e. if a message was not delivered within the given amount of time, then it will be deleted)

The only downside of enhanced message format is that additional information you include in the payload (4 bytes for expiration time, 4 bytes for notification id) would decrease the maximum size of your notification text message itself. Because maximum payload size, that should be sent to APNS is fixed.

Payload format when using enhanced message format is described at Local and Push Notification Programming Guide

In enhanced format, there are two additional parameters:

  • notification id (it will be returned in error-response packet from APNS),
  • expiration time (UNIX epoch date in seconds that identifies when notification is no longer valid).

All these features I've implemented in my apn_on_rails fork (hope my pull request will be successful).

Here is an code fragment how you can generate such payload in Ruby:

  # Creates the enhanced binary message needed to send to Apple in order to have the ability to
  # retrieve error description from Apple server in case of connection was cancelled.
  # Default expiry time is 10 days.
  def enhanced_message_for_sending (seconds_to_expire = configatron.apn.notification_expiration_seconds)
    json = to_apple_json
    encoded_time = [Time.now.to_i + seconds_to_expire].pack('N')
    message = "\1#{[self.id].pack('N')}#{encoded_time}\0 #{self.device.to_hexa}\0#{json.length.chr}#{json}"
    raise APN::Errors::ExceededMessageSizeError.new(message) if message.size.to_i > 256
    message
  end

source

Now, when payload is ready to send, its time to send it to APNS servers:

  def self.response_from_apns(connection)
    timeout = 5
    if IO.select([connection], nil, nil, timeout)
      buf = connection.read(6)
      if buf
        command, error_code, notification_id = buf.unpack('CCN')
        [error_code, notification_id]
      end
    end
  end

  def self.send_notifications_for_cert(the_cert, app_id)
    # unless self.unsent_notifications.nil? || self.unsent_notifications.empty?
      if (app_id == nil)
        conditions = "app_id is null"
      else
        conditions = ["app_id = ?", app_id]
      end
      begin
        APN::Connection.open_for_delivery({:cert => the_cert}) do |conn, sock|
          APN::Device.find_each(:conditions => conditions) do |dev|
            dev.unsent_notifications.each do |noty|
              begin
                conn.write(noty.enhanced_message_for_sending)
                noty.sent_at = Time.now
                noty.save
              rescue Exception => e
                if e.message == "Broken pipe"
                  #Write failed (disconnected). Read response.
                  error_code, notif_id = response_from_apns(conn)
                  if error_code == 8
                    failed_notification = APN::Notification.find(notif_id)
                    unless failed_notification.nil?
                      unless failed_notification.device.nil?
                        APN::Device.delete(failed_notification.device.id)
                        # retry sending notifications after invalid token was deleted
                        send_notifications_for_cert(the_cert, app_id)
                      end
                    end
                  end
                end
              end
            end
          end
        end
      rescue Exception => e
        log_connection_exception(e)
      end
    # end
  end

source