Krafton Jungle Eighth 1

모기·2025년 5월 2일

Study.Jungle

목록 보기
10/12
public string DevelopmentDiary(Knowledge knowledge) {
	if (knowledge != null) {
		return "level up"
    } else {
    	return "level down"
    }
}

우리 반에서 유행한지 좀 오래된 게임이다.
나는 어제 시작했다.
반 동기에 의하면, 남들 따라하는 거는 강남병이라고 한단다.
내가 그 강남병이다.
근데 이제 유행 다 지나가고 따라하는..

꽤나 재밌다.

우리 반에서 이 게임을 제일 잘하는 사람은 '천마'라는 칭호를 달고 있다.
나를 제자로 받아들여준다고 했다.
나는 이제 소교주다.
천마는 9, 8, 7을 최우선 순위로 탐색하라고 했다.

최근에 시작한 또 다른 반 동기는, 어제 '문지기'의 칭호를 받았다.
옆에서 다른 동기가 '문 안으로는 못 들어오고 문만 지킨다'고 했다.
냉혹한 세계다.

이번 주차에 '무림맹주'가 같은 조원이 됐다.
무림맹주가 제자를 권유했지만, 나는 이미 스승이 있다고 거절했다.
그가 계속 제안했고, 결국 나는 스승이 천마라고 얘기할 수 밖에 없었다. 그는 바로 납득했다.

무림맹주는 BFS로 탐색하라고 했다.

잠 깰 때 좋은 것 같다.

CS

시스템 수준 입출력

1) Unix I/O

리눅스에서 파일은 연속된 m개의 바이트다. 그리고 모든 I/O(네트워크, 디스크, 터미널)은 '파일'로 추상화된다. 모든 입력과 출력은 해당 장치에 대응하는 파일을 읽고 쓰는 방식으로 이루어진다. 이로 인해 커널은 단순하고 일관된 저수준 인터페이스*를 제공할 수 있다.

* 저수준 인터페이스

  • 운영체제 커널이 제공하는 기본적인 시스템 호출 수준의 API
  • 시스템의 구체적인 동작, 하드웨어나 운영체제에 가까운 부분과 직접적으로 상호작용하는 인터페이스
  • 운영체제에서 제공하는 시스템 콜, 파일 디스크럽터를 직접 다루는 함수 등
  • C에서는 read(), write(), open(), close() 같은 함수들이 해당됨.
  • 파일이든, 터미널이든, 소켓이든 모두 동일한 방식으로 처리할 수 있음.
  • 파일 디스크립터
    파일을 열면 커널은 파일 디스크립터라는 작은 음이 아닌 정수를 반환함. 이 디스크립터는 이후 파일에 대한 모든 작업에서 사용됨. 커널은 파일에 대한 모든 정보를 추적하며 애플리케이션은 디스크럽터만 관리함.

이 다음 설명이 조금 이해하기 어려웠는데,

  • 기본 파일 디스크럽터
    모든 프로세스는 기본적으로 세 개의 열린 파일을 가지고 시작한다.
    • 표준 입력(0), 표준 출력(1), 표준 에러(2)

뭔가 '입/출력/에러'라고 했을 때 방식을 얘기하는 것 같은데 파일이라고 해서 당황스러웠다.
GPT 역시 같은 소리를 반복했다.

이 설명을 읽고 그렇구나! 한 사람도 있겠지만
나는 아니기 때문에 좀 더 들어갔다.

응~ 아직 모르겠어

이 설명을 듣고 살짝 감이 잡히긴 했다.
그리고 이해한 내용을 교차 검증하기 위해 다른 AI한테 물어봤다.

프로세스가 시작할 때
입력/출력/에러 디스크럽터 파일이 각각 열리고
각 디스크럽터 파일은 어떤 장치에 연결이 돼 있는데,
입력 디스크럽터 파일에, A라는 내용을 쓰면, 어떤 장치에 A라는 내용이 써진다
따라서 사용자는 디스크럽터 파일을 사용하기만 하면
어떤 장치에 연결되어 있든지 그 장치를 활용할 수 있다.

내가 이해한 내용이 맞아?

이게 내가 한 질문이고

당신이 이해한 내용은 **대체로 맞지만, 일부 오해가 있습니다**.  
아래에서 정확하게 정리합니다.

## 프로세스와 파일 디스크립터의 관계

- 유닉스/리눅스에서 **프로세스가 시작될 때** 기본적으로 3개의 파일 디스크립터가 열립니다:
  - 0: 표준 입력(stdin) - 보통 키보드
  - 1: 표준 출력(stdout) - 보통 모니터
  - 2: 표준 에러(stderr) - 보통 모니터[3][4][5][6].

- 이 디스크립터들은 각각 **특정 장치**(예: 키보드, 모니터)에 연결되어 있습니다.  
  즉, 프로세스는 파일 디스크립터를 통해 실제 장치나 파일에 접근합니다.

## 디스크립터에 쓰기/읽기의 의미

- **입력 디스크립터(0번, stdin)**는 일반적으로 "읽기" 용도로 사용됩니다.  
  즉, 사용자가 키보드로 입력한 내용을 프로그램이 읽을 때 사용합니다.
- 만약 **입력 디스크립터에 "쓴다"**면, 실제로는 프로그램이 입력 장치(예: 키보드)에 출력을 시도하는 셈인데,  
  대부분의 시스템에서는 stdin에 쓰기를 시도해도 아무 효과가 없거나 오류가 발생합니다.
- 반대로, **출력 디스크립터(1번, stdout)**에 쓰면, 그 내용이 모니터(터미널)에 출력됩니다[3][4][5][6].

## "어떤 장치에 연결되어 있든 활용 가능"의 의미

- 맞습니다.  
  파일 디스크립터가 실제로 어떤 장치(키보드, 파일, 소켓 등)에 연결되어 있든,  
  프로세스는 **디스크립터 번호만 알면 read()/write() 같은 함수로 데이터를 주고받을 수 있습니다**.
- 예를 들어, 표준 입력이 파일이나 파이프로 리디렉션되면, 프로그램은 여전히 0번 디스크립터에서 데이터를 읽지만,  
  실제 데이터는 키보드가 아니라 파일이나 파이프에서 들어옵니다[3][4][5][6].

## 정리

- **표준 입력(0번)은 읽기, 표준 출력(1번)과 표준 에러(2번)는 쓰기 용도**가 일반적입니다.
- 파일 디스크립터는 실제로 어떤 장치나 파일에 연결되어 있든,  
  사용자는 디스크립터만 사용해서 데이터를 읽고 쓸 수 있습니다.
- 그래서 "어떤 장치에 연결되어 있든 디스크립터 파일만 사용하면 장치를 활용할 수 있다"는 이해는 맞습니다.
- 단, **입력 디스크립터에 쓰는 것**은 일반적으로 의미가 없거나 오류입니다;  
  읽기는 입력 디스크립터, 쓰기는 출력/에러 디스크립터에 하는 것이 일반적입니다[3][4][5][6].

---

