【问题标题】:APNS SSL operation failed with code 1APNS SSL 操作失败,代码为 1
【发布时间】:2013-08-25 01:42:11
【问题描述】:

编辑 - 使用增强的二进制格式

原来我没有使用增强型二进制格式,所以我更改了代码。

<?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

仅发送几条推送消息时,此代码可以正常工作。

简单二进制格式的旧问题 所以我很久以前就集成了推送通知,它对于发送给少于 500 人的消息效果很好。现在我正在尝试向 1000 多人发送推送通知,但随后我收到了损坏的错误

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

我已经阅读了苹果文档,并且知道无效令牌会导致套接字断开连接。一些在线解决方案建议像这样检测断开连接并重新连接:

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.

我还在运行一个反馈服务,它检测无效令牌(想要推送通知但删除了应用程序的用户)并且工作正常。该 php 脚本回显已删除的 ID,我可以确认这些令牌已从我们的 MySQL 数据库中删除。

我怎样才能检测到断开或损坏的管道并对此做出反应,以便我的推送通知可以覆盖 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);
 }


 ?>

当发生 SLL Broken Pipe 错误时,fwrite 也会返回 0 个写入字节。

我还必须提到,我不是 PHP 或 Web 开发人员,而是一名应用程序开发人员,所以我的 php 技能不是那么好。

【问题讨论】:

  • 您使用的是简单的二进制格式,它不会返回响应。您应该使用增强格式以获得错误响应。有关详细信息,请参阅this answer
  • 谢谢。我现在使用的是增强格式,它在发送大量消息时返回此错误(而不是仅发送几条消息):警告:fwrite() [function.fwrite]:SSL 操作失败,代码为 1。OpenSSL 错误消息:错误:1409F07F:SSL 例程:SSL3_WRITE_PENDING:PATH_TO_PHP_FILE_ON_LINE 中的错误写入重试
  • 看看this question,有用吗?
  • 本质上说,当您发送时,您可能会遇到写入错误。在这种情况下,您必须使用相同的参数进行相同的写入操作,否则您将收到错误 1409F07F。在此错误之前,您是否有 checkAppleErrorResponse($fp); 函数回显的内容?我看到你在循环内调用它,但你没有检查它的返回值。如果该函数打印错误,您应该管理它并再次执行 fwrite,然后再转到下一项
  • 或者,更好的是,检查 fwrite 函数写入的字节数。如果为0,则有错误,必须重新进行写操作

标签: php push-notification apple-push-notifications apns-php


【解决方案1】:

当你这样做时:

fwrite($fp, $msg);

您正在尝试写入套接字。如果出现问题,fwrite 将返回 false 或 0(取决于 php 版本)作为返回值。当它发生时,你必须管理它。 你有两种可能:

  • 放弃整个操作
  • 重试上次的写操作

如果您选择第二个选项,您必须使用相同的 $fp 和 $msg 对失败的 fwrite() 操作执行新的 fwrite($fp, $msg)。 如果更改参数,则返回1409F07F:SSL 错误

此外,在某些情况下 fwrite 仅写入“一些字节”时失败,即使是这种情况,您也应该管理这种情况,将返回的值与 $msg 的长度进行比较。 在这种情况下,您应该发送消息的剩余部分,但在某些情况下,您必须再次发送整个消息(根据this link)。

查看 fwrite 参考和 cmets:Link

【讨论】:

  • 所以基本上我需要用 strlen 检查 $msg 的长度和 fwrite 的输出并比较它们。如果它们不一样,那么用相同的值再次进行整个写入吗?但是如果令牌无效或类似的东西怎么办。脚本不会陷入这个无限循环吗?
  • 这样的,是的。为避免无限循环,您必须设置最大重试次数。此外,我建议您在重试之前等待片刻(睡觉?):如果错误是因为缓冲区已满,您将给它释放一些空间的机会。关于重试,理论上您应该重试:如果字节为 0,则使用整个消息,或者如果字节大于 0,但小于总长度,则使用消息的其余部分。但是,似乎在某些情况下(取决于服务器端),即使在最后一种情况下,您也必须发送整个消息。试一试
  • 重试次数和睡眠时间是多少? 5x 和 3s 睡眠?
  • 这真的取决于连接,你面临的错误数量......这个数字看起来很合理,但也许我会尝试使用 1s。
