public string DevelopmentDiary(Knowledge knowledge) {
if (knowledge != null) {
return "level up"
} else {
return "level down"
}
}

우리 반에서 유행한지 좀 오래된 게임이다.
나는 어제 시작했다.
반 동기에 의하면, 남들 따라하는 거는 강남병이라고 한단다.
내가 그 강남병이다.
근데 이제 유행 다 지나가고 따라하는..
꽤나 재밌다.
우리 반에서 이 게임을 제일 잘하는 사람은 '천마'라는 칭호를 달고 있다.
나를 제자로 받아들여준다고 했다.
나는 이제 소교주다.
천마는 9, 8, 7을 최우선 순위로 탐색하라고 했다.
최근에 시작한 또 다른 반 동기는, 어제 '문지기'의 칭호를 받았다.
옆에서 다른 동기가 '문 안으로는 못 들어오고 문만 지킨다'고 했다.
냉혹한 세계다.
이번 주차에 '무림맹주'가 같은 조원이 됐다.
무림맹주가 제자를 권유했지만, 나는 이미 스승이 있다고 거절했다.
그가 계속 제안했고, 결국 나는 스승이 천마라고 얘기할 수 밖에 없었다. 그는 바로 납득했다.
무림맹주는 BFS로 탐색하라고 했다.
잠 깰 때 좋은 것 같다.
리눅스에서 파일은 연속된 m개의 바이트다. 그리고 모든 I/O(네트워크, 디스크, 터미널)은 '파일'로 추상화된다. 모든 입력과 출력은 해당 장치에 대응하는 파일을 읽고 쓰는 방식으로 이루어진다. 이로 인해 커널은 단순하고 일관된 저수준 인터페이스*를 제공할 수 있다.
* 저수준 인터페이스
이 다음 설명이 조금 이해하기 어려웠는데,
뭔가 '입/출력/에러'라고 했을 때 방식을 얘기하는 것 같은데 파일이라고 해서 당황스러웠다.
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' 연산을 통해 위치를 변경할 수 있다.
읽기와 쓰기
리눅스 파일의 타입
디렉터리 구조와 경로명
디렉터리의 역할
Robust IO 패키지의 필요성
RIO 패키지의 목적
Unbuffered 함수 (rio_readn, rio_writen)
Buffered 함수 (rio_readlineb, rio_readnb)
애플리케이션이 메모리와 파일 사이에 직접 데이터를 전송할 때 사용하는 함수.
이 함수들은 short count를 robust하게 처리함.
주요 함수
또 어려운 말 섞어가면서 잘난 척한다.
다시 분석해보자.
우선 usrbuf가 뭔지 모르겠다.
usrbuf란?
rioreadn, riowriten, rioreadlineb, rioreadnb)에서 데이터를 읽거나 쓸 때 실제로 사용자(프로그램)가 데이터를 저장하거나 전달하는 메모리 버퍼를 의미함.그렇다면 메모리 버퍼란 무엇일까?
메모리 버퍼란?
메모리의 어떤 공간을 사용하는 걸까?

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

