پست

High Performance Browser Networking

HTTP

High Performance Browser Networking

توضیحات

کتابی تخصصی از ایلیا گریگوریک، مهندس Web Performance در گوگل، که در سال ۲۰۱۳ توسط O’Reilly منتشر شد و نسخه آنلاین آن به‌صورت رایگان در hpbn.co در دسترس است. کتاب یک پوشش end-to-end از شبکه ارائه می‌دهد: از لایه فیزیکی شبکه‌های بی‌سیم (WiFi، 3G/4G) تا پروتکل‌های انتقال (TCP، UDP، TLS) و پروتکل‌های لایه اپلیکیشن (HTTP/1.1، HTTP/2)، و در نهایت APIهای مرورگر (XHR، WebSocket، WebRTC، SSE). رویکرد کتاب کاملاً عملی است - هر فصل با «چه عددی باید بهینه شود» شروع می‌کند.

نظر

  • امتیاز : 08/10
  • به دیگران توصیه می‌کنم : بله
  • دوباره می‌خوانم : بله
  • ایده برجسته : «Latency مهم‌تر از Bandwidth است» - در شبکه‌های مدرن، پهنای باند کافی است؛ مشکل اصلی تأخیر است. TCP Slow Start، DNS Lookup، TLS Handshake و Head-of-Line Blocking در HTTP/1.1 روی هم جمع می‌شوند و صفحه را کُند می‌کنند - نه «کمبود سرعت اینترنت». HTTP/2 با Multiplexing همین مشکل را هدف گرفت.
  • تاثیر در من : در پروژه‌های .NET Core و Nuxt، حالا می‌دانم چرا Connection Pooling، Keep-Alive و HTTP/2 اهمیت دارند - نه از روی حفظ best practice، بلکه با درک اینکه هر Request چند RTT اضافه دارد. نگاهم به تنظیم Kestrel و NGINX هم با این کتاب عمیق‌تر شد.
  • نکات مثبت : تنها کتابی است که کل stack شبکه از Physical Layer تا Browser API را یک‌جا پوشش می‌دهد؛ نوشتار روان و مثال‌های کمّی با اعداد واقعی؛ نسخه آنلاین رایگان و به‌روز در hpbn.co
  • نکات منفی : نسخه چاپی از سال ۲۰۱۳ به‌روز نشده و بخش HTTP/2 Server Push منسوخ شده است - Chrome آن را غیرفعال کرده؛ HTTP/3 و QUIC را پوشش نمی‌دهد؛ بخش WebRTC برای استفاده عملی امروز به منابع تکمیلی نیاز دارد

مشخصات

  • نویسنده : ایلیا گریگوریک (Ilya Grigorik)
  • انتشارات : oreilly
  • صفحه مشخصات :

بخش‌هایی از کتاب

بخش اول - فصل ۱: مقدمه‌ای بر Latency و Bandwidth

✦ چرا سرعت یک «Feature» است؟

نویسنده، Ilya Grigorik (مهندس ارشد عملکرد در Google)، کتاب را با یک ادعای جسورانه آغاز می‌کند: سرعت صرفاً یک «امتیاز جانبی» نیست؛ بلکه خودش یک Feature است - و برای برخی اپلیکیشن‌ها، THE Feature.

این ادعا بر پایه داده‌های تجربی است، نه حدس و گمان:

  • سایت‌های سریع‌تر → تعامل بهتر کاربر (User Engagement بالاتر)
  • سایت‌های سریع‌تر → نرخ ماندگاری بالاتر (User Retention)
  • سایت‌های سریع‌تر → نرخ تبدیل بالاتر (Conversion Rate)

این یعنی بحث Performance مستقیماً به درآمد کسب‌وکار گره خورده است. اگر اپلیکیشن شما کند است، کاربر می‌رود.

✦ دو شاخص بنیادین: Latency و Bandwidth

نویسنده در همان ابتدا دو مفهوم کلیدی را تعریف می‌کند که ستون فقرات تمام فصل‌های بعدی هستند:

مفهومتعریف دقیق
Latencyزمان لازم برای رسیدن یک Packet از مبدأ به مقصد
Bandwidthحداکثر Throughput یک مسیر ارتباطی منطقی یا فیزیکی

این دو مفهوم به‌هم وابسته‌اند اما یکی نیستند. درک این تمایز، پیش‌نیاز همه بهینه‌سازی‌های شبکه است.

✦ اجزای تشکیل‌دهنده Latency

اشتباه رایج توسعه‌دهندگان این است که Latency را یک عدد واحد می‌بینند. نویسنده آن را به چهار جزء مستقل تجزیه می‌کند:

  • Propagation Delay: زمان فیزیکی حرکت سیگنال در فضا - تابع فاصله و سرعت انتشار سیگنال در محیط
  • Transmission Delay: زمان لازم برای «روی سیم گذاشتن» تمام بیت‌های یک Packet - تابع اندازه Packet و نرخ انتقال لینک
  • Processing Delay: زمان پردازش Header توسط Router، بررسی خطاهای بیتی و تعیین مسیر
  • Queuing Delay: زمان انتظار Packet در صف ورودی Router تا نوبت پردازشش برسد

جمع این چهار عدد = کل Latency بین Client و Server.

✦ سرعت نور: سقف غیرقابل عبور

اینجا نویسنده یک محدودیت بنیادین را مطرح می‌کند که هیچ مهندسی نمی‌تواند از آن فرار کند:

سرعت نور در خلاء 299,792,458 متر بر ثانیه است - اما Packet‌های ما از خلاء عبور نمی‌کنند.

در کابل‌های فیبر نوری، ضریب شکست نور حدود 1.4 تا 1.6 است. یعنی سرعت واقعی انتشار حدود 200,000,000 متر بر ثانیه می‌شود - ۳۳٪ کندتر از خلاء.

نتیجه عملی این محدودیت:

مسیرRTT در فیبر
نیویورک → سانفرانسیسکو~42 ms
نیویورک → لندن~56 ms
نیویورک → سیدنی~160 ms

و این اعداد خوش‌بینانه‌ترین حالت ممکن هستند - مسیر مستقیم بدون هیچ Router میانی.

✦ نقطه حساس: Last-Mile Latency

یکی از جالب‌ترین بینش‌های این فصل آن است که بدترین Latency نه در عبور از اقیانوس‌ها، بلکه در چند کیلومتر آخر رخ می‌دهد:

بر اساس گزارش FCC (2013) در ساعات اوج مصرف:

  • Fiber-to-home: میانگین 18 ms
  • Cable: میانگین 26 ms
  • DSL: میانگین 44 ms

این 18 تا 44 میلی‌ثانیه فقط برای رسیدن به نزدیک‌ترین Router کدام ISP است - قبل از اینکه Packet حتی یک قدم به سمت مقصد نهایی بردارد.

✦ Bandwidth در لبه شبکه

در حالی که Backbone اینترنت (فیبر زیردریایی) قادر به انتقال صدها Terabit در ثانیه است، کاربر نهایی چه می‌بیند؟ بر اساس داده‌های Akamai در Q1 2013:

  • میانگین جهانی: 3.1 Mbps
  • برترین کشور (کره جنوبی): 14.2 Mbps
  • ایالات متحده (رتبه 9): 8.6 Mbps

نویسنده یک نکته مهم را تأکید می‌کند: Bandwidth بیشتر لزوماً = عملکرد بهتر نیست. Latency اغلب گلوگاه واقعی است، نه Bandwidth. این ادعا را در فصل‌های بعدی (TCP و HTTP) به‌طور مفصل اثبات می‌کند.

✦ راه‌حل چیست؟

نویسنده فصل را با یک نتیجه‌گیری صادقانه پایان می‌دهد:

  • بهبود Bandwidth: ممکن است - فیبر بیشتر، WDM بهتر
  • بهبود Latency: محدود است - فیزیک قوانین خودش را دارد، حداکثر 30٪ بهبود ممکن است

پس راه‌حل عملی چیست؟

به جای اینکه نور را سریع‌تر کنیم، فاصله را کوتاه‌تر می‌کنیم. CDN، Edge Servers، Pre-fetching، Caching - همه اینها در نهایت یک هدف دارند: کاهش فاصله فیزیکی بین داده و کاربر.


فصل ۲ - Building Blocks of TCP

✦ چرا TCP؟ پایه‌ای که همه چیز روی آن بنا شده

TCP (Transmission Control Protocol) پروتکلی است که عملاً تمام ترافیک وب روی آن سوار می‌شود. درک عمیق TCP به معنای درک دلیل رفتار مرورگر، تأخیرهای شبکه، و محدودیت‌های HTTP است.

TCP سه ویژگی اصلی تضمین می‌کند:

  • تحویل مطمئن (Reliable Delivery): هر Packet که گم شود، دوباره ارسال می‌شود
  • ترتیب صحیح (In-Order Delivery): داده‌ها دقیقاً به همان ترتیبی که فرستاده شده‌اند تحویل داده می‌شوند
  • کنترل خطا (Error Checking): بیت‌های خراب شناسایی و رد می‌شوند

✦ Three-Way Handshake: هزینه پنهان هر اتصال

قبل از اینکه یک بایت داده بین Client و Server رد و بدل شود، TCP یک فرآیند مذاکره سه مرحله‌ای انجام می‌دهد:

1
2
3
4
5
6
7
8
9
Client                          Server
  |                               |
  |  ----  SYN  -------->         |   (1) Client: "می‌خوام وصل بشم"
  |                               |
  |  <---  SYN ACK  -----         |   (2) Server: "باشه، آماده‌ام"
  |                               |
  |  ----  ACK  -------->         |   (3) Client: "فهمیدم، شروع کنیم"
  |                               |
  |  ----  DATA  ------->         |   حالا داده می‌رود

هزینه واقعی: قبل از ارسال اولین Packet داده، یک Round Trip Time کامل (RTT) هدر می‌رود. اگر Latency شما 100ms باشد، قبل از دریافت اولین بایت، 100ms فقط برای Handshake صرف شده است.

این موضوع یعنی هر اتصال جدید TCP گران است - و دلیل اصلی است که HTTP/1.1 اتصالات Keep-Alive را معرفی کرد و HTTP/2.0 آن‌ها را به یک اتصال تک‌کانال رساند.

✦ Flow Control: جلوگیری از غرق کردن گیرنده

TCP نمی‌تواند بدون محدودیت داده ارسال کند - گیرنده ممکن است کندتر از فرستنده باشد. مکانیزم Flow Control برای حل این مشکل طراحی شده است:

هر طرف اتصال یک Receive Window (rwnd) تبلیغ می‌کند: مقدار داده‌ای که می‌تواند در Buffer نگه دارد و هنوز ACK نگرفته.

  • در ابتدای اتصال: rwnd حداکثر 65,535 بایت (16-bit field در TCP Header)
  • با Window Scaling Option (RFC 1323): تا 1 گیگابایت قابل افزایش است
1
2
3
4
5
6
7
Client (rwnd = 65535)          Server
  |                               |
  |  <--- DATA (10KB) ----        |
  |  <--- DATA (10KB) ----        |
  |  <--- DATA (10KB) ----        |
  |  ... تا پر شدن rwnd ...       |
  |  ---- ACK + new rwnd --->     |   "فضا باز شد، بیشتر بفرست"

نکته عملی برای Senior Developer: اگر rwnd کوچک باشد و RTT بالا، Bandwidth واقعی شما محدود می‌شود، حتی اگر لینک فیزیکی سریع باشد.

✦ Slow-Start: منطق محافظه‌کارانه TCP

اینجا یکی از مهم‌ترین مفاهیم کتاب است که مستقیماً روی Performance تأثیر می‌گذارد.

وقتی یک اتصال TCP جدید برقرار می‌شود، Server نمی‌داند ظرفیت شبکه بین خودش و Client چقدر است. TCP از یک استراتژی محتاطانه استفاده می‌کند:

قانون Slow-Start:

  • از یک Congestion Window (cwnd) کوچک شروع کن
  • به ازای هر ACK دریافتی، cwnd را دو برابر کن (رشد نمایی)
  • تا رسیدن به نقطه‌ای که Packet Drop رخ می‌دهد یا Threshold رسیده می‌شود
1
2
3
4
5
Round 1:  cwnd = 4 segments  → 4 × 1460 byte = ~5.8 KB
Round 2:  cwnd = 8 segments  → ~11.6 KB
Round 3:  cwnd = 16 segments → ~23 KB
Round 4:  cwnd = 32 segments → ~46 KB
...

مشکل واقعی: یک فایل 64KB برای رسیدن به Full Speed نیاز به چندین RTT دارد. اگر RTT شما 100ms باشد، تنها برای «گرم کردن» اتصال چند صدم ثانیه صرف می‌شود.

این دلیلی است که اتصالات کوتاه HTTP/1.x بهینه نیستند - هر اتصال جدید از Slow-Start شروع می‌کند و هرگز به سرعت نهایی نمی‌رسد.

✦ Congestion Avoidance: وقتی شبکه شلوغ می‌شود

بعد از رسیدن cwnd به ssthresh (Slow-Start Threshold)، TCP وارد فاز Congestion Avoidance می‌شود:

  • رشد cwnd از نمایی به خطی تغییر می‌کند (به ازای هر RTT، فقط یک Segment اضافه می‌شود)
  • وقتی Packet Loss رخ می‌دهد: cwnd به نصف کاهش می‌یابد
  • این رفتار «دندانه‌دار» (Sawtooth Pattern) مشخصه TCP است

این مکانیزم برای حفظ عدالت بین اتصالات مختلف در شبکه طراحی شده - نه برای حداکثر کردن Performance یک اتصال خاص.

✦ Bandwidth-Delay Product: رابطه‌ای که باید حفظ کنید

یک فرمول کلیدی:

[ \text{BDP} = \text{Bandwidth} \times \text{RTT} ]

این عدد نشان می‌دهد چند بایت داده می‌تواند همزمان «در راه» باشد تا لینک کاملاً پر شود.

مثال:

  • لینک 10 Mbps، RTT = 100ms
  • [ \text{BDP} = 10 \times 10^6 \times 0.1 = 1,000,000 \text{ بایت} = ~1 \text{ MB} ]

یعنی برای استفاده کامل از این لینک، باید 1MB داده همزمان در راه باشد. اگر rwnd کمتر از این باشد، لینک کامل استفاده نمی‌شود - Bandwidth هدر می‌رود.

✦ Head-of-Line Blocking: مشکلی که HTTP/2 آمد حلش کند

یک محدودیت ذاتی TCP که درک آن برای فهمیدن HTTP/2 ضروری است:

TCP تضمین می‌کند داده‌ها به ترتیب تحویل داده شوند. اما اگر یک Packet گم شود:

1
2
3
4
5
6
Packet 1  ✅ رسید
Packet 2  ❌ گم شد
Packet 3  ✅ رسید (اما در Buffer نگه داشته می‌شود)
Packet 4  ✅ رسید (اما در Buffer نگه داشته می‌شود)
...
← همه منتظر Retransmit شدن Packet 2 هستند

تمام Packet‌های بعدی حتی اگر رسیده باشند به Application تحویل داده نمی‌شوند - تا زمانی که Packet گمشده دوباره بیاید. این پدیده Head-of-Line Blocking نام دارد.

✦ چک‌لیست بهینه‌سازی TCP

نویسنده فصل را با توصیه‌های عملی به پایان می‌رساند:

تنظیمات Server-Side:

  • TCP_NODELAY را فعال کن: جلوگیری از الگوریتم Nagle که بسته‌های کوچک را تجمیع می‌کند
  • Initial Congestion Window را افزایش بده: مقدار پیش‌فرض kernel را از 4 به 10 segment برسان (RFC 6928)
  • Slow-Start Restart را غیرفعال کن: بعد از مکث طولانی، اتصال‌های idle دوباره از Slow-Start شروع نکنند
  • Window Scaling را فعال کن: برای لینک‌های با RTT بالا و Bandwidth زیاد
  • TCP Fast Open (TFO): امکان ارسال داده در SYN Packet اول

رویکرد Application-Level:

  • اتصالات TCP را Reuse کن - هر بار اتصال جدید نزن
  • اگر ممکن است از Connection Pooling استفاده کن
  • داده‌ها را به‌گونه‌ای طراحی کن که Burst ارسال شوند تا Slow-Start زودتر تمام شود

فصل ۳ - Building Blocks of UDP

✦ UDP چیست و چرا وجود دارد؟

اگر TCP پروتکل «مطمئن و سخت‌گیر» است، UDP (User Datagram Protocol) نقطه مقابل آن است: ساده، سریع، و بدون هیچ تضمینی.

UDP سه چیز را عمداً حذف کرده است:

  • تضمین تحویل (No Guaranteed Delivery)
  • تضمین ترتیب (No In-Order Delivery)
  • کنترل ازدحام (No Congestion Control)

این «نبودها» نقص نیستند - طراحی آگاهانه‌ای هستند که UDP را برای کاربردهای خاص ایده‌آل می‌کنند.

✦ Null Protocol Services: پروتکلی که هیچ نمی‌گوید

نویسنده UDP را “Null Protocol” می‌نامد - چون تقریباً هیچ سرویسی بالای IP اضافه نمی‌کند.

مقایسه Header اندازه: | پروتکل | اندازه Header | | ——- | ————— | | IP | 20 بایت | | TCP | 20 بایت (حداقل) | | UDP | 8 بایت |

ساختار کامل UDP Header:

1
2
3
4
5
6
7
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Source Port          |       Destination Port        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            Length             |            Checksum           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

