9.13. TCP sockets¶
TCP sockets มีสองรูปแบบที่ดูแตกต่างกันแต่ใช้ประเภทพื้นฐานเดียวกัน ได้แก่ client sockets ที่ connect() ไปยังเซิร์ฟเวอร์ระยะไกล และ server sockets ที่ bind(), listen(), และ accept() การเชื่อมต่อขาเข้า ทั้งสองบทบาทใช้คลาส socket เดียวกันที่แนะนำไว้ใน อ็อบเจกต์ Socket โดยแตกต่างกันเพียงเมธอดที่เรียกใช้เท่านั้น
9.13.1. TCP client¶
client ที่ง่ายที่สุดจะเปิดการเชื่อมต่อ ส่งคำขอ อ่านการตอบกลับ แล้วปิด:
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("192.168.1.20", 9000))
s.send(b"hello\n")
reply = s.recv(1024)
print("reply:", reply)
s.close()
connect() ทำการ three-way handshake ที่อธิบายไว้ใน TCP -- สตรีมไบต์ที่เชื่อถือได้ และคืนค่าเมื่อการเชื่อมต่อเปิดแล้ว send() เขียนไบต์ลงในการเชื่อมต่อ และ recv() อ่านไบต์จากการเชื่อมต่อได้สูงสุดตามจำนวนที่กำหนด เมื่อแอปพลิเคชันเสร็จสิ้น close() จะปิดการเชื่อมต่อ
สคริปต์เดียวกันที่ห่อด้วยรูปแบบ with-statement จาก อ็อบเจกต์ Socket เพื่อให้ socket ถูกปิดแม้ว่าจะเกิดข้อผิดพลาด:
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect(("192.168.1.20", 9000))
s.send(b"hello\n")
print(s.recv(1024))
9.13.1.1. อ่านจนครบ¶
recv() ครั้งเดียวจะคืนค่าไบต์ ได้มากสุด ตามจำนวนที่ขอ -- อาจคืนค่าน้อยกว่า เนื่องจาก TCP เป็น stream ไม่ใช่ลำดับของข้อความ แอปพลิเคชันต้องอ่านต่อไปจนกว่าจะได้รับการตอบกลับครบถ้วน:
chunks = []
while True:
chunk = s.recv(1024)
if not chunk: # empty bytes -> other side closed
break
chunks.append(chunk)
reply = b"".join(chunks)
ลูปสิ้นสุดเมื่อ recv() คืนค่า bytes ว่าง ซึ่งเกิดขึ้นเมื่ออีกฝั่งปิดการเชื่อมต่อครึ่งหนึ่งของตนอย่างสะอาด แอปพลิเคชันอ่าน "end of stream" เหมือนกับ "end of message" ในรูปแบบโปรโตคอลนี้
9.13.1.2. ส่งจนครบ¶
การเตือนในทางกลับกันใช้กับ send() คือ อาจส่งไบต์ น้อยกว่า ที่ขอ โดยคืนค่าจำนวนไบต์ที่เขียนจริง สำหรับข้อมูลขนาดใหญ่ ให้ลองส่งส่วนที่ยังไม่ได้ส่งซ้ำ:
payload = some_big_bytes
while payload:
n = s.send(payload)
payload = payload[n:]
sendall() ทำลูปนี้ภายใน ดังนั้นโค้ดส่วนใหญ่สามารถเรียกใช้แทนเพื่อหลีกเลี่ยงการลองซ้ำด้วยตนเอง:
s.sendall(some_big_bytes)
9.13.2. TCP server¶
ฝั่งเซิร์ฟเวอร์มีสี่ขั้นตอน ได้แก่ จองพอร์ต สลับ socket เป็นโหมดรับฟัง รับการเชื่อมต่อทีละรายการ และสื่อสารบน socket ที่ยอมรับแต่ละรายการ ตัวอย่าง echo server ขั้นต่ำ:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.bind(("0.0.0.0", 9000))
server.listen(1)
print("listening on port 9000")
while True:
conn, addr = server.accept()
print("connection from", addr)
while True:
data = conn.recv(1024)
if not data:
break
conn.send(data) # echo back
conn.close()
ทีละขั้นตอน:
bind()จองโฮสต์และพอร์ตบน camera"0.0.0.0"รับบนทุกอินเทอร์เฟซ การเปลี่ยนเป็น IP เฉพาะจะจำกัด listener ไปยังอินเทอร์เฟซนั้นlisten()สลับ socket จาก socket ปกติเป็น listening socket อาร์กิวเมนต์คือ backlog -- จำนวนการเชื่อมต่อที่รอดำเนินการที่ MicroPython จะจัดคิวขณะที่แอปพลิเคชันกำลังทำงาน เลือกตัวเลขเล็กน้อย โดย1เพียงพอสำหรับกรณีส่วนใหญ่accept()บล็อกจนกว่า client จะเชื่อมต่อ แล้วคืนค่า(conn, addr)ได้แก่ socket ใหม่ ที่แทนการเชื่อมต่อนี้ และที่อยู่ของ client listening socket เองยังคงเปิดอยู่เพื่อรับการเชื่อมต่อเพิ่มเติมไบต์ทั้งหมดสำหรับการสนทนาไหลผ่าน
connซึ่งเป็น socket ใหม่ การอ่านและเขียนใช้เมธอดrecv()/send()เดียวกับฝั่ง clientเมื่อ client ปิดการเชื่อมต่อ
recv()จะคืนค่าb""ลูปภายในสิ้นสุดและเซิร์ฟเวอร์ปิดฝั่งของตนด้วยclose()
while True ด้านนอกกลับไปที่ accept() เพื่อรอ client รายถัดไป เซิร์ฟเวอร์จัดการ client ทีละรายในรูปแบบนี้ การรัน client หลายรายพร้อมกันต้องใช้ threads หรือ asyncio ซึ่งหัวข้อหลังเป็นเนื้อหาของหน้าถัดไป
9.13.3. ปัญหาที่พบบ่อย¶
การปฏิบัติต่อ recv() เหมือนเป็นข้อความ ไม่ใช่เช่นนั้น การเรียก
send(b"hi")สองครั้งอาจมาถึงเป็นrecv(4)เดียวที่ได้b"hihi"หรือเป็นrecv(2)สองครั้ง แอปพลิเคชันต้องเพิ่มการกำหนดขอบเขตข้อความหากขอบเขตข้อความมีความสำคัญ เช่น ขึ้นบรรทัดใหม่ คำนำหน้าความยาว หรือวิธีอื่นลืมลองซ้ำเมื่อส่งไม่ครบ ใช้
sendall()สำหรับข้อมูลที่มากกว่าไม่กี่ร้อยไบต์ลืมปิด socket ที่ยอมรับแล้ว
connแต่ละรายการเป็น socket แยกต่างหาก การปิด listening socket ไม่ได้ปิด socket ที่ยอมรับแล้ว การใช้บล็อกwithบนทั้งสองทำให้ยากที่จะทำผิด:while True: with server.accept()[0] as conn: # ... talk on conn ...
การ bind ซ้ำกับพอร์ตที่ยังอยู่ใน TIME_WAIT เมื่อเซิร์ฟเวอร์รีสตาร์ทภายในไม่กี่วินาทีหลังปิด
bind()อาจล้มเหลวด้วย "address in use" เนื่องจาก MicroPython ยังถือพอร์ตสำหรับการเชื่อมต่อก่อนหน้าอยู่server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)ก่อนbind()จะแก้ปัญหานี้
9.13.4. ขั้นตอนถัดไป¶
การบล็อกที่ accept() หมายความว่าเซิร์ฟเวอร์สามารถให้บริการ client ได้ทีละรายเท่านั้น การบล็อกที่ recv() หมายความว่า client ที่ช้าเพียงรายเดียวทำให้ลูปทั้งหมดหยุดนิ่ง คำตอบมาตรฐานบน camera คือ asyncio -- รันแต่ละการเชื่อมต่อเป็น task ของตัวเอง ให้ event loop จัดการการสลับระหว่างกัน หน้าถัดไปครอบคลุมเวอร์ชัน asyncio ของทุกสิ่งในหน้านี้