Part 01 · Backend Mindset

From a Script to a Server

ProcessServer

একবার চলা একটা script কে এমন program বানানো যেটা চলতেই থাকে আর request এর উত্তর দেয়।

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

তোমার script চলে। তোমার বন্ধু ব্যবহার করতে পারে না।

তুমি একটা ছোট program লিখলে যেটা একটা লেখার শব্দ গোনে। চালালে, 4 print করলো, শেষ। কাজের জিনিস!

এক বন্ধু এটা তার phone থেকে ব্যবহার করতে চায়। এক teammate তার website থেকে ডাকতে চায়। কেউই পারে না: তোমার program তোমার laptop এ 20 millisecond চলেছে, তারপর আর নেই। ডাকার মতো কিছুই ছিল না।

তুমি যত backend বানাবে, সবগুলো শুরু হয় এরকম একটা function থেকে। এই lesson এ দেখবো একটা program যখন আর এমন কিছু থাকে না যা তুমি চালাও, বরং এমন কিছু হয়ে যায় যা চলতেই থাকে আর অন্য program কে উত্তর দেয়, তখন কী বদলায়। এর নামই server।

সমস্যা: script শেষ হয়ে যায়

একটা script আর একটা server এর ভেতরে একই কাজের function থাকতে পারে। পার্থক্য হলো তার চারপাশের জীবনে।

ScriptServer
কীভাবে শুরু হয়তুমি চালাওকিছু একটা চালু করে (তুমি, systemd, Docker, একটা platform), আর এটা চলতেই থাকে
Input আসে কোথা থেকেCommand-line argument, একটা file, stdinNetwork দিয়ে আসা request
কতক্ষণ বাঁচেকাজ শেষ না হওয়া পর্যন্তথামতে না বলা পর্যন্ত, সম্ভব হলে মাসের পর মাস
কয়টা কাজএকটাহাজারটা, একটার পর একটা, আবার একসাথেও
কে ব্যবহার করতে পারেযে চালায়, ওই machine এযে কেউ যে এর address আর port এ পৌঁছাতে পারে
কী মনে রাখতে পারেবন্ধ হওয়ার পর কিছুই নাযা খুশি, যতক্ষণ process বেঁচে থাকে (আর থামলে হারায়, lesson 5)

Script কে server বানানো মানে মূলত ডান দিকের column টা: একটা address, কাজের জন্য অপেক্ষা করা একটা loop, আর কিছু গোলমাল হলেও চলতে থাকার নিয়ম।

চা-ওয়ালার কথা ভাবো

Script হলো বাসায় চা বানানো: পানি ফোটালে, এক কাপ বানালে, কেতলি ধুয়ে রাখলে, শেষ। এক ঘণ্টা পর পাশের বাসার কেউ চা চাইলে তখন কোনো চা বানানো চলছে না। তাকে এসে বলতে হবে আবার শুরু করতে।

Server হলো বাসস্ট্যান্ডের চায়ের দোকান। সকালে একটা চেনা জায়গায় খোলে, তারপর দোকানদার অপেক্ষা করেন। Customer আসে, এক কাপ বানানো হয়, দোকানদার আবার অপেক্ষায় ফেরেন। এক কাপের পর দোকান বন্ধ হয় না। বন্ধ হয় রাতে, হাতের কাপগুলো শেষ করে।

চায়ের recipe দুই জায়গায় একই। দোকানকে দোকান বানায় একটা ঠিক ঠিকানা, অপেক্ষা করা, আর খোলা থাকা।

scriptstartcount wordsprintexitthe process is gone; nobody can reach itserverstartlisten on a portwait → handle → waitclientsrequest 1request 2request 3
Script শুরু হয়, শব্দ গোনে, print করে, বন্ধ হয়ে যায়; এরপর আর পৌঁছানোর মতো কিছু থাকে না। Server শুরু হয়, একটা port এ listen করে, আর একই loop বারবার ঘোরে: request এর জন্য অপেক্ষা, সামলানো, আবার অপেক্ষা।

