벌써 9월이다
그것도 9월 첫째주 월요일
미국에서는 노동절이라 불리는 휴일이다
우리나라는 그런 거 없으니 바로 시작하겠다
심지어 노동도 아니고 학습이라 쉴거면 학습절에 쉬어야 한다
물론, 난 개인적으로 매주 일요일에 학습적 가지고 있다 ㅇㅇ
타이니 서버 정리할거다
tiny 서버라는 이름이 있기 보단 그냥 기초중의 기초 기능만 넣은 작은 서버라는 뜻이다
단일 프로세스를 반복하는 간단하고 구린 서버다
main -> Accept -> doit -> (serve_static 또는 serve_dynamic) -> Close
기본 흐름은 이렇다고 볼 수 있다
서버 시작 하면 main
함수에서 포트 번호 받아 해당 포트에 리스닝 소켓을 연다
(Open_listenfd
호출) 이후 서버 시작
main
은 무한 루프를 돌며 클라이언트의 연결 요청을 기다리다 Accept
함수를 통해 새 클라이언트 연결 시 (connfd
획득)
클라이언트의 호스트이름과 포트정보를 로그로 출력
연결 이후 main
은 doit(connfd)
함수를 호출해 HTTP 요청을 처리한다
doit
함수는 요청을 읽고 해석 후, 정적 파일 요청인지 동적 콘텐츠(cgi-bin만 지금은 다룬다) 요청인지 판단 후 각각에 걸맞는 처리 함수를 호출한다
정적 콘텐츠의 경우 serve_static
함수를 통해 요청된 파일을 읽고 클라이언트에 전송한다
동적 콘텐츠의 경우 serve_dynamic
함수를 통해 새로운 프로세스를 fork
하여 CGI 프로그램 실행 후 그 출력을 클라이언트에 보낸다
doit
이 이렇게 끝나면 main
으로 복귀후 연결 소켓을 닫고(Close
호출) 다음 요청을 대기한다
여러 함수들 쓰이는 거 다 정리해 놓을 생각이다
딱 기본만 하는 놈이다
코드:
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
}
}
처음에 포트 번호 받아 에러 아니면 리스닝 소켓을 열어(Open_listenfd
) HTTP연결 대기한다
이후 무한 루프를 돌며 Accept
로 연결 수락하고 Getnameinfo
로 클라이언트 주소 정보 출력한다
그리고 doit()
호출해 요청 처리후 close()
로 닫아준다
개구린 기초 서버라 한번에 하나씩 처리하고 바로 닫아버린다
HTTP 트랜잭션 해주는 핵심 함수다
ㄹㅇ 핵심이라 하는 것도 많다
요청 라인 읽기:
Rio_readinitb
로 내부 버퍼를 초기화하고, Rio_readlineb
를 사용해
요청 라인의 한 줄(메서드, URI, 버전)을 읽는다
이후 읽은 거 printf
로 출력 후 sscanf
로 각 필드(HTTP 메서드, URI, 버전)로 분해한다
매서드 검증:
현재 서버는 구려서 GET
외의 메서드일시 오류 실행한다 clienterror
요청 헤더 읽기/무시:
지원안하는 메서드일 경우 read_requesthdrs 함수를 호출해 나머지 요청 헤더들을 읽어 버퍼를 비운다
URI해석
parse_uri
함수를 호출해 요청 URI를 파일 경로와 CGI 인자 문자열로 분리한다
이후 정적 콘텐츠인지 동적인지 판별한다
파일 상태 검사
stat
으로 filename
정보 가져온다
filename
의 파일 상태 정보를 확인해 존재 안할시 clieterror
호출
정적/동적 분기 처리
정적 콘텐츠:
sbuf
1로 가져온 파일 정보에서 일반인지 읽기 권한 있는지 확인 (S_ISREG
및 읽기 비트)
만족 여부에 따라 오류 혹은, 정적 파일을 클라이언트에 전송
(serve_static(fd, filename, sbuf.st_size)
호출)
동적 콘텐츠:
일반 파일이며 실행 권한이 있는지 (S_ISREG
및 실행 비트) 확인
만족 여부에 따라 오류 혹은, 동적 콘텐츠 실행해 결과를 클라이언트에 전송
(serve_dynamic(fd, filename, cgiargs)
호출)
함수 종료
콘텐츠 제공 완료시 doit
반환하고 main
루프로 돌아가 다음 연결 처리한다
코드:
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;
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);
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) {
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read this file");
return;
}
serve_static(fd, filename, sbuf.st_size);
}
else {
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);
}
}
포트 번호 문자열 받으면 그거 갖다가 리스닝 소켓 열고 파일 디스크립터 반환해주는 헬퍼 함수다
open_listenfd(port)
getaddrinfo
setsockopt(SO_REUSEADDR)
bind
listen
listenfd
accept
부른다
소켓하고 주소 정보 버퍼 포인터(선택 사항), 버퍼 크기 포인터(선택 사항) 넣으면 연결 전용 소켓 만들어 준다
Accept(listenfd, 클라이언트 주소, 클라이언트 길이)
형태로 사용하면 연결 전용 소켓 뱉어줘서
이를 connfd
로 저장해 사용한다
소켓 주소와 크기를 주고 host와 server의 버퍼랑 길이 주면 이름 채워서 돌려준다
Open하고 반대된다
닫는다는 뜻이다 ㅇㅇ
그냥 닫을 연결 소켓 넣어주면 끝이다
Robust I/O 패키지 애들로 찾아보면 더 자세히 나온다
여기에서 사용된건 3가지다
Rio_readinitb
:
Robust I/O용 내부 버퍼를 초기화하는 함수다
파일디스크립터랑 구조체 연결하고 초기화해 준비한다
Rio_readlineb
파일 디스크립터
, 버퍼
, 최대 길이
입력 하면 최대 최대 길이-1
만큼 한줄 읽어 버퍼에 저장
버퍼 끝엔 항상 NULL 문자(\0
) 추가해준다
최대길이-1
만큼 읽거나 EOF(파일 끝)에 도달하거나 개행문자 \n
을 읽으면 종료한다
Rio_readnb
파일 디스크립터
, 버퍼
, 길이
입력하면 최대 길이
만큼 읽어 버퍼에 저장
길이
만큼 읽거나 EOF에 도달시 종료
정적 파일 전송 과정에 사용되는 보조 함수들이다
Open / Mmap / Munmap
Open(filename, O_RDONLY, 0)
으로 filename 경로의 파일 열어 줄 수 있다
실제 open
함수의 래퍼 함수다
// 원형
int open(const char *pathname, int flags, mode_t mode);
O_RDONLY
는 flags
인자에 들어가는 열기 모드(open flags)다
O_RDONLY
→ 읽기 전용으로 열기
O_WRONLY
→ 쓰기 전용으로 열기
O_RDWR
→ 읽기/쓰기 모드
세 번째 인자 mode
는 파일 생성 시 권한과 관련 있는데
flags
에 O_CREAT
없으면 무시해서 걍 0으로 둔다
Mmap(addr, len, prot, flags, fd, offset)
은 메모리에 매핑하기 위한 함수다
fd
(열린 파일 디스크립터)의 내용을 프로세스 메모리에 붙이고
그 시작 주소를 포인터로 반환한다
교재에서는 srcfd
(파일 디스크립터) 넣으면 srcp
(시작 주소 포인터)로 받는 식으로 구현되어있다
Tiny 서버 예제에는 인자들 이렇게 들어간다
addr = 0
: 커널이 알아서 매핑 위치 정함
len = filesize
: 파일 크기만큼 매핑
prot = PROT_READ
: 읽기 전용 권한
flags = MAP_PRIVATE
: 사적 매핑 (쓰기 시 원본 파일에 반영 X)
fd = srcfd
: 매핑할 파일 디스크립터
offset = 0
: 파일 처음부터 매핑
Mmap
으로 매핑한 가상 메모리 영역 해제한다
Munmap(srcp, filesize)
는 srcp
주소로부터 filesize
만큼 영역 해제 한다
동적 콘텐츠 처리에 사용되는 프로세스 제어 관련 함수들이다
Fork / Execve / Dup2 / Wait
익숙한 놈들이 여럿 보인다
fork
다 현재 프로세스 복제해서
자식 프로세스 생성 후 에러 시 메시지 출력하는 거 한다
실패시 -1
반환하고
자식 프로세스에는 0
, 부모 프로세스에는 자식의 PID2 반환한다
execve
다
현재 프로세스 코드/데이터/스택을 통째로 교체하여 filename
프로그램을 실행한다
이를 통해 자식 프로세스를 CGI프로그램으로 바꿀 수 있다
실패할 경우에만 값(-1
)을 반환한다
int dup2(int oldfd, int newfd);
oldfd
에 복사하고 싶은 거 넣고 newfd
에 당할 거 넣으면
newfd
로 하여금 oldfd
와 같은 파일/소켓을 가리키도록 복제한다
실패시 -1
반환하고 성공시 newfd
반환한다
pid_t wait(int *status); // 자식 프로세스 종료 상태 저장용 int
부모 프로세스가 호출하면, 자식 프로세스가 종료될 때까지 대기
그리고 종료된 자식의 PID를 반환하고 status
에 종료 상태 기록한다
정적 콘텐츠는 파일 시스템에 저장된 파일(예: HTML, 이미지 파일 등)을 그대로 클라이언트에게 전송하는 것이다
요청 확인:
doit
에서 parse_uri
호출로 is_static == 1
이면
정적 파일이란 뜻
filename
변수에는 ./<uri 경로>
형태의 실제 파일 경로가 세팅된다
/foo/bar.html
-> 파일 경로 ./foo/bar.html
)파일 존재 및 권한 확인:
stat(filename, &sbuf)
호출로 파일의 상태 확인
존재하지 않을 시 404오류 반환후 종료
존재할 시 S_ISREG(sbuf.st_mode)
매크로로 정규 파일인지, 그리고 S_IRUSR & sbuf.st_mode
로 읽기 권한이 있는지 검사
파일이 디렉토리거나 권한 없으면 403오류 후 종료
HTTP 응답 헤더 생성:
드디어 serve_static
함수로 진입한다
get_filetype(filename, filetype)
을 호출해 파일 확장자에 따른 MIME 타입 문자열을 얻고 (예: .html
→text/html
)
이후 sprintf
로 buf
에 연이어 작성한다
헤더 작성된 건 Rio_writen
으로 클라이언트에 전송된다
보내지는 내용:
HTTP/1.0 200 OK 상태, Server: Tiny Web Server, Connection: close, Content-length: [파일크기], Content-type: [파일 MIME]
파일 내용 전송:
파일 전송을 위해 Open(filename, O_RDONLY, 0)
로 파일 열고
filesize
만큼 메모리에 Mmap
로 매핑한다
그 뒤 close(srcfd)
로 닫고 매핑된 주소는 Rio_writen
로 소켓에 써서 전송한다
전송완료후 Munmap
으로 매핑 해제한다
로그 및 종료:
응답 헤더를 printf
로 출력해 로그 남긴다
파일 전송 완료시 server_static
이 리턴되어 다시 doit
으로 돌아간다
정적 요청 처리 완료
요청 확인:
doit
에서 parse_uri
호출로 is_static == 0
이면
동적 파일이란 뜻
filename
에는 실행할 CGI 프로그램 경로가 세팅된다
./cgi-bin/adder
)cgiargs
에는 URI의 '?'
뒷부분, 즉 프로그램에 전달할 인자 문자열이 설정된다
/cgi-bin/adder?15000&213
이면 cgiargs="15000&213"
)파일 존재 및 권한 확인:
stat
으로 파일 정보를 가져와 존재하지 않으면 404 오류 반환
존재한다면 S_ISREG
로 일반 파일 여부 확인하고, S_IXUSR & sbuf.st_mode
로 실행 권한이 있는지 검사
실행 파일 아니거나 권한 없으면 403 오류 응답 후 종료
초기 응답 헤더 전송:
serve_dynamic
드디어 호출
상태코드의 응답줄과 Server
헤더를 간단히 생성해 클라이언트로 보낸다
CGI 프로그램 실행 준비:
Fork()
를 호출해 새 프로세스를 실행한다
자식 프로세스에서 수행할 작업과 부모 프로세스의 처리가 분리
자식 프로세스:
Fork
의 반환값이 0인 경로로 들어와 CGI실행
먼저 환경변수 QUERY_STRING
을 설정하여, 프로그램이 입력 인자를 환경변수를 통해 사용할 수 있게 한다
그 다음 Dup2(fd, STDOUT_FILENO)
를 호출해 자식 프로세스의 표준 출력(STDOUT_FILENO
)을 클라이언트와의 소켓 fd
로 연결
부모 프로세스:
Fork
의 반환값이 자식 PID2로 부모는 곧바로 Wait(NULL)
을 호출하여 자식이 종료될 때까지 기다린다
그동안 블록된 상태로 대기하다가 자식이 종료시 Wait
에서 돌아와 serve_dynamic
함수를 마무리하고 리턴
CGI 출력 전송 및 완료:
자식 프로세스에서 실행된 CGI 프로그램의 출력결과는 클라이언트로 바로 전송
이를 통해 CGI 프로그램의 응답을 클라이언트는 받을 수 있음
이후, Tiny 서버는 연결 닫고 다음 요청 처리 준비한다