https://velog.io/@mogiyoon/web-server-고찰#client
여기서 확인해보면
unbuffered 함수는 rio 버퍼를 쓰지 않고 사용자가 만든 버퍼를 쓰며
buffered 함수는 rio 버퍼를 쓴다.
왜 rio 버퍼를 사용할까? 라는 질문은 아래 5-2를 마저 읽고
생각하도록 하자.
파일에서 한 줄씩 읽거나 많은 데이터를 효율적으로 읽고 싶을 때, 1바이트씩 read를 호출하면 매우 비효율적임. Rio 패키지의 함수들은 내부 애플리케이션 버퍼를 사용해 효율적으로 텍스트 라인이나 바이너리 데이터를 읽을 수 있도록 함.
주요 함수
사용 방식
특징
* thread-safe: 공유 자원에 여러 스레드가 동시에 접근해도, 동기화 문제 없이 안전하게 동작함.
왜 usrbuf 버퍼가 있는데, rio 버퍼를 또 쓰는 것일까?
오랜 시간 고민하다가 그런 생각이 들었다.
writen에서는 usrbuf을 바로 쓰고, read에서는 rio를 쓴다.
어떻게 보면 write는 내가 가지고 있는 데이터를 보내는 것이기 때문에 중간에 끊기더라도 연속해서 보낼 수 있다.
하지만 read를 통해 읽는 데이터는 한 번에 들어올 수도, 아닐 수도 있다.
따라서 개행문자 '\n'가 나타날 때까지 글자가 들어오는 족족 read해서 rio 버퍼에 담아둔다.
그러다 개행문자가 나타나면 이제 유저 버퍼로 옮기고, 바로 출력하는 것이다.
그렇다면 유저버퍼에다 담으면서 개행문자가 나타나면 출력하면 되지않을까?
라는 의문도 든다.
그래서 rio 버퍼를 따로 두는 것이다.
사실 rio 버퍼 역할을 대체할 수 있으면
커스텀 버퍼를 활용할 수도 있다.
해당 모델에서 애플리케이션은 서버 프로세스와 하나 이상의 클라이언트 프로세스로 구성됨.
서버는 특정 자원을 관리하며, 이를 조작해서 클라이언트에게 서비스를 제공함.
* 트랜잭션
- 데이터 저장과 I/O 연산의 일관성과 안정성을 보장하기 위한 처리 단위
- 여러 개의 연산을 하나의 논리적 단위로 묶어서 처리하며, 모두 성공하거나 전혀 아무것도 수행도지 않은 것처럼 해야함.
클라이언트와 서버는 '프로세스' 단위로 동작하며 실제로 같은 컴퓨터 안에서 여러 개가 동시에 실행될 수도 있음.
해당 챕터의 트랜잭션은 데이터베이스의 트랜잭션과 달리 원자성 등의 특성을 가지지 않으며, 단순히 클라이언트와 서버가 주고받는 일련의 절차를 의미함.
네트워크를 시스템 IO 장치로 이해하기
LAN과 이더넷

