لا تُحِط HttpClient بـ using ── الممارسة العملية لاتصال HTTP في تطبيقات C# للأعمال (أنماط الإنشاء والمهلة وإعادة المحاولة)

· آخر تحديث: · · CSharp, .NET, HttpClient, IHttpClientFactory, الشبكات, async/await, تطوير Windows, الاستشارات التقنية

سجل التعديلات (2 تحديثات، آخر تحديث 2 Sep، 2026)

سجل بالتغييرات التي أُجريت على هذا المقال. وحيثما حُفظت نسخة سابقة، تبقى متاحة للقراءة عبر رابط دائم يحمل معرّف DOI.

أُعيدَت الترجمة العربية كترجمة كاملة عن النص الياباني الأصلي، وأُضيفَت خريطة المعرفة.
أعيدت الترجمة كترجمة كاملة عن النص الياباني الأصلي. كانت النسخة العربية السابقة مختصراً يسقط أبواباً وجداول ورسوم Mermaid وتعليقات الأشكال وFAQ. أُعيدت هذه العناصر وفق الأصل الياباني، والادّعاءات التقنية مطابقة للنسخة اليابانية.
النشر الأول
الاستشهاد بهذا المقال(DOI: 10.5281/zenodo.21621668)

هذا المقال محفوظ على Zenodo. يرد أدناه معرّف DOI الذي يشير دائمًا إلى أحدث نسخة، ومعرّف DOI المثبَّت على النسخة التي تقرؤها.

غو كومورا (2026). لا تُحِط HttpClient بـ using ── الممارسة العملية لاتصال HTTP في تطبيقات C# للأعمال (أنماط الإنشاء والمهلة وإعادة المحاولة). شركة كومورا سوفت ذ.م.م.. https://doi.org/10.5281/zenodo.21621668 https://comcomponent.com/ar/blog/csharp-httpclient-practical-guide/

DOI (أحدث نسخة)
10.5281/zenodo.21621668
DOI (هذه النسخة)
10.5281/zenodo.22241024

«بعد الظهر تبدأ الاتصالات بواجهات API الخارجية بالفشل بخطأ SocketException» أو «بعد تبديل الخادم الوجهة يستمر التطبيق بالاتصال بالخادم القديم» — استدعاء GetAsync على HttpClient في C# سهل إن اكتفيت بالاستدعاء، لكن الخطأ في كيفية إنشاء النسخة والاحتفاظ بها يزرع هذا النوع من الأعطال التي «تعمل حينها، ثم تنكسر بعد دخول التشغيل الفعلي».

في هذا المقال، وبافتراض أن تطبيق Windows للأعمال يستدعي واجهة Web API خارجية أو خدمة داخلية، نرتّب النمط الصحيح لإنشاء HttpClient، وتصميم المهلة، وإعادة المحاولة، ومعالجة الأخطاء، وحتى المطبّات الخاصة ببيئة Windows، بالترتيب الذي يكثر التردد عنده في العمل.

1. الخلاصة أولاً (جدول القرار)

طريقة الاحتفاظ بـ HttpClient تختلف حسب بنية التطبيق. نبدأ بجدول القرار.

نفتح أولاً المختصرات الظاهرة في الجدول. DI (Dependency Injection: حقن التبعية) أسلوب تصميم لا تصنع فيه الفئة الجزء الذي تحتاجه بـ new بل تتلقاه من الخارج، وفي .NET يكون الوعاء Microsoft.Extensions.DependencyInjection. Generic Host مضيف عام في .NET يتولى فوق حاوية DI تلك الإعداد والسجلات وتشغيل المعالجة المقيمة وإيقافها، وقد تناولناه في ما هو Generic Host واستخدام Generic Host + BackgroundService في تطبيق سطح المكتب. IHttpClientFactory جهة توريد HttpClient فوق ذلك DI (الفصل 4). نشرح بالترتيب من الوسيلة بلا DI (static + PooledConnectionLifetime) حتى يُقرأ المقال حتى في تطبيق لا يستخدم DI.

شكل التطبيق النمط الموصى به السبب
أداة كونسول تنتهي في ثوانٍ إلى دقائق (.NET) HttpClient واحد static/مفرد في عملية قصيرة العمر يمكن تجاهل مشكلة تغيّر DNS عملياً
تطبيق مقيم طويلاً أو خدمة Windows (.NET، بلا DI) HttpClient من نوع static + ضبط SocketsHttpHandler.PooledConnectionLifetime يحل استنزاف المقابس ومشكلة تغيّر DNS معاً
تطبيق يستخدم Generic Host / DI (.NET) IHttpClientFactory (AddHttpClient) يمكن ترك تجميع المعالجات واستبدالها للمصنع. يمكن فصل إعداد كل وجهة بعميل مسمّى/مُنمَّط
تطبيق .NET Framework إدخال IHttpClientFactory بحزمة Microsoft.Extensions.Http في .NET Framework يسهل استنزاف المنافذ عند الإنشاء الذاتي، والوثائق الرسمية أيضاً توصي بالمصنع1
إعداد البروكسي أو Cookie أو الشهادة يختلف حسب الوجهة افصل HttpClient حسب الإعداد (لا تُعد الاستخدام) إعداد اتصال المعالج لا يمكن تغييره بعد إرسال أول طلب2

