Help us understand the problem. What is going on with this article?

[C#] REST-APIサーバーを構築する

はじめに

C# の HttpListener クラスを使用して簡易的なREST APIサーバーを構築することができます。

環境設定

今回実行した環境は下記の通りです。

実行環境
Microsoft .NET Framework 3.5

外部からのアクセスを有効にするために、APIサーバーがデプロイされているWindowsで下記のアクセス設定を行います。

netshの設定

  • コマンドプロンプトで以下のコマンドを実行します。
コマンドプロンプト
netsh http add urlacl url=http://+:8888/ user=everyone

ファイアウォールの設定

  • コントロールパネルでファイアウォールを開いて「詳細設定」をクリックします。
  • 受信の規則で目的のルールをクリックするか新規に作成します。
    • 次のプログラム: system
    • プロトコルの種類: TCP
    • ローカルポート: 特定のポート, 8888

設定ファイル

configでAPIサーバーの設定を行います。

app.config
<setting name="API_PATH" serializeAs="String">
    <value>api</value>
</setting>
<setting name="API_PORT" serializeAs="String">
    <value>8888</value>
</setting>
  • 「API_PATH」は、URLを指定します。
  • 「API_PORT」は、ポート番号を指定します。

REST APIサーバーの実装

APIサーバーの起動の実装は下記の通りです。

ApiService.cs
class ApiService
{
    private static Logger log = Logger.GetInstance();
    private HttpListener listener;
    private ControllerMapper mapper = new ControllerMapper();

    /// <summary>
    /// APIサービスを起動する
    /// </summary>
    public void Start()
    {
        try
        {
            // HTTPサーバーを起動する
            this.listener = new HttpListener();
            this.listener.Prefixes.Add(String.Format("http://+:{0}/{1}/", Settings.Default.API_PORT, Settings.Default.API_PATH));
            this.listener.Start();

            while (this.listener.IsListening)
            {
                IAsyncResult result = this.listener.BeginGetContext(OnRequested, this.listener);
                result.AsyncWaitHandle.WaitOne();
            }
        }
        catch (Exception ex)
        {
            /* ~エラー処理~ */
        }
    }

    /// <summary>
    /// リクエスト時の処理を実行する
    /// </summary>
    /// <param name="result">結果</param>
    private void OnRequested(IAsyncResult result)
    {
        HttpListener clsListener = (HttpListener)result.AsyncState;
        if (!clsListener.IsListening)
        {
            return;
        }

        HttpListenerContext context = clsListener.EndGetContext(result);
        HttpListenerRequest req = context.Request;
        HttpListenerResponse res = context.Response;

        try
        {
            mapper.Execute(req, res);
        }
        catch (Exception ex)
        {
            log.Error(ex.ToString());
        }
        finally
        {
            try
            {
                if (null != res)
                {
                    res.Close();
                }
            }
            catch (Exception clsEx)
            {
                log.Error(clsEx.ToString());
            }
        }
    }

result.AsyncWaitHandle.WaitOne(); でサーバーにアクセスがあるまで処理を待ちます。アクセスがあると OnRequested メソッドが呼び出されます。このメソッドは異なるスレッドで呼び出されます。

APIサーバーのパス毎の振り分け処理の実装は下記の通りです。

ControllerMapper.cs
class ControllerMapper
{
    private const string CONTENT_TYPE_JSON = "application/json";
    private static Logger log = Logger.GetInstance();

    /// <summary>
    /// コンストラクタ
    /// </summary>
    public ControllerMapper()
    {
    }

    /// <summary>
    /// 実行する
    /// </summary>
    /// <param name="req">リクエスト情報</param>
    /// <param name="res">レスポンス情報</param>
    public void Execute(HttpListenerRequest req, HttpListenerResponse res)
    {
        StreamReader reader = null;
        StreamWriter writer = null;
        string reqBody = null;
        string resBoby = null;

        try
        {
            res.StatusCode = (int)HttpStatusCode.OK;
            res.ContentType = CONTENT_TYPE_JSON;
            res.ContentEncoding = Encoding.UTF8;

            reader = new StreamReader(req.InputStream);
            writer = new StreamWriter(res.OutputStream);
            reqBody = reader.ReadToEnd();

            LogStart(req, reqBody);
            resBoby = ExecuteController(req, res, reqBody);
        }
        catch (Exception ex)
        {
            /* ~エラー処理~ */
        }
        finally
        {
            try
            {
                writer.Write(resBoby);
                writer.Flush();

                if (null != reader)
                {
                    reader.Close();
                }
                if (null != writer)
                {
                    writer.Close();
                }
                LogEnd(res, resBoby);
            }
            catch (Exception clsEx)
            {
                log.Error(clsEx.ToString());
            }
        }
    }

    /// <summary>
    /// リクエストログを出力する
    /// </summary>
    /// <param name="req">リクエスト情報</param>
    /// <param name="body">リクエストボディ文字列</param>
    private void LogStart(HttpListenerRequest req, string body)
    {
        log.Info("########## Request [start] ##########");
        log.Info(String.Format(">> {0} {1}", req.HttpMethod, GetApiPath(req.RawUrl)));
        log.Info(">> IP: " + req.UserHostAddress);
        log.Info(">> UserAgent: " + req.UserAgent);
        log.Info(">> Header: " + ToNameValueString(req.Headers));
        if ("GET".Equals(req.HttpMethod))
        {
            if (0 < req.QueryString.Count)
            {
                log.Info(">> Query: " + ToNameValueString(req.QueryString));

            }
        }
        else
        {
            if (!string.IsNullOrEmpty(body))
            {
                log.Info(">> Body: " + body);
            }
        }
        log.Info("########## Request [end] ##########");
    }

    /// <summary>
    /// レスポンスログを出力する
    /// </summary>
    /// <param name="res">レスポンス情報</param>
    /// <param name="body">レスポンスボディ文字列</param>
    private void LogEnd(HttpListenerResponse res, string body)
    {
        log.Info("########## Response [start] ##########");
        log.Info(">> HTTP Status: " + res.StatusCode);
        log.Info(">> Header: " + ToNameValueString(res.Headers));
        if (!string.IsNullOrEmpty(body))
        {
            log.Info(">> Body: " + body);
        }
        log.Info("########## Response [end] ##########");
    }

    /// <summary>
    /// Name-Value文字列を取得する
    /// </summary>
    /// <param name="nvc">nvc</param>
    /// <returns>文字列</returns>
    private string ToNameValueString(NameValueCollection nvc)
    {
        return String.Join(", ", Array.ConvertAll(nvc.AllKeys, key => String.Format("{0}:{1}", key, nvc[key])));
    }

    /// <summary>
    /// APIパスを取得する
    /// </summary>
    /// <param name="srcPath">URLパス</param>
    /// <returns>APIパス</returns>
    private string GetApiPath(string srcPath)
    {
        string[] path = srcPath.Split('?');
        string condition = String.Format(@"^/{0}", Settings.Default.API_PATH);
        return Regex.Replace(path[0], condition, "");
    }

    /// <summary>
    /// APIコントローラを実行する
    /// </summary>
    /// <param name="req">リクエスト情報</param>
    /// <param name="res">レスポンス情報</param>
    /// <param name="reqBody">リクエストボディ</param>
    /// <returns>レスポンス文字列</returns>
    private string ExecuteController(HttpListenerRequest req, HttpListenerResponse res, string reqBody)
    {
        string path = GetApiPath(req.RawUrl);

        if ("/user/".Equals(path))
        {
            switch (req.HttpMethod)
            {
                case "GET":
                    return (new ReadUserController(req, res, reqBody)).Execute();
                case "POST":
                    return (new CreateUserController(req, res, reqBody)).Execute();
                case "PUT":
                    return (new UpdateUserController(req, res, reqBody)).Execute();
                case "DELETE":
                    return (new DeleteUserController(req, res, reqBody)).Execute();
            }
        }
        if ("/users/".Equals(path) && "GET".Equals("GET"))
        {
            return (new ReadUsersController(req, res, reqBody)).Execute();
        }
        return "";
    }
}

ExecuteController メソッドでAPIパス毎にコントローラーを振り分けています。例えば、POST http:/localhost:8888/api/user/ でサーバーにアクセスすると CreateUserController コントローラーが呼び出されます。

さいごに

ソースコードをGitHubに公開しています。

ソースファイルはこちら

以上です。

yun_bow
サービス志向エンジニアです。プログラミングを使ったモノづくりが好きです。AWS、Python、GO言語を勉強中。 こちらで投稿した記事は、所属会社の公式見解を示すものではないです。
pa-rk
Webアプリ、スマホアプリの開発を手掛ける技術者集団です。
https://www.pa-rk.co.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした