본문 바로가기

42cursus

webserv 하루 일기

1일차 (21 / 6 / 7)

- select 함수의 사용법 익힘
- 간단한 에코서버 구현
- writefds의 이용 -> 변수를 이용하여 제어

 

2일차 (21 / 6 / 8)

- conf파싱 -> request파싱 -> response 하나씩 구현하기로 함

- conf, server, location에 대한 대략적인 구조 논의
-> 일단 만들기, 유효성에 대해서는 스스로 정하기

- conf파싱, request파싱, 기본구조에 대한 분업 협의
-> conf(heryu), request(hyeonkim), 기본구조(mijeong)

- git repo 파기

 

3일차 (21 / 6 / 9)

- 기본적인 파싱 완성
-> 리펙토링, Valid검사, 변수설정 필요

- valid하지 않으면 무조건 예외던짐

 

4일차 (21 / 6 / 10)

- config parsing 리펙토링함
-> config class에서 모두 처리하기로 함
-> 그 대신 Server, Location class에서 friend 걸어줌

- Location에 데이터를 직접 넣음

- config파일의 구현 범위 정리

더보기

1. 기본 규격

server {

key values ;

location PATH {

key values ;

}

}

servers {
host
port
server_name
error_page


locations / {
methods
root
auto_index
index
cgi_extension
cgi_path_info
return
*upload_enable
#del#upload_dir

*client_body_size
}
}

 

2. 괄호 쌍이 맞지 않으면 invalid

-> location의 괄호인 경우는 undefined

 

3. "server"가 무조건 와야 함

-> ser@ver{...}, server@{...} 는 모두 invalid

-> server {...}\n 123 server{...}같은 경우도 invalid

 

4. "location" 이어야 함

-> locati@on, location@ 같은 경우 유효하지 않음 -> 정확히는 undefined 임

 

5. location 이후에 path가 없으면 invalid

-> 있는데 공백이 포함되면 에러

 

6. key value; 이어야 함

-> data; 같은 경우 (띄어쓰기가 없는 경우) 에러임

-> 세미콜론 없으면 undefined

 

7. 기본 규격하에 공백은 어디에 넣어도 상관없음

-> server{key value; location PATH {key value;}} 가능

-> server { key value ;location PATH { key value; } }

 