بعد ذلك نكتب الخلاصة أولاً.

  • لا تنشئ new HttpClient() وتتخلّص منه بـ using عند كل طلب. HttpClient يحمل مجمّع اتصالات داخلياً، ومصمَّم على افتراض إعادة الاستخدام. الإنشاء والتخلّص في كل مرة يستنفد المقابس المتاحة تحت الحمل المرتفع فيظهر SocketException2.
  • ومع ذلك لا يكفي جعله static. HttpClient لا يحل DNS إلا عند إنشاء الاتصال، لذلك يستمر في استخدام الاتصال القديم حتى لو تغيّر عنوان IP للوجهة. قطع عمر الاتصال بـ SocketsHttpHandler.PooledConnectionLifetime هو الحل الموصى به رسمياً1.
  • إن كنت تستخدم Generic Host أو DI فاترك الأمر لـ IHttpClientFactory. يجمع المصنع المعالجات ويستبدلها كل دقيقتين افتراضياً، فيتعامل مع استنزاف المقابس وتغيّر DNS معاً3.
  • القيمة الافتراضية للمهلة 100 ثانية. في إحساس تطبيق الأعمال هذا طول لا يختلف كثيراً عن «الجمود بلا نهاية»، فاضبطه صراحة حسب متطلبات كل وجهة.
  • لا تصنع إعادة المحاولة بنفسك، وابدأ بالمعالج القياسي في Microsoft.Extensions.Http.Resilience. يُدخل إعادة المحاولة وقاطع الدائرة والمهلة كمجموعة قيم افتراضية مجرَّبة، ويتجنّب في مرحلة التصميم حوادث شائعة في حلقة إعادة المحاولة المصنوعة يدوياً مثل «إعادة إرسال POST فاشل بلا شرط فيحدث تسجيل مزدوج»4.

أمثلة الشيفرة في هذا المقال كلها شذرات اقتُطعت بما يلزم للشرح. logger هو Microsoft.Extensions.Logging.ILogger، وct هو CancellationToken الممرَّر من جهة الاستدعاء، وurl وclient يشيران إلى عنوان الوجهة ونسخة HttpClient، وكلها مفترضة جاهزة في الفئة أو الدالة المحيطة. لن تعمل إن لصقتها كما هي في Main.

في المخطّط، يشير الخطّ المتّصل إلى علاقة قائمة دائماً، ويشير الخطّ المتقطّع إلى علاقة مشروطة (شروط قيامها مذكورة في شرح كلّ علاقة في الصفحة التفصيليّة). القائمة الكاملة للعلاقات (المجموع 19، مع الأدلّة ودرجة اليقين) وتعريفات المفاهيم الرئيسة مجمّعة في صفحة تفاصيل خريطة المعرفة (باليابانية). البيانات: JSON-LD / Turtle

2. لماذا لا يصح «الإنشاء في كل مرة داخل using» ── استنزاف المقابس

لأن HttpClient ينفّذ IDisposable، تبدو الشيفرة التالية صحيحة للوهلة الأولى.

// アンチパターン: リクエストのたびに生成して破棄する
public async Task<string> GetDataAsync(string url)
{
    using var client = new HttpClient();
    return await client.GetStringAsync(url);
}

المشكلة أن المقبس على مستوى نظام التشغيل لا يُحرَّر فوراً حتى بعد Dispose. حسب مواصفات TCP يبقى مقبس الجهة التي أغلقت في حالة TIME_WAIT بعض الوقت. ما دام تكرار الاستدعاء منخفضاً لا يحدث شيء، لكن إن ارتفع الحمل تتراكم المقابس غير المحرَّرة، ثم فجأة يتعذّر الاتصال بـ SocketException2.

غير أن «بقاء المقبس» وحده لا يُقنع بسبب الوصول إلى الاستنزاف. ننظر درجة أعمق في الداخل. المفتاح أن رقم المنفذ في جهتك، الذي يُستهلك عند كل اتصال، محدود.

  1. عندما يفتح العميل اتصال TCP إلى الخادم، منفذ الوجهة محدد (443 في HTTPS)، لكن رقم المنفذ في جهتك تخصّصه نظام التشغيل تلقائياً من الشواغر. هذا هو المنفذ العابر (ephemeral port، وفي مصطلح Windows المنفذ الديناميكي).
  2. النطاق الافتراضي للمنافذ الديناميكية في Windows هو 49152 إلى 65535، أي 16,384 منفذاً5. هذا المجمّع الذي تخصّص منه نظام التشغيل عند فتح اتصال صادر، ويُضبط لكل نقل (TCP/UDP) ولكل IPv4/IPv65. ليس سقفاً «حتى 16,384 اتصالاً متزامناً لكل جهاز». يميّز TCP الاتصال بالرباعية «عنوانك ومنفذك» و«عنوان الطرف ومنفذه»6، لذلك يجوز مواصفاتً أن يحمل الرقم نفسه اتصالات متعددة لوجهات مختلفة. ما يعمل ليس المجموع نفسه بقدر بأي سرعة ينحت عملية واحدة المجمّع.
  3. حتى بعد إغلاق الاتصال، لا يمكن إعادة استخدام منفذ الجهة التي أغلقت أولاً فوراً. منعاً لتسلل رزمة قديمة متأخرة إلى اتصال جديد فُتح بالتركيبة نفسها من المنافذ، يُبقي TCP حالة TIME-WAIT زمناً محدداً (2×MSL: ضعف Maximum Segment Lifetime)6.
  4. النتيجة أن تصميم «افتح اتصالاً وأغلقه عند كل طلب» يعني حجز منفذ واحد لكل طلب، عشرات الثواني إلى بضع دقائق، كمستخدَم. بمئة طلب في الثانية تنحت إطار الـ 16,384 بسرعة 100 في الثانية، فيُستنفد في دقائق.

من يحمل مجمّع الاتصالات ليس HttpClient نفسه بل المعالج داخله. إعادة إنشاء النسخة تعيد إنشاء المجمّع كله، وهذا مدخل الاستنزاف.

new ثم Dispose بـ using عند كل طلبيُعاد إنشاء المعالج كلهمجمّع الاتصالات فارغ في كل مرةيُفتح اتصال TCP في كل مرةوبعد الإغلاق يبقى المنفذ في TIME-WAITالنحت المستمر للمنافذ = استنزاف المقابسإعادة استخدام HttpClient واحد staticالمعالج يحتفظ بمجمّع الاتصالات باستمراراستلام العميل من المصنع في كل مرة(المُعاد استخدامه المعالج لا العميل - الفصل 4)إعادة استخدام اتصال قائملا يُستهلك منفذ جديد

