목표: 클라이언트 연결을 받을 수 있는 기본 서버 구조 구현
int main(int argc, char **argv) {
int listenfd, connfd;
socklen_t clientlen;
struct sockaddr_storage clientaddr;
// 명령행 인수 검증
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);
doit(connfd); // HTTP 요청 처리
Close(connfd);
}
}
포인트:
Open_listenfd(): 지정된 포트에서 리스닝 소켓 생성, 클라이언트의 요청을 받아야하기 때문Accept(): 클라이언트 연결 수락개요: HTTP 요청을 파싱하고 처리하는 메인 로직
void doit(int fd) {
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char hostname[MAXLINE], port[MAXLINE], path[MAXLINE];
rio_t rio;
int serverfd;
// 1. RIO 구조체 초기화
Rio_readinitb(&rio, fd);
// 2. HTTP 요청 라인 읽기
if (!(Rio_readlineb(&rio, buf, MAXLINE))) {
return;
}
// 3. 요청 라인 파싱
sscanf(buf, "%s %s %s", method, uri, version);
// 4. GET/HEAD 메소드만 허용
if (strcasecmp(method, "GET") && strcasecmp(method, "HEAD")) {
clienterror(fd, method, "501", "Not implemented",
"Proxy does not implement this method");
return;
}
// 5. HTTP 헤더들 읽기
read_requesthdrs(&rio);
// 6. URI 파싱
if (parse_uri(uri, hostname, port, path) < 0) {
clienterror(fd, uri, "400", "Bad Request", "Invalid URI format");
return;
}
// 7. 원본 서버에 연결
serverfd = Open_clientfd(hostname, port);
if (serverfd < 0) {
clienterror(fd, hostname, "502", "Bad Gateway",
"Could not connect to origin server");
return;
}
// 8. 원본 서버로 요청 전달
forward_request(serverfd, method, path, version, &rio, hostname, port);
// 9. 원본 서버 응답을 클라이언트로 전달
forward_response(fd, serverfd);
// 10. 서버 연결 종료
Close(serverfd);
}
포인트:
Rio_readlineb(): 라인 단위로 HTTP 요청 읽기sscanf(): 요청 라인을 method, uri, version으로 파싱목표: URI를 hostname, port, path로 분리
int parse_uri(char *uri, char *hostname, char *port, char *path) {
char *ptr = uri;
// 1. "http://" 제거
if (strncmp(uri, "http://", 7) == 0) {
ptr += 7;
}
// 2. 경로 분리 (먼저)
char *path_ptr = strchr(ptr, '/');
if (path_ptr) {
strcpy(path, path_ptr);
*path_ptr = '\0';
} else {
strcpy(path, "/");
}
// 3. 포트 분리 (나중에)
char *port_ptr = strchr(ptr, ':');
if (port_ptr) {
strcpy(port, port_ptr + 1);
*port_ptr = '\0';
} else {
strcpy(port, "80");
}
// 4. 호스트명 복사
strcpy(hostname, ptr);
return 0;
}
포인트: 순서가 매우 중요
/home.html을 먼저 분리:8000을 나중에 분리처음엔 포트를 먼저 분리해버려서 Getaddrinfo error: Name or service not known 에러가 났다.
잘못된 순서의 예:
입력: http://localhost:8000/home.html
잘못된 순서: port=8000/home.html, path=/
올바른 순서: port=8000, path=/home.html
목표: 파싱된 정보를 바탕으로 원본 서버에 HTTP 요청 전송
void forward_request(int serverfd, char *method, char *path, char *version,
rio_t *rio, char *hostname, char *port) {
char buf[MAXLINE];
// 1. 요청 라인 생성 및 전송
sprintf(buf, "%s %s HTTP/%s\r\n", method, path, version);
Rio_writen(serverfd, buf, strlen(buf));
// 2. Host 헤더 생성 및 전송
sprintf(buf, "Host: %s:%s\r\n", hostname, port);
Rio_writen(serverfd, buf, strlen(buf));
// 3. Connection 헤더 생성 및 전송
sprintf(buf, "Connection: close\r\n");
Rio_writen(serverfd, buf, strlen(buf));
// 4. User-Agent 헤더 전송
Rio_writen(serverfd, user_agent_hdr, strlen(user_agent_hdr));
// 5. 헤더 끝 표시
Rio_writen(serverfd, "\r\n", 2);
}
핵심 포인트:
\r\n 포함\r\n 추가user_agent_hdr: 미리 정의된 User-Agent 문자열 사용목표: 원본 서버의 응답을 클라이언트에게 전달
void forward_response(int clientfd, int serverfd) {
rio_t rio;
char buf[MAXLINE];
int n;
Rio_readinitb(&rio, serverfd);
while ((n = Rio_readnb(&rio, buf, MAXLINE)) > 0) {
Rio_writen(clientfd, buf, n);
}
}
바이너리 파일 처리의 중요성:
// ❌ 텍스트 파일용 (바이너리 파일 실패)
while (Rio_readlineb(&rio, buf, MAXLINE) > 0) {
Rio_writen(clientfd, buf, strlen(buf)); // NULL 바이트에서 끊어짐
}
// ✅ 바이너리 파일용 (모든 파일 타입 처리)
while ((n = Rio_readnb(&rio, buf, MAXLINE)) > 0) {
Rio_writen(clientfd, buf, n); // 실제 읽은 바이트 수 사용
}
Open_listenfd()로 서버 소켓 생성Open_clientfd()로 원본 서버 연결Rio_writen(), Rio_readnb() 사용GET /path HTTP/1.1sscanf(), strchr(), strncmp()strcpy()*ptr = '\0'로 문자열 분리Rio_readlineb vs Rio_readnbstrlen() vs 실제 읽은 바이트 수문제: 포트에 경로가 포함됨
URI: http://localhost:8000/home.html
잘못된 결과: port=8000/home.html, path=/
해결: 파싱 순서 변경 (경로 → 포트 → 호스트명)
문제: 이미지 파일 전송 실패
Failure: Files differ. (godzilla.jpg, tiny)
해결: Rio_readnb() 사용으로 바이너리 안전 처리


어찌저찌 tiny까지는 책을 보면서 했지만, proxy.c를 작성하면서 벽에 막힌 기분이 들었다...
아직 동시성 처리와 캐시가 남았지만 이번 순차적처리를 통해서 다시 잘 작성해봐야겠다.
이제는 Pintos까지 진행하게 되면 더 어려워질텐데 아직 많이 부족한거 같아 걱정이 많지만 정글이 끝나더라도 꾸준히 노력을 한다면 실력이 더 늘지 않을까 생각한다ㅠ