Analogy টা কোথায় খাটে না। চা-ওয়ালা একবারে একজনকে দেন। Server একসাথে অনেক request সামলাতে পারে (lesson 1), আর "ঠিকানা" টা ফুটপাতের একটা জায়গা না, দুটো সংখ্যার জোড়া, একটা IP আর একটা port (Module 2)। আর দোকানের মতো না, server কে আলাদা আলাদা জায়গায় কয়েকবার চালু করা যায়, হুবহু একই copy হিসেবে (lesson 5)।

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

  1. একটা process। তুমি program চালালে operating system একটা process বানায়: চলতে থাকা একটা instance, নিজের memory সহ। Script এর process শেষ হয় main return করলেই। Server এর process বানানোই হয় যাতে শেষ না হয়। (Module 4, lesson 2।)
  2. একটা port এ listen করা। Server operating system এর কাছে একটা IP address এ একটা port চায় (1 থেকে 65535 এর মধ্যে একটা সংখ্যা)। এরপর থেকে ওই IP আর port এ আসা connection গুলো এই process কে দেওয়া হয়। Port 0 মানে "যেকোনো ফাঁকা port আমাকে দাও", test এর সময় কাজে লাগে। (Module 4, lesson 4।)
  3. Accept loop। প্রতিটা server এর মূলে একটা loop: connection এর জন্য অপেক্ষা → request পড়া → কাজ করা → response লেখা → আবার অপেক্ষা। Framework এটা লুকিয়ে রাখে, কিন্তু এটা সবসময় আছে।
  4. লম্বা সময়ের state। Process চলতেই থাকে বলে দুই request এর মাঝে memory তে জিনিস রাখতে পারে: একটা counter, একটা cache, database connection এর একটা pool। কাজের, আবার বিপজ্জনক: process থামলে সব উধাও (lesson 4 আর 5)।
  5. Failure হয় request এর, process এর না। Script error পেলে সোজা crash করতে পারে। Server পারে না: একটা খারাপ request এর জন্য বাকি সবার কাজ বন্ধ হওয়া চলবে না। Server প্রতিটা request এর error আলাদা করে ধরে, log করে, একটা 500 ফেরত দেয়, আর চলতে থাকে।
  6. জেনেবুঝে থামানো। Server থামানো হয় একটা signal দিয়ে (Ctrl+C, একটা deploy)। ভালো server বন্ধ হওয়ার আগে হাতে থাকা request গুলো শেষ করে: graceful shutdown। (Module 4, lesson 13।)
আগে অনুমান করো

তুমি শব্দ গোনার script টাকে একটা server বানালে, যেটা কয়টা request সামলালো তাও গোনে। এটা 3টা request এর উত্তর দিলো, তারপর তুমি restart করে আরো 1টা পাঠালে। শেষ request এ এটা কী বলবে?

ভেতরে কী হয়: একটা server এর জীবন

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

চালু থেকে বন্ধ পর্যন্ত

Operating systemServer processClient

ধাপ 1/6 Process শুরু হয়

কেউ program টা চালায়। Operating system একটা process বানায়, code load করে, main কে ডাকে। এ পর্যন্ত একদম script এর মতোই।

state
starting

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

Server হলো তিনটা স্তরে মোড়ানো একটা script:

  1. একটা address: একটা IP আর একটা port, যাতে অন্যরা খুঁজে পায়।
  2. একটা loop: অপেক্ষা, সামলানো, উত্তর, আবার অপেক্ষা।
  3. একটা safety net: প্রতিটা request এর error ধরা, timeout, আর graceful shutdown, যাতে মাসের পর মাস চলে।

মাঝখানের কাজের function বদলায় না। কোনো framework এর "hello world" পড়ার সময় তিনটা স্তর খুঁজে দেখো: listen call টা হলো address, loop টা framework তোমার হয়ে চালায়, আর safety net সাধারণত দেয় middleware।

চেষ্টা করো: script নাকি server?

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

এটা script হবে নাকি server?

resize ./photos --width 800
কাজ করে: Script। একটাই কাজ, পরিষ্কার শেষ আছে, আর অন্য কারো এটা ডাকার দরকার নেই।

গোড়া থেকে code

একই countWords function দুইভাবে ব্যবহার:

  1. Script হিসেবে: একবার ডাকো, print করো, শেষ।
  2. Server হিসেবে: standard library দিয়ে একটা আসল HTTP server, এমন port এ যেটা OS বেছে দেয়। কয়টা request সামলালো তার একটা counter memory তে রাখে। হাতে লিখে তিনটা request পাঠানো হয়, তারপর বন্ধ করা হয়।
package main

import (
	"bufio"
	"fmt"
	"net"
	"net/http"
	"strings"
	"sync/atomic"
)

// The useful work. A script and a server can share exactly the same function.
func countWords(text string) int { return len(strings.Fields(text)) }