الشكل 1: ما يُعاد استخدامه هو مجمّع الاتصالات الذي يحمله المعالج. إعادة إنشاء HttpClient ترمي المجمّع معه.

إن استُنفد المجمّع يفشل طلب الاتصال التالي لانعدام منفذ يُخصَّص. العرض المحير ظاهرياً الاتصال وحده يتعذّر رغم وفرة المعالج والذاكرة سببه أن المورد المستنزَف مورد آخر هو رقم المنفذ. غير أن ظهور SocketException ≠ استنزاف. توقف الطرف، وقطع المسار، وDNS، وفشل TLS تصعد بالنوع نفسه من الاستثناء. العزل بالإجراء في الفقرة التالية. تنص إرشادات Microsoft الرسمية أيضاً على أن منفذ TCP لا يُحرَّر فوراً حتى بعد إغلاق الاتصال، وأن تكرار الطلبات العالي قد يبلغ سقف عدد المنافذ المتاحة في نظام التشغيل1.

ما يجعل هذا العطل عنيداً أنه نادراً ما يُعاد إنتاجه أثناء التطوير والاختبار. بضع عمليات يدوية في الثانية لا تنحت الإطار حتى القاع، فيظهر بشكل سقوط في ساعة الذروة في الإنتاج فقط، أو سقوط عند دفعة نهاية الشهر فقط. عند التحقيق، تحقق بعدد مقابس TIME_WAIT ونطاق المنافذ في بيئتك بالأمر التالي عند حدوث الظاهرة.

# TIME_WAIT状態のソケット数を数える
(netstat -ano | Select-String "TIME_WAIT").Count

# 自分の環境の動的ポート範囲を確認する(既定は開始49152・16384個)
netsh int ipv4 show dynamicport tcp

هنا لا تقارن مجموع TIME_WAIT للجهاز كله بنطاق 16,384. إن كان الجهاز يتصل بوجهات كثيرة قد يكبر المجموع، لكنك حينها تعد اتصالات سليمة لوجهات أخرى، ولا يعني ذلك أن تطبيقك قريب من الاستنزاف. تنص إجراءات العزل لدى Microsoft أيضاً على أن كثرة TIME_WAIT نفسها ليست دليلاً على الاستنزاف (تدل فقط على احتمال الاستنزاف مستقبلاً)7.

ما ينبغي النظر إليه هو التفصيل حسب العملية وحسب الوجهة وطريقة الازدياد.

# プロセスと状態で束ねる。1つのPIDにTIME_WAITが偏っていれば、そのプロセスが犯人
Get-NetTCPConnection | Group-Object State, OwningProcess |
    Sort-Object Count -Descending | Select-Object -First 10 Count, Name

# 疑わしい宛先だけを、ローカルアドレスと相手側エンドポイントで束ねる
Get-NetTCPConnection -State TimeWait |
    Group-Object LocalAddress, RemoteAddress, RemotePort |
    Sort-Object Count -Descending | Select-Object -First 10 Count, Name

بعد ذلك تحقق بالأعراض مما إذا كان الاستنزاف حقيقياً. إجراءات التحقق التي تذكرها Microsoft ثلاث نقاط7.

ما يُتحقق منه طريقة النظر
هل تفشل الاتصالات الصادرة جملة جرّب الوصول إلى مشاركة، وRDP إلى خادم آخر، وtelnet. إن فشل وجهة واحدة فقط فليس استنزافاً
سجل الأحداث معرّف الحدث 4227 / 4231 في سجل النظام
الانحياز إلى PID محدد هل يتركز TIME_WAIT على PID واحد في netstat -anob

«طريقة الازدياد» أدلة أوثق من العدد المطلق. إن ازداد بخط صاعد فور إعادة تشغيل التطبيق ولم ينخفض دقائق حتى بعد إيقاف الحمل، يمكن شبه الجزم أن نمط الإنشاء هو السبب. وبالعكس، إن زاد ونقص مع الحمل ثم استقر فالاتصالات تُعاد استخدامها. كذلك قد تنفصل مقابس TIME_WAIT عن العملية فلا يمكن تحديد تطبيق الاستدعاء من عمود PID. في تلك الحالة ضيّق بعمود العنوان الخارجي (الوجهة). في Windows 10 / Windows Server 2016 وما بعد، يمكن أيضاً رؤية المنافذ التي خرجت من TIME_WAIT وصارت BOUND عبر netstat -anobq أو Get-NetTCPConnection7.

لاحظ أن HttpClient المستلم عبر IHttpClientFactory خارج هذا الحديث. عميل المصنع إن نُفّذ عليه Dispose لا يُتخلَّص من المعالج (كيان مجمّع الاتصالات)، لذلك إحاطته بـ using آمنة3.

3. جعله static لا ينهي القصة ── مشكلة تغيّر DNS

جعل HttpClient من نوع static اتجاه صحيح كإجراء ضد استنزاف المقابس، لكن مشكلة أخرى تبقى. HttpClient لا يحل DNS إلا عند إنشاء الاتصال، ولا يرجع إلى TTL لسجل DNS2. ما دام الاتصال حياً في المجمّع يستمر في الاتصال بالعنوان القديم حتى لو تغيّر عنوان IP للوجهة.

عطل «بدّلنا DNS عند التحوّل عند الفشل، لكن التطبيق ظل يرى الخادم القديم حتى إعادة التشغيل» سببه هذه الآلية. الحل الذي توصي به الإرشادات الرسمية قطع عمر الاتصال بـ SocketsHttpHandler.PooledConnectionLifetime1.

// .NET (Core) / .NET 5+ での推奨パターン:
// 接続を一定周期で作り直させることで、DNS変更に追従する
private static readonly HttpClient SharedClient = new(new SocketsHttpHandler
{
    PooledConnectionLifetime = TimeSpan.FromMinutes(2)
});

