پرش به مطلب اصلی

MuxClient

MuxClient چند اتصال منطقی WaterWall را روی تعداد کمتری اتصال مشترک transport حمل می‌کند. به‌جای ساخت یک اتصال کامل تا سمت مقابل برای هر line ورودی، یک یا چند parent line به سمت next می‌سازد یا دوباره به کار می‌گیرد و رویدادهای هر child را در قالب فریم داخلی MUX می‌فرستد. در سمت مقابل، MuxServer این child line‌های مستقل را بازسازی می‌کند.

MuxClient خودش listener یا connector نیست؛ node قبلی child line‌ها را می‌سازد و node بعدی transport مشترک parent را فراهم می‌کند.

جایگاه رایج​

TCP ساده:

client side: TcpListener -> MuxClient -> TcpConnector
server side: TcpListener -> MuxServer -> TcpConnector

با protocol stack بیرونی:

client side: TcpListener -> MuxClient -> HttpClient -> TlsClient -> TcpConnector
server side: TcpListener -> TlsServer -> HttpServer -> MuxServer -> TcpConnector

از MUX زمانی استفاده کنید که تعداد زیادی اتصال منطقی کوتاه یا متوسط باید روی تعداد کمتری اتصال بیرونی transport عبور کنند.

این نود چه می‌کند؟​

  • line‌های child معمولی را از node قبلی می‌پذیرد.
  • parent transport line‌ها را به سمت next می‌سازد.
  • parent line‌ها را بر اساس حالت concurrency انتخاب‌شده دوباره به کار می‌گیرد.
  • به هر child یک connection id یا cid اختصاص می‌دهد.
  • payload و رویدادهای pause، resume و Finish هر child را در فریم MUX encode می‌کند.
  • فریم‌های برگشتی MuxServer را به child مناسب هدایت می‌کند.
  • اگر سمت نوشتن child محلی pause باشد، داده رسیده از parent را در صف نگه می‌دارد.
  • backpressure per-child را با FlowPause و FlowResume frame بازتاب می‌دهد.
  • parent line‌های exhausted و بی‌کار را پس از بسته شدن آخرین child و تحویل خروجی صف‌شده می‌بندد.

MuxClient object‌های parent از نوع line_t را داخل خود می‌سازد و مسئول آزاد کردن آن‌هاست. child line‌هایی را که node قبلی ساخته، آزاد نمی‌کند.

نمونه تنظیم‌ها​

حالت timer:

{
"name": "mux-client",
"type": "MuxClient",
"settings": {
"mode": "timer",
"connection-duration-ms": 30000
},
"next": "outbound-transport"
}

حالت counter:

{
"name": "mux-client",
"type": "MuxClient",
"settings": {
"mode": "counter",
"connection-capacity": 128
},
"next": "outbound-transport"
}

حالت pool ثابت parent‌ها:

{
"name": "mux-client",
"type": "MuxClient",
"settings": {
"mode": "fixed-connections-count",
"per-worker-connections-count": 2
},
"next": "outbound-transport"
}

فیلدهای اجباری​

فیلدهای سطح اصلی:

فیلدنوعتوضیح
namestringنام یکتای node در config.
typestringباید دقیقاً "MuxClient" باشد.
settingsobjectاجباری. باید mode و تنظیم ویژه همان حالت را داشته باشد.
nextstringاجباری. node بعدی parent transport line‌ها را حمل می‌کند.

فیلدهای اجباری داخل settings:

گزینهاجباری درتوضیح
modeهمیشهیکی از timer، counter یا fixed-connections-count.
connection-duration-msmode: "timer"مدت پذیرش child جدید روی هر parent، به میلی‌ثانیه. باید بزرگ‌تر از 60 باشد.
connection-capacitymode: "counter"حداکثر تعداد cid باز شده روی یک parent. باید بزرگ‌تر از 0 باشد.
per-worker-connections-countmode: "fixed-connections-count"تعداد parent line‌های ثابت برای هر worker. باید بزرگ‌تر از 0 باشد.

تنظیمات اختیاری​

گزینهنوعپیش‌فرضتوضیح
max-childreninteger10000بیشترین child live هم‌زمان روی یک parent. باید مثبت باشد و از connection-capacity تجمعی در حالت counter مستقل است.
child-buffer-limitinteger134217728سقف charge منابع sbuf_t نگه‌داشته‌شده برای یک child در حالت pause. برای ordinary هزینهٔ تخصیص و برای splice ظرفیت منطقی sbuf به‌علاوهٔ header و alignment شمرده می‌شود؛ padding یک‌بار حساب می‌شود. باید بزرگ‌تر از 0 باشد؛ رسیدن به سقف child را با فریم Close می‌بندد.
child-buffer-resume-thresholdinteger524288Low-water mark برحسب بایت منطقی payload صف‌شده برای ارسال FlowResume پس از writableشدن child محلی. باید بزرگ‌تر از 0 باشد و مقدار عددی آن به child-buffer-limit cap می‌شود. مقدار بزرگ‌تر peer را زودتر Resume می‌کند، اما Hysteresis را کاهش می‌دهد و ممکن است چرخه‌های Pause/Resume را بیشتر کند.
parent-buffer-limitinteger536870912سقف ظرفیت ورودی در حال تشکیل فریم به‌علاوهٔ صف child‌های متصل روی هر parent. ابتدا ترکیب سودمند ورودی و سپس بستن بزرگ‌ترین صف با اولویت قدیمی‌ترین child در تساوی انجام می‌شود؛ فشار حل‌نشده parent را می‌بندد. برای غیرفعال کردن سقف تجمیعی مقدار 0 را قرار دهید.
detached-buffer-limitintegerبر اساس ram-profileسقف charge منابع نگه‌داشته‌شده به‌ازای هر worker برای صف child‌های مسدودشده پس از از دست رفتن parent. رسیدن به سقف فقط child تازه detachedشده را abort می‌کند. برای غیرفعال کردن این سقف تجمیعی مقدار 0 را قرار دهید.
detached-child-limitintegerبر اساس ram-profileسقف تعداد به‌ازای هر worker برای child‌های مسدودشده‌ای که پس از از دست رفتن parent نگه داشته می‌شوند. رسیدن به سقف فقط child مسدودشده‌ای را که به‌تازگی detached شده است abort می‌کند. برای غیرفعال کردن این سقف تجمیعی مقدار 0 را قرار دهید.
log-main-line-statsbooleanfalseاگر فعال باشد، parent line‌ها هر 5000 ms آمار best-effort را در لاگ می‌نویسند. مرگ منطقی parent اجرای عادی را suppress می‌کند و هنگام رسیدن runner صف‌شده/زمان‌بندی‌شده، task با LineDead settle می‌شود. quiescence worker کار معلق را فعالانه لغو می‌کند و هیچ‌یک از این مسیرها آن را دوباره arm نمی‌کند.

