web-dev-qa-db-ja.com

PHPを経由してルーティングされた場合、ブラウザはリクエストをキャンセルしません

Apacheが.htaccessを介してPHPスクリプトを介して特定のディレクトリ内のファイルに対するすべてのリクエストをルーティングする機能を備えたサイトがあります。トラブルシューティングの方法がわからないという問題が発生しました。

各ページにビデオがあるオンラインコースがあり、ページはajaxを介してロードされます。 Apacheが動画を直接配信する上記の機能を無効にすると、ユーザーは動画のダウンロード中に[次へ]ボタンを押すことができ、次のページがすぐに読み込まれます。この機能を有効にすると、動画のダウンロードが完了する前に[次へ]ボタンを押すと、ダウンロードが完了するまでブラウザは基本的にハングし、次のページが読み込まれます。私はこれがなぜ起こるかを理解するのに苦労していますが、ブラウザがファイル全体が終了するのを待つようにするPHPを呼び出すことについて何かありますか?

これらは、直接要求の応答ヘッダーです。

HTTP/1.1 206 Partial Content
Date: Fri, 31 Oct 2014 21:28:10 GMT
Server: Apache
Last-Modified: Mon, 20 Oct 2014 15:45:20 GMT
Etag: "1601ee2-c4fcc5-505dc9a1e4fe5"
Accept-Ranges: bytes
Content-Length: 12909765
Vary: User-Agent
Content-Range: bytes 0-12909764/12909765
Keep-Alive: timeout=15, max=142
Connection: Keep-Alive
Content-Type: video/mp4

そして、PHPを経由するリクエストの場合:

HTTP/1.1 206 Partial Content
Date: Fri, 31 Oct 2014 19:11:31 GMT
Server: Apache
P3P: CP="OTI DSP COR CURa ADMa HISa OUR IND STA"
X-Lms: auth
Last-Modified: Mon, 20 Oct 2014 15:45:20 GMT
Etag: "1601ee2-c4fcc5-505dc9a1e4fe5"
Accept-Ranges: bytes
Content-Length: 12909765
Vary: User-Agent
Content-Range: bytes 0-12909764/12909765
Keep-Alive: timeout=15, max=149
Connection: Keep-Alive
Content-Type: video/mp4

PHPのヘッダーのリストに表示されないのは、PHP応答のキャッシュを無効にするデフォルトのヘッダーセットをオーバーライドするために、cache-control、pragma、expiresに空のヘッダーを送信することです、つまり:

header('Cache-Control: ');
header('Pragma: ');
header('Expires: ');

FirefoxとChromeでも同じ動作が見られますが、他のブラウザーではまだテストしていません。 Chromeは特に悪く、タブ全体が応答しなくなります。別のタブに切り替えて再び戻ると、空白の白になります。 Chromeのタスクマネージャーを使用してそのタブのプロセスを強制終了しない限り、タブもブラウザーも閉じることができません。転送が完了すると、再び応答します。

誰がこれを引き起こす可能性があるか知っていますか?サーバーはApache 2.2.26およびPHP 5.4.22を実行しています。 WHMでは、PHP 5ハンドラーとして「dso」が選択されており、suEXECが有効になっています。

編集:

クライアントの中断、またはストリームのタイムアウトをチェックするために、単一読み取りステートメントをループに置き換えましたが、問題は修正されていません。ファイルの実際の送信を処理するPHPコードの部分の下に貼り付けています(304、403、404などの応答とは対照的に)。

(フォーマットについては申し訳ありませんが、なぜ多くの余分な行が追加され、強調表示が途切れたのかわかりません)

// file not cached or cache outdated, we respond '200 OK' (or 206) and
// output the file.
if (!isset($headers['Range'])) {
  $response_code = 200;
}
else {
  $response_code = 206;
}

// send Last-Modified header, and set response code
header('Last-Modified: '.gmdate('D, d M Y H:i:s', $mtime).' GMT',
  true, $response_code);
