ในโพสต์นี้ ฉันสรุปการใช้งานที่เรียบง่ายและเข้าใจง่ายสำหรับสององค์ประกอบของไคลเอนต์ Redis ใน Go เพื่อทำความเข้าใจวิธีการทำงานของ Redisprotocol และสิ่งที่ทำให้มันยอดเยี่ยม
หากคุณกำลังมองหาไคลเอ็นต์ Redis ที่มีคุณสมบัติครบถ้วนและพร้อมสำหรับการผลิตใน Go ขอแนะนำให้ดูที่ไลบรารี redigo ของ Gary Burd
ก่อนที่เราจะเริ่มต้น อย่าลืมอ่านคำแนะนำเบื้องต้นเกี่ยวกับโปรโตคอล Redis อย่างอ่อนโยน ซึ่งครอบคลุมพื้นฐานของโปรโตคอลที่คุณจำเป็นต้องเข้าใจสำหรับคำแนะนำนี้
ตัวเขียนคำสั่ง RESP ใน Go
สำหรับไคลเอนต์ Redis สมมุติของเรา มีอ็อบเจ็กต์เพียงประเภทเดียวที่เราจะต้องเขียน:อาร์เรย์ของสตริงจำนวนมากสำหรับส่งคำสั่งไปยัง Redis นี่คือการใช้งานอย่างง่ายของตัวเขียน command-to-RESP:
package redis
import (
"bufio"
"io"
"strconv" // for converting integers to strings
)
var (
arrayPrefixSlice = []byte{'*'}
bulkStringPrefixSlice = []byte{'$'}
lineEndingSlice = []byte{'\r', '\n'}
)
type RESPWriter struct {
*bufio.Writer
}
func NewRESPWriter(writer io.Writer) *RESPWriter {
return &RESPWriter{
Writer: bufio.NewWriter(writer),
}
}
func (w *RESPWriter) WriteCommand(args ...string) (err error) {
// Write the array prefix and the number of arguments in the array.
w.Write(arrayPrefixSlice)
w.WriteString(strconv.Itoa(len(args)))
w.Write(lineEndingSlice)
// Write a bulk string for each argument.
for _, arg := range args {
w.Write(bulkStringPrefixSlice)
w.WriteString(strconv.Itoa(len(arg)))
w.Write(lineEndingSlice)
w.WriteString(arg)
w.Write(lineEndingSlice)
}
return w.Flush()
}
แทนที่จะเขียนไปที่ net.Conn
วัตถุ RESPWriter
เขียนถึง io.Writer
วัตถุ. วิธีนี้ช่วยให้เราทดสอบ parser โดยไม่ต้องเชื่อมต่อกับ net
ซ้อนกัน. เราเพียงแค่ทดสอบโปรโตคอลเครือข่ายแบบเดียวกับที่เราทำกับ io
. อื่นๆ .
ตัวอย่างเช่น เราสามารถส่งต่อ bytes.Buffer
เพื่อตรวจสอบ RESP สุดท้าย:
var buf bytes.Buffer
writer := NewRESPWriter(&buf)
writer.WriteCommand("GET", "foo")
buf.Bytes() // *2\r\n$3\r\nGET\r\n$3\r\nfoo\r\n
โปรแกรมอ่าน RESP อย่างง่ายใน Go
หลังจากส่งคำสั่งไปยัง Redis ด้วย RESPWriter
ลูกค้าของเราจะใช้RESPReader
เพื่ออ่านจากการเชื่อมต่อ TCP จนกว่าจะได้รับ RESPreply แบบเต็ม ในการเริ่มต้น เราจำเป็นต้องมีแพ็คเกจสองสามตัวเพื่อจัดการกับบัฟเฟอร์และแยกวิเคราะห์ข้อมูลที่เข้ามา:
package redis
import (
"bufio"
"bytes"
"errors"
"io"
"strconv"
)
และเราจะใช้ตัวแปรและค่าคงที่สองสามตัวเพื่อทำให้โค้ดของเราอ่านง่ายขึ้น:
const (
SIMPLE_STRING = '+'
BULK_STRING = '$'
INTEGER = ':'
ARRAY = '*'
ERROR = '-'
)
var (
ErrInvalidSyntax = errors.New("resp: invalid syntax")
)
ชอบ RESPWriter
, RESPReader
ไม่สนใจรายละเอียดการใช้งานของวัตถุที่อ่าน RESP ทั้งหมดนั้นต้องการความสามารถในการอ่านไบต์จนกระทั่งได้อ่านวัตถุ RESP แบบเต็มแล้ว ในกรณีนี้ จำเป็นต้องมี io.Reader
ซึ่งปิดด้วย bufio.Reader
เพื่อจัดการกับการบัฟเฟอร์ของข้อมูลที่เข้ามา
วัตถุและตัวเริ่มต้นของเรานั้นเรียบง่าย:
type RESPReader struct {
*bufio.Reader
}
func NewReader(reader io.Reader) *RESPReader {
return &RESPReader{
Reader: bufio.NewReaderSize(reader, 32*1024),
}
}
ขนาดบัฟเฟอร์สำหรับ bufio.Reader
เป็นเพียงการคาดเดาระหว่างการพัฒนา ในไคลเอนต์จริง คุณต้องการทำให้ขนาดสามารถกำหนดค่าได้และอาจทดสอบเพื่อหาขนาดที่เหมาะสมที่สุด 32KB จะทำงานได้ดีสำหรับการพัฒนา
RESPReader
มีเพียงหนึ่งวิธี:ReadObject()
ซึ่งส่งคืนการแบ่งไบต์ที่มีวัตถุ RESP แบบเต็มในการโทรแต่ละครั้ง มันจะส่งกลับข้อผิดพลาดที่พบจาก io.Reader
และจะส่งคืนข้อผิดพลาดเมื่อพบไวยากรณ์ RESP ที่ไม่ถูกต้อง
ลักษณะคำนำหน้าของ RESP หมายความว่าเราจำเป็นต้องอ่านไบต์แรกเท่านั้นเพื่อตัดสินใจว่าจะจัดการกับไบต์ต่อไปนี้อย่างไร อย่างไรก็ตาม เนื่องจากเราจำเป็นต้องอ่านอย่างน้อยบรรทัดแรกเต็มเสมอ (เช่น จนถึง \r\n
แรก) ) เราสามารถเริ่มต้นด้วยการอ่านบรรทัดแรกทั้งหมด:
func (r *RESPReader) ReadObject() ([]byte, error) {
line, err := r.readLine()
if err != nil {
return nil, err
}
switch line[0] {
case SIMPLE_STRING, INTEGER, ERROR:
return line, nil
case BULK_STRING:
return r.readBulkString(line)
case ARRAY:
return r.readArray(line) default:
return nil, ErrInvalidSyntax
}
}
เมื่อบรรทัดที่เราอ่านมีสตริงอย่างง่าย จำนวนเต็ม หรือคำนำหน้าข้อผิดพลาด ให้เปลี่ยนบรรทัดเต็มเป็นวัตถุ RESP ที่ได้รับ เนื่องจากประเภทวัตถุเหล่านั้นอยู่ภายในบรรทัดเดียว
ใน readLine()
เราอ่านจนเกิดครั้งแรกของ \n
แล้วตรวจสอบเพื่อให้แน่ใจว่ามี \r
. นำหน้า ก่อนที่จะกลับบรรทัดเป็นไบต์:
func (r *RESPReader) readLine() (line []byte, err error) {
line, err = r.ReadBytes('\n')
if err != nil {
return nil, err
}
if len(line) > 1 && line[len(line)-2] == '\r' {
return line, nil
} else {
// Line was too short or \n wasn't preceded by \r.
return nil, ErrInvalidSyntax
}
}
ใน readBulkString()
เราแยกวิเคราะห์ข้อกำหนดความยาวสำหรับสตริงจำนวนมากเพื่อทราบว่าเราต้องอ่านกี่ไบต์ เมื่อเสร็จแล้ว เราอ่านจำนวนไบต์นั้นและ \r\n
ตัวสิ้นสุดบรรทัด:
func (r *RESPReader) readBulkString(line []byte) ([]byte, error) {
count, err := r.getCount(line)
if err != nil {
return nil, err
}
if count == -1 {
return line, nil
}
buf := make([]byte, len(line)+count+2)
copy(buf, line)
_, err = io.ReadFull(r, buf[len(line):])
if err != nil {
return nil, err
}
return buf, nil
}
ฉันได้ดึง getCount()
ออกไปเป็นวิธีการที่แยกจากกันเพราะว่า lengthspecification ยังใช้สำหรับอาร์เรย์:
func (r *RESPReader) getCount(line []byte) (int, error) {
end := bytes.IndexByte(line, '\r')
return strconv.Atoi(string(line[1:end]))
}
ในการจัดการอาร์เรย์ เราได้รับจำนวนองค์ประกอบอาร์เรย์ แล้วเรียกReadObject()
ซ้ำๆ โดยเพิ่มวัตถุที่เป็นผลลัพธ์ไปยัง RESPbuffer ปัจจุบันของเรา:
func (r *RESPReader) readArray(line []byte) ([]byte, error) {
// Get number of array elements.
count, err := r.getCount(line)
if err != nil {
return nil, err
}
// Read `count` number of RESP objects in the array.
for i := 0; i < count; i++ {
buf, err := r.ReadObject()
if err != nil {
return nil, err
}
line = append(line, buf...)
}
return line, nil
}
สรุป
ร้อยบรรทัดข้างต้นทั้งหมดที่จำเป็นในการอ่านวัตถุ RESP จาก Redis อย่างไรก็ตาม มีส่วนที่ขาดหายไปจำนวนหนึ่งที่เราจำเป็นต้องนำไปใช้ก่อนที่จะใช้ไลบรารีนี้ในสภาพแวดล้อมที่ใช้งานจริง:
- ความสามารถในการดึงค่าจริงจาก RESP
RESPReader
ปัจจุบันส่งคืนการตอบกลับ RESP แบบเต็มเท่านั้น แต่จะไม่ส่งคืนสตริงจากการตอบกลับสตริงจำนวนมาก อย่างไรก็ตาม การดำเนินการนี้จะเป็นเรื่องง่าย RESPReader
ต้องการการจัดการข้อผิดพลาดทางไวยากรณ์ที่ดีขึ้น
รหัสนี้ยังไม่ได้รับการปรับให้เหมาะสมทั้งหมดและมีการจัดสรรและคัดลอกมากกว่าที่จำเป็น ตัวอย่างเช่น readArray()
เมธอด:สำหรับแต่ละอ็อบเจ็กต์ในอาเรย์ เราอ่านในออบเจ็กต์แล้วคัดลอกไปยังบัฟเฟอร์ในเครื่องของเรา
หากคุณสนใจที่จะเรียนรู้วิธีนำผลงานเหล่านี้ไปใช้ ฉันขอแนะนำให้ดูว่าห้องสมุดยอดนิยมอย่างเช่น
ขอขอบคุณ Niel Smith เป็นพิเศษที่ช่วยเราตรวจจับจุดบกพร่องในโค้ดที่อยู่ในโพสต์นี้