Part 02 · Machine গুলো কীভাবে কথা বলে

The TCP Handshake and Goodbye

TCPHandshake

একটা connection কীভাবে খোলে, ঠিক থাকে আর বন্ধ হয়, এক segment করে।

⏱ 1-2 ঘণ্টা · Station 19 / 153

তোমার request বের হওয়ার আগেই তিনটা packet আসা-যাওয়া করে ফেলেছে

তোমার API client GET /orders ডাকলো। হয়তো ভাবছো একটা message যায় আর একটা ফেরে। কিন্তু ওই request এর প্রথম byte পাঠানোর আগেই শুধু connection টা খুলতে তিনটা ছোট packet আসা-যাওয়া করে। আর কাজ শেষে বন্ধ করতে আরো চারটা পর্যন্ত যায়।

দ্রুত local network এ কেউ টের পায় না। ঢাকা আর Frankfurt এর একটা server এর মাঝে প্রতিটা round trip প্রায় 150 ms, তাই শুধু খোলার আনুষ্ঠানিকতাতেই 150 ms খরচ, তোমার API request টা দেখারও আগে। এজন্যই backend এর কাজে connection আবার ব্যবহার করা পারফরম্যান্সের সবচেয়ে বড় লাভগুলোর একটা।

এই lesson খোলার process টা (three-way handshake) ধাপে ধাপে দেখায়, এর সংখ্যাগুলোর মানে কী, আর বিদায়ের অংশটা, যেখানে TIME_WAIT নামের একটা অদ্ভুত state আছে যেটা প্রায় প্রত্যেক backend developer কে কখনো না কখনো বিভ্রান্ত করে।

সমস্যা: কথা বলার আগে দুজন অচেনাকে একমত হতে হয়

TCP reliable, ক্রমমতো পৌঁছানোর কথা দেয় (lesson 8)। কথা রাখতে হলে data চলার আগেই দুই মাথাকে কিছু বিষয়ে একমত হতে হয়:

কী নিয়ে একমত হতে হবেকেন
দুই পক্ষই আসলে আছে আর রাজিযে server listen করছে না তাকে data পাঠানো অপচয়
প্রতিটা দিকের জন্য একটা শুরুর sequence numberযাতে প্রতিটা byte এ নম্বর দেওয়া, প্রাপ্তি জানানো আর সাজানো যায়
দুজনই পাঠাতে আর নিতে পারেএকমুখী পথ connection না
কথাবার্তা কীভাবে আর কখন শেষ হবেযাতে কেউ চিরকাল অপেক্ষা না করে বা শেষ bytes না হারায়

Handshake প্রথম তিনটার উত্তর দেয়; বন্ধের ধাপগুলো শেষটার।

ফোন কল শুরু করার কথা ভাবো

  • তুমি ফোন করে বললে "হ্যালো, শুনতে পাচ্ছো?" (SYN)
  • বন্ধু বললো "হ্যাঁ, শুনছি, তুমি আমাকে শুনছো?" (SYN+ACK)
  • তুমি বললে "হ্যাঁ!" (ACK)

এরপরই আসল কথা শুরু, যে জন্য ফোন করেছিলে। প্রত্যেকে নিশ্চিত হয়েছে শব্দ দুই দিকেই যাচ্ছে।

ভদ্রভাবে ফোন রাখাও আলাদা একটা ছোট আয়োজন: "আচ্ছা, আমার কথা শেষ" / "আচ্ছা" … "আমারও শেষ" / "আচ্ছা, রাখি।" একজনের কথা শেষ হওয়ার পরেও আরেকজনের কিছু বলার থাকতে পারে, তাই প্রত্যেকে নিজের বিদায় নিজে বলে।

clientserverSYN seq=1000SYN+ACK ack=1001ACK ack=50011 round trip before any dataGET /200 OKFINFIN
Client পাঠায় SYN, server উত্তর দেয় SYN+ACK, আর client ACK দিয়ে নিশ্চিত করে: কোনো data র আগে একটা round trip। তারপর request আর response চলে। বন্ধ করতে প্রত্যেক পক্ষ FIN পাঠায় আর অন্যজন তার প্রাপ্তি জানায়।

Analogy টা কোথায় খাটে না। "হ্যালো" শুধু লাইন check করে না; এর সাথে যায় শুরুর নম্বর, যেটা দিয়ে প্রত্যেক পক্ষ bytes গুনবে। আর এই নম্বরগুলো 1 থেকে শুরু না, random ভাবে বাছা হয়, যাতে একজন attacker আন্দাজ করে অন্য কারো connection এ নকল data ঢুকিয়ে দিতে না পারে।

