Ошибка операции APNS SSL с кодом 1

ИЗМЕНИТЬ - Использование расширенного двоичного формата

Оказалось, что я не использовал расширенный двоичный формат, поэтому я изменил свой код.

<?php

$message = $_POST['message'];
$passphrase = $_POST['pass'];

//Connect to db


if ($db_found) {

// Create the payload body
$body['aps'] = array(
    'alert' => $message,
    'sound' => 'default'
);

$streamContext = stream_context_create();
stream_context_set_option($streamContext, 'ssl', 'local_cert', 'x.pem');
stream_context_set_option($streamContext, 'ssl', 'passphrase', $passphrase);

$fp = stream_socket_client('ssl://gateway.push.apple.com:2195', $error, $errorString, 15, STREAM_CLIENT_CONNECT, $streamContext);
stream_set_blocking ($fp, 0); 

if (!$fp)
    exit("Failed to connect: $err $errstr" . PHP_EOL);

echo 'Connected to APNS for Push Notification' . PHP_EOL;

// Keep push alive (waiting for delivery) for 90 days
$apple_expiry = time() + (90 * 24 * 60 * 60);



$tokenResult = //SQL QUERY TO GET TOKENS

while($row = mysql_fetch_array($tokenResult)) {
    $apple_identifier = $row["id"];
    $deviceToken = $row['device_id'];
    $payload = json_encode($body);

    // Enhanced Notification
    $msg = pack("C", 1) . pack("N", $apple_identifier) . pack("N", $apple_expiry) . pack("n", 32) . pack('H*', str_replace(' ', '', $deviceToken)) . pack("n", strlen($payload)) . $payload; 

    // SEND PUSH
    fwrite($fp, $msg);

    // We can check if an error has been returned while we are sending, but we also need to 
    // check once more after we are done sending in case there was a delay with error response.
    checkAppleErrorResponse($fp); 
}

// Workaround to check if there were any errors during the last seconds of sending.
// Pause for half a second. 
// Note I tested this with up to a 5 minute pause, and the error message was still available to be retrieved
usleep(500000); 

checkAppleErrorResponse($fp);

echo 'Completed';

fclose($fp);


// SIMPLE BINARY FORMAT
/*for($i = 0; $i<count($deviceToken); $i++) {

    // Encode the payload as JSON
    $payload = json_encode($body);

    // Build the binary notification
    $msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken[$i]) . pack('n', strlen($payload)) . $payload;

    // Send it to the server
    $result = fwrite($fp, $msg, strlen($msg));

    $bodyError .= 'result: '.$result.', devicetoken: '.$deviceToken[$i].'';

    if (!$result) {
        $errCounter = $errCounter + 1;
        echo 'Message not delivered' . PHP_EOL;
    }
    else
        echo 'Message successfully delivered' . PHP_EOL;
}*/


// Close the connection to the server
//fclose($fp);


//Insert message into database

mysql_close($db_handle);

}

else {

    print "Database niet gevonden ";
    mysql_close($db_handle);
}

