이 게시물에서 저는 Redisprotocol이 어떻게 작동하고 무엇이 훌륭한지를 이해하는 방법으로 Go에서 Redis 클라이언트의 두 가지 구성 요소에 대한 간단하고 이해하기 쉬운 구현을 간략하게 설명합니다.
Go에서 완전한 기능을 갖춘 프로덕션 준비 Redis 클라이언트를 찾고 있다면 Gary Burd의 redigo 라이브러리를 살펴보는 것이 좋습니다.
시작하기 전에 , Redis 프로토콜에 대한 간단한 소개를 읽어보세요. 이 가이드에서 이해해야 하는 프로토콜의 기본 사항을 다룹니다.
Go의 RESP 명령 작성기
가상의 Redis 클라이언트의 경우 작성해야 하는 객체는 한 가지뿐입니다. 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
Go의 간단한 RESP 리더
RESPWriter
를 사용하여 Redis에 명령을 보낸 후 , 우리 클라이언트는 RESPReader
를 사용할 것입니다. 전체 RESPreply를 수신할 때까지 TCP 연결에서 읽습니다. 시작하려면 들어오는 데이터의 버퍼링 및 구문 분석을 처리하기 위해 몇 가지 패키지가 필요합니다.
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
}
마무리
위의 100줄은 Redis에서 RESP 개체를 읽는 데 필요한 전부입니다. 그러나 프로덕션 환경에서 이 라이브러리를 사용하기 전에 구현해야 하는 누락된 부분이 많이 있습니다.
- RESP에서 실제 값을 추출하는 기능.
RESPReader
현재는 전체 RESP 응답만 반환하며, 예를 들어 대량 문자열 응답에서 문자열을 반환하지 않습니다. 하지만 이를 구현하는 것은 쉬울 것입니다. RESPReader
더 나은 구문 오류 처리가 필요합니다.
이 코드도 완전히 최적화되지 않았으며 필요한 것보다 더 많은 할당과 복사를 수행합니다. 예를 들어, readArray()
방법:배열의 각 개체에 대해 개체를 읽은 다음 로컬 버퍼에 복사합니다.
이러한 부분을 구현하는 방법을 배우는 데 관심이 있다면 Hiredis 또는 redigoimplement와 같은 인기 있는 라이브러리를 살펴보는 것이 좋습니다.
이 게시물에 포함된 코드에서 몇 가지 버그를 잡는 데 도움을 주신 Niel Smith에게 특별히 감사드립니다.