Redis 직렬화 프로토콜 스펙

Redis 직렬화 프로토콜 스펙 (RESP)

Redis 직렬화 프로토콜(RESP) — 클라이언트가 구현하는 유선 프로토콜.

출처: 공식문서

Redis 서버와 통신하기 위해 Redis 클라이언트는 Redis Serialization Protocol (RESP) 이라는 프로토콜을 사용합니다. 프로토콜이 Redis 전용으로 설계되었지만, 다른 클라이언트-서버 소프트웨어 프로젝트에도 쓸 수 있습니다.

RESP는 다음 고려사항 사이의 절충입니다:

  • 구현이 단순
  • 파싱이 빠름
  • 사람이 읽을 수 있음

RESP는 정수, 문자열, 배열을 포함한 여러 데이터 타입을 직렬화할 수 있습니다. 또한 오류-특정 타입도 특징으로 합니다. 클라이언트는 Redis 서버에 문자열 배열로 요청을 보냅니다. 배열의 내용은 서버가 실행해야 할 명령과 그 인수입니다. 서버의 응답 타입은 명령-특정적입니다.

RESP는 바이너리-안전하며, 벌크 데이터 전송에 프리픽스 길이(prefixed length)를 사용하므로 한 프로세스에서 다른 프로세스로 전송되는 벌크 데이터를 처리할 필요가 없습니다.

RESP는 Redis 클라이언트에서 구현해야 하는 프로토콜입니다. 여기 설명하는 프로토콜은 클라이언트-서버 통신에만 씁니다. Redis Cluster는 노드 간 메시지 교환에 다른 바이너리 프로토콜을 사용합니다.

RESP 버전

RESP 프로토콜의 첫 버전 지원은 Redis 1.2에서 도입되었습니다. Redis 1.2에서 RESP 사용은 선택적이었고 주로 프로토콜의 문제를 해소하는 역할을 했습니다.

Redis 2.0에서 프로토콜의 다음 버전인 RESP2가 클라이언트-서버 통신의 표준 방식이 되었습니다.

RESP3는 대체로 RESP2의 슈퍼셋이며, 주로 클라이언트 작성자의 삶을 조금 더 쉽게 만드는 데 목적이 있습니다. Redis 6.0이 RESP3 기능의 실험적 옵트인 지원을 도입했습니다(스트리밍 문자열과 스트리밍 집계 제외). 또한 HELLO 명령의 도입으로 클라이언트가 핸드셰이크해 연결의 프로토콜 버전을 업그레이드할 수 있게 되었습니다(Client handshake 참고).

Redis 버전 7부터 RESP2와 RESP3 클라이언트 모두 모든 코어 명령을 호출할 수 있습니다. 그러나 명령은 프로토콜 버전에 따라 다르게 타입된 응답을 반환할 수 있습니다. 각 명령에는 참조할 수 있는 RESP2 및 RESP3 반환 값 설명이 있습니다.

미래의 Redis 버전이 기본 프로토콜 버전을 바꿀 수는 있지만, RESP2가 완전히 폐기될 가능성은 낮습니다. 그러나 다가오는 버전의 새 기능이 RESP3 사용을 요구할 수는 있습니다.

네트워크 계층

클라이언트는 Redis 서버의 포트(기본 6379)에 TCP 연결을 만들어 연결합니다.

RESP는 기술적으로 TCP-특정이 아니지만, 프로토콜은 Redis 맥락에서 TCP 연결(또는 Unix 소켓 같은 동등한 스트림 지향 연결)과만 배타적으로 사용됩니다.

요청-응답 모델

Redis 서버는 여러 인수로 구성된 명령을 받아들입니다. 그런 다음 서버는 명령을 처리하고 클라이언트에 응답을 보냅니다.

이것이 가능한 가장 단순한 모델입니다. 그러나 몇 가지 예외가 있습니다:

  • Redis 요청은 파이프라인될 수 있습니다. 파이프라이닝은 클라이언트가 여러 명령을 한 번에 보내고 나중에 응답을 기다리게 합니다
  • RESP2 연결이 Pub/Sub 채널을 구독하면 프로토콜의 의미가 바뀌어 push 프로토콜이 됩니다. 클라이언트는 더 이상 명령을 보낼 필요가 없습니다. 서버가 (클라이언트가 구독한 채널에 대해) 새 메시지를 받는 즉시 자동으로 보내기 때문입니다
  • MONITOR 명령. MONITOR를 호출하면 연결이 임시 push 모드로 전환됩니다. 이 모드의 프로토콜은 명시되지 않았지만 파싱하기는 명확합니다
  • 보호 모드(Protected mode). 보호 모드에서 non-loopback 주소에서 Redis로 열린 연결은 서버가 거부·종료합니다. 연결을 종료하기 전에 Redis는 클라이언트가 소켓에 쓰는지와 무관하게 조건 없이 -DENIED 응답을 보냅니다
  • RESP3 Push 타입. 이름이 시사하듯 push 타입은 서버가 연결에 대역외(out-of-band) 데이터를 보낼 수 있게 합니다. 서버는 언제든 데이터를 push할 수 있고, 그 데이터는 클라이언트가 실행한 특정 명령과 반드시 관련될 필요는 없습니다