মূল জিনিসগুলো

  1. Flag। প্রতিটা TCP segment এ কিছু নিয়ন্ত্রণের flag থাকে। এখানে জরুরিগুলো: SYN ("চলো sequence number মিলিয়ে নিই"), ACK ("ack এর ঘরটা ঠিক আছে"), FIN ("আমার আর পাঠানোর কিছু নেই"), RST ("এই connection এখনই বাতিল")।
  2. Sequence আর acknowledgement number। প্রত্যেক পক্ষ নিজে যে bytes পাঠায় সেগুলোতে নম্বর দেয়, একটা random শুরুর sequence number (ISN) থেকে। ack নম্বর বলে: "এই নম্বরের আগের সবকিছু পেয়েছি; এরপর এটা পাঠাও"। SYN আর FIN প্রত্যেকে এক byte হিসেবে গোনা হয়, তাই এদেরও প্রাপ্তি জানাতে হয়।
  3. Three-way handshake।
    • Client → SYN, seq=x
    • Server → SYN+ACK, seq=y, ack=x+1
    • Client → ACK, ack=y+1 এখন দুই পক্ষ একে অপরের শুরুর নম্বর জানে, আর জানে দুই দিকই কাজ করছে। খরচ: client data পাঠানোর আগে একটা round trip (ওপরে TLS থাকলে আরো; lesson 11)।
  4. State। প্রত্যেক পক্ষ নাম দেওয়া state এর মধ্য দিয়ে যায়: server বসে থাকে LISTEN এ; handshake SYN_SENT আর SYN_RECEIVED পার হয়ে ESTABLISHED এ পৌঁছায়। Linux এ ss -tan দিয়ে এগুলো দেখা যায়।
  5. Four-way close। প্রতিটা দিক আলাদা ভাবে বন্ধ হয়। যে আগে বন্ধ করে সে FIN পাঠায় (→ FIN_WAIT_1), অন্যজন ACK দেয় (→ তার দিকে CLOSE_WAIT, তখনো data পাঠাতে পারে), তারপর নিজের FIN পাঠায় (→ LAST_ACK), যেটার ACK আসে।
  6. TIME_WAIT। যে আগে বন্ধ করেছে, সে connection টা পুরোপুরি ভুলে যাওয়ার আগে প্রায় এক মিনিট অপেক্ষা করে (Linux এ প্রায়ই 60 s)। যাতে এই connection এর দেরিতে আসা কোনো packet একই address আর port এর নতুন একটা connection এর অংশ বলে ভুল না হয়, আর শেষ ACK হারালে আবার পাঠানো যায়।
  7. RST। ভদ্র বিদায়ের বদলে যেকোনো পক্ষ RST পাঠিয়ে সাথে সাথে বাতিল করতে পারে। কেউ listen করছে না এমন port এ connection চাইলে একটা RST ফেরত আসে, সেটাই তুমি দেখো "connection refused" হিসেবে (lesson 3)।
আগে অনুমান করো

তোমার server Frankfurt এ; ঢাকা থেকে round trip 150 ms। একটা client প্রতিটা API call এর জন্য নতুন TCP connection খোলে, আর server উত্তর দিতে 10 ms নেয়। TLS বাদ দিলে client এর দৃষ্টিতে প্রতিটা call এ মোটামুটি কত সময় লাগে?

ভেতরে কী হয়: একটা connection এর পুরো জীবন

ধাপে ধাপে দেখো

খোলা, ব্যবহার, বন্ধ

ClientNetworkServer

ধাপ 1/6 Server listen করছে

Server port 443 এ listen() ডেকেছে। তার socket LISTEN এ, SYN এর অপেক্ষায়। অলস বসে থাকতে প্রায় কোনো খরচ নেই।

server
LISTEN

মাথায় রাখার ছবি

একটা TCP connection এর তিনটা পর্ব, আর প্রত্যেকটার খরচ আছে:

  1. খোলা: SYN → SYN+ACK → ACK। কাজের কোনো data র আগে একটা round trip।
  2. ব্যবহার: দুই দিকে নম্বর দেওয়া bytes আর প্রাপ্তি জানানো।
  3. বন্ধ: প্রত্যেক পক্ষের FIN আর ACK, তারপর যে আগে বন্ধ করেছে তার দিকে TIME_WAIT।

