Stateful protocol, Server’sview
Wait for
Client
Wait for
message
Message
in
START
In: client connected
Out: send client accepted code
In: received message begin code
In: received message end code
In: received message
(message != end code)
Out: send processed message
6.
Iterative server
• while문한 cycle에 Client 하나 accept
• Client socket과 연결 후 데이터 처리
• 한번에 1개의 Client만 처리 가능!
while (1) {
...
int newsockfd =
accept(sockfd, (struct
sockaddr*)&peer_addr, &peer_addr_len);
...
report_peer_connected(&peer_addr,
peer_addr_len);
serve_connection(newsockfd);
printf("peer donen");
}
7.
Multiple concurrent clients
14:14:17,763:conn1connected...
14:14:17,763:conn1 sending b'^abc$de^abte$f'
14:14:17,763:conn1 received b'b'
14:14:17,802:conn1 received b'cdbcuf'
14:14:18,764:conn1 sending b'xyz^123'
14:14:18,764:conn1 received b'234'
14:14:19,764:conn1 sending b'25$^ab0000$abab'
14:14:19,765:conn1 received b'36bc1111'
14:14:19,965:conn1 disconnecting
14:14:19,966:conn2 connected...
14:14:19,967:conn2 sending b'^abc$de^abte$f'
14:14:19,967:conn2 received b'b'
14:14:20,006:conn2 received b'cdbcuf'
14:14:20,968:conn2 sending b'xyz^123'
14:14:20,969:conn2 received b'234'
14:14:21,970:conn2 sending b'25$^ab0000$abab'
14:14:21,970:conn2 received b'36bc1111'
14:14:22,171:conn2 disconnecting
14:14:22,171:conn0 connected...
14:14:22,172:conn0 sending b'^abc$de^abte$f'
14:14:22,172:conn0 received b'b'
14:14:22,210:conn0 received b'cdbcuf'
14:14:23,173:conn0 sending b'xyz^123'
14:14:23,174:conn0 received b'234'
14:14:24,175:conn0 sending b'25$^ab0000$abab'
14:14:24,176:conn0 received b'36bc1111'
14:14:24,376:conn0 disconnecting
• Client 하나가 연결되면 다음 Client는 이전 연결
이 끊길때까지 기다려야 한다!
• Clients가 많고 작업이 길수록 비 효율적인 구조
8.
Threads
• Thread: 프로세스내에서 실행되는 여러 흐름의 단위
– 프로세스 내에서 Stack만 따로 할당, Code, Data, Heap 공유
– 멀티프로세싱의 Context Switching 오버헤드 감소
– 공유 데이터 사용시 충돌 가능성 up
– 디버깅이 까다로움
• Multi-thread를 활용해서 Server의 concurrency를 해결
– One thread per client
– Thread pools
9.
Approach to concurrentserver design
Iterative server Multi-threaded server
Runtime
Runtime
: Arrival time
: Process time: Wait time
One Thread perClient
void* server_thread(void* arg) {
thread_config_t* config =
(thread_config_t*)arg;
int sockfd = config->sockfd;
free(config);
unsigned long id = (unsigned
long)pthread_self();
printf("Thread %lu created to handle
connection with socket %dn", id,
sockfd);
serve_connection(sockfd);
printf("Thread %lu donen", id);
return 0;
}
• Thread에서 Client를 처리한다.
12.
One Thread perClient
06:31:56,632:conn1 connected...
06:31:56,632:conn2 connected...
06:31:56,632:conn0 connected...
06:31:56,632:conn1 sending b'^abc$de^abte$f'
06:31:56,632:conn2 sending b'^abc$de^abte$f'
06:31:56,632:conn0 sending b'^abc$de^abte$f'
06:31:56,633:conn1 received b'b'
06:31:56,633:conn2 received b'b'
06:31:56,633:conn0 received b'b'
06:31:56,670:conn1 received b'cdbcuf'
06:31:56,671:conn0 received b'cdbcuf'
06:31:56,671:conn2 received b'cdbcuf'
06:31:57,634:conn1 sending b'xyz^123'
06:31:57,634:conn2 sending b'xyz^123'
06:31:57,634:conn1 received b'234'
06:31:57,634:conn0 sending b'xyz^123'
06:31:57,634:conn2 received b'234'
06:31:57,634:conn0 received b'234'
06:31:58,635:conn1 sending b'25$^ab0000$abab'
06:31:58,635:conn2 sending b'25$^ab0000$abab'
06:31:58,636:conn1 received b'36bc1111'
06:31:58,636:conn2 received b'36bc1111'
06:31:58,637:conn0 sending b'25$^ab0000$abab'
06:31:58,637:conn0 received b'36bc1111'
06:31:58,836:conn2 disconnecting
06:31:58,836:conn1 disconnecting
06:31:58,837:conn0 disconnecting
• Clients가 동시에 연결된다.
• One thread per client로 Concurrency 만족
13.
One Thread perClient
• 처리속도보다 많은 Client가 들어올 경우 너무 많은 thread가 생성
된다.
– Context switching에 메모리와 cpu time이 많이 소모된다.
– 메모리가 부족해지고 처리시간이 급격하게 증가하거나 오류를 발생시킬 수 있다.
• 짧은 작업의 다수의 Client가 들어올 경우 thread를 생성하고 종료
할 때 너무 많은 자원이 소모된다.
• DoS attack과 같은 공격에 취약하다.
14.
Thread pools
• Worker-crew모델 이라고도 한다.
• 여러 개의 threads가 task가 오길 기다리며
concurrent server로서 기능한다.
• Thread의 개수는 workload, cpu 코어의 수,
network socket 등 여러 요인에 따라 바꿀
수 있다.
• 처리속도보다 Client 수가 많아지면 wait
time은 길어지지만 처리속도를 유지한다.
· · ·
· · ·
Task Queue
Completed Tasks
Thread Pool
15.
Blocking vs. nonblockingI/O
Blocking I/O
• 일반적인 I/O API가 작동하는 방식이다.
• Data를 socket에서 받을 때 버퍼가 비어있
으면 block system call 호출
• Block된 프로그램은 CPU를 사용하지 않고
커널 응답을 기다린다.
• 응답이 되돌아오면 어플리케이션 블록이
풀린다.
Nonblocking I/O
• Nonblock 모드에서 recv와 같은 함수는 버
퍼가 비어 있어도 언제나 바로 리턴한다.
• 프로그램은 기다리지 않고 다음 코드를 실
행한다.
16.
Blocking, Nonblocking
Synchronous, Asynchronous
•Blocking/NonBlocking
– 호출되는 함수가 바로 리턴 하지 않으면 Blocking
– 바로 리턴 하면 NonBlocking
• Synchronous/Asynchronous
– 호출되는 함수의 작업 완료를 호출한 함수가 신경 쓰면
Synchronous
– 호출되는 함수의 작업 완료를 호출된 함수가 신경 쓰면
Asynchronous
Select
int select( intmaxfdNum, //파일 디스크립터의 관찰 범위 (0 ~ maxfdNum -1)
fd_set *restrict readfds, //read I/O를 통지받을 FD_SET의 주소, 없으면 NULL
fd_set *restrict writefds,//write I/O를 통지받을 FD_SET의 주소, 없으면 NULL
fd_set *restrict errorfds,//error I/O를 통지받을 FD_SET의 주소, 없으면 NULL
struct timeval *restrict timeout //null이면 변화가 있을 때까지 계속 Block,
//아니면 주어진 시간만큼 대기후 timeout.
);
//반환값 : 오류 발생시 -1, timeout에 의한 반환은 0, 정상 작동일때 변경된 파일 디스크립터 개수
• Single thread로 다중 I/O를 처리하는 멀티플렉싱 통지모
델의 대표적인 방법이다.
19.
Select example
int main(intargc, char** argv){
...
Fd_set readfds_master;
FD_ZERO(&readfds_master);
Fd_set writefds_master;
FD_ZERO(&writefds_master);
FD_SET(listener_sockfd, &readfds_master);
int fdset_max = listener_sockfd;
while (1) {
fd_set readfds = readfds_master;
fd_set writefds = writefds_master;
int nready = select(fdset_max + 1, &readfds,
&writefds, NULL, NULL);
...
• readfds와 writefds를 초기화한다.
• fdset의 크기를 설정하여 select시에
매번 FD_SETSIZE만큼 반복하는 것
을 방지한다.
• Select는 인자로 받은 FD_SET의 값
을 변경시키므로, 복사된 값을 넘긴
다.
20.
Select example
...
for (intfd = 0; fd <= fdset_max && nready > 0; fd++) {
if (FD_ISSET(fd, &readfds)) {
nready--;
if (fd == listener_sockfd) {
...
} else {
fd_status_t status = on_peer_ready_recv(fd);
if (status.want_read) {
FD_SET(fd, &readfds_master);
} else {
FD_CLR(fd, &readfds_master);
}
if (status.want_write) {
FD_SET(fd, &writefds_master);
} else {
FD_CLR(fd, &writefds_master);
}
if (!status.want_read && !status.want_write) {
printf("socket %d closingn", fd);
close(fd);
}
}
• read 할게 있는지 확인한다.
• Server의 listener socket을 등
록한다.
• Read하고 상태를 set, clr 하는
단계이다.
• read, write할게 모두 없는 경
우 소켓을 닫는다.
21.
Select example
05:29:15,864:conn1 connected...
05:29:15,864:conn2connected...
05:29:15,864:conn0 connected...
05:29:15,865:conn1 sending b'^abc$de^abte$f'
05:29:15,865:conn2 sending b'^abc$de^abte$f'
05:29:15,865:conn0 sending b'^abc$de^abte$f'
05:29:15,865:conn1 received b'bcdbcuf'
05:29:15,865:conn2 received b'bcdbcuf'
05:29:15,865:conn0 received b'bcdbcuf'
05:29:16,866:conn1 sending b'xyz^123'
05:29:16,867:conn0 sending b'xyz^123'
05:29:16,867:conn2 sending b'xyz^123'
05:29:16,867:conn1 received b'234'
05:29:16,868:conn0 received b'234'
05:29:16,868:conn2 received b'234'
05:29:17,868:conn1 sending b'25$^ab0000$abab'
05:29:17,869:conn1 received b'36bc1111'
05:29:17,869:conn0 sending b'25$^ab0000$abab'
05:29:17,870:conn0 received b'36bc1111'
05:29:17,870:conn2 sending b'25$^ab0000$abab'
05:29:17,870:conn2 received b'36bc1111'
05:29:18,069:conn1 disconnecting
05:29:18,070:conn0 disconnecting
05:29:18,070:conn2 disconnecting
22.
The limitations ofselect
• 최대 fd의 개수가 1024개로 제한되어 있다.
• fd의 주소를 넘겨주는 것이 아닌 개수만 넘겨주므로 최악의
경우 모든 fd를 다 돌아야 한다.
– fd가 많아질수록 이 overhead가 증가하면서 성능이 낮아진다.
• 단점을 보완한 기법으로는 epoll, iocp, RIO, kqueue 등이 있으
며 linux 환경에서 사용하는 I/O통지 기법은 epoll이다.
• epoll은 기본적인 동작방법은 select와 같지만 read나 write가
준비된 fd를 buffer에 채운다. 준비된 fd를 찾아주기 때문에
select의 O(N)의 복잡도에서 O(1)로 좋아진다.
23.
Epoll
int epoll_create(int size);
//size는epoll_fd의 크기정보를 전달한다.
//반환 값 : 실패 시 -1, 일반적으로 epoll_fd의 값을 리턴
int epoll_ctl(int epoll_fd,
int operate_enum,
//어떤 변경을 할지 결정하는 enum값
int enroll_fd,
//등록할 fd
struct epoll_event* event
//관찰 대상의 관찰 이벤트 유형
);
//반환 값 : 실패 시 -1, 성공시 0
int epoll_wait( int epoll_fd,
struct epoll_event* event,
//event 버퍼의 주소
int maxevents,
//버퍼에 들어갈 수 있는 구조체 최대 개수
int timeout
//select의 timeout과 동일 단위는 1/1000
);
//성공시 이벤트 발생한 파일 디스크립터 개수 반환, 실패시 -1
반환
• size는 epoll_fd의 크기 정보를
전달하고, return값은 epoll_fd
이다.
• 관찰 대상이 되는 fd를 등록,
삭제하는데 사용된다.
• select 의 select()와 같은 역할
이다.
24.
Epoll
struct epoll_event
{
__uint32_t events;
epoll_data_tdata;
}
typedef epoll_data
{
void* ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
enum Events
{
EPOLLIN, //수신할 데이터가 있다.
EPOLLOUT, //송신 가능하다.
EPOLLPRI, //중요한 데이터(OOB)가 발생.
EPOLLRDHUD,//연결 종료 or Half-close 발생
EPOLLERR, //에러 발생
EPOLLET, //엣지 트리거 방식으로 설정
EPOLLONESHOT, //한번만 이벤트 받음
}
• fd와 event, 기타 정보를 묶어
서 만든 구조체이다.
25.
Epoll
struct epoll_event accept_event;
accept_event.data.fd= listener_sockfd;
accept_event.events = EPOLLIN;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listener_sockfd, &accept_event) < 0) {
perror_die(“epoll_ctl EPOLL_CTL_ADD”);
}
struct epoll_event* events = calloc(MAXFDS, sizeof(struct epoll_event));
if (events == NULL) {
die("Unable to allocate memory for epoll_events");
}
while (1) {
int nready = epoll_wait(epollfd, events, MAXFDS, -1);
for (int i = 0; i < nready; i++) {
if (events[i].events & EPOLLERR) {
perror_die("epoll_wait returned EPOLLERR");
}
if (events[i].data.fd == listener_sockfd) {
// The listening socket is ready; this means a new peer is connecting.
...
} else {
// A peer socket is ready.
if (events[i].events & EPOLLIN) {
// Ready for reading.
...
} else if (events[i].events & EPOLLOUT) {
// Ready for writing.
...
}
}
}
}
• Server의 listener socket을 등록한다.
• connect request, read, write
• epoll_wait()에서 event가 일어난 fd만 가져
오기 때문에 FD_ISSET으로 모든 client를 확
인하는 select보다 빠르다.
26.
Libuv(Unicorn Velociraptor Library)
•초기에는 Node.js를 위해 만들
어졌다.
• 멀티플랫폼 C library로 event
loop기반의 AIO를 제공한다.
• epoll, kqueue, IOCP, Solaris
event port를 지원한다.
27.
libuv
• asynchronous, event-driven스타일의 개발을 도와준다.
• event loop을 지원하고 I/O와 다른 작업들을 callback을 기
반으로 실행한다.
• timer, non-blocking network, asynchronous file system
access 등 여러 유틸리티를 제공한다.
28.
libuv example
int portnum= 9090;
if (argc >= 2) {
portnum = atoi(argv[1]);
}
printf("Serving on port %dn", portnum);
int rc;
uv_tcp_t server_stream;
if ((rc = uv_tcp_init(uv_default_loop(),
&server_stream)) < 0) {
die("uv_tcp_init failed: %s", uv_strerror(rc));
}
struct sockaddr_in server_address;
if ((rc = uv_ip4_addr("0.0.0.0", portnum,
&server_address)) < 0) {
die("uv_ip4_addr failed: %s", uv_strerror(rc));
}
if ((rc = uv_tcp_bind(&server_stream, (const struct
sockaddr*)&server_address, 0)) < 0) {
die("uv_tcp_bind failed: %s", uv_strerror(rc));
}
if ((rc = uv_listen((uv_stream_t*)&server_stream,
N_BACKLOG, on_peer_connected)) < 0) {
die("uv_listen failed: %s", uv_strerror(rc));
}
• socket create, socket bind,
listen 과정이다.
• uv_listen 실행중에 새로운
connection을 받으면 callback
으로 on_peer_connected 를
호출한다.
libuv example
void on_peer_connected(uv_stream_t*server_stream, int status) {
...
uv_tcp_t* client = (uv_tcp_t*)xmalloc(sizeof(*client));
int rc;
if ((rc = uv_tcp_init(uv_default_loop(), client)) < 0) {
die("uv_tcp_init failed: %s", uv_strerror(rc));
}
client->data = NULL;
if (uv_accept(server_stream, (uv_stream_t*)client) == 0) {
struct sockaddr_storage peername;
int namelen = sizeof(peername);
if ((rc = uv_tcp_getpeername(client, (struct sockaddr*)&peername,
&namelen)) < 0) {
die("uv_tcp_getpeername failed: %s", uv_strerror(rc));
}
report_peer_connected((const struct sockaddr_in*)&peername, namelen);
peer_state_t* peerstate = (peer_state_t*)xmalloc(sizeof(*peerstate));
peerstate->state = INITIAL_ACK;
peerstate->sendbuf[0] = '*';
peerstate->sendbuf_end = 1;
peerstate->client = client;
client->data = peerstate;
uv_buf_t writebuf = uv_buf_init(peerstate->sendbuf, peerstate-
>sendbuf_end);
uv_write_t* req = (uv_write_t*)xmalloc(sizeof(*req));
req->data = peerstate;
if ((rc = uv_write(req, (uv_stream_t*)client, &writebuf, 1,
on_wrote_init_ack)) < 0) {
die("uv_write failed: %s", uv_strerror(rc));
}
} else {
uv_close((uv_handle_t*)client, on_client_closed);
}
}
• 새로운 client를 받을 tcp handle을 loop에
init 해준다.
• client와 server가 연결되었다.
– 함수명이 getpeername이지만 주소를
구한다.
• 연결 후 최초 response할 초기 설정을 한다.
• uv_write를 호출하고 on_wrote_init_ack를
callback한다.
31.
libuv example
void on_wrote_init_ack(uv_write_t*req, int status) {
if (status) {
die("Write error: %sn", uv_strerror(status));
}
peer_state_t* peerstate = (peer_state_t*)req->data;
// Flip the peer state to WAIT_FOR_MSG, and start
listening for incoming data
// from this peer.
peerstate->state = WAIT_FOR_MSG;
peerstate->sendbuf_end = 0;
int rc;
if ((rc = uv_read_start((uv_stream_t*)peerstate-
>client, on_alloc_buffer,
on_peer_read)) < 0) {
die("uv_read_start failed: %s", uv_strerror(rc));
}
// Note: the write request doesn't own the peer state,
hence we only free the
// request itself, not the state.
free(req);
}
• sendbuf에 있는 data를 다 보냈으니 state를
WAIT_FOR_MSG로 변경하고 sendbuf_end
를 0으로 변경한다.
• uv_read_start를 호출하고 on_peer_read를
callback한다. on_peer_read는 읽을 data가
없거나 uv_read_stop이 호출될 때 까지 여
러 번 호출된다.
32.
libuv example
void on_peer_read(uv_stream_t*client, ssize_t nread, const uv_buf_t* buf) {
...
if (nread == 0) {
// from the documentation of uv_read_cb: nread might be 0, which does not
// indicate an error or eof. this is equivalent to eagain or ewouldblock
// under read(2).
} else {
// nread > 0
assert(buf->len >= nread);
peer_state_t* peerstate = (peer_state_t*)client->data;
if (peerstate->state == initial_ack) {
free(buf->base);
return;
}
for (int i = 0; i < nread; ++i) {
...
}
if (peerstate->sendbuf_end > 0) {
// we have data to send. the write buffer will point to the buffer
stored
// in the peer state for this client.
uv_buf_t writebuf =
uv_buf_init(peerstate->sendbuf, peerstate->sendbuf_end);
uv_write_t* writereq = (uv_write_t*)xmalloc(sizeof(*writereq));
writereq->data = peerstate;
int rc;
if ((rc = uv_write(writereq, (uv_stream_t*)client, &writebuf, 1,
on_wrote_buf)) < 0) {
die("uv_write failed: %s", uv_strerror(rc));
}
}
}
free(buf->base);
}
• data를 읽고 처리한다.
• uv_write를 호출하고 on_wrote_buf를
callback한다.
33.
libuv example
void on_wrote_buf(uv_write_t*req, int
status) {
if (status) {
die("Write error: %sn",
uv_strerror(status));
}
peer_state_t* peerstate =
(peer_state_t*)req->data;
// The send buffer is done;
//move pointer back to 0.
peerstate->sendbuf_end = 0;
free(req);
}
• loop cycle 완료