[php] [chat]シンプルなIPアドレスファイル名ルームでのチャットを作った。

お客様のチャット

<!-- chat.php -->

<?php
function trace($txt){
    //print($txt);
    $a="a";
}
class RoomLog
{
    
    public $LOG_FILE_NAME;
    public $name;
    public $SPLIT_Str;
    public $message;
    public function get_ip_file_name(){
        global $_SERVER;
        $ip_Address=$_SERVER['REMOTE_ADDR'];
        print("ip_Address= " . $ip_Address . "<br>");
        if($ip_Address=="::1"){
            $ip_Address="192.168.1.29";
            print("テストのためip変数上書きしました。ip_Address= " . $ip_Address . "<br>");
        }
        $ip_Address_Arr = explode(".", $ip_Address);
        $ip_file_name = join('_', $ip_Address_Arr);
        $ip_file_name = "log/ip_" . $ip_file_name . "_chatlog.txt";
        print("ip_file_name= " . $ip_file_name . "<br>");
        return $ip_file_name;
    }
    public function initLog(){
        trace("■ initLog()<br>");
        global $LOG_FILE_NAME;
        global $name;
        global $SPLIT_Str;
        global $message;
        
        $ip_file_name=$this->get_ip_file_name();
        
        // データを書き込むファイルの名前
        //$LOG_FILE_NAME = "chat_class_txt_log.txt";
        $LOG_FILE_NAME = $ip_file_name;
     
        // 区切りのための文字列
        $SPLIT_Str = "|-|";
     
        // 名前を格納する変数
        $name = "お客様";
        // メッセージを格納する変数
        $message = "";
        
        $this->LOG_FILE_NAME=$LOG_FILE_NAME;
        $this->name=$name;
        $this->SPLIT_Str=$SPLIT_Str;
        $this->message=$message;
        /*
        trace("<br>");
        trace("this->LOG_FILE_NAME= " . $this->LOG_FILE_NAME . "<br>");
        trace("this->name= " . $this->name . "<br>");
        trace("this->SPLIT_Str= " . $this->SPLIT_Str . "<br>");
        trace("this->message= " . $this->message . "<br>");
        */
    }
    
    public function startMain(){
        trace("■ startMain()<br>");
        $this->initLog();
        $this->CheckFormPostData();
        $this->LogShow();
    }
    

    public function CheckFormPostData(){
        trace("■ CheckFormPostData()<br>");
        global $_POST;
        
        $name = $this->name;
        $SPLIT_Str = $this->SPLIT_Str;
        $message = $this->message;
        
        
        // 送信されたデータ
        $_POST_string = join(', ', $_POST);
        trace("_POST_string2= " . $_POST_string . "<br>");
        
        if (empty($_POST)) {
            trace("The form is empty.");
        } else {
            trace("Form data is present.");
        }
        
        // 送信された名前とメッセージを変数に代入
        if (isset($_POST['name'])) {
          $name = $_POST['name'];
     
          if (strpos($name, $SPLIT_Str) !== false) {
            // 名前に区切り文字が含まれている場合の処理
            echo "使用できない文字列「|-|」が含まれています。";
            return;
          }
     
          if ($name == "") {
            $name = "お客様";
          }
        }
        if (isset($_POST['message'])) {
          $message = $_POST['message'];
     
          if (strpos($message, $SPLIT_Str) !== false) {
            // メッセージに区切り文字が含まれている場合の処理
            echo "使用できない文字列「|-|」が含まれています。";
            return;
          }
     
          //$this->LOG_FILE_NAME=$LOG_FILE_NAME;
          $this->name=$name;
          //$this->SPLIT_Str=$SPLIT_Str;
          $this->message=$message;
          $this->LogWrite();
        }
    }
   
    
    public function LogWrite(){
      trace("■ LogWrite()<br>");
      $LOG_FILE_NAME = $this->LOG_FILE_NAME;
      $name = $this->name;
      $SPLIT_Str = $this->SPLIT_Str;
      $message = $this->message;
        
      //書き込みモードでファイルを開く
      $fp = fopen($LOG_FILE_NAME, "a") or exit($LOG_FILE_NAME . "が開けません");
 
      // | を区切り文字として2つのデータを繋げて書き込む
      fwrite($fp, $name . $SPLIT_Str . $message . "\n");
      
      
      // リダイレクトのためのHTTPヘッダーを送信
      //header("Location: " . $_SERVER['PHP_SELF'], true, 303);
      
      fclose($fp);
    }