اگر این دو گزینه در تنظیمات node نوشته نشوند، مقدار پیش‌فرض آن‌ها از misc.ram-profile سراسری انتخاب می‌شود:

پروفایل RAMسقف پیش‌فرض charge detachedسقف پیش‌فرض child‌های detached
S1 (minimal / ultralow)33554432 (32 MiB)4096
S280740352 (77 MiB)5677
M1 (client)127926272 (122 MiB)7258
M2 (client-larger)174063616 (166 MiB)8838
L1221249536 (211 MiB)10419
L2 (server، پیش‌فرض سراسری)268435456 (256 MiB)12000

مقادیر بین کمینه و بیشینه روی شش رده مرتب پروفایل RAM به‌صورت خطی درون‌یابی می‌شوند و سقف charge به نزدیک‌ترین MiB گرد می‌شود. نوشتن صریح هر گزینه در تنظیمات node، فقط مقدار مشتق‌شده از پروفایل برای همان گزینه را جایگزین می‌کند.

child-buffer-resume-threshold طول منطقی payload را می‌شمارد. FlowPause بر اساس وضعیت Pause محلی child ارسال می‌شود و به طول صف وابسته نیست. charge منابع برای سقف‌های سخت صف و threshold خروجی parent استفاده می‌شود؛ طول محتوا همچنان معیار صف‌های معمولی و flow control است.

آمار اختیاری هر پنج ثانیه شامل parent-output-queued-bytes، parent-output-queue-charge، parent-output-queue-items، parent-transport-paused، parent-sources-throttled، children-parent-write-paused و children-peer-flow-paused است. تعداد محلی می‌تواند غیرصفر باشد در حالی که gate تجمیعی خاموش است. parent-output-throttle-ms و parent-output-last-throttle-ms فقط مدت gate تجمیعی را با ساعت monotonic ۶۴ بیتی گزارش می‌کنند. سه threshold مؤثر نیز ثبت می‌شوند. parent-child-queue-charge و parent-input-queue-charge مربوط به ذخیره‌سازی ورودی‌اند، نه backlog خروجی.

مدل parent و child​

MuxClient دو نقش line دارد:

نقشمعنی
child lineاتصال منطقی WaterWall دریافتی از node قبلی.
parent lineاتصال transport مشترک که MuxClient به سمت next می‌سازد.

هر child یک cid می‌گیرد. مقدار cid در محدوده همان parent line محلی است و در فریم‌های MUX به کار می‌رود تا MuxServer بتواند payload و رویدادهای کنترلی را به stream منطقی درست نسبت دهد.

در حالت timer و counter، هر worker یک parent فعال و قابل استفاده دارد. child‌های جدید تا exhausted شدن parent به آن متصل می‌شوند. اگر با رسیدن child بعدی count به max-children رسیده باشد، parent از selection بازنشسته می‌شود، برای child‌های فعلی زنده می‌ماند و پس از صفر شدن تعداد child و تحویل خروجی صف بسته می‌شود؛ child جدید parent دیگری می‌گیرد.

در حالت pool ثابت، هر worker به تعداد per-worker-connections-count جایگاه قابل انتخاب دارد. ابتدا parentهای غیرمشکوک که به max-children نرسیده‌اند ترجیح داده می‌شوند؛ سپس انتخاب با هش rendezvous میان همان گروه انجام می‌شود. هر child جدید یک کلید مستقل در worker و هر parent یک شناسه ثابت در طول عمرش دارد. تعداد childها فقط سقف پذیرش است و امتیاز انتخاب نیست. اتصال childهای موجود تغییر نمی‌کند. اگر تمام گزینه‌ها مشکوک باشند، انتخاب در همان گروه ادامه دارد؛ اگر همه پر یا در حال پایان باشند، child جدید Finish می‌گیرد و صف انتظار نامحدود ساخته نمی‌شود.

Ping/Pong و جایگزینی نرم parent​

گزینهٔ boolean به نام keepalive به‌صورت پیش‌فرض true است و در همهٔ modeها اعمال می‌شود. false ارسال probe، پذیرش پاسخ و جایگزینی مبتنی بر سلامت را غیرفعال می‌کند؛ انتخاب rendezvous در حالت ثابت باقی می‌ماند. ping-interval پیش‌فرض 15000 و tolerance-ms پیش‌فرض 45000 میلی‌ثانیه دارد. هر دو باید حتی در حالت غیرفعال عدد صحیح بین 1 و 2147483647 باشند.