همین و بس. Source Port، Destination Port، Length، Checksum - چهار فیلد، هشت بایت. هیچ Sequence Number، هیچ ACK، هیچ Window Size.

حتی Checksum هم اختیاری است (در IPv4) - و اگر فعال باشد، خطا را فقط تشخیص می‌دهد، نه اصلاح.

✦ UDP و NAT: یک رابطه پیچیده

اینجا نویسنده یک چالش مهم معماری شبکه را مطرح می‌کند که برای هر توسعه‌دهنده‌ای که با UDP کار می‌کند حیاتی است.

مشکل اصلی: TCP یک اتصال واضح دارد (SYN/FIN)، اما UDP Stateless است. وقتی یک Packet UDP از پشت NAT عبور می‌کند، NAT باید بدون هیچ سیگنال «شروع» یا «پایان» اتصال، یک Mapping نگه دارد.

1
2
3
4
5
6
7
Client (192.168.1.5:54321)
       |
       ↓

       |
       ↓
  Internet → Server

مشکل Connection-State Timeout: چون UDP هیچ سیگنال قطع ارتباط ندارد، NAT نمی‌داند کِی باید Mapping را حذف کند. هر NAT یک Timeout مستقل دارد:

  • بعضی NAT‌ها: 30 ثانیه
  • بعضی دیگر: 3 دقیقه
  • RFC 4787 توصیه می‌کند: حداقل 2 دقیقه

نتیجه: اگر Client و Server برای مدتی Packet رد و بدل نکنند، Mapping از NAT حذف می‌شود و Packet‌های بعدی به مقصد نمی‌رسند - بدون هیچ اطلاع‌رسانی. این دقیقاً دلیلی است که بسیاری از اپلیکیشن‌های UDP مکانیزم Keepalive مصنوعی پیاده‌سازی می‌کنند.

✦ NAT Traversal: عبور از دیوار

یکی از چالش‌برانگیزترین مسائل در شبکه‌های مدرن، ارتباط مستقیم بین دو Client است که هر دو پشت NAT هستند. UDP این کار را ممکن می‌کند - اما نه به سادگی.

تکنیک اصلی: UDP Hole Punching

1
2
3
4
5
6
7
Client A (پشت NAT-A)          Server واسط          Client B (پشت NAT-B)
        |                          |                          |
        |--- "آدرس B چیه؟" ------> |                          |
        |                          | <--- "آدرس A چیه؟" ------|
        |<-------- آدرس B ---------|--------- آدرس A -------->|
        |                          |                          |
        |------------- مستقیم به هم وصل می‌شوند -------------|

هر دو Client همزمان به آدرس یکدیگر Packet می‌فرستند. این باعث می‌شود NAT هر طرف یک Mapping برای طرف مقابل ایجاد کند - و ارتباط Peer-to-Peer برقرار می‌شود.

✦ STUN، TURN و ICE: زیرساخت واقعی WebRTC

نویسنده سه پروتکل را معرفی می‌کند که روی UDP بنا شده‌اند و امروزه ستون فقرات WebRTC هستند:

STUN (Session Traversal Utilities for NAT):

  • به Client کمک می‌کند آدرس Public خودش را کشف کند
  • یک Server ساده STUN کافی است
  • وقتی NAT Traversal مستقیم ممکن باشد، STUN کافی است

TURN (Traversal Using Relays around NAT):

  • وقتی NAT Traversal مستقیم شکست می‌خورد (Symmetric NAT)
  • ترافیک از طریق یک Relay Server عبور می‌کند
  • گران‌تر است (Bandwidth مصرف می‌کند) اما همیشه کار می‌کند

ICE (Interactive Connectivity Establishment):

  • یک Framework که STUN و TURN را ترکیب می‌کند
  • تمام مسیرهای ممکن را آزمایش می‌کند و بهترین را انتخاب می‌کند
  • این همان مکانیزمی است که WebRTC برای برقراری ارتباط Peer-to-Peer استفاده می‌کند
1
2
3
4
اولویت‌بندی ICE:
1. Host Candidate    → مستقیم در همان شبکه
2. Server Reflexive  → از طریق STUN (NAT Traversal)
3. Relay Candidate   → از طریق TURN (Relay)

✦ چرا UDP را انتخاب کنیم؟

کاربردهایی که UDP در آن‌ها بر TCP برتری دارد:

  • DNS: یک Query کوچک، یک Response - نیازی به اتصال ندارد
  • بازی‌های آنلاین: یک Packet قدیمی بهتر از انتظار برای Retransmit است
  • VoIP و Video Streaming: تأخیر مهم‌تر از کامل بودن است - یک فریم گمشده قابل تحمل است
  • WebRTC: ارتباط Real-Time مستقیم بین Browser‌ها

نکته کلیدی نویسنده: UDP جایگزین TCP نیست - ابزار متفاوتی برای مسئله متفاوت است. وقتی می‌خواهید کنترل کامل بر رفتار اتصال داشته باشید و منطق Reliability خودتان را پیاده‌سازی کنید، UDP انتخاب درست است.

✦ چک‌لیست بهینه‌سازی UDP

نویسنده فصل را با توصیه‌های عملی تمام می‌کند:

  • اگر Order مهم است: خودت Sequence Number پیاده‌سازی کن
  • اگر Reliability مهم است: مکانیزم ACK خودت را بساز
  • برای عبور از NAT: STUN را پیاده‌سازی کن؛ اگر شکست خورد، TURN را داشته باش
  • Keepalive دوره‌ای بفرست: تا Mapping در NAT زنده بماند (هر 15-30 ثانیه)
  • MTU Discovery را انجام بده: از Fragmentation جلوگیری کن - اندازه Packet را کمتر از 1280 بایت نگه دار تا مشکل IPv6 هم نداشته باشی

فصل ۴ - Transport Layer Security (TLS)

✦ چرا TLS؟ سه هدف بنیادین

HTTP به‌تنهایی داده را به‌صورت Plain Text منتقل می‌کند - هر کسی در مسیر می‌تواند آن را بخواند، تغییر دهد، یا جعل کند. TLS سه مشکل را همزمان حل می‌کند:

هدفمعنی عملی
Encryption (رمزنگاری)داده‌ها برای هر کسی جز گیرنده نهایی ناخوانا هستند
Authentication (احراز هویت)مطمئن می‌شویم واقعاً با google.com صحبت می‌کنیم، نه یک مهاجم
Integrity (یکپارچگی)هیچ‌کس نمی‌تواند داده را در مسیر تغییر دهد بدون اینکه کشف شود

این سه با هم یک زنجیره اعتماد می‌سازند - نبود هر کدام، امنیت کل سیستم را زیر سوال می‌برد.

✦ TLS Handshake: هزینه امنیت

قبل از اینکه یک بایت داده رمزنگاری‌شده رد و بدل شود، TLS یک فرآیند مذاکره انجام می‌دهد که گران‌ترین بخش اتصال است:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Client                                    Server
  |                                          |
  |  ----  ClientHello  --------->           |   RTT 0: "از چه الگوریتمی حمایت می‌کنم"
  |                                          |
  |  <---  ServerHello  ----------           |   RTT 1: "این الگوریتم را انتخاب کردم"
  |  <---  Certificate  ----------           |           + گواهینامه‌ام را بگیر
  |  <---  ServerHelloDone  ------           |
  |                                          |
  |  ----  ClientKeyExchange  --->           |   RTT 1: تبادل کلید رمزنگاری
  |  ----  ChangeCipherSpec  ---->           |
  |  ----  Finished  ----------->           |
  |                                          |
  |  <---  ChangeCipherSpec  -----           |   RTT 2: تأیید نهایی
  |  <---  Finished  -------------           |
  |                                          |
  |  ----  GET /index.html  ----->           |   حالا اولین Request واقعی می‌رود

هزینه واقعی: یک اتصال HTTPS جدید نیاز به 2 RTT اضافه دارد - علاوه بر 1 RTT برای TCP Handshake. روی یک اتصال با Latency 100ms، فقط برای «شروع صحبت» 300ms هدر می‌رود.

✦ زنجیره اعتماد و Certificate Authority

چطور مرورگر مطمئن می‌شود که گواهینامه‌ای که Server ارائه می‌دهد واقعی است؟

مدل اعتماد سلسله‌مراتبی:

1
2
3
Root CA (VeriSign, DigiCert, ...)
    └── Intermediate CA
            └── گواهینامه سایت شما (example.com)

مرورگرها یک لیست از Root CA‌های مورد اعتماد دارند که از پیش نصب شده است. هر گواهینامه‌ای که توسط یکی از این Root CA‌ها (یا Intermediate CA تأییدشده توسط آن‌ها) امضا شده باشد، مورد قبول است.

نکته مهم: اگر Intermediate CA را در پاسخ ارسال نکنید، مرورگر مجبور می‌شود آن را جداگانه Fetch کند - این یک Round Trip اضافه و یکی از اشتباهات رایج پیکربندی TLS است.

✦ TLS Session Resumption: راه‌حل هزینه Handshake

نویسنده دو مکانیزم برای حذف هزینه Handshake تکراری معرفی می‌کند:

Session Identifiers (RFC 5246):

  • Server یک Session ID ذخیره می‌کند
  • Client در اتصال بعدی این ID را می‌فرستد
  • Server آن را تشخیص می‌دهد و Handshake کوتاه‌تری انجام می‌شود (1 RTT به جای 2 RTT)
  • مشکل: اگر Load Balancer داشته باشید، باید مطمئن شوید Client همیشه به همان Server وصل می‌شود (Sticky Sessions) - یا Session Cache مشترک داشته باشید

Session Tickets (RFC 5077):

  • Server اطلاعات Session را رمزنگاری می‌کند و به Client می‌دهد
  • Client در اتصال بعدی این Ticket را برمی‌گرداند
  • Server دیگر نیازی به ذخیره State ندارد
  • مزیت: با Load Balancer کار می‌کند بدون نیاز به هماهنگی

✦ Certificate Revocation: وقتی گواهینامه باطل می‌شود

اگر کلید خصوصی یک گواهینامه لو رفت، باید آن را «باطل» کرد. دو مکانیزم وجود دارد:

CRL (Certificate Revocation List):

  • یک لیست از گواهینامه‌های باطل‌شده که CA منتشر می‌کند
  • مشکل: این لیست می‌تواند بسیار بزرگ شود (صدها کیلوبایت) و باید دوره‌ای دانلود شود

OCSP (Online Certificate Status Protocol):

  • مرورگر در لحظه، وضعیت یک گواهینامه خاص را از CA می‌پرسد
  • مشکل: یک Round Trip اضافه به سرور CA اضافه می‌شود - و اگر CA سرور کند باشد، اتصال شما هم کند می‌شود

راه‌حل: OCSP Stapling

  • Server خودش از CA می‌پرسد و نتیجه را Timestamp‌دار و امضاشده کَش می‌کند
  • این نتیجه را همراه با گواهینامه به Client ارسال می‌کند
  • Client نیازی به تماس مستقل با CA ندارد - یک Round Trip حذف می‌شود

✦ TLS Record Protocol: واحد انتقال داده

بعد از Handshake، داده در قالب TLS Record منتقل می‌شود. هر Record شامل:

1
2
3
4
5
6
7
8
9
+------------------+
|  Content Type    |  (1 بایت) - داده، Alert، یا Handshake؟
+------------------+
|  Version         |  (2 بایت)
+------------------+
|  Length          |  (2 بایت) - حداکثر 16KB
+------------------+
|  Data + MAC      |  داده رمزنگاری‌شده + کد یکپارچگی
+------------------+

نکته مهم درباره اندازه Record:

  • Record بزرگ (16KB): Throughput بهتر - اما اگر یک بیت خراب باشد، کل Record باید دوباره بیاید
  • Record کوچک: Latency کمتر - مناسب برای Real-Time اپلیکیشن‌ها مثل VoIP

برای اکثر اپلیکیشن‌های وب، Record اول را کوچک نگه دارید تا در یک TCP Segment جا شود، بقیه را می‌توان بزرگ‌تر کرد.

✦ چک‌لیست بهینه‌سازی TLS

نویسنده فصل را با یک چک‌لیست جامع تمام می‌کند:

کاهش Computational Cost:

  • از TLS Session Resumption استفاده کن (Session Tickets ترجیحاً)
  • سرور را نزدیک به کاربر مستقر کن (CDN با TLS Termination در لبه)

کاهش Latency:

  • TLS False Start را فعال کن: ارسال داده قبل از تکمیل کامل Handshake
  • OCSP Stapling را پیکربندی کن
  • زنجیره گواهینامه را کوتاه کن - Intermediate CA را همیشه ارسال کن

امنیت:

  • HSTS (HTTP Strict Transport Security) را فعال کن: به مرورگر می‌گویید همیشه از HTTPS استفاده کن
  • از TLS 1.2 به بالا استفاده کن - SSLv3 و TLS 1.0 کاملاً شکسته شده‌اند
  • Perfect Forward Secrecy را با الگوریتم‌های ECDHE فعال کن: اگر کلید اصلی لو رفت، Session‌های قبلی همچنان امن باقی می‌مانند

بخش دوم - فصل ۵: Introduction to Wireless Networks

✦ اتصال همه‌جا: واقعیت جدید

نویسنده بخش دوم کتاب را با یک واقعیت ساده آغاز می‌کند: امروزه اکثر کاربران وب از طریق شبکه‌های بی‌سیم - نه کابل - به اینترنت متصل می‌شوند. این تغییر بنیادین، قواعد بهینه‌سازی Performance را کاملاً عوض می‌کند.

شبکه‌های بی‌سیم با شبکه‌های سیمی یک تفاوت اساسی دارند: محیط انتقال مشترک است. همه دستگاه‌های اطراف شما روی همان «هوا» ارتباط برقرار می‌کنند - این یعنی تداخل، رقابت، و عملکرد غیرقابل‌پیش‌بینی.

✦ انواع شبکه‌های بی‌سیم

نویسنده شبکه‌های بی‌سیم را بر اساس برد و کاربرد دسته‌بندی می‌کند:

نوعاستانداردبردکاربرد
WPANBluetooth, ZigBee~10 متردستگاه‌های شخصی
WLANWiFi (802.11)~100 مترخانه، اداره
WMANWiMAX~10 کیلومترشهری
WWAN3G, 4G LTEده‌ها کیلومترموبایل، ملی

هر کدام از این‌ها trade-off متفاوتی بین برد، Bandwidth، و مصرف انرژی دارند.

✦ سه عامل بنیادین عملکرد بی‌سیم

نویسنده عملکرد هر شبکه بی‌سیم را به سه عامل اصلی تقلیل می‌دهد:

۱. Bandwidth (پهنای باند فرکانسی)

در دنیای بی‌سیم، «Bandwidth» دو معنی دارد - و اشتباه گرفتن آن‌ها رایج است:

  • Bandwidth فیزیکی: پهنای باند فرکانسی که به یک کانال اختصاص داده شده (بر حسب Hz)
  • Bandwidth منطقی: نرخ انتقال داده (بر حسب Mbps)

قانون Shannon-Hartley رابطه این دو را بیان می‌کند:

\ ]

که در آن:

  • (C) = ظرفیت کانال (bps)
  • (B) = پهنای باند فرکانسی (Hz)
  • (S/N) = نسبت سیگنال به نویز (Signal-to-Noise Ratio)

نتیجه عملی: برای دو برابر کردن سرعت انتقال، یا باید پهنای باند فرکانسی را دو برابر کنید، یا SNR را به‌شدت افزایش دهید. این دلیل است که WiFi در اتاق کناری کندتر است - فاصله، SNR را کاهش می‌دهد.

۲. Signal Power (قدرت سیگنال)

قدرت سیگنال با فاصله کاهش می‌یابد - و این کاهش توانی (نه خطی) است:

[ \text{Signal Power} \propto \frac{1}{d^2} ]

اما در دنیای واقعی، دیوار، مبل، بدن انسان، و حتی مایکروویو همگی سیگنال را جذب یا منعکس می‌کنند. نویسنده تأکید می‌کند که Multipath Interference - رسیدن همان سیگنال از مسیرهای مختلف با تأخیرهای متفاوت - یکی از عوامل اصلی کاهش عملکرد WiFi در محیط‌های بسته است.

۳. Modulation (مدولاسیون)

Modulation روشی است که داده دیجیتال را روی سیگنال رادیویی سوار می‌کند. انتخاب روش Modulation مستقیماً روی نرخ انتقال تأثیر می‌گذارد:

روشبیت در Symbolنیاز به SNR
BPSK1کم
QPSK2متوسط
16-QAM4زیاد
64-QAM6خیلی زیاد

نکته کلیدی: دستگاه‌های WiFi و LTE به‌صورت Adaptive روش Modulation را بر اساس کیفیت لحظه‌ای سیگنال انتخاب می‌کنند. وقتی سیگنال ضعیف است، به BPSK برمی‌گردند - سرعت کم، اما اتصال پایدار.

✦ اندازه‌گیری عملکرد واقعی بی‌سیم

نویسنده یک هشدار مهم می‌دهد که اغلب نادیده گرفته می‌شود:

اعداد روی جعبه دستگاه‌های WiFi (مثل «300 Mbps» یا «1 Gbps») Peak Theoretical Throughput هستند - در شرایط آزمایشگاهی ایده‌آل. عملکرد واقعی ممکن است 50 تا 70 درصد پایین‌تر باشد.