    function LogShow(){
        trace("■ LogShow()<br>");
        //global $LOG_FILE_NAME;
        global $linesNum;
        global $lines;
        $LOG_FILE_NAME = $this->LOG_FILE_NAME;
        //$name = $this->name;
        //$SPLIT_Str = $this->SPLIT_Str;
        //$message = $this->message;
        
        if (!file_exists($LOG_FILE_NAME)) {
          // ファイルがない場合
          echo "書き込みはありません。";
     
          $linesNum = 0;
        } else {
     
          // ファイルの全行を読み取る
          $lines = file($LOG_FILE_NAME);
     
          // 読み込んだ行数
          $linesNum = count($lines);
        }
    }
    
}

$RoomLog_ins = new RoomLog();
$RoomLog_ins->startMain();

?>
 
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>1行メッセージ</title>
</head>
 
<body>
 
<h1>お問い合わせ内容</h1>
 
  <section>
    <?php
 
      // 一行ずつ表示する処理
      for ($i = 0; $i < $linesNum; $i++) {
 
         // 区切り文字でデータを区切って配列に格納
         $array = explode($SPLIT_Str, $lines[$i]);
 
         // 区切り文字の前の部分は名前
         $name = htmlspecialchars($array[0]);
         
         
         // 区切り文字の後の部分はメッセージ
         
         $array_length = count($array);
         if ($array_length == 2) {
           $message = htmlspecialchars($array[1]);
         }
         // 名前とメッセージを表示
         echo '<p>' . $name . "「" . str_replace(PHP_EOL, "", $message) . '」</p>';
      }
    ?>
  </section>


  
 
  <form method="post" action="chat_class_txt.php">
    <div>
      <!--<b>おなまえ</b>-->
      <!--<input name="name" type="text" size="20" maxlength="10">-->
      <input name="name" type="hidden" size="20" maxlength="10">
    </div>
    <div>
      <!--<b>ご入力</b>-->
      <input name="message" type="text" size="50" maxlength="50" required>
      <button name="submit" type="submit">送信</button>
    </div>
    
  </form>
  
</body>
</html>

担当者のチャット画面

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>1行メッセージ</title>
</head>
 
<body>
<!-- chat.php -->

<style>
.base {
  display: flex;
  flex-direction: row;
  width: 100vw;
  height: 90vh;
}
.class1 {
  width: 10%;
}
 
.class2 {
  display: flex;
  flex-direction: column;
  width: 90%;
}

</style>
<style>
  div {
    text-align: center;
    /* background-color: #aaaaaa; 16進数のカラーコードを使用する例 */
  }
  button{
      font-size: 200%;
      width:100px;
      height:100px;
  }
</style>
  <div class="base">
    <div class="class1">

<?php
//$dir = "log";
//$files = scandir($dir);
$files = glob("log/*");
$counter=0;
foreach ($files as $file) {
    //echo("<h2>" . $file . "</h2><br>");
    $url='log_frame.php';
    //echo('<a href="'.$url.'"><h1><div >' . $counter . '</div></h1></a><br>');
    
    $htmlStr='';
    $htmlStr=$htmlStr.'<form method="post" action="chat_class_txt_admin.php">';
    $htmlStr=$htmlStr.'<input type="hidden" name="LOG_FILE_NAME" value="' . $file . '">';
    $htmlStr=$htmlStr.'<button name="submit" type="submit">' . $counter . '</button>';
    $htmlStr=$htmlStr.'</form>';
    echo($htmlStr);

    $counter=$counter+1;
}