// FUNCTION to check if there is an error response from Apple
// Returns TRUE if there was and FALSE if there was not
function checkAppleErrorResponse($fp) {

//byte1=always 8, byte2=StatusCode, bytes3,4,5,6=identifier(rowID). 
// Should return nothing if OK.

//NOTE: Make sure you set stream_set_blocking($fp, 0) or else fread will pause your script and wait 
// forever when there is no response to be sent. 

$apple_error_response = fread($fp, 6);

if ($apple_error_response) {

    // unpack the error response (first byte 'command" should always be 8)
    $error_response = unpack('Ccommand/Cstatus_code/Nidentifier', $apple_error_response); 

    if ($error_response['status_code'] == '0') {
    $error_response['status_code'] = '0-No errors encountered';

    } else if ($error_response['status_code'] == '1') {
    $error_response['status_code'] = '1-Processing error';

    } else if ($error_response['status_code'] == '2') {
    $error_response['status_code'] = '2-Missing device token';

    } else if ($error_response['status_code'] == '3') {
    $error_response['status_code'] = '3-Missing topic';

    } else if ($error_response['status_code'] == '4') {
    $error_response['status_code'] = '4-Missing payload';

    } else if ($error_response['status_code'] == '5') {
    $error_response['status_code'] = '5-Invalid token size';

    } else if ($error_response['status_code'] == '6') {
    $error_response['status_code'] = '6-Invalid topic size';

    } else if ($error_response['status_code'] == '7') {
    $error_response['status_code'] = '7-Invalid payload size';

    } else if ($error_response['status_code'] == '8') {
    $error_response['status_code'] = '8-Invalid token';

    } else if ($error_response['status_code'] == '255') {
    $error_response['status_code'] = '255-None (unknown)';

    } else {
    $error_response['status_code'] = $error_response['status_code'].'-Not listed';

    }

    echo '<br><b>+ + + + + + ERROR</b> Response Command:<b>' . $error_response['command'] . '</b>&nbsp;&nbsp;&nbsp;Identifier:<b>' . $error_response['identifier'] . '</b>&nbsp;&nbsp;&nbsp;Status:<b>' . $error_response['status_code'] . '</b><br>';

    echo 'Identifier is the rowID (index) in the database that caused the problem, and Apple will disconnect you from server. To continue sending Push Notifications, just start at the next rowID after this Identifier.<br>';

    return true;
}

return false;
}

?>

При использовании этого нового кода я все еще не могу отправить более 300 сообщений из-за этой ошибки:

Warning: fwrite() [function.fwrite]: SSL operation failed with code 1. OpenSSL Error messages: error:1409F07F:SSL routines:SSL3_WRITE_PENDING:bad write retry in PATH_TO_SCRIPT.php on line NUMBER

этот код отлично работает при отправке всего нескольких push-сообщений.

СТАРЫЙ ВОПРОС в простом двоичном формате. Я уже давно интегрировал Push-уведомления, и они отлично работают с сообщениями, отправленными менее чем 500 людям. Теперь я пытаюсь отправить push-уведомление более чем 1000 человек, но получаю ошибку.

Warning: fwrite() [function.fwrite]: SSL: Broken pipe in PATH_TO.PHP on line x

Я читал документы Apple и знаю, что недействительные токены могут привести к отключению сокета. Некоторые онлайн-решения рекомендуют обнаружение отключений и повторное подключение, например:

Your server needs to detect disconnections and reconnect if necessary. Nothing is
"instant" when networking is involved; there's always some latency and code needs to take
that into account. Also, consider using the enhanced binary interface so you can check the
return response and know why the connection was dropped. The connection can also be
dropped as a result of TCP keep-alive, which is outside of Apple's control.

Я также использую службу обратной связи, которая обнаруживает недействительные токены (пользователи, которые хотели push-уведомления, но удалили приложение), и это отлично работает. Этот скрипт php повторяет удаленные идентификаторы, и я могу подтвердить, что эти токены удалены из нашей базы данных MySQL.

Как я могу обнаружить отключение или сломанный канал и отреагировать на это, чтобы мои push-уведомления могли достигнуть более 1000 человек?

В настоящее время я использую этот простой скрипт push.php.