이더넷 어댑터
네트워크 장치와 컴퓨터 사이에서 이더넷 프레임을 처리하는 하드웨어
| 송신 | 수신 |
|---|---|
| 데이터를 프레임으로 패킹 | 프레임을 분해해서 데이터 추출 |
| 프레임을 전기 신호로 변환해 전송 | 전기 신호를 수신해 프레임으로 재구성 |
| 출발지 MAC 주소* 붙임 | 목적지 MAC 주소가 자기 것인지 확인 |
* MAC 주소: 네트워크 인터페이스(LAN 카드, WiFi 카드 등)에 할당된 고유한 식별자
이더넷 프레임
이더넷 네트워크에서 데이터를 전송할 때 사용하는 기본 단위
헤더 + 페이로드(데이터) + 에필로그 구조
[목적지 MAC][출발지 MAC][타입][데이터][CRC]
브릿지
* 이더넷 세그먼트
같은 물리적 전송 매체를 공유하는 모든 장치들의 집합
몇 개의 전선들과 허브라고 부르는 작은 상자로 구성됨.
Bryant, R. E., & O’Hallaron, D. R. (2023). Computer systems: A programmer’s perspective (3rd ed.). Pearson.
(LAN의 개념도) 허브와 브릿지를 하나의 수평선으로 연결했다.
* 패킷: 데이터를 작은 조각으로 나누어서 네트워크로 전송할 때, 각 조각 하나하나를 패킷이라 함.
LAN1의 호스트 A에서 LAN2의 호스트 B로 데이터 바이트를 전송하는 단계
호스트A -> 프로토콜 소프트웨어 -> LAN1 어뎁터 ->
라우터(LAN1 어뎁터, 프로토콜 소프트웨어, LAN2 어뎁터) ->
LAN2 어뎁터 -> 프로토콜 소프트웨어 -> 호스트B
* 의문점과 해결
Q. OSI 7계층을 떠나서 프로토콜 소프트웨어에서는 IP를, 어뎁터에서는 MAC주소를 패키징하는데 왜 이런 순서로 패키징을 할까?
A. MAC 주소는 다음 장치의 주소를 가리키고 있고, 라우터 등의 장치를 거치게 되면 새로운 장치의 주소로 갱신이 된다. 따라서 IP 주소를 바탕으로 최종 목적지 네트워크를 가면서, MAC 주소를 계속 갱신하며 장치 사이를 이동한다.
각 인터넷 호스트는 TCP/IP 프로토콜을 구현하는 소프트웨어 실행함.
TCP/IP는 여러 프로토콜의 집합으로 다양한 기능을 제공함.
프로그래머 관점에서 인터넷 클라이언트와 서버는 소켓 인터페이스와 유닉스 IO함수의 조합을 통해 통신함. 소켓 함수들은 시스템 콜로 구현되어 커널의 TCP/IP 코드와 상호작용함.
하드웨어 및 소프트웨어 구조
인터넷에서 각 호스트를 식별하는 32비트 부호 없는 정수
네트워크 상에서 고유성을 보장하며 실제 네트워크 통신에 사용됨.
* 리틀 엔디안:
* 빅 엔디안:
IP 주소는 사람이 읽기 쉬운 점-십진수(dotted-decimal) 문자열과 내부의 32비트 정수(네트워크 바이트 오더) 사이를 변환할 수 있음
인터넷 클라이언트와 서버는 IP주소를 사용하여 통신하지만, 사람이 기억하기 어렵기 때문에 도메인 이름 체계를 사용함.
도메인 이름은 트리 구조의 계층을 가짐.
도메인 이름 특징
* 루프백 주소: 자기 자신(호스트 자신)을 가리키는 IP주소, 컴퓨터가 스스로에게 네트워크 통신을 할 때 사용하는 특수한 주소로 운영체제 내에서 프로그램끼리 통신할 때 사용함.
인터넷에서 클라이언트와 서버는 '연결'을 통해 데이터를 주고 받음. 이 연결은 두 프로세스가 서로 데이터를 교환할 수 있는 점대점의 통신 경로임. 연결은 full duplex, 양방향으로 동시에 데이터 송수신이 가능함.
리눅스 커널 관점에서 소켓은 통신의 끝점이며, 파일 디스크립터와 연결된 열린 파일과 같음.
인터넷 소켓 주소는 16바이트 크기의 'struct sockaddr_in' 구조체에 저장됨
주요 필드
일반 소켓 주소 구조체
클라이언트와 서버 모두 네트워크 통신의 끝점인 소켓 디스크립터를 생성할 때 사용함.
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);
ex)
clientfd = Socket(AF_INET, SOCK_STREAM, 0);
AF_INET은 32비트 IP주소를 사용하고 있음을
SOCK_STREAM은 연결지향(TCP)을
0은 자동으로 해당 도메인/타입에 맞는 프로토콜을 선택한다.
getaddrinfo 함수를 이용하면 매개변수들을 자동으로 생성한다.
socket 함수만 호출한 상태에서는 소켓이 아직 "부분적으로 열린 상태"이다.
이후
client는 connect
server는 bind/listen/accpet를 호출해 소켓을 완전히 개방한다.
클라이언트가 서버와의 연결을 설정할 때 사용하는 함수, 클라이언트 측 소켓 디스크럽터를 완전히 열어서 데이터 송수신이 가능하게 만듦.
#include <sys/socket.h>
int connect(int clientfd, const structure sockaddr* addr, socklen_t addrlen);
서버 소켓의 디스크립터와 특정 소켓 주소를 커널에 연결(바인딩)함. 서버는 이 과정을 통해 자신이 어떤 IP와 포트에서 클라이언트의 연결 요청을 받을지 명확히 지정함.
#include <sys.socket.h>
int bind(int sockfd, const struct sockaddr* addr, socklen_t addrlen);
서버 소켓을 수신 대기 상태로 전환함.
클라이언트의 연결 요청을 받을 준비를 마친 소켓(바인딩된 소켓)을 '수신 대기 소켓'으로 만들어 커널이 클라이언트 요청을 큐에 저장할 수 있게 함.
#include <sys/socket.h>
int listen(int sockfd, int backlog);
구조
동작
서버가 클라이언트의 연결 요청을 수락할 때 사용하는 함수
서버는 accept를 호출해 클라이언트의 연결 요청이 도착할 때까지 블로킹 상태로 대기하며, 연결이 오면 새로운 연결 소켓 디스크립터를 반환함.
#include <sys/socket.h>
int accept(int listenfd, struct sockaddr *addr, socklen_t *addrlen);
패
스
여기서는 쉽게 쉽게 설명하고 넘어갈 예정이기 때문에
깊은 이해가 필요하다면 아래 글을 참고하길 바란다.
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 버퍼에서 꺼내서 바이트 수를 확인하고
그걸 그대로 다시 클라이언트에게 전달한다.
이상 끝.
웹 클라이언트와 서버
웹 시스템은 텍스트 기반의 애플리케이션 계층 프로토콜인 HTTP를 사용하여 클라이언트와 서버가 상호작용함.
웹 클라이언트는 서버에 인터넷 연결을 열고 특정 콘텐츠를 요청함. 서버는 요청받는 콘텐츠를 응답으로 보내고, 그 후 연결을 닫음. 브라우저는 받은 콘텐츠를 화면에 표시함.
웹과 기존 파일 전송 서비스의 차이
웹 서비스와 FTP 같은 기존 파일 전송 서비스의 가장 큰 차이점은, 웹 콘텐츠가 HTML로 작성된다는 점임. HTML은 텍스트와 그래픽 객체의 표시 방법을 브라우저에 지시하는 태그로 구성된 언어임.
하이퍼링크
HTML의 진정한 힘은 하이퍼링크에 있음.
사용자가 특정 텍스트를 클릭하면 CMU 웹 서버에 해당 html 파일을 요청하도록 만듦.
웹 서버와 클라이언트가 주고받는 "컨텐츠"는 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)
클라이언트가 서버에 요청(request)를 보내고, 서버가 이에 응답(response)하는 단순한 텍스트 기반 프로토콜
웹 클라이언트가 서버에 원하는 자원을 요청할 때 사용하는 메시지 형식. HTTP 요청은 텍스트 기반 프로토콜임.
웹 서버가 클라이언트의 요청에 대해 반환하는 메시지 형식
| 상태 코드 | 메시지 | 설명 |
|---|---|---|
| 200 | OK | 요청이 정상적으로 처리됨 |
| 301 | Moved permanently | 콘텐츠가 Location 헤더에 명시된 위치로 이동됨 |
| 400 | Bad request | 요청이 잘못되어 서버가 이해할 수 없음 |
| 403 | Forbidden | 요청한 파일에 접근 권한이 없음 |
| 404 | Not found | 요청한 파일을 찾을 수 없음 |
| 501 | Not implemented | 서버가 해당 요청 메서드를 지원하지 않음 |
| 505 | HTTP version not supported | 서버가 요청받은 HTTP 버전을 지원하지 않음 |
동적 컨텐츠란, 클라이언트의 요청이 들어올 때마다 서버가 특정 프로그램을 실행하여 그 결과를 HTTP 응답으로 반환하는 방식을 의미함. 이는 정적 콘텐츠와 달리, 요청 시마다 결과가 달라질 수 있음.
동적 컨텐츠 제공의 원리
* CGI(Common Gateway Interface)
웹 서버와 외부 애플리케이션 간의 데이터를 주고받는 표준 인터페이스 규약
1. 역할
- 정적 웹 페이지는 서버에 저장된 파일을 그대로 클라이언트에게 전달
- 동적 웹 페이지는 사용자의 입력이나 요청에 따라 서버에서 실시간으로 결과를 만들어야 함
- CGI는 웹 서버가 이러한 동작 처리를 외부 프로그램에 맡기고 그 결과를 클라이언트에게 전달함.
2. 동작 원리
1) 클라이언트 요청: 사용자가 폼 제출 등 동적 처리가 필요한 요청을 웹 서버에 보냄.
2) 웹 서버가 CGI 프로그램 실행: 웹 서버는 요청을 처리하기 위해 CGI 프로그램을 실행함.
사용자의 입력 데이터는 환경변수, 표준 입력 등으로 CGI에 전달
3) CGI 프로그램의 처리: CGI 프로그램은 전달받은 데이터를 처리하고 결과를 표준 출력으로 웹 서버에 반환.
4) 웹 서버가 응답 전송: 웹 서버는 CGI 프로그램의 출력을 받아 HTTP 응답으로 클라이언트에 전달
3. 특징
- 언어 독립적: CGI 프로그램은 다양한 언어로 작성할 수 있음.
※ 브라우저가 JS를 실행해서 생성되는 동적 컨텐츠는 CGI가 아님.
- 프로세스 기반: 요청마다 독립적인 프로세스가 생성되어 실행됨. (요청이 몰리면 서버 자원 소모가 커짐)
- 동적 콘텐츠 생성: 사용자의 요청에 따라 실시간으로 결과를 생성함.
4. 한계
- CGI는 요청마다 별도의 프로세스를 생성하므로 대량의 트래픽이 발생하면 성능 저하 및 자원 낭비가 발생함.
- 이를 보완하기 위해 프로세스 재사용 및 스레드 기반의 대체 기술이 등장함.
특징 및 구조
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 프로그램을 실행한다.