📜 ⬆️ ⬇️

How to organize sending push-notifications on iPhone

At Surfingbird, we use push notifications to inform our users of urgent news and simply inform them of interesting materials about the day. Already in the first weeks of testing, the fluffs showed their tremendous effectiveness in terms of retension hobby. 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 the user comes to push, his entire attention is focused on this notification.

We implemented sending push notifications from the backend in the Perl programming language. However, when we first started introducing pushes, we encountered some difficulties. We also want to tell about difficulties and their overcoming in this post.

image

To begin with, we initially did not want to use third-party services, such as Amazon SNS , Parse , Push IO , etc. No matter how comfortable all these solutions are, they deprive you of the flexibility and ability to somehow influence the process.
')
A little about why we need push notifications at all. If an important event occurs in the world, we want to notify our users about this as quickly as possible. It doesn't matter what this event is: Parmesan has disappeared from the supermarket shelves, the dollar rate is breaking all records, an accident in the subway, or the Japanese have started putting cats in dolls from Ikea on Instagram . Any such infopovod needs to work as quickly as possible and deliver to the user's phone.

Of course, we immediately went to metacpan in search of a ready-made module.
The first we came across was Net :: APNS . We really liked the code of the sweetest avatar with chanterelle. But the code was completely unsuitable for use in production. He opened the connection before each sending a message to each device and, after sending, closed it. This, firstly, takes a lot of time, and secondly, Apple can (and will) be perceived as a DoS attack.

Well, in general, the module code is clear and supported, it only remains to enrich it. At the very beginning, we used a simple notification format:

image

The code is:

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 would be better not to use the simple notification format. The thing is, 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 must be sent again. The error is returned in this format:

image

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

image

To do this, we rewrite the two methods. As $ push_id, you can use the token sequence number:

 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 puschi, until you receive a response with an error. After that, we take out 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.

With the problematic notification you need to understand privately, there can be several reasons and they are all described here .

But that's not all. The user can delete your application or simply deny accepting push. In order not to send notifications on dead tokens, you need to use feedback service. It returns a list of all tokens to which you no longer need to send notifications. Feedback format:

image

As Apple recommends, it’s enough just once a day, according to crown, to connect to feedback.push.apple.com:2196 and read from the socket. The resulting tokens just 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/aa*', $bytes; $token = unpack 'H*', $token; push @$result, { 'ts' => $ts, 'token' => $token, }; } return $result; } 

By the way, to send notifications to the android, it is enough to make an ordinary http request, in which you can transfer 1000 tokens at once. Of course, in parallel, you can make several requests. And if this seems too slow, you can use the Cloud Connection Server (XMPP) .

In the comments, we would be interested to know how you solve the problem of push notifications in your applications.

one;

Source: https://habr.com/ru/post/245087/


All Articles