4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

HttpListenerの裏側、HTTP Server API(WindowsAPI)

Last updated at Posted at 2022-08-28

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

そもそも HttpListener

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

cmd
REM 事前にFirewall的な何かに、アドレスの穴をあける(もちろん本物のFWへの穴あけも必要)
netsh http add urlacl url=http://hoge.localhost:80/ user=Everyone
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の参考)

HttpListener.Prefixes

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

image.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が使われています。

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(...);

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

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

image.png

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

HttpListener.Prefixes について

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

他の実装について

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

.NET Core系

HttpListener.Managed.csに実装があります。

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

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);
        ...
    }
}

Mono(Unity)

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

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);
        ...
    }
}

ASP.NETCore

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

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

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

IIS(Internet Information Services)

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

まとめ

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

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

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?