PowerShellでlocalhostを立ててみたらWebサーバの動きがちょっぴりわかったので紹介したい

# 「困った」は突然に
フロントエンドコンテンツを作成した際の確認に用いるlocalhost。
大多数のイケイケエンジニアの皆さんは以下のようにPythonでCoolに一撃で立ち上げているかと思います。

```bash
$ python -m http.server 8000
```
仕事柄、このような動作確認をすることはそうなかったものの、たまに行う必要が生じた際は私も上記のコマンドを叩いてバシッとキメていました。

ところがある日、ふと上記のコマンドを打とうとしたときにあることに気が付きます。
<br>
<b>「このPC、Pythonが入ってねえ...!」</b>
<br>
最後に上記のコマンドを打って以降、異動となりPCが代わり、そこにはPythonが入っていないのでした。
Pythonを入れることもできなくはないのですが、ネットワークの都合上コマンドからはインストールできず、インストーラを所定の方法でダウンロードして...となかなかの手間がかかります。
ほんとたまにしか行わない作業のためにわざわざPythonを入れるのはなんか負けた気がするなあと思い悩んでいたところ、ふと、
<br>
<b>「俺たちにはPowerShellがあるじゃないか!」</b>
<br>
ということに気が付き、せっせとPowerShellでlocalhostを立ててみました。

# PowerShellでlocalhostを立ててみた
いきなりですが、コードの全文を晒します。

```powershell:PowerShell
# 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`を作成しておきます。

```html: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 にアクセスしてください。

![実行結果.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/123999/7c189526-58cb-f146-71c9-a3bdb8a7a3a6.png)

(何の捻りもありませんが...)
無事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
# 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を取得し、それをローカルのファイルパスに変換します。

その後、そのファイルが存在するかどうかを確認し、存在する場合はその内容をレスポンスとして返します。
```powershell
Write-Output $filepath
```
ここでは、`$filepath`変数の内容をコンソールに出力しています。
これはデバッグのためのもので、リクエストが来たときにどのファイルが要求されているのかを確認するために使用します。
```powershell
if (Test-Path -Path $filepath)
```
次に、`Test-Path`コマンドレットを使用して、要求されたファイルが存在するかどうかを確認しています。存在する場合は、その後のコードが実行されます。
```powershell
$extension = [System.IO.Path]::GetExtension($filepath)
```
ここでは、`GetExtension`メソッドを使用して、要求されたファイルの拡張子を取得します。
```powershell
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`メソッドを使用してファイルの内容をバイト配列として読み込んでいます。
```powershell
$response.ContentLength64 = $buffer.Length
$output = $response.OutputStream
$output.Write($buffer, 0, $buffer.Length)
$output.Close()
```
この部分では、レスポンスを生成しています。

まず、`ContentLength64`プロパティにバイト配列の長さを設定して、レスポンスのコンテンツの長さを指定します。

次に、`OutputStream`プロパティを使用してレスポンスの出力ストリームを取得し、`Write`メソッドを使用してバイト配列をそのストリームに書き込みます。

最後に、`Close`メソッドを使用して出力ストリームを閉じます。
```powershell
else
{
    $response.StatusCode = 404
}
```
この部分は、要求されたファイルが存在しない場合に実行されます。
`StatusCode`プロパティに`404`を設定して、クライアントにファイルが見つからないことを通知します。

# 改めてわかるWebサーバソフトのすごさ
ここまでで紹介したコードは、リクエストに対して該当するファイルを返すだけの最低限のWebサーバの動きを実装してはいるものの、実際のApacheやNginxといったソフトではこれ以外にもWebサーバとして必要な以下のような機能が備えられています。
- 複数のリクエストを同時に処理するためにマルチスレッドやマルチプロセス
- 静的ファイルを効率よく配信するためのキャッシング、圧縮、範囲リクエストといった機能
- PHP、Python、Rubyなどのサーバーサイドスクリプトを実行して動的なコンテンツを生成する機能
- SSL/TLSによる暗号化、アクセス制御、レートリミティングなど、Webサーバとしてのセキュリティを強化するための機能
- 大量のトラフィックを分散させるロードバランシング

Webサーバソフトの偉大さを再認識し、これからも有り難く使わせていただこうと思います。

# おわりに
Pythonコード1行でサクっと作業を進めるのもアリですが、時にはちゃんと処理を書いてみて普段使っている仕組みの尊さを噛む（しがむ※）のもいいなぁと思ったのでした。
※しがむ：関西の一部の地域の方言で「噛みしめる」の意。