جدول idle محلی هر worker، probe و مهلت پاسخ parentهای قابل انتخاب و established را زمان‌بندی می‌کند، نه childها را. دقت بررسی حدود یک ثانیه است؛ probe آماده یا timeout ممکن است حدود یک ثانیه دیر اجرا شود، حتی اگر مدت تنظیم‌شده کوتاه‌تر باشد. نخستین Ping یک ping-interval پس از Est سررسید می‌شود و در اولین expiry مجاز در آن زمان یا پس از آن ارسال می‌شود؛ این انتظار parent را مشکوک نمی‌کند و مانع پذیرش child نیست. ارسال‌های بعدی فاصلهٔ تنظیم‌شده را رعایت می‌کنند. روی هر parent فقط یک probe در انتظار پاسخ است. Ping و Pong بدنه ندارند و CID آن‌ها یک token سی‌ودوبیتی است. فقط Pong کامل با همان token روی همان parent پاسخ محسوب می‌شود؛ Data، بخشی از header و token اشتباه کافی نیست.

اولین پاسخ معتبر پشتیبانی peer را تأیید می‌کند. قبل از آن، probe بی‌پاسخ دوباره امتحان می‌شود ولی parent مشکوک یا جایگزین نمی‌شود. بنابراین خاموش‌بودن پاسخ‌گویی MuxServer باعث چرخش اتصال نمی‌شود؛ البته parentی که هرگز اولین پاسخ را نمی‌دهد از این راه بازیابی نمی‌شود. در سکوت شبانه و آپلود یک‌طرفه، خود MuxServer پاسخ می‌دهد و نیازی به پاسخ برنامهٔ کاربر نیست.

تا وقتی خروجی parent Pause است، pump فعال دارد یا صفش خالی نیست، probe جدید ارسال نمی‌شود و probe آماده حدود یک ثانیه بعد دوباره امتحان می‌شود. بررسی probe و مهلت پاسخ در Pause ترنسپورت یا FlowPause child ادامه دارد. زمان با ساعت monotonic همان worker اندازه‌گیری می‌شود؛ فقط تحویل واقعی Ping به node بعدی مهلت پاسخ را آغاز می‌کند. صف‌های nodeهای بعدی، Pause محلی و انتقال شبکه از tolerance مصرف می‌کنند. tickهای ازدست‌رفته به یک probe آماده تبدیل می‌شوند، نه چند ارسال پشت سر هم. Pong از همان FIFO خروجی parent، سقف charge و gate مربوط به Pause عبور می‌کند.

پس از یک‌چهارم tolerance، با گردکردن رو به پایین و حداقل ۱ میلی‌ثانیه (پیش‌فرض 11250)، parent تأییدشده اولویت کمتری در حالت ثابت دارد. پس از کل مدت، expiry مهلت پاسخ یا درخواست child جدید می‌تواند آن را بازنشسته کند، حتی در Pause؛ جایگزین فقط هنگام درخواست child جدید ساخته می‌شود. Pong معتبر دیررس parentی را که هنوز بازنشسته نشده سالم می‌کند. parent بازنشسته دوباره انتخاب نمی‌شود، ولی childهای موجود روی آن می‌مانند و تا پایان معمول خود کار می‌کنند. پس از خروج آخرین child بسته می‌شود و خروجی نهایی مسدود را بدون انتظار برای Resume آزاد می‌کند. parent بی‌پاسخِ بدون child با پاک‌سازی معمول، حتی همراه خروجی مسدود، بسته می‌شود.

سقف parentهای بازنشستهٔ keepalive در هر worker برابر per-worker-connections-count در حالت ثابت و یک در حالت‌های timer/counter است. خروجی نهایی در انتظار تخلیه نیز در این سقف حساب می‌شود. با پرشدن سقف، parent دارای child همچنان قابل انتخاب می‌ماند تا ظرفیت آزاد شود و expiry حدود هر ثانیه بازنشستگی را دوباره امتحان می‌کند؛ parent خالی قابل جایگزینی است. چرخش معمول timer/counter مستقل باقی می‌ماند و عمر parent با ساعت monotonic worker مالک سنجیده می‌شود. callbackهای جدول idle probe و expiry را اجرا می‌کنند؛ جایگزینی هنگام درخواست child جدید انجام می‌شود و هیچ child فعالی به‌اجبار قطع نمی‌شود. timeoutهای آداپتور و peer مستقل‌اند.

رفتار modeها​

modeparent چه زمانی child جدید نمی‌پذیردمناسب برای
timerوقتی عمر parent از connection-duration-ms بیشتر شودچرخش parent transport line‌ها در طول زمان.
counterوقتی cid به connection-capacity برسدcap ساده روی stream‌های منطقی به‌ازای هر parent.
fixed-connections-countعمر اتصال و counter تجمعی باعث چرخش نمی‌شوند؛ سقف child و سیاست Ping/Pong اعمال می‌شوندpool قابل انتخاب با جایگزینی نرم محدود.

parentِ exhausted بی‌درنگ بسته نمی‌شود؛ فقط child تازه نمی‌پذیرد و child‌های موجود تا پایان ادامه می‌دهند. اگر چنین parent‌ای child نداشته باشد، تا تحویل آخرین خروجی و پایان ارسال فعال در فهرست مالکیت می‌ماند، ولی دیگر برای child تازه انتخاب نمی‌شود. در حالت fixed، فقط parent بازنشسته‌نشده قابل انتخاب مجدد است. worker Stop همه parent‌های تحت مالکیت، حتی parent بازنشسته بدون child، را بدون انتظار برای Resume می‌بندد و خروجی باقی‌مانده را آزاد می‌کند.