তাই backend এর কাজে মোটা দাগের নিয়ম হলো: connection খোলা দামি, খোলা রাখা সস্তা। Keep-alive, connection pool আর HTTP/2 সবই এটা নিয়ে।

চেষ্টা করো: এই connection কোন state এ?

প্রতিটা case চালিয়ে দেখো

TCP state পড়া (`ss -tan` যেভাবে দেখায়)

LISTEN  0  4096  0.0.0.0:443  0.0.0.0:*
কাজ করে: Port 443 এ নতুন connection এর অপেক্ষায় থাকা একটা server socket। Listen করা প্রতিটা service এর এরকম একটা।

গোড়া থেকে code

একটা TCP connection এর দুই মাথার ছোট একটা simulator। প্রত্যেক পক্ষ হিসাব রাখে তার state, পরের যে sequence number পাঠাবে, আর পরের যেটা আশা করছে (তার ack)। একটা table বলে দেয় প্রতিটা ধরনের segment পাঠালে বা পেলে state কীভাবে বদলায়। SYN আর FIN প্রত্যেকে এক byte হিসেবে গোনা। তারপর একটা পুরো connection চালানো হয়: handshake, একটা request আর response, আর একটা four-way close।

package main

import "fmt"

// One end of a TCP connection: its state, the next sequence number it will send,
// and the next one it expects to receive (which is what it acknowledges).
type side struct {
	state    string
	seq, ack int
}

type segment struct {
	flags    string // "SYN", "SYN+ACK", "ACK", "FIN", or "" for plain data
	data     string
	seq, ack int
}

// What a segment does to a state, when sent or received.
var onSend = map[string]string{"CLOSED/SYN": "SYN_SENT", "ESTABLISHED/FIN": "FIN_WAIT_1", "CLOSE_WAIT/FIN": "LAST_ACK"}
var onRecv = map[string]string{
	"LISTEN/SYN": "SYN_RECEIVED", "SYN_SENT/SYN+ACK": "ESTABLISHED", "SYN_RECEIVED/ACK": "ESTABLISHED",
	"ESTABLISHED/FIN": "CLOSE_WAIT", "FIN_WAIT_1/ACK": "FIN_WAIT_2", "FIN_WAIT_2/FIN": "TIME_WAIT", "LAST_ACK/ACK": "CLOSED",
}

// SYN and FIN each use up one sequence number, like one invisible byte.
func length(s segment) int {
	n := len(s.data)
	if s.flags == "SYN" || s.flags == "SYN+ACK" || s.flags == "FIN" {
		n++
	}
	return n
}

func (s *side) send(flags, data string) segment {
	seg := segment{flags, data, s.seq, s.ack}
	s.seq += length(seg)
	if next, ok := onSend[s.state+"/"+flags]; ok {
		s.state = next
	}
	return seg
}

func (s *side) receive(seg segment) {
	if length(seg) > 0 {
		s.ack = seg.seq + length(seg) // "I have everything before this number"
	}
	if next, ok := onRecv[s.state+"/"+seg.flags]; ok {
		s.state = next
	}
}

func main() {
	client := &side{"CLOSED", 1000, 0} // each side picks a random starting number
	server := &side{"LISTEN", 5000, 0}
	fmt.Printf("%-34s %-12s %s\n", "", "client", "server")
	fmt.Printf("%-34s %-12s %s\n", "       start", client.state, server.state)
	step := func(from, to *side, arrow, flags, data string) {
		seg := from.send(flags, data)
		to.receive(seg)
		label := flags
		if data != "" {
			label = fmt.Sprintf("%q", data)
		}
		desc := fmt.Sprintf("%s %s seq=%d", arrow, label, seg.seq)
		if flags != "SYN" {
			desc += fmt.Sprintf(" ack=%d", seg.ack)
		}
		fmt.Printf("%-34s %-12s %s\n", desc, client.state, server.state)
	}
	step(client, server, "C -> S", "SYN", "")
	step(server, client, "S -> C", "SYN+ACK", "")
	step(client, server, "C -> S", "ACK", "")
	step(client, server, "C -> S", "", "GET /")
	step(server, client, "S -> C", "", "200 OK")
	step(client, server, "C -> S", "FIN", "")
	step(server, client, "S -> C", "ACK", "")
	step(server, client, "S -> C", "FIN", "")
	step(client, server, "C -> S", "ACK", "")
}