الاتصال الذي بلغ عمره يُعاد إنشاؤه عند الطلب التالي، وفي ذلك التوقيت يُحل DNS من جديد. تُحدَّد القيمة بحسب مدى السرعة التي تريد بها متابعة تغيّر DNS. مثال الوثائق الرسمية يستخدم دقيقتين، لكن نظاماً داخلياً نادراً ما تتغيّر وجهته يمكن إطالة القيمة بلا مشكلة1.

لاحظ أن SocketsHttpHandler تنفيذ من .NET Core 2.1 فما بعد، و.NET Framework لا يدعمه. في حالة .NET Framework استخدم IHttpClientFactory في الفصل التالي1.

نرتّب هنا البيئة المستهدفة في موضع واحد. من الوسائل الظاهرة في هذا المقال، تحقق بهذا الجدول أيها يعمل في بيئتك.

الوسيلة .NET Framework 4.8 .NET Core 2.1+ / .NET 5+
HttpClient نفسه يعمل (التنفيذ الداخلي HttpWebRequest / ServicePoint) يعمل (التنفيذ الداخلي SocketsHttpHandler)
SocketsHttpHandler.PooledConnectionLifetime لا يعمل يعمل. الخيار الأول لمتابعة DNS1
IHttpClientFactory يعمل بحزمة Microsoft.Extensions.Http. توصية رسمية1 يعمل
ServicePointManager.DefaultConnectionLimit يعمل (انتبه لصغر القيمة الافتراضية 2)8 لا يؤثّر على HttpClient. يُحدَّد السقف بـ MaxConnectionsPerServer
ServicePoint.ConnectionLeaseTimeout يعمل (أدناه) لا يحمل معنى فعلياً9

الخيار الأول في .NET Framework، وفق الإرشادات الرسمية، إدخال IHttpClientFactory بحزمة Microsoft.Extensions.Http1. غير أن كثيراً من المواقع تقول «تطبيق WinForms كبير قائم، ولا متسع للبدء بإدخال DI أولاً». الحل الوسط حينها مشاركة HttpClient واحد، وقطع عمر الاتصال بـ ServicePoint.ConnectionLeaseTimeout.

using System;
using System.Net;
using System.Net.Http;

// .NET Framework 4.8 での折衷案:
// HttpClientは共有し、接続の寿命はServicePoint側で区切ってDNS変更に追従させる
private static readonly Uri ApiBase = new Uri("https://order.example.co.jp/");

private static readonly HttpClient SharedClient = CreateSharedClient();

private static HttpClient CreateSharedClient()
{
    // 最初のリクエストより前に必ず通す。ここを呼び忘れると
    // ConnectionLeaseTimeout は既定の -1(接続を無期限に維持)のままで、
    // DNS変更に追従しない=この節の対策が何も効いていない状態になる
    ServicePoint sp = ServicePointManager.FindServicePoint(ApiBase);
    sp.ConnectionLeaseTimeout = (int)TimeSpan.FromMinutes(2).TotalMilliseconds; // 単位はミリ秒

    return new HttpClient { BaseAddress = ApiBase };
}

ConnectionLeaseTimeout إعداد «بعد انقضاء الزمن المحدد، أغلق ذلك الاتصال عند انتهاء معالجة طلب واحد»، وهو معدّ لحالات تريد فيها إعادة فتح الاتصال دورياً مثل تبديل موازن الحمل9. الاتصال المغلَق يُعاد إنشاؤه في الطلب التالي، وهناك يُحل DNS من جديد. الفكرة مطابقة تماماً لـ PooledConnectionLifetime، فاعتبره المقابل في جانب .NET Framework.

غير أن واجهات WebRequest بما فيها ServicePoint منصوص رسمياً على عدم استخدامها في التطوير الجديد9. هذا علاج لإطالة العمر مع البقاء على .NET Framework، وعند الانتقال إلى .NET استبدله بـ PooledConnectionLifetime أو IHttpClientFactory. منظور الانتقال ملخّص في قائمة التحقق قبل الانتقال من .NET Framework إلى .NET.

فشلا الفصلين حتى هنا يأتيان كلاهما من عدم تقرير «متى يُعاد إنشاء الاتصال». علاقة الفصل 2 والفصل 3 في ورقة واحدة كما يلي.

new ثم Dispose بـ using عند كل طلبالاحتفاظ بواحد static فقطمشاركة مجمّع الاتصالات مع منح الاتصال عمراًكيف تحتفظ بـ HttpClient وبمجمّع الاتصالاتيُرمى مجمّع الاتصالات في كل مرة→ نحت مستمر للمنافذ العابرة= استنزاف المقابس (الفصل 2)يبقى الاتصال في المجمّع→ DNS لا يُحل إلا عند إنشاء الاتصال= الاتصال بالعنوان القديم بعد التبديل (الفصل 3)إعادة إنشاء الاتصال دورياً= التعامل مع الاستنزاف وتغيّر DNS معاًالوسيلة حسب البيئة كما في جدول قرار الفصل 1· .NET 5+ : static + PooledConnectionLifetime· DI / Generic Host : IHttpClientFactory(يُستلم العميل في كل مرة، والمشترَك المعالج - الفصل 4)· .NET Framework : IHttpClientFactory،وإن تعسّر فـ ConnectionLeaseTimeout

الشكل 2: «عدم إعادة الاستخدام» و«إعادة الاستخدام بلا حد» كلاهما فشل. الصواب إعادة استخدام مجمّع الاتصالات مع منح الاتصال عمراً. ما يُشارَك يختلف حسب الوسيلة (في المصنع المعالج لا العميل).

4. عند استخدام DI، اعتمد على IHttpClientFactory

في تطبيق يستخدم Generic Host أو حاوية DI، الخيار الأول IHttpClientFactory (AddHttpClient). شرح Generic Host نفسه في ما هو Generic Host، وإدخاله في تطبيق سطح المكتب في استخدام Generic Host + BackgroundService في تطبيق سطح المكتب.

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// 名前付きクライアント: 接続先ごとに設定を分離する
builder.Services.AddHttpClient("OrderApi", client =>
{
    client.BaseAddress = new Uri("https://order.example.co.jp/");
    client.Timeout = TimeSpan.FromSeconds(10);
});

