
How to organize sending push notifications to iPhone
At Surfingbird, we use push notifications to inform our users of breaking news and simply inform them of interesting materials for the day. In the first weeks of tests, the pushies showed their tremendous effectiveness in terms of hobby retention. There is a logical explanation for this - the user’s phone is always with him, in the subway, in the toilet, at meetings, etc. When a user receives a push, all his attention is concentrated on this notification.
We have implemented sending push notifications from the backend in the Perl programming language. However, when we just started to push, we encountered some difficulties. We want to tell about difficulties and their overcoming in this post.

To begin with, we initially did not want to use third-party services such as Amazon SNS ,Parse , Push IO , etc. No matter how convenient all these solutions are, they deprive you of the flexibility and the ability to somehow influence the process.
A little bit about why we even need push notifications. If some important event takes place in the world, we want to inform our users about it as quickly as possible. It doesn’t matter what kind of event: Parmesan has disappeared from the shelves of supermarkets, the dollar exchange rate is breaking all records, the accident in the subway or the Japanese started spreading seals on dolls from Ikea on Instagram . Any such information line needs to be worked out as quickly as possible and delivered to the user's phone. Of
course, we immediately went to metacpan in search of a ready-made module.
The first we came across Net :: APNS. We really liked thecode cutest avatar with a fox. But the code was completely unsuitable for use in production. He opened the connection before each message was sent to each device and, after sending, closed it. This, firstly, takes a very long time, and secondly, Apple may (and will) perceive it as a DoS attack.
Well, in general, the module code is clear and supported, it remains only to enrich it a little. At the very beginning, we used a simple notification format:

The code is as follows:
However, after the very first tests it became clear that in production it is better not to use a simple notification format. The thing is that if you send an incorrect or illegible notification, Apple returns an error and closes the connection.
In the future, all notifications sent over the same connection will be discarded and need to be resent. The error is returned in this format:

As you can see, the description of the error contains the identifier of the notification that caused this error. In order for notifications to have identifiers, it is necessary to use the extended format of notifications:

For this, we rewrite two methods. You can use the token serial number as $ push_id:
So much better! Now you can safely send pushes until an error response arrives. After that, we extract the identifier of the problem notification from the error description, close the old one and open a new connection, continue to send notifications from the place where the error occurred.
You need to deal with the problem notification privately, there may be several reasons and they are all described here .
But that is not all. The user can delete your application or simply prohibit the reception of pushes. In order not to send notifications about dead tokens, you need to use the feedback service. It returns a list of all tokens that you no longer need to send notifications to. Feedback format:

As Apple recommends, it’s enough to connect to feedback.push.apple.com:2196 and read from the socket only once a day, according to the crown. The received tokens simply need to be removed from the database.
By the way, to send notifications to android, it is enough to make a regular http-request, in which you can transfer 1000 tokens at once. Of course, several queries can be made in parallel. And if that seems too slow, you can use Cloud Connection Server (XMPP) .
In the comments, we would be interested to know how you solve the problem of push notifications in your applications.
1;
We have implemented sending push notifications from the backend in the Perl programming language. However, when we just started to push, we encountered some difficulties. We want to tell about difficulties and their overcoming in this post.

To begin with, we initially did not want to use third-party services such as Amazon SNS ,Parse , Push IO , etc. No matter how convenient all these solutions are, they deprive you of the flexibility and the ability to somehow influence the process.
A little bit about why we even need push notifications. If some important event takes place in the world, we want to inform our users about it as quickly as possible. It doesn’t matter what kind of event: Parmesan has disappeared from the shelves of supermarkets, the dollar exchange rate is breaking all records, the accident in the subway or the Japanese started spreading seals on dolls from Ikea on Instagram . Any such information line needs to be worked out as quickly as possible and delivered to the user's phone. Of
course, we immediately went to metacpan in search of a ready-made module.
The first we came across Net :: APNS. We really liked the
Well, in general, the module code is clear and supported, it remains only to enrich it a little. At the very beginning, we used a simple notification format:

