এই পর্বে যা শিখবেন
- ব্রাউজারে একটি ঠিকানা লিখে Enter চাপার পর থেকে পাতা দেখা পর্যন্ত ধাপে ধাপে কী ঘটে
- TCP আর UDP-র পার্থক্য, three-way handshake, আর কেন TCP-তে বার্তার সীমানা নিজেকে চিহ্নিত করতে হয়
- C-তে server ও client-এর socket ফাংশনগুলো কোন ক্রমে ডাকা হয় — আর প্রতিটি আসলে কী করে
- একসাথে হাজার হাজার client সামলানোর চারটি নকশা, আর C10K সমস্যার গল্প
- নেটওয়ার্ক কোডের নিরাপত্তা-ফাঁদ এবং AI-এর লেখা socket কোড যাচাইয়ের চেকলিস্ট
১ · ঠিকানা লাগবে — IP, port আর DNS
রাতে ঢাকার একটি মেসে বসে নুসরাত ফোনে একটি অনলাইন জাজের সাইট খুলল। এক সেকেন্ডের মধ্যে পাতা চলে এলো। অথচ সেই পাতা রাখা আছে হয়তো হাজার হাজার কিলোমিটার দূরের কোনো সার্ভারে। এই এক সেকেন্ডে তার ফোন আর ওই দূরের কম্পিউটার কয়েক ডজন বার্তা আদান-প্রদান করেছে। এই পর্বে আমরা সেই কথোপকথনের ভেতরে ঢুকব — এবং দেখব C প্রোগ্রাম কীভাবে নিজেই এমন কথোপকথনে অংশ নেয়।
প্রথমে একটি সহজ প্রশ্ন: চিঠি পাঠাতে কী লাগে? ঠিকানা। নেটওয়ার্ক-এও তাই। দুটি প্রোগ্রাম কথা বলতে চাইলে একে অপরকে খুঁজে পেতে হবে। এজন্য দুই স্তরের ঠিকানা আছে।
🏠 IP address — বাড়ির ঠিকানা
নেটওয়ার্কে প্রতিটি যন্ত্রের একটি নম্বর-ঠিকানা থাকে, যেমন 203.0.113.25. পুরোনো IPv4 ঠিকানা ৩২ বিটের — মোট প্রায় ৪৩০ কোটি ঠিকানা, যা পৃথিবীর যন্ত্রের তুলনায় কম। তাই নতুন IPv6 ঠিকানা ১২৮ বিটের।
🚪 Port — ফ্ল্যাট নম্বর
একটি কম্পিউটারে একসাথে অনেক প্রোগ্রাম নেটওয়ার্ক ব্যবহার করে। Port হলো ০ থেকে ৬৫৫৩৫-এর মধ্যে একটি নম্বর, যা বলে বার্তাটি ভেতরের কোন প্রোগ্রামের কাছে যাবে। ওয়েব সাধারণত ৮০ (HTTP) ও ৪৪৩ (HTTPS), SSH ২২, DNS ৫৩.
কোন প্রোগ্রাম?📒 DNS — ফোনবুক
মানুষ নম্বর মনে রাখতে পারে না, নাম মনে রাখে। codeforces.com লিখলে আপনার ফোন প্রথমে DNS সার্ভারকে জিজ্ঞেস করে, “এই নামের IP কত?” — ঠিক যেমন ফোনের contact list নাম থেকে নম্বর বের করে দেয়।
ধরুন আপনি ধানমন্ডির একটি বড় অ্যাপার্টমেন্ট ভবনে কুরিয়ার পাঠাবেন। শুধু “বাড়ি নং ২৭, রোড ৪” লিখলে কুরিয়ারের লোক ভবনে পৌঁছাবেন, কিন্তু কোন ফ্ল্যাটে দেবেন বুঝবেন না। “ফ্ল্যাট ৫বি” লিখলে তবেই ঠিক মানুষের হাতে যাবে। IP address হলো ভবনের ঠিকানা, port হলো ফ্ল্যাট নম্বর। আর আপনি যদি শুধু “রহিম সাহেবের বাসা” জানেন, তাহলে কারও কাছে জিজ্ঞেস করে আসল ঠিকানা জানতে হবে — সেই জিজ্ঞাসার কাজটাই DNS.
Client আর server — কে ডাকে, কে অপেক্ষা করে
প্রায় সব নেটওয়ার্ক যোগাযোগে দুটি ভূমিকা থাকে। Server হলো সেই প্রোগ্রাম, যে একটি নির্দিষ্ট port-এ বসে অপেক্ষা করে — যেমন ব্যাংকের কাউন্টারে বসে থাকা কর্মকর্তা। Client হলো সেই প্রোগ্রাম, যে নিজে উদ্যোগ নিয়ে যোগাযোগ শুরু করে — যেমন লাইনে দাঁড়ানো গ্রাহক। আপনার ব্রাউজার, bKash বা Nagad অ্যাপ, গেমের অ্যাপ — এরা client. যে কম্পিউটারে এদের তথ্য রাখা ও প্রক্রিয়া হয়, সেখানে চলে server.
একটি TCP সংযোগকে পুরোপুরি চেনা যায় চারটি জিনিস দিয়ে: client-এর IP, client-এর port, server-এর IP, server-এর port. Server-এর port সবসময় একই থাকে (যেমন ৪৪৩), কিন্তু প্রতিটি client-কে তার অপারেটিং সিস্টেম একটি অস্থায়ী port দেয়। তাই একই server, একই port-এ লাখো client-এর সংযোগ আলাদা করে চিনতে পারে।
২ · স্তরে স্তরে ইন্টারনেট — TCP বনাম UDP
নেটওয়ার্কে বার্তা পাঠানোর কাজটি অনেক বড়। তাই একে কয়েকটি স্তরে ভাগ করা হয়েছে। প্রতিটি স্তর শুধু নিজের কাজটুকু করে, আর ঠিক নিচের স্তরের ওপর ভরসা রাখে। প্রতিটি স্তরের নিয়মকানুনকে বলে protocol — দুই পক্ষ আগে থেকে যে ভাষা ও আচরণে একমত।
| স্তর | উদাহরণ | কাজ | ডাক বিভাগের সাথে তুলনা |
|---|---|---|---|
| Application | HTTP, DNS, SMTP | বার্তার অর্থ — কী চাই, কী দিলাম | চিঠির ভেতরের লেখা |
| Transport | TCP, UDP | কোন প্রোগ্রাম (port), নির্ভরযোগ্যতা, ক্রম | রেজিস্টার্ড ডাক নাকি সাধারণ ডাক |
| Network | IP | যন্ত্র থেকে যন্ত্রে পথ খুঁজে পৌঁছানো | খামের ওপর ঠিকানা, ডাকঘর থেকে ডাকঘরে বস্তা |
| Link / Physical | Wi-Fi, Ethernet, 4G, ফাইবার | এক যন্ত্র থেকে পাশের যন্ত্রে সংকেত | ডাকের ভ্যান, ট্রেন, রিকশা |
IP স্তর বার্তাকে ছোট ছোট টুকরো বা packet-এ ভাগ করে পাঠায়। একেকটি packet সাধারণত দেড় হাজার বাইটের মতো বড় হয়। পথে প্রতিটি router শুধু দেখে “এই packet কোন দিকে পাঠাব?” — বাংলাদেশ থেকে বিদেশের সার্ভারে যাওয়া packet হয়তো কক্সবাজার বা কুয়াকাটায় নামা সাবমেরিন কেবল ধরে সিঙ্গাপুর হয়ে যায়। IP কোনো নিশ্চয়তা দেয় না: packet হারাতে পারে, দুবার পৌঁছাতে পারে, আগেরটা পরে আসতে পারে।
📮 TCP — রেজিস্টার্ড ডাক, প্রাপ্তিস্বীকারসহ
প্রতিটি byte-কে নম্বর দেয়। প্রাপক “পেয়েছি” (ACK) না জানালে আবার পাঠায়। ক্রম উল্টে গেলে সাজিয়ে দেয়। প্রাপক ধীর হলে পাঠানো কমায়। ফল: যা পাঠালেন, ঠিক সেই byte-গুলো, সেই ক্রমেই পৌঁছায় — নয়তো সংযোগ ভেঙে গেছে বলে জানায়।
ওয়েব, ইমেইল, ব্যাংকিং অ্যাপ, ফাইল ডাউনলোড📢 UDP — মাইকিং
একেকটি বার্তা (datagram) আলাদা করে ছুড়ে দেয় — কেউ পেল কি না খোঁজ নেয় না, আবার পাঠায় না, ক্রমও দেখে না। কিন্তু সংযোগ তৈরির ঝামেলা নেই, তাই খুব দ্রুত। একটা packet হারালে যেখানে আবার পাঠানোর চেয়ে এগিয়ে যাওয়া ভালো, সেখানে UDP.
ভিডিও কল, লাইভ স্ট্রিম, অনলাইন গেম, DNSরেডিওতে ক্রিকেটের লাইভ ধারাভাষ্য শুনছেন। মাঝখানে এক সেকেন্ড শব্দ কেটে গেল। আপনি কি চাইবেন ধারাভাষ্যকার থেমে গিয়ে ওই এক সেকেন্ড আবার বলুন? না — খেলা এগিয়ে গেছে, পুরোনো কথা এখন অর্থহীন। এটাই UDP-র জগৎ। কিন্তু bKash-এ টাকা পাঠানোর বার্তার একটি অঙ্কও হারানো চলবে না, দেরি হলেও সঠিক হতে হবে — এটাই TCP-র জগৎ।
১৯৭৪ সালে Vint Cerf ও Bob Kahn একটি গবেষণাপত্রে দেখান কীভাবে আলাদা আলাদা নেটওয়ার্ককে একটি সাধারণ protocol দিয়ে জোড়া যায় — সেখান থেকেই TCP/IP-র জন্ম। ১৯৮৩ সালের ১ জানুয়ারি মার্কিন গবেষণা নেটওয়ার্ক ARPANET পুরোপুরি TCP/IP-তে চলে আসে; অনেকে এই দিনটিকেই ইন্টারনেটের জন্মদিন বলেন। একই বছর University of California, Berkeley-র Unix সংস্করণ 4.2BSD প্রোগ্রামারদের জন্য একটি C interface দেয় — socket API. চল্লিশ বছর পরেও Linux, macOS, Android, এমনকি Windows-এর নেটওয়ার্ক প্রোগ্রামিং সেই নকশার ওপরই দাঁড়িয়ে।
এবার সব একসাথে দেখি। নুসরাত ব্রাউজারে ঠিকানা লিখে Enter চাপল। নিচের ডায়াগ্রামে “পরে” চাপুন — প্রতিটি ধাপে তারের ওপর দিয়ে আসলে কী যাচ্ছে, ডান দিকের প্যানেলে দেখা যাবে:
- DNS
নাম → IPফোনবুক দেখা - TCP সংযোগ
SYN / ACKহ্যান্ডশেক - TLS
HTTPSতালা লাগানো - অনুরোধ
GET /যা চাই - উত্তর
200 OKপাতা এলো - বন্ধ
FINবিদায়
ফোন → DNS সার্ভার : codeforces.com-এর IP কত? DNS সার্ভার → ফোন : codeforces.com = 203.0.113.10 (উত্তর কিছুক্ষণ cache-এ থাকে — পরের বার আর জিজ্ঞেস করতে হয় না)
client → server : SYN "কথা বলতে চাই, আমার শুরুর নম্বর x" server → client : SYN-ACK "রাজি, x পেয়েছি; আমার শুরুর নম্বর y" client → server : ACK "y পেয়েছি — শুরু করা যাক"
client → server : আমি এই এই encryption পদ্ধতি জানি server → client : এই নাও আমার certificate (আমি সত্যিই codeforces.com) দুপক্ষ : গোপন চাবি ঠিক হলো — পথের কেউ আর পড়তে পারবে না
GET / HTTP/1.1 Host: codeforces.com User-Agent: Mozilla/5.0 ... Accept: text/html (একটি খালি লাইন = header শেষ)
HTTP/1.1 200 OK Content-Type: text/html; charset=UTF-8 Content-Length: 48213 <!DOCTYPE html><html> ... (পাতার আসল লেখা)
এক পক্ষ → অন্য পক্ষ : FIN "আমার আর পাঠানোর কিছু নেই" অন্য পক্ষ → এক পক্ষ : ACK, তারপর নিজের FIN এক পক্ষ → অন্য পক্ষ : ACK — সংযোগ শেষ
- ধাপ ১ — DNS: নাম থেকে নম্বর। ব্রাউজার
codeforces.comনামটি বোঝে না, তার দরকার IP. তাই প্রথমে DNS সার্ভারকে জিজ্ঞেস করে। এই ছোট প্রশ্ন-উত্তর সাধারণত UDP-তে যায় — একটা প্রশ্ন, একটা উত্তর, সংযোগ তৈরির ঝামেলা নেই। (এখানে দেখানো IP শুধু উদাহরণ।) - ধাপ ২ — TCP সংযোগ তৈরি। IP পেয়ে ব্রাউজার server-এর port ৪৪৩-এ TCP সংযোগ চায়। তিনটি ছোট বার্তা — three-way handshake — দিয়ে দুপক্ষ নিশ্চিত হয় যে দুজনেই শুনতে ও বলতে পারছে, আর কোন নম্বর থেকে byte গোনা শুরু হবে।
- ধাপ ৩ — TLS: তালা লাগানো। HTTPS-এ এরপর TLS handshake হয়। Server একটি certificate দেখিয়ে প্রমাণ করে সে আসল সাইট, তারপর দুপক্ষ একটি গোপন চাবিতে একমত হয়। এরপর থেকে পথের Wi-Fi মালিক বা অন্য কেউ ডেটা পড়তে বা বদলাতে পারে না।
- ধাপ ৪ — HTTP অনুরোধ। এবার ব্রাউজার আসল অনুরোধটি পাঠায় — দেখুন, এটি সাধারণ ইংরেজি লেখা! “
/পাতাটি দাও, HTTP 1.1 নিয়মে, এই host-এর।” TCP-র কাছে এটি শুধু কিছু byte; এর অর্থ বোঝে শুধু server প্রোগ্রাম। - ধাপ ৫ — উত্তর। Server প্রথমে একটি status line পাঠায় (
200 OKমানে সফল), তারপর কয়েকটি header, একটি খালি লাইন, তারপর আসল পাতা।Content-Lengthবলে দেয় body কত byte — একটু পরেই দেখবেন কেন এটি এত জরুরি। - ধাপ ৬ — সংযোগ বন্ধ। কাজ শেষে দুপক্ষ FIN বার্তা দিয়ে নিজ নিজ দিক বন্ধ করে। (বাস্তবে ব্রাউজার সংযোগটি কিছুক্ষণ খোলা রাখে, পরের অনুরোধের জন্য।) এই পুরো যাত্রা সাধারণত এক সেকেন্ডেরও কম সময়ে শেষ হয়।
৩ · TCP-র ভেতরে — হ্যান্ডশেক আর byte-এর স্রোত
Handshake কেন তিন ধাপের? ফোনে কথা বলার কথা ভাবুন। আপনি বললেন, “হ্যালো, শুনতে পাচ্ছেন?” ওপাশ বলল, “হ্যাঁ, শুনছি — আপনি আমাকে শুনছেন?” আপনি বললেন, “হ্যাঁ।” এখন দুজনেই নিশ্চিত যে দুই দিকের লাইনই কাজ করছে। দুই ধাপে থামলে ওপাশের মানুষটি জানতেন না যে তাঁর কথা আপনার কাছে পৌঁছেছে। TCP ঠিক এই যুক্তিতে SYN, SYN-ACK, ACK — তিনটি বার্তা পাঠায়। সাথে দুপক্ষ নিজের byte গোনার শুরুর নম্বরও জানিয়ে দেয়, যাতে পরে হারানো বা উল্টো ক্রমের byte চেনা যায়।
একটি সূক্ষ্ম কিন্তু জরুরি তথ্য: handshake করে আপনার প্রোগ্রাম নয়, অপারেটিং সিস্টেমের kernel. Server প্রোগ্রাম শুধু বলে রাখে “এই port-এ সংযোগ নিতে রাজি”, বাকিটা kernel সামলায়। তৈরি হওয়া সংযোগগুলো একটি সারিতে জমা থাকে, প্রোগ্রাম সময়মতো সেখান থেকে একটি একটি করে তুলে নেয়। এই কথাটি একটু পরে আমাদের প্রথম কোডে কাজে লাগবে।
সবচেয়ে বড় ভুল ধারণা: “একবার পাঠালাম, একবারেই পাবে”
TCP আপনাকে দেয় একটি byte stream — পানির পাইপের মতো একটানা byte-এর স্রোত। আপনি এক বালতি পানি ঢাললেন, তারপর আরেক বালতি। ওপাশের মানুষ কল খুলে যা পান, তা একটানা পানি — কোন বালতি কোথায় শেষ, পানিতে লেখা থাকে না। একইভাবে, আপনি দুবার send করে “HELLO” আর “WORLD” পাঠালে অন্য পাশে recv হয়তো পাবে “HEL”, তারপর “LOWOR”, তারপর “LD”. এটি TCP-র ভুল নয় — এটাই TCP-র প্রতিশ্রুতি: byte ঠিক, ক্রম ঠিক, কিন্তু বার্তার সীমানা নেই।
কেন এমন হয়? পথে packet-এর আকার আলাদা, kernel-এর buffer কখন কতটুকু ভরা, প্রাপক প্রোগ্রাম কখন recv ডাকল — সব মিলিয়ে ভাগ যেকোনোভাবে হতে পারে। নিজের কম্পিউটারে ছোট বার্তা প্রায় সবসময় একবারেই আসে, তাই ভুলটা ধরা পড়ে আসল নেটওয়ার্কে — ব্যবহারকারীর হাতে। নিচে ধাপে ধাপে দেখুন:
| প্রেরক send() করেছে | |
| তারের ওপর (সীমানাহীন) | |
| এই recv() পেল | |
| প্রাপকের জমানো buffer |
- প্রেরক দুবার পাঠাল। প্রথমে
send("HELLO"), তারপরsend("WORLD"). প্রেরকের মাথায় এগুলো দুটি আলাদা বার্তা। কিন্তু kernel এগুলোকে একটানা ১০টি byte হিসেবে স্রোতে ঢেলে দিল —HELLOWORLD. কোথায় একটা শেষ, অন্যটা শুরু — সেই তথ্য আর কোথাও নেই। - প্রথম
recvপেল মাত্র ৩টি byte. নেটওয়ার্কে প্রথম packet-এ হয়তো এটুকুই এসেছিল। যে প্রোগ্রাম ধরে নেয় “একটি recv = একটি বার্তা”, সে এখনইHEL-কে একটি পুরো বার্তা ভেবে কাজ শুরু করে দিল — প্রথম ভুল। - দ্বিতীয়
recvপেল ৫টি byte — দুই বার্তার টুকরো মিশে।LOWOR— প্রথম বার্তার শেষ অংশ আর দ্বিতীয়টির শুরু একসাথে। সরল প্রোগ্রাম এটাকেও একটি আলাদা বার্তা ভাবল। - তৃতীয়
recvপেল বাকি ২টি byte. সব byte ঠিকঠাক, সঠিক ক্রমে এসেছে — TCP তার কথা রেখেছে। তবু প্রোগ্রাম তিনটি ভুল বার্তা ছাপল। আর জমানো buffer-এHELLOWORLDদেখেও কেউ বলতে পারবে না প্রথম বার্তা ৫ byte-এর ছিল, নাকি ৩ বা ৭ byte-এর। দোষ TCP-র নয়, আমাদের — আমরা সীমানার কথা বলিনি। - সমাধান: length prefix. এবার প্রেরক প্রতিটি বার্তার আগে তার দৈর্ঘ্য লিখে দিল —
[5]HELLO. প্রাপক আগে দৈর্ঘ্যটি পড়ে, তারপর জমাতে থাকে। মাত্র ৩টি byte এসেছে, দরকার ৫টি — তাই সে অপেক্ষা করে, অর্ধেক বার্তা নিয়ে কোনো কাজ করে না। - পুরো বার্তা জমলে তবেই কাজ। পরের
recv-এ বাকি byte এলো। এখন buffer-এ পুরোHELLOআছে — সেটি তুলে নিয়ে কাজ করল, তারপর[5]WORLD-ও পুরোটা পেয়ে কাজ করল। TCP-র ভাগ যেভাবেই হোক, ফল সবসময় সঠিক। এটাই message framing.
Framing-এর দুটি প্রচলিত উপায় আছে। প্রথমটি length prefix — বার্তার আগে নির্দিষ্ট কয়েক byte-এ দৈর্ঘ্য লেখা; বেশিরভাগ binary protocol (যেমন ডেটাবেসের protocol) এটি ব্যবহার করে। দ্বিতীয়টি delimiter — একটি বিশেষ চিহ্ন দিয়ে বার্তা শেষ করা, যেমন newline; HTTP-র header-গুলো এভাবেই লাইনে লাইনে আলাদা, আর body-র দৈর্ঘ্য বলে দেয় Content-Length. Delimiter সহজ, কিন্তু বার্তার ভেতরেই ওই চিহ্ন থাকলে বিপদ — তখন escape করতে হয়।
TCP কথা দেয় না: আপনার বার্তা যে আকারে পাঠালেন, সেই আকারে পৌঁছাবে।
৪ · Socket API — নেটওয়ার্কও একটা ফাইল
পর্ব ৪৮-এ দেখেছিলেন, Unix-এ খোলা ফাইল, pipe, terminal — সবকিছুকে প্রোগ্রাম চেনে একটি ছোট পূর্ণসংখ্যা দিয়ে, যার নাম file descriptor. নেটওয়ার্ক সংযোগও তার ব্যতিক্রম নয়। Socket হলো নেটওয়ার্কে যোগাযোগের একটি প্রান্ত, আর socket() ফাংশন আপনাকে ফেরত দেয় ঠিক এমন একটি file descriptor. এরপর সেখানে লেখা মানে অন্য কম্পিউটারে পাঠানো, পড়া মানে অন্য কম্পিউটার থেকে আসা byte হাতে পাওয়া। প্রতিটি socket ফাংশন আসলে kernel-এর কাছে একটি system call.
তবে server আর client-এর কাজের ধারা আলাদা। নিচের ডায়াগ্রামে বাঁয়ে server, ডানে client, মাঝখানে তারের ওপর দিয়ে যাওয়া বার্তা। প্রতিটি ধাপে লাল দাগের ফাংশন ও বার্তাগুলো দেখুন:
socket()— দুপক্ষই একটি প্রান্ত চায়।socket(AF_INET, SOCK_STREAM, 0)মানে “IPv4 ঠিকানা, আর stream অর্থাৎ TCP.” UDP চাইলেSOCK_DGRAM. ফেরত আসে একটি file descriptor, যেমন3. ব্যর্থ হলে-1— তাই সবসময় যাচাই করতে হয়।bind()— server একটি port নিজের নামে নেয়। “এই কম্পিউটারের port ৮০৮০-তে যা আসবে, তা আমার।” bind হলো দোকানের গায়ে সাইনবোর্ড লাগানো। অন্য কোনো প্রোগ্রাম আগেই port-টি নিয়ে থাকলে এখানে ব্যর্থ হয় (EADDRINUSE). Client সাধারণত bind করে না — kernel তাকে একটি অস্থায়ী port দিয়ে দেয়।listen()— দরজা খোলা। listen socket-টিকে “শোনার” অবস্থায় নেয়: এখন থেকে kernel এই port-এ আসা সংযোগের অনুরোধ গ্রহণ করে handshake সেরে একটি অপেক্ষার সারিতে রাখবে। সারির দৈর্ঘ্য (backlog) আপনি বলে দেন।connect()আরaccept()— সংযোগ তৈরি। Client-এর connect হ্যান্ডশেক শুরু করে। Kernel হ্যান্ডশেক শেষ করে সংযোগটি সারিতে রাখে। Server-এর accept সারি থেকে একটি সংযোগ তুলে নতুন একটি file descriptor দেয় — শুধু এই client-এর সাথে কথা বলার জন্য। আগের listening socket আগের মতোই দরজায় থাকে, পরের অতিথির জন্য।send()→recv()— client-এর অনুরোধ। Client byte পাঠায়, server পড়ে। মনে রাখুন আগের অংশের কথা:recvহয়তো অর্ধেক বার্তা দেবে।recv0ফেরত দিলে বুঝবেন ওপাশ সংযোগ বন্ধ করেছে;-1মানে error.send()→recv()— server-এর উত্তর। এবার দিক উল্টো। TCP দুই দিকেই একসাথে চলতে পারে — যেন দুই লেনের রাস্তা।send-ও সব byte একবারে না পাঠিয়ে কম পাঠাতে পারে; ফেরত দেওয়া সংখ্যাটি দেখে বাকিটা আবার পাঠাতে হয়।close()— বিদায়। দুপক্ষ file descriptor বন্ধ করে, kernel FIN বার্তা পাঠায়। Server-এর listening socket কিন্তু খোলাই থাকে — সে আবারaccept-এ ফিরে পরের client-এর অপেক্ষা করে। File descriptor বন্ধ করতে ভুলে গেলে সেটি হারিয়ে যায় (fd leak), আর এক সময় server নতুন সংযোগই নিতে পারে না।
একটি ভালো রেস্তোরাঁর দরজায় একজন অভ্যর্থনাকারী দাঁড়িয়ে থাকেন। অতিথি এলে তিনি তাঁকে একটি টেবিলে বসিয়ে একজন ওয়েটারকে দায়িত্ব দেন, তারপর নিজে আবার দরজায় ফিরে যান। Listening socket হলো সেই অভ্যর্থনাকারী; accept-এর দেওয়া নতুন socket হলো টেবিলের ওয়েটার। অভ্যর্থনাকারী কখনো খাবার পরিবেশন করেন না, আর ওয়েটার কখনো দরজায় দাঁড়ান না।
এবার আসল কোড। নিচের প্রোগ্রামে একই প্রোগ্রামের ভেতরে একটি server আর একটি client তৈরি হয়েছে। Server বসছে loopback ঠিকানা 127.0.0.1-এ — অর্থাৎ “এই কম্পিউটারই”, বাইরের নেটওয়ার্ক লাগে না। Port হিসেবে দেওয়া হয়েছে ০, যার মানে “OS নিজেই একটা খালি port বেছে দেবে”:
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
int main(void) {
struct sockaddr_in a = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) };
socklen_t len = sizeof a; /* port 0: OS নিজেই একটা খালি port দেবে */
int srv = socket(AF_INET, SOCK_STREAM, 0), cli = socket(AF_INET, SOCK_STREAM, 0);
if (srv < 0 || cli < 0 || bind(srv, (struct sockaddr *)&a, len) < 0 || listen(srv, 4) < 0) { perror("server"); return 1; }
getsockname(srv, (struct sockaddr *)&a, &len); /* কোন port পেলাম, জেনে নিই */
if (connect(cli, (struct sockaddr *)&a, len) < 0) { perror("connect"); return 1; }
int conn = accept(srv, NULL, NULL); /* handshake kernel আগেই সেরে রেখেছে */
char buf[32];
send(cli, "HELLO", 5, 0); /* client -> server */
ssize_t n = recv(conn, buf, sizeof buf, 0);
printf("server got %zd bytes: %.*s\n", n, (int)n, buf);
send(conn, "OK", 2, 0); /* server -> client */
n = recv(cli, buf, sizeof buf, 0);
printf("client got %zd bytes: %.*s\n", n, (int)n, buf);
close(conn); close(cli); close(srv);
return 0;
}লক্ষ করুন, connect সফল হয়েছে আগে, accept ডাকা হয়েছে পরে — তবু কাজ হলো। কারণ handshake করেছে kernel, প্রোগ্রাম নয়; listen-এর পর থেকেই kernel সংযোগ নিয়ে সারিতে রাখছিল। তিনটি file descriptor তৈরি হয়েছে: srv (অভ্যর্থনাকারী), conn (server পাশের ওয়েটার), cli (client-এর প্রান্ত)। %.*s দিয়ে ঠিক nটি byte ছাপা হয়েছে, কারণ নেটওয়ার্ক থেকে আসা ডেটার শেষে '\0' থাকে না — এটি C string নয়, শুধু byte. ছোট বার্তা নিজের কম্পিউটারে একবারেই এসেছে; আসল নেটওয়ার্কে recv-কে লুপে ডাকতে হয়।
৫ · Byte-এর ক্রম — htons কেন লাগে
পর্ব ২২ (union) আর পর্ব ২৪ (binary ফাইল)-এ আপনি endianness-এর কথা দেখেছেন: একাধিক byte-এর সংখ্যা মেমরিতে কোন ক্রমে রাখা হয়। আপনার ল্যাপটপের Intel বা AMD প্রসেসর, আর ফোনের ARM প্রসেসরও সাধারণত little-endian — ছোট ঘরের byte আগে। কিন্তু ইন্টারনেটের protocol-গুলো ঠিক করেছে, তারের ওপর সংখ্যা যাবে big-endian ক্রমে — বড় ঘরের byte আগে। একে বলে network byte order.
কেন একটা নির্দিষ্ট নিয়ম লাগে? ধরুন একজন লেখেন তারিখ “দিন/মাস/বছর”, আরেকজন পড়েন “মাস/দিন/বছর”। ০৩/০৪ মানে একজনের কাছে ৩ এপ্রিল, আরেকজনের কাছে ৪ মার্চ! নেটওয়ার্কে দুই প্রান্তের যন্ত্র আলাদা ধরনের হতে পারে, তাই সবাই একটি সাধারণ ক্রমে একমত। প্রতিটি যন্ত্র পাঠানোর আগে নিজের ক্রম থেকে নেটওয়ার্ক ক্রমে বদলায়, পাওয়ার পর আবার ফিরিয়ে আনে। এর জন্য চারটি ছোট ফাংশন:
| ফাংশন | নামের অর্থ | কোথায় লাগে |
|---|---|---|
htons(x) | host to network, short (১৬ বিট) | port নম্বর বসানোর সময় |
htonl(x) | host to network, long (৩২ বিট) | IPv4 ঠিকানা, নিজের protocol-এর দৈর্ঘ্য |
ntohs(x), ntohl(x) | network to host — উল্টো দিকে | পাওয়া সংখ্যা পড়ার সময় |
নিচের প্রোগ্রামটি দুটি কাজ করে। প্রথমে দেখায় ৮০৮০ সংখ্যাটি মেমরিতে আর তারের ক্রমে কেমন দেখায়। তারপর আগের stepper-এর length-prefix framing হাতে-কলমে করে — একটি buffer-এ দুটি পুরো বার্তা আর একটি অসম্পূর্ণ বার্তা জোড়া লাগানো আছে, দৈর্ঘ্য লেখা দুই byte-এ, big-endian ক্রমে:
#include <stdio.h>
#include <arpa/inet.h>
int main(void) {
unsigned short port = 8080, net = htons(port);
unsigned char *m = (unsigned char *)&port, *w = (unsigned char *)&net;
printf("8080 = 0x%04x | memory: %02x %02x | wire: %02x %02x\n", port, m[0], m[1], w[0], w[1]);
/* stream: [length hi][length lo][data] ... two messages glued together */
unsigned char s[] = {0, 5, 'H', 'E', 'L', 'L', 'O', 0, 5, 'W', 'O', 'R', 'L', 'D', 0, 9, 'B'};
size_t pos = 0;
while (pos + 2 <= sizeof s) {
size_t len = (size_t)(s[pos] << 8 | s[pos + 1]);
if (pos + 2 + len > sizeof s) { /* অর্ধেক এসেছে — আরও ডেটার অপেক্ষা */
printf("incomplete: need %zu bytes, have %zu\n", len, sizeof s - pos - 2);
break;
}
printf("message: %.*s\n", (int)len, (const char *)s + pos + 2);
pos += 2 + len;
}
return 0;
}প্রথম লাইনে দেখবেন memory: 90 1f কিন্তু wire: 1f 90 — x86 প্রসেসরে ৮০৮০ (hex 0x1f90) মেমরিতে উল্টো ক্রমে থাকে, আর htons তাকে নেটওয়ার্কের ক্রমে সোজা করে দিয়েছে। Big-endian যন্ত্রে htons কিছুই করে না, কিন্তু তবুও লিখতে হয় — তাহলে একই কোড সব যন্ত্রে ঠিক চলে। লুপের ভেতরে s[pos] << 8 | s[pos + 1] হাতে-হাতে দুই byte থেকে big-endian সংখ্যা বানাচ্ছে। শেষ বার্তাটি বলছে “৯ byte আসবে” কিন্তু এসেছে মাত্র ১টি — প্রোগ্রাম সেটি নিয়ে কাজ না করে অপেক্ষার কথা জানাল। এটাই সঠিক আচরণ: অসম্পূর্ণ বার্তা জমিয়ে রাখুন, পরের recv-এর জন্য অপেক্ষা করুন।
htons ভুলে গেলে কী হয়? আপনি addr.sin_port = 8080; লিখলেন। x86-এ এটি তারের ক্রমে পড়লে হয়ে যায় 0x901f = ৩৬৮৯৫. আপনার server চুপচাপ সম্পূর্ণ অন্য port-এ বসে থাকবে, কোনো error ছাড়াই! Client সংযোগ পাবে না, আর আপনি ঘণ্টার পর ঘণ্টা ভাববেন firewall-এর দোষ। AI-এর লেখা কোডেও এই ভুল মাঝে মাঝে দেখা যায়।
৬ · একসাথে অনেক client — fork, thread, নাকি event loop
আমাদের উদাহরণে server একজন client-এর সাথে কথা বলেই শেষ। কিন্তু আসল server-এ একই মুহূর্তে হাজারো মানুষ। ভর্তি পরীক্ষার ফল প্রকাশের রাতে একটি ফলাফলের সাইটে কী চাপ পড়ে, ভাবুন! সমস্যা হলো recv সাধারণভাবে অপেক্ষা করে (block করে) — যতক্ষণ ডেটা না আসে, প্রোগ্রাম থেমে থাকে। একজন ধীর মোবাইল ব্যবহারকারী তখন সবাইকে আটকে রাখতে পারেন। চারটি নকশা আছে:
১. একে একে (iterative)
একজনের কাজ পুরো শেষ করে তবে পরের জনকে accept. সবচেয়ে সহজ, কিন্তু একজন ধীর হলে সারি থেমে যায়। শুধু শেখার জন্য বা খুব ছোট কাজের জন্য।
২. প্রতি client-এ একটি process
accept-এর পর fork() (পর্ব ৪৮) — নতুন child process সেই client সামলায়, parent আবার দরজায় ফেরে। একজনের crash অন্যদের ক্ষতি করে না। কিন্তু process ভারী; হাজার হাজার হলে মেমরি আর সময় দুটোই ফুরায়।
৩. প্রতি client-এ একটি thread
Process-এর চেয়ে হালকা thread (পর্ব ৪৯) — সবাই একই মেমরি ভাগ করে। কোড লেখা সহজ। কিন্তু প্রতিটি thread-এর নিজস্ব stack লাগে, আর হাজার হাজার thread-এর মধ্যে বারবার পালা বদলাতে CPU-র অনেক সময় যায়। শেয়ার করা ডেটায় race condition-এর ঝুঁকিও থাকে।
অনেক Java server, ডেটাবেস৪. Event loop — এক thread, অনেক socket
একটি লুপ OS-কে জিজ্ঞেস করে, “এই হাজার socket-এর মধ্যে কোনগুলোতে এখন ডেটা এসেছে?” শুধু সেগুলোর কাজ করে, কখনো কারও জন্য বসে থাকে না। Linux-এ এই প্রশ্ন করার দ্রুত উপায় epoll; পুরোনো উপায় select ও poll.
Event loop কীভাবে একা হাতে তিনজনকে সামলায়, দেখুন। একজন চা-দোকানির কথা ভাবুন, যিনি একা দশজন খদ্দের সামলান — কারও চা ফুটছে, কেউ টাকা গুনছেন, কেউ ভাবছেন কী খাবেন। তিনি কারও সামনে দাঁড়িয়ে অপেক্ষা করেন না; যার কাজ এখন করা যায়, তারটাই করেন:
1for (;;) {2 n = epoll_wait(ep, ready, 64, 5000);3 for (i = 0; i < n; i++) {4 if (ready[i] has full message)5 handle_and_reply(ready[i]);6 else if (partial) keep_in_buffer();7 else if (closed) close(fd);8 }9}
| A (Dhaka, Wi-Fi) | |
| B (Sylhet, 4G) | |
| C (Rajshahi, 3G) |
- সবাই চুপ — লুপ ঘুমিয়ে আছে।
epoll_waitkernel-কে বলে, “যেকোনো socket-এ কিছু এলে আমাকে জাগিয়ো, নয়তো সর্বোচ্চ ৫ সেকেন্ড পর।” এই সময়ে CPU একটুও খরচ হয় না। - A-র অনুরোধ এলো। Kernel লুপকে জাগিয়ে বলল, “শুধু A তৈরি।” লুপ A-র পুরো অনুরোধ পড়ল, উত্তর দিল। B আর C-র কথা ভাবতেই হয়নি — তাদের কিছু আসেনি।
- B আর C একসাথে তৈরি। এবার
epoll_waitদুটি socket ফেরত দিল। লুপ পালা করে দুজনেরই কাজ করবে। B-র পুরো অনুরোধ এসেছে, তাই সাথে সাথে উত্তর গেল। - C-র বার্তা অর্ধেক। ধীর 3G-তে C-র অনুরোধের মাত্র কয়েক byte এসেছে। Event loop এখানে অপেক্ষা করে না — অর্ধেক বার্তা C-র নিজের buffer-এ রেখে লুপ এগিয়ে যায়। Thread-per-client নকশায় এখানে একটি thread বসে থাকত; এখানে কেউ বসে নেই। এজন্যই প্রতিটি সংযোগের আলাদা buffer আর framing এখানে বাধ্যতামূলক।
- A সংযোগ বন্ধ করল। A-র socket-এ
recv0ফেরত দিল — মানে ওপাশ বিদায় নিয়েছে। লুপ A-র file descriptor বন্ধ করে, তার buffer মুক্ত করে। - Timeout — কে অনেকক্ষণ চুপ? ৫ সেকেন্ড কিছু না এলেও লুপ জাগে, আর দেখে কোন client অনেকক্ষণ অর্ধেক বার্তা নিয়ে বসে আছে। এমন সংযোগ বন্ধ না করলে কেউ ইচ্ছা করে হাজারো ধীর সংযোগ খুলে server আটকে দিতে পারে। একটি thread-এই হাজার হাজার client — এটাই event loop-এর শক্তি।
১৯৯৯ সালের দিকে প্রোগ্রামার Dan Kegel একটি লেখায় প্রশ্ন তোলেন: একটি সাধারণ server কি একসাথে দশ হাজার client সামলাতে পারবে? এই প্রশ্নের নাম হয়ে যায় C10K problem. তখনকার process-per-client বা thread-per-client নকশায় এটি কঠিন ছিল, আর select-এর মতো পুরোনো উপায়ে প্রতিবার সব socket একে একে পরীক্ষা করতে হতো। এর উত্তরে FreeBSD-তে kqueue আর Linux-এ epoll আসে, যা শুধু তৈরি socket-গুলোর তালিকা দেয়। ২০০৪ সালে রাশিয়ার প্রোগ্রামার Igor Sysoev প্রকাশ করেন nginx — event loop-এর ওপর গড়া একটি web server, যা আজ পৃথিবীর বিপুলসংখ্যক ওয়েবসাইট চালায়।
৭ · HTTP — লেখার একটি চুক্তি, আর বাস্তবের অ্যাপ
এতক্ষণ TCP শুধু byte বহন করছিল। ওই byte-এর অর্থ ঠিক করে application protocol. ওয়েবের protocol HTTP — ১৯৮৯–৯১ সালে CERN-এ Tim Berners-Lee এটি তৈরি করেন। এর সবচেয়ে সুন্দর দিক হলো, HTTP/1.1 মানুষের পড়ার মতো সাধারণ লেখা। Terminal-এ curl -v http://example.com চালালে আপনার কম্পিউটার নিচের মতো লেখা পাঠায়:
GET / HTTP/1.1
Host: example.com
User-Agent: curl/8.5.0
Accept: */*
← এই খালি লাইনই বলে “header শেষ”
আর server ফেরত পাঠায় এমন কিছু:
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Content-Length: 1256
<!doctype html>
<html> ... পাতার লেখা ... </html>
| অংশ | অর্থ |
|---|---|
GET / HTTP/1.1 | request line: কী করতে চাই (GET = দেখতে চাই, POST = তথ্য পাঠাতে চাই), কোন পথ, কোন version |
Host: example.com | একই IP-তে অনেক ওয়েবসাইট থাকতে পারে — কোনটি চাই, তা এখানে বলা হয়। HTTP/1.1-এ এটি বাধ্যতামূলক। |
200 OK | status line: ২০০ সফল, ৩০১ অন্য ঠিকানায় সরেছে, ৪০৪ পাওয়া যায়নি, ৫০০ server-এর ভেতরের সমস্যা |
Content-Length | body কত byte — TCP-র সীমানাহীন স্রোতে এটাই HTTP-র framing |
| প্রতিটি লাইনের শেষ | আসলে দুটি অক্ষর: \r\n. শুধু \n পাঠালে অনেক server মানে না। |
বাস্তবের জগতে এই সবকিছু
- মোবাইল ব্যাংকিং ও ওয়ালেট অ্যাপ: আপনি “Send Money” চাপলে অ্যাপটি TLS দিয়ে সুরক্ষিত একটি HTTPS সংযোগে server-কে একটি অনুরোধ পাঠায়, server হিসাব মিলিয়ে উত্তর দেয়। নিচে সেই একই TCP, একই socket — শুধু ওপরে encryption আর অনেক যাচাই।
- অনলাইন জাজ (পর্ব ৪৪): আপনার কোড HTTPS দিয়ে জাজের ওয়েব server-এ যায়, সেখান থেকে একটি সারিতে, তারপর আলাদা judge কম্পিউটারে চলে, ফল আবার নেটওয়ার্ক দিয়েই আপনার ব্রাউজারে ফেরে।
- মাল্টিপ্লেয়ার গেম: খেলোয়াড়ের অবস্থান সেকেন্ডে বহুবার পাঠাতে হয়। একটা আপডেট হারালে পরেরটা তো আসছেই — তাই অনেক গেম অবস্থানের আপডেট UDP-তে পাঠায়, আর কেনাকাটা বা লগইন TCP-তে।
- বড় সাইটের পেছনে: একটি load balancer সামনে বসে লাখো সংযোগ অনেকগুলো server-এ ভাগ করে দেয় — যাতে একটি কম্পিউটার নষ্ট হলেও সাইট বন্ধ না হয়।
৮ · ভুল আর নিরাপত্তা — নেটওয়ার্ক কাউকে বিশ্বাস করে না
নিজের কম্পিউটারের ফাইল থেকে পড়লে অন্তত ফাইলটি আপনার। কিন্তু নেটওয়ার্কের ওপাশে পৃথিবীর যেকোনো মানুষ, যেকোনো উদ্দেশ্যে। তাই নেটওয়ার্ক কোডের নিয়ম কঠোর। প্রতিটি নিয়মের পেছনে বাস্তবের কোনো না কোনো দুর্ঘটনা আছে:
| সমস্যা | যা দেখবেন | সমাধান |
|---|---|---|
| Server বন্ধ করে সাথে সাথে আবার চালালেন | bind: Address already in use | পুরোনো সংযোগ কিছুক্ষণ (প্রায় এক মিনিট) TIME_WAIT অবস্থায় port ধরে রাখে। bind-এর আগে SO_REUSEADDR চালু করুন। |
| সাধারণ ব্যবহারকারী হিসেবে port ৮০-তে bind | Permission denied | Unix-এ ১০২৪-এর নিচের port-এর জন্য বিশেষ অনুমতি লাগে। শেখার সময় ৮০৮০-এর মতো port নিন। |
ওপাশ বন্ধ হওয়ার পর send | প্রোগ্রাম হঠাৎ নিঃশব্দে মারা গেল | Linux পাঠায় SIGPIPE signal. MSG_NOSIGNAL flag দিন বা signal-টি উপেক্ষা করুন, তারপর EPIPE error দেখুন। |
| Client চুপ করে বসে আছে | Server-এর thread বা slot চিরকাল আটকে | Timeout রাখুন — poll-এর সময়সীমা বা SO_RCVTIMEO. |
| Return value দেখা হয়নি | অদ্ভুত, মাঝে মাঝে হওয়া বাগ | প্রতিটি কল যাচাই; ব্যর্থ হলে errno দেখে perror দিয়ে কারণ ছাপুন। |
আংশিক send-এর সঠিক সমাধানটি দেখতে এমন — AI-এর কোডে এই ফাংশনটি না থাকলে সন্দেহ করুন (এটি শুধু পড়ার জন্য, চালানোর জন্য নয়):
/* len byte-এর সবগুলো না যাওয়া পর্যন্ত পাঠাতে থাকো */
int send_all(int fd, const char *buf, size_t len) {
while (len > 0) {
ssize_t n = send(fd, buf, len, MSG_NOSIGNAL);
if (n < 0) {
if (errno == EINTR) continue; /* signal-এ বাধা পেলে আবার চেষ্টা */
return -1; /* আসল error — ডাকা ফাংশন সামলাবে */
}
buf += n; /* যতটুকু গেছে, ততটুকু এগোও */
len -= (size_t)n;
}
return 0;
}কখনো ইনপুট বিশ্বাস করবেন না
ধরুন আপনার protocol বলে, “প্রথম দুই byte-এ বার্তার দৈর্ঘ্য।” একজন আক্রমণকারী লিখে পাঠাল ৬০০০০, অথচ আপনার buffer মাত্র ১০২৪ byte. আপনি যদি যাচাই না করে ওই দৈর্ঘ্য অনুযায়ী recv বা memcpy করেন, তাহলে buffer-এর বাইরে লেখা হবে — buffer overflow. আক্রমণকারী তখন আপনার প্রোগ্রামের stack-এ নিজের পছন্দের ডেটা বসাতে পারে, কখনো কখনো নিজের কোড চালাতেও পারে। উল্টো দিকে, দাবি করা দৈর্ঘ্য বিশ্বাস করে বেশি পড়ে ফেরত পাঠালে হয় buffer over-read — মেমরির গোপন তথ্য বাইরে চলে যায়। strcpy বা gets দিয়ে নেটওয়ার্কের ডেটা কপি করা তাই নিষিদ্ধের মতো।
১৯৮৮ সালের Morris worm — ইন্টারনেটের প্রথম দিকের সবচেয়ে আলোচিত আক্রমণগুলোর একটি — অন্যতম পথ হিসেবে ব্যবহার করেছিল fingerd নামের একটি নেটওয়ার্ক server-এর buffer overflow, যেখানে gets() দিয়ে নেটওয়ার্ক থেকে লাইন পড়া হতো। ২০১৪ সালের Heartbleed-এ OpenSSL ক্লায়েন্টের দাবি করা দৈর্ঘ্য যাচাই না করেই মেমরি ফেরত পাঠাত। নিয়ম একটাই: নেটওয়ার্ক থেকে আসা প্রতিটি সংখ্যা, দৈর্ঘ্য আর লেখা — ব্যবহারের আগে সীমা যাচাই করুন। এটাই input validation.
Socket-এ যা যায়, তা সাধারণ অবস্থায় পথের যেকোনো router বা একই Wi-Fi-র যে কেউ দেখতে পারে। পাসওয়ার্ড, টাকার হিসাব, ব্যক্তিগত বার্তা কখনো সাধারণ TCP-তে নয় — সবসময় TLS-এর ভেতরে, অর্থাৎ HTTPS. নিজে TLS লেখার চেষ্টা করবেন না; OpenSSL-এর মতো পরীক্ষিত library ব্যবহার করুন। বিস্তারিত জানতে ABCL TECH-এর Cybersecurity কোর্স দেখুন।
৯ · AI-এর যুগে নেটওয়ার্ক কোড — চলছে মানেই ঠিক নয়
AI-কে “একটা C echo server লিখে দাও” বললে কয়েক সেকেন্ডে কোড পাবেন, আর নিজের কম্পিউটারে সেটি চলবেও। সমস্যা এখানেই — নেটওয়ার্ক কোডের বেশিরভাগ বাগ নিজের কম্পিউটারে দেখা যায় না। Loopback-এ বার্তা ভাঙে না, client কখনো ধীর হয় না, কেউ আক্রমণ করে না। বাগ বের হয় আসল ব্যবহারকারীর হাতে, প্রায়ই সবচেয়ে খারাপ সময়ে।
সবকিছু ঠিক থাকলে কোডটি কাজ করে।
AI-এর socket কোডে সাধারণত যা পাবেন
| AI-এর ভুল | কখন ধরা পড়ে | কী খুঁজবেন |
|---|---|---|
একটি recv = একটি পুরো বার্তা ধরে নেওয়া | ধীর নেটওয়ার্ক, বড় বার্তা | লুপ আর framing আছে কি না |
send-এর ফেরত মান উপেক্ষা | বড় ডেটা পাঠানোর সময় | send_all-এর মতো লুপ |
socket/bind/accept-এর error দেখে না | port ব্যস্ত, fd শেষ | প্রতিটি কলের পর < 0 যাচাই |
char buf[256] আর strcpy | আক্রমণকারী লম্বা ইনপুট দিলে | দৈর্ঘ্যের সীমা যাচাই |
নেটওয়ার্ক ডেটাকে C string ধরে printf("%s") | ডেটায় '\0' না থাকলে | ঠিক কত byte এলো, সেটি ব্যবহার |
| Timeout নেই, চিরকাল block | চুপ থাকা client | poll বা SO_RCVTIMEO |
htons ভুলে যাওয়া | “সংযোগ হচ্ছে না” — কোনো error ছাড়া | sin_port-এ htons |
error-এর পথে close নেই | দীর্ঘক্ষণ চলার পর | প্রতিটি পথে fd বন্ধ হয় কি না |
AI-কে শুধু “server লিখে দাও” না বলে, পরিবেশ আর ঝুঁকির কথা আগেই বলে দিন। তাতে AI-এর উত্তর অনেক বেশি পেশাদার হয়:
AI-এর লেখা socket কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১ওয়েব পাতা এতদিন TCP-তে চলত। তাহলে নতুন HTTP/3 কেন UDP-র ওপর QUIC বেছে নিল?
ওয়েব পাতার প্রতিটি byte দরকার — একটি অক্ষর হারালে HTML ভেঙে যায়। তাই নির্ভরযোগ্য, ক্রমানুসারী TCP ছিল স্বাভাবিক পছন্দ। কিন্তু আধুনিক ওয়েবে TCP-র তিনটি দুর্বলতা বড় হয়ে দেখা দিল:
- Head-of-line blocking: একটি পাতায় ছবি, CSS, JavaScript মিলিয়ে অনেক ফাইল একই TCP সংযোগে একসাথে আসে (HTTP/2). TCP-র কাছে সব এক স্রোত — মাঝখানের একটি packet হারালে সেটি আবার না আসা পর্যন্ত পরের সব byte আটকে থাকে, এমনকি অন্য ফাইলের byte-ও। দুর্বল মোবাইল নেটওয়ার্কে এটি বড় সমস্যা।
- ধীর শুরু: আগে TCP handshake, তারপর আলাদা TLS handshake — দূরের server-এর ক্ষেত্রে প্রতিটি আসা-যাওয়ায় সময় লাগে।
- বদলানো কঠিন: TCP থাকে অপারেটিং সিস্টেমের kernel-এ, আর পথের অসংখ্য router ও firewall TCP-র নির্দিষ্ট চেহারা ধরে নেয়। নতুন কিছু যোগ করতে কয়েক বছর লেগে যায়।
QUIC এই তিনটির উত্তর। Google-এর পরীক্ষা থেকে শুরু হয়ে এটি ২০২১ সালে IETF standard হয়, আর তার ওপর HTTP/3 আসে ২০২২ সালে। QUIC চলে UDP-র ওপর, কিন্তু নির্ভরযোগ্যতা, ক্রম, আবার পাঠানো — সব নিজে করে, আর প্রতিটি ফাইলের জন্য আলাদা স্রোত রাখে, তাই এক স্রোতের হারানো packet অন্যদের আটকায় না। TLS ১.৩ এর ভেতরেই বসানো, তাই handshake-এ কম আসা-যাওয়া লাগে। আর এটি চলে অ্যাপ বা ব্রাউজারের ভেতরে, তাই kernel আপডেটের অপেক্ষা ছাড়াই উন্নত করা যায়। UDP এখানে শুধু একটি খাম — কারণ UDP-কে পথের যন্ত্রগুলো আগে থেকেই চেনে।
মূল উপলব্ধি: “নির্ভরযোগ্যতা” বিনামূল্যে আসে না — কে, কোন স্তরে, কতটা সূক্ষ্মভাবে দেয়, সেটাই নকশা। QUIC দেখায় যে পুরোনো স্তরের সীমা বুঝলে তার ওপরে নতুন স্তর বানিয়েও সমস্যা এড়ানো যায়।
প্র ০২বাসায় Wi-Fi-তে একটি বড় ফাইল ডাউনলোড হচ্ছে। বাইরে বের হতেই ফোন মোবাইল ডেটায় চলে গেল। TCP সংযোগটির কী হবে?
মনে করুন, একটি TCP সংযোগ চেনা হয় চারটি জিনিস দিয়ে: দুই প্রান্তের IP ও port. Wi-Fi থেকে মোবাইল ডেটায় গেলে ফোনের IP address বদলে যায় — বাসার router-এর পেছনের ঠিকানা থেকে মোবাইল অপারেটরের দেওয়া ঠিকানায়। Server-এর কাছে নতুন ঠিকানা থেকে আসা packet সম্পূর্ণ অচেনা — সে বোঝে না এটি সেই পুরোনো সংযোগ। ফলে পুরোনো সংযোগটি মারা যায়।
তাহলে আমাদের ডাউনলোড বা ভিডিও কেন প্রায়ই চলতেই থাকে? কারণ অ্যাপগুলো এই সমস্যা জেনে নকশা করা:
- Reconnect আর resume: অ্যাপ সংযোগ ভাঙা টের পেলে নতুন সংযোগ খোলে, আর HTTP-র Range header দিয়ে বলে, “এত নম্বর byte থেকে বাকিটা দাও।” এজন্যই ভালো ডাউনলোড ম্যানেজার আধা ডাউনলোড থেকে আবার শুরু করতে পারে।
- ভিডিও ছোট ছোট টুকরোয়: স্ট্রিমিং সাইট ভিডিওকে কয়েক সেকেন্ডের টুকরোয় ভাগ করে আলাদা অনুরোধে আনে, তাই সংযোগ বদলালেও শুধু একটি টুকরো আবার আনতে হয়।
- QUIC-এর connection ID: QUIC সংযোগকে IP/port দিয়ে নয়, একটি নিজস্ব পরিচয় নম্বর দিয়ে চেনে। তাই ঠিকানা বদলালেও একই সংযোগ চালিয়ে যাওয়া যায় (connection migration).
মূল উপলব্ধি: নেটওয়ার্ক কখনো স্থির নয়। ভালো সিস্টেম ধরে নেয় সংযোগ যেকোনো মুহূর্তে ভাঙবে, আর এমনভাবে নকশা করে যাতে আবার যুক্ত হওয়া সহজ ও নিরাপদ হয় — একই অনুরোধ দুবার গেলেও যেন টাকা দুবার না কাটে।
প্র ০৩C10K সমস্যা কেন কঠিন ছিল, আর আজ যখন একটি server লাখো সংযোগ ধরে রাখে, তখন নতুন সীমাটা কোথায়?
দশ হাজার সংযোগ শুনতে খুব বেশি নয়। কিন্তু ১৯৯০-এর দশকের শেষের নকশায় প্রতিটি সংযোগ মানে একটি process বা thread. প্রতিটির নিজস্ব stack, kernel-এ নিজস্ব হিসাব, আর CPU-কে বারবার এক থেকে আরেকজনে পালা বদলাতে হয় (context switch). দশ হাজার thread-এর বেশিরভাগই শুধু বসে অপেক্ষা করছে, তবু মেমরি দখল করে রাখছে।
দ্বিতীয় সমস্যা ছিল select. প্রতিবার ডাকার সময় পুরো socket-তালিকা kernel-কে দিতে হতো, kernel সবগুলো একে একে দেখত, আর ফেরত আসার পর প্রোগ্রামকেও আবার সবগুলো দেখতে হতো কোনটি তৈরি। দশ হাজার socket-এ মাত্র কয়েকটি সক্রিয় হলেও কাজ দশ হাজারের। তার ওপর সাধারণ Linux-এ select-এর সর্বোচ্চ সীমা ১০২৪টি fd.
উত্তর এলো দুই দিক থেকে। OS-এর দিক থেকে: epoll ও kqueue — আগ্রহের তালিকা একবার kernel-এ রেখে দিন, তারপর শুধু যেগুলো তৈরি সেগুলোর খবর পান। কাজ হয় সক্রিয় সংযোগের অনুপাতে, মোট সংযোগের নয়। প্রোগ্রামের দিক থেকে: event loop নকশা, যেখানে প্রতিটি সংযোগ শুধু একটি ছোট struct আর buffer — thread নয়। nginx, Node.js, Redis এই পথেই চলে।
পরে প্রশ্ন আরও বড় হয় — “C10M”, অর্থাৎ এক কোটি সংযোগ। তখন দেখা গেল kernel-এর নেটওয়ার্ক স্তরই খরচের বড় অংশ। কিছু বিশেষ সিস্টেম তাই kernel পাশ কাটিয়ে নেটওয়ার্ক কার্ড থেকে সরাসরি ডেটা পড়ে। আজকের সীমা সাধারণত মেমরি (প্রতিটি সংযোগের buffer), CPU-র cache, আর প্রতিটি অনুরোধে আপনার নিজের কোড কতটা কাজ করে।
মূল উপলব্ধি: সমস্যার মাপ দশ গুণ বাড়লে শুধু দ্রুত কম্পিউটার যথেষ্ট নয় — নকশা বদলাতে হয়। “প্রতি ব্যবহারকারীতে একজন কর্মী” থেকে “একজন কর্মী, যিনি কখনো বসে থাকেন না” — এই চিন্তার বদলই C10K-এর আসল শিক্ষা।
প্র ০৪একটি বড় সাইটে লাখো মানুষ একসাথে ঢোকে। Load balancer কীভাবে তাদের অনেকগুলো server-এ ভাগ করে, আর কোথায় এটি কঠিন?
একটি server যত শক্তিশালীই হোক, তার একটি সীমা আছে, আর সেটি নষ্ট হলে পুরো সাইট বন্ধ। তাই বড় সিস্টেমে একই কাজের অনেকগুলো server থাকে, আর সামনে বসে load balancer — ব্যাংকের সেই কর্মকর্তার মতো যিনি টোকেন দিয়ে বলেন “আপনি ৩ নম্বর কাউন্টারে যান।”
- ভাগের নিয়ম: সবচেয়ে সহজ round-robin — পালা করে একজন একজন। আরও ভালো “যার হাতে সবচেয়ে কম সংযোগ”। আবার একই ব্যবহারকারীকে একই server-এ পাঠাতে চাইলে তার IP বা পরিচয়ের hash ব্যবহার হয়।
- কোন স্তরে: Layer 4 load balancer শুধু TCP সংযোগ দেখে ভাগ করে — খুব দ্রুত, কিন্তু ভেতরের HTTP বোঝে না। Layer 7 load balancer HTTP অনুরোধ পড়ে —
/apiএক দলে, ছবি আরেক দলে পাঠাতে পারে, TLS-ও এখানেই খুলতে পারে। - Health check: প্রতি কয়েক সেকেন্ডে প্রতিটি server-কে জিজ্ঞেস করে “ঠিক আছ?” — উত্তর না দিলে তাকে তালিকা থেকে বাদ দেয়। ব্যবহারকারী টেরও পান না।
কঠিন কোথায়? প্রথমত, state. কেউ লগইন করেছেন server ১-এ; পরের অনুরোধ server ২-এ গেলে সে তাঁকে চেনে না। সমাধান: লগইনের তথ্য server-এ না রেখে একটি সাধারণ ডেটাবেস বা cache-এ রাখা, যাতে যেকোনো server যেকোনো অনুরোধ সামলাতে পারে। দ্বিতীয়ত, হঠাৎ ভিড় (ফল প্রকাশ, বড় সেল): নতুন server চালু হতে সময় লাগে, তাই সারি, অপেক্ষার পাতা আর backpressure দিয়ে ভিড় সামলাতে হয়।
মূল উপলব্ধি: বড় সিস্টেম গড়ার কৌশল হলো server-গুলোকে বদলযোগ্য বানানো — কোনো একটির ওপর কিছু নির্ভর না করা। এর বিস্তারিত পাবেন ABCL TECH-এর System Design কোর্সে।
প্র ০৫নেটওয়ার্ক থেকে আসা ডেটা পড়ার কোডে একটি ছোট বাগ কেন অন্য জায়গার বাগের চেয়ে বহু গুণ বিপজ্জনক? Heartbleed থেকে কী শিখি?
নিজের কম্পিউটারের কোনো প্রোগ্রামে buffer overflow থাকলে সেটি কাজে লাগাতে আক্রমণকারীকে আগে আপনার কম্পিউটারে ঢুকতে হবে। কিন্তু ইন্টারনেটে খোলা একটি server-এর parser-এ — যে অংশ আসা byte পড়ে অর্থ বের করে — বাগ থাকলে পৃথিবীর যেকোনো জায়গা থেকে, পাসওয়ার্ড ছাড়াই, কয়েকটি বিশেষ byte পাঠিয়ে সেটি কাজে লাগানো যায়। এবং একই software হাজার হাজার server-এ চলে, তাই একটি বাগ মানে একসাথে হাজার হাজার দরজা খোলা।
Heartbleed (২০১৪): OpenSSL-এ TLS-এর “heartbeat” নামে একটি ছোট সুবিধা ছিল — ক্লায়েন্ট একটি লেখা পাঠিয়ে বলে, “এটা আমাকে ফেরত দাও, আর এর দৈর্ঘ্য এত।” বাগটি ছিল: OpenSSL দাবি করা দৈর্ঘ্যটি আসল লেখার দৈর্ঘ্যের সাথে মিলিয়ে দেখত না। আক্রমণকারী ১ byte পাঠিয়ে বলত “দৈর্ঘ্য প্রায় ৬৪ হাজার” — আর server তার মেমরি থেকে পাশের হাজার হাজার byte কপি করে ফেরত পাঠাত। সেই মেমরিতে থাকতে পারত অন্য ব্যবহারকারীর পাসওয়ার্ড, cookie, এমনকি server-এর গোপন চাবি। এটি আমাদের ৮ নম্বর অংশের ঠিক সেই ভুল — নেটওয়ার্ক থেকে আসা দৈর্ঘ্য বিশ্বাস করা। সমাধান ছিল কয়েক লাইনের একটি সীমা-যাচাই।
এখান থেকে শিল্পজগৎ যা শিখেছে:
- Fuzzing: একটি প্রোগ্রাম লাখো এলোমেলো ও অদ্ভুত ইনপুট বানিয়ে parser-এ ছুড়ে দেখে কোথায় ভাঙে — sanitizer-এর সাথে মিলে এটি বহু বাগ ধরে।
- Memory-safe ভাষা: নতুন নেটওয়ার্ক কোড ক্রমশ Rust-এর মতো ভাষায় লেখা হচ্ছে, যেখানে সীমার বাইরে পড়া compiler-ই আটকে দেয়।
মূল উপলব্ধি: নেটওয়ার্কের সীমানা হলো বিশ্বাসের সীমানা। এর ওপাশ থেকে আসা প্রতিটি byte শত্রুর লেখা ধরে নিয়ে পড়ুন — এটি সন্দেহবাতিক নয়, পেশাদারিত্ব।
অনুশীলন
এগুলোর বেশিরভাগে কোড লিখতে হবে না — নিজের কম্পিউটারে দেখা আর ভাবাই আসল কাজ।
-
আপনার কম্পিউটারে কে কে শুনছে? Linux বা Mac-এ
ss -tln(বাnetstat -an), Windows-এnetstat -anচালান।LISTENলেখা লাইনগুলো দেখুন। কোন কোন port খোলা? সেগুলো কোন প্রোগ্রামের হতে পারে?প্রতিটি
LISTENলাইন একটি listening socket — কোনো প্রোগ্রামbindআরlistenকরে বসে আছে।127.0.0.1:...মানে শুধু এই কম্পিউটার থেকে সংযোগ নেবে (loopback);0.0.0.0:...মানে যেকোনো নেটওয়ার্ক থেকে।ESTABLISHEDলাইনগুলো চলমান সংযোগ — প্রতিটির দুই প্রান্তের IP:port দেখুন; মূল ধারণার সেই চারটি জিনিস চোখের সামনে। -
HTTP নিজের চোখে:
curl -v http://example.comচালান।>চিহ্নের লাইনগুলো আপনার পাঠানো,<চিহ্নের লাইনগুলো server-এর। Request line,Hostheader আর status line খুঁজে বের করুন।প্রথমে দেখবেন
Trying ...আরConnected to example.com— DNS থেকে IP পাওয়া আর TCP handshake. তারপর> GET / HTTP/1.1,> Host: example.comএবং একটি খালি>লাইন — header শেষ। উত্তরে< HTTP/1.1 200 OKআরContent-Lengthসহ header. এবারhttps://দিয়ে চালালে মাঝখানে TLS handshake-এর লাইনও দেখবেন। সবই এই পর্বের stepper-এর ধাপ। -
Framing নকশা করুন: একটি ক্লাস-চ্যাট অ্যাপের জন্য newline দিয়ে বার্তা আলাদা করবেন, নাকি length prefix দিয়ে? কেউ বার্তার ভেতরে Enter চাপলে কী হবে? কেউ দাবি করলে “দৈর্ঘ্য ৪০০ কোটি” — server কী করবে?
Newline সহজ ও পড়তে সুবিধা (
ncদিয়ে হাতেই পরীক্ষা করা যায়), কিন্তু বার্তার ভেতরে Enter থাকলে বার্তা দুই ভাগ হয়ে যাবে — তখন newline-কে escape করতে হবে (যেমন\nলিখে পাঠানো)। Length prefix-এ বার্তার ভেতরে যা খুশি থাকতে পারে, ছবিও। কিন্তু দৈর্ঘ্যের একটি সর্বোচ্চ সীমা (যেমন ৬৪ KB) আগেই ঠিক করুন; তার বেশি দাবি এলে সাথে সাথে সংযোগ বন্ধ করুন, নইলে কেউ বিশাল দৈর্ঘ্য দাবি করে আপনার server-এর সব মেমরি দখলের চেষ্টা করবে। -
কোড (ঐচ্ছিক): উপরের
loopback.c-এ client দিয়ে দুবারsendকরান —"HELLO"আর"WORLD"— তারপর server-এ একবারrecvকরুন। কত byte পেলেন? এটি কি প্রমাণ করে TCP সবসময় বার্তা জোড়া লাগায়?Loopback-এ সাধারণত দেখবেন
server got 10 bytes: HELLOWORLD— দুটি বার্তা মিশে একটি হয়ে গেছে। এটি byte stream-এর প্রত্যক্ষ প্রমাণ: দুটিsendমানে দুটিrecvনয়। কিন্তু এটি কোনো নিয়মও প্রমাণ করে না — আরেকবার চালালে, বা আসল নেটওয়ার্কে, ভাগ অন্যরকম হতে পারে। ঠিক এজন্যই কোড কখনো নির্দিষ্ট ভাগের ওপর নির্ভর করবে না; framing দিয়ে নিজে সীমানা ঠিক করবে।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি TCP server-এর ফাংশন ডাকার সঠিক ক্রম সাজান (▲▼ চেপে সরান)
socket()— একটি প্রান্ত তৈরিbind()— নিজের port নেওয়াlisten()— সংযোগ নিতে রাজি হওয়াaccept()— একটি client-এর সংযোগ তুলে নেওয়াrecv()/send()— কথা বলাclose()— সংযোগ বন্ধ
একটি কম্পিউটারে ব্রাউজার, ইমেইল আর গেম — তিনটিই নেটওয়ার্ক ব্যবহার করছে। আসা ডেটা কোন প্রোগ্রামের, তা কী দিয়ে ঠিক হয়?
- IP address
- Port নম্বর
- DNS
- Wi-Fi-র নাম
একটি লাইভ ভিডিও কল অ্যাপ সাধারণত কেন UDP পছন্দ করে?
- UDP স্বয়ংক্রিয়ভাবে ডেটা encrypt করে
- UDP হারানো packet নিজে থেকে আবার পাঠায়
- হারানো এক টুকরো আবার আনার অপেক্ষার চেয়ে এগিয়ে যাওয়া ভালো, তাই দেরি কম
- UDP-র handshake TCP-র চেয়ে নিরাপদ
Client দুবার send করল: “HELLO” আর “WORLD”. Server-এর প্রথম recv কী ফেরত দিতে পারে?
- “HEL”, “HELLO”, “HELLOWORLD” — যেকোনোটি, সব সম্ভব
- সবসময় ঠিক “HELLO”
- “WORLD”, কারণ শেষেরটা আগে আসে
- “HLO” — কিছু অক্ষর পথে হারাতে পারে
Server-এ accept() কী ফেরত দেয়?
- client-এর IP address
- একই listening socket, এখন client-এর সাথে যুক্ত
- client-এর পাঠানো প্রথম বার্তা
- শুধু এই client-এর সাথে কথা বলার জন্য একটি নতুন file descriptor
x86 কম্পিউটারে কেউ addr.sin_port = 8080; লিখল, htons ছাড়া। কী হবে?
- Compile error আসবে
- Server কোনো error ছাড়াই ভুল port-এ বসবে
- কিছুই হবে না, kernel নিজে ঠিক করে নেবে
- bind সবসময় ব্যর্থ হবে
একটি event loop server-এ একজন client-এর অর্ধেক বার্তা এসেছে। Server-এর কী করা উচিত?
- বাকিটা না আসা পর্যন্ত ওই socket-এ recv করে বসে থাকা
- যা এসেছে তা দিয়েই উত্তর দিয়ে দেওয়া
- অংশটি সেই client-এর buffer-এ জমিয়ে অন্যদের কাজ চালানো, সাথে timeout রাখা
- সাথে সাথে সংযোগ বন্ধ করে দেওয়া
AI-এর লেখা server নিজের কম্পিউটারে 127.0.0.1-এ নিখুঁত চলছে। কোন সমস্যাটি এই পরীক্ষায় সবচেয়ে কম ধরা পড়ে?
- আংশিক recv না সামলানো আর timeout না থাকা
- ভুল header ফাইল include করা
- ব্যস্ত port-এ bind করা
- htons ছাড়া port বসানো
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- IP = কোন কম্পিউটার, port = কোন প্রোগ্রাম, DNS = নাম থেকে IP; একটি TCP সংযোগ চেনা যায় দুই প্রান্তের IP ও port দিয়ে।
- TCP নির্ভরযোগ্য, ক্রমানুসারী byte stream — কিন্তু বার্তার সীমানা নেই; framing (length prefix বা delimiter) আপনার দায়িত্ব। UDP দ্রুত, নিশ্চয়তাহীন।
- Server:
socket → bind → listen → accept → recv/send → close; client:socket → connect → send/recv → close. Socket একটি file descriptor. - তারে সংখ্যা যায় big-endian ক্রমে —
htons/htonlপাঠানোর আগে,ntohs/ntohlপাওয়ার পরে। অনেক client-এর জন্য fork, thread, বা event loop (epoll). - প্রতিটি return value যাচাই, আংশিক send/recv লুপে, timeout, ইনপুটের দৈর্ঘ্য যাচাই, গোপন তথ্য শুধু TLS-এ — AI-এর কোডে এগুলোই আগে খুঁজুন।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫১ · এমবেডেড C — হার্ডওয়্যারের সাথে কথাপরবর্তী পর্বOS ছাড়া মাইক্রোকন্ট্রোলারে C — রেজিস্টার, interrupt আর সীমিত মেমরির কৌশল।
- C প্রোগ্রামিং · কোর্স হোম৫৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Computer Networksগভীরে যানIP routing, TCP-র congestion control, DNS আর HTTP-র ভেতরের খুঁটিনাটি।
- Cybersecurityপাশের ট্র্যাকTLS কীভাবে কাজ করে, নেটওয়ার্ক আক্রমণ আর প্রতিরক্ষা।
- System Designপাশের ট্র্যাকLoad balancer, cache, সারি — লাখো ব্যবহারকারীর সিস্টেম কীভাবে গড়া হয়।