소켓 응용 2/5 · FLOW

큰 파일은
조금씩 보내야 합니다.

빠른 송신자가 느린 수신자에게 계속 밀어 넣으면 버퍼가 쌓입니다. 청크, backpressure, 체크섬과 이어받기를 눈으로 확인합니다.

이 편의 핵심 질문수신자가 느릴 때 송신자는 무엇을 해야 할까?버퍼가 차면 멈추고
비면 다시 보내기

나쁜 결과와 안전한 결과를 비교하세요

느린 수신자에게 큰 파일을 보냅니다

보는 순서제어 없음 선택버퍼 관찰체크섬 비교

  1. 1분할
  2. 2밀어 넣기
  3. 3과부하
  4. 4검증
실험 시작을 누르면 첫 장면부터 움직입니다1 / 8 전송
file.bin8개 청크
전송 버퍼12%WRITE · 계속 전송
수신 디스크1 / 8 저장
1저장2남음3남음4남음5남음6남음7남음8남음

전체 체크섬전송 뒤 확인

계속 밀어 넣기 시나리오입니다. 실험 시작을 눌러 네 장면을 따라가세요.

시작 전

화면에서 코드로 옮길 때

이 편의 원리 세 가지

01경계는 직접 만듭니다.

TCP 바이트 스트림 위에 길이, 청크 번호와 전송 ID를 붙입니다.

02느리면 잠시 멈춥니다.

쓰기 버퍼가 차면 drain 같은 신호까지 생산을 멈춥니다.

03완료는 검증 뒤에 확정합니다.

청크 개수만 보지 않고 전체 길이와 체크섬을 확인합니다.

구현 체크리스트

최소 네 조각으로 시작하세요.

데모의 시각 요소를 실제 프로토콜 필드와 런타임 동작으로 옮기면 됩니다.

  1. 1프레임 헤더transferId, chunkNo, length와 전체 크기
  2. 2상한이 있는 버퍼메모리 사용을 제한하는 high-water mark
  3. 3체크포인트수신자가 디스크에 확정한 마지막 청크
  4. 4무결성 검사완성된 파일의 SHA-256 같은 체크섬
이 데모는 네트워크 동작을 단계별로 보여주는 개념 시뮬레이션입니다.

브라우저 데모는 개념 시뮬레이션입니다. 실제 서버 소켓에서는 런타임의 write 반환값과 drain·await drain을 사용하세요.

이 데모에 대하여

느린 수신자에게 계속 밀어 넣으면 어디가 쌓이는지 봅니다

이 화면의 중심은 버퍼 막대 하나입니다. 보내는 쪽이 빠르고 받는 쪽이 느릴 때 그 차이가 어디에 쌓이는지를 눈으로 보여주고, 제어를 켜면 그 막대가 어떻게 다르게 움직이는지 비교합니다. 보낸 양과 받은 양을 따로 세어 두었기 때문에, 보냈다는 숫자가 늘어나는 동안 받은 숫자는 그만큼 따라오지 않는다는 점을 같은 화면에서 확인할 수 있습니다.

  1. 제어 없는 설계를 고르고 실행하며 버퍼 막대를 지켜봅니다.
  2. 보낸 양과 받은 양의 숫자가 벌어지는 것을 확인합니다.
  3. 버퍼가 한계까지 차오르는 장면에서 판정 색이 바뀌는지 봅니다.
  4. 제어를 켠 설계로 바꿔 같은 조건으로 실행합니다.
  5. 일시 정지 표시가 켜지는 시점과 버퍼 높이를 함께 봅니다.
  6. 중간에 연결이 끊기는 장면에서 어디까지 확정됐는지 확인합니다.
  7. 마지막 검증 단계에서 통과와 실패가 어떻게 갈리는지 봅니다.
  8. 두 설계에서 최종 결과가 같은지 다른지 정리합니다.
  9. 버퍼 막대가 가장 높이 올라간 지점을 두 설계에서 비교합니다.
  10. 일시 정지가 몇 번 일어나는지 세어 봅니다.
  11. 받은 개수가 끊김 전후로 유지되는지 확인합니다.

이 화면이 상태로 들고 있는 값

01

보낸 양과 받은 양을 따로 셉니다.

화면은 두 숫자를 각각 표시합니다. 보내는 쪽 코드에서 전송 함수가 성공을 돌려줬다고 해서 상대가 그만큼 받아 처리한 것이 아니기 때문입니다. 그 사이의 차이가 어딘가에 머물러 있고, 그 머무는 곳이 메모리입니다. 두 숫자를 합쳐 진행률 하나로만 보여줬다면 이 문제가 화면에서 사라졌을 것입니다.

02

버퍼는 무한하지 않습니다.

쌓이는 양에 상한을 두고 그 비율을 막대로 표시합니다. 상한이 없다고 가정하면 메모리가 계속 늘다가 프로그램이 죽는데, 실제로 큰 파일을 다룰 때 겪는 대표적인 사고가 이것입니다. 화면에서 막대가 가득 차는 장면에 위험 판정을 붙여 둔 이유이고, 이 지점이 곧 생산을 멈춰야 하는 시점입니다.

