Redis 의 RESP 프로토콜

Redis 의 RESP 프로토콜

Redis 클라이언트는 Redis 서버에(보통 6379번 포트) TCP 커넥션을 맺어 통신을 하는데 RESP 라는 프로토콜을 사용하여 통신을 한다.

RESP 란?

RESP(REdis Serialization Protocol) 는 Redis 클라이언트가 Redis 서버와 통신할 때 사용하는 프로토콜이다.

RESP 는 Redis 1.2 버전에서 처음 소개되어 2.0 버전부터는 Redis 클라이언트가 구현해야만하는 표준 프로토콜이 되었다.

(참고로 RESP 는 Redis 의 client-server 통신에만 사용되며, Redis Cluster 에서 노드간의 통신은 RESP 가 아닌 binary protocol 을[1] 사용한다.)

RESP 는 꼭 Redis 가 아니더라도 Client-Server 방식으로 통신한다면 어디에서든 사용될 수 있는 프로토콜이다.

꼭 TCP 에서 사용되어야 하는 것도 아니지만 Redis 에서는 TCP 로만 사용한다.

RESP 는 다음 3가지 요소를 중점적으로 여기며 고안되었다.

  • 쉬운 구현
  • 빠른 파싱
  • 사람이 읽을 수 있어야 함

위와 같은 장점에 더해, Redis 문서에 따르면 성능 또한 binary protocol 과 비슷한 수준이라고 한다.

Request-Response 모델

Redis 는 기본적으로 다음의 2가지 경우를 제외하곤 Request-Response 모델을 사용한다.

  • Pipelining
    • 여러개의 명령어(Command) 를 한 번에 보내고 모든 답장이 올 때까지 기다린다.
  • Pub/Sub
    • 어떤 Client 가 특정 channel 을 subscribe 하면 push protocol 로 전환되어 더이상 명령어를 보내지 않게 된다.

Redis 의 Request-Response 프로토콜은 다음과 같이 동작한다.

  • 클라이언트가 Bulk String 의 Array 타입으로 명령어를 서버로 전송한다.
  • 서버는 클라이언트가 보낸 명령어에 맞는 타입으로 응답들을 보낸다.

RESP 의 Data Types

RESP 에는 5가지의 데이터 타입이 존재한다.

  • Simple Strings
  • Erros
  • Integers
  • Bulk Strings
  • Arrays

서버와 클라이언트가 주고받는 메시지 내 데이터의 타입은 위 5가지 중 하나이다.

데이터의 타입은 데이터의 첫번째 바이트를 통해 구분한다. 각 타입들의 첫번째 문자는 다음과 같다.

  • Simple Strings 는 "+"
  • Erros 는 "-"
  • Integers 는 ":"
  • Bulk Strings 는 "$"
  • Arrays 는 "*"

RESP 를 통해 오고가는 Request 와 Response는 항상 "\r\n" 로 끝난다.

그럼 각각의 타입에 대해 자세히 알아보자.

Simple Strings

Simple Strings 타입은 binary-safe[2] 하지 않은 일반 문자열을 전송할 때 사용하는 데이터 타입이다.

(binary-safe 한 문자열을 전송하기 위해서는 이후 설명할 Bulk Strings 타입으로 전송해야 한다.)

앞서 설명한 것처럼 첫번째 바이트는 "+" 다. 뒤이어 문자열이 나오고 "\r\n" 으로 끝난다.

참고로 문자열은 Newline 을 포함할 수 없다. 다음은 Simple Strings 타입을 전송한 예다.

"+OK\r\n"

Errors

Errors 타입은 이름대로 에러 정보에 대한 타입이며, "-" 문자로 시작한다.

강제는 아니지만 관습적으로 "-ERRORTYPE Description for the error" 와 같은 형태로 먼저 에러의 이름을 쓰고

한 칸 띄운 뒤에 에러 발생 원인과 같은 더 자세한 설명을 적는다.

클라이언트에서 이러한 타입의 데이터를 받으면, 에러 타입에 맞는 예외를 발생시키거나 false 를 반환하는 식으로 구현할 수 있겠다.

다음은 Errors 타입 데이터를 전송한 예이다.

"-ERR unknown command 'foobar'\r\n"
"-WRONGTYPE Operation against a key holding the wrong kind of value\r\n"

Integers

정수 형태의 데이터이며, ":" 문자로 시작한다.

또한, 숫자의 크기는 signed 64 bit 범위 내여야 한다.