دلایل این شکاف:

  • Protocol Overhead: Header‌های WiFi، ACK‌ها، و مکانیزم‌های کنترل Bandwidth مصرف می‌کنند
  • Half-Duplex بودن: اکثر شبکه‌های WiFi نمی‌توانند همزمان ارسال و دریافت کنند
  • Contention: همه دستگاه‌ها باید نوبت بگیرند - مکانیزم CSMA/CA این نوبت‌دهی را مدیریت می‌کند
  • تداخل فرکانسی: مایکروویو، بلوتوث، و شبکه‌های WiFi همسایه روی همان فرکانس‌ها کار می‌کنند

✦ نتیجه‌گیری راهبردی

نویسنده این فصل را با یک بینش راهبردی تمام می‌کند که کل بخش دوم کتاب را هدایت می‌کند:

بهینه‌سازی برای شبکه‌های بی‌سیم، متفاوت از بهینه‌سازی برای شبکه‌های سیمی است. در شبکه سیمی، Latency و Bandwidth نسبتاً ثابت و قابل پیش‌بینی هستند. در شبکه بی‌سیم، هر دو متغیر، ناپایدار، و وابسته به محیط فیزیکی هستند.

این یعنی اپلیکیشن شما باید برای تغییرپذیری طراحی شود، نه فقط برای یک نقطه عملکردی ثابت.


فصل ۶ - WiFi

✦ از Ethernet به شبکه بی‌سیم

WiFi داستان جالبی دارد - در واقع ریشه آن در Ethernet است. استاندارد 802.11 (نام رسمی WiFi) عمداً برای شبیه‌سازی رفتار Ethernet طراحی شد، با یک تفاوت اساسی: به جای کابل، از امواج رادیویی استفاده می‌کند.

در Ethernet سیمی، تشخیص تداخل (Collision Detection) ساده است - دستگاه همزمان می‌فرستد و گوش می‌دهد. اما در فضای رادیویی این کار ممکن نیست؛ دستگاهی که دارد ارسال می‌کند، سیگنال خودش را بسیار قوی‌تر از سیگنال دیگران می‌شنود. به همین دلیل WiFi از CSMA/CA (Collision Avoidance) به جای CSMA/CD (Collision Detection) استفاده می‌کند.

✦ استانداردهای WiFi: یک سیر تکاملی

استانداردسالفرکانسPeak Throughput
802.11b19992.4 GHz11 Mbps
802.11a19995 GHz54 Mbps
802.11g20032.4 GHz54 Mbps
802.11n20092.4 / 5 GHz600 Mbps
802.11ac20135 GHz1+ Gbps

نکته مهم درباره فرکانس:

  • 2.4 GHz: برد بیشتر، نفوذ بهتر از دیوار - اما شلوغ‌تر (مایکروویو، بلوتوث، WiFi همسایه‌ها)
  • 5 GHz: سرعت بیشتر، تداخل کمتر - اما برد کمتر و نفوذ ضعیف‌تر از موانع

این trade-off ساده اما مهم است: نزدیک به Router هستید؟ 5 GHz. دور یا پشت دیوار؟ 2.4 GHz.

✦ CSMA/CA: مکانیزم نوبت‌دهی WiFi

این مکانیزم قلب عملکرد WiFi است و درک آن برای فهمیدن محدودیت‌های WiFi ضروری است:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
دستگاه می‌خواهد بفرستد
         ↓
آیا کانال خالی است؟
    |           |
   بله          خیر
    |            ↓
    |     صبر کن تا خالی شود
    ↓            ↓
یک زمان تصادفی کوچک صبر کن (Backoff)
         ↓
اگر هنوز خالی بود → بفرست
         ↓
منتظر ACK بمان
         ↓
ACK نرسید؟ → دوباره تلاش کن

چرا Backoff تصادفی؟ اگر همه دستگاه‌ها بلافاصله بعد از خالی شدن کانال شروع به ارسال کنند، همه با هم تداخل ایجاد می‌کنند. تأخیر تصادفی احتمال این تداخل را کاهش می‌دهد.

هزینه پنهان: این مکانیزم یعنی WiFi ذاتاً Half-Duplex است - در یک لحظه، فقط یک دستگاه می‌تواند ارسال کند. هرچه دستگاه‌های بیشتری به یک Access Point وصل باشند، هر دستگاه سهم کمتری از Bandwidth دارد.

✦ Packet Loss در WiFi: متفاوت از TCP

اینجا یک بینش حیاتی است که بسیاری از توسعه‌دهندگان از آن غافل هستند:

در شبکه سیمی، Packet Loss تقریباً همیشه نشانه ازدحام شبکه (Congestion) است. TCP این را می‌داند و وقتی Packet گم می‌شود، cwnd را کاهش می‌دهد (Congestion Avoidance).

اما در WiFi، Packet Loss اغلب ربطی به Congestion ندارد - ناشی از تداخل رادیویی، ضعف سیگنال، یا حرکت فیزیکی دستگاه است.

نتیجه تراژیک: TCP نمی‌تواند این دو نوع Loss را از هم تشخیص دهد. وقتی WiFi یک Packet را به خاطر تداخل رادیویی Drop می‌کند، TCP فکر می‌کند شبکه شلوغ شده و سرعت خود را کاهش می‌دهد - در حالی که مشکل اصلی Congestion نبود.

این یکی از دلایل اصلی است که TCP روی WiFi همیشه به Bandwidth کامل نمی‌رسد.

✦ اندازه‌گیری و بهینه‌سازی عملکرد WiFi

نویسنده برای اندازه‌گیری دقیق عملکرد WiFi توصیه می‌کند که همیشه هر دو را اندازه بگیرید:

  • Signal Strength (RSSI): قدرت سیگنال دریافتی (بر حسب dBm - هرچه منفی‌تر، ضعیف‌تر)
  • SNR (Signal-to-Noise Ratio): نسبت سیگنال به نویز

یک RSSI قوی به‌تنهایی کافی نیست - اگر نویز محیط هم بالا باشد، SNR پایین می‌ماند و عملکرد افت می‌کند.

✦ چک‌لیست بهینه‌سازی برای WiFi

نویسنده سه اصل راهبردی برای توسعه‌دهندگان ارائه می‌دهد:

۱. از Bandwidth اضافی استفاده کن (Leverage Unmetered Bandwidth)

  • WiFi معمولاً Unmetered است - برخلاف شبکه موبایل
  • Pre-fetching محتوای احتمالی، Prefetch DNS، و Cache کردن aggressive روی WiFi منطقی است
  • اما این استراتژی را فقط روی WiFi اعمال کنید - روی موبایل هزینه دارد

۲. با Bandwidth متغیر کنار بیا (Adapt to Variable Bandwidth)

  • هرگز Bandwidth WiFi را ثابت فرض نکنید
  • از Adaptive Bitrate برای ویدئو استفاده کنید
  • اپلیکیشن باید بتواند با 1 Mbps هم کار کند، نه فقط با 100 Mbps

۳. با Latency متغیر کنار بیا (Adapt to Variable Latency)

  • Latency WiFi می‌تواند در عرض چند ثانیه از 1ms به 100ms برسد
  • Timeout‌های شبکه را با این واقعیت تنظیم کنید
  • UI را طوری طراحی کنید که در صورت تأخیر، کاربر را سردرگم نکند

✦ یک درس کلی از این فصل

نویسنده با یک نکته معماری مهم فصل را جمع‌بندی می‌کند:

بهترین بهینه‌سازی WiFi آن است که تعداد Round Tripها را کاهش دهید - نه فقط حجم داده را. چون هر بار که کانال آزاد می‌شود، دستگاه باید دوباره وارد فرآیند CSMA/CA شود. یک Request بزرگ بهتر از ده Request کوچک است.

این اصل مستقیماً با آنچه در فصل‌های HTTP خواهیم دید ارتباط دارد - و توضیح می‌دهد که چرا HTTP/2 با Multiplexing روی یک اتصال، روی WiFi هم بهتر از HTTP/1.x عمل می‌کند.


فصل ۷ - Mobile Networks

✦ چرا شبکه موبایل با WiFi فرق دارد؟

نویسنده این فصل را با یک واقعیت ساده آغاز می‌کند: شبکه موبایل (3G/4G) فقط «WiFi بی‌سیم‌تر» نیست - معماری آن از پایه با WiFi متفاوت است. یک لایه کنترلی پیچیده به نام RRC (Radio Resource Controller) وجود دارد که تمام جنبه‌های ارتباط رادیویی - از تخصیص فرکانس تا توان مصرفی - را مدیریت می‌کند و این لایه تأثیر مستقیم بر Latency اپلیکیشن دارد.

✦ تاریخچه کوتاه نسل‌ها (The G’s)

نسلاستانداردویژگی کلیدی
2GGSM, GPRS, EDGEاولین سرویس داده - چند ده Kbps
3GUMTS, HSPA, HSPA+داده پرسرعت موبایل - تا چند Mbps
4G LTELTE, LTE-Advancedمعماری All-IP، Latency کمتر

نکته مهم: 3GPP (استاندارد UMTS و LTE) و 3GPP2 (استاندارد CDMA/EV-DO) دو بدنه استانداردسازی مجزا هستند که رقیب هم بودند. امروزه 3GPP عملاً برنده این رقابت شده است.

✦ معماری End-to-End شبکه موبایل

برای فهمیدن اینکه یک Request چطور از مرورگر موبایل به سرور می‌رسد، باید معماری کلی شبکه را بشناسیم. نویسنده آن را به دو بخش تقسیم می‌کند:

Radio Access Network (RAN):

1
گوشی موبایل ←──── امواج رادیویی ────→ BTS (آنتن) ←──→ RNC (کنترلر)
  • BTS (Base Transceiver Station): آنتن‌هایی که سیگنال رادیویی را ارسال/دریافت می‌کنند
  • RNC (Radio Network Controller): چندین BTS را مدیریت می‌کند و تصمیم می‌گیرد که هر دستگاه با چه توان و فرکانسی ارتباط برقرار کند

Core Network (CN):

1
RNC ←──→ SGSN ←──→ GGSN ←──→ اینترنت عمومی
  • SGSN: احراز هویت و ردیابی موقعیت دستگاه
  • GGSN: دروازه ورود به اینترنت عمومی

Backhaul: اتصال بین BTS و Core Network - اغلب نقطه گلوگاه است. بسیاری از BTS‌ها با Microwave Link به شبکه وصل می‌شوند، نه فیبر. یعنی حتی با 4G قوی، Backhaul ممکن است محدودیت‌ایجاد کند.

✦ RRC State Machine: قلب ضربان شبکه موبایل

این مهم‌ترین مفهوم فصل است. RRC یک ماشین حالت (State Machine) است که وضعیت رادیویی هر دستگاه را کنترل می‌کند. دستگاه در هر لحظه در یکی از این حالت‌ها قرار دارد:

LTE RRC State Machine

1
2
3
4
5
6
7
8
9
10
┌─────────────────────────────────────────────────────────┐
│                                                         │
│   [RRC Idle]  ←────── Timer ──────  [RRC Connected]    │
│   < 15 mW              │             1000–3500 mW      │
│   فقط گوش می‌دهد        │             داده می‌فرستد     │
│                    ┌───┴───┐                            │
│                [Short DRX] [Long DRX]                   │
│                (نیمه‌بیدار) (خواب‌آلوده)                │
│                                                         │
└─────────────────────────────────────────────────────────┘
حالتتوان مصرفیتوضیح
RRC Idle< 15 mWفقط به Broadcast گوش می‌دهد، منابع رادیویی تخصیص داده نشده
Short DRXمتوسطContext شبکه حفظ است، منابع نیست، دوره‌های کوتاه خواب
Long DRXکم‌ترمانند Short DRX اما دوره‌های خواب طولانی‌تر
RRC Connected1000–3500 mWContext کامل، منابع تخصیصی، داده منتقل می‌شود

نکته حیاتی: وقتی گوشی در حالت Idle است و می‌خواهد داده بفرستد، باید از Idle به Connected ترقی پیدا کند. این ترقی چند Round Trip مذاکره با RRC نیاز دارد:

شبکهزمان Idle → Connected
LTE< 100 ms
LTE-Advanced< 50 ms
HSPA+ (3.5G)150–500 ms
HSPA / 3Gتا 2,000 ms
CDMA/EV-DOتا 2,500 ms

یعنی اگر گوشی کاربر چند ثانیه بی‌کار بوده، اولین Request اپلیکیشن شما ممکن است چند ثانیه فقط صرف برقراری مجدد ارتباط رادیویی کند - قبل از اینکه یک بایت داده رد و بدل شود.

✦ UMTS RRC: پیچیده‌تر از LTE

شبکه‌های 3G (HSPA, HSPA+) یک حالت میانی اضافه به نام Cell FACH دارند:

1
2
3
[Idle] ←──── Timer ────  [Cell FACH]  ←──── Buffer < Threshold ────  [Cell DCH]
کم‌مصرف‌ترین         کانال مشترک کم‌سرعت                     پرمصرف‌ترین
                       (< 20 Kbps)

Cell FACH برای داده‌های کم‌حجم (مثل Keepalive یا Polling) طراحی شده، اما هر ارسال داده بیش از 100 تا 1000 بایت دستگاه را به Cell DCH ترقی می‌دهد. این ترقی خودش یک هزینه Latency دارد.

✦ انرژی دُم (Energy Tail): هزینه پنهان هر Request

یکی از جالب‌ترین بینش‌های این فصل مفهوم Energy Tail است:

وقتی دستگاه داده‌ای می‌فرستد، RRC آن را به حالت Connected ترقی می‌دهد. بعد از پایان انتقال، دستگاه بلافاصله به Idle برنمی‌گردد - یک Timer شروع می‌کند (معمولاً 5–10 ثانیه برای LTE، تا چند دقیقه برای 3G) و در طول این مدت رادیو همچنان با توان بالا روشن است اما هیچ داده‌ای منتقل نمی‌شود.

1
2
3
4
5
زمان:    0s         2s           12s          22s
         │          │             │            │
داده:   ████       هیچی         هیچی         هیچی
رادیو: [High] ── [High] ──── [DRX] ──────── [Idle]
               ↑ Energy Tail ↑ (Timer هنوز تمام نشده)

نتیجه: یک Request کوچک 100 بایتی ممکن است رادیو را 10 ثانیه در حالت پرمصرف نگه دارد. اگر اپلیکیشن شما هر 20 ثانیه یک Keepalive می‌فرستد، رادیو هرگز وارد حالت Idle نمی‌شود و باتری به سرعت تخلیه می‌شود.

✦ عملکرد واقعی شبکه‌های موبایل

نویسنده داده‌های تجربی را با صداقت کامل ارائه می‌دهد:

شبکهLatency (RTT)Throughput
WiFi2–10 ms1–50 Mbps
4G LTE30–70 ms5–50 Mbps
HSPA+40–100 ms2–20 Mbps
HSPA / 3G100–500 ms0.1–5 Mbps

اما این اعداد بدون احتساب RRC State Transition هستند. در دنیای واقعی، اولین Request می‌تواند چند برابر این اعداد Latency داشته باشد.

✦ جمع‌بندی معماری: جریان یک Packet در شبکه موبایل

یک Request ساده از مرورگر موبایل این مسیر را طی می‌کند:

1
2
مرورگر → RRC State Check → [ترقی به Connected اگر لازم است]
       → BTS (آنتن محلی) → RNC → SGSN → GGSN → اینترنت → سرور

هر مرحله Latency اضافه می‌کند - و RRC State Transition اغلب بزرگ‌ترین سهم را دارد، نه فاصله فیزیکی.


فصل ۸ - Optimizing for Mobile Networks

✦ چارچوب فکری این فصل

نویسنده این فصل را با یک پیش‌فرض صریح آغاز می‌کند: بهینه‌سازی برای موبایل یعنی بهینه‌سازی برای باتری، نه فقط سرعت. در شبکه‌های موبایل، این دو هدف اغلب در تضاد هستند - و فهمیدن این تضاد، پایه تمام تصمیم‌های معماری این فصل است.

✦ اصل اول: حفظ باتری (Preserve Battery Power)

رادیوی موبایل در حالت Connected پرمصرف‌ترین مؤلفه سخت‌افزاری گوشی است. نویسنده یک حساب‌کتاب ساده اما تکان‌دهنده ارائه می‌دهد:

1
2
3
4
ظرفیت باتری یک گوشی معمول:  5 Wh = 18,000 ژول
انرژی مصرفی هر چرخه Idle→Connected→Idle:  ~10 ژول
Polling هر 1 دقیقه:  60 × 10 = 600 ژول در ساعت
درصد مصرف باتری:  600 / 18,000 = ~3% در ساعت

نتیجه: فقط یک اپلیکیشن با Polling هر دقیقه، ۳٪ باتری در ساعت مصرف می‌کند. دو یا سه اپلیکیشن با فاصله‌های Polling متفاوت و باتری تا ظهر تمام شده است.

✦ اصل دوم: حذف انتقال‌های دوره‌ای و ناکارآمد

رایج‌ترین اشتباه در اپلیکیشن‌های موبایل، Polling دوره‌ای است - ارسال درخواست‌های منظم برای بررسی آپدیت‌های جدید. این الگو سه مشکل همزمان ایجاد می‌کند:

  • باتری: هر Polling یک چرخه RRC کامل Idle → Connected → Idle را تریگر می‌کند
  • Latency: کاربر منتظر می‌ماند تا نوبت بعدی Polling فرا برسد
  • ترافیک بی‌معنی: اکثر Pollingها جواب «هیچ تغییری نیست» دریافت می‌کنند

راه‌حل: به جای Pull، از Push استفاده کنید. سرویس‌هایی مثل Google Cloud Messaging (GCM) پیام‌ها را تجمیع می‌کنند و فقط زمانی که دستگاه رادیو فعال دارد تحویل می‌دهند.

