MochiuWiki : SUSE, EC, PCB
案内
メインページ
最近の更新
おまかせ表示
MediaWiki についてのヘルプ
ツール
リンク元
関連ページの更新状況
特別ページ
ページ情報
We ask for
Donations
検索
個人用ツール
ログイン
Toggle dark mode
名前空間
ページ
議論
表示
閲覧
ソースを閲覧
履歴を表示
C Sharpとネットワーク - HttpClientのソースを表示
提供: MochiuWiki : SUSE, EC, PCB
←
C Sharpとネットワーク - HttpClient
あなたには「このページの編集」を行う権限がありません。理由は以下の通りです:
この操作は、次のグループのいずれかに属する利用者のみが実行できます:
管理者
、new-group。
このページのソースの閲覧やコピーができます。
== 概要 == <code>HttpClient</code>クラスは、HTTPリクエストを投げる場合に使用するクラスである。<br> <br> .NET Framework 4.0以前では、それまでは<code>HttpWebRequest</code>クラス、<code>WebClient</code>が使用されていた。<br> <code>HttpClient</code>クラスは.NET Framework 4.5以降から提供された機能であり、簡単にHTTPリクエストを投げることができるクラスとして追加された。<br> <br><br> == HttpClientクラスの仕様 == <code>HttpClient</code>クラスのインスタンスを生成する時、内部では新しいソケットを開く。<br> したがって、メソッド内で<code>HttpClient</code>クラスのインスタンスを生成する場合、常に新しいソケットを開くため、リソースを消費することになる。<br> <br> <code>HttpClient</code>クラスのインスタンスを破棄した場合、ソケットが閉じるタイミングは、状態が<code>TIME_WAIT</code>に遷移して、暫く時間が経つと自動的に解放される。<br> <br> これは、リクエストする頻度が少ない場合は問題無いが、大量にリクエストを行う場合は大きなボトルネックとなる。<br> <br><br> == アンチパターン == ==== HttpClientクラス ==== <code>HttpClient</code>クラスのインスタンスの生成において、<code>IDisposable</code>インターフェースを実装しているので<code>using</code>ブロックで囲うものがある。<br> しかし、これは通信を実行するごとにソケットを開くことにより、大量のリソースを消費してリソースが枯渇する場合がある。<br> <br> 以下の例では、http://aspnetmonsters.com に対して、GETを行う10リクエストを開く。<br> <syntaxhighlight lang="c#"> // アンチパターン using System; using System.Net.Http; public class Program { public static async Task Main(string[] args) { for (var i = 0; i < 10; i++) { using(var client = new HttpClient()) { var result = await client.GetAsync("http://aspnetmonsters.com"); Console.WriteLine(result.StatusCode); } } Console.WriteLine("Connections done"); } } </syntaxhighlight> <br> 次に、アプリケーションを終了して、netstatコマンドを実行してPCのソケットの状態を確認する。<br> <br> 状態は<code>TIME_WAIT</code>であり、WebサイトをホストしているPCへの接続が開かれている状態である。<br> これは、接続は閉じられているが、ネットワーク上で遅延が発生している可能性があるため、追加のパケットが送られてくるのを待つ状態である。<br> Proto Local Address Foreign Address State TCP 10.211.55.6:12050 waws-prod-bay-017:http TIME_WAIT TCP 10.211.55.6:12051 waws-prod-bay-017:http TIME_WAIT TCP 10.211.55.6:12053 waws-prod-bay-017:http TIME_WAIT TCP 10.211.55.6:12054 waws-prod-bay-017:http TIME_WAIT TCP 10.211.55.6:12055 waws-prod-bay-017:http TIME_WAIT TCP 10.211.55.6:12056 waws-prod-bay-017:http TIME_WAIT TCP 10.211.55.6:12057 waws-prod-bay-017:http TIME_WAIT TCP 10.211.55.6:12058 waws-prod-bay-017:http TIME_WAIT TCP 10.211.55.6:12059 waws-prod-bay-017:http TIME_WAIT TCP 10.211.55.6:12060 waws-prod-bay-017:http TIME_WAIT TCP 10.211.55.6:12061 waws-prod-bay-017:http TIME_WAIT TCP 10.211.55.6:12062 waws-prod-bay-017:http TIME_WAIT ...略 <br> Windowsでは、デフォルトではTIME_WAITの状態で240秒間コネクションを保持する。<br> これは、[HKEY_LOCAL_MACHINE_SYSTEM] - [CurrentControlSet] - [Services] - [Tcpip] - [Parameters] - [TcpTimedWaitDelay]で設定される。<br> <br> OSが新しいソケットを開くことが可能なスループットには限界があるため、コネクションプールを使い切ると、以下に示すようなエラーが表示される。<br> Unable to connect to the remote server System.Net.Sockets.SocketException: Only one usage of each socket address (protocol/network address/port) is normally permitted. <br> <u>ただし、OSのシステム変数を変更するのではなく、根本的な設計の問題を解決する必要がある。</u><br> <br> ==== HttpRequestMessageクラス ==== 固定のリクエストヘッダや認証情報を付加した<code>HttpRequestMessage</code>クラスを使用する場合、共通の内部メソッドである<code>CreateRequest()</code>を使用する。<br> これは、<code>HttpRequestMessage</code>クラスのインスタンスを生成した後、<code>SendAsync()</code>メソッドを使用してメッセージを送信する。<br> <br> <syntaxhighlight lang="c#"> var getReult = await client.GetAsync("http://kirakira-service.com/"); var postRsult = await client.PostAsync("http://sugoi-service.com/"); </syntaxhighlight> <br> ==== Cookieのキャッシュ ==== Cookieの送受信を行う場合、Cookieがキャッシュされる。<br> これは、<code>HttpClient</code>クラスのインスタンス生成時において、<code>UseCookies</code>プロパティを<code>false</code>にすることにより回避できる。<br> <br> もし、プロキシサーバを実装しており、かつ、Cookieを引き継ぐ必要がある場合は、Cookieヘッダを追加する。<br> <syntaxhighlight lang="c#"> var handler = new HttpClientHandler() { UseCookies = false, // false : Cookieをキャッシュしない // true : Cookieをキャッシュする }; var client = new HttpClient(handler); </syntaxhighlight> <br><br> == ソリューション == ==== 手順 ==== <code>HttpClient</code>クラスは、<code>private</code>キーワードおよび<code>static</code>キーワードを指定したプロパティとして持つ必要がある。<br> <br> Microsoftの公式ドキュメント[https://docs.microsoft.com/ja-jp/azure/architecture/antipatterns/improper-instantiation/ 不適切なインスタンス化のアンチパターン]の中でこの問題について取り上げており、<br> HttpClientを使用した実装をする時は、インスタンスを静的変数(static)にして使用するとの記載がある。<br> <br> ==== サンプルコード ==== まず、<code>HttpClient</code>クラスのオブジェクトを生成する。<br> この時、タイムアウトの設定等はコンストラクタで行う必要がある。<br> <br> 複数の<code>HttoClient</code>クラスを使用して同時に実行する場合も、<code>HttpClient</code>はそのような使用を想定した設計となっている。<br> <br> <u>ただし、<code>static</code>キーワードを付加する場合、DNSの変更が反映されず、<code>HttpClient</code>クラスは(<code>HttpClientHandler</code>クラスを通じて)、ソケットが閉じるまでコネクションを無制限に使用し続ける。</u><br> <code>HttpClient</code>クラスは、DNS TTLを尊重しており、デフォルトではこの値は1時間である。<br> 1時間過ぎれば、<code>HttpClient</code>クラスはDNSのエントリが有効であることを検証して、必要に応じて更新されたIPアドレスに対して新しいコネクションを作成する。<br> <br> そのため、<code>HttpClient</code>クラスのオブジェクトに、コネクションを自動的にリサイクルするように指定する。<br> これは、アプリケーションの起動時において、アプリケーションで接続する全てのエンドポイント向けに1度だけ行う。 (エンドポイントが実行時に決まる場合は、決定する時に行う必要がある)<br> 時間は、1分〜5分程度に設定する方がよい。 (ホスト、ポート、スキーマが重要である)<br> <br> <syntaxhighlight lang="c#"> class SampleClass { private static readonly HttpClient httpclient = null; static SampleClass() { httpclient = new HttpClient(); } public async Task<SomeResponse> CallAPIAsync() { var sp = ServicePointManager.FindServicePoint(new Uri("{URL}")); sp.ConnectionLeaseTimeout = 60 * 1000; // コネクションのリサイクル時間 : 1分 await httpclient.PostAsync("{URL}"); // ...略 } } </syntaxhighlight> <br> また、1つの<code>HttpClient</code>クラスは1つのソケット(1つのホスト)として使用した方がよいため、<br> 異なるホストにもリクエストを投げる場合は、別の<code>HttpClient</code>クラスのオブジェクトを生成する方がよい。<br> <br><br> == HTTP通信 == ==== ベースとなるクラス ==== <syntaxhighlight lang="c#"> // 通信先のベースURL private readonly string baseUrl; // HTTPクライアント private readonly HttpClient httpClient; // コンストラクタ public SampleServiceHttpClient(string baseUrl) { this.baseUrl = baseUrl; this.httpClient = new HttpClient(); } </syntaxhighlight> <br> ==== GET ==== URLに情報を付加してGETリクエストを送受信する。<br> <br> <code>HttpClient</code>クラスの<code>SendAsync()</code>メソッドは、<code>HttpResponseMessage</code>クラスを返す。<br> レスポンスが取得できるため、ステータスコードやボディを確認および使用することができる。<br> <br> JSON以外のテキストファイルやPDFファイル等をダウンロードする場合、レスポンスのボディにファイル内容が入ることがある。<br> <syntaxhighlight lang="c#"> // URLに情報を付加してGETリクエストを送受信する public string Get(string someId) { String requestEndPoint = this.baseUrl + "/some/search/?someId=" + someId; var request = this.CreateRequest(HttpMethod.Get, requestEndPoint); string resBodyStr; var resStatusCoode = HttpStatusCode.NotFound; Task<HttpResponseMessage> response; // 通信の実行 // 引数にrequestを使用する場合は、GetAsync()やPostAsync()ではなく、SendAsync()である try { response = httpClient.SendAsync(request); resBodyStr = response.Result.Content.ReadAsStringAsync().Result; resStatusCoode = response.Result.StatusCode; } catch (HttpRequestException e) { return null; } if (!resStatusCoode.Equals(HttpStatusCode.OK)) { // レスポンスが200以外の場合 return null; } if (String.IsNullOrEmpty(resBodyStr)) { // レスポンスのボディが空の場合 return null; } return resBodyStr; } // HTTPリクエストメッセージを生成する // httpMethod : HTTPメソッドのオブジェクト // requestEndPoint : 通信先のURL private HttpRequestMessage CreateRequest(HttpMethod httpMethod, string requestEndPoint) { var request = new HttpRequestMessage(httpMethod, requestEndPoint); return this.AddHeaders(request); } // HTTPリクエストにヘッダーを追加する // request : リクエスト private HttpRequestMessage AddHeaders(HttpRequestMessage request) { request.Headers.Add("Accept", "application/json"); request.Headers.Add("Accept-Charset", "utf-8"); // 例えば、認証通過後のトークンが "Authorization: Bearer {トークンの文字列}" のように必要な場合は追加する return request; } </syntaxhighlight> <br> ==== POST : テキストファイル ==== 以下の例では、リクエストのボディにJSON形式の内容を格納して送受信している。<br> JSONは、JS、Ruby、Python、PHP等では連想配列を使用して簡単に作成できるが、C#においては<code>Dictionary</code>型を使用および変換後、<code>StringContent</code>型に情報を付加してリクエストに格納する。<br> <br> APIの認証方式において、事前に与えられているAPIキーや他の認証情報の文字列を送信する時、認証が成功した場合はアクセストークンを返却して、以降はそのアクセストークンをリクエストヘッダに追加してAPIを呼ぶことがある。<br> その場合は、以下の例と同様の処理となる。<br> <br> リクエストのボディにおいて、引数に指定の形式でAPIキー等を格納してPOSTで送信する時、レスポンスのボディの一部にトークンが格納されて返される。<br> 以下の例では、内部メソッドであるAddHeaders()において固定のヘッダの追加のみを行っているが、ここにトークンが格納される場合は追加する処理を記述することにより使い回すことができる。<br> <syntaxhighlight lang="c#"> // リクエストのボディに文字列のキーをJSON形式で格納してPOSTを送受信する public string Post(string someKey) { String requestEndPoint = this.baseUrl + "some/post"; var request = this.CreateRequest(HttpMethod.Post, requestEndPoint); var jsonDict = new Dictionary<string, string>() { {"someKey", someKey}, }; var reqBodyJson = JsonSerializer.Serialize(jsonDict, this.GetJsonOption()); var content = new StringContent(reqBodyJson, Encoding.UTF8, @"application/json"); request.Content = content; string resBodyStr; var resStatusCoode = HttpStatusCode.NotFound; Task<HttpResponseMessage> response; try { response = httpClient.SendAsync(request); resBodyStr = response.Result.Content.ReadAsStringAsync().Result; resStatusCoode = response.Result.StatusCode; } catch (HttpRequestException e) { // 通信が失敗した場合 return null; } if (!resStatusCoode.Equals(HttpStatusCode.OK)) { // レスポンスが200以外の場合 return null; } if (String.IsNullOrEmpty(resBodyStr)) { // レスポンスのボディが空の場合 return null; } // 取得した内容 return resBodyStr; } // HTTPリクエストメッセージを生成する // httpMethod : HTTPメソッドのオブジェクト // requestEndPoint : 通信先のURL private HttpRequestMessage CreateRequest(HttpMethod httpMethod, string requestEndPoint) { var request = new HttpRequestMessage(httpMethod, requestEndPoint); return this.AddHeaders(request); } // HTTPリクエストにヘッダーを追加する // request : リクエスト private HttpRequestMessage AddHeaders(HttpRequestMessage request) { request.Headers.Add("Accept", "application/json"); request.Headers.Add("Accept-Charset", "utf-8"); // 例えば、認証通過後のトークンが "Authorization: Bearer {トークンの文字列}" のように必要な場合は追加する return request; } </syntaxhighlight> <br> ==== POST : バイナリファイル ==== POSTリクエストは、テキストファイルやPDFファイル等のバイナリファイルをアップロードすることができる。<br> バイナリファイルを送信するためには、MIMEタイプを<code>multipart/form-data</code>に指定する。<br> <br> 以下の例では、マルチパートで区切ることにより、複数のデータを送信している。<br> マルチパートの1つ目は<code>StringContent</code>型でテキストデータ、マルチパートの2つ目は<code>StreamContent</code>型でバイナリファイルのデータである。<br> これら2つのデータをボディ部に設定して、POSTリクエストで送信している。<br> <syntaxhighlight lang="c#"> // テキストデータおよびバイナリデータをボディ部にセットして、POSTリクエストを送受信する。 // filePath : アップロードするバイナリファイルのパス public string PostPdfFile(string filePath) { String requestEndPoint = this.baseUrl + "resume/upload"; var request = this.CreateRequest(HttpMethod.Post, requestEndPoint); // Accept: multipart/form-dataを指定 request.Headers.Remove("Accept"); request.Headers.Add("Accept", "multipart/form-data"); // 生成するボディ部 // Content-Type : multipart/form-data; boundary = "{MultipartFormDataContentクラスが自動で設定}" // Content-Length: {MultipartFormDataContentクラスが自動で設定} var content = new MultipartFormDataContent(); // ボディ部1に--boundaryで区切られたマルチパートのテキストデータを追加 // --boundary // Content-Type: text/plain; charset=utf-8 // Content-Disposition: form-data; name=SamplePart // // Sample var multiDocumentsContent = new StringContent("Sample"); content.Add(multiDocumentsContent, "SamplePart"); StreamContent streamContent = null; var resStatusCoode = HttpStatusCode.NotFound; Task<HttpResponseMessage> response; String resBodyStr; using (var fileStream = File.OpenRead(filePath)) { streamContent = new StreamContent(fileStream); // {Content-Disposition: form-data; name=file; filename="{ファイル名}"] //streamContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") //{ // Name = "file", // FileName = Path.GetFileName(filePath) //}; // ファイル名がマルチバイト文字の場合は文字化けするため、ファイル名を手動でエンコードしてヘッダを別に作成する var finfo = new FileInfo(filePath); var headerStr = string.Format("form-data; name=\"file\"; filename=\"{0}\"", finfo.Name); var headerValueByteArray = Encoding.UTF8.GetBytes(headerStr); var encodedHeaderValue = new StringBuilder(); foreach (var b in headerValueByteArray) { encodedHeaderValue.Append((char)b); } streamContent.Headers.ContentDisposition = null; // デフォルトで用意されているので一旦削除 streamContent.Headers.Add("Content-Disposition", encodedHeaderValue.ToString()); // バイナリファイル (PDFファイル) streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/pdf"); streamContent.Headers.Add("Content-Length", fileStream.Length.ToString()); content.Add(streamContent, "file"); // 生成するボディ部 // ボディ部2に--boundaryから--boundary--までで区切られたマルチパートのバイナリデータを追加 // --boundary // Content-Disposition: form-data; name="file"; filename="{エンコードされたファイル名}" // Content-Type: application/pdf // Content-Length: {上で計算された値} // // {バイナリファイルの実体} // --boundary-- // 2つの部分を加えたボディ部をPOSTリクエストとして送信する request.Content = content; try { response = httpClient.SendAsync(request); resBodyStr = response.Result.Content.ReadAsStringAsync().Result; resStatusCoode = response.Result.StatusCode; } catch (HttpRequestException e) { // 通信が失敗した場合 return null; } fileStream.Close(); } if (!resStatusCoode.Equals(HttpStatusCode.OK)) { // レスポンスが200以外の場合 return null; } if (String.IsNullOrEmpty(resBodyStr)) { // レスポンスのボディが空の場合 return null; } return resBodyStr; } </syntaxhighlight> <br> ==== MIMEタイプ (コンテンツタイプ) ==== application/x-www-form-urlencodedおよびmultipart/form-dataは、HTTPのPOSTリクエストでデータを送信するための異なるMIMEタイプである。<br> <br> * application/x-www-form-urlencoded *: HTMLフォームデータをエンコードして送信するためのデフォルトのMIMEタイプである。 *: データはキーと値のペアになり、それらのペアは<u>key1=value1&key2=value2</u>のように<code>&</code>で区切られる。 *: <br> *: 特殊文字は、パーセントエンコーディング(%エンコーディング)される。 *: 例: スペースは%20にエンコーディングされる。 *: <br> *: 一般的に、簡単なフォームデータやクエリ文字列を送信する場合に使用する。 *: <br> * multipart/form-data *: バイナリデータや大きなファイルを含む複数のデータタイプをサポートするためのMIMEタイプである。 *: <br> *: データは複数の部分に分かれており、各部分にはヘッダがあり、ヘッダにはContent-Dispositionが含まれる。 *: このヘッダは、データがどのように処理されるべきかを示している。 *: <br> *: バイナリデータを直接送信するために使用され、一般的には、ファイルのアップロード等で使用される。 *: <br> *: <code>--boundary</code>、<code>--boundary--</code>は、各部分の開始と終了を示すために使用され、部分はそれぞれ独立して処理される。 *: どちらを選択するかは、データの種類と送信する内容に依存する。 *: 一般的には、HTMLフォームでテキストデータを送信する場合は<u>application/x-www-form-urlencoded</u>が使用され、ファイルのアップロード等でバイナリデータを送信する場合は<u>multipart/form-data</u>が使用される。 <syntaxhighlight lang="css"> /* 例: */ --boundary Content-Disposition: form-data; name="key1" value1 --boundary Content-Disposition: form-data; name="key2" value2 --boundary-- </syntaxhighlight> <br> ==== DELETE ==== <code>GET</code>と同様、HTTPメソッドの<code>DELETE</code>を指定して<code>HttpRequestMessage</code>クラスのインスタンスを生成して渡す。<br> <br> JSからの通信では、あまり使用しない。<br> <syntaxhighlight lang="c#"> // URLに情報を付加してDELETEを送受信する public bool Delete(string someId) { String requestEndPoint = this.baseUrl + "some/" + someId; var request = this.CreateRequest(HttpMethod.Delete, requestEndPoint); var resStatusCoode = HttpStatusCode.NotFound; Task<HttpResponseMessage> response; String resBodyStr; try { response = httpClient.SendAsync(request); resBodyStr = response.Result.Content.ReadAsStringAsync().Result; resStatusCoode = response.Result.StatusCode; } catch (HttpRequestException e) { // 通信が失敗した場合 return false; } if (!resStatusCoode.Equals(HttpStatusCode.OK)) { // レスポンスが200以外の場合 return false; } if (String.IsNullOrEmpty(resBodyStr)) { // レスポンスのボディが空の場合 return false; } return true; } </syntaxhighlight> <br><br> __FORCETOC__ [[カテゴリ:C_Sharp]]
C Sharpとネットワーク - HttpClient
に戻る。
案内
メインページ
最近の更新
おまかせ表示
MediaWiki についてのヘルプ
ツール
リンク元
関連ページの更新状況
特別ページ
ページ情報
We ask for
Donations
Collapse