はじめに
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に公開しています。
以上です。