✦ اصل سوم: حذف Keepaliveهای غیرضروری

یک تصور غلط رایج: «باید Keepalive بفرستیم تا اتصال TCP قطع نشود.»

نویسنده این تصور را اصلاح می‌کند: اتصال TCP و وضعیت رادیو مستقل از هم هستند. شبکه موبایل می‌تواند رادیو را Idle کند در حالی که اتصال TCP در Core Network زنده نگه داشته می‌شود.

قانون عملی:

  • اکثر Carrier‌ها یک NAT Timeout بین 5 تا 30 دقیقه دارند
  • یک Keepalive هر 5 دقیقه کافی است تا اتصال Idle باز بماند
  • اگر برنامه‌تان Keepalive بیشتر از این نیاز دارد، مشکل در تنظیمات سرور یا Proxy شماست - نه در شبکه

✦ اصل چهارم: پیش‌بینی Latency شبکه

نویسنده یک جدول واقع‌بینانه از هزینه یک HTTP Request ساده روی موبایل ارائه می‌دهد:

مرحله3G4G
Control Plane (RRC)200–2,500 ms50–100 ms
DNS Lookup200 ms100 ms
TCP Handshake200 ms100 ms
TLS Handshake200–400 ms100–200 ms
HTTP Request200 ms100 ms
جمع کل200–3,500 ms100–600 ms

این جدول به معنای واقعی کلمه تکان‌دهنده است: یک Request «ساده» روی 3G می‌تواند بیش از 3 ثانیه طول بکشد - قبل از اینکه حتی یک بایت داده واقعی رد و بدل شود.

✦ اصل پنجم: جداسازی تعامل کاربر از ارتباط شبکه

این اصل از نظر UX حیاتی است:

هرگز UI را به شبکه گره نزنید. کاربر باید در کمتر از 100–200 میلی‌ثانیه بازخورد بصری ببیند - حتی اگر درخواست شبکه هنوز در حال انجام باشد.

الگوی درست:

1
2
3
4
5
6
7
کاربر دکمه را می‌زند
        ↓
فوری: UI بهروز می‌شود (Optimistic Update)
        ↓ (در پس‌زمینه)
Request شبکه ارسال می‌شود
        ↓
در صورت خطا: UI به حالت قبل برمی‌گردد + پیام خطا

Control Plane Latency به‌تنهایی می‌تواند بودجه 100ms تعامل کاربر را تمام کند - برنامه‌ریزی برای این واقعیت یک ضرورت طراحی است، نه یک بهینه‌سازی اختیاری.

✦ اصل ششم: Burst کردن داده و بازگشت به Idle

این مهم‌ترین اصل معماری این فصل است:

رادیوی موبایل برای انتقال‌های انبوه و کوتاه بهینه شده است - نه برای جریان‌های پیوسته کوچک.

استراتژی درست:

  • همه داده‌های مورد نیاز را یکجا دانلود کن
  • رادیو را سریع به Idle برگردان
  • Prefetch کن - محتوایی که کاربر احتمالاً بعداً نیاز دارد را از قبل بگیر

استراتژی اشتباه:

  • دانلود Progressive در قطعه‌های کوچک → رادیو هرگز به Idle نمی‌رسد
  • درخواست‌های پراکنده با فاصله → هر بار یک چرخه RRC جدید

نویسنده مثالی عینی می‌زند: اپلیکیشن Pandora وقتی آهنگ دانلود می‌کند، کل فایل را یکجا می‌گیرد - نه Streaming در لحظه. این به ظاهر «بیشتر» است، اما در واقع باتری کمتری مصرف می‌کند.

✦ اصل هفتم: Offload به WiFi

صنعت تخمین می‌زند که نزدیک به 90٪ ترافیک موبایل در فضای داخلی تولید می‌شود - جایی که اغلب WiFi هم در دسترس است.

قانون ساده:

  • برای انتقال‌های بزرگ (ویدئو، پشتیبان‌گیری، آپدیت): منتظر WiFi بمان
  • اگر اجباری است روی موبایل انجام شود: کاربر را آگاه کن و اجازه بگیر
  • WiFi از نظر مصرف انرژی برای انتقال‌های حجیم کارآمدتر از LTE است

✦ اصل هشتم: طراحی برای عدم‌دسترسی شبکه

نویسنده با یک اصل معماری مهم فصل را جمع می‌کند:

قطعی شبکه را استثناء نپندارید - آن را قاعده بدانید.

چک‌لیست طراحی:

  • وضعیت شبکه را Cache نکنید - همیشه می‌تواند تغییر کند
  • برای Retry از Backoff Algorithm استفاده کنید - حلقه بی‌نهایت نزنید
  • از HTML5 AppCache و localStorage برای حالت Offline استفاده کنید
  • به navigator.onLine گوش بدهید و UI را بر اساس آن تنظیم کنید
  • Transient Error جزء طبیعی موبایل است - کاربر نباید آن را به چشم «خرابی» ببیند

✦ چرا تاریخچه HTTP مهم است؟

نویسنده این فصل را با یک هدف مشخص آغاز می‌کند: فهمیدن چرایی تکامل HTTP، نه صرفاً آشنایی با نسخه‌های آن. هر نسخه جدید HTTP در پاسخ به یک دردِ واقعی متولد شد - و درک آن درد، درک HTTP/2 را ممکن می‌کند.

✦ HTTP 0.9: پروتکل تک‌خطی (1991)

Tim Berners-Lee در سال 1991 HTTP را با ساده‌ترین طراحی ممکن ارائه داد. هدف اصلی او، تسهیل پذیرش ایده جدیدش - World Wide Web - بود.

کل پروتکل را می‌توان در چند سطر خلاصه کرد:

1
2
3
4
5
6
7
$> telnet google.com 80
Connected to 74.125.xxx.xxx

GET /about/

(hypertext response)
(connection closed)

ویژگی‌های HTTP 0.9:

  • درخواست: یک رشته ASCII تک‌خطی - فقط متد GET و مسیر
  • پاسخ: فقط HTML - بدون Header، بدون Status Code، بدون Metadata
  • اتصال: بعد از هر درخواست فوری قطع می‌شد

این سادگی مزیت بزرگی بود - اما همین سادگی به‌زودی به محدودیت بزرگ تبدیل شد.

✦ HTTP 1.0: رشد سریع (1996)

بین 1991 تا 1995، سه اتفاق همزمان رخ داد که وب را متحول کرد: مرورگر NCSA Mosaic (اولین مرورگر محبوب)، شرکت Netscape (که Marc Andreessen تأسیس کرد)، و دسترسی عمومی به اینترنت از طریق CompuServe و AOL.

این رشد انفجاری نیازهای جدیدی آفرید که HTTP 0.9 اصلاً برای آن‌ها طراحی نشده بود. جامعه توسعه‌دهندگان به‌صورت آزمایشی شروع به افزودن ویژگی‌ها کرد، و در نهایت در RFC 1945 (می 1996) مستند شدند:

1
2
3
4
5
6
7
8
9
GET /rfc/rfc1945.txt HTTP/1.0
User-Agent: CERN-LineMode/2.15
Accept: */*

HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
(محتوای فایل)
(connection closed)

دستاوردهای HTTP 1.0:

  • Request Headers: ارسال Metadata مثل User-Agent و Accept
  • Response Status Line: وضعیت پاسخ (200 OK, 404 Not Found, …)
  • Response Headers: Metadata پاسخ مثل Content-Type و Content-Length
  • پاسخ نه‌فقط HTML: هر نوع محتوایی قابل انتقال شد

اما یک مشکل اساسی باقی ماند: هنوز هر Request یک اتصال TCP جداگانه نیاز داشت. با یادآوری فصل ۲، می‌دانیم این یعنی هر Request شامل TCP Three-Way Handshake + Slow-Start می‌شد - هزینه‌ای که با رشد تعداد Resourceها در صفحات وب، به‌سرعت غیرقابل‌تحمل شد.

✦ HTTP 1.1: استاندارد رسمی (1997–1999)

HTTP 1.1 در RFC 2068 (ژانویه 1997) منتشر شد و در RFC 2616 (ژوئن 1999) تکمیل گردید. این نسخه مشکلات اساسی HTTP 1.0 را هدف قرار داد:

مهم‌ترین تغییر - Keep-Alive به‌عنوان پیش‌فرض:

1
2
3
4
5
6
7
8
9
10
11
12
13
GET /index.html HTTP/1.1
Host: website.org
Connection: keep-alive

HTTP/1.1 200 OK
Connection: keep-alive
Transfer-Encoding: chunked

...

GET /favicon.ico HTTP/1.1   ← همان اتصال TCP، Request دوم
Host: website.org
Connection: close           ← اعلام پایان اتصال

در HTTP 1.0 برای هر Request یک TCP جدید باز می‌شد. در HTTP 1.1 اتصال به‌صورت پیش‌فرض زنده می‌ماند تا چندین Request روی آن انجام شود.

سایر بهبودهای مهم HTTP 1.1:

ویژگیکاربرد
Chunked Transfer Encodingارسال پاسخ به‌صورت تکه‌تکه، بدون نیاز به دانستن Content-Length از پیش
Byte-Range Requestsدانلود بخشی از فایل، امکان Resume دانلود
Caching Headers جدیدCache-Control، ETag، If-None-Match
Request Pipeliningارسال چند Request بدون انتظار برای پاسخ (در نظریه)
Host Header اجباریامکان هاستینگ چندین سایت روی یک IP

✦ مشکل پنهان HTTP 1.1: سربار Header

نویسنده یک نکته مهم را اینجا مطرح می‌کند که مستقیماً HTTP 2.0 را توجیه می‌کند. هر Request در HTTP 1.1، صدها تا هزاران بایت Header با خود حمل می‌کند - بدون هیچ فشرده‌سازی:

1
2
3
4
5
6
7
یک درخواست API با محتوای 15 بایت:

→ Header ارسالی:  218 بایت
→ Payload:         15 بایت  ({\"msg\":\"hello\"})
← Header پاسخ:   134 بایت

96٪ ترافیک = سربار پروتکل، فقط 4٪ = داده واقعی

هر Cookie، هر User-Agent، هر Accept-Encoding - همه در هر Request تکرار می‌شوند، بدون استثنا.

✦ HTTP 2.0: پاسخ به درد واقعی (2015)

نویسنده توضیح می‌دهد که HTTP 2.0 از پروژه SPDY گوگل (2009) متولد شد. گوگل با داده‌های واقعی نشان داد که می‌توان با تغییر لایه Transport - نه Semantics - عملکرد HTTP را به‌طور چشمگیری بهبود داد.

تفاوت فلسفی کلیدی: HTTP 2.0 هیچ Semantics جدیدی اضافه نمی‌کند - متدهای HTTP، Status Codeها، و Headerها دقیقاً همان‌هایی هستند که در HTTP 1.1 بودند. تنها چیزی که تغییر می‌کند نحوه قالب‌بندی و انتقال داده است:

مشکل HTTP 1.xراه‌حل HTTP 2.0
Head-of-Line BlockingMultiplexing - چند Stream روی یک TCP
Header تکراری و بدون فشرده‌سازیHPACK - فشرده‌سازی Header
Client باید Request بزندServer Push - ارسال پیش‌دستانه Resource
اتصالات TCP متعددیک اتصال برای همه منابع یک Origin

✦ خط قرمز این فصل

نویسنده فصل را با یک بینش معماری محوری جمع‌بندی می‌کند:

هر نسخه HTTP درسی دارد که معماران نرم‌افزار باید از آن بیاموزند: محدودیت‌های شبکه را نمی‌توان با اضافه کردن Feature در لایه Application حل کرد - باید پروتکل Transport را اصلاح کرد. HTTP 2.0 نه با افزودن قابلیت جدید، بلکه با حذف هزینه‌های پنهان لایه‌های زیرین، عملکرد را بهبود داد.


✦ هدف این فصل: تعریف دقیق مسئله

نویسنده این فصل را با یک مقدمه هوشمندانه آغاز می‌کند: قبل از اینکه بهینه‌سازی کنیم، باید بدانیم دقیقاً چه چیزی را بهینه می‌کنیم. وب Performance یک مفهوم مبهم نیست - اجزای دقیقی دارد که هر کدام معیار اندازه‌گیری مستقلی نیاز دارند.

✦ سه نوع تجربه وب، سه تعریف Performance

نویسنده وب را به سه دوره تاریخی تقسیم می‌کند که هر کدام تعریف متفاوتی از «سریع بودن» دارند:

نوعویژگیمعیار Performance
Hypertext Documentمتن ساده + لینکزمان دریافت یک سند
Web Pageتصویر + CSS + فونتPage Load Time (PLT)
Web ApplicationJavaScript + AJAX + تعاملزمان تعامل اول کاربر

نکته کلیدی: PLT (Page Load Time) که سال‌ها معیار استاندارد بود، برای Web Application دیگر کافی نیست. کاربر پیش از اینکه صفحه کاملاً Load شود، شروع به تعامل می‌کند - ما باید زمان تعامل اول (Time to First Interaction) را اندازه بگیریم، نه فقط زمان تکمیل بارگذاری.

✦ آناتومی یک اپلیکیشن مدرن

نویسنده با داده‌های واقعی (بر اساس آرشیو HTTP Archive تا مارس 2013) نشان می‌دهد که وب‌سایت‌های مدرن چقدر سنگین شده‌اند:

  • میانگین تعداد Request در هر صفحه: ~90 درخواست
  • میانگین حجم صفحه: ~1.3 MB
  • ترکیب منابع:
    • JavaScript: ~450 KB
    • تصاویر: ~650 KB
    • CSS: ~60 KB
    • HTML: ~55 KB

نتیجه مستقیم: هر صفحه وب معمول امروز ده‌ها اتصال TCP جداگانه نیاز دارد - هر کدام با Handshake، Slow-Start، و بعضاً TLS خودش. این دقیقاً جایی است که همه درس‌های فصل‌های قبلی به هم وصل می‌شوند.

✦ سرعت، Performance و ادراک انسانی

یکی از مهم‌ترین بخش‌های این فصل، ارتباط دادن اعداد فنی به تجربه ذهنی کاربر است. نویسنده بر اساس تحقیقات روان‌شناختی سه آستانه را معرفی می‌کند:

تأخیرادراک کاربر
< 100 msفوری - کاربر واکنش را بلادرنگ حس می‌کند
100–300 msکمی کند - توجه کاربر را می‌گیرد
300–1,000 msدستگاه دارد «فکر می‌کند» - احساس کندی واضح
> 1,000 msکاربر ذهنش را درگیر کار دیگری می‌کند

تأثیر معماری: این اعداد به معنای واقعی کلمه معماری سیستم را تعیین می‌کنند. اگر TCP Handshake تنها 56ms طول بکشد (لندن به نیویورک) و TLS Handshake 100ms دیگر اضافه کند، از بودجه 100ms تعامل کاربر فراتر رفته‌ایم - بدون اینکه حتی یک بایت داده واقعی منتقل شده باشد.

✦ تحلیل Resource Waterfall

نویسنده ابزار WebPageTest را به عنوان قدرتمندترین ابزار تحلیل Performance معرفی می‌کند. یک Waterfall نشان می‌دهد که چرا اکثر زمان بارگذاری صفحه صرف شبکه می‌شود، نه دانلود:

1
2
3
4
5
6
یک Request معمول در Waterfall:

[DNS]──[TCP]──[TLS]──[Waiting]──────────────────[Download]
 20ms   56ms  100ms    200ms                        30ms
 ←────────────────── 376ms ──────────────────→←30ms→
        96% زمان: انتظار و پروتکل       4%: دانلود واقعی

بررسی Yahoo.com در مارس 2013 نشان داد: 15 DNS Lookup، 30 TCP Handshake، و اکثر زمان صرف انتظار برای اولین بایت پاسخ (TTFB) می‌شود - نه دانلود خود منابع.

✦ سه ستون Performance و چرا Bandwidth مهم نیست

نویسنده Performance را به سه ستون تقسیم می‌کند:

  • Computing: پردازش JavaScript، Rendering، محاسبات CSS
  • Rendering: ساخت DOM، CSSOM، Layout، Paint
  • Networking: DNS، TCP، TLS، HTTP

و سپس یکی از جسورانه‌ترین ادعاهای کتاب را مطرح می‌کند:

Bandwidth بیشتر دیگر مهم نیست - Latency گلوگاه واقعی است.

این ادعا را با داده اثبات می‌کند:

تحقیق Google نشان داد که افزایش Bandwidth از 1 Mbps به 2 Mbps، PLT را 40% بهبود می‌دهد. اما افزایش از 5 Mbps به 10 Mbps؟ فقط 5% بهبود. از این نقطه به بعد، کاهش Latency هر 20ms، تأثیر بیشتری از دو برابر کردن Bandwidth دارد.

چرا؟ چون HTTP برای دریافت هر Resource به یک Round Trip نیاز دارد. اگر 90 Resource دارید و هر RTT 50ms است، فقط برای مذاکرات شبکه بیش از 4 ثانیه نیاز دارید - صرف نظر از اینکه Bandwidth چقدر باشد.

✦ دو نوع اندازه‌گیری: Synthetic و Real-User

نویسنده بین دو رویکرد اندازه‌گیری تمایز می‌گذارد که هر دو ضروری هستند:

Synthetic Measurement:

  • ابزارهایی مثل WebPageTest، Lighthouse
  • محیط کنترل‌شده، قابل تکرار
  • مناسب برای Debug و پیدا کردن Regression
  • نقطه ضعف: شرایط کاربر واقعی را منعکس نمی‌کند

Real-User Measurement (RUM):

  • جمع‌آوری داده از مرورگر کاربران واقعی با Navigation Timing API
  • نمایش توزیع واقعی Latency در جغرافیاهای مختلف، اتصال‌های مختلف، و دستگاه‌های مختلف
  • نقطه ضعف: Debug کردن سخت‌تر است

قانون طلایی: از Synthetic برای پیدا کردن مشکل استفاده کنید، از RUM برای تأیید اینکه مشکل واقعی است - نه برعکس.

✦ بهینه‌سازی‌های خودکار مرورگر

نویسنده فصل را با توصیف «موتور پنهان» مرورگر به پایان می‌رساند. مرورگرها خودشان بسیاری از بهینه‌سازی‌ها را انجام می‌دهند، اما توسعه‌دهنده باید به آن‌ها کمک کند:

  • DNS Pre-fetching: <link rel="dns-prefetch" href="//hostname.com">
  • Resource Pre-fetching: <link rel="prefetch" href="/next-page.js">
  • Pre-rendering: <link rel="prerender" href="//next-page.html">
  • Subresource Hints: <link rel="subresource" href="/critical.js">

هر یک از این Hintها به مرورگر می‌گوید: «این را می‌دانی که بعداً نیاز داری - همین الان شروع کن.» مرورگر تضمینی نمی‌دهد که به آن‌ها عمل کند، اما اگر Hint درست باشد، می‌تواند یک RTT کامل از Critical Path حذف کند.


✦ چارچوب فکری این فصل

نویسنده این فصل را نه با معرفی راه‌حل، بلکه با تشریح درد آغاز می‌کند. HTTP 1.x پروتکلی است که برای وب ابتدایی طراحی شد، اما اکنون باید وزن ۹۰+ Resource را در هر صفحه تحمل کند. نتیجه؟ توسعه‌دهندگان مجبور به اختراع Workaroundهایی شدند که هر کدام هزینه‌های پنهان خود را دارند.

✦ Keepalive: اولین و مهم‌ترین بهینه‌سازی

نویسنده با یک محاسبه ساده اما قاطع نشان می‌دهد که Keepalive چقدر تأثیرگذار است.

فرض کنید یک صفحه با N Resource داریم:

1
2
3
4
5
6
7
8
9
10
11
بدون Keepalive:
  هر Request = TCP Handshake + Response = 2 RTT
  کل لتانسی = N × 2 × RTT

با Keepalive:
  Request اول = 2 RTT
  هر Request بعدی = 1 RTT
  کل لتانسی = 2RTT + (N-1) × RTT

صرفه‌جویی = (N-1) × RTT
با N=90 و RTT=56ms → صرفه‌جویی ≈ 4.9 ثانیه!

نویسنده یک مثال عددی واقعی با دو Request را جزء‌به‌جزء مقایسه می‌کند:

سناریوزمان کل
دو اتصال TCP مجزا284ms
Keepalive فعال228ms
Keepalive + Pipelining172ms

نکته معماری: بسیاری از HTTP Libraryها به صورت پیش‌فرض Keepalive را غیرفعال می‌کنند چون «API ساده‌تری» می‌دهد. این یک به‌اصطلاح راحتی توسعه است که هزینه‌اش را کاربر نهایی پرداخت می‌کند.

✦ HTTP Pipelining: وعده‌ای که محقق نشد

Pipelining ایده‌ای جذاب بود: به جای اینکه Client صبر کند پاسخ Request اول برگردد و بعد Request دوم را بفرستد، هر دو را با هم ارسال کند. این یعنی حذف یک RTT کامل بین هر دو Request.

اما مشکل اینجاست که HTTP 1.x هیچ مکانیزم Multiplexing ندارد. پاسخ‌ها باید دقیقاً به همان ترتیب ارسال Request برگردند:

1
2
3
4
5
6
7
8
Request:   GET /html → GET /css
           ──────────────────────→

Server:    پردازش HTML (40ms) | پردازش CSS (20ms)

Response:  [HTML: 40ms] → [CSS: باید صبر کند!]
           ← ─────────────────────────────────
              CSS آماده بود اما باید پشت HTML صبر می‌کرد

این مشکل Head-of-Line Blocking است - دقیقاً همان پدیده‌ای که در فصل TCP دیدیم، اما این بار در لایه HTTP.

پیامدهای عملی که نویسنده فهرست می‌کند:

  • یک پاسخ کُند، تمام Requestهای پشت سر خود را مسدود می‌کند
  • سرور باید پاسخ‌های آماده را Buffer کند - خطر اتمام حافظه
  • یک پاسخ ناموفق می‌تواند کل اتصال TCP را ببندد
  • Proxyها و Intermediaryها اغلب Pipelining را نمی‌فهمند

نتیجه؟ امروز Pipelining در اغلب مرورگرها غیرفعال است.

✦ اتصالات TCP موازی: Workaround شماره ۱

وقتی Pipelining عملی نشد، مرورگرها به سراغ راه‌حل خام‌تر رفتند: باز کردن چندین اتصال TCP موازی به یک Host.

معماری فعلی اکثر مرورگرهای مدرن:

1
www.example.com → تا ۶ اتصال TCP همزمان

مزایا:

  • تا ۶ Request می‌تواند به صورت موازی پردازش شود
  • cwnd مؤثر ۶ برابر می‌شود → دور زدن Slow-Start

هزینه‌های پنهان که نویسنده صریحاً بیان می‌کند:

  • مصرف Memory و CPU اضافه روی Client، Server، و تمام Intermediaryها
  • رقابت بین ۶ جریان TCP برای Bandwidth مشترک
  • هیچ‌کدام از این اتصال‌ها از مزیت Slow-Start Ramp-up کافی بهره نمی‌برند
  • اگر اتصالی برای WebSocket یا SSE استفاده شود، آن شکاف از ۶ کم می‌شود و ممکن است Request جدید Hang کند

این راه‌حل را نویسنده صراحتاً «راه‌حل درست بلندمدت نیست» توصیف می‌کند، بلکه یک Workaround اجباری است.

✦ Domain Sharding: Workaround شماره ۲

محدودیت ۶ اتصال به ازای هر Hostname اعمال می‌شود، نه به ازای هر IP. توسعه‌دهندگان از این خلأ استفاده کردند:

1
2
3
4
5
6
7
8
به جای:    www.example.com   (سقف: ۶ اتصال)

استفاده از:
  shard1.example.com  → ۶ اتصال
  shard2.example.com  → ۶ اتصال
  shard3.example.com  → ۶ اتصال
  ────────────────────────────────
  مجموع:              ۱۸ اتصال موازی

نکته جالب که نویسنده اشاره می‌کند: در عمل همه این Subdomainها می‌توانند CNAME به همان IP باشند. مرورگر بر اساس Hostname محدودیت اعمال می‌کند، نه IP.

هزینه‌های Domain Sharding:

  • DNS Lookup اضافه برای هر Shard - هر کدام می‌تواند ده‌ها میلی‌ثانیه هزینه داشته باشد
  • TLS Handshake مجزا برای هر Shard (در HTTPS) - یک RTT اضافه برای هر Shard
  • TCP Slow-Start مجدد برای هر اتصال جدید
  • پیچیدگی Deployment و نگهداری

قانون طلایی که نویسنده ارائه می‌دهد:

با کمترین تعداد Shard (صفر) شروع کن. افزایش تدریجی بده و هر بار اثر آن را با معیار واقعی اندازه بگیر. در عمل، تعداد بسیار کمی از سایت‌ها از بیش از یک دوجین اتصال سود می‌برند.

✦ کنترل سربار Header: ابزار اندازه‌گیری

نویسنده برای اندازه‌گیری سربار واقعی Header، استفاده از ابزار curl را توضیح می‌دهد:

1
2
3
4
5
6
7
8
9
10
11
# مشاهده Request Headers
curl --trace-ascii - https://www.igvita.com/ 2>&1 | grep -A 10 "=> Send header"

# یک نمونه واقعی از Header Request:
GET / HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0 ...       ← ~120 bytes
Accept: text/html,...             ← ~60 bytes
Accept-Language: en-US,...        ← ~35 bytes
Accept-Encoding: gzip,deflate     ← ~30 bytes
Cookie: session_id=...            ← گاهی تا 1KB+

هر Request این Headerها را بدون هیچ فشرده‌سازی حمل می‌کند و در صفحاتی با ۹۰+ Resource، این سربار جمع می‌شود. این دقیقاً مشکلی است که HPACK در HTTP/2 برطرف می‌کند.

✦ Concatenation و Spriting: Workaround لایه Application

وقتی پروتکل نمی‌تواند به درستی Requestها را Multiplex کند، راه‌حل توسعه‌دهندگان این شد که تعداد Requestها را کاهش دهند:

تکنیکروشهدف
Concatenationادغام تمام فایل‌های JS/CSS در یک فایلکاهش تعداد Request
Spritingادغام چندین آیکون در یک تصویر بزرگکاهش Request تصاویر
Resource Inliningقرار دادن CSS/JS مستقیم در HTML با <style> یا <script>حذف کامل Request

هزینه‌های پنهان این روش‌ها که نویسنده صراحتاً برمی‌شمرد:

  • Concatenation: اگر یک خط JS تغییر کند، کل فایل Concatenated باید مجدد دانلود شود - Cache busting اجباری
  • Spriting: اگر یک آیکون کوچک به Sprite اضافه یا حذف شود، کل تصویر باید عوض شود
  • Inlining: Resource نمی‌تواند جداگانه Cache شود - در هر بارگذاری صفحه، مجدد دانلود می‌شود

نتیجه کلیدی این فصل: تمام این تکنیک‌ها - Pipelining، Multiple TCP Connections، Domain Sharding، Concatenation، Spriting، Inlining - Workaroundهای ناقصی هستند که برای جبران محدودیت‌های بنیادی HTTP 1.x ابداع شدند. HTTP/2 اکثر این مشکلات را در سطح پروتکل حل می‌کند - و در فصل بعدی خواهیم دید که چگونه.


فصل ۱۲ - HTTP 2.0

✦ چارچوب فکری: یک تحول، نه یک به‌روزرسانی

نویسنده این فصل را با یک تمایز کلیدی آغاز می‌کند: HTTP 2.0 هیچ Semantics جدیدی اضافه نمی‌کند - متدها، Status Codeها، و Headerها دقیقاً همان‌هایی هستند که در HTTP 1.1 بودند. آنچه تغییر می‌کند، نحوه قالب‌بندی و انتقال داده است - و این تفاوت ظاهراً جزئی، تمام محدودیت‌های فصل قبلی را در لایه پروتکل حل می‌کند.

✦ تاریخچه: از SPDY تا HTTP 2.0

در سال ۲۰۰۹، تیم مهندسی گوگل پروژه SPDY را آغاز کرد - یک پروتکل آزمایشی با هدف کاهش تأخیر بارگذاری صفحات وب. گوگل آن را روی سرویس‌های خود مانند Gmail و Google Search اجرا کرد و نتایج واقعی جمع‌آوری کرد: کاهش ۶۴٪ در PLT برای اتصالات پرتأخیر.

در تابستان ۲۰۱۲، گروه کاری IETF (HTTP-WG) پیش‌نویس SPDY v2 را به عنوان نقطه شروع HTTP 2.0 پذیرفت. SPDY و HTTP 2.0 به‌صورت موازی تکامل یافتند:

  • SPDY v3 (2012): فرمت Framing به‌روز شد + اولین پیاده‌سازی Flow Control
  • SPDY v4 (2013–2014): بهبود Prioritization، Flow Control، و Server Push
  • هدف نهایی: وقتی HTTP 2.0 نهایی شود، SPDY بازنشسته می‌شود

✦ Binary Framing Layer: قلب HTTP 2.0

مهم‌ترین تغییر معماری HTTP 2.0 یک لایه جدید بین Socket Interface و HTTP API است که نویسنده آن را Binary Framing Layer می‌نامد.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
HTTP 1.x - متن‌محور:

GET /resource HTTP/1.1\r\n
Host: example.com\r\n
\r\n

─────────────────────────────────────
HTTP 2.0 - باینری:

+-----------------------------------------------+
| Length (24 bits) | Type (8) | Flags (8)        |
+-----------------------------------------------+
| R | Stream Identifier (31 bits)               |
+-----------------------------------------------+
| Frame Payload                                 |
+-----------------------------------------------+

این تفاوت ظاهراً فنی، یک پیامد اساسی دارد: اکنون هر پیام HTTP می‌تواند به Frameهای کوچک تقسیم شود - و این Frameها از چندین Stream مختلف می‌توانند روی یک اتصال TCP با هم قاطی شوند.

✦ سه مفهوم بنیادی: Stream، Message، Frame

نویسنده اصطلاح‌شناسی جدیدی معرفی می‌کند که درک HTTP 2.0 بدون آن ممکن نیست:

مفهومتعریفمثال
Streamیک کانال دوطرفه مجازی درون یک اتصال TCP - با یک شناسه عددی یکتاStream 1, Stream 3, Stream 5
Messageیک پیام HTTP کامل (Request یا Response) که از یک یا چند Frame تشکیل شدهیک GET Request
Frameکوچک‌ترین واحد ارتباطی - حامل داده‌هایی مثل HTTP Header یا PayloadHEADERS frame, DATA frame

رابطه این سه: یک اتصال TCP ← چندین Stream ← هر Stream چندین Message ← هر Message چندین Frame.

✦ Multiplexing: حل بنیادی Head-of-Line Blocking

این مهم‌ترین قابلیت HTTP 2.0 است. در HTTP 1.x، هر اتصال TCP فقط می‌توانست یک Response را در لحظه تحویل دهد. حالا نگاه کنید به آنچه HTTP 2.0 ممکن می‌کند:

1
2
3
4
5
6
7
8
9
HTTP 1.x - Sequential:
──[Request A]──→
←──[Response A]──
──[Request B]──→
←──[Response B]──

HTTP 2.0 - Multiplexed:
──[A: Frame1][B: Frame1][A: Frame2][B: Frame2]──→
←──[A: Frame1][B: Frame1][A: Frame2][B: Frame2]──

بر روی یک اتصال TCP، Client می‌تواند DATA Frame از Stream 5 ارسال کند، درحالی‌که Server همزمان Frameهای Stream 1 و Stream 3 را برمی‌گرداند. سه تبادل Request-Response موازی، روی یک TCP.

نتایج مستقیم این قابلیت:

  • نیازی به Multiple TCP Connection نیست - Domain Sharding منسوخ می‌شود
  • نیازی به Concatenation و Spriting نیست - هر فایل می‌تواند جداگانه Cache شود
  • Head-of-Line Blocking در لایه HTTP از بین می‌رود
  • مصرف CPU و Memory سرور و Client به‌شدت کاهش می‌یابد

✦ Request Prioritization

وقتی صدها Stream می‌توانند روی یک اتصال همزمان در جریان باشند، یک سؤال مطرح می‌شود: کدام Stream اول پردازش شود؟ HTTP 2.0 به هر Stream یک مقدار Priority 31 بیتی اختصاص می‌دهد:

  • 0 = بالاترین اولویت
  • 2³¹ − 1 = پایین‌ترین اولویت

Client می‌تواند بگوید: «CSS را قبل از تصاویر بفرست» - و Server می‌تواند تخصیص منابع (CPU، Memory، Bandwidth) را متناسب با این اولویت‌ها تنظیم کند. نویسنده تأکید می‌کند که HTTP 2.0 هیچ الگوریتم مشخصی برای Prioritization تعریف نمی‌کند - پیاده‌سازی آن به توسعه‌دهندگان Server واگذار شده است.

✦ One Connection Per Origin: یک اتصال، همه چیز

HTTP 2.0 یک اصل ساده دارد: فقط یک اتصال TCP به ازای هر Origin. این در تضاد آشکار با HTTP 1.x است که ۶ اتصال موازی باز می‌کرد.

چرا یک اتصال کافی است؟ چون Multiplexing این نیاز را حذف کرده. و چرا این بهتر است؟

1
2
3
4
5
6
7
8
HTTP 1.x:
  6 TCP Connection × Slow-Start = 6 بار رشد کُند
  6 اتصال × Memory = مصرف بالا در سرور

HTTP 2.0:
  1 TCP Connection با cwnd بزرگ‌تر
  Slow-Start فقط یک بار اتفاق می‌افتد
  اتصال در طول زمان «گرم می‌شود»

✦ Flow Control در HTTP 2.0

HTTP 2.0 Flow Control خودش را در سطح Stream پیاده‌سازی می‌کند، نه فقط در سطح Connection. این یعنی Client می‌تواند بگوید: «از Stream 3 بیشتر داده نفرست - هنوز دارم پردازش می‌کنم»، درحالی‌که Stream 1 بدون هیچ محدودیتی جریان دارد.

مکانیزم کار دقیقاً شبیه TCP rwnd است:

  • هر Stream یک Flow Control Window جداگانه دارد
  • Receiver می‌تواند Window را به صفر برساند تا جریان را متوقف کند
  • با ارسال یک WINDOW_UPDATE Frame، جریان از سر گرفته می‌شود

✦ Server Push: پایان دادن به Inlining

Server Push یکی از قدرتمندترین قابلیت‌های HTTP 2.0 است. ایده ساده است: Server قبل از اینکه Client درخواست کند، Resource مورد نیاز را می‌فرستد.

1
2
3
4
5
6
7
8
9
10
11
بدون Server Push:
  Client: GET /index.html
  Server: ← HTML
  Client: (HTML را Parse می‌کند، style.css را می‌بیند)
  Client: GET /style.css         ← یک RTT اضافه
  Server: ← CSS

با Server Push:
  Client: GET /index.html
  Server: ← HTML + PUSH_PROMISE(style.css) + CSS
  Client: (HTML را Parse می‌کند، CSS قبلاً دریافت شده!)

نویسنده نکته مهمی را اضافه می‌کند: همه Pushed Resourceها قابل Cache شدن هستند - برخلاف Inlining در HTTP 1.x که هر بار با HTML مجدد دانلود می‌شد. Client همچنین می‌تواند یک Push را رد کند - برای مثال وقتی Resource قبلاً در Cache است.

مکانیزم فنی آن PUSH_PROMISE Frame است که Server قبل از ارسال پاسخ اصلی می‌فرستد تا Race Condition جلوگیری شود.

✦ Header Compression با HPACK

به یاد می‌آورید که در فصل ۱۱ نشان دادیم ۹۶٪ یک Request می‌تواند Header باشد؟ HTTP 2.0 این مشکل را با الگوریتم HPACK حل می‌کند:

سه مکانیزم HPACK:

  1. Static Table: یک جدول از ۶۱ Header رایج که از پیش شماره‌گذاری شده‌اند - به جای ارسال content-type: text/html، فقط عدد 16 فرستاده می‌شود
  2. Dynamic Table: Headerهایی که در طول Session ارسال شده‌اند در یک جدول مشترک ذخیره می‌شوند - دفعه بعد فقط Index آن‌ها فرستاده می‌شود
  3. Huffman Encoding: فشرده‌سازی مقادیر رشته‌ای
1
2
3
4
5
6
7
Request اول:
  ارسال کامل: method, path, host, user-agent, cookie → ~500 bytes

Request دوم (همان مسیر):
  فقط تفاوت‌ها ارسال می‌شود → ~20 bytes

کاهش سربار: ~96%

✦ مکانیزم Upgrade: چگونه HTTP 2.0 کشف می‌شود

نویسنده توضیح می‌دهد که HTTP 2.0 باید با HTTP 1.x سازگار باشد - یعنی Client نمی‌داند آیا Server از HTTP 2.0 پشتیبانی می‌کند یا نه. سه روش Upgrade وجود دارد:

روشمکانیزمزمان استفاده
TLS + ALPNدر TLS Handshake، پروتکل مذاکره می‌شودHTTPS - روش توصیه‌شده
HTTP Upgrade HeaderUpgrade: h2c در اولین Request HTTP 1.1HTTP بدون TLS
Prior KnowledgeClient از قبل می‌داند Server از HTTP 2.0 پشتیبانی می‌کندسناریوهای خاص

نکته معماری: روش ALPN (Application-Layer Protocol Negotiation) هیچ RTT اضافه‌ای ندارد - Negotiation درون TLS Handshake که قبلاً انجام می‌شد جا می‌گیرد.

✦ جمع‌بندی معماری: HTTP 2.0 مشکل‌ها را کجا حل می‌کند

مشکل HTTP 1.xراه‌حل موقتِ فصل قبلیراه‌حل ساختاری HTTP 2.0
Head-of-Line BlockingMultiple TCP ConnectionsMultiplexing روی یک TCP
Header تکراری و بزرگهیچHPACK Compression
عدم اولویت‌بندیهیچStream Prioritization
Inlining برای کاهش RequestResource InliningServer Push
کشف Resource دیرDNS Pre-fetch، Resource HintsServer Push
۶ TCP × Slow-StartDomain Shardingیک TCP با cwnd بزرگ‌تر

فصل ۱۳ - Optimizing Application Delivery

✦ چارچوب فکری: سه لایه بهینه‌سازی

نویسنده این فصل را با یک جمع‌بندی معماری آغاز می‌کند: بهینه‌سازی‌ها به سه دسته تقسیم می‌شوند. اول، Evergreen Best Practicesهایی که در هر پروتکلی کاربرد دارند. دوم، بهینه‌سازی‌های خاص HTTP 1.x. سوم، بهینه‌سازی‌های خاص HTTP 2.0 - که برخی از آن‌ها مستقیماً با آنچه برای HTTP 1.x کردیم در تضاد هستند.

✦ Evergreen Best Practices: اصول ابدی

این چهار اصل صرف نظر از پروتکل، زبان، یا پلتفرم همیشه درست هستند:

۱. Cache Resources on the Client

هیچ درخواستی سریع‌تر از درخواستی نیست که اصلاً ارسال نشود. نویسنده سه Header کلیدی Cache را معرفی می‌کند:

1
2
3
Cache-Control: max-age=3600              ← مدت اعتبار: ۱ ساعت
Last-Modified: Thu, 01 Jan 2015 00:00:00 ← آخرین تغییر
ETag: "15f0fff99ed5aae4edffdd6496d7131f"  ← اثرانگشت محتوا

قانون طلایی: برای Static Assetها (JS، CSS، تصاویر) یک Fingerprint (مثل Hash فایل) به نام فایل اضافه کنید و Cache-Control: max-age=31536000 (یک سال) بگذارید. اگر فایل تغییر کند، نام عوض می‌شود - Cache خودکار باطل می‌شود.

۲. Compress Transferred Data

همه متن‌ها (HTML، CSS، JS، JSON) باید با Gzip یا Brotli فشرده شوند.

نتایج واقعی که نویسنده ذکر می‌کند:

  • HTML: کاهش ~70%
  • CSS: کاهش ~80%
  • JavaScript: کاهش ~65%

⚠️ تصاویر، ویدئو، و فایل‌های از پیش فشرده‌شده (ZIP، PDF) را نباید مجدد Gzip کنید - CPU بیشتری مصرف می‌شود، اما اندازه تغییری نمی‌کند.

۳. Eliminate Unnecessary Request Bytes

اکثر توسعه‌دهندگان نمی‌دانند چقدر Cookie و Header اضافه با هر Request می‌فرستند. نویسنده با یک مثال ساده نشان می‌دهد:

1
2
3
4
یک صفحه ساده با ۹۰ Request:
  هر Cookie معمول: ~200 bytes
  ۹۰ Request × 200 bytes = 18,000 bytes = ~18 KB
  فقط برای تکرار Cookie در هر درخواست!

توصیه‌های عملی:

  • Cookieهای غیرضروری را حذف کنید
  • Static Assetها را از یک Cookie-Free Domain سرو کنید
  • Headerهای Custom غیرضروری را Remove کنید

۴. Parallelize Request and Response Processing

مرورگر یک Thread نیست - می‌تواند کارها را موازی کند، اما به کمک شما نیاز دارد.

ابزارهایی که نویسنده معرفی می‌کند:

  • dns-prefetch: رزولو کردن DNS قبل از نیاز
  • preconnect: باز کردن TCP+TLS به سرور مقصد
  • prefetch: دانلود Resource مورد نیاز صفحه بعدی
  • prerender: رندر کامل صفحه بعدی در پس‌زمینه

✦ بهینه‌سازی برای HTTP 1.x

این بهینه‌سازی‌ها مکمل اصول قبلی هستند و فقط برای زمانی معنی دارند که هنوز از HTTP 1.x استفاده می‌کنید:

تکنیکهدفتوضیح
Keepaliveحذف Handshake مجددپیش‌فرض HTTP 1.1 - اطمینان حاصل کنید فعال است
Domain Shardingافزایش اتصالات موازیاز shard1.example.com و shard2.example.com استفاده کنید
Concatenationکاهش تعداد RequestJS/CSS فایل‌ها را در یک فایل ادغام کنید
Spritingکاهش Request تصاویرآیکون‌ها را در یک تصویر بزرگ ترکیب کنید
Resource Inliningحذف کامل RequestCSS/JS حیاتی را مستقیم در HTML جاگذاری کنید

نویسنده یک هشدار صریح می‌دهد: این تکنیک‌ها برای HTTP 1.x ضروری هستند - اما اگر HTTP 2.0 را اجرا کنید و همچنان از آن‌ها استفاده کنید، عملکرد شما کاهش می‌یابد.

✦ بهینه‌سازی برای HTTP 2.0: حذف Workaroundها

این بخش یکی از مهم‌ترین بخش‌های عملی کتاب است. نویسنده صریحاً می‌گوید که باید بهینه‌سازی‌های HTTP 1.x را معکوس کنید:

1
2
3
HTTP 1.x:   Concatenate all JS into one file → فایل کمتر = Request کمتر
HTTP 2.0:   Keep files separate → هر فایل Cache مجزا دارد + Multiplexing
                                   = بهتر از یک فایل بزرگ!

موارد مشخصی که باید حذف شوند:

  • Domain Sharding را بردارید: هر Shard یک اتصال TCP جدید است. در HTTP 2.0، یک اتصال با Multiplexing از ۶ اتصال جداگانه سریع‌تر است.
  • Concatenation را بردارید: اگر فقط یک خط JS تغییر کند، Cacheبودن کل Bundle باطل می‌شود. فایل‌های جداگانه Cache‌های مستقل دارند.
  • Spriting را بردارید: هر آیکون جداگانه می‌تواند Cache شود و Server Push می‌تواند آن‌ها را Proactively بفرستد.
  • Inlining را کاهش دهید: فقط برای Critical CSS/JS که واقعاً روی Rendering Path اول تأثیر دارد، Inline کنید.

✦ استراتژی Dual-Protocol: پشتیبانی از هر دو

در دنیای واقعی، هرگز نمی‌توان همه کاربران را یکجا به HTTP 2.0 منتقل کرد. بنابراین باید از هر دو پروتکل به‌طور همزمان پشتیبانی کرد. نویسنده یک چارچوب تصمیم‌گیری ارائه می‌دهد:

1
2
3
4
Client ← → Server
Client: "آیا HTTP/2 بلدی؟"  (از طریق ALPN در TLS Handshake)
Server: "بله" → اتصال HTTP/2 برقرار می‌شود
Server: "خیر" → Fallback به HTTP/1.1

برای سرویس‌دهی بهینه به هر دو گروه، نویسنده توصیه می‌کند:

  • Build Pipeline را دوگانه کنید: یک خروجی برای HTTP 2.0 (فایل‌های جداگانه) و یک خروجی برای HTTP 1.x (Bundle شده)
  • یا با Feature Detection در Runtime تصمیم بگیرید که کدام نسخه را سرو کنید

✦ ارزیابی کیفیت سرور HTTP 2.0

نویسنده این فصل را با یک هشدار جدی پایان می‌دهد: صرفاً اینکه سرور از HTTP 2.0 پشتیبانی می‌کند، کافی نیست - کیفیت پیاده‌سازی اهمیت دارد.

معیارهای ارزیابی که باید بررسی کنید:

معیاراهمیت
Server Push پیاده‌سازی شده؟بدون Push، یک RTT کامل از دست می‌رود
Header Compression درست است؟HPACK باید درست پیاده‌سازی شده باشد
Prioritization رعایت می‌شود؟CSS باید قبل از تصاویر دریافت شود
TLS فعال است؟مرورگرها HTTP 2.0 را بدون TLS قبول نمی‌کنند
Load Balancer HTTP 2.0 را می‌فهمد؟اکثر Load Balancerهای قدیمی فقط HTTP 1.x می‌دانند

نکته نهایی که نویسنده تأکید می‌کند: بسیاری از Load Balancerها و Proxyها HTTP 2.0 را در Terminateمی‌کنند و به Backend با HTTP 1.1 ارتباط برقرار می‌کنند. اگر این وضعیت در معماری شما وجود دارد، تمام مزایای Multiplexing بین Client و Load Balancer محدود می‌شود - و Connection بین Load Balancer و سرور همچنان از محدودیت‌های HTTP 1.x رنج می‌برد.


فصل ۱۴ - Primer on Browser Networking

✦ چارچوب فکری: مرورگر یک سیستم عامل شبکه است

نویسنده این فصل را با یک بینش ساده اما عمیق آغاز می‌کند: مرورگر مدرن چیزی بیش از یک نمایش‌دهنده HTML است. در واقع یک پلتفرم شبکه‌ای پیچیده است که خودش مدیریت اتصال، کش، امنیت، و API را انجام می‌دهد - و درک این موتور پنهان، کلید بهینه‌سازی هر Application وب است.

✦ لایه‌های چهارگانه Browser Networking

نویسنده معماری درونی مرورگر را به چهار لایه مجزا تقسیم می‌کند که هر کدام وظیفه مستقلی دارند:

1
2
3
4
5
6
7
8
9
┌──────────────────────────────────────────────────┐
│          Application APIs and Protocols          │  ← XHR, SSE, WebSocket, WebRTC
├──────────────────────────────────────────────────┤
│       Resource and Client State Caching          │  ← HTTP Cache, Cookie, Session
├──────────────────────────────────────────────────┤
│          Network Security and Sandboxing         │  ← Same-Origin Policy, TLS
├──────────────────────────────────────────────────┤
│        Connection Management & Optimization      │  ← Socket Pool, DNS Cache
└──────────────────────────────────────────────────┘

این چهار لایه با هم تعامل دارند، و توسعه‌دهنده هر اشتباهی در لایه بالاتر بکند، لایه‌های پایین‌تر نمی‌توانند آن را جبران کنند.

✦ لایه اول: Connection Management و Socket Pool

یکی از پیچیده‌ترین - و در عین حال نامرئی‌ترین - بخش‌های مرورگر، Socket Manager است. مرورگر به هر Application یک Socket خام نمی‌دهد؛ بلکه یک Pool از Socket های مدیریت‌شده را پشت پرده نگه‌داری می‌کند.

Socket Pool در Chrome چگونه کار می‌کند:

1
2
3
4
5
6
7
8
9
10
11
12
13
یک Request جدید به example.com می‌رسد:

1. آیا DNS برای example.com در Cache هست؟
   بله → بدون Lookup
   خیر → DNS Resolution شروع می‌شود

2. آیا Socket کارکشته‌ای (Warmed-up) به example.com موجود است؟
   بله → Socket را از Pool بردار و Request را بفرست
   خیر → TCP Handshake جدید (و احتمالاً TLS)

3. آیا به سقف اتصال رسیده‌ایم؟
   بله → Request در Queue قرار می‌گیرد
   خیر → اتصال جدید باز می‌شود

این Pool به صورت Cross-Tab مشترک است - اگر دو Tab از یک مرورگر به gmail.com وصل باشند، اتصالات TCP را با هم به اشتراک می‌گذارند.

بهینه‌سازی‌های Predictive که مرورگر انجام می‌دهد

نویسنده تأکید می‌کند که مرورگر منفعل نیست - به صورت فعالانه رفتار کاربر را پیش‌بینی می‌کند:

بهینه‌سازیزمان اجراتوضیح
DNS Pre-resolveوقتی کاربر روی لینک Hover می‌کندDNS را رزولو می‌کند قبل از کلیک
TCP Pre-connectبر اساس الگوی NavigationTCP Handshake را قبل از نیاز انجام می‌دهد
Resource Pre-fetchبر اساس History کاربرResourceی که احتمالاً نیاز دارد را دانلود می‌کند
Pre-renderبر اساس تحلیل رفتاریصفحه بعدی را کاملاً در پس‌زمینه رندر می‌کند

نکته معماری: توسعه‌دهنده می‌تواند با Resource Hintها این پیش‌بینی را دقیق‌تر کند. اما اگر Hintها اشتباه باشند، منابع شبکه و CPU هدر می‌رود.

✦ لایه دوم: Network Security و Sandboxing

مرورگر به عنوان یک Untrusted Environment طراحی شده - کدی که از اینترنت دانلود می‌شود، نباید بتواند به سرویس‌های داخلی شبکه دسترسی داشته باشد.

اصلی‌ترین مکانیزم امنیتی، Same-Origin Policy است:

1
2
3
4
5
6
Origin = Scheme + Hostname + Port

https://example.com:443 → Origin A
http://example.com:443  → Origin متفاوت (Scheme فرق دارد)
https://api.example.com → Origin متفاوت (Hostname فرق دارد)
https://example.com:80  → Origin متفاوت (Port فرق دارد)

JavaScript اجرا شده در یک Origin، نمی‌تواند به منابع Origin دیگری دسترسی داشته باشد - مگر با مکانیزم‌های صریح مانند CORS (Cross-Origin Resource Sharing).

محدودیت‌هایی که مرورگر در لایه شبکه اعمال می‌کند:

  • اتصال به یک IP داخلی شبکه (مثل 192.168.x.x) از صفحه‌ای که از اینترنت آمده، ممنوع است
  • هر Request Cross-Origin باید از فیلتر CORS رد شود
  • Cookieها فقط برای Origin مشخص‌شده فرستاده می‌شوند
  • پروتکل‌های خاص (مثل file://) محدودیت‌های بیشتری دارند

✦ لایه سوم: Resource و Client State Caching

مرورگر چندین لایه Cache مجزا دارد که هر کدام برای نوع خاصی از داده بهینه شده‌اند:

1
2
3
4
5
6
7
8
┌─────────────────────────────────────────────────────────┐
│  DNS Cache       → نگهداری نتایج DNS Lookup             │
│  TCP Socket Pool → نگهداری اتصالات TCP گرم              │
│  HTTP Cache      → نگهداری Response های HTTP            │
│  TLS Session     → نگهداری Session های TLS              │
│  Cookie Store    → نگهداری Cookieها                    │
│  localStorage    → نگهداری داده‌های Persistent          │
└─────────────────────────────────────────────────────────┘

نویسنده یک نکته مهم را مطرح می‌کند که اغلب نادیده گرفته می‌شود: هر لایه Cache طول عمر متفاوتی دارد. DNS Cache مرورگر معمولاً کوتاه‌تر از TTL اعلام‌شده توسط DNS Server است. HTTP Cache تا زمان Eviction یا Expiry پیش می‌رود. TCP Socket Pool می‌تواند هر لحظه توسط OS بسته شود.

✦ لایه چهارم: Application APIs و Protocols

بالاترین لایه، APIهایی است که توسعه‌دهنده مستقیماً با آن‌ها کار می‌کند. نویسنده این فصل را با معرفی سه API اصلی به پایان می‌برد که هر کدام در فصل‌های بعدی به تفصیل بررسی می‌شوند:

APIحالت ارتباطمناسب برای
XMLHttpRequest (XHR)Request-Response، یک‌طرفهAPI Call، فرم ارسال
Server-Sent Events (SSE)یک‌طرفه Server به ClientNotification، Live Feed
WebSocketدوطرفه، Real-TimeChat، بازی آنلاین، داشبورد
WebRTCPeer-to-Peerویدیوکنفرانس، انتقال فایل

اصل طلایی این فصل: هیچ‌کدام از این APIها مستقیم به شبکه دسترسی ندارند - همه از طریق لایه‌های زیرین (Connection Pool، Security، Cache) عبور می‌کنند. این یعنی یک درخواست XHR که ظاهراً ساده به نظر می‌رسد، ممکن است پشت پرده از یک Socket Warm استفاده کند، یک Response از Cache برگرداند، یا به دلیل Same-Origin Policy مسدود شود - بدون اینکه توسعه‌دهنده خبر داشته باشد.


فصل ۱۵ - XMLHttpRequest (XHR)

✦ چارچوب فکری: قدیمی اما همه‌جا حاضر

XHR اولین API بود که به JavaScript اجازه داد بدون reload کردن صفحه با سرور ارتباط برقرار کند. این یعنی XHR پایه‌گذار تمام تجربه‌های Ajax، SPA، و وب مدرن است - و با اینکه APIهای جدیدتری مثل fetch معرفی شده‌اند، XHR هنوز پرکاربردترین مکانیزم ارتباط شبکه در مرورگر باقی مانده است.

✦ تاریخچه مختصر XHR

XHR در ابتدا توسط تیم Microsoft به عنوان یک ActiveX Object در Internet Explorer 5 (سال 1999) معرفی شد. هدف اصلی آن، ارسال و دریافت داده از سرور Exchange در Outlook Web Access بود. سپس Mozilla، Safari، و Opera همین ایده را پیاده‌سازی کردند - و نهایتاً در سال 2006 توسط W3C استانداردسازی شد.

✦ ساختار پایه XHR

یک درخواست XHR از سه مرحله تشکیل می‌شود:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ۱. ایجاد آبجکت
var xhr = new XMLHttpRequest();

// ۲. پیکربندی
xhr.open('GET', '/api/data', true);    // true = Async
xhr.setRequestHeader('Accept', 'application/json');
xhr.timeout = 5000;                    // 5 ثانیه Timeout

// ۳. ارسال و دریافت
xhr.onload = function() {
    if (xhr.status === 200) {
        console.log(xhr.responseText);
    }
};
xhr.onerror = function() { /* مدیریت خطا */ };
xhr.send();