?>
      
</div>
<div class="class2">
<?php
function trace($txt){
    //print($txt);
    $a="a";
}
class RoomLog
{
    
    public $LOG_FILE_NAME;
    public $LOG_FILE_NAME_Opened;
    public $name;
    public $SPLIT_Str;
    public $message;
    public function get_ip_file_name(){
        global $_SERVER;
        $ip_Address=$_SERVER['REMOTE_ADDR'];
        trace("ip_Address= " . $ip_Address . "<br>");
        if($ip_Address=="::1"){
            $ip_Address="192.168.1.29";
            trace("テストのためip変数上書きしました。ip_Address= " . $ip_Address . "<br>");
        }
        $ip_Address_Arr = explode(".", $ip_Address);
        $ip_file_name = join('_', $ip_Address_Arr);
        $ip_file_name = "log/ip_" . $ip_file_name . "_chatlog.txt";
        trace("ip_file_name= " . $ip_file_name . "<br>");
        return $ip_file_name;
    }
    
    public function CheckFormPostData_Admin(){
        trace("■ CheckFormPostData_Admin()<br>");
        global $_POST;
        
        // 送信されたデータ
        $_POST_string = join(', ', $_POST);
        trace("_POST_string1= " . $_POST_string . "<br>");
        
        if (empty($_POST)) {
            trace("The form is empty.<br>");
        } else {
            trace("Form data is OK present.<br>");
        }
        if (isset($_POST['LOG_FILE_NAME'])) {
          $LOG_FILE_NAME = $_POST['LOG_FILE_NAME'];
          $this->LOG_FILE_NAME=$LOG_FILE_NAME;
          print("LOG_FILE_NAME= " . $LOG_FILE_NAME . "<br>");
        }
        /*
        // 送信された名前とメッセージを変数に代入
        if (isset($_POST['name'])) {
          $name = $_POST['name'];
     
          if (strpos($name, $SPLIT_Str) !== false) {
            // 名前に区切り文字が含まれている場合の処理
            echo "使用できない文字列「|-|」が含まれています。";
            return;
          }
     
          if ($name == "") {
            $name = "お客様";
          }
        }
        if (isset($_POST['message'])) {
          $message = $_POST['message'];
     
          if (strpos($message, $SPLIT_Str) !== false) {
            // メッセージに区切り文字が含まれている場合の処理
            echo "使用できない文字列「|-|」が含まれています。";
            return;
          }
     
          //$this->LOG_FILE_NAME=$LOG_FILE_NAME;
          $this->name=$name;
          //$this->SPLIT_Str=$SPLIT_Str;
          $this->message=$message;
          $this->LogWrite();
        }
        */
        return $LOG_FILE_NAME;
    }

    public function initLog(){
        trace("■ initLog()<br>");
        global $LOG_FILE_NAME;
        global $name;
        global $SPLIT_Str;
        global $message;
        
        $ip_file_name=$this->CheckFormPostData_Admin();
        
        // データを書き込むファイルの名前
        //$LOG_FILE_NAME = "chat_class_txt_log.txt";
        $LOG_FILE_NAME = $ip_file_name;
     
        // 区切りのための文字列
        $SPLIT_Str = "|-|";
     
        // 名前を格納する変数
        $name = "担当者";
        // メッセージを格納する変数
        $message = "";
        
        $this->LOG_FILE_NAME=$LOG_FILE_NAME;
        $this->name=$name;
        $this->SPLIT_Str=$SPLIT_Str;
        $this->message=$message;
        /*
        trace("<br>");
        trace("this->LOG_FILE_NAME= " . $this->LOG_FILE_NAME . "<br>");
        trace("this->name= " . $this->name . "<br>");
        trace("this->SPLIT_Str= " . $this->SPLIT_Str . "<br>");
        trace("this->message= " . $this->message . "<br>");
        */
    }
    
