프레이밍
고친 사람 github-actions[bot]
프레이밍은 끊임없이 이어지는 데이터를 한 덩이씩 끊어 읽게 해 줍니다. 보내는 쪽이 한 덩이의 시작과 끝을 표시해 둡니다. 받는 쪽은 그 표시를 보고 덩이를 잘라 냅니다. 이 표시가 없는 데이터는 어디까지가 한 덩이인지 알아낼 길이 없습니다.
쉽고 빠른 이해
무슨 일을 하는 물건인가 — 이어져 흐르는 데이터에 덩이의 경계를 그어 주는 일입니다. 랜선 위를 흐르는 신호에서 데이터 한 덩이를 떼어 내는 것이 프레이밍입니다.
왜 이렇게 하나 — 선 위로도 연결 위로도 데이터는 한 줄로 흘러갑니다. 몇 번에 나눠 보냈는지는 받는 쪽에 전해지지 않습니다. 경계가 없으면 받는 쪽은 두 덩이를 붙여 읽거나 한 덩이를 반만 읽습니다.
어떻게 도나
- 보내는 쪽이 덩이 앞에 길이를 적거나 앞뒤에 약속한 표시를 붙입니다
- 받는 쪽은 그 길이나 표시를 읽어 어디서 끊을지 정합니다
- 덩이 하나가 다 모이면 위로 넘기고 남은 바이트는 다음 덩이로 둡니다
대가 — 길이와 표시가 차지하는 만큼 실어 나를 몫이 줄어듭니다. 그리고 양쪽이 같은 규칙을 써야 합니다. 한쪽이 다르게 끊으면 그다음 덩이부터 전부 어긋납니다.
상세
전화로 긴 번호 여러 개를 받아 적는 자리를 떠올려 봅시다. 부르는 사람이 쉬지 않고 숫자만 이어 읽으면, 받아 적는 사람에게는 어디서 한 번호가 끝나는지 알아낼 길이 없습니다. 부르는 사람이 「여덟 자리입니다」라고 미리 일러 주거나 번호 끝마다 「다음」 하고 끊어 줘야 번호가 하나씩 제대로 적힙니다.
프레이밍은 이어져 흐르는 데이터에 덩이의 경계를 표시해, 받는 쪽이 한 덩이씩 떼어 낼 수 있게 하는 일입니다. 랜선 위를 지나는 신호에서 프레임 한 장을 떼어 내는 일이 그 하나입니다. 프레임은 선 하나를 건너가는 데이터 한 덩이를 가리킵니다. 열린 연결에서 메시지 하나를 잘라 내는 일도 프레이밍입니다.
경계가 사라지는 통로
데이터를 나르는 통로는 대개 덩이를 기억하지 않습니다. 선 위로는 0과 1이 줄줄이 흐를 뿐이라 어디까지가 한 덩이인지가 신호에 새겨져 있지 않습니다. 덩이 경계를 그대로 지켜 주는 통로도 있습니다. 그런 통로 위에서는 이 일을 따로 하지 않습니다.
연결 위도 같습니다. TCP(Transmission Control Protocol, 전송 제어 프로토콜)는 두 프로그램 사이에 연결을 만들어 데이터를 순서대로 날라 주는 규약입니다.
그 연결이 주는 것은 바이트 스트림입니다. 바이트 스트림은 길이가 미리 정해지지 않은 바이트 한 줄입니다. 보내는 쪽이 몇 번에 나눠 썼는지는 받는 쪽에 전해지지 않습니다. 지켜지는 것은 바이트가 놓인 순서뿐입니다.
그래서 받는 쪽은 두 가지 실수를 합니다. 두 덩이가 붙어서 한 덩이로 읽히거나, 한 덩이가 다 오기 전에 반만 읽힙니다. 경계를 데이터 안에 적어 두면 이 두 실수가 사라집니다.
block-beta columns 6 T["보낸 쪽이 끊어 보낸 덩이"]:6 A["덩이 A"]:2 B["덩이 B"]:2 C["덩이 C"]:2 U["받는 쪽이 읽어 온 덩어리"]:6 R1["A 와 B 가 붙어 한 덩이로"]:4 R2["C 는 반만"]:1 R3["아직 안 옴"]:1
위아래 칸금이 안 맞는 것이 그 두 실수입니다.
경계를 세우는 방법 넷
표시하는 방법은 크게 넷입니다. 방법마다 받는 쪽이 할 일이 달라지므로 받는 쪽 눈으로 따라갑니다.
한 덩이는 경계를 알리는 표시와 알맹이로 나뉩니다. 알맹이는 덩이에서 길이와 표시를 뺀, 실제로 나르려는 데이터입니다. 페이로드라고도 부릅니다.
| 방법 | 어떻게 표시하나 | 받는 쪽이 하는 일 |
|---|---|---|
| 길이 접두 | 덩이 앞에 이 덩이의 바이트 수를 적습니다 | 길이를 먼저 읽고 그만큼 세어 떼어 냅니다 |
| 구분자 | 덩이의 끝에, 또는 앞뒤에 약속한 표시를 넣습니다 | 그 표시가 나올 때까지 읽습니다 |
| 고정 길이 | 표시 없이 모든 덩이를 같은 크기로 맞춥니다 | 정해진 크기만큼씩 끊습니다 |
| 연결 닫기 | 보낼 것을 다 보내고 연결을 끊습니다 | 더 읽을 것이 없을 때까지 읽습니다 |
길이 접두는 받는 쪽이 얼마를 기다려야 하는지 미리 알려 줍니다. 그 대가로 보내는 쪽은 덩이를 다 만들어 길이를 센 뒤에야 첫 바이트를 내보낼 수 있습니다.
구분자는 길이를 모르는 데이터도 흘려보낼 수 있습니다. 다만 알맹이 안에 그 표시와 같은 값이 들어 있으면 엉뚱한 데서 끊깁니다. 이 문제는 아래 소절에서 다시 봅니다.
고정 길이는 규칙이 제일 단순합니다. 대신 짧은 덩이도 정해진 크기를 다 채워 보내야 해서 남는 공간이 그대로 선 위로 나갑니다.
연결 닫기는 한 번 쓰고 마는 교환에만 씁니다. 연결을 이어서 다시 쓸 수 없습니다. 중간에 끊긴 것인지 다 보낸 것인지도 받는 쪽이 가릴 수 없습니다.
길이 접두를 읽는 순서
길이 접두를 쓰는 받는 쪽은 버퍼 하나를 두고 같은 검사를 되풀이합니다. 버퍼는 아직 다 읽지 못한 바이트를 쌓아 두는 그릇입니다.
flowchart TD
A[버퍼에 쌓인 바이트를 본다] --> B{길이 접두가 다 왔나}
B -- 아니다 --> Z[더 올 때까지 기다린다]
B -- 그렇다 --> C[길이를 읽는다]
C --> D{알맹이가 그만큼 왔나}
D -- 아니다 --> Z
D -- 그렇다 --> E[한 덩이를 떼어 위로 넘긴다]
E --> A
Z -- 바이트가 더 오면 --> A
덩이 하나를 떼어 낸 뒤 버퍼에 남은 바이트는 버리지 않습니다. 그것이 다음 덩이의 앞부분이기 때문입니다. 한 번 읽어 온 데이터에 덩이가 둘 들어 있기도 하고 반 덩이만 들어 있기도 합니다.
flowchart TD
subgraph S1["1 · 반 덩이만 쌓였다"]
B1["길이 · 알맹이 앞부분"]
end
subgraph S2["2 · 한 덩이가 다 오고 다음 덩이 앞부분도 왔다"]
B2["길이 · 알맹이 · 길이 · 알맹이 앞부분"]
end
subgraph S3["3 · 한 덩이를 떼어 낸 뒤"]
B3["길이 · 알맹이 앞부분"]
end
B1 -- 더 받는다 --> B2
B2 -- 한 덩이를 떼어 낸다 --> B3
버퍼는 이렇게 찼다 줄었다 합니다. 마지막 그릇에 남은 것이 다음 덩이의 앞부분입니다.
알맹이에 섞인 구분자
구분자를 쓰면 알맹이 안에 그 값이 들어 있을 때가 문제입니다. 받는 쪽은 그것을 경계로 보고 덩이를 거기서 잘라 버립니다.
해결은 보내기 전에 알맹이를 손보는 것입니다. 구분자와 같은 값이 나오면 그 앞에 약속한 탈출 문자를 하나 끼워 넣습니다. 받는 쪽은 탈출 문자를 만나면 바로 뒤 값을 경계가 아니라 알맹이로 읽습니다. 이 일을 바이트 스터핑이라고 합니다.
구분자를 F, 탈출 문자를 E 라고 두겠습니다. 여기서는 덩이를 F 로 앞뒤에서 감싸는 방식으로
둡니다.
block-beta columns 6 L1["알맹이"]:6 a1["A"]:2 f1["F"]:2 b1["B"]:2 L2["선 위로 나간 것 · 안쪽 F 앞에 E 를 끼웠다"]:6 o1["F"]:1 o2["A"]:1 o3["E"]:1 o4["F"]:1 o5["B"]:1 o6["F"]:1 L3["바깥 F 를 떼고 E 를 걷어 낸 뒤"]:6 r1["A"]:2 r2["F"]:2 r3["B"]:2
알맹이에 구분자와 같은 값이 많이 섞여 있을수록 선 위로 나가는 양이 늘어납니다.
선 하나를 건너가는 프레임은 경계를 1이 여러 개 이어진 비트 패턴으로 표시하기도 합니다. 그래서 알맹이 안에 그 패턴이 그대로 나오면 안 됩니다. 정해진 개수의 1이 이어지면 뒤에 0 하나를 억지로 끼워 넣습니다. 이것이 비트 스터핑입니다.
층마다 따로 긋는 경계
네트워크가 하는 일은 여러 계층으로 나눠 설명합니다. 계층은 일을 층층이 나눈 것입니다. 각 층은 자기 일만 합니다. 나머지는 아래층에 맡깁니다.
프레이밍은 그중 한 군데서만 하는 일이 아닙니다. 층마다 자기가 다루는 덩이의 경계를 따로 긋습니다.
| 어디서 | 무엇의 경계를 긋나 | 표시 방법 |
|---|---|---|
| 링크 계층 | 선 하나를 건너가는 프레임 | 앞뒤의 정해진 표시와 스터핑 |
| 연결 위의 응용 프로토콜 | 프로그램이 주고받는 메시지 | 길이 접두나 구분자 |
| 여러 흐름을 섞어 보내는 프로토콜 | 섞여 오는 조각 하나 | 조각마다 붙는 길이와 번호 |
층끼리 경계는 서로 포개지지 않습니다. 응용이 보낸 메시지 한 장이 아래층에서는 프레임 둘에 나뉘어 실리기도 합니다.
flowchart TD
subgraph 응용["응용 프로토콜"]
M["메시지 한 장"]
end
subgraph 연결["연결"]
S["바이트 스트림"]
end
subgraph 링크["링크 계층"]
F1["프레임 1"]
F2["프레임 2"]
end
M --> S
S --> F1
S --> F2
링크 계층은 선 하나로 이어진 옆 장비까지 데이터를 건네는 층입니다. 이 층의 프레이밍은 이더넷 같은 규격이 정해 둡니다. 받는 장비가 신호에서 프레임을 떼어 내지 못하면 그 위의 어느 층도 일을 시작할 수 없습니다.
응용 프로토콜은 사정이 다릅니다. 연결이 이미 바이트 스트림을 주므로 경계는 프로토콜이 스스로 정합니다.
HTTP(HyperText Transfer Protocol, 하이퍼텍스트 전송 프로토콜)는 본문 길이를 헤더에 적습니다. 이것이 길이 접두입니다. 빈 줄로 헤더의 끝을 알리는 것은 구분자입니다.
한 연결에 여러 흐름을 섞어 보내는 프로토콜은 프레이밍에 더 기댑니다. 다중화는 통로 하나에 여러 흐름을 섞어 보내는 일입니다. 섞어 놓은 것을 받는 쪽에서 다시 갈라내려면 조각마다 경계와 번호가 붙어 있어야 합니다.
그래서 이런 프로토콜은 주고받는 모든 것을 정해진 꼴의 작은 덩이로 쪼개 보냅니다. 웹소켓과 HTTP/2 가 그렇습니다.
어긋난 경계가 부르는 오류
양쪽이 경계를 다르게 세면 그 뒤가 전부 밀립니다. 한 덩이의 끝을 잘못 잡으면 다음 덩이의 시작도 틀리고, 그다음 덩이도 틀립니다. 덩이 하나를 버리고 이어 가는 것으로는 못 맞춥니다. 그래서 이런 오류는 대개 연결을 닫고 처음부터 다시 맞추는 것으로 수습합니다.
같은 데이터를 두 방식으로 끊을 수 있게 열어 두면 위험이 하나 더 생깁니다. 예를 들어 한 요청에 본문 길이를 알리는 정보가 두 군데에 적혀 오면, 어느 쪽을 볼지 중간에 선 프록시와 뒤쪽 서버가 갈립니다. 한쪽이 알맹이로 본 바이트를 다른 쪽은 새 덩이의 시작으로 읽습니다. 그래서 경계를 정하는 규칙은 하나만 남기고 나머지 해석은 거절하는 쪽으로 정해집니다.
이름이 같은 다른 쓰임
「프레이밍」은 기술 밖에서도 쓰는 말입니다. 같은 사실을 어떤 틀에 넣어 보여 주느냐에 따라 사람의 판단이 달라지는 현상을 심리학에서 프레이밍이라고 부릅니다. 사진과 영상에서는 화면 안에 무엇을 담고 무엇을 뺄지 정하는 일을 가리킵니다. 두 쓰임 모두 데이터의 경계를 긋는 이 일과는 이어지지 않습니다.
관련 항목
프레이밍이 경계를 긋는 데이터 단위
프레임 · 메시지 · 패킷 · 세그먼트 · 데이터그램 · 바이트 스트림 · 스트림 · 옥텟
프레이밍이 경계를 표시하는 방식
길이 접두 · 구분자 · 고정 길이 레코드 · 청크 전송 인코딩 · 바이트 스터핑 · 비트 스터핑 · 메시지 경계
프레이밍이 덩이에 덧붙이는 구성 요소
헤더 · 트레일러 · 페이로드 · 프리앰블 · 시작 구분자 · 체크섬
프레이밍과 앞뒤로 이어지는 처리 단계
캡슐화 · 역캡슐화 · 단편화 · 재조립 · 직렬화 · 다중화 · 버퍼
프레이밍 규칙을 스스로 정하는 프로토콜
이더넷 · TCP · UDP · HTTP · HTTP-2 · 웹소켓 · QUIC · gRPC
프레이밍을 맡는 계층과 규격
링크 계층 · 데이터 링크 계층 · 전송 계층 · OSI 7계층 · 프로토콜 · MTU
프레이밍이 어긋났을 때 나는 오류
프레이밍 오류 · 요청 밀반입 · 짧은 읽기 · 헤드 오브 라인 블로킹 · 버퍼 오버플로
다른 이름: framing