**요약:**  
대부분의 이해는 정확합니다.  
단, "입력 디스크립터에 쓰면 장치에 내용이 써진다"는 부분은 실제로는 그렇지 않습니다.  
입력 디스크립터(0번)는 주로 읽기 용도이며, 쓰기 연산은 일반적으로 허용되지 않습니다.  
나머지, "디스크립터를 통해 어떤 장치든 활용 가능하다"는 점은 맞는 설명입니다.

---
Perplexity로부터의 답변: pplx.ai/share

이게 AI의 답변이다.
그럼 아마 입/출력/에러가 연결하는 파일/장치들에는 다양한 것이 있을 것이다.

표준 입력(stdin, 0번 디스크립터)
1. 터미널 키보드 - 사용자가 직접 키보드로 입력하는 경우(기본값).
2. 파일 - 예: command < input.txt → input.txt 파일이 입력 스트림이 됨.
3. 파이프 - 예: echo "hello" | command → 앞 명령의 출력이 다음 명령의 입력이 됨.
4. 네트워크 소켓/장치 - 네트워크 프로그램에서 표준 입력을 소켓에 연결할 수 있음(고급 예시).

표준 출력(stdout, 1번 디스크립터)
1. 터미널 모니터(콘솔 화면) - 기본적으로 명령 결과가 화면에 출력됨.
2. 파일 - 예: command > output.txt → 명령의 출력이 output.txt 파일에 저장됨.
3. 파이프 - 예: command1 | command2 → command1의 출력이 command2의 입력이 됨.
4. /dev/null(널 장치) - 예: command > /dev/null → 출력이 버려짐, 아무 곳에도 기록되지 않음.
5. 네트워크 소켓/장치 - 네트워크 프로그램에서 표준 출력을 소켓에 연결할 수 있음(고급 예시).

표준 에러(stderr, 2번 디스크립터)
1. 터미널 모니터(콘솔 화면)- 오류 메시지가 화면에 출력됨(기본값).
2. 파일 - 예: command 2> error.txt → 오류 메시지가 error.txt 파일에 저장됨.
3. /dev/null(널 장치) - 예: command 2> /dev/null → 오류 메시지가 모두 버려짐.
4. 파이프/다른 파일 - 예: command 2>&1 → 표준 에러를 표준 출력과 같은 곳으로 보냄.
프로세스 이름표준 입력(0)표준 출력(1)표준 에러(2)
A키보드모니터모니터
B파일네트워크 소켓네트워크 소켓
C네트워크 소켓모니터모니터
D파이프파이프파이프

이런 느낌이다.
즉, 표준 입력에 있는 장치들을 파일처럼 여겨서, read()라는 함수나 시스템 콜 하나만으로 읽을 수 있다는 얘기다.
표준 출력이나 표준 에러도 관련 함수/시스템 콜로 통일성 있게 동작할 것이다.

이제 남은 부분들을 보자.

  • 파일 열기와 닫기
    파일을 열 때는 'open' 시스템 콜을 사용하고, 파일 작업이 끝나면 'close'로 닫는다. 닫힌 디스크럽터를 다시 닫으려하면 에러가 발생한다.

  • 파일 위치
    커널은 각 열린 파일마다 현재 파일 위치를 관리한다. 읽기/쓰기 작업 시 이 위치를 기준으로 데이터가 이동하며 'seek' 연산을 통해 위치를 변경할 수 있다.

  • 읽기와 쓰기

    • read는 파일의 현재 위치에서 n바이트를 메모리로 복사하고, 파일 위치를 n만큼 증가시킨다.
    • write는 메모리에서 n바이트를 파일의 현재 위치에 복사하고, 파일 위치를 n만큼 증가시킨다.
    • 파일 끝(EOF)에 도달하면 read는 0을 반환한다. EOF는 특별한 문자가 있는 것이 아니고 데이터가 없을 때 반환값으로 구분한다.

2) 파일

  • 리눅스 파일의 타입

    • 일반 파일: 임의의 데이터를 저장하는 파일. 텍스트 파일(ASCII/유니코드)과 바이너리 파일로 구분할 있음. 커널 입장에서는 차이가 없음.
    • 디렉터리: 파일 이름과 파일(또는 다른 디렉터리) 간의 매핑을 저장하는 특수 파일. 각 디렉터리는 최소한 자기 자신(.)과 부모 디렉터리(..)에 대한 링크를 포함함.
    • 소켓: 네트워크 통신을 위한 파일. 다른 프로세스와의 통신에 사용됨
  • 디렉터리 구조와 경로명

    • 모든 파일을 루트(/) 디렉터리 아래에 두는 단일 디렉터리 계층 구조를 사용함.
    • 파일의 위치는 경로명(pathname)으로 지정하며, 절대 경로(루트부터)와 상대 경로(현재 작업 디렉토리 기준)이 있음.
  • 디렉터리의 역할

    • 파일 이름과 파일 간의 매핑을 저장하는 파일
    • 각 엔트리는 파일 이름과 해당 파일의 incode를 포함함.

3) 파일 열기와 닫기

4) 파일 읽기와 쓰기

5) RIO 패키지를 이용한 안정적인 읽기와 쓰기

Robust IO 패키지의 필요성

  • 저수준 I/O 함수인 read와 write는 요청한 바이트 수만큼 항상 데이터를 전송하지 않음.
  • 네트워크 소켓, 파이프, 터미널 등에서 short count 현상이 자주 발생함.
  • 위 문제를 해결하기 위해 개발자가 직접 반복적으로 read/write를 호출해서 모든 데이터를 전송해야 함.

RIO 패키지의 목적

  • 네트워크 프로그래밍 등 short count가 빈번한 환경에서 신뢰성 있는 데이터 입출력을 보장함.

Unbuffered 함수 (rio_readn, rio_writen)

  • 메모리와 파일 간에 직접 데이터를 전송(버퍼링 없음)
  • 네트워크와 같은 바이너리 데이터의 입출력에 적합하며 short count를 자동으로 처리함.

Buffered 함수 (rio_readlineb, rio_readnb)

  • 내부 애플리케이션 버퍼를 사용하여 효율적으로 텍스트 라인이나 바이너리 데이터를 읽음.
  • 표준 I/O 버퍼링와 유사하지만, thread-safe하며, 텍스트 라인과 바이너리 데이터를 자유롭게 섞어 읽을 수 있음.

5-1) RIO 버퍼 없는 입력 및 출력 함수

애플리케이션이 메모리와 파일 사이에 직접 데이터를 전송할 때 사용하는 함수.
이 함수들은 short count를 robust하게 처리함.

주요 함수

  • rioreadn(int fd, void* usrbuf, size_t n)
    • 파일 디스크립터 fd에서 최대 n바이트를 usrbuf로 읽어옴.
    • 반환값: 실제 읽은 바이트 수(0이상), EOF에 도달하면 0, 에러 시 -1
    • short count는 EOF에서만 발생함.
  • riowriten(int fd, void* usrbuf, size_t n)
    • usrbuf에서 n바이트를 fd로 쓴다.
    • 반환값: 실제 쓴 바이트 수(항상 n 또는 에러 시 -1)
    • short count가 발생하지 않도록 모든 바이트를 쓸 때까지 반복함.
  • 함수의 특징
    • 내부적으로 read/write가 시그널 핸들러에 의해 인터럽트될 경우 자동으로 재시도함.
    • rioreadn과 riowriten은 같은 디스크립터에 대해 자유롭게 섞어서 호출할 수 있음.