이 예외들을 제외하면 Redis 프로토콜은 단순한 요청-응답 프로토콜입니다.

RESP 프로토콜 설명

RESP는 본질적으로 여러 데이터 타입을 지원하는 직렬화 프로토콜입니다. RESP에서 데이터의 첫 바이트가 그 타입을 결정합니다.

Redis는 일반적으로 RESP를 요청-응답 프로토콜로 다음과 같이 사용합니다:

  • 클라이언트는 벌크 문자열 배열로 Redis 서버에 명령을 보냅니다. 배열의 첫(때로는 둘째) 벌크 문자열이 명령의 이름입니다. 배열의 후속 요소는 명령의 인수입니다
  • 서버는 RESP 타입으로 응답합니다. 응답의 타입은 명령의 구현과, 가능하면 클라이언트의 프로토콜 버전에 의해 결정됩니다

RESP는 표준 ASCII로 인코딩된 제어 시퀀스를 사용하는 바이너리 프로토콜입니다. 예를 들어 A 문자는 이진 바이트 값 65로 인코딩됩니다. 마찬가지로 문자 CR(\r), LF(\n), SP( )는 각각 이진 바이트 값 13, 10, 32를 가집니다.

\r\n(CRLF)은 프로토콜의 종결자로, 항상 부분들을 분리합니다.

RESP-직렬화된 페이로드의 첫 바이트는 항상 그 타입을 식별합니다. 후속 바이트들이 타입의 내용을 구성합니다.

모든 RESP 데이터 타입을 simple(단순), bulk(벌크), aggregate(집계) 중 하나로 분류합니다.

Simple 타입은 프로그래밍 언어의 스칼라와 유사해 단순 리터럴 값을 나타냅니다. Boolean과 Integer가 그런 예입니다.

RESP 문자열은 simple이거나 bulk입니다. Simple 문자열은 결코 캐리지 리턴(\r)이나 라인 피드(\n) 문자를 포함하지 않습니다. Bulk 문자열은 어떤 이진 데이터도 포함할 수 있고 이진 또는 blob이라고도 합니다. 벌크 문자열은 클라이언트가 넓은 멀티바이트 인코딩 같은 것으로 추가 인코딩·디코딩될 수 있음에 주의하세요.

Arrays와 Maps 같은 집계 타입은 다양한 수의 하위 요소와 중첩 수준을 가질 수 있습니다.

RESP3은 또한 전송 시작 시 길이를 모르는 페이로드를 위한 두 가지 스트리밍 인코딩(streamed strings와 streamed aggregated data types)을 정의합니다. 이것들은 테이블의 타입에 대한 대체 인코딩이지 타입 자체가 아니므로 테이블에 나열되지 않습니다.

Simple strings (단순 문자열)

Simple string은 더하기(+) 문자와 그 뒤의 문자열로 인코딩됩니다. 문자열은 CR(\r) 또는 LF(\n) 문자를 포함하면 안 되고 CRLF(즉 \r\n)로 종결됩니다.

Simple string은 최소 오버헤드로 짧고 비-이진 문자열을 전송합니다. 예를 들어 많은 Redis 명령이 성공 시 "OK"라고만 응답합니다. 이 Simple String의 인코딩은 다음 5바이트입니다:

+OK\r\n

Redis가 simple string으로 응답하면 클라이언트 라이브러리는 + 뒤의 첫 문자부터 문자열 끝까지(마지막 CRLF 바이트 제외)로 구성된 문자열 값을 호출자에게 반환해야 합니다.

이진 문자열을 보내려면 대신 bulk string을 쓰세요.

Simple errors (단순 오류)

RESP는 오류를 위한 특정 데이터 타입을 갖습니다. Simple error, 또는 간단히 error는 simple string과 유사하지만 첫 문자가 빼기(-) 문자입니다. RESP에서 simple string과 error의 차이는 클라이언트가 error를 예외로 취급해야 하는 반면, error 타입에 인코딩된 문자열이 오류 메시지 그 자체라는 점입니다.

기본 형식:

-Error message\r\n

Redis는 무언가 잘못됐을 때만 오류로 응답합니다. 예를 들어 잘못된 데이터 타입에 대해 연산을 시도하거나 명령이 존재하지 않을 때. 클라이언트는 Error 응답을 받으면 예외를 발생시켜야 합니다.

오류 응답의 예:

-ERR unknown command 'asdf'
-WRONGTYPE Operation against a key holding the wrong kind of value

- 뒤의, 첫 공백이나 개행까지의 첫 대문자 단어가 반환된 오류의 종류를 나타냅니다. 이 단어를 error prefix라 합니다. error prefix는 RESP error 타입의 일부가 아니라 Redis가 쓰는 관례입니다.

