APNS SSL操作失败,错误代码为1。

10

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

事实证明我没有使用增强二进制格式,因此我修改了我的代码。

<?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断开连接错误发生时,fwrite返回0个写入字节。

我必须提到,我不是PHP或Web开发者,而是应用程序开发者,因此我的PHP技能并不那么好。


您正在使用简单的二进制格式,该格式不会返回响应。为了获得错误响应,您应该使用增强格式。有关更多详细信息,请参见此答案:http://stackoverflow.com/questions/17724614/using-push-notification-usingf-api/17726433#17726433。 - Eran
谢谢。我现在正在使用增强格式,当发送大量消息时(而不是发送少量消息时),它返回此错误:警告:fwrite() [function.fwrite]:SSL操作失败,代码为1。 OpenSSL错误消息:error:1409F07F:SSL例程:SSL3_WRITE_PENDING:PATH_TO_PHP_FILE_ON_LINE中的错误重试写入失败。 - Mark Molina
看看这个问题,它有帮助吗? - LombaX
基本上它说,当你发送时,可能会出现写入错误。在这种情况下,您必须使用相同的参数执行相同的写操作,否则您将收到错误代码1409F07F。在此错误之前,checkAppleErrorResponse($fp);函数是否有任何回显?我看到您在循环内调用它,但您没有检查其返回值。如果该函数打印错误,则应处理它并再次进行fwrite,然后再转到下一个项目。 - LombaX
或者更好的方法是检查fwrite函数写入的字节数。如果为0,则表示出现错误,必须重新执行写操作。 - LombaX
5个回答

4
当您进行以下操作时:
fwrite($fp, $msg);

您正在尝试向套接字写入数据。如果出现问题,fwrite将返回false或0(根据PHP版本),此时您需要处理它。 您有两个选择:

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

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

此外,有些情况下,fwrite只能成功写入“一些字节”,您还应该处理这种情况,将返回的值与$msg的长度进行比较。 在这种情况下,您应该发送消息的剩余部分,但在某些情况下,您必须重新发送整个消息(根据此链接)。

请查看fwrite参考和注释:链接


所以基本上我需要使用strlen检查$msg的长度,然后比较fwrite的输出,并进行比较。如果它们不同,那么用相同的值再次执行整个写操作?但是,如果令牌无效或类似的情况怎么办。难道这个脚本不会陷入无限循环吗? - Mark Molina
是的,类似这样。为了避免无限循环,您必须设置最大重试次数。此外,我建议您在重试之前等待一段时间(睡眠?):如果错误是因为缓冲区已满,您将有机会释放一些空间。关于重试,在理论上,如果字节为0,则应重试整个消息;如果字节数大于0但小于总长度,则应重试剩余部分的消息。然而,似乎在某些情况下(取决于服务器端),即使在最后一种情况下,您也必须发送整个消息。试试看。 - LombaX
五次重试和每次三秒的休眠时间是否合适? - Mark Molina
这真的取决于连接质量和你面临的错误数量...那个数字看起来合理,但也许我会尝试使用1秒。 - LombaX

3

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

