এই পর্বে যা শিখবেন
- Server আর client-এর প্রতিটি socket call কী করে, আর সেই মুহূর্তে তারে (wire-এ) কোন segment যায়
sockaddr-পরিবারের struct, cast কেন লাগে,htons/ntohsআরinet_pton/inet_ntopgetaddrinfo()দিয়ে IPv4 আর IPv6 দুটোতেই চলা client ও server লেখার প্রস্তাবিত ধাঁচ- Partial read/write-এর চিরচেনা বাগ,
readn/writen, আর length-prefix framing — নিজের চোখে loopback-এ চালিয়ে - errno, EINTR, SIGPIPE, close বনাম shutdown, timeout, আর নিরাপদ server লেখার নিয়ম
১ · Socket API — নেটওয়ার্কও একটি “ফাইল”
পর্ব ২৭-এ দেখেছেন, socket হলো প্রোগ্রাম আর OS-এর transport স্তরের মাঝের দরজা। পর্ব ২৯-এ দেখেছেন, TCP-র handshake, sequence number, retransmission — সবই চালায় kernel. তাহলে প্রোগ্রামারের হাতে কী থাকে? থাকে কয়েকটি system call — kernel-এর কাছে কয়েকটি অনুরোধ: একটি socket চাওয়া, সেটিকে একটি ঠিকানায় বসানো, শোনা শুরু করা, অন্য ঠিকানায় সংযোগ চাওয়া, আর বাইট পাঠানো ও নেওয়া। এই ডাকগুলোর সমষ্টিই socket API. এই পর্বে আমরা সেই API-কে ধাপে ধাপে, সাবধানী C কোডে, ব্যবহার করব।
অফিসের টেলিফোন ব্যবস্থার কথা ভাবুন। রিসেপশনে একটি নম্বর আছে (bind), রিসেপশনিস্ট ফোনের পাশে বসে আছেন (listen). কল এলে তিনি সেটি একজন কর্মকর্তার টেবিলের লাইনে পাঠিয়ে দেন (accept একটি নতুন সংযোগ দেয়), আর নিজে আবার মূল নম্বরে পরের কলের অপেক্ষা করেন। যিনি ফোন করছেন, তিনি শুধু নম্বর ঘোরান (connect). কথা শুরু হলে দুজনেই কথা বলেন আর শোনেন (write, read), শেষে ফোন রাখেন (close). নম্বর ঘোরানো থেকে লাইন জোড়া লাগা পর্যন্ত ভেতরের কাজটা টেলিফোন এক্সচেঞ্জের — এখানে সেটা kernel-এর TCP.
একটু ইতিহাস — Berkeley থেকে POSIX
আজকের socket API-র জন্ম ক্যালিফোর্নিয়া বিশ্ববিদ্যালয়, Berkeley-র Unix সংস্করণ BSD-তে। DARPA-র অর্থায়নে সেখানে TCP/IP Unix-এর ভেতরে বসানো হয়, আর ১৯৮৩ সালে প্রকাশিত 4.2BSD-তে socket API প্রথম ব্যাপকভাবে সবার হাতে আসে বলে ধরা হয়। নকশার মূল চিন্তা ছিল Unix-এর পুরোনো দর্শন — “সবকিছুই ফাইল”: socket-ও একটি file descriptor, তাই ফাইলের মতোই read(), write(), close() চলে। পরে এই API প্রায় হুবহু Linux, macOS, অন্যান্য Unix আর (কিছু পরিবর্তনসহ) Windows-এর Winsock-এ গেছে। POSIX মানদণ্ড এটিকে আনুষ্ঠানিক রূপ দিয়েছে, আর IPv6-এর জন্য নতুন অংশগুলো (getaddrinfo, sockaddr_in6) এসেছে IETF-এর RFC 3493-জাতীয় নথি থেকে।
| বিষয় | Linux / macOS / BSD (POSIX) | Windows (Winsock) |
|---|---|---|
| শুরুর প্রস্তুতি | কিছু লাগে না | আগে WSAStartup() ডাকতে হয় |
| Socket-এর টাইপ | int (file descriptor) | SOCKET (আলাদা handle, ফাইল নয়) |
| পড়া-লেখা | read/write বা recv/send | শুধু recv/send |
| বন্ধ করা | close() | closesocket() |
| ভুলের কারণ | errno | WSAGetLastError() |
এই পর্বের সব কোড POSIX/Linux-এর জন্য; ধারণাগুলো সব OS-এ একই, শুধু ওপরের পার্থক্যগুলো মনে রাখবেন। Socket-এর OS-দিক — file descriptor table, system call কীভাবে kernel-এ ঢোকে — জানতে চাইলে অপারেটিং সিস্টেম কোর্সের System call আর file descriptor পর্ব দুটি দেখে নিতে পারেন। C-র দিক থেকে pointer আর struct-এ হাত পাকা না থাকলে C কোর্সের পয়েন্টার আর struct পর্ব কাজে দেবে।
Socket API নিজে কোনো protocol নয়। TCP-র সব জটিল কাজ (handshake, ACK, retransmission, flow control) kernel আগে থেকেই করে; API শুধু প্রোগ্রামকে বলার সুযোগ দেয় — কার সাথে কথা বলব, কী পাঠাব, কী পেলাম। তাই socket প্রোগ্রামিং-এর বেশিরভাগ বাগ TCP-র নয়, প্রোগ্রামারের ভুল ধারণার: “একবার পড়লেই পুরো বার্তা”, “ঠিকানা struct-এ যেমন খুশি লেখা যায়”, “ভুল হবে না, তাই যাচাই লাগবে না”। এই পর্ব মূলত এই ভুলগুলো ভাঙার।
২ · Server আর client-এর call-এর ক্রম — তারে কী ঘটে
প্রথমে পুরো ছবিটা একটি সারণিতে দেখে নিই। বাম দিকে প্রোগ্রামের ডাক, ডান দিকে সেই মুহূর্তে TCP তারে কী পাঠায়। লক্ষ করুন, অনেক ডাক তারে কিছুই পাঠায় না — সেগুলো শুধু kernel-এর ভেতরের হিসাব বদলায়।
| ডাক | কে | কী করে | তারে যা যায় |
|---|---|---|---|
socket(AF_INET, SOCK_STREAM, 0) | দুজনেই | Kernel-এ একটি খালি TCP socket বানিয়ে file descriptor ফেরত দেয়। | কিছুই না |
setsockopt(SO_REUSEADDR) | server | Restart-এর পর TIME-WAIT থাকলেও একই port-এ bind করতে দেয় (পর্ব ২৯)। | কিছুই না |
bind(ঠিকানা, port) | server (client-এ ঐচ্ছিক) | Socket-কে একটি স্থানীয় IP আর port-এ বাঁধে। | কিছুই না |
listen(fd, backlog) | server | Socket-কে LISTEN অবস্থায় নেয়; SYN queue আর accept queue তৈরি হয়। | কিছুই না |
connect(ঠিকানা) | client | দরকার হলে নিজে থেকে একটি ephemeral port বেছে নেয়, তারপর handshake করে। | SYN →, ← SYN+ACK, ACK → |
accept(fd) | server | Accept queue থেকে একটি তৈরি সংযোগ তুলে নতুন file descriptor দেয়; queue খালি থাকলে অপেক্ষা করে। | কিছুই না (handshake আগেই শেষ) |
write() / send() | দুজনেই | বাইটগুলো kernel-এর send buffer-এ কপি করে, তারপরই ফেরে। | ডেটা segment (kernel-এর সুবিধামতো সময়ে) |
read() / recv() | দুজনেই | Receive buffer-এ যা আছে (চাওয়ার চেয়ে কমও হতে পারে) তা কপি করে দেয়। | পরে ACK বা window update |
shutdown(fd, SHUT_WR) | দুজনেই | শুধু লেখার দিক বন্ধ — half-close. | FIN |
close(fd) | দুজনেই | Descriptor ছেড়ে দেয়; শেষ descriptor হলে সংযোগ বন্ধ শুরু। | FIN (না-পড়া ডেটা থাকলে RST) |
এবার একই কাজ সময়ের ক্রমে, পাশাপাশি। Server 127.0.0.1:47301-এ শুনছে, client “salam” লিখে উত্তর পড়ে বন্ধ করছে। “পরে” চাপুন:
| Server-এর ডাক | |
| Client-এর ডাক | |
| তারে যা গেল | |
| Socket-এর অবস্থা |
- socket(). Server kernel-এর কাছে একটি TCP socket চাইল। ফেরত পেল একটি ছোট সংখ্যা — ৩, কারণ ০, ১, ২ আগে থেকেই stdin, stdout, stderr. তারে কিছুই যায়নি; এটি শুধু kernel-এর ভেতরের একটি খালি কাঠামো।
- setsockopt + bind.
SO_REUSEADDRচালু করে socket-কে127.0.0.1:47301-এ বাঁধা হলো। এখনো কেউ সংযোগ করতে পারবে না — socket CLOSED অবস্থায়, শুধু ঠিকানাটা দখল হলো। - listen. এবার socket LISTEN অবস্থায়, backlog ৮।
ss -tlnচালালে এখন এই port দেখা যাবে। এখনো তারে কিছু যায়নি — server চুপচাপ অপেক্ষায়। - connect আর handshake. Server
accept()-এ আটকে আছে। Client নিজের socket বানিয়েconnect()ডাকল; kernel একটি ephemeral port বেছে SYN পাঠাল। Server-এর kernel নিজেই SYN+ACK দিল — server প্রোগ্রাম কিছু করেনি। ACK পৌঁছাতেই সংযোগ তৈরি হয়ে accept queue-তে উঠল (বিস্তারিত পর্ব ২৯)। - accept ফিরল, প্রথম লেখা।
accept()queue থেকে সংযোগটি তুলে নতুন descriptor ৪ দিল। এখন server-এর দুটি socket: fd ৩ শুধু নতুন সংযোগ নেয় (listening socket), fd ৪ এই একজন client-এর সাথে কথা বলে (connected socket). Client “salam\n” (৬ বাইট) লিখল। - Echo. Server fd ৪ থেকে ৬ বাইট পড়ে সেগুলোই ফেরত লিখল; client পড়ল ৬। প্রতিটি ডেটা segment-এর জবাবে ACK যায় — সেটা kernel-এর কাজ, প্রোগ্রাম দেখেও না।
- Client বন্ধ করল। Client
close()ডাকায় FIN গেল। Server-এর পরেরread()ফেরত দিল ০ — TCP-র FIN প্রোগ্রামের কাছে পৌঁছায় EOF হিসেবে। Server-এর দিক CLOSE-WAIT, client FIN-WAIT-2. - Server-ও বন্ধ, পরের client. Server fd ৪ বন্ধ করল, FIN গেল, client শেষ ACK দিয়ে TIME-WAIT-এ। আর server ফিরে গেল fd ৩-এর
accept()-এ — পরের client-এর অপেক্ষায়। এক client শেষ হলে তবেই পরের জন — একে বলে iterative server.
এই ছবির দুটি জিনিস মনে রাখবেন। এক, accept() handshake করে না — handshake kernel আগেই সেরে রাখে, accept() শুধু তৈরি সংযোগ তুলে নেয়। দুই, একটি listening socket থেকে অসংখ্য connected socket জন্মায়, প্রতিটি আলাদা চার-মানের জোড়া (উৎস IP, উৎস port, গন্তব্য IP, গন্তব্য port) দিয়ে চেনা — পর্ব ২৭-এর demultiplexing ঠিক এভাবেই কাজ করে। Client চাইলে connect()-এর আগে নিজেও bind() করতে পারে, কিন্তু সাধারণত দরকার হয় না; kernel নিজেই একটি ephemeral port দেয়।
৩ · ঠিকানার struct, byte order আর cast
Socket API প্রথম থেকেই অনেক ধরনের নেটওয়ার্কের জন্য নকশা করা — IPv4, পরে IPv6, একই যন্ত্রের Unix domain socket ইত্যাদি। প্রতিটির ঠিকানার আকার আলাদা। তাই bind(), connect(), accept() একটি সাধারণ ধরন নেয়: struct sockaddr * আর তার দৈর্ঘ্য। আসল ঠিকানা থাকে নির্দিষ্ট struct-এ, যার প্রথম ঘর সবসময় “পরিবার” (family) — kernel সেটা দেখে বোঝে বাকিটা কীভাবে পড়তে হবে।
struct sockaddr { /* "সাধারণ" ধরন — ১৬ বাইট, শুধু pointer-এর জন্য */
sa_family_t sa_family; /* AF_INET, AF_INET6, AF_UNIX ... */
char sa_data[14];
};
struct sockaddr_in { /* IPv4 — ১৬ বাইট */
sa_family_t sin_family; /* AF_INET */
in_port_t sin_port; /* port, network byte order */
struct in_addr sin_addr; /* ৪ বাইটের IPv4 ঠিকানা, network byte order */
/* + padding */
};
struct sockaddr_in6 { /* IPv6 — ২৮ বাইট */
sa_family_t sin6_family; /* AF_INET6 */
in_port_t sin6_port;
uint32_t sin6_flowinfo;
struct in6_addr sin6_addr; /* ১৬ বাইট */
uint32_t sin6_scope_id; /* fe80:: ঠিকানার interface */
};
struct sockaddr_storage; /* যেকোনো পরিবারের ঠিকানা ধরার মতো বড় (Linux-এ ১২৮ বাইট) */
(মাপগুলো Linux x86-64-এর; অন্য OS-এ ঘরের নাম ও ক্রম সামান্য আলাদা হতে পারে, যেমন BSD-তে শুরুতে একটি দৈর্ঘ্যের বাইট থাকে।) struct sockaddr আসলে একটি “সাধারণ খাম”: C-তে তখনো void * প্রচলিত ছিল না বলে API-কে একটি সাধারণ pointer-ধরন দিতে হয়েছিল। তাই প্রতিবার নির্দিষ্ট struct-এর ঠিকানা cast করতে হয় — (struct sockaddr *)&addr — আর পাশে সঠিক দৈর্ঘ্য sizeof addr দিতে হয়। ভুল দৈর্ঘ্য দিলে kernel হয় EINVAL দেবে, নয়তো আধখানা ঠিকানা পড়বে। accept() বা recvfrom()-এ কার কাছ থেকে এল তা জানতে চাইলে কোন পরিবার আসবে আগে জানা থাকে না; তখন sockaddr_storage নিন, কারণ এটি সব পরিবারের ঠিকানা ধরার মতো বড় আর ঠিকভাবে সারিবদ্ধ।
Byte order — htons কেন
পর্ব ১৬-এ দেখেছেন, IP আর TCP header-এ বহু-বাইটের সংখ্যা থাকে big-endian ক্রমে — বড় বাইট আগে; একে বলে network byte order. কিন্তু x86 আর বেশিরভাগ ARM যন্ত্র মেমরিতে সংখ্যা রাখে little-endian ক্রমে। sin_port আর sin_addr-এ kernel হুবহু নেটওয়ার্কের ক্রম আশা করে, তাই লেখার আগে রূপান্তর লাগে:
| ফাংশন | মানে | উদাহরণ (x86-এ) |
|---|---|---|
htons(x) | host to network, short (১৬ bit) | 47301 = 0xB8C5; মেমরিতে আগে ছিল C5 B8, পরে B8 C5 |
htonl(x) | host to network, long (৩২ bit) | INADDR_LOOPBACK = 0x7F000001 → মেমরিতে 7F 00 00 01 |
ntohs(x), ntohl(x) | উল্টো দিক — নেটওয়ার্ক থেকে পড়া মান | ntohs(addr.sin_port) দিয়ে port ছাপা |
Big-endian যন্ত্রে এই ফাংশনগুলো কিছুই করে না, তবু সবসময় ডাকতে হয় — তাহলে কোড সব যন্ত্রে চলে। সবচেয়ে চেনা বাগ: addr.sin_port = 8080; লিখে htons ভুলে যাওয়া। 8080 = 0x1F90, little-endian মেমরিতে বাইট দুটি 90 1F; kernel এটাকে নেটওয়ার্ক ক্রমে পড়ে 0x901F = 36895. প্রোগ্রাম কোনো error ছাড়াই port 36895-এ শুনতে থাকে, আর client 8080-এ গিয়ে Connection refused পায়। একই কারণে length-prefix framing-এর দৈর্ঘ্যও (অংশ ৬) পাঠানোর আগে htonl আর পড়ার পরে ntohl.
লেখা ঠিকানা থেকে বাইনারি — inet_pton আর inet_ntop
“192.168.1.10” বা “::1” — মানুষের পড়ার মতো লেখা ঠিকানাকে struct-এর বাইনারি রূপে নিতে inet_pton() (presentation to network), আর উল্টো দিকে inet_ntop(). দুটোই IPv4 আর IPv6 দুটোতেই চলে:
struct sockaddr_in a = {0};
a.sin_family = AF_INET;
a.sin_port = htons(47301);
if (inet_pton(AF_INET, "127.0.0.1", &a.sin_addr) != 1) { /* 1 = ঠিক, 0 = ভুল লেখা, -1 = ভুল family */
fprintf(stderr, "bad address\n"); exit(1);
}
char text[INET_ADDRSTRLEN]; /* IPv4-এর জন্য ১৬, IPv6-এ INET6_ADDRSTRLEN = ৪৬ */
inet_ntop(AF_INET, &a.sin_addr, text, sizeof text); /* "127.0.0.1" */
পুরোনো কোডে প্রায়ই inet_addr(), inet_aton() বা inet_ntoa() দেখবেন। এগুলো শুধু IPv4 বোঝে, আর inet_addr() ভুল লেখা আর বৈধ ঠিকানা “255.255.255.255”-কে একই মান (−১) দিয়ে ফেরায় — তাই নতুন কোডে inet_pton/inet_ntop. আর নাম থেকে ঠিকানা (DNS) খোঁজার কাজ এদের নয়, সেটা পরের অংশের getaddrinfo()-এর।
৪ · getaddrinfo — IPv4 আর IPv6, দুটোর জন্য এক কোড
হাতে sockaddr_in বানানো কোড শুধু IPv4-এ চলে। কাল যদি সার্ভার শুধু IPv6-এ থাকে (পর্ব ২১), সব কোড আবার লিখতে হবে। এর সমাধান getaddrinfo(): একটি host নাম (বা লেখা ঠিকানা) আর একটি service (port নম্বর বা নাম, যেমন “https”) দিলে এটি তৈরি-করা sockaddr-সহ struct addrinfo-এর একটি linked list ফেরত দেয় — প্রতিটি সারিতে family, socktype, protocol আর ঠিকানা, ঠিক যা socket(), bind() আর connect()-এ লাগে। আপনার কোড family জানেই না; তালিকা ধরে একে একে চেষ্টা করে। নামটি কোথায় খোঁজা হবে — /etc/hosts, DNS (পর্ব ৩৫) বা অন্য কোথাও — তা ঠিক করে OS-এর resolver সেটিং।
| hints-এর ঘর | সাধারণ মান | অর্থ |
|---|---|---|
ai_family | AF_UNSPEC | IPv4 বা IPv6 — যেটাই থাকুক; শুধু একটি চাইলে AF_INET বা AF_INET6. |
ai_socktype | SOCK_STREAM | TCP (UDP হলে SOCK_DGRAM, পর্ব ৪২)। |
ai_flags | AI_PASSIVE | Server-এর জন্য: host NULL দিলে wildcard ঠিকানা (0.0.0.0 বা ::) দেবে। Host দিলে এর কোনো প্রভাব নেই। |
AI_NUMERICHOST, AI_ADDRCONFIG | শুধু লেখা ঠিকানা নেবে (DNS নয়); যন্ত্রে যে family-র ঠিকানা আছে শুধু সেটাই দেবে। |
ফলাফল ব্যবহার শেষে freeaddrinfo() দিয়ে মেমরি ফেরত দিতে হয়। আর ভুল হলে এটি errno-তে নয়, নিজের একটি কোড ফেরায় (যেমন EAI_NONAME), যার লেখা রূপ দেয় gai_strerror(). AI_PASSIVE-এর সাথে NULL host দিলে server সব interface-এ শোনে — সেটা কখন বিপজ্জনক, অংশ ৮-এ দেখব। “localhost”-এর জন্য তালিকা কেমন হয় আর client কীভাবে চেষ্টা করে, দেখুন:
| তালিকা (res) | |
| যে ডাক চলল | |
| তারে যা গেল | |
| ফল |
- তালিকা এলো।
getaddrinfo()দুটি ফলাফল দিল: আগে IPv6::1, পরে IPv4127.0.0.1. কোনটি আগে থাকবে তা resolver-এর সাজানোর নিয়ম (RFC 6724, Linux-এ/etc/gai.conf) আর/etc/hosts-এর ওপর নির্ভর করে — যন্ত্রভেদে আলাদা হতে পারে। এখানে নামটি hosts ফাইল থেকেই মিলেছে, তাই তারে কিছু যায়নি। - প্রথম চেষ্টা — ::1. তালিকার family মেনে IPv6 socket বানিয়ে
[::1]:47301-এ connect. কিন্তু server শুধু IPv4127.0.0.1-এ bind করেছে, তাই এই ঠিকানায় কেউ শুনছে না; kernel RST দিল,connect()পেলECONNREFUSED. - দ্বিতীয় চেষ্টা — 127.0.0.1. ব্যর্থ socket বন্ধ করে (নইলে descriptor leak হতো) তালিকার পরের সারি নেওয়া হলো। এবার IPv4 socket, handshake সফল।
- থামা। একটি সফল হতেই loop থেকে বেরিয়ে আসা — বাকি ঠিকানা চেষ্টার দরকার নেই।
- পরিষ্কার করা।
freeaddrinfo()দিয়ে পুরো তালিকা মুক্ত, হাতে শুধু সংযুক্ত descriptor. পুরো কোডে কোথাওAF_INETবাAF_INET6হাতে লেখা নেই — একই কোড শুধু-IPv6 সার্ভারেও চলবে।
এবার এই ধাঁচটিই চালিয়ে দেখা যাক। একটি প্রোগ্রামে fork() দিয়ে দুটি process: parent হলো server, child হলো client. দুটোই একই open_tcp() ফাংশন ব্যবহার করে — server "127.0.0.1" দিয়ে bind+listen করে (hints-এ AI_PASSIVE থাকলেও host দেওয়ায় সেটি নিষ্ক্রিয়; ইচ্ছা করেই সব interface-এ খুলিনি), client "localhost" resolve করে তালিকা ধরে connect করে। Client তিনটি লাইন পাঠায়, server প্রতিটি ফেরত দেয় (echo). লাইন পড়ার কাজটি fdopen() আর fgets()-এর হাতে দিয়েছি — stdio-র buffer নিজেই অংশে অংশে আসা বাইট জুড়ে একটি পূর্ণ লাইন বানায়, যা অংশ ৫-এর সমস্যার একটি সমাধান।
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <sys/wait.h>
static int open_tcp(const char *host, const char *port, int server) { /* try every result */
struct addrinfo *res, *p, hints = { .ai_family = AF_UNSPEC, .ai_socktype = SOCK_STREAM };
hints.ai_flags = server ? AI_PASSIVE : 0; int e, fd = -1, one = 1; /* IPv4 or IPv6, TCP */
if ((e = getaddrinfo(host, port, &hints, &res)) != 0) { puts(gai_strerror(e)); return -1; }
for (p = res; p != NULL && fd < 0; p = p->ai_next) {
char ip[64]; getnameinfo(p->ai_addr, p->ai_addrlen, ip, sizeof ip, NULL, 0, NI_NUMERICHOST);
if ((fd = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) < 0) continue;
if (server) setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
int ok = server ? bind(fd, p->ai_addr, p->ai_addrlen) == 0 && listen(fd, 8) == 0
: connect(fd, p->ai_addr, p->ai_addrlen) == 0;
printf("%s: try %-9s -> %s\n", server ? "server" : "client", ip, ok ? "OK" : strerror(errno));
if (!ok) { close(fd); fd = -1; } /* failed: try the next address */
}
freeaddrinfo(res); return fd;
}
int main(void) {
setvbuf(stdout, NULL, _IONBF, 0); int ls = open_tcp("127.0.0.1", "47301", 1), n = 0;
char line[64]; const char *msg[] = { "salam\n", "kemon achen?\n", "bye\n" };
if (ls >= 0 && fork() == 0) { /* child process = client */
FILE *in = fdopen(open_tcp("localhost", "47301", 0), "r"); /* NULL if it failed */
for (int i = 0; in && i < 3 && write(fileno(in), msg[i], strlen(msg[i])) > 0
&& fgets(line, sizeof line, in); i++)
printf("client: sent %zu bytes, echo back: %s", strlen(msg[i]), line);
_exit(0);
}
FILE *in = fdopen(accept(ls, NULL, NULL), "r"); /* serve ONE client, then stop */
while (in && fgets(line, sizeof line, in) && write(fileno(in), line, strlen(line)) > 0) n++;
wait(NULL); printf("server: echoed %d lines, then read() returned 0 (EOF) -> close\n", n);
}আউটপুট:
server: try 127.0.0.1 -> OK
client: try ::1 -> Connection refused
client: try 127.0.0.1 -> OK
client: sent 6 bytes, echo back: salam
client: sent 13 bytes, echo back: kemon achen?
client: sent 4 bytes, echo back: bye
server: echoed 3 lines, then read() returned 0 (EOF) -> close
দ্বিতীয় আর তৃতীয় লাইন হুবহু ওপরের stepper: চালানোর পরিবেশে “localhost” আগে ::1 দিয়েছে, সেখানে কেউ না থাকায় Connection refused, তারপর 127.0.0.1-এ সফল। যে client শুধু তালিকার প্রথম ঠিকানা চেষ্টা করত, সে এখানেই ব্যর্থ হতো — অথচ server দিব্যি চালু। “kemon achen?\n” ১৩ বাইট (\n সহ), echo-ও ঠিক সেই লাইন। শেষ লাইনটি server ছাপে wait()-এর পরে, তাই সবসময় সবার শেষে আসে; client _exit() করতেই তার socket বন্ধ হয়ে FIN যায়, আর server-এর fgets() EOF পেয়ে loop শেষ করে। লক্ষ করুন, server একজন client-কে সেবা দিয়েই থেমেছে — একসাথে অনেকজনকে সামলানো পর্ব ৪৩-এর বিষয়।
IPv6 socket কি IPv4-ও নেয়? Server যদি AF_INET6 socket-এ :: (সব ঠিকানা)-য় bind করে, Linux-এ ডিফল্টে (net.ipv6.bindv6only = 0) সেটি IPv4 সংযোগও নেয় — IPv4 client-কে দেখায় IPv4-mapped ঠিকানা ::ffff:192.0.2.7 রূপে। এটি নিয়ন্ত্রণ করে socket option IPV6_V6ONLY. কিন্তু ডিফল্ট OS-ভেদে আলাদা — Windows আর কিছু BSD-তে ডিফল্ট “শুধু IPv6” বলে জানা যায়। তাই dual-stack server-এর নিরাপদ ধাঁচ: হয় IPV6_V6ONLY নিজে স্পষ্ট করে বসানো, নয়তো getaddrinfo(NULL, port, AI_PASSIVE)-এর সব ফলাফলে আলাদা socket খুলে (IPv6-টিতে V6ONLY = 1) শোনা।
৫ · Partial read আর write — চিরচেনা বাগ
পর্ব ২৯-এ পড়েছেন, TCP একটি byte stream — বার্তার কোনো সীমানা রাখে না। প্রোগ্রামারের ভাষায় এর মানে খুব কঠিন একটি সত্য: read(fd, buf, 10000) মানে “সর্বোচ্চ ১০,০০০ বাইট দাও”, “ঠিক ১০,০০০ বাইট দাও” নয়। Receive buffer-এ যা আছে — ১ বাইট থেকে চাওয়া পরিমাণ পর্যন্ত যেকোনো সংখ্যা — read() তাই দিয়েই ফেরে। কিছুই না থাকলে অপেক্ষা করে, আর অন্য পক্ষ বন্ধ করে দিলে ফেরত দেয় ০ — এটাই EOF.
| read() ফেরত দিল | মানে | কী করবেন |
|---|---|---|
n > 0 | n বাইট এসেছে — চাওয়ার চেয়ে কম হতে পারে | যা এসেছে জমান; বার্তা পূর্ণ না হলে আবার পড়ুন |
0 | EOF: অন্য পক্ষ লেখার দিক বন্ধ করেছে (FIN) | আর ডেটা আসবে না; আধখানা বার্তা থাকলে সেটা ভুল |
-1, errno == EINTR | Signal এসে অপেক্ষা ভেঙেছে | আবার ডাকুন |
-1, EAGAIN/EWOULDBLOCK | Non-blocking socket-এ এখন কিছু নেই, বা timeout | পরে আবার (পর্ব ৪৪), বা timeout হিসেবে সামলান |
-1, অন্য errno | আসল ভুল, যেমন ECONNRESET | সংযোগ বন্ধ করুন, কারণ log করুন |
এই বাগ ধরা কঠিন, কারণ loopback-এ বা একই LAN-এ ছোট বার্তা প্রায় সবসময় একবারেই চলে আসে। কোড পরীক্ষায় পাস করে, তারপর দূরের সার্ভারে, ধীর মোবাইল নেটওয়ার্কে বা বড় বার্তায় হঠাৎ আধখানা ডেটা নিয়ে ভাঙে। তাই নিচের পরীক্ষায় ইচ্ছা করে receive buffer ছোট (SO_RCVBUF = ২০৪৮) করা হয়েছে, যাতে loopback-এও সমস্যাটা চোখে পড়ে। প্রেরক একটিমাত্র write()-এ ১০,০০০ বাইট পাঠায়; প্রাপক প্রথমে “ভুল” কোডের মতো একবার পড়ে, তারপর একটি সঠিক readn() loop দিয়ে বাকিটা জোড়ে। আগে ধাপগুলো দেখে নিন:
| ডাক | |
| read() ফেরত দিল | |
| হাতে আছে | |
| কী বুঝব |
- এক read, অর্ধেকও নয়। প্রাপক ১০,০০০ চাইল, পেল ২০৪৮ — receive buffer ছোট বলে প্রেরক একবারে এর বেশি পাঠাতে পারেনি (flow control, পর্ব ৩১)। “একটি read = একটি বার্তা” ভাবা কোড এখানে অসম্পূর্ণ ডেটা নিয়ে এগিয়ে যেত।
- readn শুরু। বাকি ৭৯৫২ বাইটের জন্য loop: প্রতিবার buffer-এর যেখানে শেষ লেখা হয়েছে তার পর থেকে (
buf + got), আর যতটুকু বাকি (n − got) ততটুকু চাওয়া। পেল আরও ২০৪৮। - আবার। প্রাপক পড়ায় buffer খালি হলো, window খুলল, প্রেরকের kernel আরেক দফা পাঠাল। মোট ৬১৪৪।
- আবার। মোট ৮১৯২। প্রেরকের প্রোগ্রাম এতক্ষণ ধরে তার একটিমাত্র
write()-এর ভেতরে অপেক্ষা করছে — kernel বাকি বাইট send buffer-এ রাখতে না পারা পর্যন্ত। - শেষ টুকরো। বাকি ছিল ১৮০৮, এলো ১৮০৮। হিসাব মেলে: ৪ × ২০৪৮ + ১৮০৮ = ১০,০০০।
- শেষ।
got == n, loop থামল। মাঝপথে প্রেরক বন্ধ করলেread()০ দিত, আর readn কম সংখ্যা ফেরত দিয়ে জানাত “বার্তা অসম্পূর্ণ” — caller-কে সেটা যাচাই করতেই হবে।
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define N 10000
static ssize_t readn(int fd, char *buf, size_t n) { /* keep reading until n bytes or EOF */
size_t got = 0;
while (got < n) {
ssize_t r = read(fd, buf + got, n - got);
if (r < 0 && errno == EINTR) continue; /* interrupted by a signal: retry */
if (r <= 0) break; /* error, or EOF (peer closed) */
printf(" readn: read() returned %4zd, so far %4zu\n", r, got += (size_t)r);
}
return (ssize_t)got;
}
int main(void) {
struct sockaddr_in a = { .sin_family = AF_INET, .sin_port = htons(47302) };
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
int ls = socket(AF_INET, SOCK_STREAM, 0), small = 2048, one = 1; static char msg[N], buf[N];
setsockopt(ls, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
setsockopt(ls, SOL_SOCKET, SO_RCVBUF, &small, sizeof small); /* copied to accepted socket */
if (bind(ls, (struct sockaddr *)&a, sizeof a) < 0 || listen(ls, 1) < 0) return 1;
if (fork() == 0) { /* child = sender */
int c = socket(AF_INET, SOCK_STREAM, 0);
if (connect(c, (struct sockaddr *)&a, sizeof a) < 0) _exit(1);
_exit(write(c, msg, N) == N ? 0 : 2); /* ONE write() of 10,000 bytes */
}
int s = accept(ls, NULL, NULL), st; usleep(100000); /* let some data arrive first */
ssize_t n = read(s, buf, N); /* naive: "one read = one message" */
printf("naive: read(fd, buf, 10000) returned %zd\n", n);
ssize_t rest = n < 0 ? 0 : readn(s, buf + n, N - (size_t)n);
wait(&st); printf("total %zd of %d bytes, sender exit code %d\n", n + rest, N, WEXITSTATUS(st));
}আউটপুট:
naive: read(fd, buf, 10000) returned 2048
readn: read() returned 2048, so far 2048
readn: read() returned 2048, so far 4096
readn: read() returned 2048, so far 6144
readn: read() returned 1808, so far 7952
total 10000 of 10000 bytes, sender exit code 0
১০০ মিলিসেকেন্ড অপেক্ষার পরেও প্রথম read() পেল মাত্র ২০৪৮ বাইট — একবারের লেখা ১০,০০০ বাইট পাঁচ টুকরোয় এসেছে (২০৪৮ × ৪ + ১৮০৮)। readn-এর “so far” হিসাব প্রথম read-এর পরের বাকি ৭৯৫২ বাইটের; দুটো মিলিয়ে ১০,০০০। শেষ লাইনের sender exit code 0 মানে প্রেরকের একটিমাত্র write() পুরো ১০,০০০ ফেরত দিয়েছিল — blocking socket-এ Linux সাধারণত সব বাইট kernel-এ না তোলা পর্যন্ত লেখককে আটকে রাখে। ২০৪৮ সংখ্যাটা কোথা থেকে? Linux SO_RCVBUF-এ দেওয়া মান দ্বিগুণ করে রাখে আর তার একটি অংশ নিজের হিসাবের কাজে লাগায় (man 7 socket); এই পরিবেশে প্রতি দফায় window দাঁড়িয়েছে ২০৪৮-এ। অন্য kernel বা অন্য সেটিং-এ টুকরোর মাপ আলাদা হবে — কিন্তু “টুকরো হতে পারে”, এই সত্যটা বদলাবে না।
লেখার দিক — writen
write()-ও চাওয়ার চেয়ে কম বাইট লিখে ফিরতে পারে: non-blocking socket-এ send buffer ভরে গেলে, signal এসে অপেক্ষা ভাঙলে, বা timeout হলে। ওপরের পরীক্ষায় blocking socket-এ পুরোটা লেখা হয়েছে ঠিকই, কিন্তু সাবধানী কোড সেটা ধরে নেয় না। সঠিক ধাঁচ readn-এর জমজ:
ssize_t writen(int fd, const void *p, size_t n) { /* সব n বাইট লিখবে, নয়তো -1 */
const char *b = p; size_t left = n;
while (left > 0) {
ssize_t w = write(fd, b, left);
if (w < 0) { if (errno == EINTR) continue; return -1; }
b += w; left -= (size_t)w;
}
return (ssize_t)n;
}
Python-এর sendall() বা Go-র Write ভেতরে ঠিক এই loop চালায়; C-তে আপনাকে নিজে লিখতে হয়। EINTR নিয়ে একটি কথা: Linux-এ signal handler SA_RESTART দিয়ে বসালে অনেক system call নিজেই আবার শুরু হয়, তবু সব call নয় আর সব OS-এ নয় — তাই loop-এ EINTR সামলানো সস্তা বীমা (বিস্তারিত C কোর্সের signal আর low-level I/O পর্বে)।
৬ · বার্তার সীমানা — delimiter আর length prefix
readn জানে কত বাইট পড়তে হবে। কিন্তু সেটা জানবে কীভাবে? Byte stream-এর ওপর বার্তা চিনতে application-কে নিজের একটি নিয়ম বানাতে হয় — একে বলে message framing, পর্ব ৩৪-এ protocol নকশার অংশ হিসেবে দেখেছেন। প্রচলিত দুটি উপায়:
Delimiter — শেষ-চিহ্ন
প্রতিটি বার্তার শেষে একটি নির্দিষ্ট চিহ্ন, যেমন \n বা \r\n. প্রাপক চিহ্ন না পাওয়া পর্যন্ত জমাতে থাকে। মানুষের পড়ার মতো, debug সহজ — HTTP/1.1-এর header, SMTP, Redis-এর protocol এভাবে চলে (পর্ব ৩৬)। সমস্যা: বার্তার ভেতরেই চিহ্ন থাকলে escape করতে হয়, আর সর্বোচ্চ লাইন-দৈর্ঘ্য না বাঁধলে কেউ চিহ্ন না পাঠিয়ে মেমরি ভরাতে পারে।
Length prefix — আগে দৈর্ঘ্য
প্রতিটি বার্তার আগে একটি নির্দিষ্ট মাপের দৈর্ঘ্য-ঘর, যেমন ৪ বাইট network byte order-এ। প্রাপক আগে readn দিয়ে ঠিক ৪ বাইট, তারপর ঠিক ততগুলো বাইট পড়ে। যেকোনো বাইনারি ডেটা চলে, পার্স দ্রুত। TLS record, HTTP/2 frame (পর্ব ৩৭) এই পরিবারের। সমস্যা: দৈর্ঘ্য-ঘর মিথ্যা বলতে পারে।
বাইনারি protocolএবার length-prefix framing চালিয়ে দেখি। প্রেরক তিনটি বার্তা পাঠায়, কিন্তু বাইটগুলো লেখে দুই কিস্তিতে: প্রথম কিস্তি শেষ হয় দ্বিতীয় বার্তার ৪-বাইট header-এর ঠিক মাঝখানে, তারপর ১০০ মিলিসেকেন্ড বিরতি। শেষে একটি “মিথ্যা” header — দাবি করে ১০ লাখ বাইট আসছে। প্রাপকের নিয়ম: header-এর দৈর্ঘ্য MAXMSG (৬৪)-এর বেশি হলে বিশ্বাস না করে প্রত্যাখ্যান। এখানে socketpair() ব্যবহার করেছি — একই যন্ত্রের দুটি জোড়া socket, framing-এর নিয়ম TCP-তেও হুবহু একই।
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define MAXMSG 64 /* never trust a length that came from the network */
static int readn(int fd, void *p, size_t n) { /* 1 = got all n bytes, 0 = EOF/error */
for (ssize_t r; n > 0; n -= (size_t)r, p = (char *)p + r) {
if ((r = read(fd, p, n)) <= 0) return 0;
if ((size_t)r < n) printf(" (short read: got %zd of %zu bytes)\n", r, n);
}
return 1;
}
static size_t put(char *out, const char *s, uint32_t len) { /* [4-byte length][text] */
uint32_t h = htonl(len); memcpy(out, &h, 4); memcpy(out + 4, s, strlen(s)); /* big-endian */
return 4 + strlen(s);
}
int main(void) {
int sv[2]; char buf[MAXMSG + 1], out[128]; uint32_t h;
if (socketpair(AF_UNIX, SOCK_STREAM, 0, sv) < 0) { perror("socketpair"); return 1; }
size_t a = put(out, "salam", 5), b = a + put(out + a, "kemon achen?", 12);
b += put(out + b, "bye", 3); b += put(out + b, "", 1000000); /* a lying header */
if (fork() == 0) { /* child = sender */
if (write(sv[1], out, a + 2) < 0) _exit(1); /* frame 1 + first 2 bytes of header 2 */
usleep(100000); _exit(write(sv[1], out + a + 2, b - a - 2) < 0); /* pause, the rest */
}
close(sv[1]);
while (readn(sv[0], &h, 4)) {
if ((h = ntohl(h)) > MAXMSG) { printf("frame says %u bytes > %d: reject\n", h, MAXMSG); break; }
if (!readn(sv[0], buf, h)) break;
buf[h] = '\0'; printf("frame %2u bytes: \"%s\"\n", h, buf); /* we add the '\0' */
}
close(sv[0]); wait(NULL);
}আউটপুট:
frame 5 bytes: "salam"
(short read: got 2 of 4 bytes)
frame 12 bytes: "kemon achen?"
frame 3 bytes: "bye"
frame says 1000000 bytes > 64: reject
প্রথম বার্তা ঠিকঠাক: header 00 00 00 05, তারপর ৫ বাইট “salam”. দ্বিতীয় বার্তার header-এর মাত্র ২ বাইট এসেছিল — read() ২ দিয়েই ফিরেছে (short read: got 2 of 4), আর readn loop বিরতির পরে বাকি ২ বাইট পড়ে header সম্পূর্ণ করেছে। যে কোড ধরে নিত “৪ চাইলে ৪ পাব”, সে এখানে 00 00 আর আবর্জনা মিলিয়ে একটি ভুল দৈর্ঘ্য বানাত, তারপর থেকে পুরো stream-এর ছন্দ হারাত। তৃতীয় বার্তা আবার স্বাভাবিক। শেষ header-এর দাবি ১০,০০,০০০ বাইট — বিশ্বাস করে সেই মাপের buffer নিলে বা buf-এ (৬৫ বাইট) পড়তে গেলে মেমরি নষ্ট বা buffer overflow; তাই সীমা দেখে প্রত্যাখ্যান। লক্ষ করুন, “salam” পড়ার পর buf[h] = '\0' আমরা নিজে বসিয়েছি — নেটওয়ার্কের বাইটে কোনো null terminator থাকে না, আর থাকলেও তার ওপর ভরসা করা যায় না।
নেটওয়ার্ক থেকে আসা কোনো সংখ্যা বিশ্বাস করবেন না। দৈর্ঘ্য, গণনা, offset, index — সবকিছু যাচাই করুন: সর্বোচ্চ সীমার ভেতরে কি না, আর যতটুকু বাইট আসলে এসেছে তার সাথে মেলে কি না। দুটি দৈর্ঘ্য যোগ বা গুণ করার আগে integer overflow ভাবুন। buf[n] = '\0' করতে চাইলে read()-এ sizeof buf - 1 পর্যন্ত চাইবেন। আর sizeof একটি array-তে কাজ করে, function-এ পাঠানো pointer-এ নয় — সেখানে দেয় pointer-এর মাপ (৮)। এই ভুলগুলোর ফল কী হতে পারে, ভাবনার প্রশ্ন ৩-এর Heartbleed-এর গল্পে দেখবেন। C-র দিক থেকে বিস্তারিত: স্ট্রিং আর নিরাপদ কোডিং.
৭ · ভুল সামলানো — errno, SIGPIPE, close আর timeout
Socket-এর প্রায় প্রতিটি call ব্যর্থ হতে পারে, আর ব্যর্থ হলে সাধারণত −১ ফেরায়, কারণের সংখ্যাটি বসায় thread-নিজস্ব চলক errno-তে। perror("connect") সেটিকে লেখায় রূপ দিয়ে stderr-এ ছাপে, strerror(errno) লেখাটি ফেরত দেয়। একটি নিয়ম মনে রাখুন: errno দেখতে হবে ব্যর্থতার ঠিক পরেই — মাঝে অন্য কোনো library call (এমনকি printf) errno বদলে দিতে পারে। আর সফল call errno-কে শূন্য করে না, তাই আগে ফেরত মান দেখে তবেই errno. C-তে ভুল সামলানোর সাধারণ কৌশল জানতে C কোর্সের এরর হ্যান্ডলিং পর্ব দেখুন।
| errno | কোথায় | মানে |
|---|---|---|
| ECONNREFUSED | connect | গন্তব্য port-এ কেউ শুনছে না, RST এসেছে। |
ETIMEDOUT | connect, read | কোনো উত্তর নেই — প্রায়ই firewall SYN ফেলে দিচ্ছে, বা যন্ত্র বন্ধ। |
| EADDRINUSE | bind | এই ঠিকানা আর port ইতিমধ্যে ব্যবহৃত। |
EACCES | bind | অনুমতি নেই — যেমন সাধারণ ব্যবহারকারী হিসেবে ১০২৪-এর নিচের port. |
| ECONNRESET | read, write | অন্য পক্ষ RST পাঠিয়েছে — হঠাৎ বাতিল। |
| EPIPE | write, send | এমন সংযোগে লেখা, যার অন্য দিক আর পড়বে না। |
EINTR | অপেক্ষা করে এমন যেকোনো call | Signal এসে অপেক্ষা ভেঙেছে — আবার চেষ্টা। |
EAGAIN / EWOULDBLOCK | read, write, accept | Non-blocking-এ এখন সম্ভব নয়, অথবা socket timeout পার হয়েছে। |
SIGPIPE — বন্ধ সংযোগে লিখলে প্রোগ্রাম মরে যেতে পারে
অন্য পক্ষ সংযোগ বন্ধ করে চলে গেছে, আর আপনি তখনো লিখছেন। প্রথম লেখা প্রায়ই “সফল” হয় — বাইটগুলো শুধু নিজের kernel-এর buffer-এ গেছে। অন্য পক্ষের kernel সেই ডেটা পেয়ে দেখে socket নেই, উত্তরে RST দেয়। এরপরের লেখায় Unix ঐতিহ্য অনুযায়ী kernel process-কে SIGPIPE signal পাঠায়, যার ডিফল্ট কাজ — process শেষ করে দেওয়া, কোনো error বার্তা ছাড়াই। Shell-এর pipe-এ (yes | head) এই আচরণ সুবিধাজনক, কিন্তু server-এ একজন client হঠাৎ চলে গেলে পুরো server নিঃশব্দে মরে যাওয়া ভয়ংকর। প্রতিকার:
- প্রতিটি
send()-এ MSG_NOSIGNAL: signal নয়, শুধুEPIPEফেরত। Linux-এ আছে আর POSIX-এর নতুন সংস্করণে মানদণ্ডভুক্ত; macOS-এ ঐতিহ্যগতভাবে এর বদলে socket optionSO_NOSIGPIPE. - পুরো process-এ উপেক্ষা: শুরুতেই
signal(SIGPIPE, SIG_IGN);— তারপর সব লেখা শুধুEPIPEদেবে। অনেক server এটাই করে।
Signal-এর OS-দিক অপারেটিং সিস্টেম কোর্সের Signal পর্বে আছে।
চারটি ভুল, নিজের চোখে
নিচের প্রোগ্রাম loopback-এ ইচ্ছা করে চারটি ভুল ঘটায় আর প্রতিটির errno-র নাম ছাপে (strerrorname_np(), glibc ২.৩২ থেকে আছে, GNU-নির্দিষ্ট): (১) কেউ শুনছে না এমন port-এ connect; (২) চালু listener-এর port-এ আবার bind, SO_REUSEADDR ছাড়া ও সহ; (৩) listener বন্ধ, কিন্তু আগের একটি সংযোগ TIME-WAIT-এ পড়ে আছে — তখন আবার bind; (৪) অন্য পক্ষ বন্ধ করার পর MSG_NOSIGNAL দিয়ে দুবার send().
#define _GNU_SOURCE /* for strerrorname_np() (glibc 2.32+) */
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
static struct sockaddr_in A; /* the address used by the next call */
#define SA (struct sockaddr *)&A
static int tcp_at(int port, int reuse) { /* new TCP socket for 127.0.0.1:port */
A = (struct sockaddr_in){ .sin_family = AF_INET, .sin_port = htons(port) };
A.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
int fd = socket(AF_INET, SOCK_STREAM, 0);
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof reuse); return fd;
}
static void show(const char *what, int rc) {
printf("%-46s -> %s\n", what, rc == 0 ? "OK" : strerrorname_np(errno)); }
int main(void) {
show("1. connect to 127.0.0.1:47399 (nobody listens)", connect(tcp_at(47399, 0), SA, sizeof A));
int ls = tcp_at(47304, 1);
show("2. bind+listen 47304", bind(ls, SA, sizeof A) || listen(ls, 8));
show(" bind 47304 again, no SO_REUSEADDR", bind(tcp_at(47304, 0), SA, sizeof A));
show(" bind 47304 again, SO_REUSEADDR", bind(tcp_at(47304, 1), SA, sizeof A));
int c = tcp_at(47304, 0); connect(c, SA, sizeof A);
close(accept(ls, NULL, NULL)); close(c); close(ls); /* server side closed first */
usleep(50000); /* -> TIME-WAIT on port 47304 */
show("3. listener gone, TIME-WAIT left; no REUSEADDR", bind(tcp_at(47304, 0), SA, sizeof A));
show(" same, with SO_REUSEADDR", bind(tcp_at(47304, 1), SA, sizeof A));
ls = tcp_at(47305, 1); bind(ls, SA, sizeof A); listen(ls, 8);
c = tcp_at(47305, 0); connect(c, SA, sizeof A);
close(accept(ls, NULL, NULL)); usleep(50000); /* the peer closes */
show("4. send #1 after peer close (MSG_NOSIGNAL)", send(c, "x", 1, MSG_NOSIGNAL) < 0);
usleep(50000); /* the peer's RST arrives */
show(" send #2 after the RST (MSG_NOSIGNAL)", send(c, "x", 1, MSG_NOSIGNAL) < 0);
}আউটপুট:
1. connect to 127.0.0.1:47399 (nobody listens) -> ECONNREFUSED
2. bind+listen 47304 -> OK
bind 47304 again, no SO_REUSEADDR -> EADDRINUSE
bind 47304 again, SO_REUSEADDR -> EADDRINUSE
3. listener gone, TIME-WAIT left; no REUSEADDR -> EADDRINUSE
same, with SO_REUSEADDR -> OK
4. send #1 after peer close (MSG_NOSIGNAL) -> OK
send #2 after the RST (MSG_NOSIGNAL) -> EPIPE
(১) RST এসেছে, তাই ECONNREFUSED — পর্ব ২৯-এ দেখা ঘটনাই, এবার নাম ধরে। (২) গুরুত্বপূর্ণ ফল: একটি চালু listener-এর port-এ SO_REUSEADDR দিয়েও bind করা যায় না। এই option অন্যের চালু server-এর port দখলের অনুমতি নয়। (৩) এটাই SO_REUSEADDR-এর আসল কাজ: server-এর দিক আগে বন্ধ করেছিল, তাই তার পুরোনো সংযোগ 127.0.0.1:47304-এ TIME-WAIT-এ আছে। Option ছাড়া restart করা server ৬০ সেকেন্ড EADDRINUSE পেত; option সহ সঙ্গে সঙ্গে OK. (৪) অন্য পক্ষ বন্ধ করার পরের প্রথম send() “OK” — মাত্র এক বাইট নিজের kernel-এ জমা হয়েছে, বিপদের খবর তখনো আসেনি। সেই বাইট পৌঁছাতেই অন্য পক্ষ RST দিল, আর দ্বিতীয় send() পেল EPIPE. MSG_NOSIGNAL না থাকলে এখানেই SIGPIPE-এ process শেষ হয়ে যেত, শেষ লাইনটি ছাপাই হতো না। (একই অবস্থায় কিছু ক্ষেত্রে বা অন্য OS-এ ECONNRESET-ও আসতে পারে; দুটোকেই “সংযোগ গেছে” ধরে সামলান।)
close বনাম shutdown
close(fd) descriptor ছেড়ে দেয়; এটাই শেষ descriptor হলে kernel FIN পাঠায় (receive buffer-এ না-পড়া ডেটা থাকলে Linux FIN-এর বদলে RST পাঠায়)। কিন্তু fork()-এর পর parent আর child দুজনের কাছেই একই socket-এর descriptor থাকে — একজন close() করলে সংযোগ বন্ধ হয় না, শুধু তার কপি যায়। shutdown(fd, SHUT_WR) descriptor-এর সংখ্যা না দেখেই লেখার দিকে FIN পাঠায়, আর পড়ার দিক খোলা রাখে — পর্ব ২৯-এর half-close. “আমার অনুরোধ শেষ, উত্তর দিন” বোঝাতে এটাই সঠিক যন্ত্র; তারপরও descriptor মুক্ত করতে শেষে close() লাগে।
Timeout — চিরকাল অপেক্ষা নয়
Blocking read() অন্য পক্ষ চুপ থাকলে অনন্তকাল অপেক্ষা করতে পারে — নীরব, অর্ধেক-খোলা সংযোগে (পর্ব ২৯) এটা সত্যিই ঘটে। দুটি সহজ উপায়:
struct timeval tv = { .tv_sec = 5, .tv_usec = 0 }; /* ৫ সেকেন্ড */
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof tv); /* read: -1, errno EAGAIN/EWOULDBLOCK */
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof tv); /* write-এর জন্য */
SO_RCVTIMEO পার হলে read() −১ ফেরত দেয়, errno EAGAIN বা EWOULDBLOCK. connect()-এর timeout আলাদা বিষয়: কোনো উত্তর না এলে Linux ডিফল্টে কয়েকবার SYN আবার পাঠায় (tcp_syn_retries = ৬ হলে মোট প্রায় দুই মিনিট)। নিজের মাপের connect timeout-এর বহনযোগ্য উপায় হলো socket-কে non-blocking করে connect() (ফেরত দেয় EINPROGRESS), তারপর poll() দিয়ে নির্দিষ্ট সময় লেখার-প্রস্তুতির অপেক্ষা, শেষে getsockopt(SO_ERROR) দিয়ে ফল দেখা। poll() আর non-blocking socket-এর পুরো গল্প পর্ব ৪৪-এ।
৮ · একজন-একজন করে সেবা, নিরাপদ server আর টুল
Iterative server — সহজ, কিন্তু একজন ধীর হলে সবাই আটকে
এই পর্বের server-এর কাঠামো এমন: for (;;) { c = accept(ls); serve(c); close(c); }. একজন client-কে পুরো সেবা দিয়ে তবেই পরের accept(). লেখা সহজ, বোঝা সহজ, কোনো race condition নেই। কিন্তু একজন client যদি ধীর হয় — ধীর মোবাইল নেটওয়ার্কে আছে, বা ইচ্ছা করে প্রতি মিনিটে এক বাইট পাঠায় — তাহলে serve() তার read()-এ আটকে থাকে, আর বাকি সবাই accept queue-তে লাইনে দাঁড়িয়ে। Queue ভরে গেলে নতুনরা সংযোগই পায় না।
পাড়ার একমাত্র ফটোকপির দোকানের কথা ভাবুন, যেখানে একজনই কর্মী। তিনি একজন গ্রাহকের কাজ পুরো শেষ করে তবেই পরের জনকে ধরেন। সবাই দ্রুত হলে ব্যবস্থাটা চমৎকার। কিন্তু একজন গ্রাহক যদি কাউন্টারে দাঁড়িয়ে ব্যাগ থেকে একটা একটা করে পাতা বের করেন, পেছনের লাইন থেমে থাকে — কর্মী ব্যস্ত নন, শুধু অপেক্ষা করছেন। সমাধান হয় বেশি কর্মী (পর্ব ৪৩: fork, thread, thread pool), নয়তো এমন একজন দক্ষ কর্মী যিনি যার কাগজ তৈরি তার কাজ ধরেন আর বাকিদের অপেক্ষা করতে দেন (পর্ব ৪৪: select, poll, epoll).
তবু iterative server-এর জায়গা আছে: খুব ছোট, দ্রুত অনুরোধের সেবা, শেখা আর পরীক্ষা, বা যেখানে ক্রম বজায় রাখা জরুরি। আর এটি concurrent server-এর ভিত্তি — serve() ফাংশন বদলায় না, বদলায় শুধু কে কখন সেটা ডাকে। পর্ব ৪৫-এ এই সব একসাথে করে একটি ছোট HTTP server বানানো হবে।
নিরাপদ server-এর চেকলিস্ট
| বিষয় | ঝুঁকি | সাবধানী অভ্যাস |
|---|---|---|
| কোন ঠিকানায় bind | 0.0.0.0 (বা ::) মানে যন্ত্রের সব interface — Wi-Fi, অফিস LAN, public IP সবখান থেকে প্রবেশ। ডেভেলপমেন্টের ডেটাবেস বা admin panel এভাবে প্রায়ই ভুলে খোলা থাকে। | শুধু নিজের যন্ত্রের জন্য হলে 127.0.0.1/::1; বাইরের জন্য খুললে firewall আর authentication. ss -tlnp দিয়ে যাচাই। |
| Port ১০২৪-এর নিচে | Unix-ধর্মী OS-এ ঐতিহ্যগতভাবে শুধু root পারে (privileged port); পুরো server root হিসেবে চালানো বিপজ্জনক। | Linux-এ শুধু CAP_NET_BIND_SERVICE capability দেওয়া, বা bind করে privilege ছেড়ে দেওয়া, বা উঁচু port-এ চালিয়ে সামনে reverse proxy. (Linux-এ ip_unprivileged_port_start দিয়ে সীমা বদলানো যায়; macOS আর Windows-এর নিয়ম আলাদা।) |
| Buffer আর দৈর্ঘ্য | ছোট buffer-এ বড় লেখা, মিথ্যা দৈর্ঘ্য, '\0' ছাড়া স্ট্রিং। | প্রতিটি দৈর্ঘ্যের সীমা, sizeof buf - 1 পর্যন্ত পড়া, নিজে '\0', strcpy/sprintf-এর বদলে সীমাসহ ফাংশন। |
| সময় | নীরব বা ধীর client server আটকে রাখে। | SO_RCVTIMEO, প্রতি বার্তার সর্বোচ্চ মাপ আর সময়, প্রতি IP-তে সংযোগ-সীমা। |
| ভুল সামলানো | এক client-এর ভুলে পুরো server শেষ (SIGPIPE, unchecked −১)। | প্রতিটি call যাচাই, SIGPIPE উপেক্ষা, এক সংযোগের ভুলে শুধু সেই সংযোগ বন্ধ। |
টুল দিয়ে দেখা — ss, nc, telnet
নিজের server কোন ঠিকানায় শুনছে, তা দেখার সবচেয়ে দ্রুত উপায় ss (পর্ব ০৬)। -t TCP, -l শুধু LISTEN, -n নামের বদলে সংখ্যা, -p কোন process (অন্য ব্যবহারকারীর process দেখতে root লাগে)। একটি সাধারণ Linux যন্ত্রের উদাহরণ (এখানে চালানো নয়):
$ ss -tlnp # উদাহরণ আউটপুট
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 8 127.0.0.1:47301 0.0.0.0:* users:(("echo_srv",pid=2817,fd=3))
LISTEN 0 4096 0.0.0.0:8080 0.0.0.0:* users:(("devserver",pid=2901,fd=5))
LISTEN 0 244 [::1]:5432 [::]:* users:(("postgres",pid=1190,fd=6))
প্রথম সারি আমাদের echo server — শুধু loopback, backlog ৮ (Send-Q ঘরে)। দ্বিতীয় সারি সাবধান হওয়ার মতো: 0.0.0.0:8080 মানে LAN-এর যে কেউ এই ডেভেলপমেন্ট server-এ পৌঁছাতে পারে। তৃতীয়টি শুধু IPv6 loopback-এ। হাতে সংযোগ করে বাইট পাঠাতে netcat সবচেয়ে সুবিধার — শুধু নিজের যন্ত্রে বা নিজের অনুমতিপ্রাপ্ত পরীক্ষাগারে:
# উদাহরণ — নিজের যন্ত্রে, দুটি terminal-এ
$ nc -l 127.0.0.1 47301 # terminal ১: একটি অস্থায়ী server (OpenBSD nc-এর ধরন)
$ nc 127.0.0.1 47301 # terminal ২: client; লিখে Enter চাপলে অন্য পাশে দেখা যাবে
$ telnet 127.0.0.1 47301 # পুরোনো বিকল্প; বের হতে Ctrl+] তারপর quit
nc-এর কয়েকটি আলাদা রূপ আছে (OpenBSD netcat, GNU netcat, Nmap-এর ncat); শোনার option-এর লেখা (-l -p port বনাম -l port) তাদের মধ্যে আলাদা — নিজের যন্ত্রের man nc দেখে নিন।
অন্য ভাষায় একই কাজ
প্রায় সব ভাষার নেটওয়ার্ক library ভেতরে এই Berkeley API-ই ডাকে; শুধু কিছু ঝামেলা নিজে সামলে দেয়। এই সারণি তুলনার জন্য, এখানে চালানোর জন্য নয়:
| কাজ | C (POSIX) | Python | Go | Java |
|---|---|---|---|---|
| Server খোলা | getaddrinfo + socket + bind + listen | socket.create_server(("127.0.0.1", 47301)) | net.Listen("tcp", "127.0.0.1:47301") | new ServerSocket(47301, 8, addr) |
| Client সংযোগ | getaddrinfo + সব ফলাফলে loop + connect | socket.create_connection(("localhost", 47301), timeout=5) — ভেতরে সব ঠিকানা চেষ্টা করে | net.Dial("tcp", "localhost:47301") | new Socket("localhost", 47301) |
| সব বাইট লেখা | নিজের writen() | sock.sendall(data) | conn.Write(b) (সব লেখে বা error) | out.write(b) |
| ঠিক n বাইট পড়া | নিজের readn() | নিজের loop (recv-ও আংশিক দেয়) | io.ReadFull(conn, buf) | DataInputStream.readFully(buf) |
| ভুল জানানো | −১ আর errno | Exception (ConnectionRefusedError) | error মান | Exception (ConnectException) |
খেয়াল করুন: কোনো ভাষাই “একটি read = একটি বার্তা” দেয় না। Python-এর recv(1024)-ও আংশিক ফেরত দিতে পারে; framing সবখানে application-এর দায়িত্ব। C কোর্সের নেটওয়ার্ক প্রোগ্রামিং — TCP সকেট পর্বে C-র দিক থেকে আরেকটি সহজ পরিচয় আছে।
আরও গভীরে: Linux-এ socket একটি VFS file, তাই read()/write() ফাইলের মতোই socket layer হয়ে TCP-র tcp_recvmsg/tcp_sendmsg-এ পৌঁছায়; ডেটা kernel-এ sk_buff কাঠামোয় থাকে, আর প্রতিটি call-এ user আর kernel মেমরির মধ্যে কপি হয়। এই কপি আর system call-এর খরচ কমাতে নতুন ব্যবস্থা এসেছে — MSG_ZEROCOPY, sendfile(), আর io_uring. SO_REUSEPORT দিয়ে কয়েকটি process একই port-এ আলাদা listening socket রেখে নতুন সংযোগ ভাগ করে নিতে পারে। আর মানদণ্ডের দিক থেকে IPv6-এর socket API-র বর্ণনা RFC 3493 আর RFC 3542-এ, POSIX-এ socket()-পরিবারের আনুষ্ঠানিক সংজ্ঞা।
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- TCP server-এর socket call-এর ক্রম লিখুন। — socket → (setsockopt) → bind → listen → accept → read/write → close; client: socket → connect → write/read → close.
- listening socket আর accept()-এর ফেরত socket-এর পার্থক্য? — প্রথমটি শুধু নতুন সংযোগ নেয়; accept প্রতিটি সংযোগের জন্য আলাদা connected socket দেয়।
- htons() কেন লাগে? — Port আর ঠিকানা নেটওয়ার্কে big-endian; little-endian যন্ত্রে বাইট উল্টাতে হয়, নইলে ভুল port.
- recv(fd, buf, 100) কি সবসময় ১০০ বাইট দেয়? — না; TCP byte stream, কম দিতে পারে, ০ মানে EOF — loop আর framing লাগে।
- SO_REUSEADDR কী করে? — TIME-WAIT থাকলেও listening port-এ আবার bind করতে দেয়; চালু অন্য listener-এর port দখল করতে দেয় না।
৯ · AI-এর যুগে socket কোড — চলছে মানেই ঠিক নয়
“C-তে একটি TCP echo server লিখে দাও” — AI কয়েক সেকেন্ডে চলনসই কোড দেবে, আর loopback-এ সেটা চলবেও। সমস্যা হলো, socket কোডের বেশিরভাগ বাগ loopback-এ লুকিয়ে থাকে: ছোট বার্তা একবারে আসে, কেউ হঠাৎ চলে যায় না, IPv6 নিয়ে কেউ ভাবে না। AI-এর লেখা কোডে বারবার দেখা যায় এমন ভুল:
| AI-এর কোডে | সমস্যা | যাচাইয়ের উপায় |
|---|---|---|
একটি recv()-কে পুরো বার্তা ধরা | বড় বা দূরের বার্তায় আধখানা ডেটা | এই পর্বের মতো ছোট SO_RCVBUF দিয়ে বড় বার্তা পাঠিয়ে দেখুন |
gethostbyname(), inet_addr(), হাতে sockaddr_in | শুধু IPv4, পুরোনো ও বাদ-পড়া API | man getaddrinfo; “::1”-এ চালিয়ে দেখুন |
| তালিকার শুধু প্রথম ঠিকানা চেষ্টা | IPv6 ঠিকানা আগে এলে অকারণ ব্যর্থতা | ফলাফল ছাপিয়ে দেখুন, server শুধু IPv4-এ রেখে পরীক্ষা |
bind/listen/accept-এর ফেরত মান উপেক্ষা | নিঃশব্দ ব্যর্থতা, ভুল port-এ চলা | gcc -Wall -Wextra, প্রতিটি call-এর পাশে যাচাই আছে কি না পড়ুন |
htons() বাদ, বা sizeof(struct sockaddr) IPv6-এ | ভুল port, কাটা ঠিকানা | ss -tln-এ আসল port দেখুন |
SIGPIPE-এর কথা নেই; 0.0.0.0-এ bind | এক client চলে গেলে server মারা যায়; অকারণে সবার জন্য খোলা | Client-কে মাঝপথে মেরে দেখুন; ss -tlnp |
| বানানো option বা flag, OS-নির্দিষ্ট জিনিস সব জায়গার বলে চালানো | Compile হয় না, বা অন্য OS-এ ভিন্ন আচরণ | man 7 socket, man 7 tcp, man 7 ipv6; লক্ষ্য OS-এ পরীক্ষা |
AI-কে কোড চাওয়ার সময় শর্তগুলো স্পষ্ট লিখে দিন, আর পরীক্ষাও চেয়ে নিন:
AI-এর socket কোড চালানোর আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কমান্ড-জানা মানুষ থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১Socket API কেন ফাইলের মতো দেখতে? এই নকশা কোথায় সুবিধা দিল, আর কোথায় খোঁচা দেয়?
১৯৮০-র দশকের শুরুতে DARPA চেয়েছিল ARPANET-এর নতুন TCP/IP যেন সহজলভ্য Unix যন্ত্রে চলে। Berkeley-র Computer Systems Research Group সেই কাজ নেয়, আর ১৯৮৩ সালের 4.2BSD-তে socket API সামনে আসে। নকশাকারীদের সামনে Unix-এর সবচেয়ে সফল ধারণা ছিল: ফাইল, terminal, pipe — সবই একটি file descriptor, সবই read() আর write(). Socket-কেও তারা সেই পরিবারে আনলেন। ফল দাঁড়াল চমৎকার: যে প্রোগ্রাম stdin থেকে পড়ে stdout-এ লেখে, তাকে socket-এর descriptor দিলে সে নেটওয়ার্কেও কাজ করে। পুরোনো Unix-এর inetd ঠিক এভাবেই সাধারণ প্রোগ্রামকে নেটওয়ার্ক সেবা বানাত, আর আজকের poll/epoll ফাইল আর socket একসাথে দেখতে পারে।
কয়েক বছর পরে AT&T-র System V-তে আরেকটি নকশা এসেছিল — TLI (পরে XTI), STREAMS-ভিত্তিক, তত্ত্বে বেশি সাধারণ। কিন্তু BSD socket-এর সরলতা, BSD-র খোলা কোড আর পরে Linux ও Winsock-এর গ্রহণ socket-কেই কার্যত মানদণ্ড করে দেয়।
তবু “নেটওয়ার্ক = ফাইল” উপমা কয়েক জায়গায় খোঁচা দেয়। ফাইলের read() শেষ ছাড়া প্রায় সবসময় পুরোটা দেয়; socket-এর read() আংশিক দেয় — এই পর্বের সবচেয়ে বড় বাগের উৎস ঠিক এই প্রত্যাশার অমিল। ফাইল হঠাৎ “অন্য প্রান্তে বন্ধ” হয় না; socket হয়, তাই SIGPIPE আর ECONNRESET. ফাইলের লেখা মেমরির গতিতে চলে; socket-এর লেখা অনির্দিষ্টকাল আটকে থাকতে পারে, তাই timeout লাগে। আর ঠিকানার sockaddr cast-এর জটিলতা এসেছে সব ধরনের নেটওয়ার্ককে এক API-তে আনার চেষ্টা থেকে — C-তে তখনো void * ব্যাপক ছিল না বলে।
মূল উপলব্ধি: একটি ভালো abstraction পরিচিত ধারণা দিয়ে নতুন জিনিস সহজ করে, কিন্তু যেখানে নতুন জিনিসটি সত্যিই আলাদা (আংশিক পড়া, অন্য প্রান্তের ব্যর্থতা, অনির্দিষ্ট দেরি), সেখানে পার্থক্যগুলো প্রোগ্রামারকে জানতেই হয়।
প্র ০২“আমার কম্পিউটারে তো চলছে!” — partial read-এর বাগ কেন পরীক্ষায় ধরা পড়ে না, আর বাস্তবে কখন হঠাৎ দেখা দেয়?
Loopback-এ ছোট বার্তা পাঠালে প্রেরকের write() আর প্রাপকের read()-এর মাঝে প্রায় কিছুই ঘটে না: kernel পুরো বাইটগুলো একবারে প্রাপকের buffer-এ রেখে দেয়, প্রাপক জাগে, সব পায়। ফলে “একটি write = একটি read” ভুল ধারণাটি দিনের পর দিন সত্যি বলে মনে হয়। ডেভেলপারের যন্ত্রে, CI-তে, এমনকি একই data center-এ — সব পরীক্ষা সবুজ।
বাস্তব নেটওয়ার্কে কয়েকটি জিনিস টুকরো তৈরি করে। Ethernet-এর MTU ১৫০০ বলে একটি segment-এ প্রায় ১৪৪৮–১৪৬০ বাইটের বেশি যায় না; ১০ KB বার্তা কয়েকটি segment-এ ভাগ হয়, আর প্রাপক প্রথম segment পৌঁছাতেই জেগে উঠে read() করলে শুধু সেটুকুই পায়। একটি segment হারালে বাকিগুলো buffer-এ অপেক্ষা করে, প্রোগ্রাম পায় ফাঁকের আগ পর্যন্ত। প্রাপক ধীর হলে receive window ছোট হয় (পর্ব ৩১), ঠিক যেমন এই পর্বের পরীক্ষায় হয়েছে। Nagle-এর algorithm বা প্রেরকের দুটি দ্রুত write() উল্টো সমস্যাও তৈরি করে — দুটি বার্তা একটি read()-এ জুড়ে আসা। আর ধীর, পরিবর্তনশীল মোবাইল নেটওয়ার্কে এগুলো সবচেয়ে বেশি ঘটে — অর্থাৎ বাংলাদেশের মতো দেশে, যেখানে অনেক ব্যবহারকারী মূলত মোবাইল ডেটায়, বাগটি ঠিক আপনার আসল ব্যবহারকারীদের কাছেই দেখা দেবে।
লক্ষণগুলোও বিভ্রান্তিকর: মাঝে মাঝে JSON parse error, ছবির নিচের অংশ ধূসর, কখনো দুটি অনুরোধ একসাথে জুড়ে যাওয়া — সবই “মাঝে মাঝে”, তাই পুনরায় ঘটানো কঠিন। এ কারণেই অভিজ্ঞ প্রকৌশলীরা পরীক্ষার সময় ইচ্ছা করে টুকরো তৈরি করেন: ছোট SO_RCVBUF, প্রতি এক বাইট করে লেখা ফাঁকি-প্রেরক, Linux-এর tc netem দিয়ে দেরি আর ক্ষতি যোগ করা।
মূল উপলব্ধি: নেটওয়ার্ক কোডের সঠিকতা প্রমাণ হয় সবচেয়ে খারাপ পরিস্থিতিতে, সবচেয়ে সুবিধার পরিস্থিতিতে নয়। “টুকরো আসতে পারে” ধরে নিয়ে লেখা কোড loopback-এ যেমন চলে, দূরের মোবাইল ব্যবহারকারীর কাছেও তেমনই চলে।
প্র ০৩Heartbleed কী শেখায়? নেটওয়ার্ক থেকে আসা একটি দৈর্ঘ্য-সংখ্যা বিশ্বাস করার দাম কত বড় হতে পারে?
২০১৪ সালের এপ্রিলে প্রকাশ পায় OpenSSL-এর একটি বাগ, যার নাম দেওয়া হয় Heartbleed (CVE-2014-0160). TLS-এর একটি ছোট সুবিধা ছিল “heartbeat”: এক পক্ষ কিছু বাইট আর সেই বাইটের দৈর্ঘ্য পাঠায়, অন্য পক্ষ ঠিক ততগুলো বাইট ফেরত দেয় — “অন্য পাশে কেউ আছেন তো?” জাতীয় সংকেত। আক্রান্ত সংস্করণগুলোর কোড বার্তার ভেতরের দৈর্ঘ্য-ঘরটিকে বিশ্বাস করত, কিন্তু যাচাই করত না যে আসলে ততগুলো বাইট এসেছে কি না। কেউ এক বাইট পাঠিয়ে দাবি করত “৬৪ KB পাঠালাম”, আর server নিজের মেমরি থেকে ৬৪ KB কপি করে ফেরত দিত — যার মধ্যে থাকতে পারত অন্যদের পাসওয়ার্ড, session cookie, এমনকি server-এর গোপন চাবি।
বাগটি কয়েক লাইনের, কিন্তু OpenSSL তখন ইন্টারনেটের বিপুল সংখ্যক HTTPS server-এ ব্যবহৃত হতো; সারা পৃথিবীতে certificate বাতিল আর নতুন চাবি বানানোর হিড়িক পড়ে। এর পরের আলোচনায় বড় একটি দিক ছিল — এত গুরুত্বপূর্ণ খোলা-কোডের প্রকল্প চলছিল খুব অল্প মানুষ আর অল্প অর্থে; এরপর এমন প্রকল্পে সহায়তার জন্য শিল্প-উদ্যোগ গড়ে ওঠে।
এই পর্বের framing প্রোগ্রামে ঠিক এই শ্রেণির ভুলটিই ঠেকানো হয়েছে: header বলে ১০ লাখ বাইট, প্রাপক বিশ্বাস করে না। প্রতিরক্ষা কয়েক স্তরের — দৈর্ঘ্যের সর্বোচ্চ সীমা; দাবি করা দৈর্ঘ্য আর আসলে পাওয়া বাইটের তুলনা; দৈর্ঘ্য যোগ-গুণে integer overflow যাচাই; buffer সবসময় নিজের জানা মাপে। সাথে টুলের সাহায্য: compiler-এর -fsanitize=address এমন out-of-bounds পড়া চালানোর সময় ধরে ফেলে, আর fuzzing (এলোমেলো, বিকৃত ইনপুট দিয়ে লাখোবার চালানো) protocol parser-এর এই বাগ খুঁজতে খুব কার্যকর। Rust বা Go-র মতো memory-safe ভাষা এই শ্রেণির ভুল ভাষা-স্তরেই কঠিন করে দেয়।
মূল উপলব্ধি: নেটওয়ার্ক থেকে আসা প্রতিটি বাইট শত্রুর লেখা বলে ধরে নিন। দৈর্ঘ্য-ঘর কোনো তথ্য নয়, একটি দাবি — যাচাই না করে সেটার ওপর মেমরি কপি করা মানেই অন্যের হাতে নিজের মেমরির চাবি তুলে দেওয়া।
প্র ০৪gethostbyname থেকে getaddrinfo, তারপর Happy Eyeballs — একটি সাধারণ সংযোগ খোলার কাজ কেন এত জটিল হলো?
পুরোনো BSD যুগের gethostbyname() একটি নাম নিয়ে IPv4 ঠিকানার তালিকা দিত, ফলাফল রাখত একটি static মেমরিতে — তাই একসাথে দুটি thread ডাকলে একে অপরের ফল মুছে দিত। IPv6 এলে বোঝা গেল, ঠিকানার মাপ ৪ বাইট ধরে লেখা হাজারো প্রোগ্রাম বদলাতে হবে। তাই ১৯৯০-এর দশকের শেষে IETF IPv6-এর socket API সংজ্ঞায়িত করে (RFC 2133, পরে RFC 2553, শেষে ২০০৩ সালের RFC 3493), যার কেন্দ্রে getaddrinfo(): thread-নিরাপদ, family-নিরপেক্ষ, নাম আর service দুটোই একসাথে। POSIX পরে এটি গ্রহণ করে, আর gethostbyname-কে অপ্রচলিত ঘোষণা করে বাদ দেয়।
কিন্তু নতুন সমস্যা এলো। Dual-stack যন্ত্রে ফলাফলের তালিকায় সাধারণত IPv6 আগে থাকে। যদি কোনো নেটওয়ার্কে IPv6 “আছে” বলে মনে হয় কিন্তু আসলে কোথাও ভাঙা থাকে, তাহলে সরল loop প্রথমে IPv6-এ connect করে timeout পর্যন্ত অপেক্ষা করে — কয়েক সেকেন্ড থেকে মিনিটখানেক — তারপর IPv4. ব্যবহারকারীর চোখে “IPv6 চালু করলে ইন্টারনেট ধীর”, যা IPv6 গ্রহণের পথে বড় বাধা ছিল।
সমাধান “Happy Eyeballs” (RFC 6555, ২০১২; উন্নত রূপ RFC 8305, ২০১৭): প্রথম ঠিকানায় চেষ্টা শুরু করে খুব অল্প সময় (সুপারিশ অনুযায়ী প্রায় ২৫০ মিলিসেকেন্ড) উত্তর না এলে পরের family-তে সমান্তরাল চেষ্টা, যে আগে সফল হয় সেটাই ব্যবহার। আধুনিক browser আর অনেক ভাষার library (যেমন Go-র net.Dial) এর কোনো রূপ ব্যবহার করে বলে জানা যায়। এই পর্বের ক্রমিক loop তাই শেখার জন্য সঠিক ভিত্তি, কিন্তু ব্যবহারকারীমুখী client-এ ব্যর্থ ঠিকানায় অপেক্ষা কমাতে timeout বা সমান্তরাল চেষ্টা লাগে — যা পর্ব ৪৪-এর non-blocking connect দিয়ে বানানো যায়। বাংলাদেশেও মোবাইল অপারেটরদের নেটওয়ার্কে IPv6 ধীরে ধীরে ছড়াচ্ছে বলে জানা যায়; তাই দুই family-তেই নির্ভরযোগ্যভাবে চলা কোড এখন কোনো বিলাসিতা নয়।
মূল উপলব্ধি: “এক ঠিকানায় সংযোগ” আসলে “অনেক সম্ভাব্য পথের মধ্যে দ্রুত সফলটি বেছে নেওয়া”। API-র বিবর্তন — static IPv4 তালিকা থেকে family-নিরপেক্ষ তালিকা, তারপর সমান্তরাল চেষ্টা — এই বাস্তবতাকেই ধাপে ধাপে স্বীকার করেছে।
প্র ০৫Iterative server-এর দুর্বলতা কি শুধু গতির? “ধীর client” কীভাবে একটি আক্রমণে পরিণত হতে পারে, আর এর প্রতিরক্ষা কী?
Iterative server-এ এক সময়ে একজনই সেবা পায়। দুর্বলতাটা তাই গতির চেয়ে বেশি — প্রাপ্যতা (availability)-র। একজন client সংযোগ খুলে চুপ করে বসে থাকলে, বা প্রতি কয়েক সেকেন্ডে একটি করে বাইট পাঠালে, server তার read()-এ আটকে থাকে, আর বাকি সবাই অপেক্ষায়। এর জন্য আক্রমণকারীর বড় bandwidth লাগে না — একটি ধীর সংযোগই যথেষ্ট।
ঘটনাটি শুধু খেলনা server-এর নয়। ২০০৯ সালে নিরাপত্তা গবেষক Robert Hansen “Slowloris” নামের একটি কৌশল জনসমক্ষে আলোচনায় আনেন: অনেকগুলো HTTP সংযোগ খুলে প্রতিটিতে অনুরোধের header খুব ধীরে, একটু একটু করে পাঠানো, কখনো শেষ না করা। প্রতি-সংযোগে একটি করে process বা thread রাখা ওয়েব server-এর (যেমন তখনকার Apache-এর প্রচলিত সেটআপ) সব কর্মী আটকে যেত, অথচ নেটওয়ার্কে প্রায় কোনো ট্রাফিকই নেই। অর্থাৎ পর্ব ৪৩-এর “বেশি কর্মী” সমাধানও সীমাহীন নয় — কর্মীর সংখ্যা ফুরায়।
প্রতিরক্ষা কয়েক স্তরের, আর সবই এই পর্বের ধারণা থেকে আসে: প্রতিটি সংযোগে পড়ার timeout (SO_RCVTIMEO বা event loop-এর timer); পুরো অনুরোধ পাওয়ার একটি সর্বোচ্চ সময়, শুধু দুই বাইটের মাঝের সময় নয়; প্রতি IP থেকে একসাথে খোলা সংযোগের সীমা; বার্তার সর্বোচ্চ মাপ। আর স্থাপত্যের দিক থেকে event-driven server (পর্ব ৪৪), যেমন nginx, প্রতিটি নিষ্ক্রিয় সংযোগের জন্য শুধু একটু মেমরি খরচ করে, কোনো কর্মী আটকে রাখে না — তাই ধীর সংযোগ তার কাছে সস্তা। ১৯৯৯ সালে Dan Kegel-এর “C10K problem” লেখাটি এক যন্ত্রে দশ হাজার একসাথে সংযোগ সামলানোর প্রশ্ন তুলে এই স্থাপত্যের আলোচনাকে জনপ্রিয় করেছিল।
মূল উপলব্ধি: Server-এর নকশায় প্রশ্ন শুধু “কত দ্রুত” নয়, “একজন খারাপ client কতটা আটকে রাখতে পারে”। Timeout, সীমা আর অপেক্ষাকে সস্তা করা — এই তিনটিই একজনের ধীরতাকে সবার সমস্যা হতে দেয় না।
অনুশীলন
খাতা-কলম নিয়ে বসুন — হিসাব আর পূর্বানুমানই আসল কাজ।
-
হাতে বাইট আর টুকরো গোনা: (ক) x86 (little-endian) যন্ত্রে
htons(8080)-এর পরেsin_port-এর দুটি বাইট মেমরিতে কোন ক্রমে থাকবে?htonsভুলে গেলে server আসলে কোন port-এ শুনবে? (খ) ৩০০ বাইটের একটি বার্তার ৪-বাইট length header-এর বাইটগুলো লিখুন। (গ) প্রতিread()-এ সর্বোচ্চ ১৪৪৮ বাইট এলে ১০,০০০ বাইটের বার্তা পড়তে readn কমপক্ষে কতবারread()ডাকবে, শেষ টুকরো কত?(ক) 8080 = 0x1F90.
htons-এর পরে মেমরিতে1F 90(network order). ভুলে গেলে মেমরিতে থাকে little-endian90 1F, যা kernel নেটওয়ার্ক ক্রমে পড়ে 0x901F = ৩৬৮৯৫ — server শুনবে port 36895-এ।(খ) ৩০০ = 0x0000012C, network order-এ বাইট:
00 00 01 2C.(গ) ১০০০০ ÷ ১৪৪৮ = ৬.৯… → কমপক্ষে ৭ বার: ৬ × ১৪৪৮ = ৮৬৮৮, শেষ টুকরো ১০০০০ − ৮৬৮৮ = ১৩১২ বাইট. (“কমপক্ষে”, কারণ বাস্তবে আরও ছোট টুকরো আসতে পারে।)
-
আউটপুট অনুমান করুন: একটি যন্ত্রে (LAN ঠিকানা 192.168.1.10) server
127.0.0.1:47301-এ bind করে চালু। (ক) একই যন্ত্র থেকেnc 127.0.0.1 47301; (খ) একই যন্ত্র থেকেnc 192.168.1.10 47301; (গ) পাশের ল্যাপটপ থেকেnc 192.168.1.10 47301(firewall নেই)। প্রতিটির ফল কী?ss -tln-এ server-এর সারির Local Address কী দেখাবে? তারপর server0.0.0.0-এ bind করলে কী বদলাবে?(ক) সংযোগ হবে। (খ)
Connection refused— socket শুধু 127.0.0.1 ঠিকানায় বাঁধা, 192.168.1.10-এর ওই port-এ কেউ শুনছে না, তাই kernel RST দেয়, যদিও দুটোই একই যন্ত্র। (গ) একইভাবেConnection refused.ss -tln-এ Local Address:127.0.0.1:47301.0.0.0.0-এ bind করলে তিনটিই সংযোগ পাবে, আরssদেখাবে0.0.0.0:47301— অর্থাৎ LAN-এর যে কেউ এখন server-এ পৌঁছাতে পারে, তাই authentication আর firewall-এর কথা ভাবতে হবে। -
কোড লিখুন: একটি ফাংশন
int send_msg(int fd, const char *s, uint32_t len)লিখুন, যা ৪-বাইট network-order দৈর্ঘ্য আর তারপর বার্তা পাঠায়, partial write সামলায়, SIGPIPE-এ process মারা যেতে দেয় না, আর সফল হলে ০, ব্যর্থ হলে −১ ফেরত দেয়।static int sendn(int fd, const void *p, size_t n) { const char *b = p; while (n > 0) { ssize_t w = send(fd, b, n, MSG_NOSIGNAL); /* EPIPE, not SIGPIPE */ if (w < 0) { if (errno == EINTR) continue; return -1; } b += w; n -= (size_t)w; } return 0; } int send_msg(int fd, const char *s, uint32_t len) { uint32_t h = htonl(len); /* length in network byte order */ if (sendn(fd, &h, 4) < 0) return -1; return sendn(fd, s, len); }ভালো উন্নতি: দুবার
send-এর বদলে header আর বার্তা একটি buffer-এ জুড়ে (বাwritevদিয়ে) একবারে পাঠানো — তাতে Nagle-এর algorithm-এর সাথে অকারণ দেরি এড়ানো যায় (পর্ব ৩০–৩১)। -
বাগ খুঁজুন: AI-এর লেখা এই server অংশে অন্তত পাঁচটি সমস্যা আছে — খুঁজে বের করুন:
struct sockaddr_in a; a.sin_family = AF_INET; a.sin_port = 9000; a.sin_addr.s_addr = INADDR_ANY; int ls = socket(AF_INET, SOCK_STREAM, 0); bind(ls, (struct sockaddr *)&a, sizeof a); listen(ls, 5); int c = accept(ls, NULL, NULL); char buf[64]; read(c, buf, sizeof buf); printf("got: %s\n", buf);sin_port = 9000-এhtonsনেই — little-endian যন্ত্রে ভুল port (0x2328 → 0x2823 = ১০২৭৫)।aশূন্য দিয়ে শুরু হয়নি (= {0}নেই) — padding-এ আবর্জনা।INADDR_ANY= সব interface — দরকার না হলে127.0.0.1; আরSO_REUSEADDRনেই, restart-এEADDRINUSE.socket,bind,listen,accept,read— কোনোটির ফেরত মান যাচাই নেই।read()৬৪ বাইট পর্যন্ত পড়ে কিন্তু'\0'বসায় না, আর পুরো buffer ভরলে জায়গাও নেই —printf("%s")buffer-এর বাইরে পড়তে পারে। ঠিক:n = read(c, buf, sizeof buf - 1); if (n > 0) buf[n] = '\0';- একটি
read()-কে পুরো বার্তা ধরা হয়েছে — framing নেই; আরcকখনো বন্ধ হয়নি, শুধু একজন client-এর পরই থামে, IPv6 নেই (getaddrinfoব্যবহার করা উচিত)।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি TCP server-এর socket call-গুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- socket() — একটি TCP socket তৈরি
- setsockopt(SO_REUSEADDR)
- bind() — ঠিকানা আর port-এ বাঁধা
- listen() — LISTEN অবস্থা
- accept() — তৈরি সংযোগের নতুন socket
- read()/write(), তারপর close()
একটি TCP server-এ accept() ফেরত আসার মুহূর্তে কী সত্য?
- accept() এইমাত্র SYN+ACK পাঠিয়েছে
- Listening socket নিজেই এখন ESTABLISHED হয়ে গেছে
- Handshake আগেই শেষ; accept() একটি নতুন connected socket-এর descriptor দিয়েছে
- Client অন্তত এক বাইট ডেটা পাঠিয়েছে
একটি little-endian যন্ত্রে addr.sin_port = 8080; লেখা হলো (htons ছাড়া), তারপর bind আর listen সফল। Server আসলে কোন port-এ শুনবে? (8080 = 0x1F90)
- 8080
- 36895
- bind ব্যর্থ হতো, তাই কোনো port-এ নয়
- Kernel একটি এলোমেলো ephemeral port বেছে নেবে
একটি TCP socket-এ n = read(fd, buf, 4096) ফেরত দিল ০। এর অর্থ কী?
- এই মুহূর্তে কোনো ডেটা নেই, একটু পরে আবার পড়তে হবে
- একটি ভুল হয়েছে, errno দেখতে হবে
- Buffer খুব ছোট
- অন্য পক্ষ লেখার দিক বন্ধ করেছে (EOF)
একটি client getaddrinfo("localhost", "47301", ...) থেকে প্রথমে ::1, পরে 127.0.0.1 পেল। Server শুধু 127.0.0.1:47301-এ শুনছে। সঠিকভাবে লেখা client কী করবে?
- ::1-এ ECONNREFUSED পেয়ে socket বন্ধ করে 127.0.0.1 চেষ্টা করবে, আর সফল হবে
- ::1-এ ব্যর্থ হয়ে সাথে সাথে error দিয়ে বেরিয়ে যাবে
- getaddrinfo নিজেই কাজ-করা ঠিকানাটি শুধু ফেরত দেয়, তাই এমন হয় না
- ::1 দিয়েই সংযোগ হবে, কারণ IPv6 IPv4-কেও ধরে
একটি server-এ signal(SIGPIPE, SIG_IGN) বা MSG_NOSIGNAL নেই। একজন client হঠাৎ বন্ধ হয়ে গেলে, server সেই সংযোগে বারবার লিখতে থাকলে কী হতে পারে?
- সব লেখা সফল হবে, ডেটা শুধু হারিয়ে যাবে
- SIGPIPE-এর ডিফল্ট আচরণে পুরো server process শেষ হয়ে যেতে পারে
- Kernel নিজে client-এর সাথে আবার সংযোগ করবে
- write() চিরকাল আটকে থাকবে
একটি protocol-এ প্রতিটি বার্তার আগে ৪-বাইট দৈর্ঘ্য থাকে। প্রাপক header পড়ে দৈর্ঘ্য পেল 1,000,000; তার সর্বোচ্চ বার্তা ৬৪ KB. নিরাপদ আচরণ কোনটি?
- 1,000,000 বাইটের buffer malloc করে পুরোটা পড়া
- প্রথম ৬৪ KB পড়ে বাকিটা উপেক্ষা করা
- সীমা ছাড়িয়েছে বলে বার্তা প্রত্যাখ্যান করে সংযোগ বন্ধ করা
- ntohl ভুল হয়েছে ধরে নিয়ে বাইট উল্টে আবার পড়া
Server restart-এর পর bind() EADDRINUSE দিচ্ছে, যদিও পুরোনো process আর চলছে না; ss -tan-এ ওই port-এ কয়েকটি TIME-WAIT দেখা যাচ্ছে। সঠিক সমাধান কোনটি?
- প্রতিবার নতুন port নম্বর ব্যবহার করা
- Server root হিসেবে চালানো
- TIME-WAIT বন্ধ করার sysctl খোঁজা
- bind()-এর আগে setsockopt দিয়ে SO_REUSEADDR চালু করা
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- Server: socket → setsockopt(SO_REUSEADDR) → bind → listen → accept → read/write → close; client: socket → connect → write/read → close. Handshake করে kernel;
accept()শুধু তৈরি সংযোগ তুলে নতুন descriptor দেয়। - ঠিকানা:
sockaddr_in/sockaddr_in6/sockaddr_storage, cast করেstruct sockaddr *; port আর ঠিকানা network byte order-এ (htons/htonl); লেখা ঠিকানাinet_pton/inet_ntop. - getaddrinfo (AF_UNSPEC, AI_PASSIVE) দেয় ঠিকানার তালিকা — সব চেষ্টা, ব্যর্থ socket বন্ধ, শেষে
freeaddrinfo; ভুলের লেখাgai_strerror. - read() কম দিতে পারে, ০ মানে EOF; readn/writen loop আর framing (delimiter বা length prefix) ছাড়া TCP কোড ঠিক নয়। নেটওয়ার্কের দৈর্ঘ্য কখনো বিশ্বাস নয়।
- প্রতিটি call যাচাই, errno তৎক্ষণাৎ; SIGPIPE থেকে বাঁচতে
MSG_NOSIGNAL/SIG_IGN; timeout; দরকার ছাড়া0.0.0.0নয়। Iterative server-এ এক ধীর client সবাইকে আটকায়।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৪২ · UDP socket প্রোগ্রামিং — datagram, broadcast আর timeoutপরবর্তী পর্বসংযোগহীন socket — sendto, recvfrom আর নিজের timeout.
- পর্ব ৪৩ · একসাথে অনেক client — fork, thread আর thread pool সার্ভারএই মডিউলIterative server-এর সীমা ভাঙার প্রথম পথ।
- পর্ব ৪৪ · Event-driven সার্ভার — select, poll আর epollএই মডিউলNon-blocking socket, connect timeout আর হাজারো সংযোগ।
- পর্ব ২৯ · TCP সংযোগ — three-way handshake, state আর বন্ধ করাভিত্তিএই পর্বের প্রতিটি call-এর নিচে তারে কী ঘটে।
- পর্ব ৩৪ · Application layer — client-server, P2P আর protocol নকশাভিত্তিFraming আর নিজের protocol নকশা।
- পর্ব ৪৫ · প্রজেক্ট — C-তে একটি ছোট HTTP সার্ভারএই মডিউলএই পর্বের socket, framing আর ভুল সামলানো এক প্রজেক্টে।
- কম্পিউটার নেটওয়ার্ক · কোর্স হোম৬২টি পর্বপ্রজেক্ট HTTP server (পর্ব ৪৫) আর TLS (পর্ব ৪৮) পর্যন্ত পুরো রোডম্যাপ।
- C প্রোগ্রামিং · নেটওয়ার্ক প্রোগ্রামিং — TCP সকেটপাশের ট্র্যাকC-র দিক থেকে socket-এর প্রথম পরিচয়।
- অপারেটিং সিস্টেম · fork, exec আর waitপাশের ট্র্যাকএই পর্বের fork-ভিত্তিক পরীক্ষাগুলোর OS-দিক।