【解决方案2】:

我不能给你实际的 PHP 代码,因为我不懂 PHP,但这是你应该使用的逻辑(根据 Apple):

推送通知吞吐量和错误检查

如果您发现吞吐量低于每秒 9,000 条通知,您的服务器可能会受益于改进的错误处理逻辑。

以下是使用增强型二进制接口时检查错误的方法。继续写入,直到写入失败。如果流已准备好再次写入,请重新发送通知并继续。如果流尚未准备好写入,请查看流是否可供读取。

如果是,请从流中读取所有可用的内容。如果返回零字节,则连接因错误(例如无效的命令字节或其他解析错误)而关闭。如果您返回六个字节,这是一个错误响应,您可以检查响应代码和导致错误的通知的 ID。您需要再次发送该通知之后的所有通知。

所有内容发送完毕后,最后一次检查错误响应。

由于正常延迟,断开的连接可能需要一段时间才能从 APN 回到您的服务器。由于连接断开,在写入失败之前可以发送超过 500 条通知。大约 1,700 次通知写入可能会因为管道已满而失败,因此在这种情况下,请在流准备好再次写入时重试。

现在,权衡取舍变得有趣了。您可以在每次写入后检查错误响应,并立即发现错误。但这会导致发送一批通知所需的时间大幅增加。

如果您正确捕获了设备令牌并将它们发送到正确的环境,那么它们几乎都应该是有效的。因此,假设故障很少见,进行优化是有意义的。如果您在检查错误响应之前等待写入失败或批处理完成,甚至计算再次发送丢弃的通知的时间,您将获得更好的性能。

这些都不是真正特定于 APN 的,它适用于大多数套接字级编程。

如果您选择的开发工具支持多线程或进程间通信,您可以让一个线程或进程一直等待错误响应,并让主发送线程或进程知道何时应该放弃并重试。

这摘自 Apple 的技术说明:Troubleshooting Push Notifications

编辑

我不知道你是如何在 PHP 中检测到写入失败的,但是当它发生时,你应该再次尝试写入失败的通知,如果再次失败,尝试读取错误响应并关闭连接.

如果您设法阅读错误响应,您将知道哪个通知失败并且您将知道错误类型(最可能的错误是 8 - 无效的设备令牌)。如果在编写 100 条消息后收到第 80 条消息的错误响应,则必须重新发送消息 81 到 100,因为 Apple 从未收到它们。在我的情况下(Java 服务器),我并不总是能够读取错误响应(有时在尝试从套接字读取响应时会出错)。在这种情况下,我只能继续发送下一个通知(并且无法知道 Apple 实际收到了哪些通知)。这就是为什么保持数据库中没有无效令牌很重要的原因。

无论如何,您不应该陷入无限循环,因为在发送 N 个通知后出现错误时,您不会重新发送这 N 个通知。除非您设法从 Apple 读取错误响应(在这种情况下,您确切知道要重新发送什么),否则您只会重新发送最后一个通知,即使该通知恰好是带有无效令牌的通知,您也可能会发送更多通知后出现下一个错误(很不幸,如果您立即收到故障,检测无效令牌会容易得多)。

如果您保持数据库清洁(即仅在其中存储 Apple 发送到您的应用程序的设备令牌,并且所有设备令牌都属于相同的推送环境 - 沙盒或生产环境),您应该不会遇到任何无效的设备令牌。

反馈服务返回的设备令牌不是无效令牌。它们是卸载您的应用程序的设备的有效令牌。无效令牌从未对当前推送环境有效,也永远不会。识别无效令牌的唯一方法是读取 Apple 的错误响应。

