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 Broken Pipe 错误时,fwrite 也会返回 0 个写入字节。

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

4

5 回答 5

4

当你这样做时:

fwrite($fp, $msg);

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

  • 丢弃整个操作
  • 再试一次最后的写操作

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

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

看看 fwrite 参考和评论:链接

于 2013-08-28T13:14:38.117 回答
3

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

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

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

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

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

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

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

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

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

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

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

这取自 Apple 的技术说明:故障排除推送通知

编辑

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

于 2013-08-28T14:43:17.253 回答
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]

于 2014-04-07T11:16:12.550 回答
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));
}
于 2016-04-29T13:49:53.753 回答
0

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

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

正如补丁评论所说

首先检查是否仍有 SSL3_BUFFER 被写出。这将发生在非阻塞 IO 上

为什么我在尝试 SSL_write 时收到“错误:1409F07F:SSL 例程:SSL3_WRITE_PENDING:错误的写入重试”错误的答案?说:

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

当您尝试写入时,可能ssl缓冲区仍在写入,您可以检查缓冲区是否没有写入,重试或限制缓冲区就足够了。

重复:

附加(编辑)

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

如果没有办法,请尝试:

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

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

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

  • stream_set_write_buffer()设置为 0
于 2013-08-28T15:11:03.393 回答