কম্পিউটার

গো-তে রিডিস প্রোটোকল পড়া এবং লেখা

এই পোস্টে, আমি রেডিসপ্রোটোকল কীভাবে কাজ করে এবং কী এটিকে দুর্দান্ত করে তা বোঝার উপায় হিসাবে গো-তে একটি রেডিস ক্লায়েন্টের দুটি উপাদানের জন্য একটি সহজ, বুঝতে সহজ বাস্তবায়নের রূপরেখা দিই৷

আপনি যদি গো-তে একটি সম্পূর্ণ বৈশিষ্ট্যযুক্ত, প্রোডাকশন-রেডি রেডিস ক্লায়েন্ট খুঁজছেন, তাহলে গ্যারি বার্ডের রেডিগো লাইব্রেরি দেখে নেওয়ার পরামর্শ দেওয়া হচ্ছে।

আমরা শুরু করার আগে , নিশ্চিত হোন যে আপনি Redis প্রোটোকলের সাথে আমাদের মৃদু ভূমিকা পড়েছেন - এটি প্রোটোকলের মূল বিষয়গুলিকে কভার করে যা এই নির্দেশিকাটির জন্য আপনাকে বুঝতে হবে৷

গো-তে একজন 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 কে লেখে বস্তু এটি আমাদেরকে 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 পাঠক

RESPWriter দিয়ে Redis-এ একটি কমান্ড পাঠানোর পর , আমাদের ক্লায়েন্ট RESPReader ব্যবহার করবে TCP সংযোগ থেকে এটি একটি সম্পূর্ণ RESPউত্তর না পাওয়া পর্যন্ত পড়তে। শুরু করার জন্য, বাফারিং এবং ইনকামিং ডেটা পার্স করার জন্য আমাদের কয়েকটি প্যাকেজের প্রয়োজন হবে:

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() টেনে নিয়েছি একটি পৃথক পদ্ধতিতে আউট কারণ দৈর্ঘ্য নির্দিষ্টকরণটি অ্যারেগুলির জন্যও ব্যবহৃত হয়:

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 অবজেক্ট পড়ার জন্য উপরের শত লাইনগুলিই প্রয়োজন। যাইহোক, উত্পাদন পরিবেশে এই লাইব্রেরিটি ব্যবহার করার আগে আমাদের বাস্তবায়ন করতে হবে এমন অনেকগুলি অনুপস্থিত অংশ রয়েছে:

  • RESP থেকে প্রকৃত মান বের করার ক্ষমতা। RESPReader বর্তমানে শুধুমাত্র সম্পূর্ণ RESP প্রতিক্রিয়া প্রদান করে, এটি উদাহরণস্বরূপ, একটি বাল্ক স্ট্রিং প্রতিক্রিয়া থেকে একটি স্ট্রিং ফেরত দেয় না। যাইহোক, এটি বাস্তবায়ন করা সহজ হবে।
  • RESPReader আরও ভাল সিনট্যাক্স ত্রুটি পরিচালনার প্রয়োজন৷

এই কোডটি সম্পূর্ণরূপে অপ্টিমাইজ করা হয়েছে এবং এটির প্রয়োজনের চেয়ে বেশি বরাদ্দ এবং অনুলিপি করে। উদাহরণস্বরূপ, readArray() পদ্ধতি:অ্যারেতে থাকা প্রতিটি বস্তুর জন্য, আমরা অবজেক্টে পড়ি এবং তারপর আমাদের স্থানীয় বাফারে কপি করি।

আপনি যদি এই টুকরোগুলি কীভাবে বাস্তবায়ন করতে হয় তা শিখতে আগ্রহী হন, আমি সুপারিশ করছি যে কীভাবে জনপ্রিয় লাইব্রেরিগুলি যেমন hiredis বা redigoimplement করে।

এই পোস্টে থাকা কোডে কিছু বাগ ধরতে সাহায্য করার জন্য নিল স্মিথকে বিশেষ ধন্যবাদ৷


  1. RedisInsight 1.6 RedisGears সমর্থন এবং Redis 6 ACL সামঞ্জস্য নিয়ে আসে

  2. Redis.io রিফ্রেশ করা এবং প্রসারিত করা

  3. Go-redis, Upstash এবং OpenTelemetry এর সাথে ট্রেসিং বিতরণ করা হয়েছে

  4. সার্ভারলেস রেডিস এবং প্রতিক্রিয়া নেটিভ সহ অ্যাপ-মধ্যস্থ ঘোষণা