Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Redis

การอ่านและการเขียนโปรโตคอล Redis ใน Go

ในโพสต์นี้ ฉันสรุปการใช้งานที่เรียบง่ายและเข้าใจง่ายสำหรับสององค์ประกอบของไคลเอนต์ 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 เป็นพิเศษที่ช่วยเราตรวจจับจุดบกพร่องในโค้ดที่อยู่ในโพสต์นี้