8. 주석(#)뒤 줄은 무조건 무시


9. 키가 중복되면 undefined

-> 나중에 작성 된(뒤에 있는) key value가 유효함

 

10. Location에 유효하지 않은 key가 있는 경우 invalid

 

11. Server에 유효하지 않은 key가 있는 경우 invalid

 

12. Server가 없는 경우 (개수 0) invalid

 

13. Server들의 포트가 중복된 경우 invalid

 

14. Server들의 호스트가 중복된 경우 invalid

 

15. Server들의 서버네임이 중복된 경우 invalid

 

16. 한 서버의 Location의 경로가 중복된 경우 invalid

 

17. 한 서버의 Location이 없는 경우 (개수 0) invalid

 

18. 나머지 value가 이상하다면 *undefined*

 

19. 나머지 value가 이상하다면 *undefined*

 

20. 나머지 value가 이상하다면 *undefined*

 

 

5일차 (21 / 6 / 11)

- config 완성 (구현 범위, valid 범위)
-> 부족한 면은 그때그때 수정하고 채워나가면 됨

- Server, Config를 병합함
-> trim, iter의 문제는 수정함

- Request는 good path정도는 완성된 듯

 

6일차 (21 / 6 / 14)

- 진행이 더디기 시작
- 무엇을 모르는지 모르는 상태 (개념 부족)
- Request가 GET으로 들어왔을 때부터 처리하기로 함
-> RFC, HTTP가이드, MDN을 먼저 살펴보기로 함

 

7일차 (21 / 6 / 15)

- 무엇을 해야하는지 어느정도 정리됨 ㅡ 나이스
- read와 request 타이밍에 문제가 있어서 토의함
- response는 Location찾고 절대경로 얻는 것 까지만 완성
- cgi 진행전에 코드 수정할 것 하기로 함
- 상태코드는 사용할 것만 정리하고 특정 타이밍에 예외를 던져서 처리하기로 함
- POST와 PUT의 차이는 덮어쓰냐 아님 추가하냐의 차이, 웬지 그에 따른 응답코드도 다를 것 같음

 

 

8일차 (21 / 6 / 16)

- GET의 기본구현은 끝남
- socket클래스의 bool변수의 문제가 있었는데 수정함
- 에러코드 구문은 map으로 처리함
-> request마다 map에서 데이터를 만드므로 비효율적이나 테스터 돌리기전까지는 수정하지 않음
- 기본틀에서 response스위치 켜기 전까지의 과정에서 고생이 심함
- autoindex는 a tag의 href를 request uri로 처리
-> request를 매개변수로 받을지, 아니면 request를 response 멤버로 가질지 고민해야 함
- CGI의 fd도 결국 select에 거쳐야 함에 조심
- location에 /를 무조건 달아줌
- uri와 location path의 비교는 uri에도 /를 달아서 비교하게 함
-> 왜인지 모르겠는데 uri /a일 때 location /a를 찾는 문제가 있었음, 집에서 했을때는 찾지 못하긴 했음

더보기

1. Config.cpp::_getLocationPath에서 location PATH를 무조건 /를 붙여도 되는가?

-> root와 mapping 되는 것으로 설정하기에 dir로 판단해도 무방함

 

2. Response.cpp::_getMatchingLocation에서 uri를 잠시 /를 붙여도 되는가?

-> 잠시 붙이는 것이므로 이후에 영향을 미치지 않아서 문제없음

-> 또한, location을 mapping 하는 과정이므로 문제없음

-> uri /a 와 location /a/ 는 매칭되어야 함

 

3. Response.cpp::_setBodyFromDir에서 path에 /를 잠시 붙여도 되는가?

-> 잠시 붙이는 것이므로 이후에 영향을 미치지 않아서 문제없음

-> 어차피 인자로 받는 path가 디렉터리 경로이므로 /를 붙이든 말든 상관없음

-> opendir도 마찬가지

 

4. Response.cpp::_setBodyFromAutoIndex에서 requestURI에 /를 붙여도 되는가?

-> autoindex에서 새로운 요청을 위해 없으면 "/"를 붙여줘야 함

 

 

9일차 (21 / 6 / 17)

- cgi를 제외하고 기본구현 끝남
- 모든 코드 수정필요 (최적화, 재활용)
  -> 특히 response에서 string 덧붙이기에 재활용 필요
  -> 또한 string을 반환하냐 string&를 반환하냐의 차이도 고민해야 함
- POST, PUT, DELETE의 구현 범위 정리

더보기

DELETE

- 해당 요청이 유효한 directory라면 indexpage찾아 삭제, 없으면 404

- 해당 요청이 유효한 파일이라면 삭제

- 유효하지 않은 uri라면 404

 

POST (append), PUT (truncate)

- 클라이언트 바디 사이즈 확인, 넘으면 413

- 허용된 업로드 Location확인, 아니면 403

- 해당 요청이 유효한 directory일 때

  -> config에 index자체가 없다면 500

  -> config에 index가 있는데, indexpage를 app(OR)trunc (없으면 생성(201), 있으면 새파일로(200))

- 해당 요청이 유효한 파일이면 app(OR)trunc  (200)

- 해당 요청이 없는 리소스일 때 (ex. /a/b/c)

  -> "/"로 끝나면 directory 요청이라고 간주하고 500

  -> 그렇지 않다면 파일이라고 간주함

     -> 그 중에 상위 디렉터리가 있다면 만들어주고 201 (ex. /a/b/가 있다면)

     -> 상위 디렉터리도 없다면 404

 

 

10일차 (21 / 6 / 18)

- CGI를 어떻게 하는지 공부해오기로 함

- siege -b 테스트는 config파일이나 옵션으로 connection = keep - alive를 해줘야 함

 -> 앵무새 보면 통과한다고 함

- 중간 점검느낌으로 하루 버림

 

 

11일차 (21 / 6 / 21)

- CGI를 학습하고 따라함

 -> cgi에서 필요한 값들은 env에 넣어줘야 함 (하나씩 해봄, 슬랙 & IBM)

 -> get메소드일 때, 쿼리를 env에 넣어줌

 -> post메소드일 때, 쿼리를 env에 넣는게 아닌 바디에 넣음

- 서브젝트에는 어떠한 env가 필요한지에 대해서 설명되어있지 않아 그냥 하나씩 넣고 테스트해봄

- 일반 파이프로 하면 무한 블락상태가 되어서, 자식프로세스(cgi)의 결과를 파일에 출력하게 함

  -> 자식프로세스 입장 : stdin->pipe, stdout->file

  -> 부모프로세스 입장: 그대로, 쿼리스트링을 body에 넣어줌

 

 

12일차 (21 / 6 / 22)

- 앵무새를 드디어 봄

- 어떻게 어떻게 최적화하여 57 -> 17분까지는 줄임

- cgi스폐셜헤더가 있길래 하드하게 넣어줌

- 이제 코드수정 및 select를 거치게 해야함

- 스트링 재할당 및 O(N^2)에 문제가 많았음

 

 

13일차 (21 / 6 / 23)

- 팀원분들이 read를 최적화를 더함

- iterator를 레퍼런스로 받으로 받아서 다른 문제들도 해결함

- cgi에서 index페이지 찾는거와 error페이지를 찾게끔 코드를 수정함

- 남은 것은 select, 코드수정...

 

 

14일차 (21 / 6 / 24)

- select를 어떻게 거쳐갈지에 대한 고민만을 함

 -> 최대한 있는 것을 활용하려 함

 -> 일단 비효율적이지만 select를 거치는 방법에 중점으로 생각

- 1 -> 2 -> 3 -> 4 -> 5 인 순차로직 중에

 -> 4함수에서 1을 다시 거치거나, 5를 그대로 진행한다면

 -> 이벤트플래그같은 것을 이용하기로 함

 -> 현재 상태체크를 위한 FDManager를 싱글톤 구현하기로 함

 

 

15일차 (21 / 6 / 25)

- select를 거치고 나서 생긴 문제들 (모두 해결)

 -> Response* 값이 벡터 재할당중에 변경되는 문제

 -> Response의 상태가 아닌 Socket의 상태로 변경함

 -> vsc가 아닌 일반 터미널로 실행하는 경우 fd 최댓값이 256이라서 128테스트를 하지 못하는 문제

 -> vsc로 실행 함 (혹은 OPEN_MAX값을 변경해야 함)

 -> Response의 복사생성자 문제

 -> 모든 복사생성자나 기본생성자는 얕은 복사만 하는 경우 그냥 암시적함수를 사용하거나, 하나하나 다 적어줘야 함

 

- 일반적인 구현은 끝남

 -> siege -b

 -> fd leaks 클리어

 

 

16일차 (21/ 6 / 28)

- FDManager의 getResult를 변경함

 -> 원래는 fd에 해당하는 socket fd를 찾아서 result를 리턴했으나, 그냥 매개변수로 fd가 아닌 socket으로 받아서 사용

 -> fd는 모두가 close이후에 변경이 가능하지만, socket은 그렇지 않기 때문

 

- 헤더파일에 선언되어있는 소스코드를 수정함

 

- signal 달아서 fd를 모두 close하게 함

 -> open을 체크하기 위해 dup을 사용해서 -1를 체크함

 

- 남은 것

 -> 1. 평가페이지대로 따라해보기

 -> 2. 예외처리

 -> 3. 계속해서 유지보수 (코드 수정)

 

 

 

 

 

 

==============================================

Socket 프로그래밍을 위해 아주 간단한 정리
-> 웹서버를 만들 때 사용, 기본적인 로직이므로 알아 둘 것


int socket(int domain, int type, int protocol)
>>> 소켓 생성함수 (fd하나 팜 -> 이것을 bind, listen, accept에서 사용할 것)

- domain: 프로토콜 family 지정(ex. IPv4: PF_INET)
- type: SOCK_STREAM(TCP), SOCK_DGRAM(UDP)
- protocol: 0, IPPROTO_TCP(TCP 일때), IPPROTO_UDP(UDP 일때)
-> 0 to select the system's default for the given combination of family and type
- return: 소켓의 fd, 실패시 -1

_listenSocket = socket(PF_INET, SOCK_STREAM, 0);

 

struct sockaddr_in {

__uint8_t sin_len;
sa_family_t sin_family; // 8bit -> AF_INET (IPv4)
in_port_t sin_port; // unsigned short -> port number
struct in_addr sin_addr; // unsigned long -> internet address
char sin_zero[8];
};
>>> 구조체를 초기화 해야 함 (주소 정보, 포트 정보, 패밀리 정보)

memset(&_sAddr, 0, sizeof(_sAddr)); _sAddr.sin_addr.s_addr = htonl(INADDR_ANY); _sAddr.sin_family = AF_INET; _sAddr.sin_port = htons(_port);

 

int bind(int socket, const struct sockaddr *address, socklen_t address_len);
>>> socket_fd와 주소정보를 바인딩함

- socket: socket()으로 생성한 socket fd
- address: 위에서 생성한 sockaddr_in의 주소
- len: sockaddr_in의 크기
- return: 성공 시 0, 실패 시 -1

bind(_listenSocket, (struct sockaddr *)&_sAddr, sizeof(_sAddr));

 

int listen(int socket, int backlog);
>>> 커널에게 알려주는 것 (fd를 네트워크 통신에 사용하게 한다는 것)

- socket: 서버의 socket_fd
- backlog: 연결에 대기할 queue의 크기 (클라이언트의 connect이후의 accept까지의 대기상태)
- return: 성공 시 0, 실패 시 -1

listen(_listenSocket, backLog);

 

int accept(int socket, struct sockaddr *address, socklen_t *address_len);

>>> 클라이언트와 서버간의 fd를 이용한 통신이 가능하게 함
- socket: 서버의 socket_fd
- address: 클라이언트의 주소정보구조체
- len: 클라이언트의 주소정보구조체의 크기를 담은 변수의 포인터
- return: 성공 시 클라이언트와 통신이 가능한 socket_fd, 실패 시 -1

int tmpSock = accept(_listenSocket, (struct sockaddr *) &_cAddr, (socklen_t *)&len);

 

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *errorfds, struct timeval *timeout);
>>> fd_set들 중에 fd가 변화가 있는 것들의 개수를 세서 리턴함
>>> readset은 accept, read가 요청이 있을 때, writeset은 송신할 수 있을 때, errorset은 에러감지
>>> select는 변화가 있을 때까지 감지하는데, 시간제한은 timeout으로 둘 수 있음 (NULL은 blocking)
- nfds: max_fd값 + 1임 (검사할 소켓의 개수)
- readfds, writefds, errorfds: 변화를 감지할 fd_set들 (fd_set 자료형을 사용)
- timeout: 얼만큼의 시간동안 변화를 감지할 것인지
- return: 변화가 있다면 변화한 fd의 개수, 실패 시 -1, 변화가 없고 시간이 다 되었으면 0

if (select(maxFd + 1, &copyRead, &copyWrite, NULL, NULL) == -1) { std::cout << "select Fail!!!!\n"; exit (-1); }

 

select를 위한 매크로 함수들
void FD_ZERO(fd_set *fdset); // fdset을 모두 0으로 초기화 시킴

void FD_SET(fd, fd_set *fdset); // fdset에서 fd번을 1로 세팅해 놓음
void FD_CLR(fd, fd_set *fdset); // fdset에서 fd번을 0으로 세팅해 놓음
>>> select에서 변화를 감지할 fd번호임
>>> FD_SET으로 1로 설정해놓으면 select에서 해당 fd번의 변화를 찾아줌

int FD_ISSET(fd, fd_set *fdset); // select이후에 해당 fdset에서 fd번이 변화가 있는지 체크

void FD_COPY(fd_set *fdset_orig, fd_set *fdset_copy); // 복사? (fd_set cpSet = originSet; 으로 가능)