HttpListenerの裏側、HTTP Server API（WindowsAPI）

C#（.NET）でシンプルなHTTPServerを立ち上げたい場合、`HttpListener` というクラスが利用できます。
このクラスの裏側の実装と、WindowsのHttpServerとの関係について解説します。

# そもそも `HttpListener`

https://docs.microsoft.com/ja-jp/dotnet/api/system.net.httplistener?view=net-6.0

基本的なHTTPプロトコルは、ヘッダーとボディのペアを、リクエスト/レスポンスで一問一答形式でやりとりするプロトコルです。`HttpListener` はこの上り下りのヘッダーとボディ（＋URL&メソッド）のみを抽象化するクラスです。HTMLやCookieとかはヘッダーやボディの中身の話になってくるので、`HttpListener` の責任範囲の外になります。

```bat:cmd
REM 事前にFirewall的な何かに、アドレスの穴をあける（もちろん本物のFWへの穴あけも必要）
netsh http add urlacl url=http://hoge.localhost:80/ user=Everyone
```

```cs
using var listener = new HttpListener();
listener.Prefixes.Add("http://hoge.localhost:80");
listener.Start();
await listener.GetContextAsync();

// Request
Log(listener.Request.HttpMethod); // POST
Log(listener.Request.RawUrl); // http://hoge.localhost:80/login.html
Log(listener.Request.Headers); // Contents-Length: xxx
Log(listenrr.Request.InputStream); // { "userID": "hoge", "password": "0123" }

// Response
listener.Response.StatusCode = 200;
listener.Response.Headers.Add("SessionID", "334");
listener.Response.OutputStream.Write(GetUserPageHTML().ToUft8()); // <!DOCTYPE HTML><html>...</html>
listener.Response.OutputStream.Close();
```

コードの初めに出てくる `Prefix`ですが、これは受け付けるアドレスをフィルタするものです。そして、これが今回の重要ポイントとなります。

（ACLの参考）

https://tech.blog.aerie.jp/entry/2017/12/16/012207

# `HttpListener.Prefixes`

サーバーのアドレス、例えば `google.com` というのは、本来DNSサーバーでIPアドレスを解決するためのものです。TCP/IPの層で言えば、Internet層で解決されている話であり、Application層であるHTTPではアドレスは不要な情報のはずです。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/6766abd1-8056-17c2-c9f4-0ade3bd22d30.png)
> https://ja.wikipedia.org/wiki/インターネット・プロトコル・スイート

しかし、`HttpListener`では、このPrefixの指定を求められます。これは一体なぜ必要なのでしょうか？

# Http Server API 及び、Http.sys

Prefixについては一旦脇に置いておいて、`HttpListener`の実装についてみてみましょう。もちろん、CLRの各実装（FX/Mono/Core）やターゲットプラットフォームによって異なってきます。

Fx（Frameworks）とWindows向けのCore実装では HTTP Server APIが使われています。

https://docs.microsoft.com/en-us/windows/win32/api/_http/

```cs
public sealed unsafe class HttpListener : IDisposable {
    public HttpListenerContext GetContext() {
        ...
        statusCode = UnsafeNclNativeMethods.HttpApi.HttpReceiveHttpRequest(...);
        ...
    }
}

[DllImport("httpapi.dll", ...)] // Frameworks4.8
internal static extern uint HttpReceiveHttpRequest(...);
[LibraryImport("httpapi.dll", ...)] // Core
internal static unsafe partial uint HttpReceiveHttpRequest(...);
```
https://referencesource.microsoft.com/#System/net/System/Net/HttpListener.cs,1163

https://github.com/dotnet/runtime/blob/bb3be8197/src/libraries/System.Net.HttpListener/src/System/Net/Windows/HttpListener.Windows.cs#L514

Http Server API は、システムカーネルドライバのHttp.sysのインターフェースとなるAPIです。
このHttp.sysとは、システム内で代表してHTTPを受信し、登録されている各アプリケーションにルーティングするサービスです。

https://docs.microsoft.com/ja-jp/iis/get-started/introduction-to-iis/introduction-to-iis-architecture#hypertext-transfer-protocol-stack-httpsys

このような代表サービスが存在しない場合、各アプリケーションはソケットを直接開くことになります。一つのソケットを複数開くということは当然できませんので、例えばポート80を利用できるアプリケーションは一つだけとなってしまいます。一方、Http.sysでは各アプリケーションはソケットを開かず、Http.sysからのルーティングを口を開けて待っているだけなので、ポート80を共有できるというわけです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280975/e8496c9f-c176-1637-bcef-0737cd2b10c1.png)

Http.sysには他にもキャッシュやセキュリティに関わる機能がありますが、利用するアプリケーションから見た場合はあまり意識することはないので、ここでは割愛します。

