PHP、Smarty、ケータイサイトについて発信中。書籍 「Smarty動的webサイト構築入門」(技術評論社) 好評発売中
RSS icon Home icon
  • 処理時間が長いとき、すばやくページ表示したい(5)


      フォーム送信後、サーバ側の処理時間が長いとき、なかなか画面が変わらないことがあります。サーバ側の処理時間が長いときでも、すばやくページを表示するいくつかの方法について説明します。

    (5) いきなり難しい方法

     前回の(4)ではプログレスバーを表示しました。これまでの(1)~(4)は、ブラウザへの表示処理を工夫しました。しかし、ブラウザがメイン処理の終わりを待っているという点に切り込んでいません。

     これは例えるなら次のような状況です。鈴木さん(ブラウザ)が高橋商店(サーバ)の田中さん(メイン処理)に電話をかけて、至急の仕事を頼みました。ところが、鈴木さん(ブラウザ)は電話を切らずに、田中さん(メイン処理)の仕事が終わるまで、ぼんやりと待っています。

     通常は、いったん電話を切り、田中さんが仕事を終えてから、鈴木さんへ電話で報告します。今回は、この方法に近いものを目指しました。ただし、田中さん(メイン処理)から鈴木さん(ブラウザ)へ電話で報告する、という部分が難しいです。そこで、田中さん(メイン処理)は進行状況をホワイトボードに書き、鈴木さん(ブラウザ)は不定期に電話をかけて、田中さん以外の人にホワイドボードを見てもらう、と置き換えています。

     ページ遷移は、form.html ⇒ task.php ⇒ show_status.php ⇒ thanks.html となります。オモテにはでてきませんが、ホワイトボードに相当するのは、ticket.phpです。

    5_11 5_21

     最初は、task.phpで、ポイントは3つあります。

     1つめは、進行状況の仲介役のチケットです。最初にチケットを新規発行し、メイン処理の進行に合わせて、チケットのstatusを更新します。チケットIDをURLで引き回すと、第三者がURLでアクセスする可能性があるので、セッションに保存しました。

     2つめは、レスポンスヘッダのcontent-lengthです。これを設定することで、task.phpがメイン処理を終わっていないのに、ブラウザはページロードを完了します。つまり、次のshow_status.phpへすぐに移動できるというわけです。

     3つめは、レスポンスヘッダのconnection: closeです。これがないと、ブラウザがshow_status.phpへリクエストできても、サーバ側では、すぐにはshow_status.phpを実行できず、task.php のメイン処理の終了を待ちます。

    <?php
    require( 'ticket.php' );
     
    $ticket = new Ticket;
    $ticket_id = $ticket->create( 'Start' );
     
    session_start();
    $_SESSION['ticket_id'] = $ticket_id;
    session_write_close();
     
    header( "connection: close" );
     
    ob_start();
    print( '<meta http-equiv="refresh" content="0; url=show_status.php" />' );
    $buf = ob_get_clean();
    $len = strlen( $buf );
    header( "Content-length: $len" );
    echo $buf;
    flush();
     
    // メイン処理
    for ( $i = 0; $i < 10; ++$i ) {
     sleep( 1 );
     $ticket->update( $ticket_id, "Running\t$i/10" );
    }
     
    $ticket->update( $ticket_id, 'Finished' );
    exit();
    ?>

     次は、show_status.php です。セッションからチケットIDを取り出し、進行状況を問い合わせます。 

    <?php
    require( 'ticket.php' );
     
    // セッションからチケットIDを取り出し、進行状況を問い合わせる
    session_start();
    $ticket = new Ticket;
    $status = $ticket->get( $_SESSION['ticket_id'] );
    session_write_close();
    
    if ( 'Finished' == $status ) {
     // 終了していたら、thanks.htmlへページ移動する
     echo '<meta http-equiv="refresh" content="0; url=thanks.html" />';
     
    } else if ( preg_match( '/Running\s+([0-9]+)\/([0-9]+)/', $status, $matches ) ) {
     $tpl['rcd_pos'] = $matches[1];
     $tpl['rcd_cnt'] = $matches[2];
     $tpl['percent'] = floor(100 * $tpl['rcd_pos'] / $tpl['rcd_cnt']);
     
    ?>
    <meta http-equiv="refresh" content="1" />
    <style>
    .frame {
     border: 1px solid #000;
     width: 400px;
     height: 50px;
    }
    .progress {
     width: <?php echo $tpl['percent'] ?>%;
     height: 50px;
     background: #00f;
    }
    </style>
    処理中です<br />
    全件数 : <?php echo $tpl['rcd_cnt'] ?><br />
    処理済み: <?php echo $tpl['rcd_pos'] ?><br />
    <div class="frame">
      <div class="progress">
        <br />
      </div>
    </div>
    <?php
     
    } else {
     // 1秒毎に進行状況を表示する
     echo '<meta http-equiv="refresh" content="1" />';
     echo 'show_status<br />';
     echo "$status<br />";
    }
    ?>

     最後に、ticket.php です。MySQLとPEAR::DBを使いました。idとstatusの2カラムのテーブルを作成し、DB_USER、DB_PASS、DB_HOST、DB_NAMEを置き換えてください。

    <?php
    /*
    CREATE TABLE ticket (
    `id` BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
    `status` VARCHAR( 255 ) NOT NULL
    );
    */
    require_once( 'DB.php' );
     
    class Ticket {
     var $db;
     
     function Ticket() {
      $this->db = DB::connect( 'mysql://DB_USER:DB_PASS@DB_HOST/DB_NAME' );
      if ( PEAR::isError($this->db) ) {
       die( $this->db->getMessage() );
      }
      $res = $this->db->query( "SET NAMES 'UTF8'" );
      if ( PEAR::isError($res) ) {
       die( $sth->getMessage() );
      }
      $this->db->freeResult( $res );
     }
     
     function create ( $i_status ) {
      $query = "INSERT INTO ticket(id,status) VALUES(null,?)";
      $arr = array( $i_status );
    
      $sth = $this->db->prepare( $query );
      if ( PEAR::isError($sth) ) {
       die( $sth->getMessage() );
      }
      $res = $this->db->execute( $sth, $arr );
      if ( PEAR::isError($res) ) {
       die( $sth->getMessage() );
      }
    
      $this->db->freeResult( $res );
      $this->db->freePrepared( $sth );
      
      $id = $this->db->getOne( 'SELECT LAST_INSERT_ID() FROM ticket' );
      return $id;
     }
     
     function update ( $i_id, $i_status ) {
      $query = "UPDATE ticket SET status = ? WHERE id = ?";
      $arr = array( $i_status, $i_id );
    
      $sth = $this->db->prepare( $query );
      if ( PEAR::isError($sth) ) {
       die( $sth->getMessage() );
      }
      $res = $this->db->execute( $sth, $arr );
      if ( PEAR::isError($res) ) {
       die( $sth->getMessage() );
      }
      $this->db->freeResult( $res );
      $this->db->freePrepared( $sth );
     }
     function get ( $i_id ) {
      $query = "SELECT status FROM ticket WHERE id = ?";
      $arr = array( $i_id );
      
      $status = $this->db->getOne( $query, $arr );
      return $status;
     }
    }
    ?>

     今回の方法は、ブラウザを閉じたり、[中止]ボタンを押しても、メイン処理は停止しません。キャンセルをしたい場合は、キャンセルの仕組みをチケットサーバとメイン処理に仕込む必要があります。

    実験ページ(5)

    2009-04-19 aoki No comments 02.PHP

    Leave a reply