APNS SSL操作失败,代码为1

时间:2013-08-22 10:57:20

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

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

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

<?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技能不是很好。

5 个答案:

答案 0 :(得分:4)

当你这样做时:

fwrite($fp, $msg);

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

  • 放弃整个操作
  • 再次尝试上次写入操作

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

此外,有些情况下fwrite只能写入“some bytes”,你应该管理这种情况,将返回值与$ msg的长度进行比较。 在这种情况下,您应该发送消息的剩余部分,但在某些情况下,您必须再次发送整个消息(根据this link)。

查看fwrite参考和评论:Link

答案 1 :(得分:3)

我无法为您提供实际的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读取错误响应(在这种情况下您确切知道要重新发送的内容),否则您只会重新发送最后一个通知,即使该通知恰好是带有无效令牌的通知,您也可以# 39; ll在发送更多通知后可能会收到下一个错误(这很不幸,因为如果你立即得到失败,那么检测无效令牌就会容易得多。)

如果你保持你的数据库干净(即只存储由Apple发送到你的应用程序的设备令牌,并且它们都属于相同的推送环境 - 沙盒或生产),你不应该遇到任何无效的设备令牌。

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

<强> EDIT2:

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

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

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

答案 2 :(得分:1)

我的解决方案(对于现在的半旧问题)是我的数据库中有一些开发环境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]

答案 3 :(得分:1)

解决方案是:

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

答案 4 :(得分:0)

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

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缓冲区仍然在写,你可以检查缓冲区是否写入,重试或限制缓冲区就足够了。

重复:

其他(编辑)

上面我试着说你需要找出一种方法来确定当你尝试再次写入然后写入时套接字是否写入。

如果没办法,请尝试:

  • 禁用非阻止块
  • 写作

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

    如果成功,只需通过error_reporting禁用错误,永远不要使用@ operator。

  • stream_set_write_buffer()设为0