    public function startMain(){
        trace("■ startMain()<br>");
        $this->initLog();
        $this->CheckFormPostData();
        $this->LogShow();
    }
    

    public function CheckFormPostData(){
        trace("■ CheckFormPostData()<br>");
        global $_POST;
        
        $name = $this->name;
        $SPLIT_Str = $this->SPLIT_Str;
        $message = $this->message;
        
        
        // 送信されたデータ
        $_POST_string = join(', ', $_POST);
        //trace("_POST_string2= " . $_POST_string . "<br>");
        
        if (empty($_POST)) {
            trace("The form is empty.<br>");
        } else {
            trace("Form data is present.<br>");
        }
        
        // 送信された名前とメッセージを変数に代入
        if (isset($_POST['name'])) {
          $name = $_POST['name'];
     
          if (strpos($name, $SPLIT_Str) !== false) {
            // 名前に区切り文字が含まれている場合の処理
            echo "使用できない文字列「|-|」が含まれています。<br>";
            return;
          }
     
          if ($name == "") {
            $name = "担当者";
          }
        }
        if (isset($_POST['message'])) {
          $message = $_POST['message'];
     
          if (strpos($message, $SPLIT_Str) !== false) {
            // メッセージに区切り文字が含まれている場合の処理
            echo "使用できない文字列「|-|」が含まれています。<br>";
            return;
          }
     
          //$this->LOG_FILE_NAME=$LOG_FILE_NAME;
          $this->name=$name;
          //$this->SPLIT_Str=$SPLIT_Str;
          $this->message=$message;
          $this->LogWrite();
        }
    }
   
    
    public function LogWrite(){
      trace("■ LogWrite()<br>");
      $LOG_FILE_NAME = $this->LOG_FILE_NAME;
      $name = $this->name;
      $SPLIT_Str = $this->SPLIT_Str;
      $message = $this->message;
        
      //書き込みモードでファイルを開く
      $fp = fopen($LOG_FILE_NAME, "a") or exit($LOG_FILE_NAME . "が開けません<br>");
 
      // | を区切り文字として2つのデータを繋げて書き込む
      fwrite($fp, $name . $SPLIT_Str . $message . "\n");
      
      
      // リダイレクトのためのHTTPヘッダーを送信
      //header("Location: " . $_SERVER['PHP_SELF'], true, 303);
      
      fclose($fp);
    }

    function LogShow(){
        trace("■ LogShow()<br>");
        //global $LOG_FILE_NAME;
        global $linesNum;
        global $lines;
        $LOG_FILE_NAME = $this->LOG_FILE_NAME;
        //$name = $this->name;
        //$SPLIT_Str = $this->SPLIT_Str;
        //$message = $this->message;
        
        if (!file_exists($LOG_FILE_NAME)) {
          // ファイルがない場合
          echo "書き込みはありません。<br>";
     
          $linesNum = 0;
        } else {
     
          // ファイルの全行を読み取る
          $lines = file($LOG_FILE_NAME);
     
          // 読み込んだ行数
          $linesNum = count($lines);
        }
    }
    
}

$RoomLog_ins = new RoomLog();
$RoomLog_ins->startMain();