نویسنده یک نکته مهم مطرح می‌کند: پارامتر سوم open() که Async را تنظیم می‌کند، هرگز نباید false باشد - چون Synchronous XHR کل UI Thread مرورگر را تا پایان Request قفل می‌کند.

✦ Cross-Origin Resource Sharing (CORS)

به یاد می‌آورید که در فصل ۱۴ گفتیم Same-Origin Policy از ارتباط Cross-Origin جلوگیری می‌کند؟ CORS مکانیزمی است که این محدودیت را به شکل کنترل‌شده برمی‌دارد.

دو نوع CORS Request وجود دارد:

نوع اول - Simple Request: بدون هیچ مذاکره اضافه‌ای انجام می‌شود، مشروط به:

  • متد: GET، POST، یا HEAD
  • فقط Headerهای مجاز: Accept، Content-Type (با مقادیر محدود)، Accept-Language
1
2
3
4
5
6
7
8
Client → Server:
  GET /api/resource HTTP/1.1
  Origin: https://foo.example.com     ← Browser این را اضافه می‌کند

Server → Client:
  Access-Control-Allow-Origin: *      ← "همه می‌توانند بخوانند"
  یا:
  Access-Control-Allow-Origin: https://foo.example.com

نوع دوم - Preflight Request: قبل از Request اصلی، یک OPTIONS Request فرستاده می‌شود:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Client → Server (Preflight):
  OPTIONS /api/resource HTTP/1.1
  Origin: https://foo.example.com
  Access-Control-Request-Method: PUT
  Access-Control-Request-Headers: X-Custom-Header

