본문 바로가기
잡(job)기술

RTSP 클라이언트 첫걸음: TCP 연결하고 OPTIONS 한 번 던져보기

by 무니이구나 2026. 2. 21.

들어가며 — 서버가 정말로 “대답하는지”부터 보자

RTSP 클라이언트를 직접 만들어보겠다고 마음먹으면, 머릿속이 조금 복잡해진다.
소켓, 프로토콜, 세션, RTP, 파싱… 해야 할 일이 한가득이다. 그럴수록 한 걸음 물러서서 마음을 잡는다.

“일단 서버가 말은 하는지 보자.”

 

이 단계에서는 거창한 일을 하지 않는다. 그냥 TCP로 붙고, OPTIONS 요청 하나 보내고, 서버가 뭐라고 답하는지 원문 그대로 출력해본다.

딱 여기까지. 생각보다 이 과정이 중요하다. 응답을 직접 눈으로 보면, 프로토콜이 갑자기 추상적인 개념이 아니라 살아 있는 텍스트로 느껴진다.


TCP 연결 + RTSP OPTIONS 요청 + 응답 출력

왜 이걸 먼저 할까?

RTSP는 HTTP처럼 텍스트 기반 프로토콜이다. 하지만 포트는 보통 8554나 554를 쓴다. 약간 다르다. 클라이언트를 만들 때 가장 먼저 확인해야 할 건 사실 단순하다.

  • 서버가 실행 중인가?
  • 포트가 열려 있는가?
  • 요청을 이해하는가?
  • 응답을 돌려주는가?

이걸 확인하지 않은 채 파싱 로직부터 짜기 시작하면, 나중에 어디가 문제인지 헷갈리기 쉽다. 경험상, 기본 연결 확인을 건너뛰면 디버깅 시간이 배로 늘어난다. 그래서 OPTIONS부터 보낸다. 가장 가벼운 요청이다.

 

OPTIONS는 뭐 하는 녀석인가?

OPTIONS는 서버에게 이렇게 묻는다.

“당신, 어떤 명령을 처리할 수 있나요?”

 

서버는 보통 이런 식으로 답한다.

RTSP/1.0 200 OK
CSeq: 1
Public: OPTIONS, DESCRIBE, SETUP, PLAY, TEARDOWN

 

이 응답을 보는 순간, 몇 가지가 동시에 확인된다.

  • TCP 연결 성공
  • RTSP 프로토콜 동작 중
  • 요청 문법이 틀리지 않음

뭔가 대단한 일을 한 건 아니지만, 시스템이 살아 있다는 확신이 생긴다.


코드 흐름을 조금 천천히 살펴보자

파일 이름은 step1_connect_options.cpp. 구조는 크게 세 덩어리다. 연결하고, 요청 보내고, 응답 읽는다. 아주 단순하다.

#include <string>
#include <iostream>
#include <netdb.h>
#include <unistd.h>

static void die(const std::string &msg)
{
    std::cerr << "FATAL: " << msg << "\n";
    std::exit(1);
}

static int connect_tcp(const std::string &host, int port)
{
    addrinfo hints{};
    hints.ai_family = AF_INET;
    hints.ai_socktype = SOCK_STREAM;

    addrinfo *res = nullptr;
    if (getaddrinfo(host.c_str(), std::to_string(port).c_str(), &hints, &res) != 0)
    {
        die("getaddrinfo failed");
    }

    int fd = -1;
    for (auto *p = res; p; p = p->ai_next)
    {
        fd = ::socket(p->ai_family, p->ai_socktype, p->ai_protocol);
        if (fd < 0)
            continue;

        if (::connect(fd, p->ai_addr, p->ai_addrlen) == 0)
            break;

        ::close(fd);
        fd = -1;
    }
    freeaddrinfo(res);

    if (fd < 0)
        die("connect failed");
    return fd;
}

static void send_all(int fd, const std::string &s)
{
    const char *p = s.data();
    size_t left = s.size();
    while (left > 0)
    {
        ssize_t n = ::send(fd, p, left, 0);
        if (n <= 0)
            die("send failed");
        p += n;
        left -= (size_t)n;
    }
}

