
들어가며 — 서버가 정말로 “대답하는지”부터 보자
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를 쓰겠다는 의미다.
그 다음은 전형적인 순서다.
- socket() 생성
- connect() 시도
- 실패하면 닫고 다음 주소로
여기까지는 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.
- 이번 단계의 목표는 파싱이 아니라 “응답 확인”.
'잡(job)기술' 카테고리의 다른 글
| C++ 프로젝트가 나뉘어 있다면, CMake로 한 번에 묶어 빌드하자 - 그리고 Sanitizer는 꼭 붙이자 (1) | 2026.02.26 |
|---|---|
| RTSP 응답을 읽는다는 것 - 왜 recv() 한 번으로 끝내면 안 되는가 (3) | 2026.02.25 |
| RTSP 클라이언트를 만들기 전에 꼭 해야 할 준비― mediamtx + FFmpeg으로 테스트 환경 먼저 만들기 (3) | 2026.02.21 |
| SVN Merge 실수 줄이는 법: 체리픽부터 mergeinfo까지 실전 전략 5가지 (0) | 2026.02.11 |
| NestJS 모듈의 비밀: 왜 클래스 안은 텅 비어 있을까? (0) | 2026.02.06 |