<?php

 $message = $_POST['message'];
 $passphrase = $_POST['pass'];

 //Connect to database stuff

 if ($db_found) {
      $streamContext = stream_context_create();
      stream_context_set_option($streamContext, 'ssl', 'local_cert', 'x.pem');
      stream_context_set_option($streamContext, 'ssl', 'passphrase', $passphrase);

      $fp = stream_socket_client('ssl://gateway.push.apple.com:2195', $error, $errorString, 15, STREAM_CLIENT_CONNECT, $streamContext);

 if (!$fp)
    exit("Failed to connect: $err $errstr" . PHP_EOL);

 echo 'Connected to APNS for Push Notification' . PHP_EOL;

 $deviceToken[] = //GET ALL TOKENS FROM DATABASE AND STORE IN ARRAY

for($i = 0; $i<count($deviceToken); $i++) {
    // Create the payload body
    $body['aps'] = array(
    'alert' => $message,
    'sound' => 'default'
    );

    // Encode the payload as JSON
    $payload = json_encode($body);

    // Build the binary notification
    $msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken[$i]) . pack('n', strlen($payload)) . $payload;

    // Send it to the server
    $result = fwrite($fp, $msg, strlen($msg));

    $bodyError .= 'result: '.$result.', devicetoken: '.$deviceToken[$i].'';

    if (!$result) {
        $errCounter = $errCounter + 1;
        echo 'Message not delivered' . PHP_EOL;
    }
    else
        echo 'Message successfully delivered' . PHP_EOL;
}


echo $bodyError;

// Close the connection to the server
fclose($fp);


//CODE TO SAVE MESSAGE TO DATABSE HERE

if (!mysql_query($SQL,$db_handle)) { 
    die('Error: ' . mysql_error()); 
}

}
 else {
     print "Database niet gevonden ";
     mysql_close($db_handle);
 }


 ?>

Также fwrite возвращает 0 записанных байтов при возникновении ошибки SLL Broken Pipe.

Я также должен упомянуть, что я не PHP или не веб-разработчик, а разработчик приложений, поэтому мои навыки php не так хороши.


person Mark Molina    schedule 22.08.2013    source источник
comment
Вы используете простой двоичный формат, который не возвращает ответов. Вы должны использовать расширенный формат, чтобы получать ответы об ошибках. Дополнительные сведения см. В этом ответе.   -  person Eran    schedule 22.08.2013
comment
Спасибо за это. Сейчас я использую расширенный формат, и он возвращает эту ошибку при отправке большого количества сообщений (а не при отправке всего нескольких сообщений): Предупреждение: fwrite () [function.fwrite]: операция SSL завершилась неудачно с кодом 1. Ошибка OpenSSL сообщения: ошибка: 1409F07F: подпрограммы SSL: SSL3_WRITE_PENDING: неправильная попытка записи в PATH_TO_PHP_FILE_ON_LINE   -  person Mark Molina    schedule 28.08.2013
comment
Посмотрите на этот вопрос, это поможет?   -  person LombaX    schedule 28.08.2013
comment
По сути, это говорит о том, что при отправке у вас может быть ошибка записи. В этом случае вы должны выполнить ту же операцию записи с теми же параметрами, иначе вы получите ошибку 1409F07F. До этой ошибки у вас что-то повторяется функцией checkAppleErrorResponse($fp);? Я вижу, что вы вызываете его внутри цикла, но не проверяете возвращаемое значение. Если эта функция выводит ошибку, вы должны обработать ее и снова выполнить операцию fwrite, прежде чем переходить к следующему элементу.   -  person LombaX    schedule 28.08.2013
comment
Или, лучше, проверьте количество байтов, записанных функцией fwrite. Если это 0, значит, произошла ошибка, и вы должны снова выполнить операцию записи.   -  person LombaX    schedule 28.08.2013


Ответы (5)


Когда вы это сделаете:

fwrite($fp, $msg);

вы пытаетесь писать в сокет. Если что-то пойдет не так, fwrite вернет false или 0 (в зависимости от версии php) в качестве возвращаемого значения. Когда это произойдет, вы должны управлять этим. У вас есть две возможности:

  • отказаться от всей операции
  • попробуйте еще раз последнюю операцию записи

если вы выберете второй вариант, вы должны будете сделать новый fwrite($fp, $msg) с ОДИНАКОВЫМИ $ fp и $ msg неудачной fwrite() операции. При изменении параметров возвращается ошибка 1409F07F:SSL.