Output (পাঁচটা language এই একই):

                                   client       server
       start                       CLOSED       LISTEN
C -> S SYN seq=1000                SYN_SENT     SYN_RECEIVED
S -> C SYN+ACK seq=5000 ack=1001   ESTABLISHED  SYN_RECEIVED
C -> S ACK seq=1001 ack=5001       ESTABLISHED  ESTABLISHED
C -> S "GET /" seq=1001 ack=5001   ESTABLISHED  ESTABLISHED
S -> C "200 OK" seq=5001 ack=1006  ESTABLISHED  ESTABLISHED
C -> S FIN seq=1006 ack=5007       FIN_WAIT_1   CLOSE_WAIT
S -> C ACK seq=5007 ack=1007       FIN_WAIT_2   CLOSE_WAIT
S -> C FIN seq=5007 ack=1007       TIME_WAIT    LAST_ACK
C -> S ACK seq=1007 ack=5008       TIME_WAIT    CLOSED

Output টা পড়ি:

  • প্রথম তিনটা সারি হলো handshake। তৃতীয়টার পরেই client ESTABLISHED এ পৌঁছায়। প্রতিটা নতুন connection এই round trip টা দেয়।
  • seq=1000 এর SYN এর পর ack=1001: SYN এক byte হিসেবে গোনা, তাই "1000 পেয়েছি, এরপর 1001 পাঠাও"।
  • "GET /" seq=1001, আর server এর পরের ack 1006: পাঁচটা byte, 1001 থেকে 1005। প্রতিটা byte গোনা হয়; কিছু হারালে TCP এভাবেই ঠিক জানে কী নেই।
  • বন্ধ: client এর FIN তাকে নেয় FIN_WAIT_1 এ আর server কে CLOSE_WAIT এ। ওই state এ server তখনো data পাঠাতে পারে। Server নিজের FIN পাঠালে যায় LAST_ACK এ।
  • শেষ সারি: server CLOSED, কিন্তু client বসে আছে TIME_WAIT এ। যে আগে বন্ধ করে, অপেক্ষার দাম সে দেয়। এজন্যই server সাধারণত client কে আগে বন্ধ করতে দেয়, আর যে client হাজার হাজার connection খোলে আর বন্ধ করে সে TIME_WAIT এ ভরে যায়।

আসল engineering

  • Connection আবার ব্যবহার করো। HTTP keep-alive, HTTP/2 আর database এর connection pool সবই আছে যাতে handshake এর দাম একবার দিয়ে connection অনেকবার ব্যবহার করা যায়। যে service প্রতিটা query তে নতুন করে connect করে, সে round trip নষ্ট করে আর port ফুরিয়ে ফেলতে পারে। (Module 3 lesson 10, Module 4 lesson 14।)
  • Server user এর কাছে রাখো, বা ওখানেই শেষ করো। CDN আর edge load balancer user এর কাছেই handshake (আর TLS) শেষ করে, তারপর তোমার server এর দিকে আগে থেকে খোলা connection ব্যবহার করে।
  • CLOSE_WAIT মানে তোমার code এ bug। অন্য পক্ষ বন্ধ করেছে; তোমার process করেনি। এমন response বা connection খোঁজো যেগুলো কখনো বন্ধ হয় না (না পড়া HTTP response body একটা পরিচিত উদাহরণ, বিশেষ করে Go তে)।
  • TIME_WAIT স্বাভাবিক, পরিমিত থাকলে। এটা নতুন connection কে পুরনো packet থেকে বাঁচায়। না বুঝে kernel এর knob ঘুরিয়ে এটা "ঠিক" করতে যেও না; বরং অল্প সময়ের connection এর সংখ্যা কমাও।
  • Connect এর সময়ের timeout জরুরি। এমন address এ connect করলে যেটা চুপচাপ packet ফেলে দেয়, SYN এর চেষ্টাগুলো হাল না ছাড়া পর্যন্ত আটকে থাকে (default এ প্রায়ই এক মিনিটের বেশি)। প্রতিটা client এ আলাদা করে connect timeout দাও (Module 7, lesson 4)।

যখন ভাঙে

প্রতিটা case চালিয়ে দেখো

একটা failure বাছো