// get sends one request by hand and returns the response body.
func get(addr, path string) string {
	conn, err := net.Dial("tcp", addr)
	if err != nil {
		panic(err)
	}
	defer conn.Close()
	fmt.Fprintf(conn, "GET %s HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n", path)
	r := bufio.NewReader(conn)
	for { // skip the status line and headers: they end at the first empty line
		line, err := r.ReadString('\n')
		if line == "\r\n" || err != nil {
			break
		}
	}
	body, _ := r.ReadString(0)
	return body
}

func main() {
	// 1. As a script: get input, do the work once, print, and the program ends.
	fmt.Printf("script: count(%q) = %d, then exit\n", "the quick brown fox", countWords("the quick brown fox"))

	// 2. As a server: a process that stays alive and answers request after request.
	var served atomic.Int64 // state that lives as long as the process does
	mux := http.NewServeMux()
	mux.HandleFunc("/count", func(w http.ResponseWriter, r *http.Request) {
		n := served.Add(1)
		fmt.Fprintf(w, "%d words (request %d since start)", countWords(r.URL.Query().Get("text")), n)
	})
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "ok (request %d since start)", served.Add(1))
	})
	ln, err := net.Listen("tcp", "127.0.0.1:0") // port 0: let the OS pick a free port
	if err != nil {
		panic(err)
	}
	srv := &http.Server{Handler: mux}
	go srv.Serve(ln) // the accept loop: wait for a connection, handle it, wait again
	fmt.Println("server: listening on 127.0.0.1 (port chosen by the OS)")

	for _, path := range []string{"/count?text=hello+world", "/count?text=to+be+or+not+to+be", "/health"} {
		fmt.Printf("GET %-32s -> %s\n", path, get(ln.Addr().String(), path))
	}
	srv.Close()
	fmt.Printf("server: shutting down after %d requests\n", served.Load())
}

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

script: count("the quick brown fox") = 4, then exit
server: listening on 127.0.0.1 (port chosen by the OS)
GET /count?text=hello+world          -> 2 words (request 1 since start)
GET /count?text=to+be+or+not+to+be   -> 6 words (request 2 since start)
GET /health                          -> ok (request 3 since start)
server: shutting down after 3 requests

Output টা পড়ি:

  • script: একবার ডাকা, একটা উত্তর, আর আসল script হলে program এখানেই শেষ হতো। কথা বলার মতো আর কিছু চলছে না।
  • server: listening: process এর এখন একটা address আছে। অপেক্ষা করছে, প্রায় কোনো CPU খরচ ছাড়াই।
  • তিনটা request: প্রত্যেকটা একই loop ঘোরে। Counter 1 থেকে 3 এ ওঠে কারণ request এর মাঝে process বেঁচে থাকে আর তার memory রেখে দেয়।
  • /health: একই server এ দ্বিতীয় একটা route। আসল server এরকম একটা রাখে যাতে load balancer আর platform check করতে পারে সে বেঁচে আছে কিনা (Module 7, lesson 14)।
  • shutting down after 3 requests: থামানো এখন জেনেবুঝে করা একটা কাজ। Program আবার চালাও, counter শুরু হবে 1 থেকে: memory process এর।

আসল engineering

  • Process টা চালু রাখার জন্য কাউকে লাগে। Production এ server চালু আর আবার চালু করে একটা process manager: systemd, Docker, Kubernetes বা কোনো platform। Process crash করলে কয়েক সেকেন্ডের মধ্যে আবার চালু হয়। (Module 10।)
  • Configuration আসে বাইরে থেকে। Script প্রায়ই argument নেয়; server তার port, database URL আর secret পড়ে environment variable বা config file থেকে, যাতে একই build সব জায়গায় চলে। (Module 10, lesson 4।)
  • শুরুর খরচ একবারই। Server চালুর সময় configuration load করে, database connection pool খোলে, cache গরম করে, template compile করে, যাতে প্রতিটা request সস্তা হয়। এজন্যই deploy এর পরের প্রথম request টা একটু ধীর হতে পারে।
  • Print না, log করো। Script তার ফলাফল print করে। Server প্রতিটা request এর জন্য timestamp আর request id সহ structured log লেখে, কারণ তার terminal কেউ দেখছে না। (Module 7, lesson 13।)
  • থামার plan রাখো। Deploy এর সময় server বারবার থামে। SIGTERM সামলাও, নতুন কাজ নেওয়া বন্ধ করো, হাতে যা আছে শেষ করো, তারপর বন্ধ হও। (Module 4, lesson 13।)