ما يفعله المصنع أسرع بالشكل. أن HttpClient والمعالج الحامل لمجمّع الاتصالات شيئان مختلفان هو كل هذه الآلية.

ينتهي العمر بعد دقيقتين افتراضياً. يتوقف الصرف الجديد،ويُتخلَّص منه بعد انتهاء المعالجة الجاريةCreateClient المرة 1(عميل يُستخدم مرة)CreateClient المرة 2(عميل يُستخدم مرة)CreateClient بعد الاستبدال(عميل يُستخدم مرة)المعالج 1كيان مجمّع الاتصالات هنامجمّع الاتصالاتيعيد استخدام اتصال TCP ولا يرمي المقبسالمعالج 2يُنشأ جديداًمجمّع اتصالات جديدهنا يُحل DNS من جديد

الشكل 3: مجمّع معالجات IHttpClientFactory والاستبدال الدوري.

من هذا الشكل تُقرأ الخلاصتان المكتوبتان في صدر المقال. «الاستخدام لمرة مرفوض» لأن new HttpClient() الذاتي يجعل الطبقة العليا (العميل) والسفلى (المعالج ومجمّع الاتصالات) تموتان معاً واحداً بواحد، فترمي المقبس أيضاً عند كل رمي. «static أيضاً ناقص» لأن static يثبّت الطبقة السفلى فلا تُتخلَّص منها، فتحمي المقابس لكن سهم استبدال المعالج (= إعادة حل DNS) لا يحدث مرة. المصنع يبقي الطبقة العليا للاستخدام لمرة، ويشارك الطبقة السفلى فقط، ويستبدلها دورياً، فيلبّي الأمرين معاً.

ثلاث نقاط تُثبَّت في حركة المصنع.

  • المعالجات تُجمَّع وتُستبدل كل دقيقتين افتراضياً. كل CreateClient يُرجع HttpClient جديداً، لكن المعالج في الخلف (مجمّع الاتصالات) مشترك فلا يحدث استنزاف مقابس، والاستبدال الدوري يتابع تغيّر DNS أيضاً3.
  • HttpClient المصنوع عبر المصنع مفترض قصير العمر. إن احتفظت بالنسخة المستلمة في حقل مفرد، خرجت من مشاركة استبدال المعالج وتوقفت عن متابعة تغيّر DNS. حقن عميل مُنمَّط في خدمة مفردة يُتجنَّب للسبب نفسه3.
  • تنبيه في التطبيقات المعتمدة على Cookie. نتيجة تجميع المعالجات تُشارَك CookieContainer دون قصد. إن استخدمت Cookie، توجّه الوثائق الرسمية إلى تجنّب المصنع أو تعطيل معالجة Cookie وإرفاق الترويسة بنفسك1.

حول الحصول على الرمز عند استدعاء واجهة API محمية (مثل واجهة محمية بـ Microsoft Entra ID) تناولناه في إدخال مصادقة Entra ID في تطبيق WinForms/WPF، فراجعه هناك.

5. تصميم المهلة ── القيمة الافتراضية 100 ثانية طويلة جداً لتطبيق أعمال

القيمة الافتراضية لـ HttpClient.Timeout هي 100 ثانية10. انتظار 100 ثانية في تطبيق أعمال يستدعي API امتداداً لعملية على الشاشة يعادل «تجمّد»، فاضبط صراحة حسب كل وجهة.

// クライアント全体の既定タイムアウト
client.Timeout = TimeSpan.FromSeconds(10);

// 特定のリクエストだけ短く/長くしたい場合はCancellationTokenSourceを使う
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
HttpResponseMessage response = await client.GetAsync(url, cts.Token);

تنبيهات التصميم كما يلي.

  • الاستثناء عند المهلة هو TaskCanceledException. من .NET 5 فما بعد، مهلة ناشئة عن HttpClient.Timeout تحمل TimeoutException كاستثناء داخلي10. غير أن المهلة عبر CancellationTokenSource الذاتي كما أعلاه لا تحمل استثناء داخلياً. التمييز بين المهلة والإلغاء من المستخدم أوثق بالحكم على «هل أُلغي الرمز الممرَّر من جهة الاستدعاء» لا بالاستثناء الداخلي (مثال الشيفرة في الفصل 7). إن نظرت في كتلة catch إلى HttpRequestException فقط أفلتت المهلة، فانتبه.
  • Timeout قيد على «الطلب كله». إن أردت تقييد إنشاء الاتصال وحده بقصر، اجمع معه SocketsHttpHandler.ConnectTimeout. متطلب «إن سقط الخادم أريد الاستسلام في 3 ثوانٍ، لكن الاستجابة الكبيرة السليمة أريد انتظارها 60 ثانية» يُعبَّر عنه بهذا الجمع2.
  • تجنّب التخزين المؤقت الافتراضي عند تنزيل ملف كبير. HttpClient يقرأ الاستجابة كلها إلى الذاكرة افتراضياً، لذلك في تنزيل عشرات الميغابايت فما فوق حدّد HttpCompletionOption.ResponseHeadersRead وعالج بالتدفق2.

إخراج قيمة المهلة إلى appsettings.json وتغييرها حسب البيئة يمكن استخدام جدول القرار في الممارسة العملية لإدارة الإعداد في تطبيقات Windows للأعمال كما هو.

لاحظ أن استدعاء اتصال HTTP من WinForms/WPF مع الحظر بـ .Result أو .Wait() يسبب جمود خيط الواجهة. هذا المطبّ الثابت مشروح بالتفصيل في جدول قرار الممارسة لـ async/await في C#، فيُنصح بقراءته قبل كتابة معالجة الاتصال.