EDIT2:

我之前忘了提。在 Java 中实现推送通知服务器端时,我遇到了与您类似的问题。我无法可靠地获得 Apple 返回的所有错误响应。

我发现在 Java 中有一种方法可以禁用 TCP Nagle 算法,该算法会在将多条消息批量发送给 Apple 之前对其进行缓冲。尽管 Apple 鼓励我们使用 Nagle 的算法(出于性能原因),但我发现当我禁用它并在我发送给他们的每条消息后尝试读取 Apple 的响应时,我设法收到 100% 的错误响应(我通过编写一个模拟 APNS 服务器的进程来验证它)。

通过禁用 Nagle 的算法并慢慢发送通知并尝试在每条消息后读取错误响应,您可以找到数据库中的所有无效令牌并将其删除。一旦您知道您的数据库是干净的,您就可以启用 Nagle 的算法并快速恢复发送通知,而无需费心阅读 Apple 的错误响应。然后,每当您在向套接字写入消息时遇到错误时,您只需创建一个新套接字并重试仅发送最后一条消息。

【讨论】:

  • 感谢您的信息。然而,我的问题是我知道出了什么问题以及有关该主题的所有理论,但也不了解 PHP。有很多带有 php 的 apns 示例,但它们都无法发送大量通知,因为它们都没有检查错误。
  • 顺便说一句,我并没有停留在 php 或其他任何东西上,只是认为它很容易实现。如果有另一个简单(免费)的替代方案可以解决这个问题,我也会很高兴。
【解决方案3】:

我的解决方案(针对现在半老的问题)是我的数据库中有一些开发环境 APN 令牌试图发送到生产环境。一旦我从我的数据库中删除了它们,其余的工作就很好了。不幸的是,在 7000 多个 APN 中,我不确定哪些令牌是坏的,所以我不得不将它们全部删除,希望在用户重新打开应用程序时会创建新的令牌。到目前为止一切顺利。

如果遇到错误的 APN 令牌,Apple 将停止所有立即发送推送通知的尝试。

我在各种应用程序上出现了我以前从未见过的完全相同的消息(如下),所以我很高兴我能够解决它。

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]

【讨论】:

    【解决方案4】:

    解决办法是:

    $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));
    }
    

    【讨论】:

      【解决方案5】:

      谷歌搜索我发现了一些有趣的东西

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

      正如补丁评论所说

      首先检查是否有 SSL3_BUFFER 仍在写入 出去。非阻塞 IO 会发生这种情况

      Why am I getting "error:1409F07F:SSL routines:SSL3_WRITE_PENDING: bad write retry" error while attempting an SSL_write? 的回答说:

      SSL_Write 以 SSL_ERROR_WANT_WRITE 或 SSL_ERROR_WANT_READ 返回,您必须在满足条件后再次以相同的参数重复调用 SSL_write。

      可能ssl buffer在你尝试写的时候还在写,你可以检查buffer是否没有写,重试,或者限制buffer就够了。

      重复:

      附加(编辑)

      上面我想说的是,当你再次尝试写入然后写入时,你需要想办法确定套接字是否没有写入。

      如果没有办法,试试:

      • 禁用非阻塞块
      • 重新写入

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

        如果成功,只需通过 error_reporting 禁用错误就不要使用 @ 运算符。

      • stream_set_write_buffer()设置为0

      【讨论】:

      • 第二个链接对我来说真的没有意义,但看起来很简单。 $buffer 到底是什么?我又失败了@php
      • 缓冲区的概念现在对我来说太难解释了,以我的英语水平,也许维基百科或类似的东西可以更好地帮助你,看看编辑,我读了一点位。
      猜你喜欢
      • 2015-04-27
      • 1970-01-01
      • 2021-05-14
      • 1970-01-01
      • 2016-09-07
      • 2015-01-21
      • 2016-12-19
      • 2018-03-07
      • 1970-01-01
      相关资源
      最近更新 更多