ソースに絡まるエスカルゴ

貧弱プログラマの外部記憶装置です。

【C#】プレーンなWindows環境でローカルHTTPサーバを作成する

 某大企業ではUSBフラッシュメモリが全面禁止になったりなど、プログラムを開発する環境は何かと制限されたりすることが多いです。
 インストールするアプリに制限があるような環境での開発で「HTTPリクエストを送ってその結果を受け取って処理をする」ようなプログラムを書いたとしても、テスト用のローカルサーバを作るのも何かと大変だったりします。

 そこで今回はプレーンなWindows環境でローカルHTTPサーバを作成できることがわかったので、それについての記事になります。

 参考資料に挙げているページ様の内容を自分なりに改造してみた形となるので、元の情報を知りたい方はそちらを参照してください。


 では、始めます。


1:プレーンなWindows環境でもC#は使える
 C#の開発にはVisual Studioのインストールが必要だという認識の方も多いと思いますが、実は真っ新なWindows環境でもC#の開発ができます。

 Visual Studioをインストールしていない状態でも、以下のNET Frameworkのフォルダが存在していると思います。

C:\Windows\Microsoft.NET\Framework64

 C#.NET Frameworkの一部なので、.NET Frameworkがあれば(バージョンの違いやメモ帳などエディタは限られますが)C#の開発が可能なのです。

 では、実際にC#のプログラムを書いて実行してみます。

 メモ帳を開いて以下の内容をコピペして「hello.cs」という名前で保存します。

・hello.cs

using System;

class Hello{
  public static void Main(){
    Console.WriteLine("Hello World!");
  }
}

 次にコマンドプロンプトPowerShellを立ち上げ、hello.csを保存したフォルダへ移動します。

cd [hello.csを保存しているフォルダ]

 移動できたら以下のコマンドを実行してexe化します。

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe hello.cs

 実行すると、以下のようなメッセージが表示されます。

> C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe hello.cs
Microsoft (R) Visual C# Compiler version 4.8.9032.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240

 エラーが出たり実行できなかった場合は、対象ファイルと同じ階層にちゃんと移動しているか、コマンドのバージョン部分が合っているかなどを確認してみてください。

 コマンド実行後は、同じ階層に「hello.exe」が作成されているはずです。

 あとはコマンドを実行したコンソールで「hello.exe」と叩くだけでexeを実行できます。

 実際に実行すると以下のようになります。

>hello.exe
Hello World!

 このようにプレーンなWindows環境でもC#のプログラムを書いて動かせます。


2:C#でローカルHTTPサーバを作成する
 では本題のローカルHTTPサーバを作成していきます。と言っても、hello worldの時と同じようにコピペしてexe化するだけです。

 メモ帳を開いて以下の内容をコピペし「HttpLocalServer.cs」という名前で保存します。

・HttpLocalServer.cs

using System;
using System.IO;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Web;

public class HttpLocalServer
{
    private readonly static string DirSep = Path.DirectorySeparatorChar.ToString();
    private readonly static string ParentMid = DirSep + ".." + DirSep;
    private readonly static string ParentLast = DirSep + "..";

    private static string s_root = "./";
    private static string s_prefix = null;
    private static string PORT = "8001";

    public static void Main(string[] args)
    {
        try
        {
            ParseOptions(args);
            string prefixPath = WebUtility.UrlDecode(
                Regex.Replace(s_prefix, "https?://[^/]*", ""));

            using (HttpListener listener = new HttpListener())
            {
                listener.Prefixes.Add(s_prefix);
                listener.Start();
                Console.WriteLine("--- Start ---");
                Console.WriteLine("http://localhost:" + PORT + "/normal");
                Console.WriteLine("http://localhost:" + PORT + "/empty");
                Console.WriteLine("http://localhost:" + PORT + "/error");

                while (true)
                {
                    HttpListenerContext context = listener.GetContext();
                    HttpListenerRequest request = context.Request;

                    using (HttpListenerResponse response = context.Response)
                    {

                        Console.WriteLine("-------------------------");

                        ShowRequestData(request);

                        string rawPath = WebUtility.UrlDecode(
                            Regex.Replace(request.RawUrl, "[?;].*$", ""))
                            .Substring(prefixPath.Length-1);

                        Console.WriteLine("called " + rawPath);

                        string responseData = "";
                        response.StatusCode = 200;

                        if (rawPath == "/normal")
                        {
                            responseData = "normal";
                        }
                        else if (rawPath == "/empty")
                        {
                            responseData = "";
                        }
                        else if (rawPath == "/error")
                        {
                            responseData = "NG error\n";
                        }
                        else
                        {
                            response.StatusCode = 502; // BadGateway
                        }

                        byte[] content = Encoding.UTF8.GetBytes(responseData);
                        response.ContentType = "text/plain";
                        response.ContentLength64 = content.Length;
                        response.OutputStream.Write(content, 0, content.Length);

                        Console.WriteLine("[{0}] \"{1} {2} HTTP/{3}\" {4} {5}",
                            DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss K"),
                            request.HttpMethod,
                            request.RawUrl,
                            request.ProtocolVersion,
                            response.StatusCode,
                            response.ContentLength64);
                    }
                }
            }
        }
        catch (Exception e)
        {
            Console.Error.WriteLine(e);
        }
    }