6. إعادة المحاولة ── استخدم معالج resilience القياسي بدل حلقة مصنوعة يدوياً

الشبكة تفشل مؤقتاً، لذلك تحتاج معالجة استدعاء واجهة API خارجية إلى إعادة محاولة. غير أن إعادة المحاولة المصنوعة بحلقة for وTask.Delay تتطلب تنفيذ النقاط التالية كلها بنفسك بشكل صحيح، فلا تستحق العناء.

  • تمييز الفشل الذي تجوز إعادة محاولته (المهلة، HTTP 408/429/5xx) عن الفشل الذي لا جدوى منه (HTTP 400/401/404)
  • استبعاد طرائق HTTP التي تُحدث حادثاً عند إعادة التنفيذ (التنفيذ المزدوج للتسجيل عبر POST مثلاً)
  • التراجع الأسي وjitter (تقلقل يمنع كل العملاء من إعادة الإرسال معاً فيسقط الخادم من جديد)
  • قاطع دائرة يوقف الاستدعاء نفسه حين يستمر العطل

المعالج القياسي في حزمة Microsoft.Extensions.Http.Resilience يوفّر هذه المجموعة بقيم افتراضية مجرَّبة4.

builder.Services.AddHttpClient("OrderApi", client =>
    {
        client.BaseAddress = new Uri("https://order.example.co.jp/");
    })
    .AddStandardResilienceHandler(); // リトライ+サーキットブレーカー+タイムアウトの標準セット

القيم الافتراضية للمعالج القياسي: مهلة الطلب كله 30 ثانية، حتى 3 محاولات بتراجع أسي (تأخير أولي ثانيتان مع jitter)، مهلة 10 ثوانٍ لكل محاولة، ومعاملة HTTP 408/429/5xx وHttpRequestException كخطأ مؤقت4.

نقطة واحدة تستدعي الانتباه في الافتراضي: المعالج القياسي يعيد المحاولة افتراضياً لكل طرائق HTTP. في واجهة API يضرّها التنفيذ المزدوج لـ POST من نوع التسجيل، عطّل إعادة المحاولة للطرائق غير الآمنة4.

httpClientBuilder.AddStandardResilienceHandler(options =>
{
    // POST/PUT/DELETE等の再実行を無効化する
    options.Retry.DisableForUnsafeHttpMethods();
});

لاحظ أن إعادة المحاولة تحل الفشل المؤقت فقط. أعراض مثل «الاتصال قائم لكن البيانات لا تسري» أو «الاستجابة بطيئة جداً» كثيراً ما تكون مشكلة في طبقة TCP، وعزلها يستفيد من الأسلوب الذي تناولناه في سبب توقف اتصال الكاميرا الصناعية بإعادة إرسال TCP وعزله.

7. معالجة الأخطاء ── كيف تُعامل رموز الحالة

HttpClient لا يرمي استثناء عند فشل «عادت استجابة HTTP» مثل HTTP 404 أو 500. الاستثناء يحدث عندما لا تُحصل الاستجابة نفسها: فشل الاتصال، المهلة، الإلغاء. اكتب النوعين وعيهما منفصلين.

try
{
    using HttpResponseMessage response = await client.GetAsync(url, ct);

    if (!response.IsSuccessStatusCode)
    {
        // 応答は返ってきたが失敗: ステータスコードで分岐できる
        if (response.StatusCode == HttpStatusCode.NotFound)
        {
            return null; // 「存在しない」を正常系として扱う例
        }
        response.EnsureSuccessStatusCode(); // それ以外はHttpRequestExceptionへ
    }

    return await response.Content.ReadFromJsonAsync<Order>(ct);
}
catch (HttpRequestException ex)
{
    // فشل الاتصال، أو حالة فشل من EnsureSuccessStatusCode.
    // من .NET 5 فصاعدا يمكن قراءة رمز الحالة عند الإخفاق عبر ex.StatusCode
    logger.LogError(ex, "فشل استدعاء API الطلبات. StatusCode={StatusCode}", ex.StatusCode);
    throw;
}
catch (TaskCanceledException) when (ct.IsCancellationRequested)
{
    // إلغاء عبر الرمز الذي مرّره المستدعي (إغلاق الشاشة مثلا).
    // ليس خطأ، لذا انشره كما هو دون تلويث السجل
    throw;
}
catch (TaskCanceledException ex)
{
    // HttpClient.Timeout، أو انتهاء CTS مهلة ذاتي
    logger.LogError(ex, "انتهت مهلة API الطلبات");
    throw;
}

حكم مثل «هل تُعامل 404 كاستثناء أم كـ null» يتبع دلالات واجهة API الوجهة. إن جعلت كل شيء استثناء بضربة EnsureSuccessStatusCode واحدة تضخمت كتلة catch عند جهة الاستدعاء. خط الفصل بين ما يكون استثناء وما يُعبَّر عنه بقيمة الإرجاع ينطبق عليه تفكير الممارسة العملية لالتقاط الاستثناء والسجل ومعالجة الأخطاء كما هو.

إرسال JSON واستقباله بـ GetFromJsonAsync / PostAsJsonAsync / ReadFromJsonAsync من System.Net.Http.Json يغنيك عن كتابة التسلسل عبر سلسلة بنفسك.

8. مطبّات خاصة بتطبيقات Windows للأعمال