header('Content-Type: ' . get_mime_type($file));
if ($method == 'GET' || $method == 'HEAD') {
  header('Accept-Ranges: bytes');
}

$filesize = filesize($file);
$start_read_pos = 0;
$max_read_chunk = 65536;
$total_read_size = $filesize;
$content_length = $filesize;

// adjust content length and read position based on the range
if (isset($headers['Range'])) {
  // Range: bytes=1000-1999
  // Range: bytes=2000-
  $range_chunks = explode('=', $headers['Range']);
  // the first element should be "bytes"
  $range = $range_chunks[1]; // e.g. 1024-2048, or 1024-
  $range_chunks = explode('-', $range);
  $start_read_pos = intval($range_chunks[0]);
  $end_read_pos = isset($range_chunks[1]) ? intval($range_chunks[1]) : 0;
  if ($end_read_pos == 0) {
    $end_read_pos = $filesize - 1;
  }
  $total_read_size = $end_read_pos - $start_read_pos + 1;
  $content_length = $total_read_size;
}

header('Content-Length: ' . $content_length);
if (isset($headers['Range'])) {
  header('Content-Range: bytes ' . $start_read_pos . '-' . 
    $end_read_pos . '/' . $filesize);
}

// only output the content if this is not a head request
if ($method != 'HEAD') {

  // read the file data

  if ($ext != 'php') {
    $fh = fopen($file, 'rb');
    if ($fh) {

      if ($start_read_pos != 0) {
        // discard $start_read_pos many bytes
        fread($fh, $start_read_pos);
      }

      $total_sent = 0;
      $read_chunk = $max_read_chunk;
      $timeout = false;
      while(!connection_aborted() &&
            !feof($fh) &&
            !$timeout &&
            $total_sent < $total_read_size) {
        // if the next (i.e. last) read requires fewer 
        // than the max read chunk
        if ($total_sent + $max_read_chunk > $total_read_size) {
          $read_chunk = $total_read_size - $total_sent;
        }
        echo fread($fh, $read_chunk);
        $total_sent += $read_chunk;
        $stream_info = stream_get_meta_data($fh);
        if ($stream_info['timed_out']) {
          $timeout = true;
        }
      }

      fclose($fh);
    }
  }
  else {
    // don't show the PHP code
    // pad to match the content length 
    echo str_pad('PHP code', $content_length);header
  }

  exit();
}
2
Steve

確かに言うのは難しいですが、範囲ヘッダー処理コードはおそらく問題があるところです。 PHPを使用したビデオの配信は、特にサイズが大きい場合、メモリ効率があまり良くない傾向があります。代わりにX-Sendfileなどを使用することをお勧めします。この方法では、PHPで認証チェックを実行しますが、実際のファイルサービングのためにWebサーバーに負荷をかけます。

2
Tim Fountain

ここでの問題は、Apacheの構成に関連しているのではなく、PHPスクリプトに関連していると思われます。たとえば、スクリプトが一度にビデオファイル全体をロードし、それを一度にすべてブラウザに送信すると、この問題が発生する可能性があります。

<?php
header( 'Content-Type: video/mp4' );
$sVideo = @file_get_contents( '/var/www/www.example.com/videos/video.mp4' );
echo( $sVideo );
exit;

ただし、代わりにビデオファイルをロードし、一連の非常に小さなデータの塊でブラウザに送り返す場合、これで問題が解決する可能性があります。

<?php
header( 'Content-Type: video/mp4' );
$hVideo = fopen( '/var/www/www.example.com/videos/video.mp4', 'rb' );
while( !feof( $hVideo )) {
  echo( fread( $hVideo, 4096 ));
  $aStreamInfo = stream_get_meta_data( $hVideo );
  if( $aStreamInfo['timed_out'] ) break;
}
fclose( $hVideo );
exit;
1
richhallstoke