「困った」は突然に
フロントエンドコンテンツを作成した際の確認に用いるlocalhost。
大多数のイケイケエンジニアの皆さんは以下のようにPythonでCoolに一撃で立ち上げているかと思います。
$ python -m http.server 8000
仕事柄、このような動作確認をすることはそうなかったものの、たまに行う必要が生じた際は私も上記のコマンドを叩いてバシッとキメていました。
ところがある日、ふと上記のコマンドを打とうとしたときにあることに気が付きます。
「このPC、Pythonが入ってねえ...!」
最後に上記のコマンドを打って以降、異動となりPCが代わり、そこにはPythonが入っていないのでした。
Pythonを入れることもできなくはないのですが、ネットワークの都合上コマンドからはインストールできず、インストーラを所定の方法でダウンロードして...となかなかの手間がかかります。
ほんとたまにしか行わない作業のためにわざわざPythonを入れるのはなんか負けた気がするなあと思い悩んでいたところ、ふと、
「俺たちにはPowerShellがあるじゃないか!」
ということに気が付き、せっせとPowerShellでlocalhostを立ててみました。
PowerShellでlocalhostを立ててみた
いきなりですが、コードの全文を晒します。
# PowerShellのバージョンが3.0以上であることを確認
$PSVersionTable.PSVersion
# ディレクトリパス設定
$dirPath = 'C:\Users\Desktop\localhostHandson'
# PowerShellでWebサーバを立てる
cd $dirPath
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add('http://localhost:8000/')
$listener.Start()
echo 'Listening at http://localhost:8000/ ...'
while ($listener.IsListening)
{
$context = $listener.GetContext()
$requestUrl = $context.Request.Url
$localPath = $requestUrl.LocalPath
$response = $context.Response
# URLパスをファイルパスに変換
$filepath = Join-Path -Path $dirPath -ChildPath ($localPath.TrimStart('/'))
Write-Output $filepath
if (Test-Path -Path $filepath)
{
# ファイルの拡張子によって処理を分ける
$extension = [System.IO.Path]::GetExtension($filepath)
if ($extension -eq '.html' -or $extension -eq '.css' -or $extension -eq '.js')
{
# テキストファイルの場合
$fileContents = Get-Content -Path $filepath -Raw -Encoding UTF8
$buffer = [System.Text.Encoding]::UTF8.GetBytes($fileContents)
}
else
{
# バイナリファイルの場合
$buffer = [System.IO.File]::ReadAllBytes($filepath)
}
$response.ContentLength64 = $buffer.Length
$output = $response.OutputStream
$output.Write($buffer, 0, $buffer.Length)
$output.Close()
}
else
{
$response.StatusCode = 404
}
$response.Close()
}
動作確認用にテスト用のディレクトリ(サンプルではC:\Users\Desktop\localhostHandson
としていますが、任意のディレクトリに置き換えてください)の配下にindex.html
を作成しておきます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
Hello world !
</body>
</html>
そのうえでPowerShell(ISEでも可)を起動し、上記PowerShellのコードを貼り付けて実行してください。
そしてブラウザで http://localhost:8000/index.html にアクセスしてください。
(何の捻りもありませんが...)
無事localhostにアクセスできていることが確認できました。
なお、localhostを停止させる場合はPowerShellの画面を閉じるか、[Ctrl]+[C]を押下します。
ちょっと見えた「Webサーバの仕組み」
ここまでで「PowerShellでlocalhostを動かしたい!」という目的は達成できました。
これでPCが変わろうが安心して好きなときにlocalhostを立てることができます。
めでたしめでたし。
...なのですが、書いたコードを眺めていると、冒頭のたった1行のPythonのコードを実行しているときにはさっぱりわからなかった裏側の動きがなんとなく見えてきました。
ITオタクの探求心がこれを放ってはおきません。
ここからは、Webサーバの裏側をちょっぴり探求してみましょう。
サーバの設定・起動
$listener = New-Object System.Net.HttpListener
と$listener.Prefixes.Add('http://localhost:8000/')
で、新しいHttpListener
オブジェクトを作成し、それがhttp://localhost:8000/
でリクエストを待ち受けるように設定しています。
# PowerShellでWebサーバを立てる
cd $dirPath
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add('http://localhost:8000/')
$listener.Start()
echo 'Listening at http://localhost:8000/ ...'
cd $dirPath
では、カレントディレクトリをWebサーバが提供するファイルが格納されているディレクトリ(ここでは、コードの冒頭で定義しているC:\Users\Desktop\localhostHandson
)に変更します。
次に、New-Object System.Net.HttpListener
で新しいHttpListener
オブジェクトを作成し、$listener.Prefixes.Add('http://localhost:8000/')
で、このオブジェクトがhttp://localhost:8000/
でリクエストを待ち受けるように設定します。
最後に、$listener.Start()
でサーバを起動しています。
これにより、サーバは指定したアドレスでHTTPリクエストを待ち受けるようになります。
リクエストの処理
while ($listener.IsListening)
のループ内で、サーバがリクエストを待ち受け、それを処理します。具体的には、リクエストが来ると、そのURLを取得し、それをローカルのファイルパスに変換します。
その後、そのファイルが存在するかどうかを確認し、存在する場合はその内容をレスポンスとして返します。
Write-Output $filepath
ここでは、$filepath
変数の内容をコンソールに出力しています。
これはデバッグのためのもので、リクエストが来たときにどのファイルが要求されているのかを確認するために使用します。
if (Test-Path -Path $filepath)
次に、Test-Path
コマンドレットを使用して、要求されたファイルが存在するかどうかを確認しています。存在する場合は、その後のコードが実行されます。
$extension = [System.IO.Path]::GetExtension($filepath)
ここでは、GetExtension
メソッドを使用して、要求されたファイルの拡張子を取得します。
if ($extension -eq '.html' -or $extension -eq '.css' -or $extension -eq '.js')
{
# テキストファイルの場合
$fileContents = Get-Content -Path $filepath -Raw -Encoding UTF8
$buffer = [System.Text.Encoding]::UTF8.GetBytes($fileContents)
}
else
{
# バイナリファイルの場合
$buffer = [System.IO.File]::ReadAllBytes($filepath)
}
この部分では、ファイルの拡張子によって処理を分けています。拡張子が.html
、.css
、または.js
の場合(つまり、テキストファイルの場合)、Get-Content
コマンドレットを使用してファイルの内容を読み込み、それをUTF-8
のバイト配列に変換しています。
それ以外の場合(つまり、バイナリファイルの場合)、ReadAllBytes
メソッドを使用してファイルの内容をバイト配列として読み込んでいます。
$response.ContentLength64 = $buffer.Length
$output = $response.OutputStream
$output.Write($buffer, 0, $buffer.Length)
$output.Close()
この部分では、レスポンスを生成しています。
まず、ContentLength64
プロパティにバイト配列の長さを設定して、レスポンスのコンテンツの長さを指定します。
次に、OutputStream
プロパティを使用してレスポンスの出力ストリームを取得し、Write
メソッドを使用してバイト配列をそのストリームに書き込みます。
最後に、Close
メソッドを使用して出力ストリームを閉じます。
else
{
$response.StatusCode = 404
}
この部分は、要求されたファイルが存在しない場合に実行されます。
StatusCode
プロパティに404
を設定して、クライアントにファイルが見つからないことを通知します。
改めてわかるWebサーバソフトのすごさ
ここまでで紹介したコードは、リクエストに対して該当するファイルを返すだけの最低限のWebサーバの動きを実装してはいるものの、実際のApacheやNginxといったソフトではこれ以外にもWebサーバとして必要な以下のような機能が備えられています。
- 複数のリクエストを同時に処理するためにマルチスレッドやマルチプロセス
- 静的ファイルを効率よく配信するためのキャッシング、圧縮、範囲リクエストといった機能
- PHP、Python、Rubyなどのサーバーサイドスクリプトを実行して動的なコンテンツを生成する機能
- SSL/TLSによる暗号化、アクセス制御、レートリミティングなど、Webサーバとしてのセキュリティを強化するための機能
- 大量のトラフィックを分散させるロードバランシング
Webサーバソフトの偉大さを再認識し、これからも有り難く使わせていただこうと思います。
おわりに
Pythonコード1行でサクっと作業を進めるのもアリですが、時にはちゃんと処理を書いてみて普段使っている仕組みの尊さを噛む(しがむ※)のもいいなぁと思ったのでした。
※しがむ:関西の一部の地域の方言で「噛みしめる」の意。