はじめに
この記事では、C# を使用して最も単純な HTTP サーバー クラスを実装します。これを独自のプロジェクトに埋め込むことも、コードを読んで HTTP プロトコルについて学ぶこともできます。
背景
高性能 WEB アプリケーションは通常、IIS、Apache、Tomcat などの強力な WEB サーバーにインストールされます。ただし、HTML は非常に柔軟な UI マークアップ言語であるため、どのアプリケーションやバックエンド サービスでも HTML 生成サポートを提供できます。この小さな例では、IIS や Apache などのサーバーが大量のリソースを消費します。Web リクエストを処理するには、単純な HTTP サーバーを独自に実装し、アプリケーションに組み込む必要があります。これを実装するにはクラスが 1 つだけ必要で、非常に簡単です。
コードの実装
まずクラスの使用方法を確認してから、実装の具体的な詳細を分析していきます。ここでは、HttpServer から継承し、2 つの抽象メソッド handleGETRequest と handlePOSTRequest を実装するクラスを作成します。
public class MyHttpServer : HttpServer { public MyHttpServer(int port) : base(port) { } public override void handleGETRequest(HttpProcessor p) { Console.WriteLine("request: {0}", p.http_url); p.writeSuccess(); p.outputStream.WriteLine("<html><body><h1>test server</h1>"); p.outputStream.WriteLine("Current Time: " + DateTime.Now.ToString()); p.outputStream.WriteLine("url : {0}", p.http_url); p.outputStream.WriteLine("<form method=post action=/form>"); p.outputStream.WriteLine("<input type=text name=foo value=foovalue>"); p.outputStream.WriteLine("<input type=submit name=bar value=barvalue>"); p.outputStream.WriteLine("</form>"); } public override void handlePOSTRequest(HttpProcessor p, StreamReader inputData) { Console.WriteLine("POST request: {0}", p.http_url); string data = inputData.ReadToEnd(); p.outputStream.WriteLine("<html><body><h1>test server</h1>"); p.outputStream.WriteLine("<a href=/test>return</a><p>"); p.outputStream.WriteLine("postbody: <pre class="brush:php;toolbar:false">{0}", data); } }
単純なリクエストの処理を開始するときは、ポート 8080 などのポートをリッスンする別のスレッドを開始する必要があります :
HttpServer httpServer = new MyHttpServer(8080); Thread thread = new Thread(new ThreadStart(httpServer.listen)); thread.Start();
このプロジェクトをコンパイルして実行すると、ブラウザ アドレス http://localhost:8080 のページに生成されたサンプル コンテンツが表示されます。この HTTP サーバー エンジンがどのように実装されているかを簡単に見てみましょう。
この WEB サーバーは 2 つのコンポーネントで構成されます。1 つは、指定されたポートをリッスンするために TcpListener を開始する役割を担う HttpServer クラスで、ループ内で AcceptTcpClient() メソッドを使用して TCP 接続要求を処理します。これが、 の最初のステップです。 TCP接続を処理しています。その後、リクエストは「指定された」ポートに到着し、クライアントからサーバーへの TCP 接続を初期化するために新しいポートのペアが作成されます。このポートのペアは TcpClient のセッションであるため、メイン ポートは引き続き新しい接続要求を受信できます。以下のコードから、リスナーが新しい TcpClien を作成するたびに、HttpServer クラスが新しい HttpProcessor を作成し、動作するスレッドを開始することがわかります。 HttpServer クラスには、実装する必要がある 2 つの抽象メソッドも含まれています。
public abstract class HttpServer { protected int port; TcpListener listener; bool is_active = true; public HttpServer(int port) { this.port = port; } public void listen() { listener = new TcpListener(port); listener.Start(); while (is_active) { TcpClient s = listener.AcceptTcpClient(); HttpProcessor processor = new HttpProcessor(s, this); Thread thread = new Thread(new ThreadStart(processor.process)); thread.Start(); Thread.Sleep(1); } } public abstract void handleGETRequest(HttpProcessor p); public abstract void handlePOSTRequest(HttpProcessor p, StreamReader inputData); }
このように、新しい TCP 接続は、HttpProcessor によって独自のスレッドで処理されます。HttpProcessor の仕事は、HTTP ヘッダーを正しく解析し、正しく実装された抽象メソッドを制御することです。 HTTP ヘッダーの処理を見てみましょう。HTTP リクエストのコードの最初の行は次のとおりです。
GET /myurl HTTP/1.0
process() の入力と出力を設定した後、HttpProcessor は parseRequest() メソッドを呼び出します。
public void parseRequest() { String request = inputStream.ReadLine(); string[] tokens = request.Split(' '); if (tokens.Length != 3) { throw new Exception("invalid http request line"); } http_method = tokens[0].ToUpper(); http_url = tokens[1]; http_protocol_versionstring = tokens[2]; Console.WriteLine("starting: " + request); }
HTTP リクエストは 3 つの部分で構成されているため、string.Split() メソッドを使用してそれらを 3 つの部分に分割するだけで済みます。次のステップは、クライアントから HTTP ヘッダー情報を受信して解析することです (各行)。ヘッダー情報 データは Key-Value (キーと値) の形式で保存されます。空白行は、HTTP ヘッダー情報を読み取るためにコード内で readHeaders メソッドを使用します。ここまでは、単純な GET リクエストと POST リクエストをそれぞれ正しいハンドラーに割り当てる方法を学習しました。この例では、データ送信時に対処する必要がある厄介な問題があります。つまり、サブクラス HttpServer の handlePOSTRequest メソッドを使用する場合、リクエスト ヘッダー情報に送信データの長さ情報が含まれるということです。データを正しく処理できるようにするには、データ長とコンテンツ長の情報を一緒にデータ ストリームに入れる必要があります。そうしないと、送信者は決して到着しないデータを待ってブロックされてしまいます。この状況に対処するために、あまりエレガントではありませんが非常に効果的な方法を使用します。それは、データを POST 処理メソッドに送信する前に MemoryStream にデータを読み取ることです。このアプローチは、次の理由により理想的とは言えません。送信されるデータが大きい場合、またはファイルをアップロードする場合でさえ、データをメモリにキャッシュすることは適切ではない、または不可能です。理想的な方法は、投稿の長さを制限することです。たとえば、データ長を 10MB に制限できます。
この単純なバージョンの HTTP サーバーのもう 1 つの簡略化は、content-type の戻り値です。HTTP プロトコルでは、サーバーは常にデータの MIME-Type をクライアントに送信して、必要なデータの種類をクライアントに伝えます。受け取る。 writeSuccess() メソッドでは、サーバーが常に text/html タイプを送信することがわかります。他のタイプを追加する必要がある場合は、このメソッドを拡張できます。