หนึ่งในฟีเจอร์ที่ Redis ได้รับการยกย่องน้อยที่สุดแต่สำคัญที่สุดคือโปรโตคอลสาย (wire protocol) นี่คือเหตุผลที่ Redis มีระบบนิเวศ client libraries คุณภาพสูงมากที่สุด โปรโตคอลสายของ Redis น่าประทับใจในความเรียบง่าย ทำให้สร้าง client ที่นำคุณสมบัติหลักของ Redis ไปใช้ได้ง่าย มันยังออกแบบมาเพื่อให้เขียน client libraries ที่รวดเร็วและมีประสิทธิภาพได้ง่าย
RESP คืออะไร?
RESP (REdis Serialization Protocol) คือชื่อของโปรโตคอลแบบข้อความที่ Redis clients และ servers ใช้สื่อสารกันผ่าน TCP การสื่อสารทั้งหมดระหว่างเซิร์ฟเวอร์และไคลเอ็นต์ประกอบด้วย 5 ประเภทพื้นฐาน:
- Simple strings ใช้สำหรับการตอบกลับทั่วไปของเซิร์ฟเวอร์ เช่น "OK" (หลังคำสั่งเขียนสำเร็จ) หรือ "PONG" (การตอบสนองสำเร็จของคำสั่ง PING)
- Bulk strings คืนค่าสำหรับคำสั่งอ่านค่าเดี่ยวเกือบทั้งหมด เช่น GET, LPOP, HGET Bulk strings ต่างจาก simple strings ที่สามารถมีอะไรก็ได้ — newlines, control characters, หรือแม้แต่ RESP ที่ถูกต้อง ทั้งหมดโดยไม่ต้อง escape หรือ encode
- Integers ใช้เป็นการตอบกลับสำหรับคำสั่งนับประเภทใดๆ เช่น STRLEN, HLEN, BITCOUNT
- Arrays สามารถมีจำนวน RESP objects ไหนก็ได้ รวมถึง arrays อื่นๆ ใช้สำหรับส่งคำสั่งไปเซิร์ฟเวอร์ และการตอบกลับที่คืนค่ามากกว่า 1 element เช่น HGETALL, LRANGE, MGET
- Errors คืนค่าทุกครั้งที่ Redis พบข้อผิดพลาดขณะจัดการคำสั่ง เช่น พยายามรันคำสั่งกับคีย์ที่เก็บชนิดข้อมูลผิด
ตอนเขียนและอ่าน RESP เราไม่ต้อง escape หรือ encode คีย์หรือค่า ส่วนเดียวของ RESP ที่ต้อง parse จริงๆ คือ metadata เรียบง่ายที่มาพร้อมแต่ละ RESP object
RESP 长什么样?
ทุก RESP objects เริ่มต้นด้วย prefix character และจบด้วย line terminator (ยกเว้น arrays ที่ไม่รวม line terminator ของตัวเอง) ตัวอย่างง่ายที่สุดของ RESP object คือการตอบกลับ simple string OK:
+OK\r\n
(RESP เป็นโปรโตคอลที่มนุษย์อ่านได้ แต่เพื่อความชัดเจน ผมจะเขียน line breaks (\r\n) ออกมาชัดเจนในทุกตัวอย่าง RESP)
ใน simple string ข้างต้น + คือ prefix ของ simple string OK คือ body และ \r\n คือ line terminator ที่จบ simple string นี้
การอ่าน simple string เราอ่านจนถึง \r\n ถัดไป คืนค่า bytes ก่อนหน้า (จนถึง +) เป็น reply string นี้ทำงานได้เพราะ RESP simple strings ไม่มี newline characters
Errors และ integers จัดรูปแบบคล้าย simple strings แต่ใช้ prefix ต่างกัน Errors ใช้ -:
-ERR unknown command 'GETT'\r\n
และ integers ใช้ ::
:99\r\n
Bulk strings เฉพาะตัวที่มีสองส่วน: การระบุความยาว และ body:
$13\r\nHello, World!\r\n
$ คือ bulk string prefix 13 คือจำนวนไบต์ใน string body จริง แล้ว \r\n จบการระบุความยาว Hello, World! คือ string body 13 ไบต์ และก็จบด้วย \r\n (ซึ่งไม่ใช่ส่วนของ string body)
เพราะ bulk string container ให้ความยาวแน่นอนสำหรับ body เราไม่ต้อง parse string body จริงๆ เพื่อหาจุดจบ — เราอ่านอีก 13 ไบต์ (บวก line terminator ท้าย) จาก socket โดยไม่ตรวจสอบเนื้อหา string หมายความว่าเราใช้ข้อมูลอะไรก็ได้ใน body โดยไม่ต้อง encode หรือ escape
Arrays เริ่มต้นด้วยการระบุความยาวเหมือนกัน แต่ length field บอกจำนวน objects ใน array แทนจำนวนไบต์ในเนื้อหา array
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
ในตัวอย่างข้างต้น เรามี array ที่มี 2 bulk strings (foo และ bar) แม้ array length specification จะไม่ให้เราข้ามไปข้างหน้าได้แบบ bulk string แต่ก็ทำให้ง่ายต่อการนำ array ไปใช้ใน client เราอ่าน array size อ่าน objects อีกเท่าจำนวนนั้น แล้วคืนค่าทั้งหมดใน array สุดท้าย
ทุกคำสั่ง Redis ส่งเป็น arrays ของ bulk strings ตัวอย่าง คำสั่ง "SET mykey 'my value'" จะเขียนและส่งเป็น:
*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$8\r\nmy value\r\n
รายละเอียด RESP เพิ่มเติม
Prefixed lengths ที่ใช้ใน RESP ทำให้ parsers สามารถนำไปใช้โดยไม่ต้องทำ state modeling ซับซ้อนหรือหลาย passes ผ่านข้อมูล ทำให้สร้าง parsers รวดเร็วได้มาก
มีรายละเอียดที่ไม่ได้ครอบคลุมที่นี่ เช่น null bulk strings และ null arrays สำหรับข้อมูลเพิ่มเติม เอกสารอย่างเป็นทางการ (เหมือนเดิม) มีหน้าเอกสาร RESP ที่อ่านง่ายและครอบคลุม
วิธีดีๆ เข้าใจโปรโตคอลลึกขึ้น คือลอง implement simple client เอง ดูคู่มือการอ่านและเขียน Redis protocol ใน Go ต่อไป