08:08입실
뮤텍스, 세마포어 코드 간단하게 짜보기
tiny 서버 만들기
네크워크 과정 OSI에 빗대어서 설명해보기
(브라우저에 google.com을 입력했을 때 일어나는 일 단계적으로 쪼개서 설명)
상호 배제(Mutual Exclusion)
하나의 자원(임계영역)에 접근할 때 프로세스/쓰레드 간의 중복 접근을 막아준다.
마치 자물쇠를 채우는 것과 같다.
자물쇠는 채운 프로세스/쓰레드만 자물쇠는 해제할 수 있다.
즉, 임계영역에 들어간 프로세스/쓰레드는 자물쇠를 채워서 다른 프로세스/쓰레드 못 들어오게 한다.
핵심 기능은 락, 언락
힘계영역은 공유변수이다.
뮤텍스는 상호배제 기능을 제공하는 것!
#include <stdio.h>
#include <pthread.h> /* pthread는 posix thread의 약자 */
pthread_mutex_t mutex;
int shared_variable = 0;
void *thread_function(void *arg)
{
pthread_t tid = pthread_self();
for (int i = 0; i < 1000000; i++)
{
pthread_mutex_lock(&mutex);
printf("Thread ID: %ld, shared_variable before: %d\n", tid, shared_variable);
shared_variable++;
printf("Thread ID: %ld, shared_variable after: %d\n", tid, shared_variable);
pthread_mutex_unlock(&mutex);
}
return NULL;
}
int main()
{
pthread_mutex_init(&mutex, NULL);
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread_function, NULL);
pthread_create(&thread2, NULL, thread_function, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
pthread_mutex_destroy(&mutex);
printf("공유 변수의 최종 값: %d\n", shared_variable);
return 0;
}
이 코드로 쓰레드 스위칭을 시각적으로 볼 수 있음!
뮤텍스 적용 시
뮤텍스 미적용 시
세마포어는 차단기, 신호기라는 의미가 있음.
s는 접근 가능한 공유 자원 수,
P연산은 세마포어 값 감소(프로세스/스레드가 임계영역 들어갈 때 연산)
V연산은 세마포어 값 증가(프로세스/스레드가 임계영역 나올 때 연산)
만약 s값이 0이하이면 접근 가능한 공유 자원이 없으므로 해당 프로세스/스레드는 대기
#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
const char *messages[] = {"Hello", "World"};
int message_index = 0;
sem_t semaphore;
void *thread_function(void *arg)
{
for (int i = 0; i < 5; i++)
{
sem_wait(&semaphore);
printf("%s ", messages[message_index]);
fflush(stdout);
message_index = 1 - message_index;
sem_post(&semaphore);
}
return NULL;
}
int main()
{
sem_init(&semaphore, 0, 1);
pthread_t thread1, thread2;
pthread_create(&thread1, NULL, thread_function, NULL);
pthread_create(&thread2, NULL, thread_function, NULL);
pthread_join(thread1, NULL);
pthread_join(thread2, NULL);
sem_destroy(&semaphore);
printf("\n");
return 0;
}
두 개의 스레드가 임계영역 입구에서 대기하다가, 직전 쓰레드가 임계영역 나오면서 변수를 다시 올려놓으면 대기 중인 스레드가 임계영역에 들어가는 것.
즉, 세마포어는 스레드 간의 순서를 지정할 수 있음.
unix: 단일 운영체제로 시작했으나, 현재는 unix 운영체제 계열을 의미
linux: GNU/Linux, unix코드를 재활용하지 않은 유사 유닉스 운영체제 계열
posix: IEEE에서 강제한 운영체제 표준
sus: The Open Group관리하는 unix 계열 운영체제 인증 표준
프로세스 생성에 사용되는 헤더가 pthread.h
여기서 p는 posix를 의미함. posix 궁금해서 간단히 찾아 봄!
IP주소는 논리적인 주소이다.
물리적인 주소는 MAC주소이다.
즉, IP주소는 특정 호스트 또는
특정 네트워크를 찾아가는 것
ARP패킷은 브로드캐스트로 가지만 응답은 유니캐스트로 한다.
특정 네트워크 도달 후 ARP를 통해 목적 호스트의 MAC을 알아내어 통신하는 것
만약, IP가 네트워크가 아니라 단일 호스트에 매핑되어 있다면 MAC를 바로 알아낼 수 있어서 ARP가 필요없다.
ARP는 동일 네트워크 간에만 일어나는게 아니라,
라우터와 라우터 간, 즉 서로 다른 네트워크 사이에서도 일어난다.
라우터도 결국 ip와 mac이 부여된 호스트이다!
무조건 DNS에 요청을 보내는 게 아니다.
캐시에 없으면 DNS로 쿼리를 보낸다.
5. Root DNS
6. Top level DNS
7. Second level DNS
8. Third level DNS
(... 여기 단계가 좀 많이 생략..?)
(TCP/IP 가정) 연결이 시작된다. 3way handshake
9. 클라이언트에서 서버에 요청 SYN
10. 서버에서 SYN + ACK
11. ACK
과정이 많이 생략된 거 같은데..
다시 보충하기!
참고영상 https://www.youtube.com/watch?v=0Oqkw8wVY_c
IP주소에 대해서 알았음!
왜 클래스를 나누는지, 서브넷이란 무엇인지, 그리고 서브넷 마스크가 무엇인지!
IP도 결국 32비트 0과 1의 조합이다.
이걸 쉽게 나타내기 위해서 8비트씩 4자리로 끊어서 10진수로 나타낸 것
IP는 네트워크 아이디와 호스트 아이디로 나뉜다
네트워크 부분: 해당 컴퓨터가 소속된 네트워크에 배정된 이름. 네트워크 식별자로 쓰임.
호스트 부분: 해당 컴퓨터 한 대에 배정된 이름. 호스트 식별자로 사용.
원래는 클래스 단위로 네트워크마다 아이디 범위를 배정했다.
근데 이게 범위가 너무 커서 낭비되는 아이피가 많이 생기니까,
클래스 단위로 나누는게 아니라 서브넷으로 나눔.
각 클래스 서브넷 마스크는 다음과 같다.
그외에 서브넷 마스크는 다음과 같다.
아이피 주소에 서브넷 마스크를 비트 AND 연산을 하면 네트워크 주소를 얻을 수 있다.
(이거 마치 malloc에서 헤더값으로 길이와 할당여부 얻는 거랑 똑같음)
그래서 xxx.xxx.xxx.0은 서브넷 마스크니까 호스트에게 할당 불가.
xxx.xxx.xxx.255는 브로드캐스팅용이므로 호스트에게 할당 불가.
xxx.xxx.xxx.xxx/24라고 하면 앞의 24비트가 테트워크 부분이라는 말
즉 이건 C클래스
여기서 24는 서브넷 마스크로 사용된 1의 개수를 의미함.
(앞의 24비트가 1이라는 뜻)
기존 클래스 체계에서는 서브넷 마스크가 각각 /8, /16, /24로 고정되어 있는 것
왜 '마스크'라고 하나?
마스크는 무엇을 가릴 때 쓰는 것. 서브넷 마스크의 비트 AND연산을 통해 아이피 주소의 일정 부분을 가려서 네트워크 주소를 찾아낼 수 있어서 마스크라고 함.
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
}
}
80포트로 실행하려고 했더니 에러가 발생한다.
80포트는 관리자 권한으로 실행해야 한다.
클라이언트에서 curl로 접속 가능하다.
(임시로 소켓 fd를 출력하게 했음)
curl은 client URL이다. 이 명령어로 클라이언트에서 url로 요청을 보낼 수 있음.
다양한 http method로 요청을 보낼 수 있다.
HTTP 요청을 처리하는 함수(정적, 동적 컨텐츠)
/* $begin tinymain */
/*
* tiny.c - A simple, iterative HTTP/1.0 Web server that uses the
* GET method to serve static and dynamic content.
*
* Updated 11/2019 droh
* - Fixed sprintf() aliasing issue in serve_static(), and clienterror().
*/
#include "csapp.h"
void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_static(int fd, char *filename, int filesize);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum, char *shortmsg,
char *longmsg);
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
}
}
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")) /* 동일하면 0을 반환 */
{
clienterror(fd, filename, "501", "Not Implemented", "Tiny does not implement this method");
}
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)
{
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode))
{
clienterror(fd, filename, "403", "Forbidden", "Tiny couldn't read the file");
return;
}
}
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;
}
server_dynamic(fd, filename, cgiargs);
}
}
클라이언트에 에러를 응답하는 함수
void clienterror(int fd, char *cause, char *errnum, char *shortmsg, char *longmsg)
{
char buf[MAXLINE], body[MAXBUF];
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);
sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: text/hrml\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
Rio_writen(fd, buf, strlen(buf));
Rio_writen(fd, body, strlen(body));
}
sprintf는 서식 문자열과 서식 지정자로 문자열을 서식화해서 첫 번째 인자에 반환한다.
요청 헤더를 읽는다.
void read_requesthdrs(rio_t *rp)
{
char buf[MAXLINE];
Rio_readlineb(rp, buf, MAXLINE);
while (strcmp(buf, "\r\n"))
{
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
}
return;
}
uri에서 동적 파라미터를 파싱한다.
int parse_uri(char *uri, char *filename, char *cgiargs)
{
char *ptr;
if (!strstr(uri, "cgi-bin")) /* 정적 컨텐츠 요청 */
{
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
if (uri[strlen(uri) - 1 == '/'])
strcat(filename, "home.html");
return 1;
}
else
{
ptr = index(uri, '?');
if (ptr)
{
strcpy(cgiargs, ptr + 1);
*ptr = '\0';
}
else
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
return 0;
}
}
strstr() 특정 부분 문자열을 찾는 함수이다.
정적 컨텐츠를 클라이언트에게 서비스한다.
void serve_static(int fd, char *filename, int filesize)
{
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
get_filetype(filename, filetype);
sprintf(buf, "HTTP/1.0 200 OK\r\n");
sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
srpintf(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);
Rio_writen(fd, buf, strlen(buf));
printf("Response headers:\n");
printf("%s, buf");
srcfd = Open(filename, O_RDONLY, 0);
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
Close(srcfd);
Rio_writen(fd, srcp, filesize);
Munmap(srcp, filesize);
}
void get_filetype(char *filename, char *filetype)
{
if (strstr(filename, ".html"))
strcpy(filetype, "text/html");
else if (strstr(filename, ".gif"))
strcpy(filetype, "image/git");
else if (strstr(filename, ".png"))
strcpy(filetype, "image/png");
else if (strstr(filename, ".jpg"))
strcpy(filetype, "image/jpeg");
else
strcpy(filetype, "text/plain");
}
자식 프로세스를 생성해서 CGI로 동적 컨텐츠를 생성하고, 해당 흐름을 fd로 넘겨서 클라이언트로 보낸다.
void serve_dynamic(int fd, char *filename, char *cgiargs)
{
char buf[MAXLINE], *emptylist[] = {NULL};
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));
if (Fork() == 0)
{
setenv("QUERY_STRING", cgiargs, 1);
Dup2(fd, STDOUT_FILENO);
Execve(filename, emptylist, environ);
}
Wait(NULL);
}
소켓은 응용의 끝점이며 파일 식별자 형태로 제공된다.
소켓 인터페이스 안에는 소켓 식별자를 열고 닫기 위한 함수들이 있다.
클라이언트와 서버는 이 식별자들을 서로 읽고 쓰는 방식으로 통신한다.
네트워크도 큰 틀에서 파일을 읽고 쓰는 것과 같다.
(물론 네트워크는 데이터를 전송해야하는 과정이 추가된다.)
이유: Content-type 오타(html인데 hrml이라고 적음)
이 코드 때문에 아이피로만 접속하면 home.html을 전송하기는 하지만
무조건 주소 뒤에 home.html을 붙여버려서 다른 페이지 라우팅이 안 되고,
이미지 파일도 제대로 불러와지지 않는다.
대괄호 위치가 잘못되었다.
if (uri[strlen(uri) - 1] == '/')
strcat(filename, "home.html");
따옴표 위치가 잘못됨.
최종 결과
웹 서버를 직접 구현해보니까 웹 통신의 과정이 더 잘 느껴짐.
특히 flask, django, express같은 프레임워크의 흐름을 더 잘 이해할 수 있을 것 같음!
이 부분이 계속 루프를 도는 거 같은데 내일 해결해 봄!
TINY의 출력을 조사해서 여러분이 사용하는 브라우저의 HTTP 버전을 확인하라.
크롬 HTTP/1.1
아크 HTTP/1.1
TINY를 확장해서 MPG 비디오 파일을 처리하도록 하시오. 실제 브라우저를 사용해서 여러분의 결과를 체크하시오.
MPG대신 MP4를 처리함!
C코드
void get_filetype(char *filename, char *filetype)
{
if (strstr(filename, ".html"))
strcpy(filetype, "text/html");
else if (strstr(filename, ".gif"))
strcpy(filetype, "image/git");
else if (strstr(filename, ".png"))
strcpy(filetype, "image/png");
else if (strstr(filename, ".jpg"))
strcpy(filetype, "image/jpeg");
else if (strstr(filename, ".mp4"))
strcpy(filetype, "video/mp4");
else
strcpy(filetype, "text/plain");
}
HTML
<html>
<head><title>test</title></head>
<body>
<img align="middle" src="godzilla.gif">
Dave O'Hallaron
<video width="320" height="240" controls>
<source src="earth.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</body>
</html>
결과
근데 비디오 태그에 타입을 지정하기 때문에 서버에서 콘텐츠 타입이 잘못 넘어와도 비디오 렌더링에는 문제 없음. 하지만 HTTP 프로토콜을 준수하기 위해서는 파일 타입을 정확히 명시하는 게 좋음.
TINY를 확장해서 HTTP HEAD 메소드를 지원하도록 하라. TELNET을 웹 클라이언트로 사용해서 작업 결과를 체크하시오.
if (strcasecmp(method, "GET") && strcasecmp(method, "HEAD"))
{
clienterror(fd, filename, "501", "Not Implemented", "Tiny does not implement this method");
return;
}
// 중략
if (!strcasecmp(method, "GET"))
serve_static(fd, filename, sbuf.st_size);
else if (!strcasecmp(method, "HEAD"))
serve_static_header(fd, filename, sbuf.st_size);
// 중략
void serve_static_header(int fd, char *filename, int filesize)
{
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
get_filetype(filename, filetype);
sprintf(buf, "HTTP/1.1 200 OK\r\n");
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);
Rio_writen(fd, buf, strlen(buf));
printf("Response headers:\n");
printf("%s", buf);
}
헤더만 나오게 함.
팁) 백엔드 포트폴리오에서 게시글이 id가 url에 암호화 없이 나오면 탈락이다.
내일은 C로 구현한 서버로 정적 사이트로 프로젝트 만들어서 배포해보기!