공부하면서 헷갈린 용어/개념
소켓 식별자는 네트워크를 통한 데이터 송수신을 가능하게 하는 특정 연결을 식별하는데 사용되는 고유한 값이다
- 소켓?
: 네트워크 통신의 endpoint,
각 소켓은 운영 체제에 의해 제공되는 고유한 정수 값인파일 디스크립터
를 통해 식별된다.- 이 파일 디스크립터가 소켓의 식별자 역할을 함. -> 이 식별자를 사용해서 소켓에 데이터 보내고 받는 작업 수행.
listenfd
: 서버가 클라이언트로 들어오는 연결 요청을 기다리는 소켓(listening socket) 식별자.
: 서버가 시작되면 서버는 특정 포트에서 연결 요청을 듣기위해 소켓을 생성함
: socket()
으로 소켓 생성, bind()
로 소켓에 주소(포트 번호, IP) 할당, listen()
함수 호출하여 클라이언트 연결 요청 기다림.
connfd
: 서버가 클라이언트의 연결 요청을 accept하고 실제 데이터 통신을 위해 생성된 소켓의 식별자
: client로부터 연결 요청이 들어오면 서버는 accept()
함수를 호출하여 그 요청을 수락. accept()
함수는 새로운 연결 소켓을 생성하고 이 소켓을 위한 파일 디스크립터 반환.
: 이 연결 식별자를 통해 서버는 특정 클라이언트와 데이터를 송수신 -> 각각의 연결은 고유한 연결 식별자를 가지며 서버는 이를 통해 개별 클라이언트와의 통신을 관리함.
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 */
//.tiny 8000 처럼 argc 가 2개 입력되지 않았다면, 포트 번호가 전달되지 않은 것 -> 프로그램 사용 법 출력하고 프로그램 Exit
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
listenfd = Open_listenfd(argv[1]); //argv[1] 포트를 열어서 들어오는 연결 요청을 기다리는 리스닝 소켓 생성
//무한 반복하여 클라이언트의 연결 요청 처리
while (1) {
clientlen = sizeof(clientaddr); //클라이언트 주소 구조체의 크기 설정
//클라이언트의 연결 요청 수락, 통신을 위한 새로운 소켓 생성(connfd)
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 connfd 연결 종료
}
}
hostname, 포트 번호를 인자로 받아 클라이언트 요청이 들어올 때마다 새로운 연결 식별자(
connfd
) 만들어서doit()
함수 호출한다.
// -> connfd가 인자로 들어오게 됨
void doit(int fd) {
int is_static; //정적 콘텐츠인지 동적 컨텐츠인지 판별하는 변수
struct stat sbuf; //파일의 상태 정보를 저장할 구조체
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];// 클라이언트에게서 받은 요청(rio)로 채워지게 된다.
char filename[MAXLINE], cgiargs[MAXLINE]; // 파싱된 파일 이름과 CGI 인수를 저장할 배열들
rio_t rio; // Robust I/O 구조체
/*Read request line and headers*/
/*request 라인과 헤더를 읽음*/
Rio_readinitb(&rio, fd); //RIO 버퍼 초기화 - rio 버퍼와 서버의 connfd를 연결시켜준다.
// Rio_readlineb(&rio, buf, MAXLINE); //request 라인 읽음
if (!(Rio_readlineb(&rio, buf, MAXLINE))){
return;
}
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //request line 파싱 -> 메소드, URI, 버전 추출
//strcasecmp(): 대소문자를 구분하지 않고 스트링 비교
// 일치하면 0 return
//요청 메소드가 GET이 아닌 경우 -> 클라이언트에게 501 에러
/*숙제 11.11*/
// 요청 Method가 GET과 HEAD가 아니면 종료.
//main으로 가서 연결 닫고 다음 요청 기다림
// if (strcasecmp(method, "GET")) {
if (!(strcasecmp(method, "GET") == 0 || strcasecmp(method, "HEAD") == 0)) {
clienterror(fd, method, "501", "Not implemented",
"Tiny does not implement this method");
return;
}
//Request header 읽음
read_requesthdrs(&rio);
/*Parse URI from GET request, GET 요청에서 URI 파싱*/
is_static = parse_uri(uri, filename, cgiargs); //URI 파싱해서 정적/동적 콘텐츠 판별 - 정적(1), 동적(0)
//파일 상태 정보를 가져오는데 실패한 경우 => 클라이언트에게 404 에러
if (stat(filename, &sbuf) < 0) {
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
return;
}
/*Serve static content, 정적 콘텐츠 제공*/
if (is_static) {
/*파일이 일반 파일이 아니거나 읽기 권한이 없는 경우 -> 403 에러(웹 페이지를 볼 수있는 권한이 없음)*/
//S_ISREG(sbuf.st_mode): 파일 모드가 정규 파일을 가맄키는지 ->false 반환하면 정규 파일이 아님(디렉토리나 링크일 수 있음)
//S_IRUSR :소유자의 읽기 권한, sbuf.st_mode : 권한 비트 -> 해당 권한이 설정되었는지 검사
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read the file");
return;
}
serve_static(fd, filename, sbuf.st_size, method); //정적 콘텐츠 제공
}
/*Serve dynamic content 동적 콘텐츠 제공*/
else {
//파일이 일반 파일이 아니거나 소유자가 실행 권한을 갖고 있지 않은 경우 -> 403 에러
//S_IXUSR: 파일 소유자의 실행 권한
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, method); //동적 콘텐츠 제공
}
}
클라이언트로부터 요청을 받고, 해당 요청이 static or dynamic 콘텐츠인지 판단한 후 요청에 맞는 콘텐츠 제공한다.
strcasecmp()
: 대소문자를 구분하지 않고 스트링 비교하는 함수인데, 일치하면 0을 return한다.
Rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
:
: Rio 구조체와 연결된 네트워크 소켓으로부터 한 줄을 읽어 버퍼에 저장 -> 읽은 byte 수를 반환하며 파일의 끝(EOF)에 도달하거나 오류가 발생하면 0반환
- rio_t *rp: RIO 구조체의 포인터. 이 구조체는 파일이나 네트워크 소켓과 관련된 버퍼링된 I/O 작업을 위해 사용됨
- void *usrbuf: 읽은 데이터를 저장할 사용자 버퍼의 주소
- size_t maxlen: 버퍼의 최대 길이
if (!(Rio_readlineb(&rio, buf, MAXLINE))){
return;
}
-> 클라이언트로부터 연결이 닫히거나 읽을 데이터가 없을 때 무한 루프에 빠지지 않고 doit 함수를 즉시 종료시키게 Return -> 서버는 더 이상 처리할 데이터가 없을 때 불필요하게 리소스 소모하거나 오류 상태에서 계속 작업 시도하는 것 방지할 수 있을듯.
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg) {
char buf[MAXLINE], body[MAXBUF];
/* Build the HTTP response body, HTTP 응답 본문*/
sprintf(body, "<html><title>Tiny Error</title>");
sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause); //긴 메시지와 원인
sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);
/*Print the HTTP response*/
sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf)); //buf에 저장된 HTTP 헤더 정보를 fd를 통해 연결된 클라이언트에게 정확한 길이만큼 전송
sprintf(buf, "Content-type: text/html\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
//Rio_writen()으로 buf와 body를 서버 소켓(connfd)을 통해 클라이언트에게 전송
Rio_writen(fd, buf, strlen(buf));
Rio_writen(fd, body, strlen(body));
}
클라이언트에게 에러 메시지 전송 -> HTML 형식의 에러 페이지를 구성하여 에러 메시지와 body를 클라이언트에게 전송
sprintf()
함수: printf()와 유사하게 동작하지만 출력 결과를 화면에 표시하는 대신 지정된 문자 배열(buffer)에 저장
지정된 형식에 따라 다양한 데이터를 문자열로 변환하여 지정된 문자열 버퍼에 저장함
int sprintf(char *str, const char *format, ...);
void read_requesthdrs(rio_t *rp) {
char buf[MAXLINE];
Rio_readlineb(rp, buf, MAXLINE); //첫번째 헤더 라인 읽음
//strcmp(): 두 문자열 비교
//HTTP의 헤더 끝까지(루프를 통해 \r\n만 포함된 빈 줄을 만날 떄까지) 데이터 읽어옴
while(strcmp(buf, "\r\n")) {
Rio_readlineb(rp, buf, MAXLINE); // 다음 헤더 라인 한줄씩 읽고
printf("%s", buf); //출력
}
return;
}
HTTP request header를 끝까지 한줄씩 읽고 그냥 출력만 하고 있음 - 헤더 내의 어떤 정보도 사용하고 있지 않음 그냥 읽고 무시
현재는 요청의 본문을 처리하기 위해 헤더 부분을 넘어가기 위한 용도로만 사용되고 있음
int parse_uri(char *uri, char *filename, char *cgiargs) {
char *ptr;
/*Static content 정적 콘텐츠 처리*/
//strstr() : 첫번째 인자(uri)에서 두번쨰 인자("cgi-bin")의 첫 번째 발생을 찾아 그 위치의 포인터 반환
// 존재하지 않으면 NULL 반환 -> 동적 콘텐츠를 처리하는 CGI 프로그램과 관련이 없는 것 => 정적 콘텐츠 요청하는 것
if (!strstr(uri, "cgi-bin")) {
strcpy(cgiargs, ""); // 정적 콘텐츠 요청하는 경우 CGI 인자가 필요없으니 빈 문자열 복사
strcpy(filename, "."); //현재 디렉토리로 기본 경로 설정
strcat(filename, uri); //URI를 파일 이름에 추가
//URI가 /로 끝나면 기본 파일 이름을 home.html로 설정
//strlen(url)-1 : '\0'를 제외한 문자수 -> 마지막 문자가 /인지 확인
//uri가 디렉토리를 가리키는지 체크
if (uri[strlen(uri)-1] == '/') {
strcat(filename, "home.html"); //filename 문자열의 끝에 home.html 문자열 붙임
//웹 서버가 디렉토리에 대한 요청을 받았을 때 사용자에게 보여줄 기본적인 웹 페이지 설정
//strcat 함수 -> 두개의 문자열을 연결하는데 사용.
}
return 1;
}
/*Dynamic content 동적 콘텐츠 처리*/
else {
//index(): 주어진 문자열에서 특정 문자를 받아 그 위치의 포인터 반환
ptr = index(uri, '?');
// ?가 존재한다면 cgiargs를 ? 뒤 인자들과 값으로 채워주고 ?를 NULL(\0)로 대체
if (ptr) {
strcpy(cgiargs, ptr+1); //쿼리 스트링을 cgiargs로 복사
*ptr = '\0'; //? 위치를 널 문자로 대체 -> URI의 경로 부분만 남기기 위함
}
// '?'가 없다면 CGI 인자 비움
else
strcpy(cgiargs, "");
strcpy(filename, "."); //파일 이름의 기본 경로를 현재 디렉토리로 설정
strcat(filename, uri); // ? 이전 부분을 파일 이름에 추가
return 0;
}
}
URI를 분석하여 정적, 동적 콘텐츠 처리
URI에 따라 웹 서버가 제공해야 할 콘텐츠가 정적인지 동적인지 결정 -> 그에 따른 적절한 filename과 CGI 인자(cgiarg) 설정
정적 콘텐츠를 클라이언트에게 제공. 정적 콘텐츠는 클라이언트의 요청에 따라 변경되지 않고 그대로 전송됨.
void serve_static(int fd, char *filename, int filesize, char *method){
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
/*Send response headers to client*/
get_filetype(filename, filetype); //파일 이름을 바탕으로 파일의 MIME 타입 결정
sprintf(buf, "HTTP/1.0 200 OK\r\n"); // HTTP 응답 시작
sprintf(buf, "%sServer: Tiny Web Server\r\n", buf); //서버 정보
sprintf(buf, "%sConnection: close\r\n", buf); //연결 닫음
sprintf(buf, "%sContent-length: %d\r\n", buf, filesize); //콘텐츠 길이
sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype); //콘텐츠 타입
/*connfd를 통해 clinetfd에게, 응답라인과 헤더를 클라이언트에게 보냄.*/
Rio_writen(fd, buf, strlen(buf));
printf("Response headers: \n");
printf("%s", buf);
if (strcasecmp(method, "HEAD")==0) {
return;
}
/*Send response body to client*/
srcfd = Open(filename, O_RDONLY, 0); //요청받은 파일을 읽기 전용 모드(O_RDONLY)로 열기
srcp = (char *)Malloc(filesize); //파일 크기만큼 메모리를 동적 할당
Rio_readn(srcfd, srcp, filesize); //파일 내용을 읽어서 동적할당한 메모리에 값을 저장.
Close(srcfd); //파일 닫음
Rio_writen(fd, srcp, filesize); //해당 메모리에 있는 파일 내용들을 클라이언트에 보낸다.
free(srcp); //메모리 해제
}
동적 컨텐츠를 처리하기 위해 웹 서버에서 사용되는 함수
CGI 프로그램을 실행하고 그 출력을 클라이언트에게 직접 전송.
CGI 자식 프로세스를fork()
=> 자식 프로세스의 표준 출력을 서버 연결 식별자를 거쳐서 클라이언트에 출력됨
void serve_dynamic(int fd, char *filename, char *cgiargs, char* method) {
char buf[MAXLINE], *emptylist[] = {NULL};
/*Return first part of HTTP response*/
//HTTP 응답의 첫 부분을 클라이언트에게 반환 -> 200: 요청이 성공적으로 처리되었음
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
//서버 정보를 클라이언트에게 보냄
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));
/*Child*/
//자식 프로세스에서 실행되는 코드
if (Fork() == 0) {
/*Real server would set all CGI vars here*/
//CGI 프로그램에 전달될 QUERY_STRING 환경 변수 -> cgiargs로 설정
setenv("QUERY_STRING", cgiargs, 1);
//요청 메소드를 환경 변수에 추가
setenv("REQUEST_METHOD", method, 1);
Dup2(fd, STDOUT_FILENO); /*Redirect stdout to client,
CGI 프로세스의 표준 출력을 connfd에 복사 -> CGI 프로세스에서 표준 출력 하면 서버 연결 식별자를 거쳐 클라이언트에 출력됨*/
Execve(filename, emptylist, environ); /*Run CGI program* CGI 프로그램 실행 */
//filename 변수에는 실행할 CGI 프로그램의 경로가 저장되어 있음.
//emptylist는 CGI 프로그램으로 전달될 인자 목록, 여기서는 인자 없이 실행됨
}
/*Parent waits for and reaps child
부모는 자식 프로세스가 종료될 때까지 기다림, 자식 프로세스가 종료되면 시스템 자원 회수*/
Wait(NULL);
}
Fork()
: 현재 실행중인 프로세스(부모 프로세스)의 정확한 복사본 생성 - 자식 프로세스 생성
자식 프로세스는 부모 프로세스와 메모리 공간을 공유하지 않음. 자신만의 독립적인 메모리 공간을 가짐
부모 프로세스에게는 자식 프로세스의 PID가 반환, 자식 프로세스에서는 0이 반환.Fork() == 0
이 참이면 현재 자식 프로세스의 컨텍스트에서 실행되고 있음을 의미. 부모 프로세스에서는 이 조건이 거짓이 됨- 동적 콘텐츠를 제공하기 위해 CGI 프로그램을 별도의 프로세스로 실행하기 위해 자식 프로세스는 독립적인 환경에서 CGI 프로그램 실행 -> 그 출력을 클라이언트에게 전송. 웹 서버의 메인 프로세스는 다른 요청을 처리할 수 있음
Dup2()
: File descriptor를 지정된 파일 디스크립터 번호로 복사하는 함수
Derive file type from filename
파일 이름 기반으로 파일 유형(MIME 타입) 결정
filename: 파일 유형을 결정할 파일의 이름
filetype: 결정된 파일 유형을 저장할 문자열 주소
void get_filetype(char *filename, char *filetype) {
//파일 이름에 .html 확장자가 포함되어 있으면
// HTML 문서로 간주하고 MIME Type을 text/html로 설정
if (strstr(filename, ".html")) {
strcpy(filetype, "text/html");
}
//파일 이름에 .gif 확장자가 포함되어 있으면
//MIME Type - GIF 이미지로 간주
else if (strstr(filename, ".gif")) {
strcpy(filetype, "image/gif");
}
//파일 이름에 .png 확장자가 포함되어 있으면
//PNG 이미지로 간주 -> MIME type : "iamge/png"
else if (strstr(filename, ".png")) {
strcpy(filetype, "image/png");
}
//파일 이름에 .jpg 확장자가 포함되어 있으면
//JPEG 이미지로 간주 -> MIME type: "image/jpeg"
else if (strstr(filename, ".jpg")) {
strcpy(filetype, "image/jpeg");
}
else if (strstr(filename, ".mp4")) {
strcpy(filetype, "video/mp4");
}
//으로 "text/plain"으로 설정
else
strcpy(filetype, "text/plain");
}
HTTP HEAD Method
- GET 요청과 유사하지만 GET 요청은 실제 리소스의 내용까지 가져오지만, HEAD 요청은 리소스의 헤더 정보만 가져옴.
=> HEAD: 리소스의 내용(content)가 아니라 메타데이터(metadata)만을 요청한다.
웹 서버로부터 해당 리소스의 본문 데이터(body)는 전송받지 않고, 헤더 정보(Header)만을 받게 된다.
Last-Modified
, ETag
Last Modified
: 마지막으로 수정된 날짜와 시간 -> 버전의 리소스가 최신 버전인지 확인할 수 있음Etag(Entity Tag)
: 리소스의 특정 버전을 나타내는 식별자. Content-Length
-> 데이터의 양/크기 예측. 리소스의 크기를 바이트 단위로 나타냄. Content-Type
: 어떤 MIME 타입인지. 어떻게 처리할지 결정 가능=> HTTP HEAD 메소드는 네트워크 트래픽을 최소화하면서 필요한 데이터를 효과적으로 수집할 수 있음
(예시) HTTP HEAD 요청이 아닐 때만 content를 출력하도록
if (strcasecmp(method, "HEAD")!=0){ printf("%s", content); }
기존의 정적 컨텐츠 처리 코드 - 메모리 매핑 방식
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); Close(srcfd); Rio_writen(fd, srcp, filesize); Munmap(srcp, filesize);
Mmap()
: 특정 파일을 가상 주소에 매핑
- 프로세스는 파일을 처리하는게 아니라 메모리에 있는 데이터를 읽거나 쓰면 됨.
- 파일의 내용을 메모리 주소 공간에 직접 매핑.
- 파일 I/O 연산 없이 메모리에 직접 데이터를 읽어올 수 있음.
파일을 읽기 전용 모드로 엶.
-> 가상 메모리에 매핑 Mmap
-> Close(fd)
: 파일이 메모리에 매핑된 이후에는 fd가 더이상 필요하지 않음
-> 닫지 않으면 메모리 누수가 발생할 수있음
-> Rio_writen
: connfd를 통해 가상 메모리에 매핑된 파일 내용을 클라이언트에 보냄
-> Munmap()
: 파일 전송이 완료되면 더이상 메모리 매핑 불필요하니까 Unmap
malloc으로 대체한 코드 - 동적 할당
srcfd = Open(filename, O_RDONLY, 0); srcp = (char *)malloc(sizeof(filesize)); Rio_readn(srcfd, srcp, filesize); Close(srcfd); Rio_writen(fd, srcp, filesize); free(srcp);
파일을 읽기 전용 모드로 엶.
-> malloc()
을 사용해서 파일 크기만큼의 메모리를 동적 할당.
-> Rio_readn(srcfd, srcp, filesize)
: 파일의 내용을 읽어서 동적할당한 메모리에 파일 값 저장
-> Close(srcfd)
: 파일 닫음
-> Rio_writen
: 해당 메모리에 있는 파일 내용들을 클라이언트에 보냄
-> free
: 동적할당한 메모리 해제
- 메모리 매핑 방식
: 큰 파일을 처리할 경우 파일 전체를 메모리 매핑해야하기 때문에 사용 가능한 메모리 양에 제한을 받을 수 있따는 단점.
메모리 매핑 실패시의 에러 처리가 필요하다.
운영체제가 파일 캐싱을 활용해서 여러 프로세스에서 같은 파일을 사용할 경우 메모리 사용 최적화할 수 있음.
- 동적 할당 방식
: 필요한 메모리만 할당하여 사용하므로 작은 파일 다룰 때 더 효율적일 수 있다.
파일 i/o -> 데이터를 읽고 쓰기 때문에 메모리 매핑 방식에 비해 성능이 떨어질 수 있다.
파일의 내용을 메모리로 복사하는 과정에서 시간이 추가로 소요
=> 성능이 중요하고 처리해야할 파일의 크기가 크고 메모리 사용량에 여유가 있으면 메모리 매핑이,
작은 파일을 주로 다루는 경우 -> 동적 할당 방식이 더 적합할 수 있을 것이다.
!!