또 어려운 말 섞어가면서 잘난 척한다.
다시 분석해보자.

우선 usrbuf가 뭔지 모르겠다.
usrbuf란?

  • usrbuf는 Robust I/O(Rio) 패키지에서 사용하는 함수들(예: rioreadn, riowriten, rioreadlineb, rioreadnb)에서 데이터를 읽거나 쓸 때 실제로 사용자(프로그램)가 데이터를 저장하거나 전달하는 메모리 버퍼를 의미함.
  • 파일 디스크립터에서 읽은 데이터가 복사되어 저장되는 곳, 혹은 파일 디스크립터로 쓸 데이터를 담고 있는 곳이 usrbuf다.

그렇다면 메모리 버퍼란 무엇일까?
메모리 버퍼란?

  • 데이터를 임시로 저장하는 메모리 공간
  • 데이터를 한 곳에서 다른 곳으로 전송할 때, 속도가 다른 두 장치 사이에서 데이터의 효율적 전달과 손실 방지를 위해 사용됨

메모리의 어떤 공간을 사용하는 걸까?

그렇다고 한다.

근데 버퍼가 없다고 했는데 왜 usrbuf이 있는 걸까?
물어보니, '버퍼가 있다/없다'는 차이가 아니고 '버퍼링이 어디에 어떻게 적용되느냐'의 차이라고 한다.

https://velog.io/@mogiyoon/web-server-고찰#client
여기서 확인해보면

unbuffered 함수는 rio 버퍼를 쓰지 않고 사용자가 만든 버퍼를 쓰며
buffered 함수는 rio 버퍼를 쓴다.

왜 rio 버퍼를 사용할까? 라는 질문은 아래 5-2를 마저 읽고
생각하도록 하자.

5-2) RIO 버퍼를 통한 입력 함수

파일에서 한 줄씩 읽거나 많은 데이터를 효율적으로 읽고 싶을 때, 1바이트씩 read를 호출하면 매우 비효율적임. Rio 패키지의 함수들은 내부 애플리케이션 버퍼를 사용해 효율적으로 텍스트 라인이나 바이너리 데이터를 읽을 수 있도록 함.

주요 함수

  • rioreadinitb(riot* rp, int fd)
    • 파일 디스크립터 fd와 내부 버퍼(riot 구조체)를 연결해 초기화함
  • rioreadlineb(riot* rp, void* usrbuf, size_t maxlen)
    • 파일에서 한 줄(개행 문자까지)을 읽어 usrbuf로 복사하고 널 문자로 종료함
    • 최대 maxlen-1 바이트까지 읽으며, 초과 시 잘라서 반환함
    • 반환값: 읽은 바이트 수(널 문자 제외), EOF면 0, 에러면 -1
  • rioreadnb(riot* rp, void* usrbuf, size_t n)
    • 내부 버퍼에서 최대 n바이트를 usrbuf로 복사함
    • 반환값: 실제로 읽은 바이트 수, EOF면 0, 에러면 -1

사용 방식

  • rioreadinitb는 열린 디스크립터마다 한 번만 호출함
  • rioreadlineb와 rioreadnb는 같은 디스크립터와 버퍼에서 자유롭게 섞어서 호출함
  • buffered 함수와 unbuffered 함수는 같은 디스크립터에서 섞어 쓰면 안 됨

특징

  • 내부 버퍼를 사용해 커널과의 read 호출 횟수를 최소화함
  • 텍스트 라인 읽기와 바이너리 데이터 읽기를 모두 지원함
  • thread-safe*하며 네트워크 소켓 등 다양한 환경에서 안전하게 사용함
* thread-safe: 공유 자원에 여러 스레드가 동시에 접근해도, 동기화 문제 없이 안전하게 동작함.

왜 usrbuf 버퍼가 있는데, rio 버퍼를 또 쓰는 것일까?

오랜 시간 고민하다가 그런 생각이 들었다.
writen에서는 usrbuf을 바로 쓰고, read에서는 rio를 쓴다.
어떻게 보면 write는 내가 가지고 있는 데이터를 보내는 것이기 때문에 중간에 끊기더라도 연속해서 보낼 수 있다.

하지만 read를 통해 읽는 데이터는 한 번에 들어올 수도, 아닐 수도 있다.
따라서 개행문자 '\n'가 나타날 때까지 글자가 들어오는 족족 read해서 rio 버퍼에 담아둔다.
그러다 개행문자가 나타나면 이제 유저 버퍼로 옮기고, 바로 출력하는 것이다.

그렇다면 유저버퍼에다 담으면서 개행문자가 나타나면 출력하면 되지않을까?
라는 의문도 든다.

그래서 rio 버퍼를 따로 두는 것이다.
사실 rio 버퍼 역할을 대체할 수 있으면
커스텀 버퍼를 활용할 수도 있다.

6) 파일 메타데이터 읽기

7) 디렉토리 내용 읽기

8) 파일 공유

9) I/O 재지정

10) 표준 I/O

11) 어떤 I/O 함수를 사용해야 하는가?

네트워크 프로그래밍

1) 클라이언트-서버 프로그래밍 모델

해당 모델에서 애플리케이션은 서버 프로세스와 하나 이상의 클라이언트 프로세스로 구성됨.
서버는 특정 자원을 관리하며, 이를 조작해서 클라이언트에게 서비스를 제공함.

  • 클라이언트-서버 트랜잭션* 4단계
    • 클라이언트가 요청을 보냄: 클라이언트가 서비스가 필요하면 서버에 요청을 보냄.
    • 서버가 요청을 처리: 서버는 요청을 받아 해석하고 자원을 적절히 조작함.
    • 서버가 응답을 보냄: 서버는 응답을 클라이언트에 보내고 다음 요청을 기다림.
    • 클라이언트가 응답을 처리: 클라이언트는 응답을 받아서 처리함.
* 트랜잭션
- 데이터 저장과 I/O 연산의 일관성과 안정성을 보장하기 위한 처리 단위
- 여러 개의 연산을 하나의 논리적 단위로 묶어서 처리하며, 모두 성공하거나 전혀 아무것도 수행도지 않은 것처럼 해야함.

클라이언트와 서버는 '프로세스' 단위로 동작하며 실제로 같은 컴퓨터 안에서 여러 개가 동시에 실행될 수도 있음.
해당 챕터의 트랜잭션은 데이터베이스의 트랜잭션과 달리 원자성 등의 특성을 가지지 않으며, 단순히 클라이언트와 서버가 주고받는 일련의 절차를 의미함.

