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

このコードは、少数のプッシュ メッセージを送信する場合に問題なく機能します。

単純なバイナリ形式のOLD QUESTION だから、ずっと前にプッシュ通知を統合しましたが、500人未満に送信されたメッセージでは問題なく機能していました。現在、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.

また、無効なトークン (プッシュ通知を希望したがアプリケーションを削除したユーザー) を検出するフィードバック サービスも実行していますが、これは正常に機能します。その 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 のバージョンによって異なります) を返します。それが起こったとき、あなたはそれを管理しなければなりません。次の 2 つの可能性があります。

  • 操作全体を破棄する
  • 最後の書き込み操作を再試行してください

2 番目のオプションを選択した場合fwrite($fp, $msg)は、失敗した操作と同じ $fp と $msg を使用して新しい操作を行う必要がありfwrite()ます。パラメータを変更すると、1409F07F:SSLエラーが返されます

また、fwrite が「数バイト」しか書き込みに失敗する場合もあります。このような場合でも、戻り値と $msg の長さを比較して対処する必要があります。この場合、メッセージの残りの部分を送信する必要がありますが、場合によっては、メッセージ全体を再度送信する必要があります (このリンクによると)。

fwrite リファレンスとコメントをご覧ください:リンク

于 2013-08-28T13:14:38.117 に答える
3

私は PHP を知らないので、実際の PHP コードを提供することはできませんが、(Apple によると) 使用すべきロジックは次のとおりです。

プッシュ通知のスループットとエラー チェック

スループットが 1 秒あたり 9,000 件未満の通知である場合、サーバーは改善されたエラー処理ロジックの恩恵を受ける可能性があります。

拡張バイナリ インターフェイスを使用する場合のエラーを確認する方法は次のとおりです。書き込みが失敗するまで書き込みを続けます。ストリームの書き込み準備が整ったら、通知を再送信して続行します。ストリームが書き込みの準備ができていない場合は、ストリームが読み取り可能かどうかを確認してください。

存在する場合は、ストリームから利用可能なすべてを読み取ります。0 バイトが返された場合は、無効なコマンド バイトやその他の解析エラーなどのエラーが原因で、接続が閉じられています。6 バイトが返された場合、それはエラー応答であり、応答コードと、エラーの原因となった通知の ID を確認できます。その後、すべての通知を再度送信する必要があります。

すべてが送信されたら、エラー応答の最後のチェックを行います。

通常の遅延のため、切断された接続が APNs からサーバーに戻るまでに時間がかかる場合があります。接続が切断されたために書き込みが失敗する前に、500 を超える通知を送信することができます。約 1,700 件の通知の書き込みが、パイプがいっぱいになったという理由だけで失敗する可能性があるため、その場合は、ストリームが再び書き込み可能になったら再試行してください。

さて、ここでトレードオフが興味深いものになります。書き込みのたびにエラー応答を確認でき、すぐにエラーをキャッチできます。ただし、これにより、通知のバッチを送信するのにかかる時間が大幅に増加します。

デバイス トークンを正しく取得し、正しい環境に送信している場合、デバイス トークンはほとんどすべて有効です。したがって、失敗がめったにないと仮定して最適化することは理にかなっています。エラー応答をチェックする前に、書き込みが失敗するか、バッチが完了するのを待つと、ドロップされた通知を再度送信する時間を数えても、パフォーマンスが大幅に向上します。

これは、APN に固有のものではなく、ほとんどのソケットレベルのプログラミングに適用されます。

選択した開発ツールが複数のスレッドまたはプロセス間通信をサポートしている場合、エラー応答を常に待機しているスレッドまたはプロセスを保持し、メインの送信スレッドまたはプロセスに、いつ中断して再試行する必要があるかを知らせることができます。

これは、Apple のテクニカル ノート:プッシュ通知のトラブルシューティングから抜粋したものです。

編集

書き込みが失敗したことを 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 のアルゴリズムを無効にし、通知を 1 つずつゆっくりと送信し、各メッセージの後にエラー応答を読み取ろうとすることで、DB 内のすべての無効なトークンを見つけて削除できます。DB がクリーンであることがわかったら、Nagle のアルゴリズムを有効にして、Apple からのエラー応答をわざわざ読まなくても、通知の送信をすぐに再開できます。そうすれば、メッセージをソケットに書き込んでいるときにエラーが発生するたびに、単純に新しいソケットを作成して、最後のメッセージだけを送信し直すことができます。

于 2013-08-28T14:43:17.253 に答える
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 msec
    }
    

    成功した場合は、error_reporting を使用してエラーを無効にするだけです。@ 演算子は使用しないでください。

  • stream_set_write_buffer()を 0 に設定する
于 2013-08-28T15:11:03.393 に答える