ملاقات با TCP
قبل از اینکه وارد خود TCP بشیم، یه چیزی رو بگم. این مقاله قرار نیست RFC رو برات ترجمه کنه یا یه تعریف خشک از TCP ارائه بده. چیزی که میخوام انجام بدم، همون کاریه که چند وقت پیش خودم انجام دادم؛ اینکه برای اولین بار بشینم و واقعاً یه مکالمه TCP رو ببینم. نه روی اسلاید، نه توی کتاب، بلکه داخل Wireshark، همونطور که هر روز بین کامپیوترها رد و بدل میشه.
TCP: اولین مکالمهای که باید خوندنش رو یاد بگیری
زمان تقریبی مطالعه: ۱۰ دقیقه پیشنیاز: آشنایی مقدماتی با HTTP و Wireshark
یه سناریوی آشنا
تقریباً همه ما این صحنه رو تجربه کردیم. یه درخواست API میزنیم و جواب نمیگیریم، یه درخواست چند ثانیه معطل میمونه، یا Connection Pool دیتابیس پر میشه و هیچ ایدهای نداریم چرا. اولین واکنش معمولاً اینه که سراغ کد میریم؛ لاگها رو بررسی میکنیم، Breakpoint میذاریم، Queryها رو دوباره اجرا میکنیم و سعی میکنیم مشکل رو داخل اپلیکیشن پیدا کنیم.
اما خیلی وقتها مشکل اصلاً اونجا نیست.
گاهی اوقات همه چیز از لایهای پایینتر شروع شده؛ جایی که نه Framework ما ازش خبر داره و نه ORM قراره چیزی دربارهش بهمون بگه. لایهای که تقریباً تمام ارتباطات اینترنتی روی اون ساخته شدن و اسمش TCP هست.
قبل از اینکه HTTP بتونه اولین GET / خودش رو بفرسته، قبل از اینکه PostgreSQL اولین Query رو دریافت کنه، قبل از اینکه SSH اولین دستور رو اجرا کنه یا مرورگر اولین بایت HTML رو دانلود کنه، یه اتفاق دیگه باید افتاده باشه.
دو تا کامپیوتر باید اول با هم آشنا بشن. باید مطمئن بشن که هر دو آماده صحبت کردن هستن. باید روی یه مکالمه توافق کنن. تمام این اتفاقها قبل از رد و بدل شدن حتی یک بایت از داده اصلی انجام میشه و اسم همه این فرایند، TCP هست.
چرا اصلاً به TCP نیاز داریم؟
برای اینکه جواب این سؤال رو بفهمیم، بهتره اول یه نگاه به چیزی بندازیم که TCP روی اون ساخته شده؛ یعنی IP.
وقتی اسم Internet Protocol میاد، خیلیها تصور میکنن مسئول انتقال مطمئن اطلاعات بین دو کامپیوتره، اما واقعیت اینه که IP تقریباً هیچ تضمینی به ما نمیده. تنها کاری که انجام میده اینه که سعی میکنه یک Packet رو از یه مبدأ به یه مقصد برسونه.
IP قول نمیده که Packet حتماً برسه. قول نمیده که فقط یک بار برسه. قول نمیده که Packetها به همون ترتیبی که ارسال شدن دریافت بشن. حتی قول نمیده که اصلاً چیزی به مقصد برسه. به همین خاطر از IP با اصطلاح Best Effort Delivery یاد میکنن؛ یعنی «تمام تلاشم رو میکنم، ولی هیچ تضمینی وجود نداره.»
حالا یه لحظه تصور کن که هیچ چیزی به اسم TCP وجود نداشت و قرار بود مستقیماً روی IP برنامه بنویسی. فرض کن میخوای فقط یه درخواست HTTP بفرستی. از کجا میفهمی درخواستت واقعاً رسیده؟ اگر وسط راه گم شد باید چی کار کنی؟ اگر جواب نرسید، از کجا بفهمی سرور پاسخ نداده یا اصلاً درخواستت هیچوقت بهش نرسیده؟ اگر بخشی از داده رسید ولی ادامهش گم شد، از کجا متوجه بشی؟ اگر Packetها با ترتیب اشتباه رسیدن، باید خودت دوباره مرتبشون کنی؟ اگر بخوای یه فایل چند صد مگابایتی منتقل کنی، باید خودت تمام این منطق رو پیادهسازی کنی؟
واضحه که این کار عملاً غیرممکن میشه. هیچکس دوست نداره برای هر پروژه دوباره مکانیزم تشخیص خطا، ارسال مجدد، مرتبسازی دادهها و مدیریت ارتباط رو از صفر بنویسه.
دقیقاً همینجا بود که TCP به وجود اومد. TCP روی IP سوار میشه و کاری میکنه که انگار دو کامپیوتر یک ارتباط مستقیم، پایدار و قابل اعتماد با هم دارن؛ در حالی که زیر این لایه، هنوز هم همون IP غیرقابلاعتماد مشغول جابهجا کردن Packetهاست.
به بیان ساده، IP فقط نامهها رو جابهجا میکنه؛ اما TCP مطمئن میشه هیچ صفحهای از نامه گم نشده، ترتیب صفحهها به هم نریخته و طرف مقابل واقعاً همهشون رو دریافت کرده.
TCP چطور این قابلیت اطمینان رو ایجاد میکنه؟
وقتی برای اولین بار درباره TCP میخوندم، تصور میکردم پشت این قابلیت اطمینان، الگوریتمهای عجیب و پیچیدهای وجود داره. اما بعد از اینکه چند تا Capture واقعی داخل Wireshark دیدم، فهمیدم ایده اصلی خیلی سادهتر از چیزیه که به نظر میرسه.
TCP تقریباً همه چیز رو با شمردن انجام میده.
هر بایتی که داخل یک Connection رد و بدل میشه، شماره خودش رو داره. این شماره همون چیزیه که بهش Sequence Number میگیم.
در طرف مقابل، گیرنده بعد از دریافت داده اعلام میکنه که تا کدوم بایت رو گرفته و منتظر دریافت کدوم بایت بعدیه. این عدد همون Acknowledgment Number هست.
در ظاهر فقط با دو تا عدد طرفیم، اما تقریباً تمام قابلیت اطمینان TCP روی همین دو مفهوم بنا شده.
یه مثال ساده
فرض کن یه سرور میخواد یه فایل هزار بایتی برای یه کلاینت ارسال کنه. به جای اینکه همه فایل رو یکجا بفرسته، اون رو به چند بخش تقسیم میکنه. اولین Packet شامل بایتهای ۱ تا ۵۰۰ میشه. کلاینت بعد از دریافتش جواب میده:
بایتهای ۱ تا ۵۰۰ رسیدن؛ حالا منتظر بایت ۵۰۱ هستم.
سرور هم بخش دوم فایل، یعنی بایتهای ۵۰۱ تا ۱۰۰۰ رو ارسال میکنه. کلاینت دوباره تأیید میکنه که همه دادهها بدون مشکل دریافت شدن. تا اینجا همه چیز عادیه. اما فرض کن Packet دوم هیچوقت به مقصد نرسه. در این حالت، کلاینت همچنان اعلام میکنه که آخرین چیزی که دریافت کرده بایت ۵۰۰ بوده و هنوز منتظر بایت ۵۰۱ هسته. این یعنی سرور خیلی راحت متوجه میشه که بخشی از داده از بین رفته و باید همون قسمت رو دوباره ارسال کنه. به این فرایند Retransmission یا ارسال مجدد گفته میشه.
نکته جالب اینجاست که TCP لازم نیست کل فایل رو دوباره بفرسته. فقط همون بخشی که گم شده دوباره ارسال میشه و بقیه دادهها دستنخورده باقی میمونن. تمام این مکانیزم، با وجود اینکه در عمل پیچیدگیهای بیشتری هم داره، از همین ایده ساده شروع میشه؛ هر چیزی شماره داره و هر شمارهای باید تأیید بشه.
اگر فقط همین بخش رو به خاطر بسپری، نصف مسیر یادگیری TCP رو رفتی. تمام مفاهیمی مثل Retransmission، Flow Control، Sliding Window و حتی بخش زیادی از تحلیل ترافیک شبکه، در نهایت به همین ایده برمیگردن که TCP همیشه میدونه آخرین بایت دریافتشده کدوم بوده و بعد از اون منتظر چه شمارهایه.
از تئوری تا واقعیت
تا اینجا فقط درباره مفهوم TCP حرف زدیم. فهمیدیم که چرا وجود داره، چه مشکلی رو حل میکنه و چطور با شمارهگذاری دادهها یه ارتباط قابل اعتماد میسازه. اما هنوز یه سؤال مهم باقی مونده. این حرفها واقعاً روی سیم اتفاق میافتن یا فقط یه مدل ذهنی برای توضیح TCP هستن؟
اینجا همون جاییه که Wireshark وارد داستان میشه.
به جای اینکه صرفاً درباره Three-Way Handshake بخونیم، میخوایم خودمون اون رو ببینیم؛ دقیقاً همون Packetهایی که هر بار موقع باز کردن یه سایت، ارسال یه درخواست HTTP یا وصل شدن به دیتابیس بین دو کامپیوتر رد و بدل میشن. برای همین یه سرور خیلی ساده با Python اجرا میکنیم، یه درخواست با curl میفرستیم و بعد، قدمبهقدم اولین مکالمه TCP رو داخل Wireshark دنبال میکنیم. و جالبترین بخش ماجرا اینه که قبل از رد و بدل شدن حتی یک بایت از HTTP، سه Packet کوتاه بین کلاینت و سرور رد و بدل میشن؛ سه Packet که پایه تقریباً تمام ارتباطات اینترنتی امروز رو تشکیل میدن.
از اولین بایت HTTP تا نگاه امنیتی
تا اینجا فهمیدیم TCP چرا به وجود اومده و چطور با شمارهگذاری دادهها یه ارتباط قابل اعتماد ایجاد میکنه. اما هنوز همه اینها روی کاغذ بودن. حالا وقتشه چیزی رو ببینیم که باعث شد خودم برای اولین بار TCP رو واقعاً بفهمم.
به جای اینکه فقط درباره Three-Way Handshake بخونیم، میخوایم خودمون شاهد اتفاق افتادنش باشیم.
اولین مکالمه
برای اینکه همهچیز تا جای ممکن ساده باشه، یه سرور HTTP خیلی معمولی با Python اجرا میکنیم.
python3 -m http.server 9090
این دستور یه وبسرور ساده روی پورت 9090 راه میاندازه. هیچ Framework خاصی هم وجود نداره؛ فقط یه سرور کوچک که منتظر یه Connection میمونه.
حالا از یه ترمینال دیگه یه درخواست میفرستیم.
curl http://localhost:9090
اگر فقط خروجی curl رو ببینیم، داستان خیلی ساده به نظر میرسه؛ درخواست ارسال شده، سرور پاسخ داده و همهچیز تموم شده. اما Wireshark یه چیز دیگه نشونمون میده.
یه Capture جدید شروع میکنیم و فیلتر زیر رو اعمال میکنیم.
tcp.port == 9090
حالا فقط Packetهایی رو میبینیم که به همین ارتباط مربوط هستن. اولین چیزی که جلب توجه میکنه اینه که هنوز خبری از HTTP نیست.
هیچ GET /.
هیچ HTTP/1.1.
هیچ Header یا Body.
قبل از اینکه حتی یک بایت از درخواست HTTP ارسال بشه، سه Packet دیگه رد و بدل میشن.
Three-Way Handshake
سه Packet اول تقریباً همیشه همین شکلی هستن.
| Packet | مبدأ → مقصد | Flags | معنی |
|---|---|---|---|
| 1 | Client → Server | SYN | میخوام یه Connection باز کنم. |
| 2 | Server → Client | SYN, ACK | درخواستت رسید؛ منم آمادهام. |
| 3 | Client → Server | ACK | عالیه، ارتباط برقرار شد. |
به این سه مرحله TCP Three-Way Handshake میگن.
اگر بخوام خیلی غیررسمی توضیحش بدم، شبیه اینه که قبل از شروع یه مکالمه تلفنی، هر دو نفر مطمئن بشن که صدا به هم میرسه.
بعد از این سه جمله، تازه مکالمه اصلی شروع میشه. همین اتفاق توی TCP هم میافته. و نکته مهم اینه که تا وقتی این سه مرحله کامل نشه، هیچ دادهای رد و بدل نمیشه.
نه HTTP. نه SSH. نه PostgreSQL. نه Redis. نه تقریباً هیچ پروتکل TCP دیگهای.
همهشون اول باید این دست دادن اولیه رو انجام بدن.
sequenceDiagram
participant C as Client
participant S as Server
C->>S: SYN
S-->>C: SYN + ACK
C->>S: ACK
Note over C,S: Connection Established
حالا نوبت HTTP است
بعد از Handshake، تازه اولین داده واقعی ارسال میشه.
اگر روی Packet بعدی کلیک کنیم، داخل بخش Payload میبینیم که درخواست HTTP دقیقاً همونجا قرار گرفته.
GET / HTTP/1.1
Host: localhost:9090
User-Agent: curl/...
چیزی که بیشتر از خود درخواست توجه من رو جلب کرد، Payload نبود.
اعداد کنار Packet بودن.
Sequence Number: 1
Acknowledgment Number: 1
همون دو عددی که چند دقیقه قبل دربارهشون صحبت کردیم، حالا جلوی چشممون ظاهر شدن.
TCP واقعاً داره تکتک بایتهایی که ارسال میکنه رو شمارهگذاری میکنه.
Sequence Number واقعاً یعنی چی؟
فرض کنیم درخواست HTTP ما ۸۷ بایت حجم داشته باشه.
سرور بعد از دریافت اون پاسخ زیر رو ارسال میکنه.
Sequence Number: 1
Acknowledgment Number: 88
اولین باری که این عدد رو دیدم، برام عجیب بود. چرا ۸۸؟
بعد متوجه شدم معنی این عدد این نیست که «۸۸ بایت دریافت کردم».
معنیش اینه که:
من همه بایتها رو تا شماره ۸۷ دریافت کردم و الان منتظر بایت شماره ۸۸ هستم.
این تفاوت کوچیک، یکی از مهمترین چیزهاییه که باید درباره TCP بدونی. ACK آخرین بایت دریافتشده رو اعلام نمیکنه. ACK شماره بایت بعدی مورد انتظار رو اعلام میکنه. همین تفاوت کوچیک باعث میشه TCP بتونه خیلی راحت تشخیص بده کدوم قسمت از داده گم شده.
وقتی Packet گم میشه
حالا یه سؤال جالب. اگر وسط ارتباط یه Packet از بین بره چی؟
فرض کنیم سرور این دو Packet رو پشت سر هم ارسال کرده.
اما Packet دوم هیچوقت به مقصد نمیرسه.
کلاینت چه جوابی میده؟
آیا میگه:
تا هزار رو گرفتم؟
نه، و جوابش تقریباً این شکلیه.
من هنوز منتظر بایت ۵۰۱ هستم.
همین و TCP هیچ حدسی نمیزنه. هیچ تلاشی برای پر کردن جای خالی نمیکنه. فقط اعلام میکنه که هنوز همون قسمت قبلی آخرین داده معتبر بوده. سرور هم دقیقاً همون بخش گمشده رو دوباره ارسال میکنه.
به این مکانیزم Retransmission گفته میشه.
پایان مکالمه
بعد از اینکه HTTP Response کامل ارسال شد، هنوز یه مرحله باقی مونده. Connection باید بسته بشه. برخلاف چیزی که خیلیها تصور میکنن، بستن ارتباط هم مثل باز کردنش یه فرایند مشخص داره.
معمولاً این چهار Packet رو میبینیم.
Server → Client FIN, ACK
Client → Server ACK
Client → Server FIN, ACK
Server → Client ACK
این چهار مرحله یعنی:
Server:
من دیگه چیزی برای گفتن ندارم.
Client:
فهمیدم.
بعد Client هم اعلام میکنه:
منم حرفم تموم شده.
و در نهایت Server آخرین تأیید رو ارسال میکنه.
اگر Handshake اول شبیه دست دادن بود، این قسمت بیشتر شبیه یه خداحافظی مؤدبانه است؛ هر دو طرف مطمئن میشن که هیچ دادهای جا نمونده و بعد ارتباط بسته میشه.
sequenceDiagram
participant C as Client
participant S as Server
S-->>C: FIN
C->>S: ACK
C->>S: FIN
S-->>C: ACK
در نهایت، چیزی که داخل Wireshark میبینیم تقریباً همیشه چنین الگویی داره.
SYN
SYN-ACK
ACK
GET / HTTP/1.1
HTTP/1.0 200 OK
FIN
ACK
FIN
ACK
وقتی برای اولین بار این توالی رو دیدم، یه اتفاق جالب افتاد. دیگه TCP برام یه پروتکل انتزاعی نبود. قبلاً فقط اسم Three-Way Handshake رو حفظ کرده بودم و میدونستم سه تا Packet وجود داره، اما الان داشتم همون Packetها رو روی سیستم خودم میدیدم. دقیقاً همون چیزی که هر بار موقع باز کردن یه سایت یا اجرای یه API اتفاق میافته. از اون لحظه به بعد، هر وقت اسم TCP میاد، قبل از اینکه به RFC یا کتاب فکر کنم، همین مکالمه توی ذهنم ظاهر میشه.
دنبال کردن یک مکالمه در Wireshark
اگر بخوای خودت همین آزمایش رو انجام بدی، چند تا فیلتر خیلی بیشتر از بقیه به کارت میان.
نمایش ترافیک یک پورت
tcp.port == 9090
نمایش فقط یک Connection
tcp.stream == 0
اگر چند ارتباط همزمان داشته باشی، فقط کافیه شماره Stream رو تغییر بدی.
فقط Packetهای SYN
tcp.flags.syn == 1
فقط SYN-ACK
tcp.flags.syn == 1 and tcp.flags.ack == 1
فقط FIN
tcp.flags.fin == 1
Packetهایی که دوباره ارسال شدن
tcp.analysis.retransmission
اما قابلیتی که احتمالاً بیشتر از همه ازش استفاده میکنی، اصلاً فیلتر نیست. روی یکی از Packetها راستکلیک کن و مسیر زیر رو انتخاب کن.
Wireshark کل مکالمه رو، از اولین SYN تا آخرین FIN، کنار هم قرار میده و دیگه لازم نیست بین صدها Packet بالا و پایین بری. اگر تازه شروع کردی، به نظرم این قابلیت ارزشش از نصف فیلترهای Wireshark هم بیشتره.
چرا این برای امنیت مهم است؟
تا اینجا بیشتر از زاویه یک برنامهنویس به TCP نگاه کردیم، اما از همین نقطه به بعد کمکم وارد دنیای امنیت هم میشیم.
چند وقت پیش توی یکی از پستهای قبلی یه جمله نوشتم که هنوز هم بهش اعتقاد دارم:
امنیت سایبری، قبل از هر چیز، یعنی بلد بودن زبان مکالمه بین کامپیوترها.
TCP اولین زبانیه که باید یاد بگیری. دلیلش هم خیلی ساده است. تا وقتی ندونی یه ارتباط سالم چه شکلیه، هیچوقت نمیفهمی یه ارتباط غیرعادی چه شکلیه. همه تحلیلهای امنیتی از همین مفهوم شروع میشن؛ چیزی که معمولاً بهش Baseline یا الگوی طبیعی شبکه گفته میشه.
SYN Flood
یکی از معروفترین حملههای لایه TCP همین SYN Flood هست. توی این حمله، مهاجم مدام SYN ارسال میکنه اما هیچوقت مرحله آخر Handshake رو کامل نمیکنه. در نتیجه سرور مجبور میشه هزاران Connection نیمهکاره رو داخل حافظه نگه داره و بعد از یه مدت دیگه نتونه ارتباطهای واقعی رو قبول کنه. اگر داخل Wireshark تعداد زیادی Packet شبیه این ببینی،
tcp.flags.syn == 1
tcp.flags.ack == 0
و خبری از ACK نهایی نباشه، احتمالاً یه چیزی غیرعادی در حال رخ دادنه.
SYN Scan
یه مثال دیگه، Port Scan معروف nmap -sS هست. اینجا مهاجم اصلاً قصد برقراری ارتباط کامل رو نداره. فقط میخواد بفهمه کدوم پورتها باز هستن. برای همین یه SYN ارسال میکنه.
اگر SYN-ACK دریافت کنه، یعنی پورت بازه. اگر RST برگرده، یعنی بسته است.
و معمولاً قبل از اینکه Handshake کامل بشه، ارتباط رو رها میکنه. به همین خاطر بهش Half-Open Scan هم گفته میشه.
جالبترین بخش ماجرا اینه که هیچکدوم از این مفاهیم پیچیده نیستن. اگر Three-Way Handshake رو بشناسی، SYN Flood رو هم میفهمی.
اگر مفهوم ACK رو بفهمی، Retransmission رو هم درک میکنی.
اگر یه مکالمه طبیعی TCP رو دیده باشی، رفتار غیرطبیعی هم خیلی سریع به چشمت میاد.
همهچیز از همون سه Packet اول شروع میشه؛ سه Packetی که شاید هزاران بار از کنار اسمشون رد شده باشیم، اما تا وقتی با چشم خودمون نبینیمشون، بعیده واقعاً درکشون کنیم.
در بخش آخر، از دنیای Packetها فاصله میگیریم و دوباره برمیگردیم به چیزی که هر روز باهاش سر و کار داریم؛ یعنی برنامهنویسی. میبینیم Socketها، Connection Pool، Keep-Alive، netcat و حتی خیلی از تصمیمهایی که داخل بکاند میگیریم، همگی روی همین مکالمه سادهای بنا شدن که حالا بالاخره میدونیم پشت پردهش چه خبره.
TCP از چشم یک برنامهنویس
تا اینجا تقریباً تمام مسیر رو از نگاه شبکه طی کردیم. دیدیم TCP چرا به وجود اومده، چطور یه ارتباط قابل اعتماد ایجاد میکنه و داخل Wireshark دقیقاً چه شکلی به نظر میرسه. اما واقعیت اینه که بیشتر ما هر روز با TCP زندگی میکنیم، بدون اینکه حتی اسمش رو به زبون بیاریم.
اگر برنامهنویس بکاند باشی، احتمال خیلی زیادی وجود داره که امروز چندین بار از TCP استفاده کرده باشی، حتی اگر تمام روزت فقط بین IDE و ترمینال گذشته باشه.
جالب اینجاست که خیلی از ابزارهایی که برای ما کاملاً عادی شدن، در نهایت فقط راههای متفاوتی برای ساختن یا مدیریت یک TCP Connection هستن.
Socket Programming
اگر حتی یک بار با Socketها کار کرده باشی، احتمالاً این خط کد برات آشناست.
socket(AF_INET, SOCK_STREAM, 0);
وقتی اولین بار این کد رو دیدم، SOCK_STREAM فقط یه ثابت (Constant) بود که از روی آموزشها حفظش کرده بودم. بعدها فهمیدم همین یک کلمه داره یکی از مهمترین تصمیمهای ارتباطی برنامه رو مشخص میکنه.
SOCK_STREAM یعنی:
من یه ارتباط TCP میخوام.
ارتباطی که:
- Connection-Oriented باشه.
- دادهها به ترتیب برسن.
- چیزی گم نشه.
- اگر گم شد، دوباره ارسال بشه.
در مقابل، اگر به جای اون از SOCK_DGRAM استفاده کنی، دیگه وارد دنیای UDP شدی؛ جایی که سرعت بیشتره، اما خبری از تضمین تحویل، ترتیب دادهها یا ارسال مجدد نیست.
همین تفاوت کوچیک باعث میشه دو برنامه که از بیرون تقریباً شبیه هم به نظر میرسن، رفتار کاملاً متفاوتی روی شبکه داشته باشن.
هر بار که curl اجرا میکنی...
چند وقت پیش یه سؤال از خودم پرسیدم. وقتی این دستور رو اجرا میکنم،
curl https://example.com
واقعاً چه اتفاقی میافته؟ قبلاً جوابم این بود که «یه درخواست HTTP ارسال میشه.» امروز جوابم فرق کرده.
اول یه Connection TCP ساخته میشه. بعد Three-Way Handshake انجام میشه. اگر HTTPS باشه، بعد از اون TLS Handshake اتفاق میافته. و تازه بعد از همه این مراحل، اولین GET / ارسال میشه.
یعنی چیزی که ما به اسم «درخواست HTTP» میشناسیم، در واقع آخرین قسمت داستانه، نه اولین قسمت.
Keep-Alive؛ چرا هر بار از اول شروع نکنیم؟
احتمالاً بارها این Header رو دیدی.
Connection: keep-alive
شاید مثل من، مدتها فقط از کنارش رد شده باشی. اما حالا که TCP رو میشناسیم، معنی این Header خیلی منطقیتر به نظر میرسه. ساختن یک Connection رایگان نیست. هر بار که بخوای یه ارتباط جدید ایجاد کنی، باید Three-Way Handshake انجام بشه. یعنی حداقل سه Packet قبل از اینکه حتی اولین بایت HTTP ارسال بشه. اگر برای هر درخواست دوباره این فرایند تکرار بشه، مقدار قابل توجهی از زمان و پهنای باند صرف برقراری ارتباط میشه، نه انتقال داده. به همین دلیل HTTP/1.1 به صورت پیشفرض از Keep-Alive استفاده میکنه. به جای اینکه بعد از هر درخواست ارتباط بسته بشه، همون Connection باز میمونه تا درخواستهای بعدی هم از همون مسیر ارسال بشن. در ظاهر فقط یه Header ساده است، اما پشت اون تصمیمی قرار داره که مستقیماً روی عملکرد TCP تأثیر میذاره.
Connection Pool؛ چیزی که هر روز استفاده میکنیم
اگر با PostgreSQL، MySQL یا تقریباً هر دیتابیس مدرن دیگهای کار کرده باشی، احتمالاً اسم Connection Pool رو شنیدی. تا قبل از اینکه TCP رو بهتر بفهمم، Connection Pool برام فقط یه راه برای افزایش Performance بود. اما الان جور دیگهای بهش نگاه میکنم. هر Connection داخل Pool، در واقع یه Connection TCP بازه. وقتی برنامه تو به دیتابیس نیاز داره، لازم نیست دوباره Handshake انجام بده و ارتباط جدیدی بسازه. یکی از Connectionهای آماده رو برمیداره، Query رو ارسال میکنه و بعد دوباره همون Connection رو برمیگردونه داخل Pool. به همین سادگی.
ابزارهایی مثل HikariCP، PgBouncer یا حتی Poolهای داخلی زبانهای مختلف، در اصل دارن یه چیز مشترک رو مدیریت میکنن:
مدیریت تعداد زیادی TCP Connection که از قبل ساخته شدن.
سادهترین شکل TCP
اگر بخوام یه ابزار معرفی کنم که TCP رو بدون هیچ لایه اضافهای بهت نشون بده، احتمالاً انتخابم netcat باشه.
دو تا ترمینال باز کن.
روی یکی بنویس:
nc -lvp 9090
و روی اون یکی:
nc localhost 9090
همین. نه HTTP وجود داره. نه SSH. نه TLS. نه Header. نه هیچ پروتکل دیگهای.
هر چیزی که تایپ کنی، مستقیماً از طریق TCP به سمت مقابل ارسال میشه.
وقتی اولین بار این آزمایش رو انجام دادم، تازه فهمیدم خیلی از پروتکلهایی که هر روز استفاده میکنیم، در نهایت فقط یه قالب مشخص برای رد و بدل کردن داده روی TCP هستن. HTTP فقط یه زبان روی TCP هست. SSH یه زبان دیگه است. SMTP هم همینطوره. حتی پروتکل دیتابیس PostgreSQL هم در نهایت روی یه TCP Connection اجرا میشه. TCP خودش نمیدونه HTTP یعنی چی. نمیدونه Query یعنی چی. فقط یه جریان مرتب از بایتها رو جابهجا میکنه.
یه نگاه به RFC
اگر بخوای بدونی همه این رفتارها دقیقاً از کجا اومدن، جوابش داخل RFCهاست. استاندارد فعلی TCP توی RFC 9293 تعریف شده؛ سندی که نسخه مدرن RFC 793 محسوب میشه و تقریباً تمام رفتارهایی که توی این مقاله دربارهشون صحبت کردیم، داخل اون توضیح داده شدن.
اگر دوست داشته باشی از خود منبع بخونی، این بخشها ارزش شروع کردن دارن.
| بخش | موضوع |
|---|---|
| Section 3.4 | برقراری Connection |
| Section 3.5 | بستن Connection |
| Section 3.3 | Sequence Number و Acknowledgment |
| State Machine | وضعیتهای مختلف TCP |
با این حال، اگر بخوام تجربه شخصی خودم رو بگم، احتمالاً RFC بهترین نقطه شروع نیست.
اولین باری که رفتم سراغ RFC، بعد از چند صفحه احساس کردم بیشتر دارم با زبان رسمی سندها میجنگم تا اینکه TCP رو یاد بگیرم.
برای همین اگر تازه وارد این مسیر شدی، پیشنهاد میکنم اول مفهوم رو از کتاب یا آزمایش عملی یاد بگیری و بعد سراغ RFC بری. اون موقع میبینی خیلی از جملههایی که قبلاً نامفهوم بودن، ناگهان معنی پیدا میکنن.
چیزی که برای من تغییر کرد
چند ماه پیش، اگر ازم میپرسیدی TCP چیه، احتمالاً یه تعریف کتابی میدادم.
میگفتم:
یه پروتکل Connection-Oriented برای انتقال قابل اعتماد داده.
تعریف اشتباهی هم نبود. فقط هیچ تصویری پشتش وجود نداشت. امروز اگر همین سؤال رو ازم بپرسی، اولین چیزی که یادم میاد سه Packet ابتدای ارتباطه. بعد Sequence Numberها. بعد ACKها. بعد Retransmission. بعد FIN. دیگه TCP برای من یه مفهوم انتزاعی نیست. یه مکالمه است. یه مکالمهای که هر روز هزاران بار جلوی چشمم اتفاق میافته، فقط قبلاً بلد نبودم ببینمش.
به نظرم بخش بزرگی از یادگیری شبکه دقیقاً همین تغییر نگاهه. از حفظ کردن اسم پروتکلها، به دیدن رفتار واقعی اونها.
قدم بعدی
توی این پست فقط داخل یه ماشین بودیم. کلاینت و سرور هر دو روی localhost اجرا میشدن. Packetها تقریباً هیچ مسیری رو طی نمیکردن. اما اینترنت که فقط localhost نیست.
اگر همین الان یه درخواست برای یه سرور توی آمریکا بفرستی، Packet از کامپیوترت خارج میشه، از مودم رد میشه، وارد شبکه ISP میشه، از چندین Router مختلف عبور میکنه، شاید از چند کشور بگذره و در نهایت به مقصد برسه.
اما چه کسی تصمیم میگیره Packet از کدوم مسیر بره؟ اگر وسط راه یکی از لینکها قطع بشه، چه اتفاقی میافته؟ Routerها از کجا میفهمن Packet بعدی رو باید به کدوم سمت بفرستن؟
همه این سؤالها از اینجا به بعد وارد دنیای Routing میشن؛ جایی که اینترنت، از مجموعهای از کابلها و کامپیوترها، تبدیل میشه به یه شبکه عظیم که میلیاردها دستگاه رو به هم وصل کرده.
جمعبندی
وقتی نوشتن این مجموعه رو شروع کردم، هدفم فقط یاد گرفتن اسم پروتکلها نبود. چیزی که دنبالش بودم، این بود که بفهمم کامپیوترها واقعاً چطور با هم حرف میزنن.
TCP اولین مکالمه مهمی بود که یاد گرفتم بخونمش. حالا میدونم چرا IP به تنهایی کافی نیست. میدونم Sequence Number و Acknowledgment Number دقیقاً چه کاری انجام میدن. میتونم یه ارتباط TCP رو داخل Wireshark دنبال کنم و بفهمم هر Packet چه نقشی توی اون مکالمه داره. و مهمتر از همه، وقتی یه رفتار غیرعادی روی شبکه میبینم، یه تصویر ذهنی از حالت طبیعی اون ارتباط دارم؛ چون قبل از اینکه دنبال حمله بگردم، یاد گرفتم یه مکالمه سالم چه شکلیه. به نظرم این مهمترین چیزی بود که از TCP یاد گرفتم. نه اسم Flagها. نه شماره RFC. نه State Machine. بلکه این واقعیت که تقریباً هر کاری که امروز روی اینترنت انجام میدیم، از باز کردن یه صفحه وب گرفته تا اجرای یه Query روی دیتابیس، با یه مکالمه ساده شروع میشه.
سه Packet برای سلام. چند تا Packet برای حرف زدن. چهار Packet برای خداحافظی.
و تمام این اتفاقها معمولاً در چند میلیثانیه رخ میدن؛ اونقدر سریع که هیچوقت متوجهشون نمیشیم، مگر اینکه یه بار Wireshark رو باز کنیم و تصمیم بگیریم به جای استفاده از اینترنت، گفتوگوی بین کامپیوترها رو تماشا کنیم.
یه پیشنهاد قبل از اینکه این صفحه رو ببندی
اگر تا اینجا همراه من اومدی، فقط به خوندن مقاله اکتفا نکن. Wireshark رو باز کن. یه Capture جدید بگیر. بعد یه ترمینال باز کن و این دو دستور رو اجرا کن.
python3 -m http.server 9090
curl http://localhost:9090
حالا برگرد به Wireshark. اولین SYN رو پیدا کن. بعد SYN-ACK. بعد ACK. روی Packetها کلیک کن، Sequence Numberها رو ببین، ACKها رو دنبال کن و در آخر هم Follow → TCP Stream رو امتحان کن.
قول میدم از اون لحظه به بعد، هر بار که یه سایت باز میکنی یا یه API صدا میزنی، پشت اون درخواست، یه مکالمه رو هم ببینی که تا قبل از امروز اصلاً متوجه وجودش نبودی.
و به نظرم، این دقیقاً همون نقطهایه که یادگیری شبکه از حفظ کردن مفاهیم، تبدیل میشه به دیدن اونها.
دیدگاهها
اولین نفری باشید که دیدگاه میگذارد.