2) 네트워크

  • 네트워크를 시스템 IO 장치로 이해하기

    • 네트워크는 호스트 입장에서 보면 데이터의 입출력을 담당하는 IO 장치처러 동작함
    • 네트워크 어댑터는 IO 버스에 연결되어 호스트와 네트워크 간의 물리적 인터페이스 역할을 함
    • 데이터는 네트워크에서 어댑터를 통해 시스템 메모리로 복사되고, 반대로 시스템 메모리에서 네트워크로도 복사됨. 이 과정은 일반적으로 DMA를 통해 이루어짐
  • LAN과 이더넷

    • 네트워크는 지리적 범위에 따라 계층적으로 구성됨
    • LAN(근거리 통신망)
      • 이더넷
        트위스트 페어 케이블과 허브로 구성되며, 허브는 각 포트로 들어온 비트를 모든 포트로 복사해 전송함. 모든 호스트는 모든 비트를 수신하지만, 목적지 주소가 일치하는 호스트만 실제로 데이터를 읽음.

  • 이더넷 어댑터
    네트워크 장치와 컴퓨터 사이에서 이더넷 프레임을 처리하는 하드웨어

    송신수신
    데이터를 프레임으로 패킹프레임을 분해해서 데이터 추출
    프레임을 전기 신호로 변환해 전송전기 신호를 수신해 프레임으로 재구성
    출발지 MAC 주소* 붙임목적지 MAC 주소가 자기 것인지 확인
* MAC 주소: 네트워크 인터페이스(LAN 카드, WiFi 카드 등)에 할당된 고유한 식별자
  • 이더넷 프레임
    이더넷 네트워크에서 데이터를 전송할 때 사용하는 기본 단위
    헤더 + 페이로드(데이터) + 에필로그 구조
    [목적지 MAC][출발지 MAC][타입][데이터][CRC]

  • 브릿지

    • 여러 개의 이더넷 세그먼트*는 브릿지를 통해 연결되어 더 큰 LAN을 구성할 수 있음.
    • 브릿지는 허브보다 효율적으로 대역폭을 사용하며, 필요한 경우에만 프레임을 다른 세그먼트로 전달함.
* 이더넷 세그먼트
같은 물리적 전송 매체를 공유하는 모든 장치들의 집합
몇 개의 전선들과 허브라고 부르는 작은 상자로 구성됨.

Bryant, R. E., & O’Hallaron, D. R. (2023). Computer systems: A programmer’s perspective (3rd ed.). Pearson.

(LAN의 개념도) 허브와 브릿지를 하나의 수평선으로 연결했다.
  • 라우터
    • 여러 종류의 LAN은 서로 다른 물리적, 논리적 특성을 가지므로 서로 연결이 불가능함(비호환성 LAN)
    • LAN끼리 또는 LAN과 WAN을 연결하는 장치 (비호환성 LAN 포함)
    • 라우터는 여러 네트워크에 연결된 포트를 가지고 있으며
    • 다양한 네트워크 기술을 상호 연결해 인터넷(internet)을 구성함.
  • 인터넷 프로토콜
    • internet은 비호환적인 LAN과 WAN들로 연결되어 있기 때문에 이러한 비호환성을 해결함
    • 프로토콜의 핵심 기능
      • 이름 지정: 각 호스트에 고유한 인터넷 주소를 부여함
      • 전달 메커니즘: 데이터를 패킷* 단위로 나누고, 각 패킷에 출발지/목적지 주소 등 헤더 정보를 붙여 전달함.
* 패킷: 데이터를 작은 조각으로 나누어서 네트워크로 전송할 때, 각 조각 하나하나를 패킷이라 함.

LAN1의 호스트 A에서 LAN2의 호스트 B로 데이터 바이트를 전송하는 단계

  1. 애플리케이션 데이터 준비
    • 클라이언트(Host A)의 애플리케이션이 전송할 데이터를 프로토콜 소프트웨어에 전달함.
  2. 패킷 및 프레임 헤더 추가
    • 클라이언트의 프로토콜 소프트웨어가 데이터에 인터넷 패킷 헤더(PH)와 LAN 프레임 헤더(FH1)를 붙여 LAN1 어댑터로 전달함.
  3. LAN1 네트워크로 프레임 전송
    • LAN1 어댑터가 프레임을 LAN1 네트워크(케이블)로 복사함.
  4. 라우터가 프레임 수신
    • 라우터의 LAN1 어댑터가 프레임을 네트워크에서 읽어 프로토콜 소프트웨어에 전달함.
  5. 라우터가 목적지 확인 및 프레임 변환
    • 라우터의 프로토콜 소프트웨어가 인터넷 패킷 헤더에서 목적지 주소를 읽고, 라우팅 테이블을 참조해 다음 네트워크(LAN2)로 보낼지 결정함.
    • 기존 LAN1 프레임 헤더를 제거하고, 새로운 LAN2 프레임 헤더(FH2)를 붙여 LAN2 어댑터로 전달함.
  6. LAN2 네트워크로 프레임 전송
    • 라우터의 LAN2 어댑터가 프레임을 LAN2 네트워크로 복사함.
  7. 목적지 호스트(Host B)가 프레임 수신
    • Host B의 LAN2 어댑터가 프레임을 네트워크에서 읽어 프로토콜 소프트웨어에 전달함.
  8. 헤더 제거 및 데이터 전달
    • Host B의 프로토콜 소프트웨어가 인터넷 패킷 헤더와 프레임 헤더를 제거하고, 남은 데이터를 서버 애플리케이션의 가상 주소 공간으로 복사함(서버가 read 시스템 콜을 사용할 때).
호스트A -> 프로토콜 소프트웨어 -> LAN1 어뎁터 ->

라우터(LAN1 어뎁터, 프로토콜 소프트웨어, LAN2 어뎁터) ->

LAN2 어뎁터 -> 프로토콜 소프트웨어 -> 호스트B

* 의문점과 해결
Q. OSI 7계층을 떠나서 프로토콜 소프트웨어에서는 IP를, 어뎁터에서는 MAC주소를 패키징하는데 왜 이런 순서로 패키징을 할까?

A. MAC 주소는 다음 장치의 주소를 가리키고 있고, 라우터 등의 장치를 거치게 되면 새로운 장치의 주소로 갱신이 된다. 따라서 IP 주소를 바탕으로 최종 목적지 네트워크를 가면서, MAC 주소를 계속 갱신하며 장치 사이를 이동한다.

3) 글로벌 IP 인터넷

각 인터넷 호스트는 TCP/IP 프로토콜을 구현하는 소프트웨어 실행함.

TCP/IP는 여러 프로토콜의 집합으로 다양한 기능을 제공함.

  • IP(Internet Protocol)
    기본적인 네이밍 체계, 패킷 전달 메커니즘 제공함. IP는 패킷 손실이나 중복에 대해 복구하지 않는 신뢰성 없는 전달 방식을 사용
  • UDP(Unreliable Datagram Protocol)
    IP 위에서 동작하며, 호스트 간이 아닌 프로세스 간 데이터그램 전송을 지원함.
  • TCP(Transmission Control Protocol)
    IP 위에서 동작하며, 신뢰성 있는 양방향 연결을 제공함.

프로그래머 관점에서 인터넷 클라이언트와 서버는 소켓 인터페이스와 유닉스 IO함수의 조합을 통해 통신함. 소켓 함수들은 시스템 콜로 구현되어 커널의 TCP/IP 코드와 상호작용함.

하드웨어 및 소프트웨어 구조

  • 각 호스트(클라이언트/서버)는 네트워크 어댑터(하드웨어)와 TCP/IP 프로토콜 스택(커널 및 사용자 소프트웨어)을 통해 인터넷에 연결되어 있음.
  • 애플리케이션은 소켓 인터페이스를 통해 데이터를 송수신함.
  • 실제 데이터 전송은 커널의 TCP/IP와 네트워크 어댑터를 통해 이루어짐.