همچنین یک hard limit مطلق برای cid وجود دارد: وقتی parent به 4294967295 برسد، exhausted می‌شود.

dispatch پاسخ child با hash index و به‌طور متوسط O(1) است. اگر MuxServer یک Open جدید را با Close(cid) رد کند، فقط همان child از نوع borrowed Finish می‌شود و parent و siblingهای دیگر ادامه می‌دهند.

باز شدن Stream​

کد فعلی یک child را در upstream Init attach می‌کند:

  1. یک parent line موجود یا تازه روی همان worker انتخاب می‌کند.
  2. state مربوط به MUX را برای child مقداردهی اولیه می‌کند.
  3. child را به parent link می‌کند.
  4. cid بعدی را اختصاص می‌دهد.
  5. downstream Est را به node قبلی برای child گزارش می‌دهد.

فریم Open هنگام Init فرستاده نمی‌شود. با رسیدن نخستین payloadِ upstream، فریم‌های Open و Data در یک buffer ارسال می‌شوند. اگر child پیش از فرستادن payload finish شود، MuxClient ابتدا Open و سپس Close می‌فرستد تا peer یک توالی کامل open/close ببیند.

فرمت frame​

MuxClient و MuxServer از header هشت‌بایتی مشترک استفاده می‌کنند:

offsetاندازهمعنی
0۳ بایتطول بدون علامت payload، به‌ترتیب big-endian.
3۱ بایتنوع فریم / flags.
4۴ بایتشناسهٔ بدون علامت اتصال، به‌ترتیب big-endian.

سقف قطعی و شامل payload برای همهٔ انواع فریم، ۱ MiB یعنی 1,048,576 بایت است؛ این مقدار به پروفایل RAM، اندازهٔ buffer یا ظرفیت درخواستی و واقعی pipe وابسته نیست. فریم حداکثری با header برابر 1,048,584 بایت است و Open پیش از آن، مجموع را به 1,048,592 می‌رساند. پس از دریافت هر هشت بایت header، طول بزرگ‌تر بی‌درنگ رد می‌شود؛ حتی برای CID یا نوع ناشناخته. فقط همان parent با مسیر عادی parent-loss بسته می‌شود. پس از header معتبر Data، هر بخش موجود از بدنه به‌ترتیب تحویل می‌شود و decoder منتظر کامل‌شدن فریم نمی‌ماند. بدنهٔ Data برای CID ناشناخته یا بسته‌شده نیز تدریجی دور ریخته می‌شود. Data خالی بدون callback به child مصرف می‌شود. بدنهٔ سایر فریم‌ها نیز تدریجی مصرف می‌شود، اما اثر کنترل فقط پس از پایان بدنه اعمال می‌شود. بایت‌های موجود بدنه و کنترل‌های کامل پیش از بررسی سقف 1,048,584 بایت برای ورودی ناقص باقی‌مانده پردازش می‌شوند. headerِ cache‌شده در این سقف حساب می‌شود، اما بایت‌های هنوز دریافت‌نشده حساب نمی‌شوند.

flag‌های frame:

flagمعنی
0Open
1Close
2FlowPause
3FlowResume
4Data
5Ping؛ بدون بدنه، CID برابر token probe
6Pong؛ بدون بدنه، CID برابر همان token پاسخ

1,048,576 بایت سقف payload در یک فریم Data است، نه سقف یک callback از نوع Payload برای child. encoder یک payload بزرگ‌تر را، با حفظ ترتیب دقیق بایت‌ها، به چند فریم پیاپی Data با حداکثر 1,048,576 بایت تقسیم می‌کند.

اندازهٔ تجمیعی encode‌شده ــ شامل payload، header هشت‌بایتی هر فریم Data و فریم اختیاری Open ــ به‌صورت checked محاسبه می‌شود و باید در uint32_t جا شود. اگر جا نشود، bufferِ ordinary ورودی recycle می‌شود و فقط همان child بسته می‌شود؛ هیچ فریم ناقصی منتشر نمی‌شود. تخصیص buffer تجمیعی نیز از قرارداد fail-fast مربوط به shift buffer پیروی می‌کند، بنابراین شکست واقعی allocator باعث خاتمه می‌شود و هرگز دادهٔ ناقص forward نخواهد شد. batch بزرگ splice به‌جای این مسیر، سقف کامل batch و بستن parent مربوط را در صورت رد پذیرش به کار می‌برد؛ جزئیات در بخش نگه‌داری splice آمده است.

جهت و رفتار Callback​

callback ورودیرفتار
upstream Init childیک parent انتخاب یا می‌سازد، child را link می‌کند، cid اختصاص می‌دهد، Est را به node قبلی برای child گزارش می‌دهد.
upstream Payload childبرای اولین payload Open + Data prepend می‌کند، در غیر این صورت Data prepend می‌کند، سپس به parent به سمت next می‌فرستد.
upstream Pause / Resume childFlowPause می‌فرستد / داده queue شده را flush می‌کند و در صورت نیاز FlowResume می‌فرستد.
upstream Finish childClose می‌فرستد، یا Open + Close اگر هنوز payload stream را باز نکرده؛ در صورت نیاز parent exhausted بیکار را می‌بندد.
downstream Payload parentفریم‌های MUX از MuxServer را تجزیه می‌کند و Data، Close، FlowPause و FlowResume را به child مناسب می‌فرستد.
downstream Pause / Resume parentخروجی parent را بی‌درنگ متوقف می‌کند / FIFO را تخلیه می‌کند؛ نویسندهٔ مسدود به‌صورت محلی و همهٔ child‌ها در آستانهٔ تجمیعی محدود می‌شوند.
downstream Finish parentstate و line تحت مالکیت parent را بی‌درنگ از بین می‌برد، صف‌های پذیرفته‌شده child‌ها را به drain‌های مستقل از parent منتقل می‌کند و هر child را پس از خالی و قابل‌نوشتن شدن صف آن finish می‌کند.
upstream Estغیرفعال؛ رسیدن به این callback fatal است.
downstream Initغیرفعال؛ رسیدن به این callback fatal است.

