こんぶにのブログ

エンジニアという職業を通して学んだことを発信するブログです。

【脱・伸び悩みWebエンジニア】phpでTCPサーバ / クライアントっぽいものを作ってみる

はじめに

今回は超簡単なWebサーバを作っていきたいと思います。 こちらの本ではコードはJavaなのですが、Javaの環境構築をするのは結構大変です。 また、私自身はPHPをよく扱うので、書籍内のコードをphpに読み替えて、作成していこうと思います。

ソケット通信で接続を確立する

サーバとクライアント間で何かをやり取りしたい!と思った時、まずは両者をつなぐものが必要になります。
一体何が必要なのでしょうか?
答えはソケットです。
ソケットとは??という問いにはこちらの記事が分かりやすかったです。

envader.plus

しっかり理解するには色々と前提知識が必要ですが、
そのあたりはハンズオンをしていく中で理解していくのがよさそうです。
とにかくクライアントとサーバで、それぞれソケットを用意して、宛先を指定してやれば接続が確立されます。
そうすればそこで確立した接続内にデータを読み書きすることで、互いにデータをやり取りできます。
早速作っていきます。

環境構築

  • windows10

  • php8.2をローカルにインストール

phpのインストール手順に関しては以下の記事で行いました。

konbuni.net

実装

さて、まずは適当なフォルダを作成します。
C直下にRepoというフォルダを作り、その中にsocket-testというフォルダを作成しました。C:\Repo\socket-test
その中に、TcpClient.phpTcpServer.phpというファイルを作ります。
新規でテキストファイルを作って、拡張子ごと変えちゃってください。
さらにsocket-testの中にtextというフォルダを作成。
server_send.txt,
server_recv.txt,
client_send.txt,`
``client_recv.txt```
の4つのファイルを作ります。
_sendとなってるファイルにはお好きな言葉を書き込んでください。

TcpServer.phpの作成

TcpServer.phpは以下のように記述しました。
server_send.txtに書かれている内容をソケットに書き込みます。
ソケットに書き込まれている値(クライアントが書き込んだ値)をserver_recv.txtに書き込みます。
ちなみにテキストの文字コードutf-8にしないと文字化けしました。

<?php
// localhostのport8001を使用
$address = '127.0.0.1';
$port = '8001';

// ソケットを作成。引数はphpに定義されている関数を使用。公式より。
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

// ソケットに名前を付けます。名前がないとどのアドレスにも関連付けられていないとみなされ、何も出来ません。
socket_bind($socket, $address, $port);

// ソケット上で接続待ち(listen)します。クライアントが上記で作ったソケットに接続するのをひたすら待ちます。
// 今回は5個の処理用キューを用意しておきます。
socket_listen($socket, 5);

// もしクライアントからソケットへの接続が来た場合、それを許可します。
$acceptSocket = socket_accept($socket);

$msg = "TcpServerに接続できました。\n";
echo $msg;
socket_write($acceptSocket, $msg, strlen($msg));

$fileName = 'text/server_send.txt';
$sendFile = fopen($fileName, 'rb');
$content = fread($sendFile, filesize($fileName));

socket_write($acceptSocket, $content, strlen($content));

// ソケット接続先のクライアントから書き込まれたデータを読み取ります。
$out = socket_read($acceptSocket, 2048);

$fileName = 'text/server_recv.txt';
$recvFile = fopen($fileName, 'w');
fwrite($recvFile, $out);

// 確立された接続を閉じます
socket_close($acceptSocket);

// ソケットそのものを終了
socket_close($socket);

TcpClient.phpの作成

TcpClient.phpは以下のように記述しました。 client_send.txtに書かれている内容をソケットに書き込みます。
ソケットに書き込まれている値(サーバーが書き込んだ値)をclient_recv.txtに書き込みます。

<?php
error_reporting(E_ALL);

$service_port = '8001';
// $service_port = '80'; あとで使うのでコメントアウト
$address = gethostbyname('localhost');

$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);

socket_connect($socket, $address, $service_port);

