এই পোস্টে, আমি Go-তে একটি Redis ক্লায়েন্টের দুটি কম্পোনেন্টের একটি সিম্পল, ইজি-টু-অন্ডারস্ট্যান্ডিং ইমপ্লিমেন্টেশন আউটলাইন করব Redis প্রোটোকল কীভাবে কাজ করে এবং এটিকে কীভাবে গ্রেট করে তা বুঝতে।
আপনি যদি একটি ফুল-ফিচার্ড, প্রোডাকশন-রেডি Redis ক্লায়েন্ট Go-তে খুঁজছেন, তবে আমরা Gary Burd-এর redigo লাইব্রেরি দেখার সুপারিশ করি।
শুরু করার আগে, নিশ্চিত করুন যে আপনি আমাদের Redis প্রোটোকলের জেন্টল ইনট্রোডাকশন পড়েছেন - এটি প্রোটোকলের বেসিকস কভার করে যা এই গাইডের জন্য প্রয়োজন।
Go-তে একটি RESP কমান্ড রাইটার
আমাদের হাইপোথেটিক্যাল Redis ক্লায়েন্টের জন্য, আমাদের শুধু একটি টাইপের অবজেক্ট রাইট করতে হবে: কমান্ড Redis-এ পাঠাতে বাল্ক স্ট্রিংসের অ্যারে। এখানে একটি কমান্ড-টু-RESP রাইটারের সিম্পল ইমপ্লিমেন্টেশন:
package redis import ( "bufio" "io" "strconv" // ইন্টিজারকে স্ট্রিংে কনভার্ট করার জন্য ) 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) { // অ্যারে প্রিফিক্স এবং অ্যারেতে আর্গুমেন্টের সংখ্যা লিখুন। w.Write(arrayPrefixSlice) w.WriteString(strconv.Itoa(len(args))) w.Write(lineEndingSlice) // প্রতিটি আর্গুমেন্টের জন্য একটি বাল্ক স্ট্রিং লিখুন। 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 Go-তে একটি সিম্পল 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 অবজেক্ট পায়। এই কেসে, এটিকে একটি 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-এর বাফার সাইজ ডেভেলপমেন্টের সময় একটি অনুমান। একটি আসল ক্লায়েন্টে, আপনি এর সাইজ কনফিগারেবল রাখতে চাইবেন এবং পারফেক্ট সাইজ খুঁজে বের করতে টেস্ট করবেন। ৩২KB ডেভেলপমেন্টের জন্য কাজ করবে।
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 { // লাইন খুব ছোট বা \n \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() রিকার্সিভলি কল করি, ফলাফল অবজেক্টগুলো আমাদের কারেন্ট RESP বাফারে অ্যাপেন্ড করি:
func (r *RESPReader) readArray(line []byte) ([]byte, error) { // অ্যারে এলিমেন্টের সংখ্যা পান। count, err := r.getCount(line) if err != nil { return nil, err } // `count` সংখ্যক RESP অবজেক্ট অ্যারেতে পড়ুন। for i := 0; i < count; i++ { buf, err := r.ReadObject() if err != nil { return nil, err } line = append(line, buf...) } return line, nil } উপসংহার
উপরের সো লাইনেই Redis থেকে যেকোনো RESP অবজেক্ট পড়ার জন্য প্রয়োজনীয় সব কিছু। যাইহোক, কিছু মিসিং পিস আছে যা প্রোডাকশন এনভায়রনমেন্টে এই লাইব্রেরি ব্যবহার করার আগে ইমপ্লিমেন্ট করতে হবে:
- RESP থেকে আসল ভ্যালু এক্সট্র্যাক্ট করার ক্ষমতা।
RESPReaderবর্তমানে শুধু ফুল RESP রেসপন্স রিটার্ন করে, এটি উদাহরণস্বরূপ বাল্ক স্ট্রিং রেসপন্স থেকে স্ট্রিং রিটার্ন করে না। যাইহোক, এটি ইমপ্লিমেন্ট করা সহজ। RESPReader-এর बेटার সিনট্যাক্স এরর হ্যান্ডেলিং প্রয়োজন।
এই কোডও সম্পূর্ণ আনঅপ্টিমাইজড এবং প্রয়োজনের চেয়ে বেশি এলোকেশন এবং কপি করে। উদাহরণস্বরূপ, readArray() মেথড: অ্যারেতে প্রতিটি অবজেক্টের জন্য, আমরা অবজেক্টটি পড়ি এবং তারপর আমাদের লোকাল বাফারে কপি করি।
আপনি যদি এই পিসগুলো কীভাবে ইমপ্লিমেন্ট করতে হয় তা শিখতে আগ্রহী হন, তবে আমি সুপারিশ করি hiredis বা redigo কীভাবে সেগুলো ইমপ্লিমেন্ট করে সেটি দেখুন।
স্পেশ্যাল থ্যাংکس Niel Smith-এর প্রতি এই পোস্টে থাকা কোডের কিছু বাগ ধরতে সাহায্যের জন্য।