فریم‌های ناشناخته، فریم‌های مربوط به cid ناموجود و فریم Open ناخواسته از سمت سرور دور ریخته می‌شوند.

صف خروجی parent​

هر parent یک FIFO مستقل و با تخصیص در اولین استفاده برای خروجی encode‌شده دارد. تنظیم‌های اختیاری زیر برحسب ظرفیت sbuf به‌علاوهٔ header buffer و alignment هستند. ظرفیت ordinary فیزیکی و ظرفیت splice منطقی است؛ padding یک‌بار شمرده می‌شود. ظرفیت pipe به آن اضافه نمی‌شود و این معیار، اندازه‌گیری حافظهٔ kernel یا RSS نیست.

تنظیمپیش‌فرضمعنی
parent-write-buffer-pause-threshold134217728 (128 MiB)با رسیدن charge صف به این مقدار، تولیدکننده‌های child متصل Pause می‌شوند.
parent-write-buffer-resume-threshold4194304 (4 MiB)آزادسازی نگه‌داری محلی در charge کمتر یا مساوی این مقدار، وقتی transport قابل‌نوشتن است.
parent-write-buffer-limit536870912 (512 MiB)سقف سخت charge صف هر parent؛ برابری با سقف مجاز است.

pause و hard باید عدد صحیح در [1, INT_MAX] و resume در [0, INT_MAX] باشند. شرط نهایی 0 <= resume < pause < hard است. پیش‌فرض resume مستقل از pause برابر 4194304 (4 MiB) است. مقدار صریح تنظیم‌شده اعمال می‌شود؛ 0 برای آزادسازی به صف خروجی خالی نیاز دارد. در همهٔ حالت‌ها transport باید قابل‌نوشتن باشد. مقدار نامعتبر clamp نمی‌شود. نمونه زیر resume برابر 1.5 MiB را صریحاً تنظیم می‌کند:

{
"parent-write-buffer-pause-threshold": 2097152,
"parent-write-buffer-resume-threshold": 1572864,
"parent-write-buffer-limit": 4194304
}

این بودجهٔ خروجی برای هر parent جداست. parent-buffer-limit مجموع ورودی در حال تشکیل فریم و صف child‌های متصل را محدود می‌کند.

با دریافت Pause از transport parent، هیچ payload تازه‌ای، حتی فریم کنترلی یا Close، به آن سمت فرستاده نمی‌شود. اگر child هنگام مسدود بودن transport داده بفرستد، پس از پذیرش کامل payload یا batch فقط تولیدکنندهٔ همان child Pause می‌شود. این قاعده برای Data خالی و ordinary و splice یکسان است. نوشتن مستقیم نیز اگر transport را در حالت Pause باقی بگذارد، پیش از بازگشت، نویسنده را متوقف می‌کند. Pause و Resume موقت بر اساس وضعیت نهایی هماهنگ می‌شوند. ارسال کنترل به‌تنهایی child را نویسنده محسوب نمی‌کند؛ child بیکار زیر آستانهٔ تجمیعی تا نخستین نوشتن خود متوقف نمی‌شود.

با رسیدن charge به parent-write-buffer-pause-threshold، محدودیت تجمیعی همچنان همهٔ child‌های باز و متصل را شامل می‌شود. نگه‌داری محلی نویسنده به‌تنهایی این gate را فعال نمی‌کند. هر child تنها یک دلیل محلی و یک جایگاه در FIFO محلی parent دارد؛ علت تکراری جایگاه را عوض نمی‌کند. نویسنده‌ای که پس از Resume دوباره متوقف شود به انتهای صف می‌رود.

آزادسازی این نگه‌داری‌ها به زنده بودن state parent، نبود Finish یا quiescence، قابل‌نوشتن بودن transport و charge کمتر یا مساوی parent-write-buffer-resume-threshold وابسته است؛ لازم نیست gate تجمیعی قبلاً فعال شده باشد. همین شرط latch تجمیعی را هم پاک می‌کند. پس از هر callback و پیش از آزادسازی بعدی یا برداشتن buffer، وضعیت دوباره بررسی می‌شود. Pause جدید، عبور از آستانهٔ Resume، فعال‌شدن دوبارهٔ gate یا نابودی parent آزادسازی را قطع می‌کند. تخلیهٔ FIFO فرصت بعدی را فراهم می‌کند؛ صف خالی و قابل‌نوشتن نباید منتظر رویدادی بماند که دیگر رخ نمی‌دهد. FlowPause همتا و حالت بسته‌شدن مستقل‌اند.

پیش از اولین callback، snapshot از همهٔ line‌های منتظر reference موقت می‌گیرد؛ callback ممکن است sibling یا parent را نابود کند. عضویت و زنده‌بودن state پیش از استفاده دوباره بررسی می‌شود. Init/Est دلیل Pause را جدا از مجوز ارسال‌شده ثبت می‌کند تا Pause گم‌شده یا Resume بی‌جفت رخ ندهد. unlink و بسته‌شدن، عضویت را پیش از نابودی state حذف می‌کنند و صرفاً برای پاک‌سازی Resume نمی‌فرستند. خروجی بازگشتی پشت Data، کنترل و باقی‌ماندهٔ batch پذیرفته‌شده می‌ماند.

