इस पोस्ट में, मैं Go में Redis क्लाइंट के दो घटकों के लिए एक सरल, समझने में आसान कार्यान्वयन की रूपरेखा देता हूं ताकि यह समझा जा सके कि Redis प्रोटोकॉल कैसे काम करता है और यह क्या इसे महान बनाता है।
यदि आप Go में एक पूर्ण-विशेषताओं वाला, प्रोडक्शन-रेडी Redis क्लाइंट ढूंढ रहे हैं, तो हम 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 को करते।
उदाहरण के लिए, हम अंतिम RESP का निरीक्षण करने के लिए इसे एक bytes.Buffer पास कर सकते हैं:
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 को कमांड भेजने के बाद, हमारा क्लाइंट TCP कनेक्शन से तब तक पढ़ने के लिए RESPReader का उपयोग करेगा जब तक उसे एक पूर्ण 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 { // लाइन बहुत छोटी थी या \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 को इस पोस्ट में निहित कोड में कुछ बग्स को पकड़ने में हमारी मदद करने के लिए विशेष धन्यवाद।