أخيراً نلخص مطبّات تُداس كثيراً في عمل بيئة Windows.

  • أول طلب بطيء بسبب الكشف التلقائي عن البروكسي. افتراض Windows أن HttpClient يستخدم إعداد بروكسي نظام التشغيل (بما فيه الكشف التلقائي). إن عرفت أن البروكسي غير مطلوب، عطّل بـ HttpClientHandler.UseProxy = false فيزول انتظار الكشف2. وبالعكس، في بيئة يلزم فيها بروكسي داخلي، التحديد الصريح بـ WebProxy يتجنّب «يعمل على جهاز التطوير ولا يعمل على الخادم».
  • أنهِ إعداد البروكسي قبل أول طلب. إعدادات اتصال المعالج لا تسري إن غيّرتها بعد إرسال طلب مرة2.
  • القيمة الافتراضية لعدد الاتصالات المتزامنة عكس في .NET و.NET Framework. في .NET (SocketsHttpHandler) عدد الاتصالات المتزامنة لـ HTTP/1.1 غير محدود افتراضياً، فقد تزداد الاتصالات بطلبات متوازية كثيرة وتصطدم بحد الجدار الناري أو الخادم. في معالجة ذات توازٍ عالٍ ضع سقفاً بـ MaxConnectionsPerServer2. وبالعكس في .NET Framework القيمة الافتراضية لـ ServicePointManager.DefaultConnectionLimit هي 2 (في بيئة غير ASP.NET)، فتنتظر الطلبات المتوازية داخلياً وتحدث مهلة. عند رفع التوازي في .NET Framework ارفع هذا السقف صراحة8.
  • عند الاستدعاء من خدمة Windows يختلف سياق البروكسي وTLS عن المستخدم. حساب تشغيل الخدمة لا يملك إعداد بروكسي المستخدم ولا بيانات اعتماده، فيصبح هذا سبباً ثابتاً لعطل اتصال «يعمل مع المستخدم التفاعلي ولا يعمل من الخدمة». سياق التشغيل الخاص بالخدمة في كيفية صنع خدمة Windows وتشغيلها.
  • لا تُضمّن عنوان URL للوجهة أو مفتاح API في الشيفرة. تبديل الوجهة عبر ملف الإعداد (الممارسة العملية لإدارة الإعداد)، وحفظ المعلومات السرية بأسلوب تجنّب الإعداد بالنص الواضح عبر DPAPI.

الخلاصة

جودة ممارسة HttpClient لا تُحسم بـ«كيف تستدعي» بقدر ما تُحسم بـ«كيف تحتفظ». الإنشاء عند كل طلب يجلب استنزاف المقابس، وجعله static بسذاجة يجلب عدم متابعة تغيّر DNS، وكلاهما لا يُرى أثناء التطوير. في .NET الجواب نسخة مشتركة مع PooledConnectionLifetime أو IHttpClientFactory، وفي .NET Framework إدخال IHttpClientFactory. فوق ذلك، اضبط المهلة صراحة حسب الوجهة، واترك إعادة المحاولة للمعالج القياسي لـ resilience — إلى هنا فقط يصبح تطبيق الأعمال قائماً على فرض «شبكة تفشل أحياناً».

مراجعة جانب الاتصال في تطبيق قائم (اتصال يسقط في ساعة الذروة فقط، ترتيب تصميم المهلة، تنفيذ جديد للتكامل مع واجهة API خارجية) كثيراً ما تحتاج حكماً مع النظر في الشيفرة الفعلية وبيئة التشغيل، فإن ترددت فاستشرنا.

مقالات ذات صلة

مجالات الاستشارة ذات الصلة

تتعامل شركة كومورا سوفت ذ.م.م. مع تطوير تطبيقات Windows للأعمال بما فيه التكامل مع واجهات API الخارجية، ومع الاستشارة التقنية لتحقيق أعطال الاتصال في التطبيقات القائمة (استنزاف المقابس، المهلة، فشل الاتصال المتقطع) وترتيب الإصلاح.

المراجع

  1. Microsoft Learn، Guidelines for using HttpClient. حول استخدام عميل طويل العمر مع PooledConnectionLifetime أو عميل قصير العمر من IHttpClientFactory في .NET Core/.NET 5+، والتوصية باستخدام IHttpClientFactory في .NET Framework، وتجنّب IHttpClientFactory في التطبيقات التي تستخدم Cookie بسبب مشاركة CookieContainer. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10

  2. Microsoft Learn، HttpClient Class. حول أن الإنشاء عند كل طلب يجلب استنزاف المقابس وSocketException، وأنه لا يحل DNS إلا عند إنشاء الاتصال ولا يرجع إلى TTL، وأن إعداد اتصال المعالج لا يمكن تغييره بعد أول طلب، وأن عدد الاتصالات المتزامنة لـ HTTP/1.1 غير محدود افتراضياً، والتوصية بالتدفق عند التنزيل الكبير، والسلوك الافتراضي للبروكسي وتعطيله بـ UseProxy. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9

  3. Microsoft Learn، IHttpClientFactory with .NET. حول أن العمر الافتراضي للمعالج دقيقتان، وأن HttpClient المصنوع عبر المصنع مفترض قصير العمر، وأن Dispose لعميل المصنع لا يتخلّص من المعالج، وأن حقن عميل مُنمَّط في مفرد يوقف متابعة تغيّر DNS. ↩ ↩2 ↩3 ↩4

  4. Microsoft Learn، Build resilient HTTP apps: Key development patterns. حول الاستراتيجية ذات المراحل الخمس التي يكوّنها AddStandardResilienceHandler (محدد المعدل / مهلة كلية 30 ثانية / حتى 3 محاولات بتراجع أسي / قاطع دائرة / مهلة محاولة 10 ثوانٍ)، ورموز الحالة المستهدفة (408/429/5xx) والاستثناءات، وتعطيل إعادة محاولة POST ونحوه بـ DisableForUnsafeHttpMethods. ↩ ↩2 ↩3 ↩4

  5. Microsoft Learn، The default dynamic port range for TCP/IP has changed in Windows Vista and in Windows Server 2008. حول أن النطاق الافتراضي للمنافذ الديناميكية من Windows Vista / Windows Server 2008 فما بعد يبدأ من 49152 وينتهي عند 65535، وإمكانية التحقق من النطاق الحالي بـ netsh int ipv4 show dynamicport tcp. ↩ ↩2

  6. IETF، RFC 9293 - Transmission Control Protocol (TCP), Section 3.3.2. حول حالة TIME-WAIT في TCP، وآلية منع تسلل الرزم المتأخرة إلى اتصال لاحق بانتظار 2×MSL (Maximum Segment Lifetime). ومعها أن اتصال TCP يُميَّز بزوج مقابس الطرفين (تركيبة العنوان والمنفذ). ↩ ↩2

  7. Microsoft Learn، TCP/IP port exhaustion troubleshooting. حول أن نطاق المنافذ الديناميكية يُضبط لكل نقل، وأن كثرة TIME_WAIT نفسها ليست دليلاً على الاستنزاف بل تدل فقط على احتمال الاستنزاف مستقبلاً، والتحقق بفشل الاتصالات الصادرة جملة ومعرّف الحدث 4227/4231 وانحياز TIME_WAIT إلى PID واحد، وإمكانية رؤية منافذ حالة BOUND أيضاً عبر netstat -anobq أو Get-NetTCPConnection في Windows 10 / Windows Server 2016 فما بعد. ↩ ↩2 ↩3

  8. Microsoft Learn، ServicePointManager.DefaultConnectionLimit Property. حول أن عدد الاتصالات المتزامنة الافتراضي 10 في تطبيق مستضاف على ASP.NET، و2 فيما عداه (تطبيق سطح المكتب وغيره). ↩ ↩2

  9. Microsoft Learn، ServicePoint.ConnectionLeaseTimeout Property. حول أن القيمة الافتراضية -1 (بلا أجل) والوحدة مللي ثانية، وأن الاتصال يُغلق بعد انقضاء الزمن المحدد عند انتهاء معالجة طلب، وأنه إعداد لحالات تريد إعادة فتح الاتصال دورياً مثل موازنة الحمل، وأن واجهات WebRequest/ServicePoint غير موصى بها في التطوير الجديد، وأنه من .NET 9 فما بعد تُربط هذه الخاصية بـ PooledConnectionLifetime لكنها لا تحمل أثراً فعلياً. ↩ ↩2 ↩3

  10. Microsoft Learn، HttpClient.Timeout Property. حول أن القيمة الافتراضية 100 ثانية، وأنه عند المهلة من .NET 5 فما بعد يُرمى TaskCanceledException يحمل TimeoutException كاستثناء داخلي. ↩ ↩2