$fileName = 'text/client_send.txt';
$sendFile = fopen($fileName, 'rb');
$content = fread($sendFile, filesize($fileName));

echo "送信中・・・";
socket_write($socket, $content);
echo "OK.\n";

$socketRead = socket_read($socket, 2048);
$fileName = 'text/client_recv.txt';
$recvFile = fopen($fileName, 'w');
// fwrite($recvFile, "");
fwrite($recvFile, $socketRead);
echo "送られてきたソケット↓:\n\n";
echo $socketRead;

echo "ソケットを閉じます...";
socket_close($socket);
echo "OK.\n\n";

エラー解決

socketで色々テストをしている時に
Fatal error: Call to undefined function socket_create() というエラーが起きました。 これはphp.iniのextension=socketsコメントアウトを取り消すことで解除できました。 また、もう一点、;extension_dir=extコメントアウト解除も必要でした。
php.iniの場所は`php --iniで確認します。

実行

さて、phpがインストールされていれば、もうphpファイルを実行できるはずです。
コマンドプロンプトを2つ開いて、片方で以下のコマンドで実行。
サーバーを起動した感じを再現します。

php TcpServer.php

もう片方で以下を実行。
ブラウザか何かでデータを送信した時の感じを再現します。

php TcpClient.php

send.txtに書き込んだ内容が、それぞれのrecvファイルに書き込まれていれば成功です。
ちなみに以下の公式のものをカットして少しアレンジした感じになります。
今回作ったのはエラーが起きた際のことを全く考慮しておらず、完全な手抜きです。
分かりやすさを重視するためやむを得ずです。 (という言い訳)

www.php.net

何が起きているか?

今回はファイルをふたつ用意して1つのコンピュータでやりましたが、
現実で通信をするときには全く別のところにある二つのコンピュータ上でこれが起きています。
サーバー側は自分もしくは他のどこかのサーバー上の指定したポートにソケットを作り、どっかのクライアントからの接続をひたすら待ちます。
クライアント側はこの世のどこかのソケットが作られているサーバーのIPアドレスとポート番号を指定して、ソケットを作成します。
両想いになったら、接続が確立されます。
これらの通信内でのデータやり取りはTCPで行っています。
コードにすると色々手順が必要ですが、やっていることはこれだけになります。

ブラウザを使ってHTTPリクエストを送ってみる

TcpServer.phpを実行し、ブラウザでこちらを入力してみます。
送るブラウザはchromeをおすすめします。(Braveだとソケットが上手く確立できませんでした。ここで何十分も無駄にしました!!!!)
また、シークレットウィンドウもうまく行きません。
通常のウィンドウをおすすめします。
以下のURLを入力します。
http://localhost:8001/index.php
「自身のPCのポート8001に対して、httpリクエストを送りたいです!」
という内容です。
すると、コンソールに以下の値が出力されました。

 "GET /index.php HTTP/1.1
Host: localhost:8001
Connection: keep-alive
sec-ch-ua: "Chromium";v="118", "Brave";v="118", "Not=A?Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8
Sec-GPC: 1
Accept-Language: ja
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate, br

"

ブラウザでURLを入力するとブラウザ君は
「よし、指定の宛先に向けてHTTPリクエストを作成するぞ!」
「HTTPヘッダー作るぞ!!(さっきコンソールに出た値はブラウザが作ってくれてた)」
「宛先とのソケット通信をするぞ!」
「ソケット通信(TCP)を使って、作ったHTTPヘッダーを送るぞ!」
とまでやってくれます。
そんなこんなでサーバーにさっきの文が送られてきているので、上の結果がrecvファイルに出力されます。

終わりに

htmlをくれ!ってどこかのサーバーのportにブラウザからお願いしたときの動きがなんとなくわかってきました。
次回はApacheで自分のPCをWebサーバにします。
そこに対して、自分のPCから(さも他のサーバからのリクエストであるかのように)リクエストを送ってみます。

ご覧いただきありがとうございました!!

自分用メモ

windowsでカレントディレクトリを確認するときは cd と入力。
apacheのバージョンを確認するときはhttpd -vと入力。