- BrilliantServer
- 목차
- 0 Introduction
- 1 socket 통신
- 2 TCP Connection
- 3 I/O 다중 처리
- 4 HTTP
- 5 Common Gateway Interface
- 6 “서버는 죽지 않아!”
- 7 References
- BrilliantServer 는 RFC 9110, 9112 에 정의된 규격을 따르는 HTTP/1.1 버전 origin server 의 구현입니다.
- 이 프로젝트는 Google C++ Style Guide 를 따라 C++98 로 작성되었습니다.
kqueue를 활용한 event loop 기반 non-blocking I/O multiplexing- RFC 3875 를 따르는 CGI 지원
- static 파일에 대한
GET/POST/DELETEHTTP Request 처리POST메소드로 파일 업로드
- HTTP 규격에 맞는 HTTP 응답 생성
- name-based virtual hosting 지원
- directory listing 지원
git clone https://github.com/brilliantshell/webserv.git && \
cd webserv && \
make -j
./BrilliantShell [path/to/config]
config 파일 작성 규칙은 다음과 같습니다. Configuration 파일 규칙
-
socket 통신은 IPC (Inter Process Communication) 의 한 종류다.
-
socket 파일은 socket 통신의 종점으로 (endpoint) socket 파일을 연 프로그램은 socket 파일을 연 다른 프로그램과 connection 을 수립하거나, 서로의 주소로 datagram 을 전송하여 서로 통신할 수 있다.
-
socket함수로 socket 파일을 열 수 있으며, 성공 시 할당 된 fd 가 반환된다.#include <sys/socket.h> int socket(int domain, int type, int protocol);
domain로 어떤 도메인에서 통신할지 정할 수 있다. 여러개가 있지만 (OS X 에는 8개, Linux 에는 더 많다) 중요한 두개는PF_LOCAL과PF_INET이다.PF_LOCAL- 도메인/UNIX socket 이라고 부르는 로컬 프로세스 간 통신을 위한 도메인PF_INET- TCP socket 이라고 부르는 IP 통신을 위한 도메인
type로 전송하는 데이터의 단위를 정할 수 있다.- SOCK_STREAM - connection 수립 & byte stream 으로 통신
- SOCK_DGRAM - 상대의 주소로 datagram 으로 통신
- SOCK_RAW - datagram 으로 통신, 내부 네트워크 인터페이스에 접근 가능
protocol에는 소켓이 따를 프로토콜을 지정한다. 같은 프로토콜로 열린 소켓들끼리만 통신이 가능하다. TCP 는 6. (/etc/protocols참고)
💡 이 문서는 Web Server 에 관련된 문서이기 때문에 아래에서는 TCP socket 에 대해서만 설명한다.
-
socket 생성 후
bind함수로 해당 소켓에 주소/식별자를 부여할 수 있다.#include <sys/socket.h> int bind(int socket, const struct sockaddr *address, socklen_t address_len);
socket은 해당 socket 의 fd 이다.address는 할당할 주소 정보를 담는 구조체의 주소값이다. UNIX socket 의 경우struct sockaddr_un의 주소값을, TCP socket 의 경우struct sockaddr_in(IPv4) /struct sockaddr_in6(IPv6) 의 주소값을 캐스팅해서 넣어준다.address_len에는address구조체의 길이를 넣어준다.
-
IPv4 의 경우 주소 구조체는 아래와 같다.
struct in_addr { in_addr_t s_addr; }; struct sockaddr_in { __uint8_t sin_len; sa_family_t sin_family; // AF_INET in_port_t sin_port; // port number struct in_addr sin_addr; // listen 할 IP 주소 sin_addr.s_addr 에 설정 char sin_zero[8]; };
sin_addr.s_addr는 Server 의 경우INADDR_ANY(0) 로 설정하여 어떤 주소든지sin_port에 설정한 port 로 연결을 시도하면 listen 하게 설정한다.- port 와 address 는 network byte order 로 저장돼야하기 때문에
<arpa/inet.h>함수들을 활용해야한다.
-
HTTP Server 는 socket 파일을 열어 (
PF_INET,SOCK_STREAM, 6) Client 프로그램들과 TCP connection 을 수립하여 통신한다. (HTTP/3.0 부터는 UDP 사용) -
Server 와 연결을 시도하는 Client 의 socket 은 “active” socket, Client 의 연결 시도를 기다리는 Server 의 socket 은 “passive” socket 이라고 부른다.
-
socket 간의 TCP connection 이 수립되는 과정을 순차적으로 설명하면 아래와 같다.
-
bind이후 Server 의 소켓은listen함수를 호출하여 “passive”/”listening” 상태로 전환한다.#include <sys/socket.h> int listen(int socket, int backlog);
socket은 해당 socket 의 fd 이다.backlog는 연결 수립을 기다리는 요청들의 queue 의 최대 길이이다. queue 가 꽉 차있는 상태에서 연결 수립 요청이 오면 Client 는 ECONNREFUSED 에러를 반환 받는다. (silent limit 128)
-
Server 는 “listening” socket 이 준비된 후
accept함수로 block 하며 Client 의 연결 수립 요청을 기다린다.#include <sys/socket.h> int accept(int socket, struct sockaddr *restrict address, socklen_t *restrict address_len);
socket은 “passive” socket 의 fd 이다.address는 Client 의 “active” socket 의 주소 정보를 담을 구조체의 주소다.address_len에는address구조체의 길이를 넣어준다.
-
Client 소켓은
connect함수를 호출하여 Server 에 TCP 연결 수립을 요청하는 “active” 역할을 수행한다.#include <sys/types.h> #include <sys/socket.h> int connect(int socket, const struct sockaddr *address, socklen_t address_len);
socket은 해당 socket 의 fd 이다.address에는 연결하고자 하는 Server 의 소켓에 할당된 주소가 입력된 구조체의 주소값을 넣어준다.address_len에는address구조체의 길이를 넣어준다.
-
Client 의 연결 수립 요청을 수신하면
accept는 blocking 을 풀고 Client 의 “active” socket 과 연결이 수립할 새로운AF_INETsocket 을 생성하고, 연결이 수립되면 (TCP ESTABLISHED) 그 socket 의 fd 를 반환한다.
-
-
Server 는
accept함수가 반환한 fd 에 read/recv 하여 Client 가 보낸 요청을 읽고, write/send 하여 Client 에게 응답을 보낼 수 있다.> 💡 socket 에 read/write 할 때, 한번의 호출로 시스템의 TCP window size 를 넘을 수 없다.sysctl -a | grep buf로 max limit 을 확인할 수 있다. (auto * bufmax 가 window size)
-
socket 설정을
getsockopt로 확인,setsockopt로 변경할 수 있다.#include <sys/socket.h> int getsockopt(int socket, int level, int option_name, void *restrict option_value, socklen_t *restrict option_len); int setsockopt(int socket, int level, int option_name, const void *option_value, socklen_t option_len);
-
setsockopt의option_name인자로send/recvbuffer size (SO_SNDBUF/SO_RCVBUF) 등 여러가지 속성을 변경할 수 있다.
-
특정 ip + port 로
bind되어있는 passive socket 이 Server 종료 시 혹은 실행 중 어떤 이유로 닫혔을 때 해당 socket 은TIME-WAIT상태가 되고 특별한 설정이 없었다면 2MSL 동안 해당 ip + port 로의bind가 불가능해진다. -
Server 가 재실행하기 위해 종료 후 2MSL 을 기다려야하는 것은 너무 불편하기 때문에, 이를 해결하기 위해 BrilliantServer 의 passive socket 들은
SO_REUSEADDR로 설정되었다. 특정 ip + port 로bind하기 전에SO_REUSEADDR설정을 하면, Server 는 2MSL 을 기다리지 않고 바로 해당 ip + port 를 재사용 (다시bind) 할 수 있다.TIME-WAITsocket 들이 남지만 이는 정상적인 종료 절차이고, Server 에게 문제가 되지 않는다.// opt 에는 0 이 아닌 숫자가 들어가면 된다 (bool 같은 역할) if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) == -1) { PRINT_ERROR("address cannot be reused : " << strerror(errno)); close(fd); return -1; } errno = 0; if (bind(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) { PRINT_ERROR("socket for " << kPort << " cannot be bound : " << strerror(errno)); close(fd); return -1; } listen(fd, BACKLOG);
-
SO_LINGER옵션으로도 이 문제를 해결할 수 있다.SO_LINGER옵션은struct linger의 주소를setsockopt의option_value로 넘겨주며,l_onoff가 0 이 아니고l_linger가 양의 정수로 설정될 경우, Server 가 socket 을close했을 때 아직 보내지지 않은 데이터가 남아 있다면l_linger초 만큼close를 block 하게 설정하는데 사용된다.l_linger값을 0 으로 설정하면 정상적인 TCP 연결 종료 절차가 시작되지 않고, TCP 연결에서RSTcontrol bit 이 보내지며close한 socket 이TIME-WAIT상태에 빠지지 않는다. 하지만 비정상적으로 TCP 연결을 끊기 때문에 이전 TCP 연결이 제대로 정리되지 않아 Connection Reset by Peer 에러가 발생할 위험이 크다.struct linger { int l_onoff; /* option on (0)/off (non-zero) */ int l_linger; /* linger time (sec) */ };
-
TCP connection state diagram
+---------+ ---------\ active OPEN | CLOSED | \ ----------- +---------+<---------\ \ create TCB | ^ \ \ snd SYN passive OPEN | | CLOSE \ \ ------------ | | ---------- \ \ create TCB | | delete TCB \ \ V | \ \ rcv RST (note 1) +---------+ CLOSE | \ -------------------->| LISTEN | ---------- | | / +---------+ delete TCB | | / rcv SYN | | SEND | | / ----------- | | ------- | V +--------+ snd SYN,ACK / \ snd SYN +--------+ | |<----------------- ------------------>| | | SYN | rcv SYN | SYN | | RCVD |<-----------------------------------------------| SENT | | | snd SYN,ACK | | | |------------------ -------------------| | +--------+ rcv ACK of SYN \ / rcv SYN,ACK +--------+ | -------------- | | ----------- | x | | snd ACK | V V | CLOSE +---------+ | ------- | ESTAB | | snd FIN +---------+ | CLOSE | | rcv FIN V ------- | | ------- +---------+ snd FIN / \ snd ACK +---------+ | FIN |<---------------- ------------------>| CLOSE | | WAIT-1 |------------------ | WAIT | +---------+ rcv FIN \ +---------+ | rcv ACK of FIN ------- | CLOSE | | -------------- snd ACK | ------- | V x V snd FIN V +---------+ +---------+ +---------+ |FINWAIT-2| | CLOSING | | LAST-ACK| +---------+ +---------+ +---------+ | rcv ACK of FIN | rcv ACK of FIN | | rcv FIN -------------- | Timeout=2MSL -------------- | | ------- x V ------------ x V \ snd ACK +---------+delete TCB +---------+ -------------------->|TIME-WAIT|------------------->| CLOSED | +---------+ +---------+ -
TCP Header Format
0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Source Port | Destination Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Sequence Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Acknowledgment Number | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Offset| Rsrvd |W|C|R|C|S|S|Y|I| Window | | | |R|E|G|K|H|T|N|N| | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Checksum | Urgent Pointer | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | [Options] | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | : : Data : +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Note that one tick mark represents one bit position.- Seqeunce Number : 32 bits
- 해당 세그먼트의 첫 데이터 octet 의 sequence 번호. 예외로 SYN 컨트롤 비트가 세팅될 때는 sequence 번호는 ISN, 첫 데이터 octet 은 ISN + 1 로 설정된다.
- Acknowledgement Number : 32 bits
- ACK 컨트롤 비트가 세팅되면, 다음에 받을 것으로 예상되는 세그먼트의 시퀀스 번호가 설정된다. 한 번 연결이 수립되면 항상 전송된다.
- Reserved (Rsrvd) : 4 bits
- 컨트롤 비트를 표시한다.
- Window : 16 bits
- 발신자가 받을 수 있는 TCP Window 크기 (unsigned number)
- Seqeunce Number : 32 bits
-
three-way-handshake
TCP Peer A TCP Peer B 1. CLOSED LISTEN 2. SYN-SENT --> <SEQ=100><CTL=SYN> --> SYN-RECEIVED 3. ESTABLISHED <-- <SEQ=300><ACK=101><CTL=SYN,ACK> <-- SYN-RECEIVED 4. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK> --> ESTABLISHED 5. ESTABLISHED --> <SEQ=101><ACK=301><CTL=ACK><DATA> --> ESTABLISHED- 새 커넥션을 만들기 위해서는 각 Peer 별로 32bit 크기의
ISN(Initial Sequence Number) 을 생성하고 식별자로써 사용한다.ISN을 사용하면 포트를 재사용 할 경우 각 커넥션을 구분하여 데이터가 혼재되지 않고 SEQ 를 추측하기 어려워지므로 보안이 강화된다. - 데이터를 주고 받을 수 있는 상태가 되려면 두 peer 모두
ESTABLISHED상태가 되어야 한다
- 서버 (위 그림에서 Peer B) 측에서 passive open 으로
LISTEN상태가 되면 클라이언트 (Peer A) 가 active open 을 시도하길 기다린다. - 클라이언트가 passive open 하면 자신의 ISN 을 시퀀스 넘버 (SEQ) 로 SYN 컨트롤 비트와 함께 서버에게 보내고
SYN_SENT로 전환한다. SYN 을 받았으므로 서버는SYN_RECEIVED상태로 전환한다. - 세그먼트를 받으면 서버는 자신의
ISN을 시퀀스 넘버(SEQ) 로, 받았던 세그먼트의 SEQ + 1 값을 ACK 으로 설정하여 보낸다. 컨트롤 비트 SYN, ACK 로 확인 응답을 받은 클라이언트가ESTABLISHED로 전환한다. - 클라이언트가 ACK 를 보내고 응답 받은 서버도
ESTABLISHED로 전환한다. - 둘 모두
ESTABLISHED인 상태에서 data 를 주고 받을 수 있다. 1.2 TCP 연결 수립 & Passive vs. Active
- 새 커넥션을 만들기 위해서는 각 Peer 별로 32bit 크기의
- TCP Window 란 각 peer 가 임시로 데이터를 받을 수 있는 버퍼이다. 응용 프로그램 측에서 버퍼를 읽어가면 clear 한다. 세그먼트의 Window 헤더 필드로 남은 Window size 를 상대편 peer 에게 알려줄 수 있다. 남은 TCP Window 사이즈가 0에 가까워 지면 버퍼가 비워졌다는 업데이트가 갈 때 까지 전송이 중단된다.
- socket read 가 지연되면 Window size 가 작아져 상대편 peer 의 전송이 중단된다.
- four-way-handshaking
TCP Peer A TCP Peer B
1. ESTABLISHED ESTABLISHED
2. (Close)
FIN-WAIT-1 --> <SEQ=100><ACK=300><CTL=FIN,ACK> --> CLOSE-WAIT
3. FIN-WAIT-2 <-- <SEQ=300><ACK=101><CTL=ACK> <-- CLOSE-WAIT
4. (Close)
TIME-WAIT <-- <SEQ=300><ACK=101><CTL=FIN,ACK> <-- LAST-ACK
5. TIME-WAIT --> <SEQ=101><ACK=301><CTL=ACK> --> CLOSED
6. (2 MSL)
CLOSED
- TIME_WAIT & CLOSE_WAIT
- 서버 (위 그림에서 Peer A) 측에서
close하고 클라이언트 ****(위 그림에서 Peer B) 측에서 ACK 를 보내기 전 프로세스를 종료해 버릴 경우 서버 측에서 답장을 받지 못했기 때문에TIME_WAIT상태에 걸리게 된다. 프로그램 상에서TIME-WAIT상태를 처리하기 어렵고, 재 연결을 위해서는 2MSL (Maximum Segment Lifetime) 만큼 기다려야 하므로 서버 측의close는 신중하게 사용해야 한다. - 어쩔 수 없는 경우
shutdown을 이용해 socket 의 write 부터 닫는다.
- 서버 (위 그림에서 Peer A) 측에서
- CLOSE 절차 중 특정 상태에서 pending 되면 정상적인 연결이 이루어지지 못하는 문제가 생길 수 있다.
- CLOSE_WAIT
- passive close 하는 경우, active close 한 상대편 peer 가 보낸
FIN을 받고close하기 전 상태이다. CLOSE_WAIT은 정상적인close요청으로 처리하는FIN_WAIT나TIME_WAIT과는 다르게 일정 시간이 지나도 사라지지 않으므로 프로세스의 종료나 네트워크 재시작으로 해결해야 한다.- Brilliant Server 에서는 kqueue TIMER 이벤트를 이용하여 일정 시간이 지나면 명시적으로
close요청을 보내는 방법으로 해결하였다.TIMER event 의if (events[i].filter == EVFILT_TIMER) { ClearConnectionResources(events[i].ident); }
ident와 바인딩 된 소켓 의fd를 동일하게 설정하여ClearConnectionResources에서close를 호출하고 자원정리한다.
- passive close 하는 경우, active close 한 상대편 peer 가 보낸
- TIME_WAIT
- active close 후 상대편의
FIN을 기다리는 상태이다. 2MSL 이 지난 후에CLOSED상태가 되어야 다시 해당 포트에 바인딩 할 수 있다. TIME_WAIT이 없고 바로 연결이 닫히는 경우 문제되는 상황close되는 소켓의 send 버퍼에 아직 보내지 않은 데이터가 남아있을 수 있다.- 상대편 peer 에서 보내고 아직 도달하지 못한 데이터가 있을 수 있다. 상대편은 아직 ACK 을 받지 못했기 때문에 계속 재전송을 시도하거나 데이터 손실이 일어난다.
- 상대편 peer 가
LAST_ACK상태에서 보낸FIN을 받을 수 없으므로 응답도 줄 수 없다. 상대편은 아직ACK을 받지 못했기 때문에 계속 재전송을 시도하거나 데이터 손실이 일어난다.
TIME_WAIT이 남아있어도 새bind를 하고 싶은 경우setsockopt를 이용할 수 있다.SO_LINGER를 사용한 방법- 일반적인 경우
close이후에 위 경우들이 모두 처리되지만l_linger를 매우 작게 설정하여SO_LINGER를 사용하면 데이터 손실 뿐만 아니라RST컨트롤 비트를 이용하여 소켓을 종료하므로 상대편에서Connection reset by peer오류를 발생시킬 수 있다.
- 일반적인 경우
SO_REUSEADDR을 사용한 방법TIME_WAIT상태에서도 새로운 socket 을 같은 주소에bind할 수 있게 한다.
- Brilliant Server 에서의 해결법 1.4.0 SO_REUSEADDR vs SO_LINGER
- active close 후 상대편의
-
I/O Multiplexing 은 하나의
event loop에서 여러개의I/O events를 처리하는 방식이다. -
각 I/O 는
non-block으로 이루어지며 I/O 작업이 일어나는FD들을 감시하는 시스템 콜(select,poll,epoll,kqueue… ) 을 활용하여FD에event의 발생 여부를 감시하고, 만약 이벤트가 있다면 적절한 작업을 해야한다. -
kqueue를 예로 들면,non-block I/O를 호출 한 뒤kevent로block을 시키고,FD에 발생한event가 있는지 확인한다.kevent는 하나의FD뿐만 아니라, 여러개의FD에 대한event를 감지할 수 있어서 여러의 I/O 를 한 프로세스에서 관리할 수 있게 된다.
- 기존
read/write호출은 프로세스를block시키고 I/O 작업이 완료되기를 기다리지만,non-blockI/O 는read/write호출시read/write가 가능하다면 작업이 수행되고, 그렇지 않다면-1을 반환하며errno가EAGAIN|EWOULDBLOCK로 설정된다.
출처 : https://ecsimsw.tistory.com/entry/Web-server-with-socket-API
-
kqueue와kevent는 kernel event notification mechanism 이며 각각 kernel queue, kernel event 를 뜻한다.#include <sys/types.h> #include <sys/event.h> #include <sys/time.h> int kqueue(void); int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout); struct kevent { uintptr_t ident; /* identifier for this event */ int16_t filter; /* filter for event */ uint16_t flags; /* general flags */ uint32_t fflags; /* filter-specific flags */ intptr_t data; /* filter-specific data */ void *udata; /* opaque user data identifier */ }; EV_SET(&kev, ident, filter, flags, fflags, data, udata);
-
kqueue()시스템 콜은 새로운kqueueFD를 반환한다. 이FD는 filters 라고 하는 kernel code 의 결과를 기반으로 kernel event 가 발생하거나 조건을 충족하면, 사용자에게 알려주는 일반적인 방법을 제공한다.
-
kevent구조체는 (ident,filter,udata(optional)) 튜플로 식별되며kevent구조체에 해당 튜플에 대해 알림을 받을 조건을 지정한다. I/O event의 경우ident로 FD 가 들어가고,filter에EVFILT_READ, EVFILT_WRITE값을 넣어서read/write이벤트를 등록 할 수 있다.void HttpServer::InitKqueue(void) { kq_ = kqueue(); // kqueue 생성 if (kq_ == -1) { PRINT_ERROR("HttpServer : kqueue failed : " << strerror(errno)); exit(EXIT_FAILURE); } // kevent 구조체 동적 할당 struct kevent* sock_ev = new (std::nothrow) struct kevent[passive_sockets_.size()]; if (sock_ev == NULL) { PRINT_ERROR("HttpServer : failed to allocate memory"); exit(EXIT_FAILURE); } int i = 0; for (ListenerMap::const_iterator it = passive_sockets_.begin(); it != passive_sockets_.end(); ++it) { // kevent 구조체 배열 초기화 (ident: fd) EV_SET(&sock_ev[i++], it->first, EVFILT_READ, EV_ADD, 0, 0, NULL); } // kevent에 changlist, nchanges를 인자로 넘겨 이벤트 등록 if (kevent(kq_, sock_ev, passive_sockets_.size(), NULL, 0, NULL) == -1) { PRINT_ERROR("HttpServer : failed to listen : " << strerror(errno)); exit(EXIT_FAILURE); } delete[] sock_ev; }
-
kevent()함수는changelist에 감시할kevent 구조체의 포인터를 받아 이벤트를 등록한다.while (true) { // 이벤트가 발생할 때 까지 block int number_of_events = kevent(kq_, NULL, 0, events, MAX_EVENTS, NULL); if (number_of_events == -1) { PRINT_ERROR("HttpServer : kevent failed : " << strerror(errno)); } for (int i = 0; i < number_of_events; ++i) { if (events[i].filter == EVFILT_READ) { /* READ 이벤트 발생, read 작업 수행하기 */ } else if (events[i].filter == EVFILT_WRITE) { /* Write 이벤트 발생, write 작업 수행하기 */ } } }
-
eventlist에는 이벤트 발생시 이벤트 데이터를 받아올kevent 구조체의 포인터를 받고, 이벤트 발생시 발생한 이벤트의 개수가 반환되고,eventlist에 넣은kevent 구조체에 데이터가 담겨온다. -
I/O 의 경우 kevent 구조체의 ident 를 FD 로 넘기고, filter 에 EVFILT_READ|WRITE 를 주면 다음과 같은 경우에 이벤트가 발생한다.
- READ 의 경우 FD 에 읽을 수 있는 데이터가 있을 때
- WRITE 의 경우 FD 에 데이터를 쓸 수 있을 때
-
이벤트가 발생한 경우 적절한 READ / WRITE 호출을 해주면, non-block I/O 임에도 적절하게 I/O 를 처리 할 수 있다.
kqueue vs select vs poll
-
select작동 방식#include <sys/select.h> int select(int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);
-
select()는 인자로 감시할fd의 개수(nfds)를 받는다. -
select()호출 시 (0 ~nfds-1)개의 배열을 순회하며 이벤트를 탐지한다.$O(nfds)$ - 이벤트 발생시 데이터가 변경된 파일의 개수가 반환되어, 배열을 다시 순회하며 어떤
fd에서 이벤트가 발생했는지 찾아야 한다. -
nfds가 1024 를 넘을 수 없다. -
fd마다bitmasking을 활용하여3bit만으로 한fd의 상태를 추적할 수 있다.
-
-
poll작동 방식#include <poll.h> int poll(struct pollfd fds[], nfds_t nfds, int timeout);
-
poll()은fd와 이벤트 정보가 담긴pollfd배열과, 실제로 감시하는fd의 수인nfds를 인자로 받는다. -
select()에선nfds사이즈의 배열을 순회하며 이벤트가 발생한fd를 찾아야 했지만,poll()실제로 감시하는fd의 개수만큼 순회를 할 수 있다.$O(fd_count)$ - 감시 가능한
fd의 수가 무제한이나, 한 구조체당64bit의 크기를 가져 많은 이벤트를 다룰 경우select()보다 성능이 떨어질 수 있다.
-
-
select(), poll()의 문제점-
select(), poll()은 호출 할 때 마다, 전체fd배열을 인자로 넘겨야 하며, 이 배열이user-space에서kernel-space로 복사될때 상당한 오버헤드가 존재한다. (95% 는 불필요한 복사) -
kernel에서 이벤트가 발생하면,kernel-space에서는 이미 이벤트가 발생한fd를 아는데도 불구하고user-space에서 발생한 이벤트를 찾기 위해 배열을 순회해야 한다.
-
-
kqueue(), kevent()의 장점-
kevent는kernel에서 실제로 이벤트가 발생한fdlist만 반환하여,application에서 이벤트를 바로 추적할 수 있다. - I/O event 뿐만 아니라 process event, signal event, timer event 등을 등록 할 수 있다.
-
-
kqueue(), kevent()의 단점- FreeBSD 계열에 한정된 시스템 콜 이라서 호환성이 좋지 않다. 도커를 활용하여 리눅스에서 실행하고 싶었는데 실패했다.
-
기존엔
send를 이용하여response를 보냈으나response의header와content가 분리되어 있는 상황에서send를 사용하기 위해선content의 불필요한 복사가 일어나는 문제가 있었다.#include <sys/uio.h> ssize_t writev(int fildes, const struct iovec *iov, int iovcnt); struct iovec { char *iov_base; /* Base address. */ size_t iov_len; /* Length. */ };
-
writev를 사용하면,header와content가 다른 버퍼에 있더라도,iovec구조체에header와content의 주소를 넘겨주면, 하나의 버퍼로write하는 것과 같은 효과가 있다. 따라서 불필요한 복사도 일어나지 않고,write시스템 콜도 줄일 수 있다.
- 다수의 클라이언트가 정의한
BUFFER_SIZE보다 큰 파일을 요청 했을 때,content가content-length보다 적게 전송되는 문제가 있었다. 이를setsocketopt함수로socket에SO_SNDLOWAT옵션을 줘서 해결했다.setsockopt(fd, SOL_SOCKET, SO_SNDLOWAT, &buf_size, sizeof(int))
SO_SNDLOWAT옵션은non-block socket에서socket buffer에output을 위한buf_size만큼의bytes가 비어있거나, 한번에 모든 데이터를socket buffer에write할 수 있을 때 (데이터의 크기가buf_size보다 작을 때)send()가 가능해지며, 그렇지 않으면send()가 아무 데이터도 전송하지 않고 에러가 발생한다.kevent의EVFILT_WRITE에SO_SNDLOWAT옵션이 적용된socket을 등록하게 되면, 해당socket에buf_size만큼socket buffer에write할 수 있을때 이벤트가 발생하게 된다.buf_size를SEND_BUFFER_SIZE인32kb로 정의 했으나, 서버가 너무 많은 요청을 받는 상태에서 여전히content가 덜 전송되는 문제가 발생하여SEND_BUFFER_SIZE의 1.5배로 설정하여 어느정도 해결했다.
- HTTP 메시지는 아래의 섹션들로 이뤄져있다.
- control data (request line / response line)
- header table
- octet-stream content
- trailer table
- HTTP 메시지의 시작과 끝을 framing 이라고 하고, framing 은 아래와 같은 형식으로 결정된다.
- body 가 없는 경우
- (시작) control data
- header
CRLFCRLF(끝)
- body 가 있는 경우
- (시작) control data
- header (
Content-Length: (positive integer)/Transfer-Encoding: chunked) CRLFCRLFContent-Length길이 만큼의 octet-stream bytes content / chunked 메시지가 끝날 때까지 (끝)
- body length 결정
- 1xx, 204 OR 304 status code 를 갖는 응답은 header fields + CRLF + CRLF 로 끝난다.
Transfer-Encoding&Content-Length둘 다 있는 메시지가 수신된다면Transfer-Encoding이Content-Length의 값을 override 한다. 이런 메시지는 request smuggling/ response splitting 의 시도일 수 있고 에러로 처리되어야한다.Transfer-Encodingheader field 가 있고chunked가 마지막 encoding 일 때, 메시지 body length 는 transfer coding 이 끝났다고 알려줄 때까지 읽으면서 결정한다. (chunked-size에 0 이 올 때까지 읽으라는 말인 것 같다.)- 응답 메시지의 경우,
chunked가 마지막 encoding 이 아닐 때, 서버가 연결을 끊을 때까지 읽는다. - 요청 메시지의 경우,
chunked가 마지막 encoding 이 아닐 때, Bad Request (400) & 연결을 끊는다.
Transfer-Encoding이 없고,Content-Length가 유효하지 않으면, 메시지 framing 이 유효하지 않다 (a 경우 외에). 서버는 Bad Request (400) & 연결을 끊는다.Content-Length: 400,400,400,400(같은 숫자 & ‘,’ separated list 이면 해당 반복되는 숫자로 지정)
Transfer-Encoding이 없고,Content-Length가 유효한 경우,Content-Length에 명시된 수가 body length.Content-Length에 명시된 수 만큼의 octets 가 수신되기 전 만약 송신자가 연결을 끊거나 수신자가 time out 되면 해당 메시지는 미완성으로 (incomplete) 으로 보고 연결을 끊어야한다 (MUST).- 요청 메시지 & 위의 어떤 케이스에도 해당하지 않으면 body length 는 0.
- 응답 메시지 & 위의 어떤 케이스에도 해당하지 않으면 서버가 응답을 close 하기 전에 보낸 만큼이 body length.
- 연결이 끝나서 메시지가 끝난 건지, 네트워크 에러로 연결이 끊겼는지 판별하는게 어렵기 때문에 서버는 encoding 혹은 length 명시를 꼭 해줘야한다 (SHOULD).
- close-delimiting 은 HTTP/1.0 과의 하위 호환성을 위해 지원한다.
- 요청 메시지는 절대 close-delimiting 을 하지 않는다. 항상
Content-Length/Transfer-Encoding으로 body 의 끝을 알려준다 (MUST).
- 요청 메시지에 body 가 있는데
Content-Length가 없는 경우 서버는 Length Required (411) 로 응답할 수 있다 (MAY). - 요청 메시지에
Transfer-Encoding이 있는데chunked이외의 coding 이 적용됐고, 메시지 길이를 알 수 있다면 클라이언트는chunked를 사용하는 것보다 유효한Content-Length를 명시하는 걸 우선해야한다 (SHOULD). 서버들이chunkedcoding 을 해석할 수 있어도 Length Required (411) 로 응답할 수도 있기 때문이다.
- body 가 없는 경우
-
같은 IP + port 안에서 여러개의 Server 호스팅이 가능해졌다. Name-based virtual server 는 요청의
Host헤더 필드 값으로 요청이 향하는 Server 를 라우팅한다. -
아래와 같이 서버가 설정되어 있을 때, 요청의
Host헤더 필드 값이 ghan 이면 첫번째 Server, yongjule 면 세번째 Server 로 라우팅된다.server { listen 80 server_name ghan ... } server { listen 80 server_name jiskim ... } server { listen 80 server_name yongjule ... }
-
HTTP/1.1 은
Transfer-Encoding: chunked헤더 필드로 content 전체 길이를 모르는 content stream 을 length-delimited buffer 의 연속으로 전송할 수 있게 해준다.chunked-body = *chunk last-chunk trailer-section CRLF chunk = chunk-size [ chunk-ext ] CRLF chunk-data CRLF chunk-size = 1*HEXDIG last-chunk = 1*("0") [ chunk-ext ] CRLF chunk-data = 1*OCTET ; a sequence of chunk-size octets
-
아래 예시와 같이, 16진수로 명시된
chunk-size다음 줄에 해당 사이즈 만큼의 octet stream 이 따른다.chunk-size0 으로 transfer coding 의 끝을 알리고,trailer section이 이어질 수도 있고, CRLF 로 body 의 끝을 표시한다.HTTP/1.1 200 OK Content-Type: text/plain Transfer-Encoding: chunked 4\r\n ghan\r\n 6\r\n jiskim\r\n 8\r\n yongjule\r\n 0\r\n \r\n
-
수신자는
chunkedtransfer coding 을 파싱할 수 있어야한다 (MUST). -
수신자는 매우 큰
chunk-size가 올 수 있다는 걸 예상하고, overflow, precision loss 와 같은 파싱 에러를 방지해야한다 (MUST). (HTTP/1.1 은 제한을 설정하지 않았다.) -
chunked에 parameter 가 붙으면 에러 (SHOULD). -
chunk-ext가 아래의 포맷으로chunk-size옆에 나올 수도 있다. 수신자는 이해할 수 없는 (unrecognized) chunk extension 을 무시해야한다 (MUST). (다 이해할 수 없기로 하자… 문법 체크만 하자…)chunk-ext = *( BWS ";" BWS chunk-ext-name [ BWS "=" BWS chunk-ext-val ] ) chunk-ext-name = token chunk-ext-val = token / quoted-string
-
chunkedcoding 을 없애는 수신자는 trailer fields 를 유지할지 없앨지 정할 수 있다 (MAY). 유지한다면, header field 와는 다른 구조체에 저장해야한다.
- Persistence
- HTTP/1.1 은 기본적으로 persistent connection 을 사용하며, 하나의 연결 에서 여러개의 요청과 응답을 주고 받을 수 있다. 이는 가장 최근에 받은
protocol version이나Connection헤더 필드에 의해 결정되는데, 아래와 같은 우선순위에 의하여 결정된다.Connection헤더 필드에close가 있다면, 현재 응답 이후에 connection 은 더이상 지속되지 않는다. else;- 받은 요청이
HTTP/1.1이면 연결은 계속 지속된다. else; - 받은 요청이
HTTP/1.0이고Connection헤더 필드가keep-alive면 연결은 계속 지속된다. - 현재 연결 이후에 연결은 닫힌다.
- HTTP/1.1 은 기본적으로 persistent connection 을 사용하며, 하나의 연결 에서 여러개의 요청과 응답을 주고 받을 수 있다. 이는 가장 최근에 받은
- pipelining
persistent connection을 지원하는 클라이언트는 요청을pipeline(응답을 기다리지 않고 여러개의 요청을 보내는것)을 할 수 있다. 서버는pipeline으로 오는 요청이 모두 안전한method를 가진 요청이라면, 이를 병렬적으로 처리할 수 있지만 각 요청에 상응하는 응답을 받은 것과 같은 순서로 보내줘야 한다.
- Common Gateway Interface (CGI) 는 HTTP Server 가 플랫폼에 상관 없이 외부 프로그램을 실행시킬 수 있게 해주는 인터페이스다.
- RFC 3875 에 규격이 정의되어 있다.
- Server 는 CGI script 를
execve로 호출하며 아래의 meta-variable 들을 env 로 설정해준다 (execve의 세번째 인자로 전달)."AUTH_TYPE=" // 서버가 사용자 인증에 사용하는 방법. "CONTENT_LENGTH=" // 요청 메시지 body 의 크기를 십진수로 표현 "CONTENT_TYPE=" // 요청 메시지 body 의 Internet Media Type "GATEWAY_INTERFACE=CGI/1.1" // server 가 적용하는 CGI version "PATH_INFO=" // Script-URI 의 script-path 에 이어 나오는 부분 "PATH_TRANSLATED=" // PATH_INFO 기반으로 해당 resource 의 local 절대 경로 "QUERY_STRING=" // URL-encoded 검색/parameter 문자열 "REMOTE_ADDR=" // 요청을 보내는 client 의 네트워크 주소 "REMOTE_HOST=" // 요청을 보내는 client 의 도메인 "REMOTE_IDENT=" "REMOTE_USER=" // 사용자 인증을 위해 client 가 제공하는 사용자 식별자 "REQUEST_METHOD=" // 요청 method "SCRIPT_NAME=" // 요청에서 cgi script 까지의 경로 "SERVER_NAME=" // 서버 명 "SERVER_PORT=" // 서버 포트 "SERVER_PROTOCOL=" // 서버 프로토콜 "SERVER_SOFTWARE=" // 서버 프로그램 명
- CGI 는 표준 출력에 CGI 응답을 작성하고
EOF로 응답의 끝을 Server 에게 알린다. - CGI 의 응답을 받기 위해 Server 는
execve전에pipe를 열어 CGI 로 부터 응답 받을 채널을 준비한다.
- CGI 응답은 header 필드와 body 로 구성된다.
- header 필드는 CGI-field (
Content-Type|Location|Status) + HTTP field (선택) + extension field (선택) 로 이루어진다. - body 는
EOF까지 쓰인 octet-stream 이다. - CGI 응답은 Document Response, Local Redirection Response, Client Redirection Response, Client Redirection Response with Document 로 나뉜다.
document-response = Content-Type [ Status ] *other-field NL
response-body- 일반적인 문서 반환,
Content-Type필드는 필수,Status는 없으면 200 으로 간주한다.
Location필드가 필수이다.Location = local-Location | client-Location client-Location = "Location:" fragment-URI NL local-Location = "Location:" local-pathquery NL fragment-URI = absoluteURI [ "#" fragment ] fragment = *uric local-pathquery = abs-path [ "?" query-string ] abs-path = "/" path-segments path-segments = segment *( "/" segment ) segment = *pchar pchar = unreserved | escaped | extra extra = ":" | "@" | "&" | "=" | "+" | "$" | ","
-
Local Redirect
local-redir-response = local-Location NL
- CGI 는
Location필드 값에 리다이렉트 할 경로를 적어준다. - Server 는 그 경로로 요청이 온 것처럼 요청을 처리한다.
- CGI 는
-
Client Redirect
client-redir-response = client-Location *extension-field NL- CGI 는
Location필드 값에 Client 가 리다이렉트 해야할 경로를 적어준다. - Server 는
302 Found상태 코드와 함께Location헤더 필드를 Client 에게 전달하며 Client 가 리다이렉션을 수행할 수 있게 한다.
- CGI 는
-
Client Redirect with Document
client-redirdoc-response = client-Location Status Content-Type *other-field NL response-body- CGI 는
Location필드 값에 Client 가 리다이렉트 해야할 경로를 적어주며,Content-Type에는 반환하는 문서의 미디어 타입을 알려준다. - Server 는
302 Found상태 코드와 함께Location헤더 필드를 Client 에게 전달하며 Client 가 리다이렉션을 수행할 수 있게 한다.
- CGI 는
- CGI 의 응답을 받은 Server 는 CGI 가 보낸 header 필드들이 의미하는 바가 Server 가 설정하는 응답 헤더 필드값과 상충된다면 어떤 값을 넣을지 결정해야한다.
- Server 는 CGI 의 응답이 HTTP 규격에 맞는지 점검하고 Client 에게 전달해야한다.
- Server 는 어떤 상황에도 꺼지지 않아야하고, HTTP/1.1 의 특성 상 Connection keep-alive 의 경우 같은 Connection 객체가 재사용되기 때문에 자원 정리가 매우 중요하다.
- 최대한 자원 할당은 생성자에서, 해제는 소멸자에서 처리한다(RAII).
- 하지만 생성과 소멸 사이클을 돌 수 없고 반복적으로 재사용되는 객체의 경우 아래의 기본적인 규칙들을 주의하여 지켜야한다.
- heap use after free/double free/pointer being freed was not allocated 를 피하기 위해 할당 해제 후 포인터
NULL로 설정한다. - 한정된
fd테이블이 재사용되기 때문에 socket, file, pipe 의 I/O event 시 사용하는fd가 전혀 다른 device 를 가리킬 수 있다. 이를 방지하기 위해 재사용 되는fd변수들은close이후 -1 로 설정한다.
- heap use after free/double free/pointer being freed was not allocated 를 피하기 위해 할당 해제 후 포인터
- Sockets (The GNU C Library)
- What is the meaning of SO_REUSEADDR (setsockopt option) - Linux?
- When is TCP option SO_LINGER (0) required?