Server → Client (Preflight Response):
  Access-Control-Allow-Origin: https://foo.example.com
  Access-Control-Allow-Methods: PUT, DELETE
  Access-Control-Allow-Headers: X-Custom-Header
  Access-Control-Max-Age: 1728000     ← Cache این Preflight را 20 روز نگه‌دار

Client → Server (Request اصلی):
  PUT /api/resource HTTP/1.1          ← حالا مجاز است

نکته Performance‌محور: هر Preflight یک RTT اضافه است. با تنظیم Access-Control-Max-Age می‌توان Preflight را Cache کرد - یعنی برای همان Origin و متد، دیگر Preflight تکرار نمی‌شود.

✦ دانلود و آپلود داده

دانلود داده:

1
2
3
4
5
6
7
8
9
10
11
// دانلود متن
xhr.responseType = 'text';        // پیش‌فرض

// دانلود JSON
xhr.responseType = 'json';

// دانلود فایل باینری (تصویر، ویدیو)
xhr.responseType = 'arraybuffer';

// دانلود Blob (مثلاً برای ساخت دانلود لینک)
xhr.responseType = 'blob';

آپلود داده:

1
2
3
4
5
6
7
8
9
10
// آپلود JSON
xhr.open('POST', '/upload');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send(JSON.stringify({ key: 'value' }));

// آپلود فایل با FormData
var formData = new FormData();
formData.append('file', fileInput.files;
formData.append('username', 'john');
xhr.send(formData);             // Content-Type خودکار تنظیم می‌شود

✦ نظارت بر Progress: ویژگی اغلب فراموش‌شده

XHR یک API کامل برای Monitoring پیشرفت دانلود و آپلود ارائه می‌دهد:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// نظارت بر دانلود
xhr.onprogress = function(event) {
    if (event.lengthComputable) {
        var percent = (event.loaded / event.total) * 100;
        console.log(percent.toFixed(1) + '% دانلود شد');
    }
};

// نظارت بر آپلود
xhr.upload.onprogress = function(event) {
    if (event.lengthComputable) {
        var percent = (event.loaded / event.total) * 100;
        progressBar.style.width = percent + '%';
    }
};

نویسنده نکته‌ای ظریف مطرح می‌کند: event.lengthComputable زمانی false است که سرور Content-Length Header را ارسال نکرده باشد - مثلاً در پاسخ‌های Chunked Transfer Encoding.

✦ Streaming داده با XHR

XHR می‌تواند داده را قبل از پایان دریافت پردازش کند - اما با یک محدودیت مهم:

1
2
3
4
5
6
xhr.onreadystatechange = function() {
    if (xhr.readyState === 3) {              // LOADING - داده در حال دریافت
        // xhr.responseText تا اینجا را داریم
        processChunk(xhr.responseText);
    }
};

چهار ReadyState اصلی:

مقدارثابتمعنی
0UNSENTآبجکت ساخته شده، open() صدا نزده
1OPENEDopen() صدا زده شده
2HEADERS_RECEIVEDHeaders دریافت شده
3LOADINGBody در حال دریافت
4DONEکامل شد

محدودیت کلیدی: در ReadyState 3، responseText همیشه تجمیعی است - یعنی کل داده تا اینجا را در خود دارد، نه فقط Chunk جدید. برای هر بار پردازش، باید موقعیت آخر را نگه‌داشته و فقط بخش جدید را پردازش کنید.

✦ الگوهای Real-Time: Polling و Long-Polling

وقتی نیاز به دریافت به‌روزرسانی‌های Real-Time از سرور داریم، XHR دو الگو ارائه می‌دهد:

Polling ساده:

1
2
3
4
5
6
7
8
9
function poll() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/updates');
    xhr.onload = function() {
        processData(xhr.responseText);
        setTimeout(poll, 3000);      // هر ۳ ثانیه یک‌بار
    };
    xhr.send();
}

هزینه: هر Polling یک TCP Handshake + Request + Response است - حتی اگر هیچ داده جدیدی وجود نداشته باشد.

Long-Polling:

1
2
3
4
5
6
7
8
9
10
function longPoll() {
    var xhr = new XMLHttpRequest();
    xhr.open('GET', '/updates');     // سرور پاسخ را نگه می‌دارد
    xhr.onload = function() {
        processData(xhr.responseText);
        longPoll();                  // بلافاصله درخواست بعدی
    };
    xhr.send();
}
// سرور تا وقتی داده‌ای نداشته باشد، Connection را باز نگه می‌دارد

Long-Polling در مقایسه با Polling ساده:

  • مزیت: Latency تحویل داده بسیار کمتر - داده به محض آماده شدن تحویل داده می‌شود
  • هزینه: هر اتصال یک Thread/Process در سرور اشغال می‌کند - مشکل C10K

✦ محدودیت‌های ذاتی XHR و چه زمانی از آن استفاده نکنیم

نویسنده این فصل را با یک صادقانه‌ترین بخش آن به پایان می‌رساند - محدودیت‌های ساختاری XHR:

محدودیتتوضیحراه‌حل
فقط Request-ResponseClient همیشه باید اول بپرسدSSE یا WebSocket
یک Request در هر XHRهر آبجکت یک Request می‌فرستدConnection Pool مرورگر
Streaming ناقصداده همیشه Buffer می‌شودWebSocket
Header محدودبرخی Headerها قابل تنظیم نیستندمحدودیت امنیتی مرورگر

قانون طلایی انتخاب API:

  • نیاز به یک‌بار دریافت داده؟ → XHR یا fetch
  • نیاز به دریافت مداوم از سرور؟ → SSE
  • نیاز به ارتباط دوطرفه Real-Time؟ → WebSocket
  • نیاز به ارتباط Peer-to-Peer؟ → WebRTC

فصل ۱۶ - Server-Sent Events (SSE)

✦ چارچوب فکری: یک‌طرفه اما کافی

در فصل قبل دیدیم که XHR برای Real-Time Notification ناکارآمد است - هر بار Client باید از سرور بپرسد. SSE این مشکل را با یک رویکرد ساده اما قدرتمند حل می‌کند: سرور می‌تواند به صورت مستمر داده‌هایی را به Client Push کند، روی یک اتصال HTTP معمولی - بدون هیچ پروتکل جدیدی.

✦ EventSource API: ساده‌ترین API شبکه‌ای مرورگر

کل پیاده‌سازی SSE در سمت Client به سه خط کد خلاصه می‌شود:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ۱. باز کردن اتصال
var source = new EventSource('/stream');

// ۲. دریافت رویداد پیش‌فرض
source.onmessage = function(event) {
    console.log(event.data);
};

// ۳. دریافت رویداد با نام دلخواه
source.addEventListener('price-update', function(event) {
    updateChart(JSON.parse(event.data));
});

// ۴. مدیریت خطا
source.onerror = function() {
    // مرورگر به صورت خودکار Reconnect می‌کند
};

برخلاف XHR، EventSource به صورت خودکار Reconnect می‌کند - اگر اتصال قطع شود، مرورگر بعد از چند ثانیه مجدداً وصل می‌شود.

✦ Event Stream Protocol: یک فرمت متنی ساده

SSE از یک پروتکل متن‌محور بسیار ساده استفاده می‌کند که روی HTTP معمولی سوار می‌شود:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Response Headers:
  Content-Type: text/event-stream
  Cache-Control: no-cache
  Connection: keep-alive

Response Body (جریان بی‌پایان):

data: این یک پیام ساده است\n\n

data: {"user": "ali", "msg": "سلام"}\n\n

event: price-update\n
data: {"price": 42.50}\n\n

id: 42\n
event: stock-update\n
data: {"symbol": "AAPL", "price": 182.3}\n\n

: این یک Comment است و نادیده گرفته می‌شود\n\n

retry: 15000\n\n    ← تنظیم زمان Reconnect به 15 ثانیه

فرمت Event Stream چهار نوع Field دارد:

Fieldکاربردمثال
dataمحتوای پیام - می‌تواند چند خط باشدdata: hello
eventنام رویداد دلخواهevent: price-update
idشناسه پیام برای Reconnectid: 42
retryزمان انتظار قبل از Reconnect (ms)retry: 3000

✦ مکانیزم Last-Event-ID: Reconnect هوشمند

یکی از هوشمندانه‌ترین ویژگی‌های SSE، نحوه مدیریت قطع و وصل است. هر بار که مرورگر Reconnect می‌کند، آخرین id دریافت‌شده را در Header می‌فرستد:

1
2
3
4
5
Client → Server (بعد از Reconnect):
  GET /stream HTTP/1.1
  Last-Event-ID: 42          ← "از پیام ۴۲ ادامه بده"

Server: می‌داند کاربر از کجا جا مانده و پیام‌های از‌دست‌رفته را می‌فرستد

این یعنی SSE می‌تواند پیام‌های از‌دست‌رفته در زمان قطعی را پس از اتصال مجدد تحویل دهد - یک قابلیتی که در XHR Polling اصلاً وجود ندارد.

✦ پیاده‌سازی سمت سرور

سمت سرور فقط باید یک Response معمولی HTTP را باز نگه‌دارد و داده‌ها را به تدریج بنویسد:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# مثال Python / Flask
from flask import Response
import time

def event_stream():
    count = 0
    while True:
        count += 1
        yield f"id: {count}\n"
        yield f"data: time\n\n"
        time.sleep(1)

@app.route('/stream')
def stream():
    return Response(
        event_stream(),
        content_type='text/event-stream'
    )

نکته معماری مهم: هر Client یک اتصال TCP باز نگه می‌دارد. این یعنی برای ۱۰,۰۰۰ کاربر همزمان، سرور باید ۱۰,۰۰۰ اتصال باز داشته باشد. سرورهای Thread-based (مثل Apache) در این سناریو به سرعت به مشکل می‌خورند - سرورهای Event-Loop-based (مثل Node.js یا Nginx) برای SSE بسیار مناسب‌ترند.

✦ محدودیت‌های SSE و مقایسه با WebSocket

