17
24

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 3 years have passed since last update.

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

Last updated at Posted at 2020-04-12

はじめに

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に公開しています。

ソースファイルはこちら

以上です。

17
24
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
17
24

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?