Более того, бывают ситуации, когда fwrite не может записать только «несколько байтов», вы должны справиться даже с этой ситуацией, сравнивая возвращаемое значение с длиной $ msg. В этом случае вы должны отправить оставшуюся часть сообщения, но в некоторых ситуациях вам нужно отправить все сообщение снова (согласно эта ссылка).

Взгляните на ссылку fwrite и комментарии: Ссылка

person LombaX    schedule 28.08.2013
comment
Итак, в основном мне нужно проверить длину $ msg с помощью strlen и вывода fwrite и сравнить их. Если они не совпадают, то все ли записывать снова с теми же значениями? Но что, если токены недействительны или что-то в этом роде. Не застревает ли скрипт в этом бесконечном цикле? - person Mark Molina; 29.08.2013
comment
Примерно так, да. Чтобы избежать бесконечных циклов, вы должны установить максимальное количество повторных попыток. Более того, я предлагаю вам немного подождать (спать?) Перед повторной попыткой: если ошибка связана с переполнением буфера, вы дадите ему возможность освободить место. Что касается повторных попыток, теоретически вы должны повторить попытку: со всем сообщением, если байты равны 0, или с оставшейся частью сообщения, если байты больше 0, но меньше общей длины. Однако кажется, что в некоторых случаях (в зависимости от стороны сервера) вам нужно отправить все сообщение даже в этом последнем случае. Попробуйте - person LombaX; 29.08.2013
comment
Какое будет хорошее количество повторных попыток и время сна? 5х и спать 3с? - person Mark Molina; 29.08.2013
comment
Это действительно зависит от соединения, количества ошибок, с которыми вы сталкиваетесь ... эти цифры кажутся разумными, но, возможно, я попробую с 1 с. - person LombaX; 29.08.2013

Я не могу дать вам настоящий PHP-код, так как я не знаю PHP, но вот логика, которую вы должны использовать (согласно Apple):

Пропускная способность push-уведомлений и проверка ошибок

Если вы видите пропускную способность ниже 9000 уведомлений в секунду, ваш сервер может выиграть от улучшенной логики обработки ошибок.

Вот как можно проверить наличие ошибок при использовании расширенного двоичного интерфейса. Продолжайте писать, пока запись не закончится. Если поток снова готов к записи, повторно отправьте уведомление и продолжайте. Если поток не готов к записи, посмотрите, доступен ли поток для чтения.

Если это так, прочтите все, что доступно в потоке. Если вы получаете нулевые байты обратно, соединение было закрыто из-за ошибки, такой как недопустимый командный байт или другая ошибка синтаксического анализа. Если вы получили шесть байтов назад, это ответ об ошибке, который вы можете проверить на предмет кода ответа и идентификатора уведомления, вызвавшего ошибку. Вам нужно будет снова отправлять каждое последующее уведомление.

Как только все будет отправлено, сделайте последнюю проверку на наличие ошибки.

Разорванное соединение может занять некоторое время, чтобы вернуться от APN к вашему серверу только из-за нормальной задержки. Можно отправить более 500 уведомлений, прежде чем запись не удастся из-за разрыва соединения. Около 1700 записей уведомлений могут завершиться ошибкой только потому, что канал заполнен, поэтому просто повторите попытку в этом случае, когда поток снова будет готов к записи.

Теперь вот где компромиссы становятся интересными. Вы можете проверять ответ об ошибке после каждой записи и сразу же обнаружите ошибку. Но это приводит к значительному увеличению времени, необходимого для отправки пакета уведомлений.

Почти все токены устройств должны быть действительными, если вы правильно их записали и отправляете в правильную среду. Поэтому имеет смысл оптимизировать, предполагая, что сбои будут редкими. Вы получите гораздо лучшую производительность, если дождетесь сбоя записи или завершения пакета, прежде чем проверять ответ об ошибке, даже если подсчитаете время для повторной отправки отброшенных уведомлений.

Ничто из этого не специфично для APN, это применимо к большинству программирования на уровне сокетов.

Если выбранный вами инструмент разработки поддерживает несколько потоков или межпроцессное взаимодействие, у вас может быть поток или процесс, постоянно ожидающий ответа об ошибке, и пусть основной поток или процесс-отправитель знает, когда он должен отказаться и повторить попытку.

Это взято из технической заметки Apple: Устранение неполадок с push-уведомлениями.

ИЗМЕНИТЬ

Я не знаю, как вы обнаруживаете в PHP, что запись не удалась, но когда это произойдет, вы должны попытаться написать уведомление о сбое еще раз, и если это снова не удастся, попробуйте прочитать ответ об ошибке и закрыть соединение.

Если вам удастся прочитать ответ об ошибке, вы узнаете, какое уведомление не удалось, и тип ошибки (наиболее вероятная ошибка - 8 - недопустимый токен устройства). Если после написания 100 сообщений вы получите ответ об ошибке для 80-го сообщения, вы должны повторно отправить сообщения с 81 по 100, поскольку Apple их никогда не получала. В моем случае (сервер Java) мне не всегда удается прочитать ответ об ошибке (иногда я получаю сообщение об ошибке при попытке прочитать ответ из сокета). В этом случае я могу только перейти к отправке следующих уведомлений (и не могу узнать, какие уведомления были фактически получены Apple). Вот почему так важно очищать вашу базу данных от недействительных токенов.

В любом случае, вы не должны зацикливаться на бесконечном цикле, поскольку при получении ошибки после отправки N уведомлений вы не собираетесь повторно отправлять эти N уведомлений. Если вам не удастся прочитать ответ об ошибке от Apple (в этом случае вы точно знаете, что отправить повторно), вы повторно отправите только последнее уведомление, и даже если это уведомление окажется тем, которое содержит недопустимый токен, вы, вероятно, получить следующую ошибку после отправки дополнительных уведомлений (что прискорбно, поскольку было бы намного проще обнаружить недействительные токены, если бы вы сразу получили сбой).

Если вы держите свою базу данных в чистоте (т.е. храните в ней только токены устройств, которые были отправлены в ваше приложение Apple, и все они принадлежат одной и той же среде push - либо песочнице, либо производственной среде), вы не должны столкнуться с недопустимыми токенами устройств.

Токены устройства, возвращенные службой обратной связи, не являются недействительными токенами. Это действительные токены устройств, с которых было удалено ваше приложение. Недействительные токены никогда не были действительными для текущей среды push и никогда не будут действительны. Единственный способ определить недействительные токены - прочитать ответы об ошибках от Apple.

РЕДАКТИРОВАТЬ2:

Я забыл упомянуть об этом раньше. Я столкнулся с аналогичной проблемой при реализации серверной части push-уведомлений на Java. Я не мог надежно получить все ответы об ошибках, возвращенные Apple.

Я обнаружил, что в Java есть способ отключить алгоритм TCP Nagle, который вызывает буферизацию нескольких сообщений перед их пакетной отправкой в ​​Apple. Хотя Apple рекомендует нам использовать алгоритм Нэгла (по соображениям производительности), я обнаружил, что когда я отключаю его, а затем пытаюсь прочитать ответ Apple после каждого отправляемого им сообщения, мне удается получить 100% ответов об ошибках (я проверил это, написав процесс, имитирующий сервер APNS).

Отключив алгоритм Нэгла и посылая уведомления одно за другим, медленно и пытаясь прочитать ответ об ошибке после каждого сообщения, вы можете найти все недопустимые токены в своей БД и удалить их. Как только вы узнаете, что ваша БД чиста, вы можете включить алгоритм Нэгла и быстро возобновить отправку уведомлений, не утруждая себя чтением сообщений об ошибках от Apple. Затем, когда вы получаете сообщение об ошибке при записи сообщения в сокет, вы можете просто создать новый сокет и повторить попытку отправки только последнего сообщения.