اگر charge buffer تازه از سقف عبور کند یا رزرو صف شکست بخورد، فقط همان parent از مسیر معمول teardown بسته می‌شود؛ buffer‌ها آزاد می‌شوند و فرایند متوقف نمی‌شود. مسیر مستقیمِ قابل‌نوشتن با صف خالی، تخصیص صف ندارد و مشمول سقف نگه‌داری نیست. فریم‌های کنترلی کوچک از رده مناسب pool با padding chain استفاده می‌کنند. با از دست رفتن parent، خروجی غیرقابل‌ارسال بی‌درنگ آزاد می‌شود؛ drain مستقل صف‌های ورودی child طبق قرارداد قبلی ادامه دارد. Open و وضعیت flow-control پیش از callback بازگشتی، پس از پذیرش در مسیر خروجی مرتب، منتشر می‌شوند.

Backpressure و صف‌ها​

MUX backpressure را per-child نگه می‌دارد:

  • اگر child محلی pause شود، MuxClient برای همان cid یک FlowPause می‌فرستد.
  • اگر FlowPause از peer برسد، خواندن از child محلی pause می‌شود.
  • اگر برای child‌ای که pause است داده‌ای از parent برسد، داده روی همان child در صف می‌ماند.
  • FlowPause همان لحظه‌ای ارسال می‌شود که callback محلی child را Pause می‌کند و منتظر رسیدن طول صف به آستانه‌ای نمی‌ماند. اگر Pause پیش از Open رخ دهد، پس از پذیرش Open و تمام اولین payload، از جمله batch بزرگ splice، وضعیت child دوباره بررسی می‌شود؛ فقط اگر هنوز متصل و Pause باشد، FlowPause ارسال می‌شود. Resume یا بسته‌شدن child در این فاصله مانع ارسال کنترل کهنه می‌شود.
  • وقتی بایت منطقی payload صف‌شده پایین‌تر از child-buffer-resume-threshold برسد، FlowResume می‌تواند ارسال شود. مقدار تنظیم‌شده به child-buffer-limit cap می‌شود.
  • اگر charge صف یک child به child-buffer-limit برسد، آن child بسته می‌شود.
  • سقف parent-buffer-limit مجموع ورودی در حال تشکیل فریم و صف child‌های متصل را می‌سنجد. ترکیب سودمند ورودی مقدم است؛ سپس بزرگ‌ترین صف با اولویت قدیمی‌ترین child در تساوی بسته می‌شود. چند قربانی ممکن است لازم باشد و فشار حل‌نشده همان parent را می‌بندد.

فریم Data با طول صفر معتبر است، اما بدون callback یا ورودی صف child مصرف می‌شود و activity را زیاد نمی‌کند. charge صف یک budget proxy برای منابع صف‌های live است، نه حافظهٔ دقیق kernel یا RSS؛ cacheهای allocator، حافظه حلقه pointer‌ها و baseline ثابت pool در charge یک صف مشخص نیستند.

به این ترتیب یک child کُند می‌تواند stream خود را متوقف کند، بی‌آنکه همه child‌های دیگر روی همان parent فوراً متوقف شوند.

فریم Close رسیده از peer پس از همه فریم‌های Data قبلی همان cid اعمال می‌شود. اگر مقصد محلی child در حالت pause باشد، MuxClient بایت‌های قبلی را نگه می‌دارد و منتظر Resume می‌ماند؛ هیچ Payloadی را برخلاف Pause اجباری ارسال نمی‌کند و Finish محلی را فقط پس از خالی و قابل‌نوشتن شدن صف می‌فرستد. فریم‌های بعدی برای آن cid بسته‌شده دور ریخته می‌شوند و child‌های نامرتبط روی همان parent به کار عادی ادامه می‌دهند.

اگر parent transport از دست برود، parent line همچنان بی‌درنگ بسته می‌شود. صف‌هایی که پیش‌تر برای child‌ها پذیرفته شده‌اند به drain‌هایی مستقل از parent تبدیل می‌شوند: child‌های قابل‌نوشتن فوراً کامل می‌شوند و child‌های pause‌شده پس از Resume ادامه می‌دهند، بدون آنکه parent مرده را نگه دارند یا به آن دسترسی پیدا کنند. داده خروجی تازه از child در این حالت رد می‌شود. detached-buffer-limit مجموع charge منابع نگه‌داشته‌شده و detached-child-limit تعداد child باقی‌مانده را به‌ازای هر worker محدود می‌کند. MuxClient مالک child line‌ها نیست؛ بنابراین owner واقعی source همچنان مسئول فهرست‌کردن و finish کردن آن‌ها هنگام shutdown است.

Buffer و Padding​

MuxClient MUX header‌ها را به payload خروجی parent prepend می‌کند. metadata node:

required_padding_left = 16
layer_group = kNodeLayer4

این 16 بایت لازم است، زیرا نخستین payloadِ child ممکن است دو header داشته باشد: یکی برای Open و دیگری برای Data. داده‌ها و فریم‌های کنترلی بعدی تنها یک header هشت‌بایتی دارند.

ورودی parent فقط برای تکمیل header یا رسیدن بایت‌های بیشتر بدنه منتظر می‌ماند؛ بخش موجود Data بی‌درنگ به child یا صف محدود آن می‌رود و بدنهٔ سایر فریم‌ها بدون تحویل مصرف می‌شود. اگر ورودی ناقص باقی‌مانده پس از پردازش بخش‌های موجود Data و کنترل‌های کامل، با احتساب header، از 1,048,584 بایت بزرگ‌تر شود، MuxClient باقی‌مانده ناقص parent را دور می‌ریزد، parent line را می‌بندد و همان رفتار detached drain را برای صف child‌هایی که پیش‌تر parse شده‌اند اعمال می‌کند.

