🔒 ملاحظات حرفهای و بهترین شیوهها (Best Practices and Considerations)
در اینجا نکات کلیدی وجود دارد که بهتر است همیشه هنگام پیادهسازی همتوانی (Idempotency) در نظر بگیریم: 👇
⏰ عمر کش (Cache Duration)
مدت زمان کش یک موضوع حساس است. ⏳ هدف من پوشش دادن پنجرههای تلاش مجدد معقول بدون نگهداری دادههای منسوخ است. یک زمان کش معقول معمولاً از چند دقیقه تا ۲۴-۴۸ ساعت متغیر است و این بسته به مورد استفاده خاص شما دارد.
🧵 مدیریت همروندی (Concurrency)
همروندی میتواند دردسرساز باشد، به ویژه در APIهایی با ترافیک بالا. 🤯 یک پیادهسازی ایمن از نظر ریسمان (thread-safe) با استفاده از قفل توزیع شده (Distributed Lock) عالی عمل میکند. این کار کنترل امور را هنگامی که چندین درخواست همزمان وارد میشوند، حفظ میکند. اما این اتفاق باید یک رخداد نادر باشد.
💾 بکاند توزیع شده: Redis
برای تنظیمات توزیع شده، Redis انتخاب من است. 🚀 این ابزار به عنوان یک کش مشترک، عالی عمل میکند و همتوانی را در تمام نمونههای (Instances) API شما سازگار نگه میدارد. علاوه بر این، Redis قابلیت قفل توزیع شده را نیز مدیریت میکند.
🚫 جلوگیری از سوء استفاده از کلید
چه اتفاقی میافتد اگر یک کلاینت، کلید همتوانی را با یک بدنه درخواست (Request Body) متفاوت مجدداً استفاده کند؟ 🧐 در این حالت، من یک خطا برمیگردانم. رویکرد من این است که بدنه درخواست را هش (Hash) کنم و آن را با کلید همتوانی ذخیره کنم. هنگامی که یک درخواست وارد میشود، هشهای بدنه درخواست را مقایسه میکنم. اگر متفاوت باشند، خطا برمیگردانم. این کار از سوء استفاده از کلیدهای همتوانی جلوگیری کرده و یکپارچگی (Integrity) API شما را حفظ میکند.
📝 جمعبندی (Summary)
پیادهسازی همتوانی در REST APIها قابلیت اطمینان و سازگاری سرویس را افزایش میدهد. 📈 این تضمین میکند که درخواستهای یکسان، نتیجهای مشابه دارند و از تکرارهای ناخواسته جلوگیری کرده و مشکلات شبکه را به خوبی مدیریت میکنند.
در حالی که پیادهسازی ما یک پایه و اساس را فراهم میکند، توصیه میکنم آن را با نیازهای خود تطبیق دهید. 🎯 بر عملیاتهای حیاتی در APIهای خود تمرکز کنید، به ویژه آنهایی که وضعیت سیستم را تغییر میدهند یا فرآیندهای مهم کسب و کار را راهاندازی میکنند.
با پذیرش همتوانی، شما در حال ساختن APIهایی قویتر و کاربرپسندتر هستید. 💪
🔖 هشتگها:
#ASPNetCore #Idempotency
به نظرم Nick با همین سه خط کارو تموم کرد. ما دیگه کاری با اون more... نداریم🤙🏻
اگه قبول داری روی more... کلیک نکن که ادامش مهم نیست😅
اگه قبول داری روی more... کلیک نکن که ادامش مهم نیست😅
📖 سری آموزشی کتاب C# 12 in a Nutshell
Delegate
یک شیء است که میداند چگونه یک متد را فراخوانی کند. 📞
یک نوع دلیگیت (Delegate Type) نوع متدی را که نمونههای آن دلیگیت میتوانند فراخوانی کنند، تعریف میکند. به طور خاص، نوع بازگشتی متد و انواع پارامترهای آن را مشخص میکند. تعریف زیر، یک نوع دلیگیت به نام Transformer را تعریف میکند:
Transformer
با هر متدی که دارای نوع بازگشتی int و یک پارامتر int باشد، سازگار است، مانند این متد:
تخصیص یک متد به یک متغیر دلیگیت، یک نمونه دلیگیت (Delegate Instance) ایجاد میکند:
شما میتوانید یک نمونه دلیگیت را دقیقاً مانند یک متد فراخوانی کنید:
مثال کامل:
یک نمونه دلیگیت، به معنای واقعی کلمه، به عنوان نماینده (Delegate) برای فراخواننده عمل میکند: فراخواننده دلیگیت را فراخوانی میکند، و سپس دلیگیت متد هدف را صدا میزند. 🔄 این عدم وابستگی (Indirection)، فراخواننده را از متد هدف جدا میکند.
نکته فنی: عبارت
Transformer t = Square;
در حقیقت کوتاه شدهی
Transformer t = new Transformer (Square);
است. هنگامی که به Square بدون پرانتز اشاره میکنیم، از یک "Method Group" استفاده میکنیم. اگر متد Overload شده باشد، #C بر اساس امضای دلیگیتی که به آن اختصاص داده میشود، Overload صحیح را انتخاب میکند.
همچنین، عبارت t(3) کوتاه شدهی t.Invoke(3) است. 💡
دلیگیتها شبیه به Callback هستند که یک اصطلاح کلیتر است و ساختارهایی مانند اشارهگرهای تابع C را شامل میشود.
یک متغیر دلیگیت در زمان اجرا، متد را به خود میگیرد. این قابلیت برای نوشتن متدهای Plug-in بسیار مفید است.
در مثال زیر، ما یک متد کاربردی به نام Transform داریم که یک تابع تبدیل (Transform) را روی هر عنصر از یک آرایه اعمال میکند. متد Transform دارای یک پارامتر دلیگیت است که میتوانید از آن برای تعیین تابع Plug-in استفاده کنید:
ما میتوانیم به سادگی با تغییر Square به Cube در خط دوم کد، نوع تبدیل را عوض کنیم. 🔄
متد Transform ما یک تابع مرتبه بالاتر (Higher-Order Function) است، زیرا تابعی است که یک تابع دیگر را به عنوان آرگومان میپذیرد. (یک متد که یک دلیگیت را برمیگرداند نیز یک تابع مرتبه بالاتر محسوب میشود.) 🧠
به نظر شما، مهمترین کاربرد دلیگیتها (به خصوص قبل از معرفی Lambda Expressions و Action/Func) در معماری کدهای #C چه بود؟
💡 مفهوم Delegates (دلیگیتها): شیئی که متدها را صدا میزند
Delegate
یک شیء است که میداند چگونه یک متد را فراخوانی کند. 📞
یک نوع دلیگیت (Delegate Type) نوع متدی را که نمونههای آن دلیگیت میتوانند فراخوانی کنند، تعریف میکند. به طور خاص، نوع بازگشتی متد و انواع پارامترهای آن را مشخص میکند. تعریف زیر، یک نوع دلیگیت به نام Transformer را تعریف میکند:
delegate int Transformer (int x);
Transformer
با هر متدی که دارای نوع بازگشتی int و یک پارامتر int باشد، سازگار است، مانند این متد:
int Square (int x) { return x * x; }
// یا به شکل کوتاهتر:
int Square (int x) => x * x;🤝 ایجاد و فراخوانی دلیگیت
تخصیص یک متد به یک متغیر دلیگیت، یک نمونه دلیگیت (Delegate Instance) ایجاد میکند:
Transformer t = Square; // ایجاد نمونه دلیگیت
شما میتوانید یک نمونه دلیگیت را دقیقاً مانند یک متد فراخوانی کنید:
int answer = t(3); // answer برابر 9 میشود
مثال کامل:
Transformer t = Square; // Create delegate instance
int result = t(3); // Invoke delegate
Console.WriteLine (result); // 9
int Square (int x) => x * x;
delegate int Transformer (int x); // Delegate type declaration
یک نمونه دلیگیت، به معنای واقعی کلمه، به عنوان نماینده (Delegate) برای فراخواننده عمل میکند: فراخواننده دلیگیت را فراخوانی میکند، و سپس دلیگیت متد هدف را صدا میزند. 🔄 این عدم وابستگی (Indirection)، فراخواننده را از متد هدف جدا میکند.
نکته فنی: عبارت
Transformer t = Square;
در حقیقت کوتاه شدهی
Transformer t = new Transformer (Square);
است. هنگامی که به Square بدون پرانتز اشاره میکنیم، از یک "Method Group" استفاده میکنیم. اگر متد Overload شده باشد، #C بر اساس امضای دلیگیتی که به آن اختصاص داده میشود، Overload صحیح را انتخاب میکند.
همچنین، عبارت t(3) کوتاه شدهی t.Invoke(3) است. 💡
دلیگیتها شبیه به Callback هستند که یک اصطلاح کلیتر است و ساختارهایی مانند اشارهگرهای تابع C را شامل میشود.
🔌 نوشتن متدهای Plug-in با دلیگیتها
یک متغیر دلیگیت در زمان اجرا، متد را به خود میگیرد. این قابلیت برای نوشتن متدهای Plug-in بسیار مفید است.
در مثال زیر، ما یک متد کاربردی به نام Transform داریم که یک تابع تبدیل (Transform) را روی هر عنصر از یک آرایه اعمال میکند. متد Transform دارای یک پارامتر دلیگیت است که میتوانید از آن برای تعیین تابع Plug-in استفاده کنید:
int[] values = { 1, 2, 3 };
Transform (values, Square); // Hook in the Square method
foreach (int i in values) Console.Write (i + " "); // خروجی: 1 4 9
// پیادهسازی متد Transform
void Transform (int[] values, Transformer t)
{
for (int i = 0; i < values.Length; i++)
values[i] = t (values[i]);
}
int Square (int x) => x * x;
int Cube (int x) => x * x * x;
delegate int Transformer (int x);ما میتوانیم به سادگی با تغییر Square به Cube در خط دوم کد، نوع تبدیل را عوض کنیم. 🔄
متد Transform ما یک تابع مرتبه بالاتر (Higher-Order Function) است، زیرا تابعی است که یک تابع دیگر را به عنوان آرگومان میپذیرد. (یک متد که یک دلیگیت را برمیگرداند نیز یک تابع مرتبه بالاتر محسوب میشود.) 🧠
🤔 جمعبندی و تجربه شما
به نظر شما، مهمترین کاربرد دلیگیتها (به خصوص قبل از معرفی Lambda Expressions و Action/Func) در معماری کدهای #C چه بود؟
🔖 هشتگها:
#CSharp #Delegates #AdvancedCSharp #OOP #Callback
📖 سری آموزشی کتاب C# 12 in a Nutshell
متد هدف یک دلیگیت (Delegate) میتواند یک متد محلی (local)، Static یا Instance باشد. 💡
مثال زیر یک متد هدف Static را نشان میدهد:
مثال زیر یک متد هدف Instance را نشان میدهد:
هنگامی که یک متد Instance به یک شیء دلیگیت اختصاص داده میشود، آن شیء نه تنها یک رفرنس به خود متد، بلکه یک رفرنس به نمونه (Instance) که متد به آن تعلق دارد، را نیز حفظ میکند. 🤝
خاصیت Target از کلاس System.Delegate نشاندهنده همین نمونه است (و برای دلیگیتی که به یک متد Static اشاره میکند، null خواهد بود).
مثالی از حفظ Instance:
از آنجایی که نمونه در خاصیت Target دلیگیت ذخیره میشود، طول عمر آن حداقل تا زمانی که طول عمر دلیگیت است، تمدید میشود. 🕰
تمامی نمونههای دلیگیت قابلیت چندپخشی (Multicast) دارند. این بدان معناست که یک نمونه دلیگیت میتواند نه تنها به یک متد هدف، بلکه به لیستی از متدهای هدف اشاره کند.
اپراتورهای + و += نمونههای دلیگیت را ترکیب میکنند:
فراخوانی d اکنون هر دو متد SomeMethod1 و SomeMethod2 را صدا میزند. دلیگیتها به همان ترتیبی که اضافه شدهاند، فراخوانی میشوند. ➡️
اپراتورهای - و -= عملوند دلیگیت سمت راست را از عملوند دلیگیت سمت چپ حذف میکنند:
فراخوانی d اکنون تنها باعث فراخوانی SomeMethod2 میشود.
فراخوانی + یا += روی یک متغیر دلیگیت با مقدار null کار میکند و معادل تخصیص یک مقدار جدید به متغیر است.
دلیگیتها تغییرناپذیر (Immutable) هستند، بنابراین وقتی شما += یا -= را فراخوانی میکنید، در واقع یک نمونه دلیگیت جدید ایجاد کرده و آن را به متغیر موجود اختصاص میدهید. 🔄
اگر یک دلیگیت چندپخشی نوع بازگشتی غیر void داشته باشد، فراخواننده مقدار بازگشتی را از آخرین متدی که فراخوانی شده است، دریافت میکند. متدهای قبلی همچنان فراخوانی میشوند، اما مقادیر بازگشتی آنها نادیده گرفته میشوند.
متد HardWork یک پارامتر دلیگیت ProgressReporter دارد که برای گزارش پیشرفت کار از آن استفاده میشود:
برای نظارت بر پیشرفت، میتوانیم دو متد مستقل را ترکیب کنیم:
🎯 متدهای هدف (Target Methods): Static یا Instance؟
متد هدف یک دلیگیت (Delegate) میتواند یک متد محلی (local)، Static یا Instance باشد. 💡
🔹 متد هدف Static
مثال زیر یک متد هدف Static را نشان میدهد:
Transformer t = Test.Square;
Console.WriteLine (t(10)); // خروجی: 100
class Test { public static int Square (int x) => x * x; }
delegate int Transformer (int x);
🔸 متد هدف Instance
مثال زیر یک متد هدف Instance را نشان میدهد:
Test test = new Test();
Transformer t = test.Square;
Console.WriteLine (t(10)); // خروجی: 100
class Test { public int Square (int x) => x * x; }
delegate int Transformer (int x);
🧠 نحوه عملکرد با Instance Methodها
هنگامی که یک متد Instance به یک شیء دلیگیت اختصاص داده میشود، آن شیء نه تنها یک رفرنس به خود متد، بلکه یک رفرنس به نمونه (Instance) که متد به آن تعلق دارد، را نیز حفظ میکند. 🤝
خاصیت Target از کلاس System.Delegate نشاندهنده همین نمونه است (و برای دلیگیتی که به یک متد Static اشاره میکند، null خواهد بود).
مثالی از حفظ Instance:
MyReporter r = new MyReporter();
r.Prefix = "%Complete: ";
ProgressReporter p = r.ReportProgress;
p(99); // خروجی: %Complete: 99
Console.WriteLine (p.Target == r); // خروجی: True
Console.WriteLine (p.Method); // خروجی: Void ReportProgress(Int32)
r.Prefix = "";
p(99); // خروجی: 99
public delegate void ProgressReporter (int percentComplete);
class MyReporter
{
public string Prefix = "";
public void ReportProgress (int percentComplete)
=> Console.WriteLine (Prefix + percentComplete);
}
از آنجایی که نمونه در خاصیت Target دلیگیت ذخیره میشود، طول عمر آن حداقل تا زمانی که طول عمر دلیگیت است، تمدید میشود. 🕰
⛓️ دلیگیتهای چندپخشی (Multicast Delegates)
تمامی نمونههای دلیگیت قابلیت چندپخشی (Multicast) دارند. این بدان معناست که یک نمونه دلیگیت میتواند نه تنها به یک متد هدف، بلکه به لیستی از متدهای هدف اشاره کند.
➕ ترکیب (Combine) دلیگیتها
اپراتورهای + و += نمونههای دلیگیت را ترکیب میکنند:
SomeDelegate d = SomeMethod1;
d += SomeMethod2;
فراخوانی d اکنون هر دو متد SomeMethod1 و SomeMethod2 را صدا میزند. دلیگیتها به همان ترتیبی که اضافه شدهاند، فراخوانی میشوند. ➡️
➖ حذف (Remove) دلیگیتها
اپراتورهای - و -= عملوند دلیگیت سمت راست را از عملوند دلیگیت سمت چپ حذف میکنند:
d -= SomeMethod1;
فراخوانی d اکنون تنها باعث فراخوانی SomeMethod2 میشود.
💡 نکات تکمیلی
فراخوانی + یا += روی یک متغیر دلیگیت با مقدار null کار میکند و معادل تخصیص یک مقدار جدید به متغیر است.
دلیگیتها تغییرناپذیر (Immutable) هستند، بنابراین وقتی شما += یا -= را فراخوانی میکنید، در واقع یک نمونه دلیگیت جدید ایجاد کرده و آن را به متغیر موجود اختصاص میدهید. 🔄
⚠️ مقادیر بازگشتی در Multicast
اگر یک دلیگیت چندپخشی نوع بازگشتی غیر void داشته باشد، فراخواننده مقدار بازگشتی را از آخرین متدی که فراخوانی شده است، دریافت میکند. متدهای قبلی همچنان فراخوانی میشوند، اما مقادیر بازگشتی آنها نادیده گرفته میشوند.
🖥 مثال عملی از Multicast Delegate
متد HardWork یک پارامتر دلیگیت ProgressReporter دارد که برای گزارش پیشرفت کار از آن استفاده میشود:
public delegate void ProgressReporter (int percentComplete);
public class Util
{
public static void HardWork (ProgressReporter p)
{
for (int i = 0; i < 10; i++)
{
p (i * 10); // Invoke delegate
System.Threading.Thread.Sleep (100); // Simulate hard work
}
}
}
برای نظارت بر پیشرفت، میتوانیم دو متد مستقل را ترکیب کنیم:
ProgressReporter p = WriteProgressToConsole;
p += WriteProgressToFile; // ترکیب دو متد
Util.HardWork (p);
void WriteProgressToConsole (int percentComplete)
=> Console.WriteLine (percentComplete);
void WriteProgressToFile (int percentComplete)
=> System.IO.File.WriteAllText ("progress.txt",
percentComplete.ToString());
🔖 هشتگها:
#CSharp #Delegates #Multicast #OOP #AdvancedCSharp
⚙️ موارد پیشرفته در Rate Limiting (محدودسازی نرخ) در NET.
محدودسازی نرخ (Rate Limiting) به معنای محدود کردن تعداد درخواستها به برنامه شماست. این محدودیت معمولاً در یک بازه زمانی خاص یا بر اساس معیارهای دیگر اعمال میشود. ⏱️
🛡 دلایل اهمیت Rate Limiting
محدودسازی نرخ به دلایل زیر بسیار مفید است:
• امنیت را بهبود میبخشد.
• در برابر حملات DDoS محافظت میکند.
• از Overloading (بارگذاری بیش از حد) سرورهای برنامه جلوگیری میکند.
• با جلوگیری از مصرف غیرضروری منابع، هزینهها را کاهش میدهد.
نکته: در حالی که NET 7. با یک Rate Limiter داخلی عرضه شد، اما باید بدانید که چگونه آن را به درستی پیادهسازی کنید تا سیستم شما دچار توقف نشود. 🛑
🎯 در این پست خواهید آموخت:
🔹️ چگونه کاربران را بر اساس آدرس IP محدود کنیم.
🔹️ چگونه کاربران را بر اساس هویت (Identity) آنها محدود کنیم.
🔹️ چگونه Rate Limiting را روی Reverse Proxy اعمال کنیم.
بنابراین، بیایید شیرجه بزنیم! 👇
✨ Rate Limiting داخلی در .NET 7
از نسخه NET 7. به بعد، ما به Middleware محدودسازی نرخ داخلی در فضای نام Microsoft.AspNetCore.RateLimiting دسترسی داریم. API آن ساده است و شما میتوانید با چند خط کد، یک سیاست محدودسازی نرخ (Rate Limit Policy) ایجاد کنید.
ما میتوانیم از یکی از چهار الگوریتم محدودسازی نرخ استفاده کنیم:
• Fixed window (پنجره ثابت)
• Sliding window (پنجره کشویی)
• Token bucket (سطل توکن)
• Concurrency (همروندی)
💻 تعریف سیاست Token Bucket
در اینجا نحوه تعریف یک سیاست محدودسازی نرخ با فراخوانی متد AddTokenBucketLimiter آمده است:
builder.Services.AddRateLimiter(rateLimiterOptions =>
{
rateLimiterOptions.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
rateLimiterOptions.AddTokenBucketLimiter("token", options =>
{
options.TokenLimit = 1000;
options.ReplenishmentPeriod = TimeSpan.FromHours(1);
options.TokensPerPeriod = 700;
options.AutoReplenishment = true;
});
});
حالا میتوانید سیاست محدودسازی نرخ با نام token را در اِندپوینت یا کنترلر خود ارجاع دهید.
شما همچنین باید RateLimitingMiddleware را به خط لوله درخواست (Request Pipeline) اضافه کنید:
app.UseRateLimiter();
شما میتوانید جزئیات بیشتری درباره Rate Limiting در NET 7. کسب کنید.
📡 محدودسازی نرخ کاربران بر اساس آدرس IP
رویکردی که پیشتر نشان داده شد، یک مشکل دارد - سیاست محدودسازی نرخ سراسری (Global) است و بر همه کاربران اعمال میشود. ❌
اغلب اوقات، شما نمیخواهید این کار را انجام دهید. محدودسازی نرخ باید جزئی (Granular) باشد و بر تکتک کاربران اعمال شود. 👤
خوشبختانه، میتوانید با ایجاد یک RateLimitPartition به این هدف برسید.
🧱 اجزای RateLimitPartition
RateLimitPartition دو جزء دارد:
🔹️ کلید پارتیشن (Partition key)
🔹️ سیاست محدودسازی نرخ (Rate limiter policy)
💻 پیادهسازی محدودسازی با IP Address
در اینجا نحوه تعریف یک Rate Limiter با سیاست Fixed Window آمده است، که در آن کلید پارتیشن، آدرس IP کاربر است:
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("fixed-by-ip", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString(),
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromMinutes(1)
}));
});
محدودسازی نرخ بر اساس آدرس IP میتواند یک لایه امنیتی خوب برای کاربران احراز هویت نشده (unauthenticated users) باشد. 🔐 شما نمیدانید چه کسی به سیستم شما دسترسی دارد و نمیتوانید محدودسازی نرخ جزئیتری اعمال کنید. این روش میتواند به محافظت از سیستم شما در برابر کاربران مخربی که سعی در انجام حمله DDoS دارند، کمک کند.
🔗 ایجاد محدودکنندههای زنجیرهای (Chained Limiters)
شما همچنین میتوانید با استفاده از API با نام CreateChained، محدودکنندههای زنجیرهای ایجاد کنید. این API به شما اجازه میدهد تا چندین PartitionedRateLimiter را ارسال کنید که در یک PartitionedRateLimiter ترکیب میشوند. محدودکننده زنجیرهای، تمام محدودکنندههای ورودی را به صورت متوالی (Sequence) (یکی پس از دیگری) اجرا میکند. 🔄
🛡 ملاحظات Reverse Proxy
اگر برنامه شما پشت یک Reverse Proxy در حال اجراست، باید مطمئن شوید که آدرس IP خود پروکسی را محدود نکنید. ⚠️ Reverse Proxyها معمولاً آدرس IP اصلی کاربر را با استفاده از هدر X-Forwarded-For ارسال میکنند. بنابراین، میتوانید از این هدر به عنوان کلید پارتیشن استفاده کنید:
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("fixed-by-ip", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
httpContext.Request.Headers["X-Forwarded-For"].ToString(), // استفاده از هدر
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromMinutes(1)
}));
});
🧑💻 محدودسازی نرخ کاربران بر اساس هویت (Identity)
اگر از کاربران میخواهید که در API شما احراز هویت (Authenticate) کنند، میتوانید تشخیص دهید که کاربر فعلی کیست. سپس میتوانید از هویت (Identity) کاربر به عنوان کلید پارتیشن (Partition Key) برای یک RateLimitPartition استفاده کنید. 🆔
💻 پیادهسازی محدودسازی با هویت کاربر
در اینجا نحوه ایجاد چنین سیاست محدودسازی نرخ آمده است:
builder.Services.AddRateLimiter(options =>
{
options.AddPolicy("fixed-by-user", httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.Identity?.Name?.ToString(),
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 10,
Window = TimeSpan.FromMinutes(1)
}));
});
من از مقدار User.Identity در HttpContext استفاده میکنم تا Claim مربوط به Name کاربر فعلی را به دست آورم. این معمولاً متناظر با Claim با نام sub درون یک JWT است - که همان شناسه کاربر میباشد.
🛡 اعمال Rate Limiting روی Reverse Proxy
در یک پیادهسازی قوی، شما میخواهید محدودسازی نرخ را در سطح Reverse Proxy اعمال کنید، پیش از آنکه درخواست به API شما برسد. و اگر یک سیستم توزیع شده دارید، این یک الزام است. در غیر این صورت، سیستم شما به درستی کار نخواهد کرد. 🚨
پیادهسازیهای متعددی برای Reverse Proxy وجود دارد که میتوانید از بین آنها انتخاب کنید.
YARP
یک Reverse Proxy با یکپارچگی عالی با NET. است. این امر تعجبآور نیست، زیرا YARP با #C نوشته شده است.
⚙️ اعمال Rate Limiting در تنظیمات YARP
برای پیادهسازی محدودسازی نرخ روی Reverse Proxy با استفاده از YARP، شما باید:
یک سیاست محدودسازی نرخ تعریف کنید (همانند مثالهای قبلی).
RateLimiterPolicy
را برای مسیر (Route) در تنظیمات YARP پیکربندی کنید:
"products-route": {
"ClusterId": "products-cluster",
"RateLimiterPolicy": "sixty-per-minute-fixed",
"Match": {
"Path": "/products/{**catch-all}"
},
"Transforms": [
{ "PathPattern": "{**catch-all}" }
]
}توجه به حافظه: Middleware محدودسازی نرخ داخلی، از یک حافظه in-memory برای ردیابی تعداد درخواستها استفاده میکند. اگر میخواهید Reverse Proxy خود را در یک راهاندازی با در دسترس بودن بالا (High-Availability) اجرا کنید، به استفاده از یک Distributed Cache نیاز خواهید داشت. استفاده از یک Redis backplane برای محدودسازی نرخ، یک گزینه خوب برای بررسی است. 💾
📝 سخن پایانی
با استفاده از PartitionedRateLimiter میتوانید به راحتی سیاستهای محدودسازی نرخ جزئی (Granular) ایجاد کنید.
دو رویکرد رایج عبارتند از:
محدودسازی نرخ بر اساس آدرس IP 🌐
محدودسازی نرخ بر اساس شناسه کاربر (User Identifier) 👤
تیم NET. محدودسازی نرخ را ارائه کرد که بسیار شگفت انگیز است. اما پیادهسازی فعلی کاستیهایی دارد. مشکل اصلی این است که فقط به صورت in-memory کار میکند. برای یک راهکار توزیع شده (distributed)، شما باید خودتان چیزی پیادهسازی کنید یا از یک کتابخانه خارجی استفاده نمایید.
شما میتوانید از Reverse Proxy YARP برای ساختن سیستمهای توزیع شده قوی و مقیاسپذیر استفاده کنید. و اضافه کردن Rate Limiting در سطح Reverse Proxy تنها به چند خط کد نیاز دارد. بهتر است که به طور گستردهای از آن در سیستمهایمان استفاده میکنیم.
از اینکه این مقاله را خواندید، متشکرم. و فوقالعاده بمانید! ✨
🔖 هشتگها:
#DotNet #RateLimiting #Security #ASPNETCore #ReverseProxy #IPAddress
💡 شکست واقعی در Backend Engineering چیست؟
شکست در Backend Engineering، نوشتن کد بد نیست. ❌
خراب شدن یک دیپلوی (Deployment) نیست. 💥
طراحی یک شمای پایگاه داده (Schema) که مقیاسپذیر نیست، نیست. 📈
اینها فقط یادگیری هستند. 🧠
🛑 پس شکست واقعی چیست؟
شکست واقعی؟
این است که بخواهی یک توسعهدهنده (Dev) باشی، اما هرگز شروع نکنی. 🚪
بیشتر مردم منتظر میمانند تا:
• آموزش "کامل" را پیدا کنند.
• دوره "درست" را پیدا کنند.
• وقت آزاد بیشتری داشته باشند.
اما Backend Engineering تنها با انجام دادن، آموخته میشود: 💻
• نوشتن اولین API خود.
• شکستن اولین شمای پایگاه داده خود.
• دیپلوی کردن چیزی که به سختی کار میکند.
این اشتباهات بخشی از فرآیند هستند. 🧩
اجتناب از آنها همان چیزی است که شما را عقب نگه میدارد. 🛑
آشفته شروع کن. کوچک شروع کن. فقط شروع کن. ✨
شکست در Backend Engineering، نوشتن کد بد نیست. ❌
خراب شدن یک دیپلوی (Deployment) نیست. 💥
طراحی یک شمای پایگاه داده (Schema) که مقیاسپذیر نیست، نیست. 📈
اینها فقط یادگیری هستند. 🧠
🛑 پس شکست واقعی چیست؟
شکست واقعی؟
این است که بخواهی یک توسعهدهنده (Dev) باشی، اما هرگز شروع نکنی. 🚪
بیشتر مردم منتظر میمانند تا:
• آموزش "کامل" را پیدا کنند.
• دوره "درست" را پیدا کنند.
• وقت آزاد بیشتری داشته باشند.
اما Backend Engineering تنها با انجام دادن، آموخته میشود: 💻
• نوشتن اولین API خود.
• شکستن اولین شمای پایگاه داده خود.
• دیپلوی کردن چیزی که به سختی کار میکند.
این اشتباهات بخشی از فرآیند هستند. 🧩
اجتناب از آنها همان چیزی است که شما را عقب نگه میدارد. 🛑
آشفته شروع کن. کوچک شروع کن. فقط شروع کن. ✨
📩 پیامرسانی ساده در NET. با Redis Pub/Sub
ردیس یک انتخاب محبوب برای کش کردن دادهها است، اما قابلیتهای آن بسیار فراتر از این است. یکی از ویژگیهای کمتر شناخته شده آن، پشتیبانی از Pub/Sub است. کانالهای Redis یک رویکرد جذاب برای پیادهسازی پیامرسانی Real-time در برنامههای NET. شما ارائه میدهند. با این حال، همانطور که به زودی خواهید دید، کانالها دارای نقاط ضعفی نیز هستند. 🧐
💡 در این پست بررسی خواهیم کرد:
• اصول اولیه کانالهای Redis.
• موارد استفاده عملی برای کانالها.
• پیادهسازی یک مثال Pub/Sub در NET.
• ابطال کش (Cache Invalidation) در سیستمهای توزیع شده.
بیایید وارد جزئیات شویم! 👇
🌐 کانالهای ردیس (Redis Channels)
Redis channels
کانالهای ارتباطی نامگذاری شدهای هستند که الگوی پیامرسانی Publish/Subscribe را پیادهسازی میکنند. 🗣
هر کانال با یک نام منحصر به فرد (مانند notifications یا updates) شناسایی میشود. کانالها تحویل پیام از Publishers (ناشران) به Subscribers (مشترکین) را تسهیل میکنند.
Publishers
از دستور PUBLISH برای ارسال پیام به یک کانال خاص استفاده میکنند.
Subscribers
از دستور SUBSCRIBE برای ثبت علاقه به دریافت پیام از یک کانال استفاده میکنند.
🔄 مدل Topic-Based
کانالهای Redis از یک مدل Publish-Subscribe مبتنی بر Topic (موضوع) پیروی میکنند. چندین Publisher میتوانند به یک کانال پیام ارسال کنند و چندین Subscriber میتوانند پیامها را از آن کانال دریافت کنند.
⚠️ محدودیت کلیدی: عدم ذخیره پیام
با این حال، توجه به این نکته بسیار حیاتی است که کانالهای Redis پیامها را ذخیره نمیکنند. ❌ اگر هنگام انتشار یک پیام، هیچ مشترکی برای یک کانال وجود نداشته باشد، آن پیام بلافاصله دور ریخته میشود.
کانالهای Redis دارای معناشناسی "حداکثر یک بار تحویل" (at-most-once delivery) هستند.
📩 موارد استفاده عملی (Practical Use Cases)
با توجه به اینکه کانالهای Redis با معناشناسی "حداکثر یک بار تحویل" (at-most-once delivery) کار میکنند (پیامها در صورت عدم حضور مشترک ممکن است از دست بروند)، این کانالها برای سناریوهایی مناسب هستند که از دست دادن گاهبهگاه پیام قابل قبول بوده و ارتباط Real-time یا نزدیک به Real-time مطلوب است. ✅
🎯 چند مورد استفاده ممکن:
🔹️فیدهای شبکههای اجتماعی: انتشار پستها یا بهروزرسانیهای جدید برای کاربران. 📢
🔹️بهروزرسانی امتیازات زنده: ارسال امتیازات زنده بازیها یا بهروزرسانیهای ورزشی برای مشترکین. ⚽️
🔹️اپلیکیشنهای چت: تحویل پیامهای چت به صورت Real-time به شرکتکنندگان فعال. 💬
🔹️ویرایش مشارکتی: انتشار تغییرات در محیطهای ویرایش مشارکتی.
🔹️بهروزرسانیهای Distributed Cache: ابطال ورودیهای کش در سرورهای متعدد هنگام تغییر دادهها. (این مورد را در ادامه با جزئیات بیشتر پوشش خواهیم داد.)
💡نکته مهم: کانالهای Redis بهترین انتخاب برای دادههای حیاتی که از دست دادن پیام در آنها غیرقابل قبول است، نیستند. در چنین مواردی، باید یک سیستم پیامرسانی با قابلیت اطمینان بالاتر را در نظر بگیرید. 🔐
💻 پیادهسازی Pub/Sub با Redis Channels در NET.
ما برای ارسال پیامها با کانالهای Redis از کتابخانه StackExchange.Redis استفاده خواهیم کرد.
📥 نصب و راهاندازی
ابتدا آن را نصب میکنیم:
Install-Package StackExchange.Redis
شما میتوانید Redis را به صورت محلی در یک کانتینر Docker اجرا کنید. پورت پیشفرض 6379 است:
docker run -it -p 6379:6379 redis
📤 سرویس Producer (ناشر)
در اینجا یک سرویس Background ساده آمده است که به عنوان Producer (ناشر) پیامهای ما عمل میکند.
ما با اتصال به نمونه Redis خود یک ConnectionMultiplexer ایجاد میکنیم. این به ما امکان میدهد تا یک ISubscriber به دست آوریم که میتوانیم از آن برای پیامرسانی Pub/Sub استفاده کنیم. ISubscriber ما را قادر میسازد تا با تعیین نام کانال، یک پیام را در آن کانال منتشر کنیم.
public class Producer(ILogger<Producer> logger) : BackgroundService
{
private static readonly string ConnectionString = "localhost:6379";
private static readonly ConnectionMultiplexer Connection =
ConnectionMultiplexer.Connect(ConnectionString);
private const string Channel = "messages";
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var subscriber = Connection.GetSubscriber();
while (!stoppingToken.IsCancellationRequested)
{
var message = new Message(Guid.NewGuid(), DateTime.UtcNow);
var json = JsonSerializer.Serialize(message);
await subscriber.PublishAsync(Channel, json);
logger.LogInformation(
"Sending message: {Channel} - {@Message}",
message);
await Task.Delay(5000, stoppingToken);
}
}
}
حالا نوبت به معرفی یک سرویس Background مجزا برای مصرف پیامها میرسد. ⬇️
📩 پیامرسانی ساده در NET. با Redis Pub/Sub (ادامه)
📥 سرویس Consumer (مصرفکننده)
Consumer
به همان نمونه Redis متصل میشود و یک ISubscriber به دست میآورد. ISubscriber متد SubscribeAsync را در اختیار ما قرار میدهد که میتوانیم از آن برای اشتراک در دریافت پیام از یک کانال مشخص استفاده کنیم. این متد یک Callback Delegate را میپذیرد که میتوانیم از آن برای رسیدگی به پیام دریافتی استفاده کنیم. 🎧
public class Consumer(ILogger<Consumer> logger) : BackgroundService
{
private static readonly string ConnectionString = "localhost:6379";
private static readonly ConnectionMultiplexer Connection =
ConnectionMultiplexer.Connect(ConnectionString);
private const string Channel = "messages";
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var subscriber = Connection.GetSubscriber();
await subscriber.SubscribeAsync(Channel, (channel, message) =>
{
var message = JsonSerializer.Deserialize<Message>(message);
logger.LogInformation(
"Received message: {Channel} - {@Message}",
channel,
message);
});
}
}
💾 ابطال کش در سیستمهای توزیع شده (Cache Invalidation)
در یک پروژه اخیر، من یک چالش رایج در سیستمهای توزیع شده را حل کردم: همگام نگه داشتن کشها. 🧩 ما از یک رویکرد کشینگ دو سطحی استفاده میکردیم:
کش in-memory روی هر وب سرور برای دسترسی فوقسریع.
کش مشترک Redis برای جلوگیری از ضربه زدن مکرر به پایگاه داده.
مشکل این بود که وقتی دادهای در پایگاه داده تغییر میکرد، به راهی نیاز داشتیم تا به سرعت به تمام وب سرورها بگوییم که کشهای in-memory خود را پاک کنند. اینجا بود که Redis Pub/Sub به کمک آمد. ما یک کانال Redis را به طور خاص برای پیامهای ابطال کش راهاندازی کردیم. 📣
💻 پیادهسازی CacheInvalidationBackgroundService
هر برنامه یک سرویس CacheInvalidationBackgroundService را اجرا میکند که در کانال ابطال کش عضو میشود (Subscribe میکند):
public class CacheInvalidationBackgroundService(
IServiceProvider serviceProvider)
: BackgroundService
{
public const string Channel = "cache-invalidation";
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await subscriber.SubscribeAsync(Channel, (channel, key) =>
{
var cache = serviceProvider.GetRequiredService<IMemoryCache>();
cache.Remove(key);
return Task.CompletedTask;
});
}
}
هر زمان که دادهای در پایگاه داده تغییر میکند، ما یک پیام را در این کانال با کلید کش داده بهروز شده، منتشر میکنیم. تمام وب سرورها در این کانال عضو هستند، بنابراین فوراً از حذف داده قدیمی از کشهای in-memory خود مطلع میشوند. از آنجایی که کش in-memory در صورت از کار افتادن برنامه پاک میشود، از دست دادن پیامهای ابطال کش مشکلی ایجاد نمیکند. این کار کشهای ما را سازگار نگه میدارد و تضمین میکند که کاربران ما همیشه بهروزترین اطلاعات را میبینند. ✅
📝 خلاصه
Redis Pub/Sub
یک راهحل جادویی برای هر نیاز پیامرسانی نیست، اما سادگی و سرعت آن، آن را به یک ابزار ارزشمند تبدیل میکند. کانالها به ما امکان میدهند تا به راحتی ارتباط بین مؤلفههای با اتصال سست (loosely coupled components) را پیادهسازی کنیم. 🔗
کانالهای Redis دارای معناشناسی "حداکثر یک بار تحویل" (at-most-once delivery) هستند، بنابراین برای مواردی که از دست دادن گاهبهگاه پیام قابل قبول است، بهترین گزینه میباشند.
موفق باشید! 👋
🔖 هشتگها:
#Redis #PubSub #CacheInvalidation #DistributedSystems #DotNet #Messaging #BackgroundService
🌐 مروری بر HTTP
بدانیم که HTTP پروتکلی برای واکشی (fetching) منابعی مانند اسناد HTML است. این پروتکل، بنیان هرگونه تبادل داده در وب محسوب میشود و یک پروتکل کلاینت-سرور (Client-Server) است. این بدان معناست که درخواستها توسط گیرنده، که معمولاً مرورگر وب است، آغاز میشوند. 🖥 یک سند کامل معمولاً از منابعی مانند محتوای متنی، دستورالعملهای طرحبندی (layout)، تصاویر، ویدئوها، اسکریپتها و موارد دیگر ساخته میشود.
📬 تبادل پیامها (Messages)
کلاینتها (Clients) و سرورها (Servers) از طریق تبادل پیامهای مجزا با یکدیگر ارتباط برقرار میکنند (برخلاف یک جریان داده یا stream).
• پیامهایی که توسط کلاینت ارسال میشوند، درخواست (Requests) نامیده میشوند.
• پیامهایی که توسط سرور به عنوان پاسخ ارسال میشوند، پاسخ (Responses) نام دارند.
⚙️ ماهیت و تکامل HTTP
ماهیت HTTP که در اوایل دهه ۱۹۹۰ طراحی شد، یک پروتکل قابل توسعه (Extensible) است که در طول زمان تکامل یافته است.
این پروتکل یک پروتکل لایه کاربرد (Application Layer Protocol) است که روی TCP یا یک اتصال TCP رمزگذاری شده با TLS ارسال میشود، اگرچه از لحاظ نظری هر پروتکل انتقال قابل اعتماد دیگری میتواند استفاده شود.
به دلیل قابلیت توسعهپذیری، HTTP نه تنها برای واکشی hypertext documents، بلکه برای تصاویر و ویدئوها یا ارسال محتوا به سرورها (مانند نتایج فرمهای HTML) نیز استفاده میشود. همچنین میتوان از HTTP برای واکشی بخشهایی از اسناد استفاده کرد تا صفحات وب را در صورت نیاز بهروزرسانی کند. 🔄
📋 در این مقاله موارد زیر بررسی خواهند شد:
• اجزای سیستمهای مبتنی بر HTTP
• جنبههای اساسی HTTP
• آنچه با HTTP قابل کنترل است
• جریان HTTP (HTTP flow)
• پیامهای HTTP (HTTP Messages)
• و Api های مبتنی بر HTTP
🏗 اجزای سازنده سیستمهای مبتنی بر HTTP
گفتیم HTTP یک پروتکل کلاینت-سرور (Client-Server) است: درخواستها توسط یک موجودیت به نام User-agent (یا یک پروکسی به نمایندگی از آن) ارسال میشوند. 👤
اغلب اوقات،User-agent همان مرورگر وب است، اما میتواند هر چیزی باشد؛ برای مثال، یک ربات که وب را میخزد تا ایندکس موتور جستجو را پر کرده و نگهداری کند.
هر درخواست مجزا به یک سرور ارسال میشود، که آن را مدیریت کرده و پاسخی به نام پاسخ (Response) ارائه میدهد. بین کلاینت و سرور، موجودیتهای متعددی وجود دارند که در مجموع پروکسیها (Proxies) نامیده میشوند و عملیاتهای مختلفی را انجام میدهند و مثلاً به عنوان دروازه (Gateways) یا کشها (Caches) عمل میکنند. 🛡
⚠️ ملاحظات لایههای زیرین
در واقعیت، کامپیوترهای بیشتری بین یک مرورگر و سروری که درخواست را مدیریت میکند، وجود دارند: مسیریابها (Routers)، مودمها و غیره. به لطف طراحی لایهای وب، این موارد در لایههای Network و Transport پنهان هستند. HTTP در بالا، در Application Layer قرار دارد. اگرچه لایههای زیرین برای تشخیص مشکلات شبکه مهم هستند، اما در توصیف HTTP عمدتاً بیربط تلقی میشوند.
1️⃣ کلاینت: (User-agent)
گفتیم User-agent هر ابزاری است که از طرف کاربر عمل میکند. این نقش در درجه اول توسط مرورگر وب انجام میشود، اما همچنین ممکن است توسط برنامههایی که مهندسان و توسعهدهندگان وب برای Debug کردن برنامههای خود استفاده میکنند، ایفا شود. 🛠
مرورگر همیشه موجودیتی است که درخواست را آغاز میکند. این کار هرگز توسط سرور انجام نمیشود (اگرچه مکانیسمهایی در طول سالها برای شبیهسازی پیامهای آغاز شده توسط سرور اضافه شدهاند).
📖 فرایند نمایش صفحه
برای نمایش یک صفحه وب، مرورگر یک درخواست اولیه برای واکشی سند HTML که نمایانگر صفحه است، ارسال میکند. سپس این فایل را تجزیه (Parse) میکند و درخواستهای اضافی مربوط به اسکریپتهای اجرایی، اطلاعات طرحبندی (CSS) برای نمایش، و زیرمنابع موجود در صفحه (معمولاً تصاویر و ویدئوها) را ایجاد میکند. سپس مرورگر وب این منابع را ترکیب میکند تا سند کامل، یعنی صفحه وب را ارائه دهد. اسکریپتهایی که توسط مرورگر اجرا میشوند، میتوانند منابع بیشتری را در مراحل بعدی واکشی کنند و مرورگر صفحه وب را بر اساس آن بهروز میکند. 🔄
🔗 مفهوم Hypertext
یک صفحه وب یک سند Hypertext است. این بدان معنی است که برخی از بخشهای محتوای نمایش داده شده، لینک هستند که میتوانند فعال شوند (معمولاً با کلیک ماوس) تا یک صفحه وب جدید واکشی شود و به کاربر اجازه میدهد تا عامل کاربر خود را هدایت کرده و در وب ناوبری (Navigate) کند. مرورگر این دستورالعملها را به درخواستهای HTTP ترجمه میکند و پاسخهای HTTP را تفسیر میکند تا یک پاسخ واضح به کاربر ارائه دهد.
2️⃣ سرور وب (The Web Server)
در سمت مخالف کانال ارتباطی، سرور قرار دارد که سندی را که توسط کلاینت درخواست شده است، سرو (Serve) میکند. 💾
یک سرور از نظر مجازی فقط به عنوان یک ماشین واحد ظاهر میشود؛ اما در واقع ممکن است مجموعهای از سرورها باشد که بار را به اشتراک میگذارند (Load Balancing)، یا نرمافزارهای دیگری (مانند کشها، یک سرور پایگاه داده، یا سرورهای تجارت الکترونیک)، که سند را به طور کامل یا جزئی بر اساس درخواست تولید میکنند.
نکته: یک سرور لزوماً یک ماشین واحد نیست، بلکه چندین نمونه نرمافزار سرور میتوانند روی یک ماشین میزبانی شوند. با HTTP/1.1 و هدر Host، حتی ممکن است آدرس IP یکسانی داشته باشند. 🏠
3️⃣ پروکسیها (Proxies)
بین مرورگر وب و سرور، کامپیوترها و ماشینهای متعددی پیامهای HTTP را منتقل میکنند. به دلیل ساختار لایهای پشته وب، اکثر اینها در لایههای انتقال، شبکه یا فیزیکی عمل میکنند و در لایه HTTP شفاف میشوند و به طور بالقوه تأثیر قابل توجهی بر عملکرد دارند. ⚙️
آنهایی که در لایههای کاربرد عمل میکنند، عموماً پروکسیها نامیده میشوند. این پروکسیها میتوانند شفاف (Transparent) باشند و درخواستهایی را که دریافت میکنند بدون هیچ تغییری به جلو ارسال کنند، یا غیرشفاف (Non-transparent) باشند، که در این صورت قبل از ارسال درخواست به سرور، آن را به نحوی تغییر خواهند داد.
🎯 وظایف پروکسیها
پروکسیها ممکن است عملکردهای متعددی را انجام دهند:
🔹️کشینگ (Caching): (کش میتواند عمومی یا خصوصی باشد، مانند کش مرورگر)
🔹️فیلترینگ (Filtering): (مانند اسکن آنتیویروس یا کنترل والدین) 🛡
🔹️توزیع بار (Load Balancing): (برای اجازه دادن به سرورهای متعدد برای ارائه درخواستهای مختلف)
🔹️احراز هویت (Authentication): (برای کنترل دسترسی به منابع مختلف)
🔹️ثبت وقایع (Logging): (اجازه ذخیره اطلاعات تاریخی) 📜
🔑 جنبههای اساسی HTTP
به طور کلی طوری طراحی شده است که برای انسانها قابل خواندن باشد، حتی با پیچیدگیهای اضافه شده در HTTP/2 از طریق کپسولهسازی پیامهای HTTP در Frameها. 📖
پیامهای HTTP را میتوان توسط انسان خواند و درک کرد، که آزمایش را برای توسعهدهندگان آسانتر کرده و پیچیدگی را برای تازهواردها کاهش میدهد.
با معرفی HTTP Headers در HTTP/1.0، این پروتکل به راحتی قابل توسعه و آزمایش است. 🛠 عملکرد جدید حتی میتواند از طریق توافق بین یک کلاینت و یک سرور در مورد معناشناسی (semantics) یک هدر جدید معرفی شود.
اما در حالی که هسته HTTP خودش بدون وضعیت است، کوکیهای HTTP اجازه استفاده از Sessionهای با وضعیت (Stateful Sessions) را میدهند. با استفاده از قابلیت توسعه هدر، کوکیهای HTTP به گردش کار اضافه میشوند و امکان ایجاد Session در هر درخواست HTTP را برای به اشتراک گذاشتن زمینه (Context) یا همان وضعیت (State) فراهم میکنند. 🍪
اتصال در لایه انتقال کنترل میشود و بنابراین اساساً خارج از حوزه HTTP است. HTTP نیاز ندارد که پروتکل انتقال زیرین مبتنی بر اتصال باشد؛ بلکه تنها نیازمند این است که قابل اعتماد (reliable) باشد، یا پیامها را از دست ندهد (حداقل در چنین مواردی خطا ارائه دهد).
از بین دو پروتکل انتقال رایج در اینترنت، TCP قابل اعتماد است و UDP نیست. بنابراین HTTP بر استاندارد TCP تکیه دارد که مبتنی بر اتصال است. 🌐
قبل از اینکه یک کلاینت و سرور بتوانند یک جفت درخواست/پاسخ HTTP را تبادل کنند، باید یک اتصال TCP ایجاد کنند، فرآیندی که نیاز به چندین رفت و برگشت (round-trips) دارد. 🐢
رفتار پیشفرض HTTP/1.0 این بود که یک اتصال TCP مجزا برای هر جفت درخواست/پاسخ HTTP باز کند. این کار در هنگام ارسال درخواستهای متعدد در فاصله زمانی کوتاه، کارایی کمتری دارد.
برای کاهش این نقص، HTTP/1.1 پایپلاینینگ (pipelining) (که پیادهسازی آن دشوار بود) و اتصالات پایدار (persistent connections) را معرفی کرد: اتصال TCP زیرین میتواند با استفاده از هدر Connection تا حدی کنترل شود.
بعد HTTP/2 یک گام فراتر رفت و با Multiplexing پیامها روی یک اتصال واحد، به گرم نگه داشتن اتصال و کارایی بیشتر کمک کرد.
آزمایشهایی برای طراحی یک پروتکل انتقال بهتر که مناسبتر برای HTTP باشد، در حال انجام است. برای مثال، Google در حال آزمایش QUIC است که بر پایه UDP ساخته شده تا یک پروتکل انتقال قابل اعتمادتر و کارآمدتر را فراهم کند.
1️⃣کلا HTTP ساده است (HTTP is simple)
به طور کلی طوری طراحی شده است که برای انسانها قابل خواندن باشد، حتی با پیچیدگیهای اضافه شده در HTTP/2 از طریق کپسولهسازی پیامهای HTTP در Frameها. 📖
پیامهای HTTP را میتوان توسط انسان خواند و درک کرد، که آزمایش را برای توسعهدهندگان آسانتر کرده و پیچیدگی را برای تازهواردها کاهش میدهد.
2️⃣ HTTP قابل توسعه است (HTTP is extensible)
با معرفی HTTP Headers در HTTP/1.0، این پروتکل به راحتی قابل توسعه و آزمایش است. 🛠 عملکرد جدید حتی میتواند از طریق توافق بین یک کلاینت و یک سرور در مورد معناشناسی (semantics) یک هدر جدید معرفی شود.
3️⃣ و HTTP بدون وضعیت است، اما بدون Session نیست (HTTP is stateless, but not sessionless)بدون وضعیت (Stateless) است: هیچ ارتباطی بین دو درخواستی که به طور متوالی روی یک اتصال انجام میشوند، وجود ندارد.
اما در حالی که هسته HTTP خودش بدون وضعیت است، کوکیهای HTTP اجازه استفاده از Sessionهای با وضعیت (Stateful Sessions) را میدهند. با استفاده از قابلیت توسعه هدر، کوکیهای HTTP به گردش کار اضافه میشوند و امکان ایجاد Session در هر درخواست HTTP را برای به اشتراک گذاشتن زمینه (Context) یا همان وضعیت (State) فراهم میکنند. 🍪
4️⃣حالا نوبت HTTP و اتصالات (HTTP and connections)
اتصال در لایه انتقال کنترل میشود و بنابراین اساساً خارج از حوزه HTTP است. HTTP نیاز ندارد که پروتکل انتقال زیرین مبتنی بر اتصال باشد؛ بلکه تنها نیازمند این است که قابل اعتماد (reliable) باشد، یا پیامها را از دست ندهد (حداقل در چنین مواردی خطا ارائه دهد).
از بین دو پروتکل انتقال رایج در اینترنت، TCP قابل اعتماد است و UDP نیست. بنابراین HTTP بر استاندارد TCP تکیه دارد که مبتنی بر اتصال است. 🌐
⚙️ بهینهسازی اتصالات
قبل از اینکه یک کلاینت و سرور بتوانند یک جفت درخواست/پاسخ HTTP را تبادل کنند، باید یک اتصال TCP ایجاد کنند، فرآیندی که نیاز به چندین رفت و برگشت (round-trips) دارد. 🐢
رفتار پیشفرض HTTP/1.0 این بود که یک اتصال TCP مجزا برای هر جفت درخواست/پاسخ HTTP باز کند. این کار در هنگام ارسال درخواستهای متعدد در فاصله زمانی کوتاه، کارایی کمتری دارد.
برای کاهش این نقص، HTTP/1.1 پایپلاینینگ (pipelining) (که پیادهسازی آن دشوار بود) و اتصالات پایدار (persistent connections) را معرفی کرد: اتصال TCP زیرین میتواند با استفاده از هدر Connection تا حدی کنترل شود.
بعد HTTP/2 یک گام فراتر رفت و با Multiplexing پیامها روی یک اتصال واحد، به گرم نگه داشتن اتصال و کارایی بیشتر کمک کرد.
آزمایشهایی برای طراحی یک پروتکل انتقال بهتر که مناسبتر برای HTTP باشد، در حال انجام است. برای مثال، Google در حال آزمایش QUIC است که بر پایه UDP ساخته شده تا یک پروتکل انتقال قابل اعتمادتر و کارآمدتر را فراهم کند.
🔁 جریان HTTP :
هنگامی که یک کلاینت میخواهد با یک سرور (چه سرور نهایی و چه یک پروکسی میانی) ارتباط برقرار کند، مراحل زیر را طی میکند: 👇
1️⃣ باز کردن اتصال TCP
اتصال TCP برای ارسال یک یا چند درخواست و دریافت پاسخ استفاده میشود. کلاینت ممکن است یک اتصال جدید باز کند، از یک اتصال موجود مجدداً استفاده کند، یا چندین اتصال TCP به سرورها باز کند. 🔗
2️⃣ ارسال پیام HTTP
پیامهای HTTP (قبل از HTTP/2) قابل خواندن توسط انسان هستند. در HTTP/2، این پیامها در Frameها کپسولهسازی میشوند که خواندن مستقیم آنها را غیرممکن میسازد، اما اصل کار یکسان باقی میماند.
📌مثال درخواست (Request):
GET / HTTP/1.1
Host: developer.mozilla.org
Accept-Language: fr
3️⃣ خواندن پاسخ سرور
پاسخ ارسال شده توسط سرور خوانده میشود، مانند:
HTTP/1.1 200 OK
Date: Sat, 09 Oct 2010 14:28:02 GMT
Server: Apache
Last-Modified: Tue, 01 Dec 2009 20:18:22 GMT
ETag: "51142bc1-7449-479b075b2891b"
Accept-Ranges: bytes
Content-Length: 29769
Content-Type: text/html
<!doctype html>… (در اینجا 29769 بایت از صفحه وب درخواستی قرار میگیرد)
4️⃣ بستن یا استفاده مجدد از اتصال
اتصال برای درخواستهای بعدی بسته یا مجدداً استفاده میشود. 🔄
⚠️ ملاحظه Pipelining
اگر پایپلاینینگ HTTP فعال باشد، چندین درخواست میتوانند بدون انتظار برای دریافت کامل اولین پاسخ ارسال شوند. با این حال، پیادهسازی پایپلاینینگ HTTP در شبکههای موجود که بخشهای قدیمی نرمافزار با نسخههای مدرن همزیستی دارند، دشوار است. پایپلاینینگ HTTP در HTTP/2 با Multiplexing قویتر درخواستها در یک Frame جایگزین شده است.
📝 پیامهای HTTP (HTTP Messages)
پیامهای HTTP، همانطور که در HTTP/1.1 و نسخههای قبلی تعریف شدهاند، قابل خواندن توسط انسان هستند. در HTTP/2، این پیامها در یک ساختار دودویی، یعنی Frame، جاسازی میشوند که امکان بهینهسازیهایی مانند فشردهسازی هدرها و Multiplexing را فراهم میکند. حتی اگر تنها بخشی از پیام اصلی HTTP در این نسخه ارسال شود، معناشناسی هر پیام بدون تغییر باقی میماند و کلاینت درخواست اصلی HTTP/1.1 را (به صورت مجازی) بازسازی میکند. بنابراین، درک پیامهای HTTP/2 در قالب HTTP/1.1 مفید است.
دو نوع پیام HTTP وجود دارد: درخواستها (Requests) و پاسخها (Responses) که هر کدام قالب خاص خود را دارند.
🔹️ درخواستها (Requests) 📤
درخواستها شامل عناصر زیر هستند:
• متد HTTP: معمولاً یک فعل مانند GET، POST یا یک اسم مانند OPTIONS یا HEAD که عملیاتی را که کلاینت میخواهد انجام دهد، تعریف میکند. به طور معمول، کلاینت میخواهد یک منبع را واکشی کند (با استفاده از GET) یا مقدار یک فرم HTML را ارسال کند (با استفاده از POST).
• مسیر منبع: URL منبع که عناصر واضح از زمینه، مانند پروتکل (http://)، دامنه (مانند developer.mozilla.org) یا پورت TCP (مانند 80) از آن حذف شده است.
• نسخه پروتکل HTTP.
• هدرها (Headers) اختیاری: که اطلاعات اضافی را برای سرورها منتقل میکنند.
• بدنه (Body): برای برخی متدها مانند POST، مشابه پاسخها، که منبع ارسال شده را شامل میشود.
🔹️ پاسخها (Responses) 📥
پاسخها شامل عناصر زیر هستند:
• نسخه پروتکل HTTP: که از آن پیروی میکنند.
• کد وضعیت (Status Code): نشان میدهد که آیا درخواست موفقیتآمیز بوده یا خیر، و چرا. 🔢
• پیام وضعیت (Status Message): یک توضیح کوتاه غیررسمی از کد وضعیت.
• هدرهای HTTP: مانند هدرهای درخواستها.
• بدنه (Body) اختیاری: که منبع واکشی شده را شامل میشود.
📝 نتیجهگیری
فهمیدیم HTTP یک پروتکل قابل توسعه (Extensible) است که استفاده از آن آسان میباشد. ساختار کلاینت-سرور، همراه با قابلیت افزودن هدرها، به HTTP اجازه میدهد تا همگام با قابلیتهای گسترده وب پیشرفت کند. 📈
اگرچه HTTP/2 با جاسازی پیامهای HTTP در Frameها برای بهبود عملکرد، مقداری پیچیدگی اضافه میکند، ساختار اصلی پیامها از زمان HTTP/1.0 یکسان باقی مانده است. جریان Session همچنان اساسی است و امکان بررسی و Debug کردن آن را با یک HTTP network monitor فراهم میکند. 🔍
🔖 هشتگها:
#HTTP #RequestResponse #Networking #CORS #WebSecurity #Session #TCP #QUIC