The code is as follows:
package Birdy::PushNotification::APNS;
use 5.018;
use Mojo::Base -base;
no if $] >= 5.018, warnings => "experimental";
use Socket;
use Net::SSLeay qw/die_now die_if_ssl_error/;
use JSON::XS;
use Encode qw(encode);
BEGIN {
$Net::SSLeay::trace = 4;
$Net::SSLeay::ssl_version = 10;
Net::SSLeay::load_error_strings();
Net::SSLeay::SSLeay_add_ssl_algorithms();
Net::SSLeay::randomize();
}
sub new {
my ($class, $sandbox) = @_;
my $port = 2195;
my $address = $sandbox
? 'gateway.sandbox.push.apple.com'
: 'gateway.push.apple.com';
my $apns_key = "$ENV{MOJO_HOME}/apns_key.pem";
my $apns_cert = "$ENV{MOJO_HOME}/apns_cert.pem";
my $socket;
socket(
$socket, PF_INET, SOCK_STREAM, getprotobyname('tcp')
) or die "socket: $!";
connect(
$socket, sockaddr_in( $port, inet_aton($address) )
) or die "Connect: $!";
my $ctx = Net::SSLeay::CTX_new() or die_now("Failed to create SSL_CTX $!.");
Net::SSLeay::CTX_set_options($ctx, &Net::SSLeay::OP_ALL);
die_if_ssl_error("ssl ctx set options");
Net::SSLeay::CTX_use_RSAPrivateKey_file($ctx, $apns_key, &Net::SSLeay::FILETYPE_PEM);
die_if_ssl_error("private key");
Net::SSLeay::CTX_use_certificate_file($ctx, $apns_cert, &Net::SSLeay::FILETYPE_PEM);
die_if_ssl_error("certificate");
my $ssl = Net::SSLeay::new($ctx);
Net::SSLeay::set_fd($ssl, fileno($socket));
Net::SSLeay::connect($ssl) or die_now("Failed SSL connect ($!)");
my $self = bless {
'ssl' => $ssl,
'ctx' => $ctx,
'socket' => $socket,
}, $class;
return $self;
}
sub close {
my ($self) = @_;
my ($ssl, $ctx, $socket) = @{$self}{qw/ssl ctx socket/}
CORE::shutdown($socket, 1);
Net::SSLeay::free($ssl);
Net::SSLeay::CTX_free($ctx);
close($socket);
}
sub write {
my ($self, $token, $alert, $data_id, $sound) = @_;
Net::SSLeay::write(
$self->{'ssl'},
$self->_pack_payload($token, $alert, $data_id, $sound)
);
}
sub _pack_payload {
my ($self, $token, $alert, $data_id, $sound) = @_;
my $data = {
'aps' => {
'alert' => encode('unicode', $alert),
},
'data_id' => $data_id,
};
# добавляем звук
$data->{'aps'}->{'sound'} = 'default' if $sound;
my $xs = JSON::XS->new->utf8(1);
my $payload =
chr(0)
. pack('n', 32)
. pack('H*', $token);
# кеширование внутри переменной
if (!$self->{'_alert'}) {
# необходимо такое сообщение, чтобы полезная нагрузка не превышала 256 байт
my $json = $xs->encode($data);
my $overload = length($payload) + length(pack 'n', length $json) + length($json) - 256;
if ($overload > 0) {
substr( $data->{'aps'}->{'alert'}, -$overload ) = '';
}
# сохраним чтобы больше не пересчитывать
$self->{'_alert'} = $data->{'aps'}->{'alert'};
}
my $json = $xs->encode($data);
$payload .= pack('n', length $json) . $json;
return $payload;
}
However, after the very first tests it became clear that in production it is better not to use a simple notification format. The thing is that if you send an incorrect or illegible notification, Apple returns an error and closes the connection.
In the future, all notifications sent over the same connection will be discarded and need to be resent. The error is returned in this format:

As you can see, the description of the error contains the identifier of the notification that caused this error. In order for notifications to have identifiers, it is necessary to use the extended format of notifications:

For this, we rewrite two methods. You can use the token serial number as $ push_id:
sub write {
my ($self, $token, $alert, $data_id, $sound, $push_id) = @_;
Net::SSLeay::write(
$self->{'ssl'},
$self->_pack_payload($token, $alert, $data_id, $sound, $push_id)
);
}
sub _pack_payload {
my ($self, $token, $alert, $data_id, $sound, $push_id) = @_;
my $data = {
'aps' => {
'alert' => encode('unicode', $alert),
},
'data_id' => $data_id,
};
# добавляем звук
$data->{'aps'}->{'sound'} = 'default' if $sound;
my $xs = JSON::XS->new->utf8(1);
my $payload =
chr(1)
. pack('N', $push_id)
. pack('N', time + (3600 * 24) )
. pack('n', 32)
. pack('H*', $token);
# кеширование внутри переменной
if (!$self->{'_alert'}) {
# необходимо такое сообщение, чтобы полезная нагрузка не превышала 256 байт
my $json = $xs->encode($data);
my $overload = length($payload) + length(pack 'n', length $json) + length($json) - 256;
if ($overload > 0) {
substr( $data->{'aps'}->{'alert'}, -$overload ) = '';
}
# сохраним чтобы больше не пересчитывать
$self->{'_alert'} = $data->{'aps'}->{'alert'};
}
my $json = $xs->encode($data);
$payload .= pack('n', length $json) . $json;
return $payload;
}
So much better! Now you can safely send pushes until an error response arrives. After that, we extract the identifier of the problem notification from the error description, close the old one and open a new connection, continue to send notifications from the place where the error occurred.
You need to deal with the problem notification privately, there may be several reasons and they are all described here .
But that is not all. The user can delete your application or simply prohibit the reception of pushes. In order not to send notifications about dead tokens, you need to use the feedback service. It returns a list of all tokens that you no longer need to send notifications to. Feedback format:

As Apple recommends, it’s enough to connect to feedback.push.apple.com:2196 and read from the socket only once a day, according to the crown. The received tokens simply need to be removed from the database.
sub read_feedback {
my ($self) = @_;
my $result = [];
my $bytes = Net::SSLeay::read( $self->{'ssl'} );
while ($bytes) {
my ($ts, $token);
($ts, $token, $bytes) = unpack 'N n/a a*', $bytes;
$token = unpack 'H*', $token;
push @$result, {
'ts' => $ts,
'token' => $token,
};
}
return $result;
}
By the way, to send notifications to android, it is enough to make a regular http-request, in which you can transfer 1000 tokens at once. Of course, several queries can be made in parallel. And if that seems too slow, you can use Cloud Connection Server (XMPP) .
In the comments, we would be interested to know how you solve the problem of push notifications in your applications.
1;