대표적으로, INCR, LLEN, LASTSAVE 와 같은 명령어에 대한 응답으로 오는 데이터의 타입이 이 Integers 타입이다.

일부 명령어(EXISTS 등) 에서는 true/false 의 의미로 이 Integers 타입의 1/0 을 쓰기도 한다.

Bulk Strings

binary-safe 한 문자열을 나타낼 때 사용하는 타입이며, "$" 로 시작한다.

"$" 에 이어 문자열의 길이가 주어지고, 그 다음 "\r\n", 그리고 실제 문자열이 등장한다.

즉, "$문자열길이\r\n문자열\r\n" 의 형태를 가진다. (e.g. "$6\r\nfoobar\r\n")

빈 문자열을 나타낼 때는 "$0\r\n\r\n" 를 사용한다.

주의해야 할 점은, null 값을 나타낼 때는 "$-1\r\n" 를 사용한다는 것이다. 이것을 Null Bulk String 이라고 부른다.

만약 Redis 서버에서 Null Bulk String 을 받았다면 클라이언트에서는, 예를 들어 C 라이브러리라면 NULL 을 반환해야하하고, Ruby 라이브러리라면 nil 을 반환해야 할 것이다.

Arrays

Arrays 는 첫번째 byte 로 "*" 를 가지며, 이어서 10진수로 배열의 크기와 "\r\n" 이 나온다.
또한, 배열 내 원소는 각각 특정한 타입을 가질 수가 있으며, 그 타입은 모두 달라도 된다.

예를 들어 1 이라는 Integer, "2" 라는 Simple String, 그리고 "bulk" 라는 Bulk String 을 가지는 Arrays 타입의 Response 는 다음과 같은 형태를 띈다.

"*3\r\n:1\r\n+2\r\n$4\r\nbulk\r\n"

Null Array 의 경우에는 사이즈가 0인 Array 와 구분하기 위해 다음과 같이 표시한다.

"*-1\r\n"

예를 들어 BLPOP command 가 timeout 이 났을 때 위와 같은 응답을 줄 수 있다.
클라이언트는 서버로부터 Null Array 를 받으면 빈 배열이 아닌, null object 를 반환해야 한다.

중첩된 형태, 즉 Arrays 의 Arrays 도 가능하다.
다음은 2개의 배열로 이루어진 배열이다. 편의상 알아보기 좋게 따옴표를 빼고 줄바꿈을 하였다.

*2\r\n
*3\r\n
:1\r\n
:2\r\n
:3\r\n
*2\r\n
+Foo\r\n
-Bar\r\n

Redis 서버에 Command 보낼 때

맨 처음에 Redis 클라이언트가 서버에 명령어를 보낼 때 Bulk String 의 Array 타입으로 전송한다고 했다.

만약 Redis 서버에 저장된 mylist 라는 List 데이터의 길이를 알고자 한다면 LLEN mylist 명령어를 사용할 것이다.

그리고 서버는 이에 대한 응답으로 리스트의 길이를 반환할 것이다.

이때 실제로 Client 와 Server 간에 오고간 데이터를 뜯어보면 다음과 같을 것이다. (편의상 줄을 바꿈)

  • Client

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

  • Server

:48293\r\n

참고: https://redis.io/topics/protocol#resp-simple-strings


  1. binary protocol 는 전송하고자 하는 데이터를 human readable 한 방식으로 인코딩하는 text protocol 과는 반대되는 개념이다. Text protocol 의 대표적인 예인 HTTP 는 "200 OK" 라는 상태 코드의 200 이라는 값을 나타낼 때 '2', '0', '0' 이라는 문자를 사용한다. 하지만 만약 200 을 binary protocol 로 전송한다면 단지 200을 binary 로 나타낸 값을 전송할 것이다. 그렇기 때문에 binary protocol 이 좀 더 compact 한 형태라는 것을 알 수 있다. ↩︎

  2. 문자열이 binary-safe 하다는 말은 문자열 내에 그 어떤 문자가 등장하더라도 문자열의 일부로 인식된다는 뜻이다. 예를 들어, C 에서 문자열 중간에 널 문자가 있다면 이를 문자열의 일부로 여기는 것이 아니라 널 문자 이후의 문자열을 무시하게 되는데, 이는 binary-safe 하지 않은 문자열의 예시이다. Redis 의 Bulk Strings 타입은 binary-safe 하다. 맨 앞에 문자열의 길이가 주어지는데, 그 길이 만큼은 어떠한 문자가 등장하더라도 문자열의 일부로 인식하기 때문이다. ↩︎

Comments