نکته‌های عملی​

  • MuxClient باید با MuxServer جفت شود.
  • mode در implementation فعلی اجباری است.
  • connection-duration-ms فقط برای timer mode اعمال می‌شود.
  • connection-capacity فقط برای counter mode اعمال می‌شود.
  • per-worker-connections-count فقط برای fixed parent pool mode اعمال می‌شود.
  • با چند worker، اندازه pool ثابت parent‌ها نیز با تعداد worker‌ها بالا می‌رود. برای مثال، per-worker-connections-count: 2 با چهار worker هشت جایگاه قابل انتخاب و حداکثر هشت parent بازنشسته زنده دارد.
  • Finish محلی child یا shutdown منظم برنامه ممکن است صف detached باقی‌مانده را به‌جای forward کردن آزاد کند.
  • detached drain هیچ بایتی از پروتکل MUX یا نیازمندی قابلیت peer را تغییر نمی‌دهد.
  • MuxClient یک تونل میانی است، نه endpoint عمومی.

خاموش‌سازی worker​

هنگام quiescence، MuxClient جدول keepalive محلی worker را متوقف می‌کند و پاک‌سازی پایانی صف‌های باقی‌مانده را بدون ارسال payload، فریم‌های Open/Close یا پیام‌های کنترل جریان آزاد می‌کند. هر worker تمام parent‌هایی را که ساخته است، مستقل از انتخاب اتصال، نگه می‌دارد؛ این فهرست اتصال‌های در حال برقراری، بیکار، فعال و بازنشسته از انتخاب را هم شامل می‌شود. در worker stop همهٔ این parent‌ها بسته، itemهای keepalive حذف و جدول خالی روی همان worker نابود می‌شود؛ Finish برای child‌های متصل به سمت مالک مبدأ ارسال می‌شود؛ فقط مالک واقعی child line از نوع borrowed را نابود می‌کند. child‌های از قبل جداشده می‌توانند تا Finish مالک مبدأ پس از اجرای hook زنده بمانند و وضعیت و حساب دقیق حافظهٔ صف آن‌ها حفظ می‌شود. هنگام نابودی نهایی نمونه، فهرست مالکیت و حساب اتصال‌های جداشده باید خالی باشند. تخلیهٔ عادی در قطع اتصال و Close دریافتی همچنان از backpressure پیروی می‌کند.

جریان بایت و UDP​

هر child یک جریان مرتب بایت است؛ مرز callback از نوع Payload و مرز فریم Data جزو قرارداد child نیستند. یک فریم می‌تواند با رسیدن بخش‌های بدنه چند callback غیرخالی ایجاد کند. برای مثال، فریم با طول اعلام‌شدهٔ ۶۴ KiB می‌تواند نخستین ۴ KiB موجود را بدون انتظار برای ۶۰ KiB دیگر تحویل دهد. فریم‌های بعدی روی همان parent همچنان پشت باقی‌ماندهٔ بدنهٔ فعلی قرار دارند.

دریافت parent فقط header هشت‌بایتی را cache می‌کند و طول باقی‌ماندهٔ بدنهٔ Data را نگه می‌دارد. هر بخش موجود با مالکیت مستقل استخراج می‌شود و header بعدی فقط پس از مصرف کامل بدنهٔ فعلی خوانده می‌شود. child در حالت Pause این بخش‌ها را به‌ترتیب FIFO و زیر سقف‌های فعلی صف نگه می‌دارد. اگر child هنگام تحویل بسته شود، باقی‌ماندهٔ بدنه بدون تحویل دور ریخته می‌شود تا header بعدی قابل پردازش باشد. قطع parent بایت‌های قبلاً تحویل‌شده را پس نمی‌گیرد. بخش‌های پذیرفته‌شده در صف نیز همان detached drain را دارند، حتی اگر انتهای فریم هرگز نرسیده باشد.

payload با طول صفر همچنان به یک header از نوع Data و بدون بدنه encode می‌شود. گیرنده آن را بدون callback به child و بدون ورودی صف مصرف می‌کند. این فریم نه EOF است و نه Close و activity مهلت idle را تازه نمی‌کند.

برای UDP، در سمت childهای MUX از framing صریح UdpOverTcp استفاده کنید:

client: UdpListener -> UdpOverTcpClient -> MuxClient -> TcpConnector
server: TcpListener -> MuxServer -> UdpOverTcpServer -> UdpConnector

آداپتور UDP که مستقیم کنار MUX قرار دارد نمی‌تواند به حفظ مرز datagram تکیه کند. جفت UdpOverTcp فعلاً datagram غیرخالی می‌خواهد؛ جریان MUX نمایش مستقلی برای datagram خالی فراهم نمی‌کند.

نگه‌داری splice و بودجهٔ ظرفیت صف​

جدول یکپارچهٔ محدودیت‌ها، روند تخصیص و حرکت داده، و حالت‌های مرزی را در راهنمای توسعه (انگلیسی) ببینید.

هزینهٔ هر buffer از فرمول مشترک زیر محاسبه می‌شود:

sizeof(sbuf_t) + sbufGetTotalCapacity(buf) + kSbufAllocationAlignment

ظرفیت شامل padding است و دوباره اضافه نمی‌شود. برای ordinary این همان هزینهٔ تخصیص قبلی است؛ برای splice ظرفیت منطقی sbuf شمرده می‌شود، بدون افزودن حافظهٔ کنترل wrapper یا ظرفیت kernel pipe. نامعلوم بودن ظرفیت cache‌شدهٔ pipe، یک منبع معتبر را به ordinary تبدیل نمی‌کند.