03

멈추고 다시 시작하는 것이 제어입니다.

제어를 켠 설계에서는 버퍼가 일정 수준에 도달하면 보내는 쪽이 잠시 멈추고, 비워지면 다시 보냅니다. 속도를 미리 정해 두고 맞추는 것이 아니라 받는 쪽의 상태를 보고 그때그때 조절하는 방식입니다. 화면에서 일시 정지 표시가 켜졌다 꺼졌다 반복하는 것이 정상 동작이며, 그 리듬 자체가 이 설계의 모습입니다.

04

조각에 번호가 있어야 이어받습니다.

연결이 끊겼을 때 처음부터 다시 보내지 않으려면 어디까지 확정됐는지 알아야 합니다. 그래서 조각마다 번호를 붙이고 받는 쪽이 확정한 마지막 번호를 기억합니다. 화면이 끊김 장면에서 받은 개수를 그대로 유지하는 것은 그 지점부터 이어서 받을 수 있다는 표현입니다.

05

개수만으로 완료를 판단하지 않습니다.

마지막에 검증 단계를 따로 둔 이유는, 조각 수가 맞아도 내용이 온전하다는 보장이 없기 때문입니다. 전체 길이와 내용 요약값을 함께 확인해야 파일이 제대로 도착했다고 말할 수 있습니다. 화면에서 검증 결과를 통과와 실패로 나눠 표시하는 것이 이 단계를 생략하면 안 된다는 표시입니다.

06

경계는 직접 만들어야 합니다.

바이트가 흐르는 통로에는 여기까지가 한 조각이라는 표시가 없습니다. 보낸 쪽이 나눠 보낸 단위와 받는 쪽이 한 번에 읽어 오는 단위가 일치하지 않으므로, 길이나 구분자를 직접 붙여야 조각을 다시 나눌 수 있습니다. 화면에서 조각 단위로 세는 것이 가능한 이유도 그런 표시를 붙였다고 가정하기 때문입니다.

07

두 설계의 결말이 같지 않습니다.

제어가 없는 쪽은 빠르게 끝나는 것처럼 보이다가 문제를 만들고, 제어가 있는 쪽은 멈췄다 가기를 반복해 느려 보이지만 끝까지 갑니다. 화면에서 두 설계의 마지막 판정을 비교하면 속도와 완주가 다른 목표라는 점이 드러납니다.

08

메모리는 조용히 늘어납니다.

버퍼가 차오르는 동안 프로그램은 오류를 내지 않고 정상적으로 동작합니다. 그래서 개발 중 작은 파일로 시험할 때는 아무 문제가 보이지 않다가, 운영에서 큰 파일이나 느린 회선을 만났을 때 갑자기 죽습니다. 화면이 버퍼 막대를 항상 띄워 두는 것은 평소에 보이지 않는 값을 눈에 보이게 만들려는 것이고, 실제 코드에서도 이 값을 지표로 내보내 두는 편이 좋습니다.

09

받는 쪽 속도는 통제할 수 없습니다.

받는 쪽이 느린 이유는 디스크가 바쁘거나 CPU 가 부족하거나 그 프로그램이 다른 일을 하고 있어서일 수 있고, 보내는 쪽에서는 어느 쪽인지 알 수 없습니다. 그래서 원인을 알아내려 하지 않고 상태만 보고 반응하는 구조를 씁니다. 상대가 왜 느린지 몰라도 옳게 동작한다는 점이 이 방식이 오래 쓰이는 이유입니다.

브라우저 안의 개념 시뮬레이션입니다.

이 화면은 실제로 파일을 읽거나 보내지 않으며 버퍼 크기와 속도 차이는 눈으로 보기 좋게 정한 값입니다. 실제 서버 코드에서는 전송 함수가 돌려주는 값과 버퍼가 비었다는 신호를 직접 확인해 생산을 멈춰야 하고, 그 방법은 쓰는 런타임마다 다릅니다. 또한 여기서는 조각이 순서대로 도착한다고 가정하지만 실제로는 재조립과 순서 보장의 책임이 어느 계층에 있는지 먼저 정해야 합니다. 검증에 쓰는 요약값의 계산 비용, 큰 파일에서 그 계산을 나눠 하는 방법, 이어받기를 위해 중간 상태를 어디에 저장할지, 여러 사람이 동시에 전송할 때의 자원 분배는 모두 다루지 않습니다. 파일 전송을 실제로 구현할 때는 직접 만들기보다 검증된 프로토콜이나 라이브러리를 쓰는 편이 나은 경우가 많습니다. 이 화면의 목적은 직접 만들기 위한 설계도가 아니라, 이미 있는 도구가 무엇을 대신해 주고 있는지 알아보게 하는 데 있습니다.

참고 자료Node.js — 스트림과 backpressureRFC 9293 — Transport Control Protocol (흐름 제어)

실험을 마쳤다면