C#(.NET)でシンプルなHTTPServerを立ち上げたい場合、HttpListener
というクラスが利用できます。
このクラスの裏側の実装と、WindowsのHttpServerとの関係について解説します。
そもそも HttpListener
基本的なHTTPプロトコルは、ヘッダーとボディのペアを、リクエスト/レスポンスで一問一答形式でやりとりするプロトコルです。HttpListener
はこの上り下りのヘッダーとボディ(+URL&メソッド)のみを抽象化するクラスです。HTMLやCookieとかはヘッダーやボディの中身の話になってくるので、HttpListener
の責任範囲の外になります。
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ではアドレスは不要な情報のはずです。
しかし、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を共有できるというわけです。
Http.sysには他にもキャッシュやセキュリティに関わる機能がありますが、利用するアプリケーションから見た場合はあまり意識することはないので、ここでは割愛します。
HttpListener.Prefixes
について
話を戻して、HttpListener
のPrefixes
についてですが、これは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実装を切り替えられるようになってると、いいんですけどねー😴