person Eran    schedule 28.08.2013
comment
Спасибо за информацию. Однако моя проблема в том, что я знаю, что идет не так, и всю теорию по этому поводу, но просто не знаю PHP. Существует так много примеров apns с php, но все они не могут отправлять большое количество уведомлений, потому что ни один из них не проверяет наличие ошибок. - person Mark Molina; 29.08.2013
comment
Кстати, я не застрял на php или чем-то еще, просто подумал, что это легко реализовать. Если есть еще одна простая (и бесплатная) альтернатива, которая может это исправить, я тоже буду счастлив. - person Mark Molina; 29.08.2013

Мое решение (на теперь уже наполовину старый вопрос) заключалось в том, что у меня были некоторые токены APN среды разработки в моей базе данных, которые пытались отправить в производственную среду. Как только я избавился от них из своей базы данных, все остальное заработало нормально. К сожалению, из 7000+ APN я не был уверен, какие токены были плохими, поэтому мне пришлось стереть их все в надежде, что новые токены будут созданы, когда пользователь повторно откроет приложение. Все идет нормально.

Apple прекратит все немедленные попытки отправки push-уведомления, если обнаружит ошибочный токен APN.

У меня появилось то же самое сообщение, которое я никогда раньше не видел (ниже) в различных приложениях, поэтому я рад, что смог его решить.

Warning: fwrite() [function.fwrite]: SSL operation failed with code 1. OpenSSL Error messages: error:1409F07F:SSL routines:SSL3_WRITE_PENDING:bad write retry in PATH_TO_SCRIPT.php on line [NUMBER]

person roycable    schedule 07.04.2014

Решение такое:

$msg = chr(0) . pack('n', 32) . pack('H*', $deviceToken) . pack('n', strlen($payload)) . $payload;
try {
    $result = fwrite($fp, $msg, strlen($msg));
} catch (Exception $ex) {
    sleep(1); //sleep for 5 seconds
    $result = fwrite($fp, $msg, strlen($msg));
}
person Pargunan    schedule 29.04.2016

Погуглил, нашел кое-что интересное

http://rt.openssl.org/Ticket/Display.html?id=598&user=guest&pass=guest

Как говорится в комментарии к патчу

сначала проверьте, записывается ли еще SSL3_BUFFER. Это произойдет с неблокирующим вводом-выводом

Ответ Почему я получаю ошибку: 1409F07F: Подпрограммы SSL: SSL3_WRITE_PENDING: ошибка неправильной попытки записи при попытке SSL_write? говорит:

SSL_Write возвращается с SSL_ERROR_WANT_WRITE или SSL_ERROR_WANT_READ, вам нужно повторить вызов SSL_write с теми же параметрами еще раз после выполнения условия.

Возможно, буфер ssl все еще пишет, когда вы пытаетесь писать, вы можете проверить, не записывает ли буфер, повторить попытку или ограничить буфер.

Дубликаты:

Дополнительно (изменить)

Выше я пытаюсь сказать, что вам нужно выяснить способ определить, не пишет ли сокет, когда вы снова пытаетесь написать, а затем писать.

Если нет способа сделать это, попробуйте:

  • Отключение неблокирующего блока
  • Рерти писать

    while(!fwrite($fp, $msg)) {
        usleep(400000); //400 msec
    }
    

    в случае успеха просто отключите ошибки с помощью error_reporting, никогда не используйте оператор @.

  • Установка для stream_set_write_buffer () значения 0
person Felipe Buccioni    schedule 28.08.2013
comment
Эта вторая ссылка на самом деле не имеет для меня смысла, но кажется простой. Что такое $ buffer? Я снова терплю неудачу @ php - person Mark Molina; 29.08.2013
comment
Мне сейчас слишком сложно объяснить концепцию буфера, и с моим уровнем английского, может быть, википедия или что-то подобное могут помочь вам в этом лучше, посмотрите правки, я немного прочитал об этом. - person Felipe Buccioni; 30.08.2013