3-1) IP 주소

인터넷에서 각 호스트를 식별하는 32비트 부호 없는 정수
네트워크 상에서 고유성을 보장하며 실제 네트워크 통신에 사용됨.

  • IP 주소 구조와 저장 방식
    네트워크 프로그램에서는 IP주소를 구조체에 저장함. 이 구조체는 32비트 정수 하나로 이루어짐
    • IP 주소와 같은 네트워크 데이터는 항상 네트워크 바이트 오더(빅엔디안*)로 저장됨.
    • 시스템 마다 바이트 오더가 다를 수 있으므로, 네트워크로 전송되는 모든 정수 데이터는 TCP/IP 표준에 따라 빅 엔디안으로 변환되어야 함.
* 리틀 엔디안:
* 빅 엔디안: 

IP 주소는 사람이 읽기 쉬운 점-십진수(dotted-decimal) 문자열과 내부의 32비트 정수(네트워크 바이트 오더) 사이를 변환할 수 있음

3-2) 인터넷 도메인 이름

인터넷 클라이언트와 서버는 IP주소를 사용하여 통신하지만, 사람이 기억하기 어렵기 때문에 도메인 이름 체계를 사용함.

도메인 이름은 트리 구조의 계층을 가짐.

  • 최상위: 이름 없는 루트
  • 1차 도메인: com, edu 등 ICANN(internet corporation for assigned names and numbers)
  • 2차 도메인: cmu.edu 기관이나 단체가 관리하는 도메인
  • 3차 도메인: cs.cmu.edu

도메인 이름 특징

  • 도메인 이름과 IP 주소의 매핑은 분산된 데이터베이스인 DNS(domain name system)이 관리함.
  • 여러 도메인이 하나의 IP에 매핑되거나 하나의 도메인이 여러 IP에 매핑될 수 있음.
  • localhost 도메인은 루프백 주소* 127.0.0.1로 매핑되어 동일한 호스트 내에서 네트워크 어플리케이션을 테스트할 때 유용함.
  • 모든 도메인 이름이 반드시 IP 주소에 매핑되는 것은 아님.
* 루프백 주소: 자기 자신(호스트 자신)을 가리키는 IP주소, 컴퓨터가 스스로에게 네트워크 통신을 할 때 사용하는 특수한 주소로 운영체제 내에서 프로그램끼리 통신할 때 사용함.

3-3) 인터넷 연결

인터넷에서 클라이언트와 서버는 '연결'을 통해 데이터를 주고 받음. 이 연결은 두 프로세스가 서로 데이터를 교환할 수 있는 점대점의 통신 경로임. 연결은 full duplex, 양방향으로 동시에 데이터 송수신이 가능함.

  • 소켓
    연결의 양 끝점을 소켓이라고 하며, 각 소켓은 (IP주소, 16비트 포트 번호)로 구성된 소켓 주소를 가짐.
    • IP주소는 호스트를 식별하고 포트 번호는 해당 호스트 내의 특정 프로세스를 식별함
    • 서버는 보통 well-known port(80번 - HTTP, 443번 - HTTPS)에 바인딩되어 클라이언트의 연결 요청을 기다림
    • 클라이언트가 연결을 요청하면 커널이 임시(emphemeral) 포트를 할당함.
    • 두 개의 소켓 주소는 소켓 쌍이라고 하며 tuple(cliaddr:cliport, servaddr:servport)로 나타낸다.

4) 소켓 인터페이스

4-1) 소켓 주소 구조체

리눅스 커널 관점에서 소켓은 통신의 끝점이며, 파일 디스크립터와 연결된 열린 파일과 같음.

인터넷 소켓 주소는 16바이트 크기의 'struct sockaddr_in' 구조체에 저장됨

주요 필드

  • sin_family: 프로토콜 패밀리(AF_INET)
  • sin_port: 16비트 포트 번호
  • sin_addr: 32비트 IP 주소
  • sin_zero: 패딩(구조체 크기 맞춤)

일반 소켓 주소 구조체

  • 소켓 관련 함수('connect', 'bind', 'accept')는 다양한 프로토콜 주소 구조체를 받아야 하므로 C에서는 프로토콜 독립적인 'struct sockaddr' 포인터를 인자로 받도록 설계됨
  • 실제 사용할 때는 structure sockaddr_in*을 struct sockaddr*로 캐스팅해서 전달함

4-2) socket 함수

클라이언트와 서버 모두 네트워크 통신의 끝점인 소켓 디스크립터를 생성할 때 사용함.

#include <sys/types.h>
#include <sys/socket.h>

int socket(int domain, int type, int protocol);
  • 구조
    • domain: 주소 체계 지정
    • type: 소켓 타입 지정
    • protocol: 일반적으로 0을 사용

ex)

clientfd = Socket(AF_INET, SOCK_STREAM, 0);

AF_INET은 32비트 IP주소를 사용하고 있음을
SOCK_STREAM은 연결지향(TCP)을
0은 자동으로 해당 도메인/타입에 맞는 프로토콜을 선택한다.

getaddrinfo 함수를 이용하면 매개변수들을 자동으로 생성한다.

socket 함수만 호출한 상태에서는 소켓이 아직 "부분적으로 열린 상태"이다.
이후
client는 connect
server는 bind/listen/accpet를 호출해 소켓을 완전히 개방한다.

4-3) connect 함수

클라이언트가 서버와의 연결을 설정할 때 사용하는 함수, 클라이언트 측 소켓 디스크럽터를 완전히 열어서 데이터 송수신이 가능하게 만듦.

#include <sys/socket.h>
int connect(int clientfd, const structure sockaddr* addr, socklen_t addrlen);
  • 구조
    • clientfd: 소켓 디스크럽터(이전에 socket 함수로 생성)
    • addr: 서버의 소켓 주소(일반적으로 struct sockaddr_in의 포인터를 캐스팅)
    • addrlen: 주소 구조체의 크기(일반적으로 sizeof(sockaddr_in))
  • 동작
    • connect 함수는 지정된 서버 주소로 연결을 시도함.
    • 연결이 성공하면 해당 소켓 디스크립터는 읽기/쓰기에 사용할 수 있게 됨
    • 함수는 연결이 완료되거나 오류가 발생할 때까지 블로킹됨.
    • 성공 시 0, 실패 시 -1 반환
  • 연결의 식별
    • 연결이 성립되면 커널은 (클라이언트IP(x), 클라이언트 포트(y), 서버IP(addr.sin_addr), 서버 포트(addr.sin_port) 쌍으로 연결을 고유하게 식별함.
    • (x:y, addr.sin_addr:addr.sin_port)

4-4) bind 함수

서버 소켓의 디스크립터와 특정 소켓 주소를 커널에 연결(바인딩)함. 서버는 이 과정을 통해 자신이 어떤 IP와 포트에서 클라이언트의 연결 요청을 받을지 명확히 지정함.

#include <sys.socket.h>