# `HttpListener.Prefixes` について

話を戻して、`HttpListener`の`Prefixes`についてですが、これはHttp.sysがアプリケーションをルーティングするための情報ということです。また、`netsh`でアドレスを指定したFirewall的なものというのも、Http.sysということになります。

# 他の実装について

C#（.NET）で利用できるHttpライブラリの全てが、このHttp.sysを利用しているわけではありません。

## .NET Core系

[`HttpListener.Managed.cs`](https://github.com/dotnet/runtime/blob/bb3be819/src/libraries/System.Net.HttpListener/src/System/Net/Managed/HttpListener.Managed.cs)に実装があります。

`HttpListener.Start`で、`Prefixes`をそれぞれ`HttpEndPointManager.AddPrefix`し、`HttpEndPointListener`を作成します。`HttpEndPointManager`はソケットを直接開きます。Widnows版とクラスは同じですが、Httpを代表しているものはないので、アプリケーションをまたいでポートを共有することはできません。


```cs
class HttpListener {
    public void Start() {
        HttpEndPointManager.AddPrefix(this);
    }
}

class HttpEndPointManager {
    public static void AddListener (HttpListener listener) {
        foreach (string prefix in listener.Prefixes) {
            AddPrefix(prefix, listener);
        }
    }
    public static void AddPrefix(string prefix, HttpListener listener) {
        var addr = Dns.GetHostAddresses(host)[0];
        if (!s_ipEndPoints.TryGetValue(addr, out var p) {
            p = s_ipEndPoints[addr] = new Dictionary<int, HttpEndPointListener>();
        }
        if (!p.TryGetValue(port, out HttpEndPointListener epl)) {
            epl = p[port] = new HttpEndPointListener(listener, addr, port, secure);
        }
    }
}

internal sealed class HttpEndPointListener {
    public HttpEndPointListener(HttpListener listener, IPAddress addr, int port, bool secure) {
        ...
        _endpoint = new IPEndPoint(addr, port);
        _socket = new Socket(addr.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
        _socket.Bind(_endpoint);
        _socket.Listen(500);
        ...
    }
}
```

https://github.com/dotnet/runtime/blob/bb3be81976/src/libraries/System.Net.HttpListener/src/System/Net/Managed/HttpListener.Managed.cs

## Mono（Unity）

若干異なりますが、Windows以外の.NETCore系とほとんど同じです。

```cs
class HttpListener {
    public void Start() {
       HttpEndPointManager.AddListener(this);
    }
}

class EndPointManager {
    public static void AddListener (HttpListener listener) {
        foreach (string prefix in listener.Prefixes) {
            AddPrefix(prefix, listener);
        }
    }
    public static void AddPrefix (string prefix, HttpListener listener) {
			EndPointListener epl = GetEPListener (lp.Host, lp.Port, listener, lp.Secure);
			epl.AddPrefix (lp, listener);
    }
}

sealed class EndPointListener {
    public EndPointListener (HttpListener listener, IPAddress addr, int port, bool secure) {
        ...
        endpoint = new IPEndPoint (addr, port);
        sock = new Socket (addr.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
        sock.Bind (endpoint);
        sock.Listen (500);
        ...
    }
}
```

https://github.com/Unity-Technologies/mono/blob/f7cd421c/mcs/class/System/System.Net/HttpListener.cs#L335

## ASP.NETCore

Kestrelサーバーを内蔵しており、Windowsでも基本はHttp.sysは利用しません。

https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-6.0

ただし、Http.sysを利用するオプションもあるようです。

```cs
public static IHostBuilder CreateHostBuilder(string[] args)
    => Host
        .CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseHttpSys(options => {...})
        });
```

https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/servers/httpsys?view=aspnetcore-6.0

## IIS（Internet Information Services）

内部的にHttp.sysを利用しているようです。
……というより、IISから、Httpのルーティング機能を独立させ、Http.sysとしHttp Server APIとして他のアプリから利用可能にしたという経緯らしいです。

https://atmarkit.itmedia.co.jp/fwin2k/dnsvrguide/iis01/iis2.html

# まとめ

ちゃんとしたWebサーバーを建てる場合ASP.NETCoreを使うでしょうし、FX版も最早レガシーです。とは言え、`HttpListener`はデバッグ用のインターフェースや認証系のリダイレクト先としては、今でも大変便利なクラスです。個人的にネックに思っていたnetshやPrefixesについても必要とされる理由がスッキリわかれば、まぁそういうものかと受け入れられると思います。

`HttpListener`のバックエンドもHttp.sysとManaged実装を切り替えられるようになってると、いいんですけどねー😴