যখন ভাঙে

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

একটা failure বাছো

listen tcp :8080: bind: address already in use
ভাঙে: আরেকটা process (প্রায়ই একই server এর পুরনো একটা copy) port টা ধরে আছে। ss -ltnp | grep 8080 বা lsof -i :8080 দিয়ে খুঁজে বের করো, থামাও, নয়তো অন্য port ব্যবহার করো।

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

তোমার word counter এর একটা 2 GB dictionary লাগে, load হতে 4 সেকেন্ড। Script হিসেবে প্রতিবার লাগে প্রায় 4 সেকেন্ড। Server হিসেবে চালুর পরের প্রথম request ও দ্রুত। কেন, আর তুমি নতুন কোন সমস্যা তৈরি করলে?

উত্তর দেখো

Server dictionary টা চালুর সময় একবার load করে, listen শুরু করার আগেই, তাই প্রতিটা request (প্রথমটাও) শুধু গোনার এক millisecond খরচ করে। নতুন সমস্যা: চালু হতে এখন 4 সেকেন্ড আর প্রতিটা copy তে 2 GB memory লাগে। Deploy আর restart ধীর, প্রতিটা বাড়তি copy তে 2 GB লাগে, আর load শেষ না হওয়া পর্যন্ত platform কে traffic পাঠানো থেকে বিরত রাখতে হবে। Readiness check এর কাজ এটাই (Module 7, lesson 14)।

নিজেকে check করো

1. কোন তিনটা জিনিস একটা function কে server বানায়?

উত্তর

একটা address (একটা IP আর port এ listen করা), request এর জন্য অপেক্ষা করে সামলানো একটা loop, আর চালু রাখার safety net (প্রতিটা request এর error ধরা, timeout, graceful shutdown)।

2. Port 0 তে listen করলে কী হয়?

উত্তর

Operating system কে বলা হয় যেকোনো ফাঁকা port বেছে দিতে। তারপর program পড়ে নেয় কোন port পেলো। Test এ কাজের, যাতে অনেকগুলো server ধাক্কা না খেয়ে চলতে পারে।

3. একটা খারাপ request এর জন্য server কেন কখনো বন্ধ হবে না?

উত্তর

কারণ process টা সবাই share করে। বন্ধ হলে বাকি প্রতিটা user disconnected হয়। খারাপ request পাবে একটা error response, আর server চলতে থাকবে।

4. Restart এর পর request counter আবার শূন্য থেকে কেন শুরু হলো?

উত্তর

Counter টা ছিল process এর memory তে। Restart নতুন memory সহ একটা নতুন process বানায়। যে data টিকে থাকতে হবে, সেটা process এর বাইরে রাখতে হয়।

তোমার challenge: একটা আসল কাজ দাও

Server এ দ্বিতীয় একটা endpoint যোগ করো, /top?text=..., যেটা লেখার সবচেয়ে বেশি আসা শব্দ আর সেটা কতবার এসেছে তা ফেরত দেবে। যেমন /top?text=to+be+or+not+to+be দেবে to (2)।

  • শব্দ গোনার logic সাধারণ function এ রাখো, যাতে script অংশও ডাকতে পারে।
  • এমন বানাও যাতে text ছাড়া request পায় 400 status সহ একটা পরিষ্কার error, আর server চলতে থাকে।

চালানোর আগে আন্দাজ করো দুটো শব্দ সমান হলে /top?text=b+a+b+a কী দেওয়া উচিত, আর একটা নিয়ম ঠিক করো (যেমন, যেটা আগে এসেছে)।

সফল হয়েছো যখন /top?text=to+be+or+not+to+be দেয় to (2), text ছাড়া /top দেয় status 400, তার পরের request ও ঠিকঠাক চলে, আর request counter সবগুলো গোনে।

Mental model

Script একটা কাজ করে শেষ হয়ে যায়। Server হলো একই কাজের code, একটা address, একটা loop আর একটা safety net এ মোড়ানো: একটা port এ listen করে, অপেক্ষা করে, একটা request সামলায়, আবার অপেক্ষা করে, মাসের পর মাস। বেঁচে থাকে বলে শুরুর খরচ একবার দিয়ে memory তে জিনিস রাখতে পারে, কিন্তু সেই memory process এর সাথেই মরে, আর প্রতিটা error কে একটা request এর মধ্যেই আটকে রাখতে হয়।

Station stamp

Chapter শেষ করো

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

Pro নিয়ে XP পাও →