int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
  • 구조
    • sockfd: socket 함수로 생성한 소켓 디스크립터
    • addr: 바인딩할 소켓 주소(일반적으로 struct sockaddr_in*을 캐스팅)
    • addrlen: 주소 구조체의 크기 (IPv4의 경우 sizeof(struct sockaddr_in))
  • 동작
    • 커널에 "이 소켓은 이 IP 주소와 포트 번호에서 연결을 수신하겠다."라고 알림.
    • 성공 시 0, 실패 시 -1을 반환함.
    • 이미 사용 중인 포트에 바인딩을 시도하면 오류가 발생할 수 있음.

4-5) listen 함수

서버 소켓을 수신 대기 상태로 전환함.
클라이언트의 연결 요청을 받을 준비를 마친 소켓(바인딩된 소켓)을 '수신 대기 소켓'으로 만들어 커널이 클라이언트 요청을 큐에 저장할 수 있게 함.

#include <sys/socket.h>

int listen(int sockfd, int backlog);
  • 구조

    • sockfd: socket과 bind로 생성/바인딩한 소켓 디스크립터
    • backlog: 커널이 동시 대기시킬 수 있는 연결 요청의 최대 개수(큐의 크기)
      • 너무 작게 설정하면 많은 클라이언트가 동시 접속할 때 일부 요청이 거부될 수 있음.
      • 관용적으로는 'SOMAXCONN' 또는 라이브러리에서 정의한 상수를 사용
  • 동작

    • 서버는 socket -> bind -> listen 순서로 소켓을 준비함.
    • listen을 호출하면, 커널이 해당 소켓을 listening 상태로 전환하고, 클라이언트 연결 요청을 큐에 쌓기 시작함.
    • 실제 연결은 이후 'accept' 함수에서 처리함.

4-6) accept 함수

서버가 클라이언트의 연결 요청을 수락할 때 사용하는 함수
서버는 accept를 호출해 클라이언트의 연결 요청이 도착할 때까지 블로킹 상태로 대기하며, 연결이 오면 새로운 연결 소켓 디스크립터를 반환함.

#include <sys/socket.h>

int accept(int listenfd, struct sockaddr *addr, socklen_t *addrlen);
  • 구조
    • listenfd: socket/bind/listen을 거쳐 생성된 수시 대기 소켓 디스크립터
    • addr: 연결을 요청한 클라이언트의 소켓 주소를 저장할 구조체 포인터
    • addrlen: 주소 구조체의 크기를 가리키는 포인터
  • 동작
    • accept는 클라이언트의 연결 요청이 큐에 들어올 때까지 블로킹됨.
    • 연결 요청이 오면, 커널은 새로운 연결 소켓 디스크립터를 생성해 변환함.
    • 반환된 connfd는 해당 클라이언트와의 통신에 사용되며, 기존의 listendfd는 계속해서 추가 연결 요청을 수신 대기함.
    • 클라이언트의 주소 정보는 'addr'에, 그 크기는 'addrlen'에 저장됨.
  • listening descriptor: 서버가 클라이언트 연결을 기다릴 때 사용. 서버 생명주기 동안 유지
  • connected descriptor: 각 클라이언트와의 실제 데이터 송수신에 사용. 서비스가 끝나면 닫음

흐름 정리

클라이언트

  1. socket 함수를 호출하여 소켓 디스크립터를 생성함. 소켓은 부분적으로 열린 상태
  2. connect 함수를 호출하여 서버에 연결을 요청함. 서버 주소와 포트 정보를 담은 소켓 주소 구조체를 인자로 받음
  3. 연결이 성공하면 소켓이 완전히 열리고 데이터 송수신이 가능해짐
  4. 이 함수는 연결이 성립되거나 오류가 발생할 때까지 블로킹됨

서버

  1. socket 함수를 호출하여 소켓 디스크립터를 생성함. 소켓은 부분적으로 열린 상태
  2. bind 함수를 호출하여 소켓을 특정 IP주소와 포트 번호에 연결
    • 서버가 어떤 네트워크 인터페이스와 포트에서 클라이언트 요청을 받을지 지정
  3. listen 함수를 호출하여 소켓을 수신 대기 상태로 전환
    • 소켓을 클라이언트 연결 요청을 받을 준비를 함(수동적 소켓으로 변환됨)
    • 커널은 클라이언트 연결 요청을 큐에 저장하기 시작함
  4. accept 함수를 호출하여 클라이언트의 연결 요청을 수락함
    • 클라이언트 연결 요청이 도착할 때까지 블로킹됨
    • 연결 요청이 오면 새로운 연결 소켓 디스크립터를 반환함.
      (bind할 때 매개변수로 받았던 IP주소와 포트 번호를 전달함
    • 원래의 수신 대기 소켓은 계속해서 다른 연결 요청을 기다림

4-7) 호스트와 서비스 변환

4-8) 소켓 인터페이스를 위한 도움 함수들

4-9) Echo 클라이언트와 서버

여기서는 쉽게 쉽게 설명하고 넘어갈 예정이기 때문에
깊은 이해가 필요하다면 아래 글을 참고하길 바란다.
https://velog.io/@mogiyoon/web-server-고찰#echo

#include "csapp.h"

int main(int argc, char ** argv)
{
  int clientfd;
  char *host, *port, buf[MAXLINE];
  rio_t rio;

  if (argc != 3)
  {
    fprintf(stderr, "usage: %s <host> <port>\n", argv[0]);
    exit(0);
  }
  host = argv[1];
  port = argv[2];

  clientfd = Open_clientfd(host, port);
  Rio_readinitb(&rio, clientfd);

  while (Fgets(buf, MAXLINE, stdin) != NULL)
  {
    Rio_writen(clientfd, buf, strlen(buf));
    Rio_readlineb(&rio, buf, MAXLINE);
    Fputs(buf, stdout);
  }
  Close(clientfd);
  exit(0);
}

클라이언트 함수이다.
여기서는 먼저 서버에 연결한 클라이언트 소켓 디스크립터를 생성한다.
이후 소켓을 쓰고 읽는 과정을 바탕으로
buf에 있는 데이터를 전달하고, rio_buf에 데이터를 받아온 뒤 데이터를 출력한다.

클라이언트 서버 설명은 끝이다.
진짜 간단하게 설명하고 넘어갈 거다.

#include "csapp.h"

void echo(int connfd);

int main(int argc, char** argv)
{
  int listenfd, connfd;
  socklen_t clientlen;
  struct sockaddr_storage clientaddr;
  char client_hostname[MAXLINE], client_port[MAXLINE];

  if (argc != 2)
  {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(0);
  }

  listenfd = Open_listenfd(argv[1]);
  while (1)
  {
    clientlen = sizeof(struct sockaddr_storage);
    connfd = Accept(listenfd, (SA*)&clientaddr, &clientlen);
    Getnameinfo((SA*) &clientaddr, clientlen, client_hostname, MAXLINE, client_port, MAXLINE, 0);
    printf("Connected to (%s, %s)\n", client_hostname, client_port);
    echo(connfd);
    Close(connfd);
  }
  exit(0);
}
\

