Computer >> 컴퓨터 >  >> 프로그램 작성 >> Redis

Go에서 Redis 프로토콜 읽기 및 쓰기

이 게시물에서 저는 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에게 특별히 감사드립니다.