parent-buffer-limit با پیش‌فرض ۵۱۲ MiB، مجموع ورودی در حال تشکیل فریم و صف‌های child متصل را محدود می‌کند. شمارندهٔ stream از pending_child_queue_charge جدا می‌ماند و مجموع با بررسی سرریز محاسبه می‌شود. بخش‌های موجود Data و کنترل‌های کامل ابتدا پردازش می‌شوند؛ عبور موقت همان delivery از سقف باعث رد آن نمی‌شود. یک فریم Data می‌تواند برای child در حالت Pause چند ورودی صف با charge مستقل بسازد. headerِ cache‌شده تا پایان بدنه در طول منطقی و سقف مستقل 1,048,584 بایت حساب می‌شود، اما sbuf charge ندارد. بایت‌های هنوز دریافت‌نشده charge نگه‌داری ندارند. مصرف بدنهٔ headِ splice ظرفیت منطقی و هزینه‌اش را کم می‌کند؛ جابه‌جایی cursorِ ordinary یا مصرف prefix چنین کاهشی ندارد.

با رسیدن به سقف دریافت، ابتدا فقط در صورت کاهش قطعی هزینه، fragment‌های ورودی در یک buffer ordinary با اندازهٔ best-fit ترکیب می‌شوند. header و ترتیب بایت‌ها حفظ می‌شوند. این کار روی هر Push اجرا نمی‌شود و صف child یا خروجی را بازنویسی نمی‌کند. اگر فشار باقی بماند، child با بزرگ‌ترین صف بسته می‌شود؛ در تساوی، قدیمی‌ترین child اولویت دارد. پس از هر callback وضعیت و هزینه دوباره بررسی می‌شوند و ممکن است چند child بسته شوند. نبود قربانی یا نبود پیشرفت، فقط همان parent را می‌بندد. مقدار 0 سقف تجمیعی دریافت را غیرفعال می‌کند و سقف پنهان دیگری جای آن نمی‌گیرد.

سقف child همچنان ۱۲۸ MiB است و برابری رد می‌شود. خروجی parent بودجه‌ای جدا با آستانهٔ پیش‌فرض Pause برابر ۱۲۸ MiB، آستانهٔ پیش‌فرض Resume برابر ۴ MiB و سقف ۵۱۲ MiB دارد؛ برابری سقف خروجی مجاز است. بودجه‌های detached روی هر worker و معنی صفر/نامحدود آن‌ها حفظ می‌شوند. آستانهٔ FlowResume طول منطقی payload را می‌شمارد؛ FlowPause تابع وضعیت Pause محلی child است. برای ordinaryِ child انتخاب ردهٔ pooled ارزان‌تر باقی است؛ spliceها برای فشار صف تک‌تک materialize نمی‌شوند.

هیچ سهمیهٔ مشترک تعداد یا ظرفیت pipe روی worker وجود ندارد. نگه‌داری یک parent باعث fallback parent دیگر نمی‌شود. این هزینه سقف FD، ظرفیت kernel، ارجاع صفحه، صف socket یا RSS نیست و سقف لحظه‌ای حافظه نیز محسوب نمی‌شود. splice بیشتر می‌تواند با همان بودجهٔ منطقی از descriptor و ظرفیت kernel بیشتری استفاده کند. recycle ممکن است pipe خالی را در pool نگه دارد؛ کاهش charge به معنی بسته‌شدن واقعی pipe نیست.

ورودی تا ۱ MiB در مسیر تک‌buffer‌ای می‌ماند؛ ورودی خالی یک Data دارد. ordinary بزرگ‌تر یک aggregate می‌سازد و splice بزرگ‌تر یک batch کامل و مرتب، با Open فقط یک‌بار پیش از نخستین Dataِ client. بدنهٔ ۱ MiB به‌علاوهٔ prefix ممکن است نیازمند تقسیم باشد. تخمین هزینهٔ pipe مقصد از ظرفیت منطقی پس از تکمیل بدنه است. هزینهٔ fallback ordinary برای فریم‌های باقی‌مانده رزرو می‌شود. تمام پذیرش‌های قابل شکست پیش از انتشار batch و callback انجام می‌شوند؛ رد پذیرش همهٔ batch را آزاد و parent مربوط را می‌بندد. مسیر مستقیم تک‌buffer‌ایِ parent قابل‌نوشتن به پذیرش FIFO نیاز ندارد.

ساخت ناموفق pipe، ظرفیت ناکافی، کمبود slot، انتقال کوتاه و EINTR همچنان با fallback کامل ordinary مدیریت می‌شوند؛ ظرفیت اسمی ۱ MiB تضمین slot کافی نیست. headroom و همهٔ بایت‌ها حتی پس از پیشرفت جزئی حفظ می‌شوند. اندازهٔ pool/pipe و پیش‌فرض‌های JSON و سقف پروتکل تغییر نمی‌کنند. Pause پمپ را متوقف می‌کند؛ Resume، ورودی بازگشتی و Close ترتیب FIFO را حفظ می‌کنند. مرگ child خروجی متعلق به parent را آزاد نمی‌کند. parent-loss ورودی و خروجی را آزاد و صف‌های مجاز child را به حساب detached منتقل می‌کند. هر pop و discard پیش از callback هزینهٔ مالک را تسویه می‌کند. BufferStream و HTTP فقط ordinary می‌پذیرند.

متادیتای نود​

metadata برگرفته از کد منبع:

ویژگیمقدار
node flagskNodeFlagSupportsSplice
can_have_prevtrue
can_have_nexttrue
layer_groupkNodeLayer4
layer_group_prev_nodekNodeLayer4
layer_group_next_nodekNodeLayer4
required_padding_left16 bytes