서버 함수이다.
리스닝용 소켓을 생성하고
리스닝용 소켓에 들어온 클라이언트 요청을 바탕으로 새로운 소켓을 생성하여 클라이언트 정보를 담는다.
이후 에코 함수를 활용하여 클라이언트에게 데이터를 전달한다.

void echo(int connfd)
{
  size_t n;
  char buf[MAXLINE];
  rio_t rio;

  Rio_readinitb(&rio, connfd);
  while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
  {
    printf("server received %d bytes\n", (int)n);
    Rio_writen(connfd, buf, n);
  }
}

에코 함수에는 클라이언트로부터 전달받은 데이터를 rio 버퍼에서 꺼내서 바이트 수를 확인하고
그걸 그대로 다시 클라이언트에게 전달한다.
이상 끝.

5) 웹 서버

5-1) 웹 기초

  • 웹 클라이언트와 서버
    웹 시스템은 텍스트 기반의 애플리케이션 계층 프로토콜인 HTTP를 사용하여 클라이언트와 서버가 상호작용함.
    웹 클라이언트는 서버에 인터넷 연결을 열고 특정 콘텐츠를 요청함. 서버는 요청받는 콘텐츠를 응답으로 보내고, 그 후 연결을 닫음. 브라우저는 받은 콘텐츠를 화면에 표시함.

  • 웹과 기존 파일 전송 서비스의 차이
    웹 서비스와 FTP 같은 기존 파일 전송 서비스의 가장 큰 차이점은, 웹 콘텐츠가 HTML로 작성된다는 점임. HTML은 텍스트와 그래픽 객체의 표시 방법을 브라우저에 지시하는 태그로 구성된 언어임.

  • 하이퍼링크
    HTML의 진정한 힘은 하이퍼링크에 있음.
    사용자가 특정 텍스트를 클릭하면 CMU 웹 서버에 해당 html 파일을 요청하도록 만듦.

5-2) 웹 컨텐츠

웹 서버와 클라이언트가 주고받는 "컨텐츠"는 MIME(multipupose internet mail extensions) 타입*의 바이트 배열임 (콘텐츠의 형식: html, plain, jpeg 등)

* MIME 타입: 인터넷에서 전송되는 데이터의 형식을 나타내는 표준 방식. 현재는 다양한 인터넷 프로토콜에서 전송되는 데이터의 종류를 식별하는 데 사용됨.

1. 역할 및 중요성
- 데이터의 형식 식별: 서버가 클라이언트에 데이터를 전송할 때, 데이터가 어떤 종류의 파일인지 명확히 알려주는 메타데이터 역할
- 처리 방식 결정: 클라이언트는 MIME타입을 보고 해당파일을 어떻게 처리할지 결정함. 예를 들면 text/html이면 HTML로 렌더링하고 image/png면 이미지를 표시함
- HTTP 헤더의 Content-Type: 웹에서는 주로 HTTP 헤더의 Content-Type 필드에 MIME 타입이 명시되어 전송됨.

2. 구조 및 형식
- 타입(type)과 서브타입(subtype)으로 구성되며 슬래시(/)로 구분함.
- type/subtype (text/html, image/jpeg, apllication/json)
  1. 정적 콘텐츠
    • 서버가 디스크에 저장된 파일(html, 이미지, 텍스트 파일)을 읽어서 클라이언트에 반환하는 방식
    • 이 과정을 정적 콘텐츠 제공이라고 함.
  2. 동적 콘텐츠
    • 서버가 어떤 실행 파일(프로그램)을 실행하여, 그 프로그램이 런타임에 생성한 출력을 클라이언트에 반환하는 방식
    • 이 과정을 동적 콘텐츠 제공이라고 함.
  • URL과 콘텐츠 매핑
    • 웹 서버가 반환하는 모든 콘텐츠는 URL로 고유하게 식별됨.
    • URL의 접두부는 클라이언트가 어떤 서버와 포트에 접속할지 결정함.
    • URL의 접미부는 서버가 실제로 어떤 파일을 반환할 지 또는 어떤 프로그램을 실행할지 결정함.
    • 동적 콘텐츠의 경우 URL의 ?뒤에 오는 부분이 프로그램 인자(argument)로 전달되고, 각 인자는 &로 구분됨.
  • 서버의 해석 규칙
    • 어떤 URL이 정적/동적 콘텐츠에 해당하는지는 서버의 정책에 따라 다름.
      예를 들어 cgi-bin 디렉토리 아래 파일은 모두 실행 파일(동적콘텐츠)로 간주하는 것이 고전적 방식
  • URL의 /는 리눅스의 루트 디렉토리가 아닌, 웹 서버가 관리하는 콘텐츠의 홈 디렉토리를 의미함.
  • /만 임력하면 서버는 보통 이를 index.html 등 기본 홈 페이지로 확장함.

5-3) HTTP 트랜잭션

클라이언트가 서버에 요청(request)를 보내고, 서버가 이에 응답(response)하는 단순한 텍스트 기반 프로토콜

HTTP 요청

웹 클라이언트가 서버에 원하는 자원을 요청할 때 사용하는 메시지 형식. HTTP 요청은 텍스트 기반 프로토콜임.

  • HTTP 요청 메시지의 구조
    1. 요청 라인(Request Line)
      형식: method(메서드) URI version(버전) (ex. GET /index.html HTTP/1.1)
      메서드: 클라이언트가 서버에 원하는 동작을 지정
      URI: 요청하는 자원의 경로 (파일 이름과 옵션인 인자들을 포함하는 URL의 접미어)
      버전: 사용할 HTTP 프로토콜 버전
    2. 요청 헤더(Request Header)
      추가적인 정보를 서버에 전달하는 키-값 쌍의 집합 (ex. HOST: www.aol.com)
      HTTP/1.1에서는 Host 헤더가 필수임. 이 헤더는 서버가 어떤 도메인에 대한 요청인지 구분하는데 사용함.
    3. 빈 줄
      요청 헤더의 끝을 알리기 위해 반드시 한 줄을 비워야 함.
    4. 요청 본문(Request Body)
      주로 POST 방식에서 사용되며, 클라이언트가 서버로 데이터를 전송할 때 포함함.

HTTP 응답

웹 서버가 클라이언트의 요청에 대해 반환하는 메시지 형식

  • HTTP 응답 메시지의 구조
    1. 응답 라인(Response Line)
      형식: version(버전) status-code(상태 코드) status-message(상태 메시지)
      버전: 서버가 사용하는 HTTP 프로토콜 버전
      상태코드: 요청 처리 결과를 나타내는 3자리 정수 (ex. 200, 404)
      상태메시지: 상태코드의 영문 설명 (ex. OK, Not Found)
    2. 응답 헤더(Response Headers)
      추가적인 정보를 전달하는 키-값 쌍의 집합
      주요 헤더
      • Content-Type: 응답 본문의 MIME 타입
      • Content-Length: 응답 본문의 바이트 단위 길이
    3. 빈 줄
      헤더의 끝을 알리기 위해 반드시 한 줄을 비워야 함.
    4. 응답 본문(Response Body)
      실제로 클라이언트가 요청한 콘텐츠(HTML, 이미지)