int main()
{
    const std::string host = "127.0.0.1";
    const int port = 8554;

    const std::string url = "rtsp://127.0.0.1:8554/mystream";

    int fd = connect_tcp(host, port);
    std::cerr << "[OK] Connected to " << host << ":" << port << "\n";

    // OPTIONS 요청
    // - CSeq는 RTSP에서 필수, 우선은 1로 고정.
    std::string req;
    req += "OPTIONS " + url + " RTSP/1.0\r\n";
    req += "CSeq: 1\r\n";
    req += "User-Agent: step1-minimal\r\n";
    req += "\r\n";

    std::cerr << "----- RTSP REQUEST -----\n"
              << req << "------------------------\n";
    send_all(fd, req);

    // 응답 출력
    char buf[4096];
    int n = (int)::recv(fd, buf, sizeof(buf) - 1, 0);
    if (n <= 0)
        die("recv failed / socket closed");
    buf[n] = '\0';

    std::cerr << "----- RTSP RESPONSE (partial) -----\n";
    std::cerr << buf;
    std::cerr << "-----------------------------------\n";

    ::close(fd);
    return 0;
}

 

 

TCP 연결 — 생각보다 평범하다

connect_tcp() 함수에서는 getaddrinfo()를 호출한다. 이 함수는 이렇게 이해하면 쉽다.

“문자열로 된 주소를 실제 연결 가능한 형태로 바꿔줘.”

 

AF_INET은 IPv4를 쓰겠다는 뜻이고, SOCK_STREAM은 TCP를 쓰겠다는 의미다.

그 다음은 전형적인 순서다.

  1. socket() 생성
  2. connect() 시도
  3. 실패하면 닫고 다음 주소로

여기까지는 RTSP와 전혀 상관없다. 그냥 네트워크 기본기다. 가끔은 이 기본기가 제일 중요하다.

 

RTSP 요청 만들기 — 텍스트 한 줄 한 줄이 전부다

요청은 문자열을 이어 붙여 만든다.

OPTIONS rtsp://127.0.0.1:8554/mystream RTSP/1.0\r\n
CSeq: 1\r\n
User-Agent: step1-minimal\r\n
\r\n

 

여기서 가장 중요한 건 줄바꿈이다. \r\n을 정확히 써야 한다. 그리고 헤더 끝에는 빈 줄 하나.

이 규칙을 어기면 서버는 조용히 연결을 끊거나, 엉뚱한 반응을 보인다. 처음엔 왜 안 되는지 한참을 헤맬 수도 있다.

CSeq도 필수다. RTSP는 이 숫자로 요청과 응답을 짝지어 관리한다. 지금은 1로 고정했지만, 실제 구현에서는 요청마다 증가시켜야 한다.

 

응답 읽기 — 아직은 거칠게

이번 단계에서는 recv()를 한 번만 호출한다. 응답 전체를 보장하지도 않는다. Content-Length도 무시한다. 헤더와 바디도 구분하지 않는다.

완벽하지 않다. 그런데 일부러 이렇게 둔다. 지금은 “응답이 오느냐”가 목표니까. 조금 덜 정교해도 괜찮다.
오히려 이 단순함 덕분에 흐름이 또렷하게 보인다.

 

실행해보면

빌드하고 실행하면, 터미널에 RTSP 응답이 그대로 찍힌다.

----- RTSP RESPONSE (partial) -----
RTSP/1.0 200 OK
CSeq: 1
Public: DESCRIBE, ANNOUNCE, SETUP, PLAY, RECORD, PAUSE, GET_PARAMETER, TEARDOWN
Server: gortsplib
-----------------------------------

 

이걸 보는 순간 묘하게 안심이 된다. 아, 서버가 대화할 준비가 되어 있구나.

 


 

핵심 정리

  • RTSP는 TCP 위에서 동작하는 텍스트 기반 프로토콜이다.
  • 가장 간단한 요청은 OPTIONS.
  • CSeq는 필수 헤더다.
  • 줄바꿈은 반드시 \r\n.
  • 이번 단계의 목표는 파싱이 아니라 “응답 확인”.