แล็ป Network Programming — จาก TCP/UDP สู่โปรแกรมจริง
เราจะไม่ได้หยุดแค่เขียนโปรแกรมให้เชื่อมต่อกันได้ แต่จะเปิดดูสิ่งที่เกิดขึ้น ทำให้ข้อความขาด ทำให้ client หลุด และใช้หลักฐานอธิบายว่าทำไมโปรแกรมจึงมีพฤติกรรมเช่นนั้น
1Socket คือจุดเชื่อมระหว่าง Application กับ Transport Layer
ในเชิงระบบปฏิบัติการ socket คือ file descriptor ชนิดหนึ่ง — เขียน/อ่านผ่าน socket คล้ายเขียน/อ่านไฟล์ แต่ข้อมูลวิ่งผ่านเครือข่ายแทนที่จะเขียนลงดิสก์ การระบุ socket หนึ่งตัวให้ไม่ซ้ำกับตัวอื่นในเครื่องใช้ 5-tuple:
พารามิเตอร์หลักตอนสร้าง socket: address family (AF_INET = IPv4, AF_INET6 = IPv6) และ socket type (SOCK_STREAM = TCP, SOCK_DGRAM = UDP) — สองค่านี้กำหนดว่า socket ที่ได้จะมีพฤติกรรมแบบ connection-oriented/reliable หรือ connectionless/best-effort
2Client–Server Model
เกือบทุกแอปพลิเคชันเครือข่ายตั้งอยู่บน asymmetry ระหว่างสองบทบาท:
| Server | Client | |
|---|---|---|
| ใครเริ่มก่อน | รอ (passive open) — bind ที่ well-known port แล้วรอ | เริ่มเชื่อมต่อ (active open) — ต้องรู้ address+port ของ server ล่วงหน้า |
| อายุการทำงาน | รันตลอดเวลา (long-lived) รอรับหลาย connection ตามลำดับ/พร้อมกัน | มักสั้น — เชื่อม ทำงาน แล้วจบ |
| จำนวน | มักมีตัวเดียว (หรือกลุ่มเล็กหลัง load balancer) | มีได้จำนวนมาก เชื่อมต่อ server เดียวกันพร้อมกัน |
3TCP Socket Lifecycle
ฝั่ง server และ client เดินผ่านลำดับ syscall ที่ต่างกัน:
socket() → bind() → listen() → accept() → recv()/send() (วน) → close()
socket() → connect() → send()/recv() (วน) → close()
| Syscall | ใช้ที่ฝั่ง | หน้าที่ |
|---|---|---|
socket() | ทั้งคู่ | สร้าง socket descriptor ใหม่ ยังไม่ผูกกับ address ใดๆ |
bind() | Server (ปกติ) | ผูก socket เข้ากับ (IP, port) ที่ระบุ — ทำให้ client รู้ว่าจะเชื่อมมาที่ไหน |
listen() | Server | เปลี่ยน socket ให้เป็น passive/listening socket พร้อมกำหนดขนาด backlog queue (คิว connection ที่รอ accept) |
accept() | Server | บล็อกรอจนมี connection ใหม่เข้าคิว แล้วคืน socket ใหม่ สำหรับคุยกับ client ตัวนั้นโดยเฉพาะ |
connect() | Client | เริ่ม TCP 3-way handshake ไปยัง server ที่ระบุ บล็อกจนเชื่อมสำเร็จหรือ error |
send() / recv() | ทั้งคู่ | ส่ง/อ่านข้อมูลผ่าน socket ที่เชื่อมต่อแล้ว |
close() | ทั้งคู่ | ปิด socket เริ่มกระบวนการ TCP 4-way close |
accept() ไม่คืน socket ตัวเดิมที่ listen() ไว้ — มันสร้างsocket ใหม่เฉพาะสำหรับคุยกับ client รายนั้น ส่วน listening socket ตัวเดิมยังว่างพร้อมรับ connection รายถัดไปต่อทันที นี่คือกลไกที่ทำให้ server ตัวเดียวรับหลาย client พร้อมกันได้ที่ port เดียวกัน
# echo_server.py
import socket
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", 9000))
srv.listen(5) # backlog = 5
print("listening on :9000")
while True:
conn, addr = srv.accept() # บล็อกจนมี client ใหม่
print("connected:", addr)
while True:
data = conn.recv(4096)
if not data: # b"" = client ปิด connection แล้ว
break
conn.sendall(data) # echo กลับ
conn.close()
# echo_client.py
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 9000))
s.sendall(b"hello server")
print(s.recv(4096))
s.close()
4TCP เป็น Byte Stream ไม่ใช่ Message
send() กี่ครั้งก็ไม่สัมพันธ์กับจำนวนครั้งที่ฝั่งรับต้องเรียก recv() เพื่อได้ข้อมูลชุดเดียวกันสาเหตุ: TCP stack อาจรวม (coalesce) ข้อมูลจากหลาย send() เข้า segment เดียว หรือแบ่ง (fragment) ข้อมูลจาก send() เดียวออกเป็นหลาย segment ตาม MSS/MTU/สภาพเครือข่าย — ทั้งหมดนี้เกิดโปร่งใสโดยที่ application ไม่รู้และควบคุมไม่ได้
send(b"SET course NetworkComputing\n") แล้วเรียก send(b"GET course\n") ทันที — ฝั่ง server เรียก recv(4096) ครั้งเดียว อาจได้รับ b"SET course NetworkComputing\nGET course\n" ครบทั้งสองคำสั่งรวมกัน หรืออาจได้รับแค่ b"SET course Net" ก็ได้ — ทั้งสองกรณีถูกต้องตามสเปกของ TCP ทั้งคู่
recv() ครั้งเดียวอาจไม่ได้ข้อความครบ — ถ้าโค้ด parse ข้อมูลจาก recv() ครั้งเดียวโดยสมมติว่าคือ 1 message เสมอ โปรแกรมจะพังแบบสุ่ม (ทำงานได้ตอนทดสอบใน localhost ที่ latency ต่ำ แต่พังตอน deploy จริงข้าม network ที่ packet มาไม่พร้อมกัน)5Message Framing — ใครเป็นคนกำหนดขอบเขตข้อความ
เพราะ TCP ไม่ทำให้ ตัว application-layer protocol ที่เราออกแบบเองต้องกำหนดขอบเขต message เอง เรียกว่า framing — มี 3 เทคนิคหลัก:
| เทคนิค | วิธีทำงาน | ตัวอย่างที่ใช้จริง |
|---|---|---|
| Delimiter-based | ใช้ตัวอักษรพิเศษ (เช่น \n) คั่นท้าย message แต่ละอัน อ่านไปเรื่อยจนเจอ delimiter | SMTP, HTTP header, Redis inline command, protocol key-value ของ lab นี้ |
| Length-prefixed | ส่งความยาว message (เช่น 4 byte integer) นำหน้า payload — ฝั่งรับอ่าน header ก่อนรู้ว่าต้องอ่าน payload กี่ byte | Protobuf-based RPC (gRPC), HTTP/2 frame, TLS record |
| Fixed-length | ทุก message ยาวเท่ากันเป๊ะ ไม่ต้องมี header เลย | ระบบ real-time ที่ payload ขนาดคงที่ เช่น sensor reading |
def recv_line(sock, buf=b""):
"""อ่านจาก socket จนกว่าจะเจอ \\n ครบหนึ่งบรรทัด คืน (line, buf_ที่เหลือ)"""
while b"\n" not in buf:
chunk = sock.recv(4096)
if not chunk:
raise ConnectionError("peer closed before line completed")
buf += chunk
line, _, rest = buf.partition(b"\n")
return line, rest
สังเกตว่าฟังก์ชันนี้ วนเรียก recv() ซ้ำ จนกว่าจะเจอ delimiter ครบ — แก้ปัญหา "recv() ครั้งเดียวอาจไม่ได้ข้อความครบ" ได้โดยตรง และต้อง buffer ส่วนเกิน (rest) ไว้ใช้กับ message ถัดไปด้วย ไม่ทิ้งไป
import struct
def recv_exact(sock, n):
"""อ่านให้ได้ครบ n byte เป๊ะ ไม่งั้น error"""
data = b""
while len(data) < n:
chunk = sock.recv(n - len(data))
if not chunk:
raise ConnectionError("peer closed mid-message")
data += chunk
return data
def recv_framed(sock):
header = recv_exact(sock, 4) # 4-byte length prefix
(length,) = struct.unpack("!I", header)
return recv_exact(sock, length)
def send_framed(sock, payload: bytes):
sock.sendall(struct.pack("!I", len(payload)) + payload)
6Serialization ด้วย JSON
Framing แก้ปัญหา "message เริ่ม/จบตรงไหน" ส่วน serialization แก้ปัญหาคนละชั้น: "โครงสร้างข้อมูลในโปรแกรม (dict, object, list) แปลงเป็นลำดับ byte ที่ส่งผ่านเครือข่ายได้อย่างไร แล้วแปลงกลับอย่างไร" — Presentation Layer พูดถึงแนวคิดนี้ในเชิงทฤษฎี (XDR, ASN.1) ส่วนในทางปฏิบัติปัจจุบัน JSON เป็นตัวเลือกที่นิยมที่สุดเพราะอ่านง่าย มีไลบรารีรองรับแทบทุกภาษา
import json
def send_json_line(sock, obj: dict):
line = json.dumps(obj) + "\n"
sock.sendall(line.encode("utf-8"))
def recv_json_line(sock, buf=b""):
line, buf = recv_line(sock, buf) # ใช้ recv_line จากหัวข้อก่อน
return json.loads(line.decode("utf-8")), buf
รูปแบบนี้เรียกว่า NDJSON — แต่ละบรรทัดคือ JSON object ที่สมบูรณ์ในตัวเอง ผสมข้อดีของ delimiter framing (ง่าย, debug ได้ด้วยตาเปล่า) กับความยืดหยุ่นของ JSON (โครงสร้างข้อมูลซับซ้อนได้)
\n ปนอยู่ (JSON string ที่ escape ถูกต้องจะไม่มีปัญหานี้) — ถ้า payload เป็น binary data ดิบ (เช่น รูปภาพ) ควรใช้ length-prefixed framing แทน delimiter-based เพราะ binary data อาจมี byte ที่ตรงกับ delimiter โดยบังเอิญ7Blocking I/O และ Concurrent Server
Echo server ในหัวข้อ 3 มีข้อจำกัดใหญ่: accept() และ recv() เป็น blocking call — ขณะ server กำลังคุยกับ client รายหนึ่ง (ติดอยู่ใน loop recv()/send()) จะไม่กลับไปเรียก accept() รับ client รายใหม่ได้เลย client รายที่สองต้องรอจนรายแรกตัดการเชื่อมต่อ
| โมเดล | วิธีทำงาน | ข้อดี / ข้อเสีย |
|---|---|---|
| Sequential (blocking เดี่ยว) | รับ client ทีละราย เสร็จค่อยรับรายถัดไป | ง่ายที่สุด แต่รองรับ client พร้อมกันไม่ได้เลย — ใช้ได้แค่ demo |
| Thread-per-connection | ทุกครั้งที่ accept() ได้ connection ใหม่ สร้าง thread แยกไปดูแล connection นั้น แล้ววนกลับไป accept() ต่อทันที | เขียนง่าย เข้าใจง่าย (โค้ดแต่ละ thread เขียนแบบ blocking ปกติ) แต่ thread จำนวนมากกินหน่วยความจำ/context-switch overhead เมื่อ client เยอะมาก |
I/O Multiplexing / Async (select/epoll/asyncio) | Thread เดียวคอยดูหลาย socket พร้อมกัน จัดการเฉพาะ socket ที่ "พร้อม" อ่าน/เขียนจริงๆ | scale ได้สูงมาก (หมื่น connection ด้วย thread เดียว) แต่โค้ดซับซ้อนกว่า ต้องคิดแบบ event-driven |
import socket, threading
def handle_client(conn, addr):
print("connected:", addr)
try:
buf = b""
while True:
line, buf = recv_line(conn, buf)
conn.sendall(b"echo: " + line + b"\n")
except ConnectionError:
pass
finally:
conn.close()
print("disconnected:", addr)
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", 9000))
srv.listen(50)
while True:
conn, addr = srv.accept()
t = threading.Thread(target=handle_client, args=(conn, addr), daemon=True)
t.start()
8Timeout และการปิด Connection
recv()/accept()/connect() จะบล็อกไม่มีกำหนดเมื่อฝ่ายตรงข้ามค้าง/หายไปกลางทาง — thread หรือ process นั้นจะถูกยึดครองตลอดไป (resource leak) โดยเฉพาะอันตรายในโมเดล thread-per-connection ที่ thread ค้างสะสมจนกิน resource หมด
sock.settimeout(5.0) # ให้เวลารอสูงสุด 5 วินาที
try:
data = sock.recv(4096)
except socket.timeout:
print("no response within 5s — treat as failure")
- ปิดแบบสุภาพ (graceful) — client เรียก
close()ปกติ ฝั่ง server จะเห็นrecv()คืนค่าb""(empty bytes) — ไม่ใช่ exception - หลุดกลางทาง (abrupt) — สาย network ขาด, process client ถูก kill, เครื่อง client ดับ — ฝั่ง server มักไม่รู้ทันที จนกว่าจะพยายาม
send()แล้วได้ConnectionResetError/BrokenPipeErrorหรือจนกว่า timeout จะหมดเวลา
Half-close: TCP รองรับการปิดทีละทิศทาง — sock.shutdown(socket.SHUT_WR) บอกอีกฝ่ายว่า "เราจะไม่ส่งอะไรเพิ่มแล้ว" แต่ยังอ่านข้อมูลที่อีกฝ่ายส่งมาต่อได้ ต่างจาก close() ที่ปิดทั้งสองทิศทางทันที มีประโยชน์เมื่อต้องส่ง request เสร็จแล้วรอ response ยาวๆ
9Lab: ออกแบบ Protocol สำหรับ Key–Value Server
รวมทุกหัวข้อข้างต้นเข้าด้วยกัน — ออกแบบ protocol ข้อความบรรทัดเดียว (delimiter-based framing ด้วย \n) สำหรับ key-value store อย่างง่าย:
| คำสั่ง | รูปแบบ | ตัวอย่าง Response |
|---|---|---|
SET | SET <key> <value> | OK |
GET | GET <key> | VALUE NetworkComputing หรือ NOT_FOUND |
DELETE | DELETE <key> | OK หรือ NOT_FOUND |
> SET course NetworkComputing < OK > GET course < VALUE NetworkComputing > DELETE course < OK > GET course < NOT_FOUND
import socket, threading
store = {}
lock = threading.Lock()
def handle(conn, addr):
buf = b""
try:
while True:
line, buf = recv_line(conn, buf)
parts = line.decode("utf-8", errors="replace").split(" ", 2)
cmd = parts[0].upper() if parts else ""
if cmd == "SET" and len(parts) == 3:
with lock:
store[parts[1]] = parts[2]
resp = "OK"
elif cmd == "GET" and len(parts) == 2:
with lock:
val = store.get(parts[1])
resp = f"VALUE {val}" if val is not None else "NOT_FOUND"
elif cmd == "DELETE" and len(parts) == 2:
with lock:
existed = store.pop(parts[1], None) is not None
resp = "OK" if existed else "NOT_FOUND"
else:
resp = "ERROR unknown or malformed command"
conn.sendall((resp + "\n").encode("utf-8"))
except ConnectionError:
pass
finally:
conn.close()
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(("0.0.0.0", 9000))
srv.listen(50)
print("key-value server on :9000")
while True:
conn, addr = srv.accept()
threading.Thread(target=handle, args=(conn, addr), daemon=True).start()
- คำสั่งผิดรูปแบบ — เช่น
SET keyไม่มี value, หรือ command ที่ไม่รู้จัก → ต้องตอบ error กลับไป ไม่ใช่ crash - Value ที่มีช่องว่าง —
str.split(" ", 2)ในตัวอย่างจำกัดจำนวนครั้งที่ split เพื่อให้ value เป็น "ส่วนที่เหลือทั้งหมด" ได้ (เช่นSET note hello world→ value ="hello world") - Concurrent access ต่อ dict เดียวกัน — หลาย thread เขียน
storeพร้อมกันได้ ต้องมีlockป้องกัน race condition - Client ปิดกลางคำสั่ง —
recv_lineโยนConnectionErrorเมื่อ peer ปิดก่อนได้ข้อความครบ ต้อง catch ไว้ไม่ให้ทั้ง server ล้ม
10แผนการทดลอง: ทำให้เห็นสิ่งที่ Socket ซ่อนไว้
เตรียมเครื่องมือและสมุดบันทึก
- ใช้ Python 3 สอง terminal โดย terminal แรกเป็น server และอีก terminal เป็น client ถ้ามีสองเครื่องในวง LAN เดียวกันให้ทดลองข้ามเครื่องหลังจาก localhost สำเร็จ
- ทุกครั้งให้บันทึกคำสั่ง เวลา IP/port ข้อความที่ส่ง ผลที่ได้ และข้อสรุป อย่าเก็บเฉพาะภาพหน้าจอ เพราะภาพบอกว่าเกิดอะไรขึ้นแต่ยังไม่บอกเหตุผล
- เพิ่ม log ก่อนและหลัง
accept(),connect(),recv()และsendall()พร้อมเวลาและ address เพื่อให้เรียงเหตุการณ์ย้อนหลังได้ - ถ้าใช้ Wireshark ให้กรอง
tcp.port == 9000ถ้าใช้ tcpdump ให้ใช้tcpdump -i any -nn tcp port 9000แล้วเปรียบเทียบ packet กับ log ของโปรแกรม
การทดลองที่ 1 — แยก listening socket ออกจาก connected socket
เริ่ม server ก่อน client และดูว่าโปรแกรมหยุดรอที่บรรทัดใด จากนั้นเชื่อม client สองตัวพร้อมกัน พิมพ์ค่า getsockname() และ getpeername() ของ socket ที่ได้จาก accept() แล้วเติมตารางต่อไปนี้
| สิ่งที่สังเกต | หลักฐานที่ต้องเก็บ | คำอธิบาย |
|---|---|---|
| server ใช้ local port 9000 เหมือนเดิม | ผล getsockname() | port นี้เป็นประตูหน้าของร้าน |
| client แต่ละตัวมี source port ต่างกัน | ผล getpeername() | เหมือนลูกค้าแต่ละคนมีหมายเลขคิว จึงไม่ปนกันแม้เข้าประตูเดียว |
accept() คืน socket ตัวใหม่ | พิมพ์ object หรือ file descriptor | listening socket รับแขก ส่วน connected socket สนทนากับแขกคนนั้น |
การทดลองที่ 2 — พิสูจน์ว่า TCP ไม่มีขอบเขตข้อความ
- ให้ client เรียก
sendall(b"ONE"),sendall(b"TWO"),sendall(b"THREE")ติดกัน ส่วน server เรียกrecv(4)ซ้ำและพิมพ์repr(data) - ทำซ้ำโดยหน่วงระหว่างการส่ง 0.5 วินาที แล้วทำซ้ำอีกครั้งด้วย payload ยาว 100 KB ห้ามสรุปจากการทดลอง localhost เพียงรอบเดียว
- อธิบายว่าจำนวนครั้งที่
sendall()ไม่จำเป็นต้องเท่ากับจำนวนครั้งที่recv()และเหตุใดการเขียนjson.loads(sock.recv(4096))จึงเป็นระเบิดเวลาของโปรแกรมจริง
# ตัวช่วยทำให้การแบ่งชิ้นมองเห็นง่ายขึ้น
while True:
block = conn.recv(4)
if block == b"":
break
print(len(block), repr(block))
b'ONET' แปลว่า TCP ทำข้อมูลเสียหรือไม่? คำตอบคือไม่ ข้อมูลและลำดับยังครบ เพียง TCP รวมปลายข้อความแรกกับต้นข้อความถัดไป โปรแกรมของเราต่างหากที่ยังไม่ได้บอกว่าข้อความจบตรงไหนการทดลองที่ 3 — เปรียบเทียบ newline framing กับ length prefix
สร้าง protocol สองรุ่น รุ่น A จบข้อความด้วย \n รุ่น B นำหน้าด้วยความยาว 4 byte แบบ network byte order ทดลองส่งภาษาไทย ช่องว่าง newline ใน value และ binary data แล้วตอบว่าแต่ละแบบเหมาะกับงานใด
# length prefix: ซองจดหมายเขียนหน้าซองว่าของข้างในยาวกี่ byte
import struct
payload = "สวัสดี network".encode("utf-8")
sock.sendall(struct.pack("!I", len(payload)) + payload)
def recv_exact(sock, n):
chunks = []
while n:
part = sock.recv(n)
if part == b"":
raise ConnectionError("peer closed before message completed")
chunks.append(part); n -= len(part)
return b"".join(chunks)
Metaphor ของ framing คือการเอาจดหมายใส่ซอง TCP เป็นรถไปรษณีย์ที่อาจขนหลายซองในเที่ยวเดียว แต่ถ้าเราเทกระดาษทุกแผ่นลงรถโดยไม่ใช้ซอง ผู้รับย่อมไม่รู้ว่ากระดาษหน้าใดเป็นของจดหมายฉบับไหน newline คือรอยคั่นระหว่างซอง ส่วน length prefix คือเลขจำนวนแผ่นที่เขียนไว้หน้าซอง
การทดลองที่ 4 — Protocol ที่อ่านได้ไม่เท่ากับ Protocol ที่แข็งแรง
- ใช้ Key–Value Server เดิมทดสอบคำสั่งปกติ แล้วทดสอบคำสั่งว่าง command ไม่รู้จัก UTF-8 ผิดรูปแบบ key ยาวมาก และ client ที่ส่งครึ่งคำสั่งแล้วหยุด
- กำหนดขนาดข้อความสูงสุด เช่น 64 KiB ถ้า buffer เกินให้ตอบ error แล้วปิด connection เพื่อป้องกัน client ที่ค่อย ๆ ส่งข้อมูลจนกินหน่วยความจำ
- เปลี่ยน response เป็น JSON ที่มี
status,data,errorและrequest_idให้ client แยกความผิดพลาดของ protocol ออกจากความผิดพลาดของระบบได้ - เขียน protocol contract หนึ่งหน้า ระบุ encoding, framing, command, response, error, maximum size และพฤติกรรมเมื่อ connection ปิดกลางข้อความ
การทดลองที่ 5 — หลาย Client, Race Condition และ Backpressure
เปิด client 20 ตัวพร้อมกัน ให้ทุกตัวเพิ่มค่า counter 1,000 ครั้ง เริ่มจาก server ที่ไม่มี lock แล้วใส่ lock เปรียบเทียบผลสุดท้าย เวลา และจำนวน thread จากนั้นให้ client ตัวหนึ่งเชื่อมต่อแต่ไม่ส่งข้อมูล เพื่อดูว่ามันยึด thread ไว้อย่างไร
การทดลองที่ 6 — การปิดแบบสุภาพ การหลุด และ Half-close
- ปิด client ด้วย
close()แล้วจดว่า server เห็นrecv()คืนb""เมื่อใด - บังคับหยุด process และตัดเครือข่ายชั่วคราว เปรียบเทียบว่าฝั่ง server รู้ทันทีหรือรู้เมื่อส่งครั้งถัดไป/timeout เท่านั้น
- ใช้
shutdown(SHUT_WR)หลังส่ง request ยาว ฝั่ง server อ่านจน EOF แล้วส่งผลกลับ แสดงให้เห็นว่า TCP ปิดทิศทางส่งได้โดยยังเปิดทิศทางรับ - เพิ่ม cleanup ใน
finallyและทดสอบว่า file descriptor ไม่เพิ่มค้างหลังเชื่อมต่อซ้ำ 100 รอบ
ชิ้นงานที่ส่ง
| ชิ้นงาน | สิ่งที่ต้องมี |
|---|---|
| Source code | client/server, README วิธีรัน และ protocol contract |
| บันทึกการทดลอง | อย่างน้อย 6 การทดลอง มีสมมติฐาน คำสั่ง ผลจริง และคำอธิบาย |
| หลักฐานเครือข่าย | packet capture หรือ output เครื่องมือที่ผูกกับ log ของโปรแกรมได้ |
| บทสะท้อน | เลือก failure หนึ่งกรณี อธิบายสาเหตุ วิธีตรวจพบ และวิธีออกแบบป้องกัน |
11สรุปและขั้นตอนถัดไป
เป้าหมายของหัวข้อนี้ไม่ใช่แค่ "เขียน socket ได้" — แต่คือการเจอปัญหาเหล่านี้ด้วยตัวเองระหว่างทดสอบ:
recv() ครั้งเดียวอาจไม่ได้ข้อความครบโดยเฉพาะเมื่อทดสอบข้าม network จริง (ไม่ใช่ localhost) หรือส่ง message ยาวrecv() คืน b"") กับ "หลุดกะทันหัน" (exception)recv() ตลอดไป- อธิบายว่าทำไม TCP ถึงเรียกว่า "byte stream" ไม่ใช่ "message stream" พร้อมยกตัวอย่างสถานการณ์ที่ทำให้เกิดปัญหาถ้าไม่เข้าใจสิ่งนี้
- เปรียบเทียบ delimiter-based framing กับ length-prefixed framing — แบบไหนเหมาะกับข้อมูล binary มากกว่า เพราะเหตุใด
- อธิบายความแตกต่างระหว่าง listening socket กับ socket ที่
accept()คืนมา - เขียน pseudocode ของฟังก์ชันที่อ่านจาก socket จนครบ n byte โดยจัดการกรณี
recv()คืนค่าไม่ครบในครั้งเดียว - ทำไม thread-per-connection ถึง scale ได้จำกัดกว่า I/O multiplexing เมื่อจำนวน client เพิ่มขึ้นมาก