dial tcp 10.0.9.4:5432: i/o timeout (75 s পরে)
ভাঙে: SYN এর কোনো উত্তরই আসেনি: একটা firewall চুপচাপ ফেলে দিচ্ছে, বা host বন্ধ। OS এক মিনিটের বেশি চেষ্টা করেছে। ছোট একটা connect timeout দাও (1-3 s) যাতে failure দ্রুত বোঝা যায়।

পড়ার আগে ভাবো

Handshake এ তিনটা message কেন লাগে? দুটো কি যথেষ্ট না: client হ্যালো বলে, server উত্তরে হ্যালো বলে?

উত্তর দেখো

দুটো হলে server কখনো জানতো না তার নিজের হ্যালো পৌঁছেছে কিনা। Client এর SYN প্রমাণ করে client পাঠাতে পারে আর server নিতে পারে। Server এর SYN+ACK প্রমাণ করে উল্টো দিকটা। কিন্তু শুধু client এর শেষ ACK ই server কে বলে "তোমার শুরুর নম্বর পেয়েছি", তাই server জানে দুই দিকই কাজ করছে আর দুজনই দুটো নম্বরে একমত। দুই পক্ষের নিশ্চিত হতে কমপক্ষে তিনটা message লাগে।

নিজেকে check করো

1. TCP handshake এর তিনটা message কী কী?

উত্তর

Client এর SYN (তার শুরুর sequence number সহ), server এর SYN+ACK (নিজের শুরুর নম্বর সহ, client এরটার প্রাপ্তি জানিয়ে), আর client এর ACK (server এরটার প্রাপ্তি জানিয়ে)।

2. শুরুর sequence number random কেন?

উত্তর

যাতে একজন attacker আন্দাজ করে connection এ নকল data ঢোকাতে না পারে, আর পুরনো connection এর packet নতুনটার সাথে গুলিয়ে না যায়।

3. তোমার server এ CLOSE_WAIT socket বাড়তে থাকলে সেটা কী বলে?

উত্তর

অন্য পক্ষ ওই connection গুলো বন্ধ করেছে, কিন্তু তোমার application কখনো নিজের দিক বন্ধ করেনি: তোমার code এ connection এর ফুটো।

4. প্রতিটা request এ নতুন connection খোলা ধীর কেন?

উত্তর

প্রতিটা নতুন connection কোনো request data পাঠানোর আগে handshake এর জন্য পুরো একটা round trip দেয় (ওপরে TLS থাকলে আরো), আর বন্ধ হলে পেছনে একটা TIME_WAIT রেখে যায়।

তোমার challenge: হারানো একটা SYN+ACK

Simulator টা বাড়াও যাতে network একটা segment ফেলে দিতে পারে।

  • Server এর প্রথম SYN+ACK ফেলে দাও।
  • Client, তখনো SYN_SENT এ, একটা timeout এর পর আবার SYN পাঠাবে (একই sequence number দিয়ে আবার send ডাকো; নম্বর যেন না এগোয়)।
  • Server, SYN_RECEIVED এ, আবার আসা SYN এর উত্তরে আরেকটা SYN+ACK দেবে।

চালানোর আগে আন্দাজ করো প্রতিটা সারির পর client আর server এর state কী হবে।

সফল হয়েছো যখন output এ ফেলে দেওয়া segment টা দেখা যায়, আবার পাঠানো SYN seq=1000 (1001 না), আর তারপর handshake শেষ হয়ে দুই পক্ষ ESTABLISHED, আগের মতোই একই sequence number সহ।

Mental model

একটা TCP connection খোলে SYN, SYN+ACK, ACK দিয়ে: প্রত্যেক পক্ষ একটা random শুরুর নম্বর জানায় আর নিশ্চিত করে দুই দিকই কাজ করছে, কোনো data র আগে একটা round trip খরচ করে। এরপরের প্রতিটা byte এ নম্বর দেওয়া আর প্রাপ্তি জানানো হয়। বন্ধ করতে প্রতিটা দিকে একটা FIN আর একটা ACK লাগে, আর যে আগে বন্ধ করে সে TIME_WAIT এ অপেক্ষা করে। খোলা দামি আর ধরে রাখা সস্তা, তাই connection আবার ব্যবহার করো, connect timeout দাও, আর CLOSE_WAIT এর স্তূপকে নিজের code এর bug হিসেবে দেখো।

Station stamp

Chapter শেষ করো

পড়া শেষ হলে Complete চাপো, progress save হবে। XP শুধু Pro তে — Pro থাকলে এই chapter এ +10 XP পেতে।

Pro নিয়ে XP পাও →