    private static void ParseOptions(string[] args)
    {
        string port = PORT;
        string host = "+";

        for (int i = 0; i < args.Length; i++)
        {
            if (args[i].Equals("-t"))
            {
                s_prefix = "http://+:80/Temporary_Listen_Addresses/";
            }
            else if (args[i].Equals("-p") && i+1 < args.Length)
            {
                port = args[++i];
            }
            else if (args[i].Equals("-b") && i+1 < args.Length)
            {
                host = args[++i];
            }
            else if (args[i].Equals("-r") && i+1 < args.Length)
            {
                s_root = args[++i];
            }
            else if (args[i].Equals("-P") && i+1 < args.Length)
            {
                s_prefix = args[++i];
            }
            else
            {
                Console.Error.WriteLine(
                    "usage: {0} [-r DIR] [-p PORT] [-b ADDR]\n" +
                    "    or {0} [-r DIR] [-t]\n" +
                    "    or {0} [-r DIR] [-P PREFIX]",
                    AppDomain.CurrentDomain.FriendlyName);
                Environment.Exit(0);
            }
        }

        if (s_prefix == null)
        {
            s_prefix = string.Format("http://{0}:{1}/", host, port);
        }
    }

    private static void ShowRequestData (HttpListenerRequest request)
    {
        if (!request.HasEntityBody)
        {
            Console.WriteLine("No client data was sent with the request.");
            return;
        }
        System.IO.Stream body = request.InputStream;
        System.Text.Encoding encoding = request.ContentEncoding;
        System.IO.StreamReader reader = new System.IO.StreamReader(body, encoding);
        string s = reader.ReadToEnd();
        Console.WriteLine(s);
        body.Close();
        reader.Close();
    }
}

 少し解説するとクラスの最初の方にある「PORT」の定数でポート番号を指定しています。
 ShowRequestData関数でrequest bodyの中身を取得してコンソール上に表示させています。
 rawPath変数にはURLのAPI部分(http://localhost:8001/xxxのxxx部分)を取得しています。
 その後API部分毎の処理を記述し、responseの返す値を変更しています。
 処理の最後にresponseを作成し、処理が終了した日時を表示しています。

 ファイルの作成できたら、先ほどと同じようにコマンドプロンプトPowerShellを立ち上げ、HttpLocalServer.csを保存したフォルダへ移動します。

cd [HttpLocalServer.csを保存しているフォルダ]

 移動できたら以下のコマンドを実行してexe化します。

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe HttpLocalServer.cs

 コマンドを実行すると同階層に「HttpLocalServer.exe」ができていると思います。

 このローカルHTTPサーバを実行したい場合は「HttpLocalServer.exe」を右クリックし「管理者として実行」をクリックして実行します。

 許可して実行すると、コンソールが立ち上がって以下の表示が出てきます。

--- Start ---
http://localhost:8001/normal
http://localhost:8001/empty
http://localhost:8001/error

 このコンソールを立ち上げた状態でブラウザなどから表示されているURLにアクセスしてみてください。

 例えば「http://localhost:8001/normal」にアクセスすると、レスポンスとして「normal」の文字列が返ってきます。
 そしてコンソール上では以下のようにアクセスした際のrequest bodyの中身(ブラウザからなので今回は何もない)と呼ばれたAPI名、呼び出された時間が表示されます。

-------------------------
No client data was sent with the request.
called /normal
[2023-11-09 12:47:17 +09:00] "GET /normal HTTP/1.1" 200 6
-------------------------
No client data was sent with the request.
called /favicon.ico
[2023-11-09 12:47:17 +09:00] "GET /favicon.ico HTTP/1.1" 502 0

 サーバを終了させたい場合は、コンソール自体を閉じるか、Ctrl + Cキーで終了させます。


 以上がプレーンなWindows環境でローカルHTTPサーバを作成する方法になります。

 正直C#に慣れていないので中身をちゃんと理解しているわけではないですが、制限された環境でも簡単なテスト用ローカルHTTPサーバであれば作成できることがわかったので、そのような環境で苦しんでいる方々の助けになれば幸いです。


・参考資料