상태 코드메시지설명
200OK요청이 정상적으로 처리됨
301Moved permanently콘텐츠가 Location 헤더에 명시된 위치로 이동됨
400Bad request요청이 잘못되어 서버가 이해할 수 없음
403Forbidden요청한 파일에 접근 권한이 없음
404Not found요청한 파일을 찾을 수 없음
501Not implemented서버가 해당 요청 메서드를 지원하지 않음
505HTTP version not supported서버가 요청받은 HTTP 버전을 지원하지 않음

5-4) 동적 컨텐츠의 처리

동적 컨텐츠란, 클라이언트의 요청이 들어올 때마다 서버가 특정 프로그램을 실행하여 그 결과를 HTTP 응답으로 반환하는 방식을 의미함. 이는 정적 콘텐츠와 달리, 요청 시마다 결과가 달라질 수 있음.

동적 컨텐츠 제공의 원리

  • 실행 파일 호출
    클라이언트가 동적 컨텐츠를 요청하면, 웹 서버는 해당 요청에 대응하는 실행 파일(CGI* 프로그램, 스크립트 등)을 새로운 프로세스로 실행함.
    • fork를 호출하여 자식 프로세스를 생성하고, execve를 호출하여 프로그램을 자식의 컨텍스트에서 실행함.
  • 입력 전달
    URL의 ? 뒤에 오는 쿼리 문자열이 프로그램의 인자로 전달됨.
  • 출력 전달
    실행된 프로그램은 표준 출력(stdout)으로 HTML이나 기타 데이터를 출력함. 서버는 이 출력을 HTTP 응답 본문으로 클라이언트에 전달함.
  • 환경 변수
    서버는 CGI 프로그램에 대한 다양한 환경 변수를 설정함.
* CGI(Common Gateway Interface)
웹 서버와 외부 애플리케이션 간의 데이터를 주고받는 표준 인터페이스 규약

1. 역할
- 정적 웹 페이지는 서버에 저장된 파일을 그대로 클라이언트에게 전달
- 동적 웹 페이지는 사용자의 입력이나 요청에 따라 서버에서 실시간으로 결과를 만들어야 함
- CGI는 웹 서버가 이러한 동작 처리를 외부 프로그램에 맡기고 그 결과를 클라이언트에게 전달함.

2. 동작 원리
  1) 클라이언트 요청: 사용자가 폼 제출 등 동적 처리가 필요한 요청을 웹 서버에 보냄.
  2) 웹 서버가 CGI 프로그램 실행: 웹 서버는 요청을 처리하기 위해 CGI 프로그램을 실행함. 
     사용자의 입력 데이터는 환경변수, 표준 입력 등으로 CGI에 전달
  3) CGI 프로그램의 처리: CGI 프로그램은 전달받은 데이터를 처리하고 결과를 표준 출력으로 웹 서버에 반환.
  4) 웹 서버가 응답 전송: 웹 서버는 CGI 프로그램의 출력을 받아 HTTP 응답으로 클라이언트에 전달
  
3. 특징
 - 언어 독립적: CGI 프로그램은 다양한 언어로 작성할 수 있음.
	※ 브라우저가 JS를 실행해서 생성되는 동적 컨텐츠는 CGI가 아님.
 - 프로세스 기반: 요청마다 독립적인 프로세스가 생성되어 실행됨. (요청이 몰리면 서버 자원 소모가 커짐)
 - 동적 콘텐츠 생성: 사용자의 요청에 따라 실시간으로 결과를 생성함.

4. 한계
 - CGI는 요청마다 별도의 프로세스를 생성하므로 대량의 트래픽이 발생하면 성능 저하 및 자원 낭비가 발생함.
 - 이를 보완하기 위해 프로세스 재사용 및 스레드 기반의 대체 기술이 등장함.

특징 및 구조

  • 요청마다 실행: 클라이언트의 각 요청마다 별도의 프로세스가 생성되어 프로그램이 실행됨
  • 결과의 동적 생성: 프로그램은 출력은 입력 인자, 서버 상태, 외부 데이터베이스 등 다양한 요인에 따라 달라질 수 있음.
  • 보안 및 성능: 동적 컨텐츠는 강력하지만, 잘못 구현하면 보안 취약점이나 성능 저하가 발생함.

6) 종합 설계: 소형 웹 서버

https://velog.io/@mogiyoon/web-server-고찰#tiny

int main(int argc, char **argv)
{
  int listenfd, connfd;
  char hostname[MAXLINE], port[MAXLINE];
  socklen_t clientlen;
  struct sockaddr_storage clientaddr;

  /* Check command line args */
  if (argc != 2)
  {
    fprintf(stderr, "usage: %s <port>\n", argv[0]);
    exit(1);
  }

  listenfd = Open_listenfd(argv[1]);
  while (1)
  {
    clientlen = sizeof(clientaddr);
    connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen); // line:netp:tiny:accept
    Getnameinfo((SA *)&clientaddr, clientlen, hostname, MAXLINE, port, MAXLINE, 0);
    printf("Accepted connection from (%s, %s)\n", hostname, port);
    doit(connfd);  // line:netp:tiny:doit
    Close(connfd); // line:netp:tiny:close
  }
}

역시 깊게 설명하지 않을 예정이다.
듣기 소켓을 열고 연결 소켓을 생성해서 doit 함수에 매개변수로 전달한다.

void doit(int fd)
{
  int is_static;
  struct stat sbuf;
  char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
  char filename[MAXLINE], cgiargs[MAXLINE];
  rio_t rio;

  /* Read request line and headers */
  Rio_readinitb(&rio, fd);
  Rio_readlineb(&rio, buf, MAXLINE);
  printf("Request headers:\n");
  printf("%s", buf);
  sscanf(buf, "%s %s %s", method, uri, version);
  if (strcasecmp(method, "GET"))
  {
    clienterror(fd, method, "501", "Not implemented", "Tiny does not implement this method.");
    return;
  }
  read_requesthdrs(&rio);

  /* Parse URI from GET request */
  is_static = parse_uri(uri, filename, cgiargs);
  if (stat(filename, &sbuf) < 0)
  {
    clienterror(fd, filename, "404", "Not found", "Tiny couldn't find this file");
    return;
  }

  if (is_static)
  {
    /* Serve static content */
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode))
    {
      clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't read the file");
      printf("error check: %d, %d\n", (S_ISREG(sbuf.st_mode)), (S_IRUSR & sbuf.st_mode));
      return;
    }
    serve_static(fd, filename, sbuf.st_size);
  }
  else
  {
    /* Serve dynamic content */
    if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode))
    {
      clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't run the CGI program");
      return;
    }
    serve_dynamic(fd, filename, cgiargs);
  }
}

doit 함수에서는 전달받은 연결 소켓을 활용해서
클라이언트가 요청을 보내면 이를 해석해서
uri, filename, cgi 인자로 나누고
이를 바탕으로
serve_static나 serve_dynamic 함수 둘 중 하나를 호출해서
클라이언트에게 응답을 보내고 파일 데이터를 보낸다.

serve_dynamic에서는 추가적으로 자식 프로세스를 생성하고, 환경변수를 통해 인자를 전달한 다음,
CGI 프로그램을 프로세스에 덮어쓴 다음 CGI 프로그램을 실행한다.

profile
안녕

0개의 댓글