أحدث المقالات التي تشترك في نفس الوسوم. عمّق فهمك بمواضيع مرتبطة.

ترتبط هذه المقالة بشكل طبيعي بصفحات الخدمات التالية.

الأسئلة الشائعة

أسئلة شائعة حول موضوع هذه المقالة.

HttpClient ينفّذ IDisposable، فلماذا لا يجوز إحاطته بـ using؟
المشكلة في نمط «الإنشاء والتخلّص عند كل طلب». يحتفظ HttpClient داخلياً بمجمّع اتصالات، وهو مصمَّم على افتراض إعادة استخدامه طوال عمر التطبيق. عند الإنشاء والتخلّص في كل مرة تبقى المقابس في حالة TIME_WAIT بعض الوقت حتى بعد التخلّص، فتُستنزف المقابس المتاحة تحت الحمل المرتفع ويظهر SocketException. تخلّص منه مرة واحدة عند إنهاء التطبيق، أو احصل عليه عبر IHttpClientFactory. أما HttpClient المستلم عبر المصنع فإحاطته بـ using آمنة، لأن Dispose لا يتخلّص من المعالج (handler) الفعلي.
هل ينبغي الانتقال من WebClient أو HttpWebRequest في .NET Framework؟
يُنصح بتوحيد الشيفرة الجديدة على HttpClient. WebClient وHttpWebRequest واجهتان قديمتان أُبقيتا للتوافق، وتوصي Microsoft نفسها باستخدام HttpClient في أي تطوير جديد. لا حاجة لاستبدال الشيفرة القائمة دفعة واحدة، لكن الانتقال التدريجي نحو HttpClient عند لمس أجزاء الاتصال يسهّل الصيانة من ناحية التحكم بالمهلة ودعم async وسهولة الاختبار.
كم مرة ينبغي إعادة المحاولة، وبأي فاصل؟
الأكثر أماناً الانطلاق من القيم الافتراضية للمعالج القياسي في Microsoft.Extensions.Http.Resilience (حد أقصى 3 محاولات، تراجع أسي مع jitter، وتأخير أولي ثانيتان) بدل تحديدها بنفسك. الأهم من العدد تمييز «هل يجوز إعادة محاولة هذا الطلب؟». العمليات مثل POST التي قد تسبّب تسجيلاً مزدوجاً عند إعادة التنفيذ يجب تعطيل إعادة المحاولة عليها افتراضياً، أو التأكد أن الخادم يضمن idempotency (استقبال الطلب نفسه مرتين لا يغيّر النتيجة) قبل تفعيل إعادة المحاولة.
لماذا يكون أول طلب فقط بطيئاً جداً في بيئة بروكسي داخلية؟
بالإعداد الافتراضي في Windows يحاول HttpClient الكشف التلقائي عن البروكسي، وقد يستغرق ذلك وقتاً عند أول اتصال. في البيئات التي يُعرف فيها أن البروكسي غير مطلوب (مثل الاتصال الداخلي بين خوادم)، يحسّن الأمر تعطيل الكشف التلقائي بضبط UseProxy على false في HttpClientHandler. وبالعكس، في البيئات الداخلية التي يلزم فيها بروكسي، يكون التحديد الصريح عبر WebProxy بدل الاعتماد على الكشف التلقائي أكثر استقراراً.

الملف الشخصي للمؤلف

صفحة الملف الشخصي لمؤلف المقالة.

غو كومورا

مؤسّس شركة كومورا سوفت ذ.م.م.

يركّز على تطوير برامج ويندوز، والاستشارات التقنية، والتحقيق في الأخطاء، ويتميّز في المشاريع التي تبقى فيها الأصول القديمة ناشطة، وفي تشخيص الأعطال التي يصعب تحديد سببها.

العودة إلى المدونة