فصل ۱۶: HttpClient، پیامهای HTTP، احراز هویت، Headerها، فرمها و Cookieها
فروش یا انتشار این ترجمه منوط به داشتن مجوز لازم از صاحب حقوق اثر است.
URI نسبی و اهمیت Slash پایانی
یک Uri نسبی، مانند page.html در مثال قبلی، اگر تقریباً هر property یا متدی غیر از IsAbsoluteUri و ToString() را روی آن فراخوانی کنید، Exception ایجاد میکند. میتوانید یک Uri نسبی را مستقیماً به این صورت بسازید:
Uri u = new Uri ("page.html", UriKind.Relative);
برای نمونه، در یک وبسرور سنتی، اگر URI به شکل http://www.albahari.com/nutshell/ باشد، میتوان انتظار داشت وبسرور HTTP به زیرشاخهٔ nutshell در پوشهٔ وب سایت مراجعه کند و سند پیشفرض، که معمولاً index.html است، را برگرداند. بدون Slash پایانی، وبسرور بهجای آن بهدنبال فایلی به نام nutshell، بدون پسوند، مستقیماً در پوشهٔ ریشهٔ سایت میگردد؛ چیزی که معمولاً خواستهٔ شما نیست. اگر چنین فایلی وجود نداشته باشد، بیشتر وبسرورها فرض میکنند کاربر اشتباه تایپ کرده است و خطای 301 Permanent Redirect را برمیگردانند و پیشنهاد میکنند کلاینت درخواست را با Slash پایانی تکرار کند. یک کلاینت HTTP در .NET بهطور پیشفرض درست مانند مرورگر وب به 301 بهشکل شفاف پاسخ میدهد و درخواست را با URI پیشنهادی دوباره میفرستد. بنابراین اگر Slash پایانی لازم را حذف کنید، درخواست همچنان کار میکند، اما یک رفتوبرگشت اضافی و غیرضروری خواهد داشت.
کلاس Uri متدهای کمکی static نیز دارد؛ از جمله EscapeUriString() که با تبدیل همهٔ نویسههایی که مقدار ASCII آنها بزرگتر از 127 است به نمایش hexadecimal، یک رشته را به URL معتبر تبدیل میکند. متدهای CheckHostName() و CheckSchemeName() یک رشته را دریافت میکنند و بررسی میکنند از نظر syntax برای property مربوط معتبر است یا نه؛ هرچند تلاش نمیکنند وجود واقعی host یا URI را تعیین کنند.
HttpClient
کلاس HttpClient یک API مدرن برای عملیات کلاینت HTTP ارائه میدهد و جایگزین نوعهای قدیمی WebClient و WebRequest/WebResponse شده است که اکنون obsolete علامتگذاری شدهاند.
HttpClient در پاسخ به رشد Web APIهای مبتنی بر HTTP و سرویسهای REST نوشته شد و هنگام کار با پروتکلهایی پیچیدهتر از صرفاً دریافت یک صفحهٔ وب، تجربهٔ مناسبی فراهم میکند. بهطور مشخص:
- یک نمونهٔ واحد از
HttpClient میتواند درخواستهای concurrent را مدیریت کند و با قابلیتهایی مانند Headerهای سفارشی، Cookieها و schemeهای احراز هویت بهخوبی کار میکند. HttpClient اجازه میدهد message handlerهای سفارشی بنویسید و به pipeline متصل کنید. این قابلیت Mocking در Unit Testها و ساخت pipelineهای سفارشی برای Logging، Compression، Encryption و موارد مشابه را ممکن میکند.
HttpClient برای Headerها و Content یک type system غنی و قابل گسترش دارد.
سادهترین شیوهٔ استفاده از HttpClient این است که آن را نمونهسازی کنید و سپس یکی از متدهای Get* را با یک URI فراخوانی کنید:
string html = await new HttpClient().GetStringAsync ("http://linqpad.net");
متدهای GetByteArrayAsync و GetStreamAsync نیز وجود دارند. همهٔ متدهای I/O-bound در HttpClient asynchronous هستند.
برخلاف پیشینیانش یعنی WebRequest/WebResponse، برای دستیابی به بهترین performance با HttpClient باید همان نمونه را دوباره استفاده کنید؛ در غیر این صورت، کارهایی مانند DNS resolution ممکن است بدون نیاز تکرار شوند و Socketها بیش از حد لازم باز بمانند. HttpClient عملیات concurrent را مجاز میداند؛ بنابراین کد زیر قانونی است و دو صفحهٔ وب را همزمان دانلود میکند:
var client = new HttpClient();
var task1 = client.GetStringAsync ("http://www.linqpad.net");
var task2 = client.GetStringAsync ("http://www.albahari.com");
Console.WriteLine (await task1);
Console.WriteLine (await task2);
HttpClient دارای propertyهای Timeout و BaseAddress است؛ BaseAddress یک URI را به ابتدای هر درخواست اضافه میکند. خود HttpClient تا حدی یک پوستهٔ نازک است و بیشتر propertyهایی که ممکن است انتظار داشته باشید روی آن ببینید، در کلاس دیگری به نام HttpClientHandler تعریف شدهاند. برای دسترسی به این کلاس، آن را نمونهسازی کرده و نمونه را به constructor مربوط به HttpClient میدهید:
var handler = new HttpClientHandler { UseProxy = false };
var client = new HttpClient (handler);
...
در این مثال به handler گفتهایم پشتیبانی از Proxy را غیرفعال کند؛ این کار گاهی با حذف هزینهٔ تشخیص خودکار Proxy باعث بهبود performance میشود. propertyهایی برای کنترل Cookie، Redirect خودکار، Authentication و موارد دیگر نیز وجود دارند که در بخشهای بعد توضیح داده میشوند.
GetAsync و پیامهای پاسخ (Response Messages)
متدهای GetStringAsync، GetByteArrayAsync و GetStreamAsync میانبُرهای مناسبی برای فراخوانی متد عمومیتر GetAsync هستند؛ متدی که یک Response Message برمیگرداند:
var client = new HttpClient();
// The GetAsync method also accepts a CancellationToken.
HttpResponseMessage response = await client.GetAsync ("http://...");
response.EnsureSuccessStatusCode();
string html = await response.Content.ReadAsStringAsync();
HttpResponseMessage propertyهایی برای دسترسی به Headerها و HTTP StatusCode در اختیار میگذارد. یک Status Code ناموفق مانند 404، یعنی Not Found، بهخودیخود Exception ایجاد نمیکند؛ مگر اینکه صریحاً EnsureSuccessStatusCode را فراخوانی کنید. با این حال، خطاهای ارتباطی یا DNS باعث Exception میشوند.
HttpContent متدی به نام CopyToAsync دارد که برای نوشتن به Stream دیگری به کار میرود و برای ذخیرهٔ خروجی در فایل مفید است:
using (var fileStream = File.Create ("linqpad.html"))
await response.Content.CopyToAsync (fileStream);
GetAsync یکی از چهار متدی است که با چهار Verb اصلی HTTP متناظرند؛ سه مورد دیگر PostAsync، PutAsync و DeleteAsync هستند. PostAsync در بخش «Uploading Form Data» توضیح داده میشود.
SendAsync و پیامهای درخواست (Request Messages)
GetAsync، PostAsync، PutAsync و DeleteAsync همگی میانبُرهایی برای فراخوانی SendAsync هستند؛ همان متد سطحپایین واحدی که همهٔ عملیات در نهایت به آن میرسند. برای استفاده از آن، ابتدا یک HttpRequestMessage میسازید:
var client = new HttpClient();
var request = new HttpRequestMessage (HttpMethod.Get, "http://...");
HttpResponseMessage response = await client.SendAsync (request);
response.EnsureSuccessStatusCode();
...
نمونهسازی HttpRequestMessage به این معناست که میتوانید propertyهای Request، مانند Headerها و خود Content، را سفارشی کنید و در نتیجه داده Upload کنید.
ارسال داده و HttpContent
پس از نمونهسازی HttpRequestMessage، با مقداردهی property آن به نام Content میتوانید Content را Upload کنید. نوع این property یک کلاس abstract به نام HttpContent است. .NET برای انواع مختلف Content زیرکلاسهای concrete زیر را فراهم میکند؛ و البته میتوانید نوع خودتان را هم بنویسید:
ByteArrayContentStringContentFormUrlEncodedContentStreamContent
برای مثال:
var client = new HttpClient (new HttpClientHandler { UseProxy = false });
var request = new HttpRequestMessage (
HttpMethod.Post, "http://www.albahari.com/EchoPost.aspx");
request.Content = new StringContent ("This is a test");
HttpResponseMessage response = await client.SendAsync (request);
response.EnsureSuccessStatusCode();
Console.WriteLine (await response.Content.ReadAsStringAsync());
HttpMessageHandler
پیشتر گفتیم بیشتر propertyهای مربوط به سفارشیسازی Requestها نه در HttpClient، بلکه در HttpClientHandler تعریف شدهاند. HttpClientHandler در واقع زیرکلاسی از کلاس abstract به نام HttpMessageHandler است که به این صورت تعریف میشود:
public abstract class HttpMessageHandler : IDisposable
{
protected internal abstract Task<HttpResponseMessage> SendAsync
(HttpRequestMessage request, CancellationToken cancellationToken);
public void Dispose();
protected virtual void Dispose (bool disposing);
}
متد SendAsync از داخل SendAsync در HttpClient فراخوانی میشود. HttpMessageHandler بهاندازهای ساده است که بهراحتی میتوان از آن subclass ساخت و نقطهٔ توسعهپذیری مناسبی در HttpClient فراهم میکند.
Unit Testing و Mocking
میتوانیم از HttpMessageHandler subclass بسازیم تا یک handler ساختگی برای کمک به Unit Testing داشته باشیم:
class MockHandler : HttpMessageHandler
{
Func <HttpRequestMessage, HttpResponseMessage> _responseGenerator;
public MockHandler
(Func <HttpRequestMessage, HttpResponseMessage> responseGenerator)
{
_responseGenerator = responseGenerator;
}
protected override Task <HttpResponseMessage> SendAsync
(HttpRequestMessage request, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var response = _responseGenerator (request);
response.RequestMessage = request;
return Task.FromResult (response);
}
}
Constructor این کلاس تابعی را میگیرد که به Mocker میگوید چگونه از روی یک Request، Response تولید کند. این منعطفترین رویکرد است، چون یک Handler واحد میتواند چند Request مختلف را تست کند.
SendAsync بهواسطهٔ Task.FromResult بهصورت synchronous عمل میکند. میتوانستیم با برگرداندن Task<HttpResponseMessage> از Response Generator، رفتار asynchronous را حفظ کنیم، اما چون انتظار داریم تابع Mocking کوتاهمدت باشد، این کار فایدهای ندارد. روش استفاده از Mock Handler ما:
var mocker = new MockHandler (request =>
new HttpResponseMessage (HttpStatusCode.OK)
{
Content = new StringContent ("You asked for " + request.RequestUri)
});
var client = new HttpClient (mocker);
var response = await client.GetAsync ("http://www.linqpad.net");
string result = await response.Content.ReadAsStringAsync();
Assert.AreEqual ("You asked for http://www.linqpad.net/", result);
Assert.AreEqual متدی است که انتظار دارید در یک Framework مربوط به Unit Testing، مانند NUnit، پیدا کنید.
زنجیرهکردن Handlerها با DelegatingHandler
با subclass کردن DelegatingHandler میتوانید یک Message Handler بسازید که Handler دیگری را فراخوانی کند و در نتیجه زنجیرهای از Handlerها شکل بگیرد. این روش برای پیادهسازی پروتکلهای سفارشی Authentication، Compression و Encryption قابل استفاده است. مثال زیر یک Logging Handler ساده را نشان میدهد:
class LoggingHandler : DelegatingHandler
{
public LoggingHandler (HttpMessageHandler nextHandler)
{
InnerHandler = nextHandler;
}
protected async override Task <HttpResponseMessage> SendAsync
(HttpRequestMessage request, CancellationToken cancellationToken)
{
Console.WriteLine ("Requesting: " + request.RequestUri);
var response = await base.SendAsync (request, cancellationToken);
Console.WriteLine ("Got response: " + response.StatusCode);
return response;
}
}
توجه کنید هنگام override کردن SendAsync، asynchronous بودن را حفظ کردهایم. افزودن modifier به نام async هنگام override کردن متدی که Task برمیگرداند کاملاً قانونی است و در این مورد مطلوب نیز هست.
راهحل بهتر از نوشتن مستقیم در Console این است که Constructor نوعی شیء Logging دریافت کند. حتی بهتر آن است که دو Delegate از نوع Action<T> بگیرد که مشخص کنند Request و Response چگونه Log شوند.
Proxyها
Proxy Server یک واسطه است که Requestهای HTTP میتوانند از طریق آن Route شوند. سازمانها گاهی Proxy Server را تنها راه دسترسی کارکنان به Internet قرار میدهند؛ عمدتاً چون Security را سادهتر میکند. Proxy آدرس خودش را دارد و میتواند Authentication مطالبه کند تا فقط کاربران منتخب روی LAN بتوانند به Internet دسترسی داشته باشند.
برای استفاده از Proxy با HttpClient، ابتدا یک HttpClientHandler بسازید، property آن به نام Proxy را مقداردهی کنید و سپس آن را به Constructor HttpClient بدهید:
WebProxy p = new WebProxy ("192.178.10.49", 808);
p.Credentials = new NetworkCredential ("username", "password", "domain");
var handler = new HttpClientHandler { Proxy = p };
var client = new HttpClient (handler);
...
HttpClientHandler property دیگری به نام UseProxy دارد که بهجای null کردن Proxy میتوانید آن را false کنید تا Autodetection غیرفعال شود.
اگر هنگام ساخت NetworkCredential یک Domain ارائه کنید، پروتکلهای Authentication مبتنی بر Windows استفاده میشوند. برای استفاده از کاربر Windows که اکنون Login است، مقدار static به نام CredentialCache.DefaultNetworkCredentials را به property Credentials مربوط به Proxy اختصاص دهید.
بهجای مقداردهی مکرر Proxy، میتوانید Default سراسری را چنین تنظیم کنید:
HttpClient.DefaultWebProxy = myWebProxy;
احراز هویت (Authentication)
میتوانید Username و Password را به این صورت در اختیار HttpClient قرار دهید:
string username = "myuser";
string password = "mypassword";
var handler = new HttpClientHandler();
handler.Credentials = new NetworkCredential (username, password);
var client = new HttpClient (handler);
...
این روش با پروتکلهای Authentication مبتنی بر Dialog مانند Basic و Digest کار میکند و از طریق کلاس AuthenticationManager قابل گسترش است. همچنین NTLM و Kerberos ویندوز را پشتیبانی میکند، به شرط آنکه هنگام ساخت NetworkCredential نام Domain را نیز وارد کنید. اگر میخواهید از کاربر فعلی Windows استفاده کنید، میتوانید property Credentials را null باقی بگذارید و بهجای آن UseDefaultCredentials را true کنید.
وقتی Credentialها را ارائه میکنید، HttpClient بهصورت خودکار یک پروتکل سازگار را مذاکره میکند. گاهی چند انتخاب وجود دارد. برای نمونه، Response اولیهٔ صفحهٔ Web Mail یک Microsoft Exchange Server ممکن است Headerهای زیر را داشته باشد:
HTTP/1.1 401 Unauthorized
Content-Length: 83
Content-Type: text/html
Server: Microsoft-IIS/6.0
WWW-Authenticate: Negotiate
WWW-Authenticate: NTLM
WWW-Authenticate: Basic realm="exchange.somedomain.com"
X-Powered-By: ASP.NET
Date: Sat, 05 Aug 2006 12:37:23 GMT
کد 401 نشان میدهد Authorization لازم است و Headerهای WWW-Authenticate مشخص میکنند چه پروتکلهای Authentication فهمیده میشوند. اگر HttpClientHandler را با Username و Password صحیح تنظیم کرده باشید، این پیام از دید شما پنهان خواهد ماند، زیرا Runtime بهطور خودکار یک پروتکل سازگار را انتخاب میکند و سپس Request اصلی را با یک Header اضافی دوباره ارسال میکند. نمونه:
Authorization: Negotiate TlRMTVNTUAAABAAAt5II2gjACDArAAACAwACACgAAAAQ
ATmKAAAAD0lVDRdPUksHUq9VUA==
این سازوکار شفافیت ایجاد میکند، اما با هر Request یک رفتوبرگشت اضافه تولید میکند. میتوانید با true کردن property به نام PreAuthenticate در HttpClientHandler، این رفتوبرگشت اضافی را برای Requestهای بعدی به همان URI حذف کنید.
CredentialCache
با یک شیء CredentialCache میتوانید پروتکل Authentication مشخصی را تحمیل کنید. Credential Cache شامل یک یا چند شیء NetworkCredential است که هرکدام بر اساس یک Protocol و یک URI Prefix کلیدگذاری میشوند. مثلاً ممکن است هنگام Login به Exchange Server بخواهید از Basic اجتناب کنید، زیرا Password را بهصورت Plain Text منتقل میکند:
CredentialCache cache = new CredentialCache();
Uri prefix = new Uri ("http://exchange.somedomain.com");
cache.Add (prefix, "Digest", new NetworkCredential ("joe", "passwd"));
cache.Add (prefix, "Negotiate", new NetworkCredential ("joe", "passwd"));
var handler = new HttpClientHandler();
handler.Credentials = cache;
...
پروتکل Authentication بهصورت یک String مشخص میشود. مقادیر معتبر شامل Basic، Digest، NTLM، Kerberos و Negotiate هستند.
در این وضعیت خاص، Negotiate انتخاب خواهد شد، زیرا Server در Headerهای Authentication اعلام نکرده است که Digest را پشتیبانی میکند. Negotiate یک پروتکل Windows است که در حال حاضر بسته به قابلیت Server در نهایت به Kerberos یا NTLM منتهی میشود، اما هنگام استقرار استانداردهای امنیتی آینده، Forward Compatibility برنامه را حفظ میکند.
property static به نام CredentialCache.DefaultNetworkCredentials اجازه میدهد کاربر Windows فعلی را بدون مشخص کردن Password به Credential Cache اضافه کنید:
cache.Add (prefix, "Negotiate", CredentialCache.DefaultNetworkCredentials);
Authentication از طریق Headerها
روش دیگر برای Authentication این است که Header احراز هویت را مستقیماً تنظیم کنید:
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue ("Basic",
Convert.ToBase64String (Encoding.UTF8.GetBytes ("username:password")));
...
این راهبرد با سیستمهای Authentication سفارشی مانند OAuth نیز کار میکند.
HttpClient اجازه میدهد Headerهای سفارشی HTTP را به Request اضافه کنید و همچنین Headerهای Response را Enumerate کنید. Header صرفاً یک جفت Key/Value حاوی Metadata است، مانند Content Type پیام یا نرمافزار Server. HttpClient Collectionهای strongly typed با propertyهایی برای Headerهای استاندارد HTTP ارائه میکند. property DefaultRequestHeaders برای Headerهایی است که روی هر Request اعمال میشوند:
var client = new HttpClient (handler);
client.DefaultRequestHeaders.UserAgent.Add (
new ProductInfoHeaderValue ("VisualStudio", "2022"));
client.DefaultRequestHeaders.Add ("CustomHeader", "VisualStudio/2022");
اما property به نام Headers در کلاس HttpRequestMessage برای Headerهای ویژهٔ یک Request خاص است.
Query Stringها
Query String رشتهای است که با علامت سؤال به URI افزوده میشود و برای ارسال دادهٔ ساده به Server به کار میرود. میتوانید چند جفت Key/Value را با syntax زیر مشخص کنید:
?key1=value1&key2=value2&key3=value3...
نمونهٔ یک URI دارای Query String:
string requestURI = "http://www.google.com/search?q=HttpClient&hl=fr";
اگر احتمال دارد Query شما شامل Symbol یا Space باشد، میتوانید از متد EscapeDataString کلاس Uri برای ساخت URI قانونی استفاده کنید:
string search = Uri.EscapeDataString ("(HttpClient or HttpRequestMessage)");
string language = Uri.EscapeDataString ("fr");
string requestURI = "http://www.google.com/search?q=" + search +
"&hl=" + language;
URI حاصل چنین است:
http://www.google.com/search?q=(HttpClient%20OR%20HttpRequestMessage)&hl=fr
EscapeDataString شبیه EscapeUriString است، با این تفاوت که نویسههایی مانند & و = را نیز Encode میکند؛ نویسههایی که در غیر این صورت Query String را بههم میریختند.
Upload کردن Form Data
برای Upload دادههای HTML Form، یک شیء FormUrlEncodedContent بسازید و مقداردهی کنید. سپس میتوانید آن را به متد PostAsync بدهید یا به property به نام Content در Request اختصاص دهید:
string uri = "http://www.albahari.com/EchoPost.aspx";
var client = new HttpClient();
var dict = new Dictionary<string,string>
{
{ "Name", "Joe Albahari" },
{ "Company", "O'Reilly" }
};
var values = new FormUrlEncodedContent (dict);
var response = await client.PostAsync (uri, values);
response.EnsureSuccessStatusCode();
Console.WriteLine (await response.Content.ReadAsStringAsync());
Cookieها
Cookie یک جفت رشتهای Name/Value است که یک HTTP Server در Response Header برای Client میفرستد. Client مرورگر وب معمولاً Cookieها را به خاطر میسپارد و تا زمان Expire شدن، آنها را در هر Request بعدی به همان آدرس برای Server دوباره ارسال میکند. Cookie به Server اجازه میدهد بدون نیاز به یک Query String شلوغ در URI تشخیص دهد آیا با همان Client یک دقیقه قبل یا حتی دیروز صحبت میکرده است یا نه.
بهطور پیشفرض، HttpClient هر Cookie دریافتشده از Server را نادیده میگیرد. برای پذیرش Cookieها، یک CookieContainer بسازید و آن را به HttpClientHandler اختصاص دهید:
var cc = new CookieContainer();
var handler = new HttpClientHandler();
handler.CookieContainer = cc;
var client = new HttpClient (handler);
...
برای Replay کردن Cookieهای دریافتشده در Requestهای بعدی، کافی است دوباره همان CookieContainer را استفاده کنید. روش دیگر این است که با یک CookieContainer تازه شروع کنید و سپس Cookieها را بهصورت دستی اضافه کنید؛ نمونهٔ آن در آغاز مقالهٔ بعدی ادامه مییابد.