TCP Echo Server
TCP Echo Server
socket/bind/listen/accept দিয়ে শুরু করে fork-per-connection, partial-send আর short-read bug, SO_REUSEADDR, আর epoll-ভিত্তিক concurrent version — একটা echo server-এর ভেতরে Level 7-এর পুরো sockets-api। nc দিয়ে acceptance test আর load-test-এ req/s printout দিয়ে শেষ।
কেন এই প্রজেক্ট
Sockets-api লেসনে socket, bind, listen, accept — ছয়টা function call- এর signature দেখেছি। Tcp-handshake লেসনে SYN, SYN-ACK, ACK-র sequence number। কিন্তু production server-এর ৯০% সমস্যা এই দুই জায়গার মাঝখানে: partial write, short read, zombie process, TIME_WAIT-এ bind failure, আর “এক client বেশি হলে” concurrency-র প্রশ্ন।
Echo server এই কারণেই textbook exercise — protocol zero, তাই মন
দিয়ে দেখার জিনিসটা শুধু transport layer-এর আচরণ। একটা write() call
সব data পাঠায় না এমন idea প্রথমবার শুনলে অবিশ্বাস্য লাগে; নিজের
server-এ ঘটিয়ে দেখার পর আর ভোলে না। শেষে epoll version লিখলে
nginx/redis-জাতীয় event-loop server-এর core model নিজের হাতে গড়া
হয়ে যায়।
ধাপে ধাপে
১. Blocking single-client echo
int fd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {0};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(7777);
bind(fd, (struct sockaddr *)&addr, sizeof addr);
listen(fd, 16);
for (;;) {
int c = accept(fd, NULL, NULL);
char buf[1024];
ssize_t n;
while ((n = read(c, buf, sizeof buf)) > 0)
write(c, buf, n); /* ⚠ ইচ্ছাকৃত ভুল — পরের ধাপ */
close(c);
}
চালান, দ্বিতীয় terminal থেকে nc 127.0.0.1 7777 — echo চলছে। এখন
আরেকটা nc খুলুন: ঝুলে আছে। Accept queue-তে বসে, কারণ server
একমাত্র client-কে serve করছে। এই limitation ঠিক করাটাই ধাপ ২ ও ৫।
২. Fork-per-connection + SIGCHLD reaping
signal(SIGCHLD, sigchld_handler);
void sigchld_handler(int sig) {
int saved = errno;
while (waitpid(-1, NULL, WNOHANG) > 0)
; /* loop — signal coalescing */
errno = saved;
}
while (...) {
int c = accept(listen_fd, NULL, NULL);
if (fork() == 0) {
close(listen_fd); /* child-এর listen fd দরকার নেই */
echo_loop(c);
_exit(0);
}
close(c); /* parent-এর copy ফেলে দিন */
}
দুটো খুঁটিনাটি: handler-এ loop করা — একসঙ্গে অনেক child মরলে signal number একবারই ডেলিভার হয়, এক waitpid-এ সব zombie যায় না। আর parent-এ connection fd close করা — না করলে fd table leak, আর client disconnect বোঝাও যাবে না (EOF আসবে না)।
Checkpoint: পরপর ২০ connection খুলে বন্ধ করুন, ps aux | grep defunct — একটাও থাকবে না। Handler comment করে আবার চালান — zombie
army দাঁড়িয়ে আছে, reaping-এর value দৃশ্যমান।
৩. Partial send + short read — classic bugs
/* ভুল সংস্করণ — write() সব পাঠায় না */
write(c, buf, n);
/* সঠিক: send-all loop */
ssize_t sent = 0;
while (sent < n) {
ssize_t k = write(c, buf + sent, n - sent);
if (k < 0) { if (errno == EINTR) continue; return -1; }
sent += k;
}
/* সঠিক: read-ও short হতে পারে — fixed-length frame-এ */
char hdr[4];
size_t got = 0;
while (got < 4) {
ssize_t k = read(c, hdr + got, 4 - got);
if (k == 0) return -1; /* peer closed — EOF */
if (k < 0 && errno != EINTR) return -1;
got += k;
}
Bug ঘটানোর রেসিপি: client-এ ছোট receive buffer দিন (SO_RCVBUF
কমিয়ে), বড় payload পাঠান, loopback-এও partial write ধরা পড়বে। Short
read দেখতে localhost-এ MTU-র চেয়ে বড় message পাঠান — TCP stream-এ
“message” বলে কিছু নেই, byte-এর ধারা যতক্ষণ application ভাগ না করে।
৪. SO_REUSEADDR
int one = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
Server kill করে সঙ্গে সঙ্গে restart করলে bind: Address already in use
— কারণ পুরনো connection-এর সংযোগ বিচ্ছিন্ন পাশ TIME_WAIT-এ বসে ২×MSL
সময়। Socket option-টা bind-এর আগে set করতে হয়। যাচাই: option ছাড়া
restart fail → netstat | grep TIME_WAIT দেখুন → option দিয়ে instant
restart। Production server-এর প্রায় প্রথম লাইনগুলোর একটা এর জন্যই।
৫. epoll concurrent version
int ep = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = listen_fd };
epoll_ctl(ep, EPOLL_CTL_ADD, listen_fd, &ev);
struct epoll_event events[64];
for (;;) {
int nfds = epoll_wait(ep, events, 64, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == listen_fd) {
int c = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK);
fcntl(c, F_SETFL, O_NONBLOCK);
struct epoll_event cev = { .events = EPOLLIN, .data.fd = c };
epoll_ctl(ep, EPOLL_CTL_ADD, c, &cev);
} else {
/* nonblocking read → EAGAIN পর্যন্ত drain, EOF-এ remove+close */
}
}
}
Fork version প্রতি connection-এ এক memory space; epoll version এক process-এ হাজার connection — state শুধু per-fd buffer-এ। Read-এ EAGAIN মানে “এখন আর data নেই”, EOF মানে fd remove করে close। Partial-write handle করতে per-connection outgoing buffer রাখেন, EPOLLOUT enable/ disable করে — echo-র জন্য ছোট message-এ বাদও দিতে পারেন, তবে করলে কেন বাদ দিলেন লিখে রাখুন।
৬. nc acceptance transcript
$ nc 127.0.0.1 7777
hello
hello
world with spaces
world with spaces
^C
দুই client parallel-এ টাইপ করে দুজনেই নিজের echo পাচ্ছে — এটাই
concurrency acceptance। আরেকটা টেস্ট: printf 'ab' | nc host port
— client বন্ধ করে দিলে server clean exit করে কি না (fd leak না,
zombie না)। Transcript দুটো জমা রাখুন, এটাই project-এর proof।
৭. Load test
# bench.py — 50 concurrent connections × 1000 echo round-trips each
import socket, time, threading
def worker(n):
s = socket.create_connection(("127.0.0.1", 7777))
for i in range(n):
s.sendall(b"x" * 256)
got = 0
while got < 256:
got += len(s.recv(4096))
s.close()
t0 = time.perf_counter()
threads = [threading.Thread(target=worker, args=(1000,)) for _ in range(50)]
[t.start() for t in threads]; [t.join() for t in threads]
dt = time.perf_counter() - t0
print(f"{50*1000/dt:.0f} req/s")
Fork version আর epoll version-এর req/s পাশাপাশি ছাপুন — gap টেবিলে থাকবে, ব্যাখ্যা (context switch + fork cost vs event loop) FINDINGS-এ।
নিজেকে চ্যালেঞ্জ করুন
- Graceful shutdown — SIGTERM-এ listener বন্ধ, চলতি connection drain করে তারপর exit
- Connection limit — max 100 active, বেশি হলে immediate RST বা polite banner+close
- Per-connection idle timeout —
EPOLL_CTL_MODদিয়ে timer, নিষ্ক্রিয় client ফেলা - Backpressure — slow client fast server-কে আটকে রাখতে পারে কি না, outgoing buffer cap দিয়ে প্রমাণ
- Edge-triggered epoll-এ transition — level-triggered code কী ভাঙে?
SO_LINGER, TCP_NODELAY experiment — Nagle delay কোন pattern-এ latency বাড়ায় measure করুন
যা এখানে শিখলেন, পরে যেখানে লাগবে
| এখানে | পরে |
|---|---|
| Partial send / short read discipline | HTTP server প্রজেক্ট — একই loop, উপরে parser |
| fd hygiene আর zombie reaping | Process supervisor, container runtime-এ PID management |
| Event-loop model | Redis/nginx architecture; async runtime-এর reactor |
| TIME_WAIT আর SO_REUSEADDR | Deploy script-এ restart race debug |
| Load-test methodology | Capacity planning — req/s ছাড়া latency distribution অর্থহীন |