?>
 

 
<h1>お問い合わせ内容</h1>
 
  <section>
    <?php
 
      // 一行ずつ表示する処理
      for ($i = 0; $i < $linesNum; $i++) {
 
         // 区切り文字でデータを区切って配列に格納
         $array = explode($SPLIT_Str, $lines[$i]);
 
         // 区切り文字の前の部分は名前
         $name = htmlspecialchars($array[0]);
         
         
         // 区切り文字の後の部分はメッセージ
         
         $array_length = count($array);
         if ($array_length == 2) {
           $message = htmlspecialchars($array[1]);
         }
         // 名前とメッセージを表示
         echo '<p>' . $name . "「" . str_replace(PHP_EOL, "", $message) . '」</p>';
      }
    ?>
  </section>


  
 
  <form method="post" action="chat_class_txt_admin.php">
    <div>
      <!--<b>おなまえ</b>-->
      <!--<input name="name" type="text" size="20" maxlength="10">-->
      <input name="name" type="hidden" size="20" maxlength="10">
    </div>
    <div>
      <!--<b>ご入力</b>-->
      <?php
      echo('<input type="hidden" name="LOG_FILE_NAME" value="' . $LOG_FILE_NAME . '">');
      ?>
      <input name="message" type="text" size="50" maxlength="50" required>
      <button name="submit" type="submit">送信</button>
    </div>
    
  </form>
  
  
    </div><!--div class="class2"-->
  </div><!--div class="base"-->
</body>
</html>

こちらのサイトを参考にしました。
PHPで作るチャット(7) クロスサイトスクリプティング(XSS)の修正

このPHPをクラスベースで書き直しました。

[xampp] php Rechet chatを作る勉強。

参考サイト

[PHP]Ratchetの初期設定とテキストのリアルタイム通信テスト

Xamppをwindowsをインストール

https://sourceforge.net/projects/xampp/files/XAMPP%20Windows/7.4.33/

お名前.comのPHPが7.4なので

https://sourceforge.net/projects/xampp/files/XAMPP%20Windows/7.4.33/xampp-windows-x64-7.4.33-0-VC15-installer.exe/download

をダウンロードした。

システム環境変数にphpのパスを通す

C:\xampp\php

PHPのバージョン確認

PS C:\xampp\htdocs> php -v
PHP 7.4.33 (cli) (built: Nov  2 2022 16:00:55) ( ZTS Visual C++ 2017 x64 )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
PS C:\xampp\htdocs>

Composerをインストール

php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
php -r "if (hash_file('sha384', 'composer-setup.php') === 'e21205b207c3ff031906575712edab6f13eb0b361f2085f1f1237b7126d785e826a450292b6cfd1d64d92e6563bbde02') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
php composer-setup.php
php -r "unlink('composer-setup.php');"

XAMPP の htdocs ディレクトリに任意の名前のフォルダを作成し、ターミナルでアクセスできる状態にします。この例では MyChat というフォルダ名にしています。

php ../composer.phar require cboden/ratchet

こんなファイルが出てきます。

composer.json を開き autoload の5行を追記します。

{
    "autoload": {
        "psr-4": {
            "MyApp\\": "src"
        }
    },
    "require": {
        "cboden/ratchet": "^0.4.3"
    }
}

ディレクトリ MyChat にフォルダ src/ を作成し、その中に(Chat.php)を作成します。

<?php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
 
class Chat implements MessageComponentInterface {
    public function onOpen(ConnectionInterface $conn) {
    }
 
    public function onMessage(ConnectionInterface $from, $msg) {
    }
 
    public function onClose(ConnectionInterface $conn) {
    }
 
    public function onError(ConnectionInterface $conn, \Exception $e) {
    }
}

同様にフォルダ bin/ を作成し、その中にスクリプト(chat-server.php)を作成してください。

<?php
use Ratchet\Server\IoServer;
use MyApp\Chat;
 
    require dirname(__DIR__) . '/vendor/autoload.php';
 
    $server = IoServer::factory(
        new Chat(),
        8080
    );
 
    $server->run();

以上のスクリプトを作成後、ターミナルで以下のコマンドを入力してください。

php bin/chat-server.php

正常に動作するとターミナルには何も表示されないはずですが、以下のようなエラーが表示される場合があります。

PS C:\xampp\htdocs\MyChat> php bin/chat-server.php
PHP Fatal error:  Uncaught Error: Class 'MyApp\Chat' not found in C:\xampp\htdocs\MyChat\bin\chat-server.php:8
Stack trace:
#0 {main}
  thrown in C:\xampp\htdocs\MyChat\bin\chat-server.php on line 8

