इस पोस्ट में, मैं रेडिस क्लाइंट के दो घटकों के लिए एक सरल, समझने में आसान कार्यान्वयन की रूपरेखा तैयार करता हूं, यह समझने के तरीके के रूप में कि रेडिसप्रोटोकॉल कैसे काम करता है और क्या इसे महान बनाता है।
यदि आप गो में एक पूर्ण विशेषताओं वाले, उत्पादन के लिए तैयार रेडिस क्लाइंट की तलाश कर रहे हैं, तो गैरी बर्ड की रेडिगो लाइब्रेरी पर एक नज़र डालने की सलाह दी जाती है।
आरंभ करने से पहले , सुनिश्चित करें कि आपने रेडिस प्रोटोकॉल के हमारे सौम्य परिचय को पढ़ा है - इसमें प्रोटोकॉल की मूल बातें शामिल हैं जिन्हें आपको इस गाइड के लिए समझने की आवश्यकता होगी।
गो में एक 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
. पास कर सकते हैं अंतिम आरईएसपी का निरीक्षण करने के लिए:
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 रीडर
Redis को RESPWriter
के साथ कमांड भेजने के बाद , हमारा क्लाइंट RESPReader
. का उपयोग करेगा टीसीपी कनेक्शन से तब तक पढ़ने के लिए जब तक कि उसे पूर्ण 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
उस वस्तु के कार्यान्वयन विवरण की परवाह नहीं करता है जिससे वह आरईएसपी पढ़ रहा है। इसे सभी बाइट्स पढ़ने की क्षमता की आवश्यकता होती है जब तक कि यह एक पूर्ण आरईएसपी ऑब्जेक्ट नहीं पढ़ लेता। इस मामले में, इसे एक 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 सिंटैक्स का सामना करने पर त्रुटियाँ भी लौटाएगा।
आरईएसपी की उपसर्ग प्रकृति का मतलब है कि हमें केवल पहले बाइट को पढ़ने की जरूरत है ताकि यह तय किया जा सके कि निम्नलिखित बाइट्स को कैसे संभालना है। हालांकि, क्योंकि हमें हमेशा कम से कम पहली पूरी लाइन पढ़नी होगी (यानी पहली \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
}
रैपिंग अप
उपरोक्त सौ लाइनें रेडिस से किसी भी आरईएसपी ऑब्जेक्ट को पढ़ने के लिए आवश्यक हैं। हालांकि, उत्पादन वातावरण में इस पुस्तकालय का उपयोग करने से पहले कई लापता टुकड़े हैं जिन्हें हमें लागू करने की आवश्यकता होगी:
- आरईएसपी से वास्तविक मूल्य निकालने की क्षमता।
RESPReader
वर्तमान में केवल पूर्ण आरईएसपी प्रतिक्रिया देता है, उदाहरण के लिए, यह एक स्ट्रिंग को बल्क स्ट्रिंग प्रतिक्रिया से वापस नहीं करता है। हालांकि, इसे लागू करना आसान होगा। RESPReader
बेहतर सिंटैक्स त्रुटि प्रबंधन की आवश्यकता है।
यह कोड भी पूरी तरह से अनुकूलित नहीं है और जरूरत से ज्यादा आवंटन और प्रतियां करता है। उदाहरण के लिए, readArray()
विधि:सरणी में प्रत्येक ऑब्जेक्ट के लिए, हम ऑब्जेक्ट में पढ़ते हैं और फिर इसे अपने स्थानीय बफर में कॉपी करते हैं।
यदि आप इन टुकड़ों को लागू करने का तरीका सीखने में रुचि रखते हैं, तो मैं यह देखने की सलाह देता हूं कि कैसे लोकप्रिय पुस्तकालय जैसे कि हायरिस या उन्हें फिर से लागू करते हैं।
इस पोस्ट में निहित कोड में कुछ बगों को पकड़ने में हमारी सहायता करने के लिए नील स्मिथ का विशेष धन्यवाद।