예를 들어 Redis에서 ERR은 일반 오류이고, WRONGTYPE은 클라이언트가 잘못된 데이터 타입에 대해 연산을 시도했음을 암시하는 더 구체적인 오류입니다. error prefix는 클라이언트가 정확한 오류 메시지를 확인하지 않고도 서버가 반환한 오류 타입을 이해하게 합니다.

클라이언트 구현은 여러 오류에 서로 다른 타입의 예외를 반환하거나, 오류 이름을 문자열로 직접 호출자에게 제공하는 일반적인 오류 포착 방법을 제공할 수 있습니다.

그러나 그런 기능은 거의 유용하지 않으므로 핵심으로 간주해서는 안 됩니다. 또한 더 단순한 클라이언트 구현은 false 같은 일반 오류 값을 반환할 수 있습니다.

Integers (정수)

이 타입은 부호 있는 10진 64비트 정수를 나타내는 CRLF-종결 문자열입니다.

RESP는 정수를 다음과 같이 인코딩합니다:

:[<+|->]<value>\r\n
  • 첫 바이트로 콜론(:)
  • 선택적 더하기(+) 또는 빼기(-) 부호
  • 정수의 부호 없는 10진 값으로 하나 이상의 십진 숫자(0..9)
  • CRLF 종결자

예를 들어 :0\r\n:1000\r\n은 정수 응답입니다(각각 0과 1000).

INCR, LLEN, LASTSAVE를 포함한 많은 Redis 명령이 RESP 정수를 반환합니다. 정수는 그것을 반환한 명령의 맥락 밖에서는 특별한 의미가 없습니다. 예를 들어 INCR에서는 증가 번호이고, LASTSAVE에서는 UNIX 타임스탬프입니다. 그러나 반환된 정수는 부호 있는 64비트 정수 범위 안임이 보장됩니다.

어떤 경우 정수는 true/false Boolean 값을 나타낼 수 있습니다. 예를 들어 SISMEMBER는 true에 1, false에 0을 반환합니다.

SADD, SREM, SETNX를 포함한 다른 명령은 데이터가 변경되면 1, 그렇지 않으면 0을 반환합니다.

Bulk strings (벌크 문자열)

Bulk string은 단일 이진 문자열을 나타냅니다. 문자열은 어떤 크기든 될 수 있지만, 기본적으로 Redis는 512MB로 제한합니다(proto-max-bulk-len 구성 지시어 참고).

RESP는 bulk string을 다음과 같이 인코딩합니다:

$<length>\r\n<data>\r\n
  • 첫 바이트로 달러 기호($)
  • 문자열의 바이트 길이의 부호 없는 10진 값으로 하나 이상의 십진 숫자(0..9)
  • CRLF 종결자
  • 데이터
  • 마지막 CRLF

그래서 문자열 "hello"는 다음과 같이 인코딩됩니다:

$5\r\nhello\r\n

빈 문자열의 인코딩:

$0\r\n\r\n

Null bulk strings (널 벌크 문자열)

RESP3에는 null 값을 위한 전용 데이터 타입이 있지만, RESP2에는 그런 타입이 없습니다. 대신 역사적 이유로 RESP2의 null 값 표현은 bulk string과 array 타입의 미리 결정된 형태를 통합니다.

Null bulk string은 존재하지 않는 값을 나타냅니다. GET 명령은 대상 키가 존재하지 않을 때 Null Bulk String을 반환합니다.

길이 -1(-1)의 bulk string으로 인코딩됩니다:

$-1\r\n

Redis 클라이언트는 서버가 null bulk string으로 응답하면 빈 문자열 대신 nil 객체를 반환해야 합니다. 예를 들어 Ruby 라이브러리는 nil을, C 라이브러리는 NULL(또는 응답 객체에 특별 플래그 설정)을 반환해야 합니다.

Arrays (배열)

클라이언트는 RESP 배열로 Redis 서버에 명령을 보냅니다. 마찬가지로 요소 집합을 반환하는 일부 Redis 명령은 응답으로 배열을 씁니다. 예로 리스트 요소를 반환하는 LRANGE 명령이 있습니다.

RESP Array 인코딩은 다음 형식을 씁니다:

*<number-of-elements>\r\n<element-1>...<element-n>
  • 첫 바이트로 별표(*)
  • 배열의 요소 수의 부호 없는 10진 값으로 하나 이상의 십진 숫자(0..9)
  • CRLF 종결자
  • 배열의 각 요소마다 추가 RESP 타입

그래서 빈 Array는 다음과 같습니다:

*0\r\n

"hello"와 "world"라는 두 bulk string으로 구성된 배열의 인코딩은:

*2\r\n$5\r\nhello\r\n$5\r\nworld\r\n

보시다시피 배열 앞의 *<count>CRLF 부분 다음에 배열을 구성하는 다른 데이터 타입들이 차례로 이어집니다. 예를 들어 세 정수의 Array는 다음과 같이 인코딩됩니다:

*3\r\n:1\r\n:2\r\n:3\r\n

배열은 혼합 데이터 타입을 포함할 수 있습니다. 예를 들어 다음 인코딩은 네 정수와 하나의 bulk string의 목록입니다:

*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$5\r\n
hello\r\n

(가독성을 위해 원시 RESP 인코딩을 여러 줄로 나눴습니다).

서버가 보낸 첫 줄은 *5\r\n입니다. 이 숫자 값은 클라이언트에 다섯 개의 응답 타입이 뒤따를 것임을 알립니다. 그런 다음 각 연속 응답이 배열의 요소가 됩니다.

모든 집계 RESP 타입은 중첩을 지원합니다. 예를 들어 두 배열의 중첩 배열은 다음과 같이 인코딩됩니다:

*2\r\n
*3\r\n
:1\r\n
:2\r\n
:3\r\n
*2\r\n
+Hello\r\n
-World\r\n

(가독성을 위해 원시 RESP 인코딩을 여러 줄로 나눴습니다).

위는 두 요소 배열을 인코딩합니다. 첫 요소는 세 정수(1, 2, 3)를 포함하는 배열입니다. 둘째 요소는 simple string과 error를 포함하는 또 다른 배열입니다.

어떤 곳에서 RESP Array 타입을 multi bulk라고 부를 수 있습니다. 둘은 같습니다.

Null arrays (널 배열)

RESP3에는 null 값을 위한 전용 데이터 타입이 있지만, RESP2에는 그런 타입이 없습니다. 대신 역사적 이유로 RESP2의 null 값 표현은 Bulk String과 array 타입의 미리 결정된 형태를 통합니다.

Null array는 null 값을 나타내는 대체 방법으로 존재합니다. 예를 들어 BLPOP 명령이 타임아웃되면 null array를 반환합니다.

Null array의 인코딩은 길이 -1의 배열입니다:

*-1\r\n

Redis가 null array로 응답하면 클라이언트는 빈 배열이 아니라 null 객체를 반환해야 합니다. 이는 빈 목록과 다른 조건(예: BLPOP의 타임아웃 조건)을 구별하는 데 필요합니다.

배열의 Null 요소

배열의 개별 요소는 null bulk string일 수 있습니다. 이는 존재하지 않는 요소들이지 빈 문자열이 아님을 알리기 위해 Redis 응답에서 쓰입니다. 예를 들어 SORT 명령을 GET 패턴 옵션과 쓸 때 지정된 키가 없으면 이런 일이 발생할 수 있습니다.

null 요소를 포함하는 배열 응답의 예:

*3\r\n
$5\r\n
hello\r\n
$-1\r\n
$5\r\n
world\r\n

위에서 둘째 요소는 null입니다. 클라이언트 라이브러리는 호출자에게 대략 다음과 같은 것을 반환해야 합니다:

["hello",nil,"world"]

Nulls

null 데이터 타입은 존재하지 않는 값을 나타냅니다.

Null의 인코딩은 밑줄(_) 문자와 CRLF 종결자(\r\n)입니다. Null의 원시 RESP 인코딩:

_\r\n

역사적 이유로 RESP2는 bulk string과 array의 null 값을 나타내는 두 개의 특별히 제작된 값을 특징으로 합니다. 이 이중성은 항상 프로토콜 자체에 의미를 더하지 않는 중복이었습니다.

RESP3에서 도입된 null 타입은 이 잘못을 고치는 것을 목표로 합니다.

Booleans (불리언)

RESP boolean은 다음과 같이 인코딩됩니다:

#<t|f>\r\n
  • 첫 바이트로 옥토소프 문자(#)
  • true 값은 t 문자, false 값은 f 문자
  • CRLF 종결자

Doubles (더블)

Double RESP 타입은 배정밀도 부동소수점 값을 인코딩합니다. Double은 다음과 같이 인코딩됩니다:

,[<+|->]<integral>[.<fractional>][<E|e>[sign]<exponent>]\r\n
  • 첫 바이트로 쉼표(,)
  • 선택적 더하기(+) 또는 빼기(-) 부호
  • 부호 없는 10진 정수 값으로 하나 이상의 십진 숫자(0..9)
  • 선택적 점(.), 뒤에 부호 없는 10진 소수 값으로 하나 이상의 십진 숫자(0..9)
  • 선택적 대문자/소문자 E(E 또는 e), 뒤에 선택적 더하기(+)·빼기(-) 지수 부호, 부호 없는 10진 지수 값으로 끝나는 하나 이상의 십진 숫자(0..9)
  • CRLF 종결자

숫자 1.23의 인코딩:

,1.23\r\n

소수 부분이 선택적이므로 정수 값 10은 정수와 더블 모두로 RESP-인코딩될 수 있습니다:

:10\r\n
,10\r\n

그런 경우 Redis 클라이언트는 해당 타입이 구현 언어에서 지원된다면 각각 네이티브 정수와 더블 값을 반환해야 합니다.

양의 무한대, 음의 무한대, NaN 값은 다음과 같이 인코딩됩니다:

,inf\r\n
,-inf\r\n
,nan\r\n

Big numbers (큰 숫자)

이 타입은 부호 있는 64비트 정수 범위 밖의 정수 값을 인코딩할 수 있습니다.

Big number는 다음 인코딩을 씁니다:

([+|-]<number>\r\n
  • 첫 바이트로 왼쪽 괄호 문자(()
  • 선택적 더하기(+) 또는 빼기(-) 부호
  • 부호 없는 10진 값으로 하나 이상의 십진 숫자(0..9)
  • CRLF 종결자

예:

(3492890328409238509324850943850943825024385\r\n

Big number는 양수나 음수일 수 있지만 소수를 포함할 수는 없습니다. big number 타입이 있는 언어로 쓰인 클라이언트 라이브러리는 big number를 반환해야 합니다. big number를 지원하지 않으면 클라이언트는 문자열을 반환하고, 가능하면 응답이 big integer임을 호출자에게 알려야 합니다(클라이언트 라이브러리가 쓰는 API에 따라).

Bulk errors (벌크 오류)

이 타입은 simple error의 목적과 bulk string의 표현력을 결합합니다.

다음으로 인코딩됩니다:

!<length>\r\n<error>\r\n
  • 첫 바이트로 느낌표(!)
  • 오류의 바이트 길이의 부호 없는 10진 값으로 하나 이상의 십진 숫자(0..9)
  • CRLF 종결자
  • 오류 자체
  • 마지막 CRLF

관례로 오류는 오류 메시지를 전달하는 대문자(공백 구분) 단어로 시작합니다.

예를 들어 오류 "SYNTAX invalid syntax"는 다음 프로토콜 인코딩으로 표현됩니다:

!21\r\n
SYNTAX invalid syntax\r\n

(가독성을 위해 원시 RESP 인코딩을 여러 줄로 나눴습니다).

Verbatim strings (베르바팀 문자열)

이 타입은 데이터의 인코딩에 대한 힌트를 제공한다는 점을 제외하면 bulk string과 유사합니다.

베르바팀 문자열의 RESP 인코딩:

=<length>\r\n<encoding>:<data>\r\n
  • 첫 바이트로 등호(=)
  • 문자열의 총 바이트 길이의 부호 없는 10진 값으로 하나 이상의 십진 숫자(0..9)
  • CRLF 종결자
  • 정확히 세(3) 바이트가 데이터의 인코딩을 나타냄
  • 콜론(:) 문자가 인코딩과 데이터를 구분
  • 데이터
  • 마지막 CRLF

예:

=15\r\n
txt:Some string\r\n

(가독성을 위해 원시 RESP 인코딩을 여러 줄로 나눴습니다).

일부 클라이언트 라이브러리는 이 타입과 string 타입의 차이를 무시하고 두 경우 모두 네이티브 문자열을 반환할 수 있습니다. 그러나 redis-cli 같은 명령줄 인터페이스 같은 대화형 클라이언트는 이 타입을 써서 출력을 문자열의 따옴표 없이 있는 그대로 인간 사용자에게 제시해야 함을 알 수 있습니다.

예를 들어 Redis INFO 명령은 개행을 포함한 보고서를 출력합니다. RESP3을 쓰면 redis-cli가 이를 올바르게 표시합니다(세 바이트가 "txt"인 Verbatim String 응답으로 보내지기 때문). 그러나 RESP2를 쓰면 redis-cli는 올바른 표시를 위해 하드코딩된 INFO 명령 탐색에 의존합니다.

Maps (맵)

RESP map은 키-값 튜플 컬렉션, 즉 사전 또는 해시를 인코딩합니다.

다음으로 인코딩됩니다:

%<number-of-entries>\r\n<key-1><value-1>...<key-n><value-n>
  • 첫 바이트로 퍼센트 문자(%)
  • 맵의 항목 또는 키-값 튜플 수의 부호 없는 10진 값으로 하나 이상의 십진 숫자(0..9)
  • CRLF 종결자
  • 맵의 모든 키와 값마다 두 개의 추가 RESP 타입

예를 들어 다음 JSON 객체:

{
    "first": 1,
    "second": 2
}

RESP로 이렇게 인코딩될 수 있습니다:

%2\r\n
+first\r\n
:1\r\n
+second\r\n
:2\r\n

(가독성을 위해 원시 RESP 인코딩을 여러 줄로 나눴습니다).

맵 키와 값 모두 RESP의 어떤 타입이든 될 수 있습니다.

Redis 클라이언트는 언어가 제공하는 관용적 사전 타입을 반환해야 합니다. 그러나 C 같은 저수준 프로그래밍 언어는 호출자에게 사전임을 나타내는 타입 정보와 함께 배열을 반환할 가능성이 높습니다.

RESP2에는 맵 타입이 없습니다. RESP2의 맵은 키와 값을 포함하는 평평한 배열로 표현됩니다. 첫 요소가 키이고, 뒤에 해당 값, 그다음 다음 키가 이어집니다: key1, value1, key2, value2, ...

Attributes (속성)

속성(attribute) 타입은 Map 타입과 정확히 같지만 첫 바이트로 % 대신 | 문자를 씁니다. Attributes는 Map 타입과 정확히 같은 사전을 설명합니다. 그러나 클라이언트는 그런 사전을 응답의 일부가 아니라 응답을 보강하는 보조 데이터로 간주해야 합니다.

참고: 아래 예에서 들여쓰기는 명확성을 위해서만 보여집니다; 추가 공백은 실제 응답의 일부가 아닙니다.

예를 들어 최신 Redis 버전은 실행된 모든 명령에 대해 키의 인기도를 보고하는 능력을 포함할 수 있습니다. MGET a b 명령의 응답은 다음과 같을 수 있습니다:

|1\r\n
    +key-popularity\r\n
    %2\r\n
        $1\r\n
        a\r\n
        ,0.1923\r\n
        $1\r\n
        b\r\n
        ,0.0012\r\n
*2\r\n
    :2039123\r\n
    :9543892\r\n

MGET의 실제 응답은 두 항목의 배열 [2039123, 9543892]뿐입니다. 반환된 속성은 원래 명령에 언급된 키들의 0.0~1.0 범위의 부동소수점으로 주어지는 인기도, 즉 요청 빈도를 지정합니다. 참고: Redis의 실제 구현은 다를 수 있습니다.

클라이언트가 응답을 읽다가 속성 타입을 만나면 속성을 읽고 응답 읽기를 계속해야 합니다. 속성 응답은 별도로 누적되어야 하고, 사용자는 그런 속성에 접근할 방법이 있어야 합니다. 예를 들어 고수준 언어의 세션을 상상하면:

> r = Redis.new
#<Redis client>
> r.mget("a","b")
#<Redis reply>
> r
[2039123,9543892]
> r.attribs
{:key-popularity => {:a => 0.1923, :b => 0.0012}}

Attributes는 특정 타입을 식별하는 프로토콜의 유효 부분 앞 어디든 나타날 수 있고, 바로 뒤따르는 응답 부분에 대한 정보만 제공합니다. 예:

*3\r\n
    :1\r\n
    :2\r\n
    |1\r\n
        +ttl\r\n
        :3600\r\n
    :3\r\n

위 예에서 배열의 세 번째 요소는 {ttl:3600}의 연관 보조 정보를 갖습니다. 속성을 해석하는 것은 클라이언트 라이브러리의 몫이 아니라, 합리적인 방식으로 호출자에게 전달해야 합니다.

Sets (집합)

Sets는 Arrays와 다소 유사하지만 순서가 없고 고유한 요소만 포함해야 합니다.

RESP set의 인코딩:

~<number-of-elements>\r\n<element-1>...<element-n>
  • 첫 바이트로 물결표(~)
  • 집합의 요소 수의 부호 없는 10진 값으로 하나 이상의 십진 숫자(0..9)
  • CRLF 종결자
  • Set의 각 요소마다 추가 RESP 타입

클라이언트는 프로그래밍 언어에서 네이티브 set 타입이 가능하면 그것을 반환해야 합니다. 대안으로 네이티브 set 타입이 없으면(C 예) 타입 정보와 결합된 배열을 쓸 수 있습니다.

Pushes (푸시)

RESP의 push는 대역외 데이터를 포함합니다. 이것들은 프로토콜의 요청-응답 모델의 예외이며 연결을 위한 일반 push 모드를 제공합니다.

Push 이벤트는 배열과 유사하게 인코딩되며, 첫 바이트만 다릅니다:

><number-of-elements>\r\n<element-1>...<element-n>
  • 첫 바이트로 큰-보다-기호(>)
  • 메시지의 요소 수의 부호 없는 10진 값으로 하나 이상의 십진 숫자(0..9)
  • CRLF 종결자
  • push 이벤트의 각 요소마다 추가 RESP 타입

푸시된 데이터는 RESP 데이터 타입 중 어떤 것보다 앞이나 뒤에 올 수 있지만 그 안에는 결코 없습니다. 즉 클라이언트는 예를 들어 map 응답 중간에서 push 데이터를 찾지 못합니다. 또한 푸시된 데이터가 명령의 응답 앞이나 뒤에 올 수도 있고, (명령 호출 없이) 단독으로 올 수도 있습니다.

클라이언트는 푸시된 데이터 처리 핸들링을 구현하는 콜백을 호출해 push에 반응해야 합니다.

Streamed strings (스트리밍 문자열)

Streamed string은 전송이 시작될 때 총 길이를 모르는 bulk string입니다. 프리픽스 길이 대신 송신자가 페이로드를 일련의 청크로 전송합니다.

Streamed string의 RESP 인코딩:

$?\r\n;<chunk-1-length>\r\n<chunk-1-data>\r\n...;0\r\n
  • 첫 바이트로 달러 기호($), 뒤에 길이 자리에 물음표(?)
  • CRLF 종결자
  • 하나 이상의 청크. 각 청크는 세미콜론(;)으로 시작하고, 청크의 바이트 길이의 부호 없는 10진 값, CRLF 종결자, 청크 데이터, 마지막 CRLF
  • 문자열의 끝을 표시하는 길이-0 청크(;0\r\n)

예:

$?\r\n
;5\r\n
Hello\r\n
;6\r\n
 world\r\n
;0\r\n

(가독성을 위해 원시 RESP 인코딩을 여러 줄로 나눴습니다).

청크들이 이어져 문자열의 값이 되므로, 이 예는 Hello world를 인코딩합니다.

Redis는 streamed string을 내보내지 않습니다. RESP3 지원이 스트리밍 타입을 배제하기 때문입니다(RESP versions 참고). RESP3 스펙은 모듈이 그 인코딩을 쓰는 것을 허용하므로, RESP3을 완전히 구현하는 클라이언트는 그것을 파싱할 수 있어야 합니다.

Streamed aggregated data types (스트리밍 집계 데이터 타입)

Arrays, Sets, Maps는 전송 시작 시 요소 수를 모를 때도 스트리밍될 수 있습니다. Streamed aggregate는 요소 수를 물음표(?)로 대체하고 전용 종결자로 끝을 표시합니다.

Streamed aggregate의 RESP 인코딩:

<first-byte>?\r\n<element-1>...<element-n>.\r\n
  • 집계의 보통 첫 바이트: 배열은 별표(*), 집합은 물결표(~), 맵은 퍼센트 기호(%)
  • 요소 수 자리에 물음표(?)
  • CRLF 종결자
  • 집계의 각 요소마다 추가 RESP 타입
  • 집계의 끝을 표시하는 점(.) 뒤에 CRLF 종결자

예:

*?\r\n
:1\r\n
:2\r\n
:3\r\n
.\r\n

(가독성을 위해 원시 RESP 인코딩을 여러 줄로 나눴습니다).

Streamed map의 요소는 일반 map처럼 필드-값 쌍이므로, streamed map은 짝수 개의 요소를 포함해야 합니다.

Streamed string처럼 Redis는 streamed aggregate를 내보내지 않습니다.

클라이언트 핸드셰이크

새 RESP 연결은 HELLO 명령을 호출해 세션을 시작해야 합니다. 이 관행은 두 가지를 이룹니다:

  • 서버가 RESP2 버전과 하위 호환되게 함. Redis에서 프로토콜 버전 3로의 전환을 더 부드럽게 하는 데 필요
  • HELLO 명령은 클라이언트가 여러 목적으로 쓸 수 있는 서버와 프로토콜에 대한 정보를 반환

HELLO 명령의 고수준 문법:

HELLO <protocol-version> [optional-arguments]

명령의 첫 인수는 연결을 설정하고 싶은 프로토콜 버전입니다. 기본적으로 연결은 RESP2 모드에서 시작합니다. 서버가 지원하지 않는 너무 큰 연결 버전을 지정하면 -NOPROTO 오류로 응답해야 합니다. 예:

Client: HELLO 4
Server: -NOPROTO sorry, this protocol version is not supported.

그 시점에 클라이언트는 더 낮은 프로토콜 버전으로 재시도할 수 있습니다.

마찬가지로 클라이언트는 RESP2만 말할 수 있는 서버를 쉽게 감지할 수 있습니다:

Client: HELLO 3
Server: -ERR unknown command 'HELLO'

클라이언트는 계속해서 RESP2로 서버와 통신할 수 있습니다.

프로토콜 버전이 지원되어도 HELLO 명령이 오류를 반환하고, 아무것도 하지 않으며 RESP2 모드에 머물 수 있다는 점에 주의하세요. 예를 들어 명령의 선택적 AUTH 절에서 잘못된 인증 자격 증명을 쓸 때:

Client: HELLO 3 AUTH default mypassword
Server: -ERR invalid password
(the connection remains in RESP2 mode)

HELLO 명령에 대한 성공 응답은 map 응답입니다. 응답의 정보는 부분적으로 서버-의존적이지만, 특정 필드는 모든 RESP3 구현에 필수입니다:

  • server: "redis"(또는 다른 소프트웨어 이름)
  • version: 서버의 버전
  • proto: 지원되는 RESP 프로토콜의 최고 버전

Redis의 RESP3 구현에서 다음 필드도 내보내집니다:

  • id: 연결의 식별자(ID)
  • mode: "standalone", "sentinel" 또는 "cluster"
  • role: "master" 또는 "replica"
  • modules: Bulk Strings의 Array로 로드된 모듈 목록

Redis 서버에 명령 보내기

이제 RESP 직렬화 형식에 익숙해졌으니, Redis 클라이언트 라이브러리를 쓰는 데 활용할 수 있습니다. 클라이언트와 서버의 상호작용이 어떻게 동작하는지 더 지정할 수 있습니다:

  • 클라이언트는 bulk string만으로 구성된 배열을 Redis 서버에 보냄
  • Redis 서버는 유효한 RESP 데이터 타입을 응답으로 보내 클라이언트에 응답함

예를 들어 전형적인 상호작용은 다음과 같을 수 있습니다.

클라이언트는 mylist 키에 저장된 목록의 길이를 얻기 위해 LLEN mylist 명령을 보냅니다. 그런 다음 서버는 다음 예처럼 정수 응답으로 응답합니다(C: 클라이언트, S: 서버):

C: *2\r\n
C: $4\r\n
C: LLEN\r\n
C: $6\r\n
C: mylist\r\n

S: :48293\r\n

평소처럼 단순화를 위해 프로토콜의 서로 다른 부분을 개행으로 분리했지만, 실제 상호작용은 클라이언트가 *2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n을 하나의 덩어리로 보내는 것입니다.

여러 명령과 파이프라이닝

클라이언트는 같은 연결로 여러 명령을 발행할 수 있습니다. 파이프라이닝이 지원되므로, 클라이언트가 단일 쓰기 연산으로 여러 명령을 보낼 수 있습니다. 클라이언트는 응답 읽기를 건너뛰고 명령을 차례로 계속 보낼 수 있습니다. 모든 응답을 마지막에 읽을 수 있습니다.

자세한 내용은 Pipelining을 참조하세요.

인라인 명령 (Inline commands)

때로 Redis 서버에 명령을 보내야 하는데 telnet만 가능할 수 있습니다. Redis 프로토콜은 구현하기 단순하지만 대화형 세션에는 이상적이지 않고, redis-cli가 항상 가능하지 않을 수 있습니다. 이런 이유로 Redis는 또한 인라인 명령 형식의 명령을 받아들입니다.

다음 예는 인라인 명령을 사용한 서버/클라이언트 교환을 보여줍니다(서버 대화는 S:, 클라이언트 대화는 C:로 시작):

C: PING
S: +PONG

서버가 정수를 반환하는 인라인 명령의 또 다른 예:

C: EXISTS somekey
S: :0

기본적으로 인라인 명령을 발행하려면 telnet 세션에 공백-구분 인수를 씁니다. 어떤 명령도 *(RESP Array의 식별 바이트)로 시작하지 않으므로, Redis는 이 조건을 감지하고 명령을 인라인으로 파싱합니다.

Redis 프로토콜을 위한 고성능 파서

Redis 프로토콜은 사람이 읽을 수 있고 구현하기 쉬우면서도, 그 구현은 바이너리 프로토콜과 유사한 성능을 보일 수 있습니다.

RESP는 벌크 데이터 전송에 프리픽스 길이를 사용합니다. 이는 JSON 파싱과 달리 페이로드에서 특수 문자를 스캔할 필요가 없게 합니다. 같은 이유로 페이로드의 따옴표·이스케이프가 필요하지 않습니다.

집계 타입(bulk string이나 array 같은)의 길이를 읽는 것은 CR 문자를 스캔하면서 문자당 단일 연산을 수행하는 코드로 처리할 수 있습니다.

예(C):

#include <stdio.h>

int main(void) {
    unsigned char *p = "$123\r\n";
    int len = 0;

    p++;
    while(*p != '\r') {
        len = (len*10)+(*p - '0');
        p++;
    }

    /* Now p points at '\r', and the length is in len. */
    printf("%d\n", len);
    return 0;
}

첫 CR이 식별된 후, 그것과 뒤따르는 LF를 추가 처리 없이 건너뛸 수 있습니다. 그런 다음 페이로드를 어떤 식으로든 살피지 않는 단일 읽기 연산으로 벌크 데이터를 읽을 수 있습니다. 마지막으로 나머지 CR과 LF 문자는 추가 처리 없이 버려집니다.

바이너리 프로토콜과 성능이 비슷하면서도 Redis 프로토콜은 대부분의 고수준 언어에서 구현이 훨씬 더 간단해, 클라이언트 소프트웨어의 버그 수를 줄입니다.

Redis 클라이언트 작성자를 위한 팁

  • 테스트 목적으로 Lua의 타입 변환을 사용해 Redis가 필요한 어떤 RESP2/RESP3으로든 응답하게 하세요. 예를 들어 RESP3 double은 이렇게 생성할 수 있습니다:
EVAL "return { double = tonumber(ARGV[1]) }" 0 1e0

더 알아보기 (Learn more)

  • Redis 클라이언트-사이드 캐싱 (Client-side caching)
  • Redis Pub/Sub 메시징
  • Redis 명령 사용하기 (Using commands)