ویژگیSSEWebSocket
جهت ارتباطفقط Server به Clientدوطرفه
پروتکلHTTP معمولیپروتکل جدید (ws://)
نوع دادهفقط متن UTF-8متن و Binary
Reconnect خودکار✅ داخلی✗ باید دستی پیاده‌سازی شود
HTTP/2 Multiplexing✅ کاملاً سازگار✗ اتصال جداگانه
Proxy و Firewall✅ مشکل کمتر✗ گاهی مسدود می‌شود
پشتیبانی مرورگرIE را پشتیبانی نمی‌کندهمه مرورگرهای مدرن

✦ SSE و HTTP/2: ترکیب ایده‌آل

نویسنده یک نکته معماری طلایی مطرح می‌کند که اغلب نادیده گرفته می‌شود: در HTTP 1.x مرورگر تنها ۶ اتصال به یک Host دارد. اگر کاربر به ۶ Stream SSE نیاز داشته باشد، همه اتصالات مصرف می‌شوند و هیچ اتصالی برای درخواست‌های معمولی باقی نمی‌ماند.

اما در HTTP/2 همه Streamها روی یک اتصال TCP Multiplex می‌شوند:

1
2
3
4
5
6
7
8
9
HTTP 1.x:
  SSE Stream 1 → اتصال TCP #1
  SSE Stream 2 → اتصال TCP #2
  SSE Stream 3 → اتصال TCP #3    ← ۳ اتصال از ۶ مصرف شد!

HTTP/2:
  SSE Stream 1 ─┐
  SSE Stream 2 ─┼─→ یک اتصال TCP
  SSE Stream 3 ─┘

نتیجه: SSE روی HTTP/2 می‌تواند کانال‌های Push نامحدودی داشته باشد - بدون هیچ هزینه اضافه‌ای.

✦ موارد استفاده مناسب SSE

SSE برای هر موردی که داده به صورت یک‌طرفه از سرور جریان دارد، انتخاب درستی است:

  • داشبوردهای Real-Time: قیمت سهام، متریک‌های سرور، آمار زنده
  • سیستم‌های Notification: پیام جدید، به‌روزرسانی وضعیت سفارش
  • Feed خبری: توییت، خوراک خبری، لاگ‌های زنده
  • پیشرفت عملیات طولانی: Progress Bar برای آپلود، پردازش، یا Import داده

قانون انتخاب که نویسنده بیان می‌کند:

اگر Client فقط نیاز به گوش دادن دارد - نه صحبت کردن - SSE انتخاب درست‌تری از WebSocket است. سادگی پروتکل، Reconnect خودکار، و سازگاری با HTTP/2 آن را برای این موارد برتر می‌کند.


فصل ۱۷ - WebSocket

✦ چارچوب فکری: وقتی HTTP کافی نیست

XHR و SSE هر دو روی HTTP ساخته شده‌اند - و HTTP یک پروتکل Request-Response است. یعنی همیشه Client باید اول بپرسد، یا سرور فقط می‌تواند Push کند. اما بسیاری از اپلیکیشن‌های مدرن نیاز به مکالمه واقعی دارند - Client و Server هر لحظه می‌توانند بدون هیچ درخواست قبلی، پیامی بفرستند. WebSocket دقیقاً برای این نیاز طراحی شده است.

✦ WebSocket API: ساده، اما قدرتمند

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ۱. برقراری اتصال
var ws = new WebSocket('wss://example.com/socket');
//                      ↑ wss = WebSocket over TLS

// ۲. رویداد باز شدن اتصال
ws.onopen = function() {
    console.log('اتصال برقرار شد');
    ws.send('سلام سرور!');         // ارسال پیام متنی
};

// ۳. دریافت پیام
ws.onmessage = function(event) {
    console.log('پیام دریافت شد:', event.data);
};

// ۴. مدیریت قطع اتصال
ws.onclose = function(event) {
    console.log('کد قطع:', event.code, event.reason);
};

// ۵. مدیریت خطا
ws.onerror = function(error) {
    console.error('خطا:', error);
};

برخلاف SSE که مرورگر Reconnect را خودکار مدیریت می‌کند، در WebSocket توسعه‌دهنده مسئول پیاده‌سازی منطق Reconnect است.

✦ ارسال متن و داده باینری

WebSocket هر دو نوع داده را پشتیبانی می‌کند:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ارسال متن
ws.send('یک پیام متنی');

// ارسال JSON
ws.send(JSON.stringify({ type: 'chat', msg: 'سلام' }));

// ارسال داده باینری (ArrayBuffer)
var buffer = new ArrayBuffer(128);
ws.send(buffer);

// ارسال Blob (مثلاً فایل تصویر)
var blob = new Blob(;
ws.send(blob);

// تنظیم نوع داده دریافتی
ws.binaryType = 'arraybuffer';  // یا 'blob'

نکته مهم: پیام‌های WebSocket Atomic هستند - یا کامل تحویل داده می‌شوند یا اصلاً تحویل داده نمی‌شوند. این با TCP متفاوت است که داده را به صورت جریان بایت‌های بدون مرز می‌بیند.

✦ Subprotocol Negotiation

WebSocket یک کانال خام است - هیچ قراری برای فرمت پیام‌ها وجود ندارد. برای اینکه Client و Server بدانند چه زبانی صحبت کنند، می‌توان در لحظه اتصال یک Subprotocol مذاکره کرد:

1
2
3
4
5
6
7
8
9
// Client پیشنهاد می‌دهد
var ws = new WebSocket('wss://example.com/chat',
    ['v1.chat.example.com', 'v2.chat.example.com']
);

// بررسی Subprotocol پذیرفته‌شده توسط سرور
ws.onopen = function() {
    console.log('پروتکل مورد توافق:', ws.protocol);
};

Server باید دقیقاً یکی از Subprotocolهای پیشنهادی را انتخاب کند - در غیر این صورت، اتصال بسته می‌شود.

✦ پروتکل WebSocket: زیر پوست

WebSocket با یک HTTP Upgrade Request آغاز می‌شود - این هوشمندانه‌ترین طراحی WebSocket است، چون با زیرساخت‌های موجود (Proxy، Firewall، Load Balancer روی پورت 80/443) سازگار است:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Client → Server:
  GET /chat HTTP/1.1
  Host: example.com
  Upgrade: websocket                        ← درخواست تبدیل پروتکل
  Connection: Upgrade
  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
  Sec-WebSocket-Version: 13
  Sec-WebSocket-Protocol: v1.chat.example.com

Server → Client:
  HTTP/1.1 101 Switching Protocols          ← پذیرش تبدیل
  Upgrade: websocket
  Connection: Upgrade
  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
  Sec-WebSocket-Protocol: v1.chat.example.com

بعد از دریافت 101 Switching Protocols، اتصال TCP دیگر HTTP نیست - یک کانال WebSocket دوطرفه است.

✦ Binary Framing Layer در WebSocket

هر پیام WebSocket به یک یا چند Frame تقسیم می‌شود. ساختار هر Frame:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| Opcode|M| Payload len |    Extended payload length    |
|I|S|S|S|  (4)  |A|     (7)    |             (16/64)           |
|N|V|V|V|       |S|             |   (if payload len==126/127)   |
| |1|2|3|       |K|             |                               |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - -+
|     Extended payload length continued, if payload len == 127  |
+ - - - - - - - - - - - - - - -+-------------------------------+
|                               | Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued)       |          Payload Data         |
+-------------------------------- - - - - - - - - - - - - - - -+

مهم‌ترین Opcodeها:

Opcodeمعنی
0x0Continuation Frame (ادامه پیام قبلی)
0x1Text Frame
0x2Binary Frame
0x8Connection Close
0x9Ping
0xAPong

نکته امنیتی مهم: تمام پیام‌های ارسالی از Client به سرور باید Mask شوند - یعنی با یک کلید ۴ بایتی XOR می‌شوند. این برای جلوگیری از حملات Cache-Poisoning روی Proxyهای قدیمی است. پیام‌های سرور به Client نیازی به Masking ندارند.

✦ Performance: سربار واقعی WebSocket

نویسنده با یک مقایسه عددی نشان می‌دهد که WebSocket چقدر کارآمدتر از XHR Polling است:

1
2
3
4
5
6
7
8
9
10
11
12
XHR Polling (هر ۱ ثانیه):
  Request Headers:  ~871 bytes
  Response Headers: ~512 bytes
  Payload:           ~20 bytes
  ─────────────────────────────
  سربار ۱ دقیقه:   ~83 KB (فقط برای Headers!)

WebSocket (همان ۶۰ پیام در دقیقه):
  اتصال اول:       ~258 bytes (Handshake)
  هر پیام:          2 bytes Header + 20 bytes Payload
  ─────────────────────────────
  سربار ۱ دقیقه:   ~1.3 KB

کاهش سربار: ~98% - این رقم در شبکه‌های موبایل تأثیر مستقیمی روی باتری و مصرف داده دارد.

✦ موارد استفاده مناسب و نامناسب

مناسب برای WebSocketنامناسب برای WebSocket
Chat و پیام‌رسانی Real-Timeدریافت Notification ساده (از SSE استفاده کنید)
بازی‌های آنلاین چندنفرهAPI Callهای معمولی (از XHR استفاده کنید)
همکاری Real-Time (مثل Google Docs)دانلود فایل
داشبوردهای دوطرفه (کنترل + مانیتور)اتصال‌های کوتاه‌مدت
ترمینال‌های Remoteمحیط‌هایی که Proxy WebSocket را مسدود می‌کند

✦ Performance Checklist فصل

نویسنده فصل را با یک Checklist عملی به پایان می‌رساند:

  • از WSS (WebSocket over TLS) استفاده کنید - علاوه بر امنیت، از مشکلات Proxy که WS را باز می‌کنند جلوگیری می‌کند
  • منطق Reconnect با Exponential Backoff پیاده‌سازی کنید - نه Reconnect فوری که سرور را تحت فشار می‌گذارد
  • Heartbeat / Ping-Pong پیاده‌سازی کنید تا اتصال‌های مرده را شناسایی کنید
  • در صورت امکان، از Binary Framing به جای JSON String استفاده کنید - سربار کمتر
  • Load Balancer باید Sticky Session یا Shared State داشته باشد - چون اتصال WebSocket طولانی‌مدت است

فصل ۱۸ - WebRTC

✦ چارچوب فکری: پایان دادن به نیاز به سرور میانی

تمام APIهایی که تا اینجا بررسی کردیم - XHR، SSE، WebSocket - یک ویژگی مشترک دارند: داده همیشه از سرور عبور می‌کند. اگر کاربر A بخواهد یک ویدیو با کاربر B برقرار کند، ویدیو ابتدا به سرور می‌رود و سپس به B می‌رسد. WebRTC این معادله را به هم می‌زند: دو مرورگر می‌توانند مستقیماً با هم ارتباط برقرار کنند - بدون سرور میانی.

✦ سه ستون اصلی WebRTC

نویسنده WebRTC را بر اساس سه قابلیت بنیادی تعریف می‌کند:

۱. MediaStream (getUserMedia): دسترسی به دوربین و میکروفون ۲. RTCPeerConnection: انتقال Audio و Video بین Peerها ۳. RTCDataChannel: انتقال داده دلخواه (متن، فایل، باینری) بین Peerها

این سه با هم یک پلتفرم کامل ارتباطی Peer-to-Peer می‌سازند که تماماً از داخل مرورگر قابل استفاده است.

✦ دسترسی به دوربین و میکروفون با getUserMedia

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// درخواست دسترسی به دوربین و میکروفون
navigator.mediaDevices.getUserMedia({
    audio: true,
    video: {
        width: { min: 640, ideal: 1280, max: 1920 },
        height: { min: 480, ideal: 720,  max: 1080 }
    }
})
.then(function(stream) {
    // نمایش ویدیو در یک <video> Element
    var videoEl = document.getElementById('localVideo');
    videoEl.srcObject = stream;
    videoEl.play();
})
.catch(function(err) {
    console.error('دسترسی رد شد:', err);
});

نکته امنیتی: مرورگر قبل از دادن دسترسی، از کاربر اجازه می‌گیرد. این مجوز فقط برای Secure Origin (HTTPS یا localhost) صادر می‌شود - یک صفحه HTTP معمولی نمی‌تواند به دوربین دسترسی داشته باشد.

✦ معماری RTCPeerConnection: پیچیده‌ترین API مرورگر

RTCPeerConnection پیچیده‌ترین API‌ای است که نویسنده در کل کتاب بررسی می‌کند. این پیچیدگی ذاتی نیست - بلکه نتیجه مستقیم مشکلاتی است که WebRTC باید حل کند:

مشکل اصلی: دو مرورگر در دنیای واقعی پشت NAT و Firewall قرار دارند. آن‌ها IP مستقیمی ندارند که بتوان به آن وصل شد.

1
2
3
4
5
6
7
کاربر A                    اینترنت                    کاربر B
┌──────┐     ┌──────┐                  ┌──────┐     ┌──────┐
│Chrome│────│Router│──────────────────│Router│────│Chrome│
└──────┘     │ NAT  │                  │ NAT  │     └──────┘
             └──────┘                  └──────┘
  IP داخلی: 192.168.1.5           IP داخلی: 10.0.0.12
  IP عمومی: 78.45.12.8            IP عمومی: 91.23.45.7

هیچ‌کدام IP داخلی دیگری را نمی‌دانند. WebRTC باید این مشکل را حل کند.

✦ ICE: Interactive Connectivity Establishment

ICE چارچوبی است که WebRTC برای پیدا کردن بهترین مسیر ارتباطی بین دو Peer استفاده می‌کند. ICE به ترتیب سه نوع Candidate را امتحان می‌کند:

1
2
3
4
5
6
7
8
9
10
اولویت ۱ - Host Candidate:
  IP مستقیم دستگاه → فقط در شبکه محلی کار می‌کند

اولویت ۲ - Server Reflexive Candidate (از طریق STUN):
  IP عمومی که STUN Server شناسایی کرده
  → در اکثر موارد NAT Traversal موفق می‌شود

اولویت ۳ - Relay Candidate (از طریق TURN):
  ترافیک از طریق TURN Server عبور می‌کند
  → همیشه کار می‌کند، اما گران‌ترین گزینه است

STUN Server فقط به دستگاه می‌گوید «IP عمومی‌ات چیست» - ترافیک از آن عبور نمی‌کند. TURN Server ترافیک واقعی را Relay می‌کند - بار شبکه روی سرور می‌افتد.

✦ Signaling: مذاکره‌ای که WebRTC خودش انجام نمی‌دهد

اینجاست که یکی از مهم‌ترین سوءتفاهم‌های WebRTC مطرح می‌شود: WebRTC پروتکل Signaling ندارد. یعنی برای اینکه دو Peer بتوانند به هم وصل شوند، باید از یک کانال خارجی برای تبادل اطلاعات اولیه استفاده کنند.

نویسنده این را با یک تمثیل توضیح می‌دهد: تصور کنید می‌خواهید با کسی تلفنی صحبت کنید - اما هیچ‌کدام شماره دیگری را ندارید. کسی باید این شماره را رد و بدل کند. در WebRTC این «کسی» یک Signaling Server است:

1
2
3
4
5
6
7
8
9
10
11
Peer A          Signaling Server          Peer B
  │                    │                    │
  │── SDP Offer ──────→│                    │
  │                    │── SDP Offer ──────→│
  │                    │←── SDP Answer ─────│
  │←── SDP Answer ─────│                    │
  │                    │                    │
  │── ICE Candidates ─→│                    │
  │                    │── ICE Candidates ─→│
  │                    │                    │
  │←────────── اتصال مستقیم P2P ───────────→│

Signaling می‌تواند از طریق WebSocket، XHR، یا حتی ایمیل انجام شود - WebRTC اهمیتی نمی‌دهد.

✦ SDP: Session Description Protocol

اطلاعاتی که در Signaling رد و بدل می‌شود، با فرمت SDP بیان می‌شود:

1
2
3
4
5
6
7
8
9
v=0
o=- 7614219274584779017 2 IN IP4 127.0.0.1
s=-
t=0 0
a=group:BUNDLE audio video       ← Audio و Video روی یک اتصال
m=audio 1 RTP/SAVPF 111 103 104  ← فرمت‌های Audio پشتیبانی‌شده
a=rtpmap:111 opus/48000/2        ← کدک Opus، 48kHz، Stereo
m=video 1 RTP/SAVPF 100 116 117  ← فرمت‌های Video
a=rtpmap:100 VP8/90000           ← کدک VP8

SDP یک فرمت متنی قدیمی است که نویسنده صریحاً آن را «نه زیبا» توصیف می‌کند - اما به دلیل سازگاری با اکوسیستم موجود، همچنان استفاده می‌شود.

✦ RTCDataChannel: WebSocket بین Peerها

DataChannel یکی از قدرتمندترین قابلیت‌های WebRTC است که اغلب نادیده گرفته می‌شود:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ایجاد DataChannel
var pc = new RTCPeerConnection(config);
var dc = pc.createDataChannel('myChannel', {
    ordered: true,        // ترتیب ارسال رعایت شود؟
    maxRetransmits: 30    // حداکثر تلاش مجدد
});

dc.onopen = function() {
    dc.send('سلام Peer!');
    dc.send(new ArrayBuffer(1024));   // باینری هم ارسال می‌شود
};

dc.onmessage = function(event) {
    console.log('دریافت شد:', event.data);
};

DataChannel روی SCTP (Stream Control Transmission Protocol) سوار می‌شود که روی DTLS (TLS برای UDP) قرار دارد:

1
2
3
4
5
6
7
DataChannel
    ↓
  SCTP
    ↓
  DTLS      ← رمزنگاری
    ↓
  UDP

مزیت کلیدی SCTP نسبت به TCP: می‌توان به صورت همزمان داشت:

  • ارسال Ordered (برای پیام‌های مهم)
  • ارسال Unordered (برای داده‌های Real-Time مثل موقعیت بازیکن)

✦ انتقال Audio و Video: موتور پنهان

وقتی از RTCPeerConnection برای ویدیوکنفرانس استفاده می‌کنید، WebRTC خودش یک موتور پردازش صدا و تصویر دارد:

پردازش Audio:

  • حذف Echo (Acoustic Echo Cancellation)
  • حذف نویز پس‌زمینه (Noise Suppression)
  • تنظیم خودکار صدا (Automatic Gain Control)
  • کدک Opus برای فشرده‌سازی

پردازش Video:

  • کدک‌های VP8/VP9 و H.264
  • تطبیق خودکار با پهنای باند (Adaptive Bitrate)
  • جبران Packet Loss

✦ امنیت: اجباری، نه اختیاری

یکی از مهم‌ترین طراحی‌های WebRTC این است که رمزنگاری اجباری است - نه اختیاری:

لایهپروتکلهدف
Audio/VideoSRTPرمزنگاری جریان Media
DataChannelDTLSرمزنگاری داده
SignalingHTTPS/WSSرمزنگاری مذاکره اولیه

هیچ پیاده‌سازی WebRTC مجاز نیست بدون رمزنگاری کار کند - این یک الزام مشخص در استاندارد است.

✦ Performance Checklist نهایی

نویسنده کتاب را با این توصیه‌های عملی به پایان می‌رساند:

  • TURN Server را حتماً پیاده‌سازی کنید - بدون آن، اتصال در شبکه‌های محدود (ادارات، دانشگاه‌ها) شکست می‌خورد
  • از Trickle ICE استفاده کنید - Candidateها را به محض کشف بفرستید، منتظر تکمیل نمانید تا Latency اتصال اول کاهش یابد
  • برای معماری Multi-Party (بیش از دو نفر)، به جای Mesh کامل (هر نفر به همه وصل می‌شود)، از SFU (Selective Forwarding Unit) استفاده کنید
  • DataChannel را برای هر داده‌ای که نیاز به Low-Latency دارد ترجیح دهید - نه از طریق سرور

✦ مقایسه نهایی چهار API مرورگر

ویژگیXHRSSEWebSocketWebRTC
جهتClient→ServerServer→ClientدوطرفهPeer-to-Peer
پروتکلHTTPHTTPWSSRTP/SCTP/DTLS
Media✗✗✗✅
Reconnect خودکار✗✅✗✗
بدون سرور✗✗✗✅ (بعد از Signaling)
پیچیدگیکمکممتوسطزیاد