Fatal error: Uncaught Error: Class 'MyApp\Chat' not found in C:\xampp\htdocs\MyChat\bin\chat-server.php:8
Stack trace:
#0 {main}
  thrown in C:\xampp\htdocs\MyChat\bin\chat-server.php on line 8
PS C:\xampp\htdocs\MyChat>

もしこのエラーが表示されたなら composer.phar のアップデートを実行してみてください。アップデートのコマンドは以下のとおりです。

	php ../composer.phar update

再度やってみます。

php bin/chat-server.php

正常に動作した場合は control+C で終了させてください。

以上で通信のための準備が整いました。

メッセージ送信の実行

実際に通信をテストするには、まず Chat.php を以下のように書き換えます。

<?php
namespace MyApp;
use Ratchet\MessageComponentInterface;
use Ratchet\ConnectionInterface;
 
class Chat implements MessageComponentInterface {
    protected $clients;
 
    public function __construct() {
        $this->clients = new \SplObjectStorage;
    }
 
    public function onOpen(ConnectionInterface $conn) {
        // Store the new connection to send messages to later
        $this->clients->attach($conn);
 
        echo "New connection! ({$conn->resourceId})\n";
    }
 
    public function onMessage(ConnectionInterface $from, $msg) {
        $numRecv = count($this->clients) - 1;
        echo sprintf('Connection %d sending message "%s" to %d other connection%s' . "\n"
            , $from->resourceId, $msg, $numRecv, $numRecv == 1 ? '' : 's');
 
        foreach ($this->clients as $client) {
            if ($from !== $client) {
                // The sender is not the receiver, send to each client connected
                $client->send($msg);
            }
        }
    }
 
    public function onClose(ConnectionInterface $conn) {
        // The connection is closed, remove it, as we can no longer send it messages
        $this->clients->detach($conn);
 
        echo "Connection {$conn->resourceId} has disconnected\n";
    }
 
    public function onError(ConnectionInterface $conn, \Exception $e) {
        echo "An error has occurred: {$e->getMessage()}\n";
 
        $conn->close();
    }
}

そして、ターミナルのウィンドウを3つ開き、それぞれ以下のコマンドを入力します。

php bin/chat-server.php
telnet localhost 8080
telnet localhost 8080

もし telnet がないと言われた場合は python を利用する以下のコマンドを試してみてください。

	python -m telnetlib localhost 8080

実行結果です。一方のtelnetにメッセージを入力すると、他方にメッセージが表示されます。接続状態などは bin/chat-server.php を実行したウィンドウに表示されます。

ブラウザを利用したメッセージの送信

chat-server.php を以下のように書き換えます。

<?php
use Ratchet\Server\IoServer;
use Ratchet\Http\HttpServer;
use Ratchet\WebSocket\WsServer;
use MyApp\Chat;
 
    require dirname(__DIR__) . '/vendor/autoload.php';
 
    $server = IoServer::factory(
        new HttpServer(
            new WsServer(
                new Chat()
            )
        ),
        8080
    );
 
    $server->run();

ターミナルで再度 bin/chat-server.php を実行し、

MyChat/index.htmlに以下のHTMLフォームボタンで動くとjavascriptを入力します。

<br><br><br><br><br><br><br><br>
<a href="javascript:myFunction();">リンクでJSのmyFunction()実行</a>
<script>
function myFunction() {
// フォームボタンがクリックされたときの処理をここに記述

//------------------------------------------------------
var conn = new WebSocket('ws://localhost:8080');
conn.onopen = function(e) {
    console.log("Connection established!");
};
 
conn.onmessage = function(e) {
    console.log(e.data);
};
//------------------------------------------------------
alert('入室しました!');
}
</script>

実行結果は以下のとおりです。

ターミナルにブラウザから send によって送信したメッセージが表示されました。

以上、Ratchetの初期設定と、それを利用した通信の例でした。