FileSharer 개발 노트 3 — HTTP 요청이 한 번에 도착하지 않을 때

English version · 2편: 공유 만료·PIN·다운로드 횟수

FileSharer 개발 노트 · 03 / 10

여러 조각으로 도착한 요청을 세 개발자가 하나의 메시지로 모읍니다.
AI 일러스트 · 여러 조각으로 도착한 요청을 세 개발자가 하나의 메시지로 모읍니다.

같은 PC에서는 열리던 다운로드 페이지가 중계 경로를 통하면 간헐적으로 실패한다면 무엇부터 봐야 할까요? 주소나 인증만 확인하다 보면, 서버가 요청을 끝까지 읽었는지를 놓치기 쉽습니다. FileSharer 소스에는 이 문제를 다루는 작은 함수 ReadRequestHead가 있습니다.

원본 ReadRequestHead와 GetHeader를 루프백 소켓에서 실행한 결과를 정리한 리포트입니다. 운영 서버 로그나 실제 사용자 요청 캡처가 아닙니다.
원본 ReadRequestHead와 GetHeader를 루프백 소켓에서 실행한 결과를 정리한 리포트입니다. 운영 서버 로그나 실제 사용자 요청 캡처가 아닙니다.

이 함수는 받은 바이트를 버퍼에 모으고 HTTP 헤더 끝인 \r\n\r\n을 찾습니다. 코드의 핵심은 반복문 자체보다 입력을 읽은 횟수와 메시지의 완성 여부를 분리한 것입니다.

Read의 반환값은 요청 한 개의 길이가 아니다

NetworkStream.Read에 큰 버퍼를 넘겨도 그 크기만큼 반드시 채워 주지는 않습니다. 현재 읽을 수 있는 범위에서 요청한 수 이하의 바이트를 반환할 수 있습니다. 연결이 정상 종료된 경우에는 0을 반환할 수 있습니다. 이 동작은 Microsoft의 NetworkStream.Read 문서에서도 확인할 수 있습니다.

예를 들어 첫 번째 읽기에서 다음 문자열만 받을 수 있습니다.

GET /d/demo0001 HTTP/1.1\r\nHo

Host라는 헤더 이름조차 아직 끝나지 않았습니다. 이 시점에 파싱을 끝내면 뒤에 올 헤더를 놓칩니다. 송신 프로그램이 한 번에 썼는지 여부도 수신 측 Read 한 번의 결과를 보장해 주지 않습니다.

헤더 종료 표시가 버퍼 경계에 걸릴 수 있다

원본 함수는 16,384바이트 버퍼를 사용합니다. 새 데이터를 읽을 때 이전 바이트와 이어지는 구간까지 확인합니다.

int scanFrom = Math.Max(3, filled);
filled += n;

검색 위치에서 직전 세 바이트를 함께 보므로 \r\n\r까지 먼저 도착하고 마지막 \n만 나중에 도착한 경우도 찾을 수 있습니다. 새로 받은 조각 안에서만 네 글자를 검색하면 이 경계를 놓치게 됩니다.

이 코드는 문자열을 조각마다 이어 붙이기보다 바이트를 먼저 모은 뒤 변환합니다. HTTP 종료 표시를 탐지하는 작업과 텍스트를 해석하는 작업의 순서를 분리한 셈입니다.

루프백으로 쪼개 보낸 결과

원본 서버 전체를 실행하지 않고 해당 메서드를 독립 C# 실행 파일에 넣었습니다. 수신 소켓은 127.0.0.1의 임시 포트에만 열고, 예시 요청을 네 번으로 나누어 보냈습니다. 마지막 두 조각에서는 헤더 종료 표시도 분리했습니다.

확인 항목 결과
조각을 합친 뒤 헤더 종료 표시 확인 true
User-Agent 값 추출 SeriesLab
헤더를 완성하지 않고 송신 종료 미완성 문자열 반환

첫 두 결과는 정상적인 분할 수신 경로가 작동함을 보여 줍니다. 세 번째는 중요한 제한입니다. 원본 함수는 EOF, 읽기 예외, 버퍼 소진으로 반복문이 끝났을 때도 이미 받은 문자열이 있으면 반환합니다. 반환값이 비어 있지 않다는 사실만으로 완전한 헤더라고 판단해서는 안 됩니다.

크기 제한과 시간 제한도 서로 다르다

호출하는 ProcessClient는 스트림의 ReadTimeout을 15,000밀리초로 설정합니다. 함수의 버퍼 크기는 16KiB입니다. 이 둘은 각각 읽기 대기와 보관할 수 있는 입력량을 제한하지만, 전체 헤더를 15초 안에 완성해야 한다는 절대 마감 시각을 구현한 것은 아닙니다.

운영용 파서로 강화한다면 완료, EOF, 시간 초과, 크기 초과를 다른 결과로 돌려주고, 헤더가 완성된 경우에만 다음 단계로 넘어가도록 설계할 수 있습니다. 전체 수신 시작 시각을 기준으로 한 마감 시간도 별도로 둘 수 있습니다. 이 부분은 개선 제안이며 이번 확인에서 추가 구현하지 않았습니다.

문제를 재현할 입력을 먼저 만든다

네트워크 오류를 조사할 때 외부 중계 서비스를 계속 재연결하기보다, 수신 코드를 흔드는 작은 입력을 만드는 편이 빠를 때가 있습니다. 헤더 이름 중간, 줄바꿈 중간, 헤더 종료 표시 중간에서 나누어 보내 보세요. 각 경우에 정상 메시지를 복원하는지와 불완전한 메시지를 거절하는지는 따로 확인해야 합니다.

이번 시험이 확인한 것은 이 두 메서드의 로컬 동작입니다. HTTP 서버 전체의 규격 적합성이나 외부 터널의 안정성까지 검증한 결과는 아닙니다.

소스 근거
FileSharer.cs:4625–4666 ProcessClient · FileSharer.cs:4998–5044 ReadRequestHead / GetHeader
원본 프로젝트 파일은 수정하지 않았습니다. 본문의 개선안은 적용된 기능과 구분합니다.

댓글