推送通知吞吐量和错误检查
如果您的吞吐量低于每秒9,000个通知,则您的服务器可能会受益于改进的错误处理逻辑。
以下是使用增强二进制接口时如何检查错误。一直写入,直到写入失败为止。如果流再次准备好写入,请重新发送通知并继续进行。如果流不准备好写入,请查看流是否可供读取。
如果是,则从流中读取所有可用内容。如果返回零字节,则连接已关闭,因为出现了错误,例如无效的命令字节或其他解析错误。如果返回六个字节,则是错误响应,您可以检查响应代码和导致错误的通知的ID。您需要重新发送该通知之后的每个通知。
一旦所有内容都已发送,请最后检查是否有错误响应。
由于正常延迟,断开连接需要一段时间才能从APNs返回到您的服务器。在连接断开之前,可能会发送超过500条通知。约1,700个通知写入可能会失败,只是因为管道已满,在流再次准备好写入的情况下重试即可。
现在,这里就出现了权衡。您可以在每次写入后检查错误响应,并立即捕获错误。但这会导致发送一批通知所需的时间大大增加。
如果您正确捕获了设备令牌并将其发送到正确的环境,则几乎所有设备令牌都应该有效。因此,假定故障很少发生是优化的合理选择。即使计算发送丢失通知所需的时间,等待写入失败或批处理完成后再检查错误响应,性能仍会得到大幅提升。
这些都与APNs无关,适用于大多数套接字级编程。
如果您选择的开发工具支持多个线程或进程间通信,则可以有一个线程或进程始终等待错误响应,并让主要发送线程或进程知道何时放弃并重试。
这是来自苹果技术笔记的内容:推送通知故障排除
编辑:
我不知道你如何在PHP中检测写入失败,但当它失败时,你应该尝试再次写入失败的通知,如果再次失败,请尝试读取错误响应并关闭连接。
如果您成功读取错误响应,您将知道哪个通知失败了,以及错误类型(最可能的错误是8-无效的设备令牌)。如果在写入100条消息后,您收到第80条消息的错误响应,则必须重新发送第81至100条消息,因为Apple从未收到它们。在我的情况下(Java服务器),我并不总是能够读取错误响应(有时我尝试从套接字读取响应时会出错)。在这种情况下,我只能继续发送下一个通知(并且无法知道哪些通知实际上已被Apple接收)。这就是为什么保持数据库中的无效令牌很重要。
无论如何,您不应该陷入无限循环,因为在发送N个通知后出现错误时,您不会重新发送这些N个通知。除非您成功读取了来自Apple的错误响应(在这种情况下,您将确切地知道要重新发送什么),否则您只会重新发送最后一个通知,即使该通知恰好是具有无效标记的通知,您可能会在发送更多通知后遇到下一个错误(这很不幸,因为如果您能立即获取失败信息,检测无效标记将变得更加容易)。
如果您保持数据库干净(即仅存储由Apple发送给您的应用程序的设备标记,并且所有这些标记都属于相同的推送环境-无论是沙盒还是生产环境),则不应遇到任何无效设备标记。
反馈服务返回的设备标记不是无效标记。它们是卸载您的应用程序的设备的有效标记。无效标记从未对当前推送环境有效,并且永远不会。唯一确定无效标记的方法是阅读来自Apple的错误响应。
编辑2:我忘了之前提到它。当我在Java中实现推送通知服务器端时,我遇到了与您类似的问题。我无法可靠地获取Apple返回的所有错误响应。
我发现在Java中有一种方法可以禁用TCP Nagle算法,这会导致在向Apple发送消息之前缓冲多个消息并将它们批量发送。虽然Apple鼓励我们使用Nagle算法(出于性能原因),但当我禁用它并尝试在每条消息发送后读取来自Apple的响应时,我设法收到了100%的错误响应(通过编写模拟APNS服务器的进程进行验证)。
通过禁用Nagle算法,逐一发送通知,并尝试在每条消息之后读取错误响应,您可以定位DB中的所有无效令牌并将其删除。一旦您知道您的数据库是干净的,您就可以启用Nagle算法,并快速恢复发送通知,而无需打扰阅读来自Apple的错误响应。然后,每当您在向套接字写入消息时遇到错误,只需创建一个新的套接字并重试仅发送最后一条消息即可。

谢谢您的信息。然而我的问题是,我知道出了什么问题,也理解了相关的理论知识,但是我不太懂 PHP。有很多使用 PHP 的 apns 示例,但它们都无法在发送大量通知时进行错误检查,导致发送失败。 - Mark Molina
顺便说一句,我并不固执于使用PHP或其他什么语言,只是觉得它很容易实现。如果有其他简单(且免费)的替代方案可以解决这个问题,我也会很高兴的。 - Mark Molina

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

1
我的解决方案(针对现在已经有点老的问题)是,我在数据库中有一些开发环境的APN令牌,试图发送到生产环境。一旦我从数据库中清除了它们,其余的就可以正常工作了。不幸的是,在7000多个APN中,我不确定哪些令牌是错误的,所以我不得不全部删除,希望当用户重新打开应用程序时会创建新的令牌。目前为止还好。
如果Apple遇到错误的APN令牌,将停止所有立即尝试发送推送通知的操作。
我曾在各种应用程序中看到完全相同的消息出现,而我之前从未见过(如下所示),所以我很高兴我能够解决它。
警告:fwrite()[function.fwrite]:SSL操作失败,代码1。OpenSSL错误消息:error:1409F07F:SSL例程:SSL3_WRITE_PENDING:bad write retry in PATH_TO_SCRIPT.php on line [NUMBER]

0

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

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

正如补丁注释所说:

首先检查是否仍在写入SSL3_BUFFER。这将发生在非阻塞IO中。

为什么我在尝试SSL_write时会收到“error:1409F07F:SSL routines:SSL3_WRITE_PENDING: bad write retry”错误的答案?

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

也许在您尝试写入时ssl缓冲区仍在写入,您可以检查缓冲区是否未写入、重试或限制缓冲区可能已足够。

重复:

附加(编辑)

以上我尝试说您需要找出一种确定套接字在再次尝试写入时是否未写入的方法,然后进行写入。

如果没有办法,请尝试:

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

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

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

  • stream_set_write_buffer() 设置为 0

那个第二个链接对我来说并没有太多意义,但看起来很简单。$buffer 究竟是什么?我又在 PHP 上失败了。 - Mark Molina
缓冲区的概念对我来说现在太难解释了,而且我的英语水平也不够,也许维基百科或类似的东西可以更好地帮助你理解,看看编辑记录,我稍微读了一些相关内容。 - Felipe Buccioni

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接