1.
문서의 목적 Ver 1.1
M$에서는 서버/클라이언트간에 통신을 함에 있어서 5가지 형태의 함수를 제공하고 있다. ( 소캣의 IO상태를 검사하는 방법을 기준으로할 때 )
1) select 를 사용하는 방식
2) 윈도우를 필요한 WSAAsyncSelect 를 사용하는 비동기 셀렉트 방식
3) WSAEventSelect 를 사용하는 이벤트 셀렉트 방식
4) Overlapped IO를 사용하는 방식
5) IOCP를 사용하는 방식
지금의 우리는 서버라고 하면 모두들 IOCP를 사용하려고 한다. 물론 검증된바 IOCP가 최고의 효율을 내기때문이다. 하지만 왜 IOCP를 해야하는지에 대한 의문은 한번씩 가진적이 있을것이다.
이 문서는 각각 형태의 IO검출 방법에서, 그 코딩방법을 비교함으로써 IOCP를 사용해야되는 당위성에대해 본인의 극히 주관적인 관점으로 설명하고저 한다. 이론적인 이야기는 여러 필자들과 각종 서적에서 훓어주고 지나갔기 때문에 구구절절 적지는 않겠다. 실무위주로 적겠다는 이야기다.
문서의 난이도는 중급정도 될까 ?!
소스와 코딩방법은 여러책들과 컬럼들에서 많이 다루었기 때문에 이번에 제외시켰다.
( 참고로 본인은 1),2),5)번 방식으로 구성된 서버를 운영했다. )
'최대 다수의 최대 행복' 이것은 벤덤과 밀의 공리주의에 대한 이야기다. 벤덤은 양적공리주의 즉 많은 인간이 많이 행복하면되는 것이고, 밀은 질적공리주의 즉 양도 중요하지만 그 질(정신적인것)도 중요하다는 이야기를 했다고 한다. 뭐… 당연한 이야기잖아.
최고의 효율을 내는 서버는 최대한 유저를 많이 붙여서 최대한 빠르게 요청을 처리해서 알려주면되는것이다. 지극히 경제학적인 관점으로 봐야한다. 최대한 유저들이 붙어서 최대한 양적/질적 만족을 하면 된다는것이다.
& 서버 프로그램을 하다보면 서버당 몇 명 유저까지 붙일수있느냐고 묻는 사람이 있다.
지극히 어려운 질문이다.
SOCKET 변수는 winsock.h 파일에 다음과 같이 저장되어 있다.
typedef u_int SOCKET;
데이터형이 unsigned int 이므로 대략 42억개정도의 소캣을 붙일수있다.
그럼 질문한 사람에게 42억개라고 말해주기에는 나의 공력이 허락하지 않는다.
소캣 하나가 생성되면 유저당 생성되는 수신/송신버퍼와 각종 변수들을 위한 메모리가 할당되야하기 때문에 , 서버 박스의 메모리에 따라 접속자가 결정되는 측면도 무시할수없다.
가령 서버박스의 메모리가 1기가이고 유저당 송신버퍼 64K, 수신버퍼 64K, 각종변수가 2K 해서 , 대략 130KB의 메모리를 사용한다면 붙을 수 있는 유저의 수는 7692가 되겠다. (1GB / 130KB = 7692 , 물론 다른 프로그램이 사용하는 메모리는 0으로 두고선.)
하지만 이것은 서버에 붙은 유저가 아무짓도 하지않는다는 가정을 둘때고 , 실제로는 다음의 내용들까지 고려해야 서버당 접속 가능한 유저수를 산출할수있다. (다음의 이야기는 얼마나 빠르게처리해서 모든 유저들을 만족시켜 줄수있을까라는 문제와 연관이 있다.)
l 유저들의 각종 요청에 따른 서버의 커맨드처리 작업시 소요되는 CPU 연산에의한 부하량을 고려 ( 어플리케이션에서 사용하는 CPU 부하량 )
l 팩킷의 송수신시에 발생되는 CPU의 부하 고려 ( 소캣 라이브러리에서 사용하는 CPU 부하량 )
l 서버가 다시 송신해 주는 팩킷의 양에 따라서, 서버를 운영하는 서비스 제공자가 할당받은 네트웍 Bandwidth의 허용하는 범위 고려.
l 내부 동적 메모리 버퍼의 증감량에 따른 메모리 문제가 생길수있는지 문제 고려
l DB 처리에 따른 병목 현상으로 서버 효율 감소 고려
위의 사항들은 철저하게 어떤 제품을 만들려는 그 회사의 운영 및 설계에 따라서 달라지기 때문에 처음에 이야기했던 서버당 접속 유저수에 대한 질문은 , 반대로 질문자가 서버에서 어떤 일을 하고싶은지에대한 답을 줘야 해결해 줄수있게 된다.
& CPU의 활용문제
서버를 돌리는데 적정한 CPU의 부하는 몇 % 일까 ?
정답은 유저들이 요청한 것을 처리하고 결과를 수신 받을 때 오래 걸리지 않았다고 느껴질만큼 서버의 CPU가 소요되고 있는 상태가 적정한 수준이다.
나의 경우는 지속적으로 서버의 CPU가 80~90% 차지하고 있지 않는다면, 수초에서 수분까지는 그 CPU가 80~90% 점유하고 있어도 문제는 없었다.
그런데 CPU의 사용이 너무 적은것도 문제가 된다. 메모리도 충분하고 팩킷을 보내는양도 적고 CPU도 계속 10~20%정도밖에 사용하고 있지 않다면 , 자원의 낭비가 있으므로 더 많은 유저수를 받을수있도록 시스템을 구성하는것도 좋을것이다.
음. 지극히 뻔한 이야기다.
그래도 서버는 돌아야한다.
3. Input / Output 발생의 검출 및 애플리케이션에 통지
그림1) 팩킷 수신 및 처리 그리고 송신
아래의 그림2),3)는 위 그림1)의 내용중 통신 라이브러리에서 사용되는 쓰래드와 함수의 관계를 그려본 것이다. 나의 경우는 검출하는 쓰래드가 멀티냐 싱글이냐에 따라서 대략 두가지 방법을 사용하고 있다.
그림 2) 검출 쓰래드가 싱글. (나의 경우 select , WSAAsyncSelect에 적용) |
IO 검출 싱글 쓰래드
( Receive, Send, Close ) |
Recv Thread
(싱글/멀티쓰래드) Parsing |
Send Thread
(싱글/멀티쓰래드)
( send list의
내용 전송 ) |
IO검출에서 send 상황 검출은 select , WSAAsyncSelect 두방식이 서로 다름, 뒤에설명
그림 3) 검출 쓰래드가 멀티. (나의 경우 IOCP 에 적용) |
IO 검출 멀티 쓰래드
( Receive, Send, Close ) |
Send Thread
(싱글쓰래드)
( send list의
내용 전송 ) |
위의 그림들은 라이브러리 구성하는 사람들마다 다르기 때문에 절대적인 것은 아니다.
일단 IO의 검출과 그 통지에 대해서 알아보자. 크게 보면 두가지 경우라고 볼수있다.
1) 유저 라이브러리에서 IO의 검출 상황을 항상 체크해서 상태를 알아 내는 방법.
2) 시스템이 IO의 검출이 있을 때 이벤트를 통해 알려주는 방법.
사실 1), 2)의 방식 모두 어떤 IO에 대한 상태가 바뀌면 그것에 대한 상태변화는 시스템이 알려주게 되어 있다. 다른점은 이번에 IO 상태의 변화가 생겼을 때 Receive, Send, Close 들중에 어떤 IO의 상태 변화인지를 직접 알려주는지 , 아니면 라이브러리에서 확인하는 루틴을 넣어야 하는지에 대한 차이이다. 이것은 개발 효율 및 라이브러리 속도에 아주 큰 영향을주는 부분이다.
1)의 방식은 select, WSAEventSelect 방식에서 사용하고 2)의 방식은 WSAAsyncSelect, IOCP에서 사용하고 있는 방법이다. 특이하게 Overlapped IO에서는 두가지 모두 지원을 한다.
전체 다이어그램을 보면서 이야기해보자.
위의 그림 2)와 그림3) 사이에 크게 별다른 것은 없다.
다만 검출하는 쓰래드가 멀티냐 싱글이냐에 따라서 그것을 수신해서 처리하는 함수를 쓰래드로 해야할지 아니면 바로 호출되는 함수로 해야할지가 달라진다는 것이다.
그림2)의 경우를 보자.
Receive가 된 것을 검출하면 해당 팩킷을 Receive 팩킷 리스트에 저장을 한다. Receive 쓰래드에서는 Receive 팩킷 리스트에서 내용을 가져와 유저별로 리시브버퍼에 넣어주고 , 역시 쓰래드에서 순차적으로 유저별 리시브 버퍼의 내용을 파싱 한뒤에 커맨드 처리 함수 부분으로 보낸다.
이런 방법도 있다. Receive가 된 것을 검출하면 Receive팩킷 리스트에 넣지 말고 바로 유저별 리시브 버퍼에 넣어주는 방법도 있다. 그리고 리시브 쓰래드(이때는 파싱 쓰래드라고 불러주는 것이 좋을거같다.)에서는 순차적으로 파싱을 해서 커맨드 처리 함수 부분으로 보내주면 된다. 속도는 위의 방법보다 이것이 팩킷을 복사하는 횟수가 줄일수 있기 때문에 더 빠른 처리를 보장한다. 하지만 리시브 버퍼가 비워지지 않았다면 복사를 할수없어서 버퍼가 비워질때까지 , 즉 파싱되어서 커맨드가 처리될때까지 기다려야되는 문제가 생긴다.
리시브 쓰래드는 싱글또는 멀티로 둘수있는데, 서버 생성시에 미리 멀티 쓰래드를 여러 개 만들어둔뒤에 유저에 따라서 쓰래드를 구분하고 동시에 파싱을 한다면 더 높은 처리율을 보일것이다.
첫번째 방법은 대용량서버에서 유용하고 두번째 방법은 서버가 수신해야될 팩킷의 양이 적은 서버에서 유용한 방법이다.
그림 3)처럼 멀티쓰래드로 검출이 된다면 바로 수신 처리 함수로 연결을 해서 따로 쓰래드를 만들지 않아도 되는 편리한점이있다. 물론 멀티로 수신 검출을 해서 리시브 리스트를 구성한뒤에 , 리시브-파싱 쓰래드에서 읽어서 쓰도록 만들어도 되겠지만 , 처리 속도를 조금이라도 더 올릴 생각이라면 그러한 작업은 불필요하다고 생각한다.
Send Thread는 다음의 작업을한다. 유저의 요청에 따라 서버가 응답 팩킷을 만든뒤에 send packet list에 넣은 것을 send(), WSASend()함수 호출에 의해서 송신하는 역할을 한다.
Receive의 검출뿐아니라 Send의 검출 또한 IO 검출 쓰래드에서 이루어진다.
Send의 검출은 해당 소켓으로 팩킷을 보낼수있는가에 초점이 맞추어진다. 하지만 이 Send에 대한 검출은 다음에서 소개하는 여러가지 IO방식에 따라서 달라지기 때문에 각각의 항목에서 설명하도록 하겠다.
그럼 정리를해서 이상적인 IO 통지 구조를 그려보자.
수신 이벤트 통지 송신 결과 통지 send packet list 전달
send packet list 추가
그림 4) 이상적인(?) IO 통지. (나의 경우 실제 IOCP에 적용 시킨 방식) |
IO 검출 멀티 쓰래드 ( Receive, Send, Close )
|
l 일단 IO의 검출은 무조건 멀티쓰래드여야 한다.
l Receive가 검출되었다면 얼마만큼의 데이터를 읽어야하고, 어떤 소켓에서 검출되었는지 이벤트로 통지해 줘야한다.
l 그러면 Recv 함수에서 해당 소켓에게 날라온 데이터만큼 읽으면 된다. (실제 IOCP에서는 다른 방법을 사용한다. 뒤의 IOCP설명에 추가하겠음) 읽혀진 데이터는 Recv Buffer에 들어간다.
l Send의 검출은 송신 결과 통지를 통해서 현재 내가 유저별 Send Buffer에서 팩킷 송신 함수를 통해서 보낸것중에서 얼마만큼이 보내졌는지 알려줘야한다. 즉 이것은 Send 완료의 통지이다. Send 완료의 통지를 해줘야 더 팩킷을 보낼수있는 없는지 알게 되기때문이다.
l 만약 보낼려고했던 팩킷보다 더 적은 팩킷을 보냈다고 통지가 오면, Send함수에서는 보내진 부분외에 부분만 더 송신함수를 이용해서 보내면 된다.
l Send Thread는 커맨드 처리부에서 만들어진 send packet list의 내용을 읽어서 유저들의 Send Buffer에 넣어주고, 팩킷 송신 함수를 호출하는 일을 한다. 이때 해당 유저의 Send Buffer가 가득차 있다면 더 이상 팩킷을 send packet list에서 읽어서 넣어주는 읽을 하지않는다.
l Receive와 Send 완료 결과의 통지는 리스트로 구성되어 큐에 들어가 있어야 한다. 윈도우 동기화개체중에 이벤트 방식으로 하는것도 좋지만 이벤트만 사용하면 데이터를 같이 넣어서 보낼수없기 때문에 데이터 리스트 큐도 이용해야한다. Recv함수/Send함수에서는 리스트 큐에 있는 것을 꺼내와서 어떤 작업이 이루어졌는지 확인을 해야하기 때문이다. IO 검출 멀티쓰래드에서는 데이터 리스트큐에서 꺼내오는 작업을 한뒤에 Recv함수와 Send함수 호출을 하게 된다.
l 어떤 소켓이 검출되었는지는 SOCKET 타입의 핸들을 알려주는것보다 직접 해당 소켓의 메모리 포인터 오브젝트(클래스 또는 구조체 포인터)를 알려주는 것이 편하다. 프로그래머가 해당 SOCKET를 가지고 링크드 리스트를 돌려서 객체를 찾는것보다 속도 면에서 훨씬 이익이기때문이다.
l Accept Thread는 개별적으로 항상 돌면서 유저들이 접속해 오는지 검사를 한다.
사실 이러한 조건을 만족하는 소켓 프로그램방식이 바로 IOCP 이다. 물론 다음에서 제공하는 5가지 방법을 이용해서 위의 조건들을 만족하는 통신 라이브 러리를 구성할수도 있다. 하지만 이미 IOCP에서 제공되는 기능을 다시 만드는 무모한짓을 하는 것이 의미있는지는 모르겠다. (학술적 차원에서 접근은 배제하고… ^.^ )
4. select 방식
이 함수는 울트라 무식 함수의 결정체인 동시에, 그 구조의 단순함으로 소켓 세상을 한동안 풍미했던 구시대의 유물이다. ( 하지만 아직도 난 이놈으로 구성한 서버를 쓰고 있다. ) 왜 함수의 이름이 ‘선택’ 인지 각자 생각해보자.
Select 방식으로 서버를 구성하려면 아래의 것들을 생각해야한다.
l 검출 쓰래드에서 select 로 한꺼번에 걸수있는 최대 소켓은 1024개이다. – 문제점1)
l 문제점1)을 해결하려면 이에 따라서 select하는 부분을 쓰래드로 나눈다고 할 때 한 쓰래드에서 관리해 줘야할 유저의 수를 정해야 한다.
한가지! select 방식에서는 Send Thread가 별로 필요가 없다. 커맨드 처리가 된 것을 Send packet list에 넣은뒤에 검출 쓰래드에서 select를 이용해 팩킷을 보낼수있는지 검사를 한뒤에 송신을 하면 되기때문이다. (한 쓰래드당 처리할 유저수를 정한뒤에 해당쓰래드에서 읽을것이 있는지, 보낼수있는지, 접속이 되었는지, 종료가 되었는지 검사하는 루틴을 넣는 방식을 이용하기 때문이다.)
문제점1)에 대해서 생각해보자.
Winsock2.h 파일을 인클루드하기 전에 FD_SETSIZE값을 새로 #define해서 최대값인 1024로 바꾸지않는다면 초기값은 64로 인식이 된다. 즉 최대 64명뿐이 받을수없는것이다. 그럼 1000으로 바꾸어서 사용한다고 생각해보자.
그림2)에서 처럼 싱글 쓰래드로 간다고 하면, 한번에 select에서 걸수있는 유저의 수가 1000명이기 때문에 유저가 1200명이라고 하면 , 먼저 1000을 select해서 receive할것이 있는지, send가능한지 파악을 한뒤에 다시 200명을 또 select 해야한다. 만약 수신과 송신할것의 개수가 매우 많다면 서버 다른 유저들것까지 처리가 느려지므로 문제가 발생된다. 이것은 서버의 효율면에서 매우 불리하다.
그럼 그림3)의 구조로 가보자. 유저를 200명 단위로 잘라서 쓰래드를 만든다고 해보자. 쓰래드 생성삭제시에 속도감소를 피하기위해서 미리 유저 제한을 2000명으로 두고 , 쓰래드도 10개를 만들어 두었다고 하자. 위의 경우보다 분명히 효율은 올릴수 있을것이다. 하지만 이때도 200명중 누군가의 팩킷 처리를 위해서 다른 사람들은 대기하고 있어야한다. 하나의 IO에 하나의 프로세스밖에 진행을 하지 못하기 때문이다.
전반적 이해를 돕기위해서 IO를 관리하는 구조를 간단하게 그려보았다.
그림 5) select 방식을 이용 IO검출 구조도 |
Recv Thread
(싱글/멀티쓰래드) Parsing |
Send Thread
(싱글/멀티쓰래드)
- send 가능 검출
- send list의
내용 전송 |
일단 Select 방식에서 사용하는 다음의 구조체와 매크로를 이해 해야한다.
typedef struct fd_set {
u_int fd_count; /* how many are SET? */
SOCKET fd_array[FD_SETSIZE]; /* an array of SOCKETs */
} fd_set;
select 방식에서는 fd_set 구조체로 read,write,except 세가지 선언을 해서 사용한다.
보면 알겠지만 fd_count는 해당 fd_set 안에 들어있는 소켓의 갯수 , 그리고 그 소켓들의 배열로 이루어져 있다. (WINDOWS.H 안에 들어있다.)
FD_CLR : fd_array 배열에서 지정된 SOCKET을 찾아서 삭제한다.
배열에서 빠진곳은 뒤의 SOCKET들을 끌어서 다시 채워준다. <- 속도 낭비
FD_SET : fd_array 배열의 마지막에 지정된 SOCKET을 추가한다.
FD_ZERO : fd_count의 값을 0으로 바꾼다.
FD_ISSET : fd_array 배열에 지정한 SOCKET이 들어있는지 검사를 한다.
Select방식의 기본 작업 순서는 다음과 같다.
1) FD_ZERO 으로 새로 소켓을 지정한다고 셋팅을 한다.
2) FD_SET으로 fd_set 변수에 현재 접속된 모든 소켓을 배열에 입력해준다. 이때 순차적으로 배열에 FD_SETSIZE로 지정된 한계까지 넣을수있다.
3) select를 이용해서 read/write/except 세가지 fd_set 을 걸어준다.
4) FD_ISSET를 이용해서 read/write/except 세가지 상황을 검사한다.
그림 5)를 쓰래드 기능별로 설명해 보겠다.
Receive, Close 검출 싱글 쓰래드
Receive할것이 있는지(Read확인) 소켓이 끊어졌는지 검사를 한다. 만약 Receive할것이 있다면 내용을 읽어서 Receive Packet List에 넣어준다. 이 쓰래드는 싱글 쓰래드로 하는 대신에 읽는 속도를 최대한 올리는데 관점을 둔다. 서버는 읽어야할것들을 최대한 빨리 읽어들여야하기때문이다.
Recv Thread
Receive Packet List에 있는 내용을 가져와서 유저별 수신 버퍼에 복사를 한뒤에 해당 팩킷을 파싱하고 커맨드처리부분까지 전달하는 역할을 한다. 이 쓰래드는 유저의 수가 몇 백명이상된다면 멀티로 하는것도 괜찮은 방법이다.
Send Thread
send 가능한지 select함수를 이용해서 검사를 하고 , 가능하다면 send packet list에 있는 내용을 전송을 한다. 이 쓰래드 역시 유저의 수가 많다면 멀티로 하는 것이 좋다.
그런데 Receive, Close 검출 싱글 쓰래드와 Recv Thread가 꼭 두개로 분리되어야할 필요는 없다. Receive, Close 검출 싱글 쓰래드를 멀티로 바꾼뒤 , Recv Thread를 사용하지 않고 파싱과 커맨드 처리부의 연결을 Receive, Close 검출 쓰래드에서 해도 문제는 없다. 다만 이 경우 서버가 receive하는 양이 너무 많지 않아야 효율을 올릴수가 있다.
select 방식은 IO의 검출을 소켓 배열에 있는것들을 하나하나씩 비교함으로써 알아내고 있다. 이것은 치명적으로 속도의 저하를 가져온다. 멀티쓰래드로 select를 구현한다고 해도 유저를 쓰래드별로 관리해야하는 불편함이있다.
만약 IO 검출이 멀티쓰래드에서 되고, 쓰래드별 유저의 분리가 자동으로되며 그 IO 검출에의한 통지가 자동으로 되고, IO에대한 수신/파싱/전송등의 처리가 유저별로 쓰래드에 분산되어 각각 할수있다면 서버의 속도가 매우 올라갈 것이다.
5. WSAAsyncSelect 방식
이것은 위의 select 방식보다 매우 편리하다.
WSAAsyncSelect 의 경우는 윈도우를 하나 만들어야 한다. 그 이유는 만든 윈도우에서 IO의 검출이 자동으로 되고 , 그 윈도우의 메시지 큐에 저장이 되기때문이다.
개발자는 단지 WSAAsyncSelect를 이용해서 생성한 윈도우 핸들을 소캣과 매핑시켜주고 , 메시지큐에 있는 내용을 받아서 각각의 이벤트에 맞게 처리만 해주면 되기때문이다.
이 방식에서 이전의 그림2)에서있는 싱글 쓰래드검출부분을 별도로 만들어주지않아도 된다. 시스템의 메인쓰래드에서 그 검출부분을 해주기때문이다.
기본적으로 WSAAsyncSelect는 앞에서 이야기했던 IO의 멀티 쓰래드 검출이라는 원칙에는 위배가된다. IO검출 자체를 시스템으로 넘겨줬기 때문에 싱글 쓰래드에서 검출해서 메시지 큐로 들어오기때문이다.( 솔직히 말하면 그럴것이라는 추측! )
메시지큐에서는 FD_ACCEPT, FD_READ, FD_WRITE, FD_CLOSE, FD_CONNECT, FD_OOB 6가지 신호를 받을수있다. Select방식과 달리 어떤 IO가 이루어지고 있는지 구별해 내는 코드가 필요없고 , 개발자는 해당 신호가 발생된것에대한 작업만 해주면 되는것이다.
이것들중에 중요한 FD_ACCEPT, FD_READ, FD_WRITE를 이야기해 보자.
FD_ACCEPT 이벤트
Select방식에서는 accept()를 호출하고 대기상태로 기다렸지만 , WSAAsyncSelect 에서는 이벤트로 이것이 뜨기 때문에 꼭 Accept Thread를 만들어서 처리할 필요는 없다. 하지만 동기화 개체를 이용할 때 일관성을 유지하고, Accept 처리를 빨리하기위해서는 Accept List Queue를 만든뒤에 유저가 들어와서 이벤트가 발생하면 list에 넣었다가 Accept Thread에서 꺼내어 쓰는 것이 여러모로 편리하다.
FD_WRITE 이벤트
WSAAsyncSelect 방식에서 FD_WRITE가 뜨는경우는 다음과 같다. (** 매우 중요)
l WSAAsyncSelect 에서 FD_WRITE가 지정 될 때 즉 소캣과 매핑이 될 때
ex) WSAAsyncSelect(m_Sock,m_hWnd,m_RecvMsg
,FD_WRITE|FD_CONNECT|FD_READ|FD_CLOSE)
l WSAEWOULDBLOCK 발생된 뒤에 일정한 시간이 지나서 이것이 해지되고, send 할 수 있는 상태가 되었을 때
WSAAsyncSelect 방식은 자동으로 넌블러킹 소켓으로 바꾸어 준다. 넌 블러킹 모드에서 send()함수를 호출하고 그 결과를 기다리지 않기 때문에 , 송신한 팩킷이 모두 갔는지 안갔는지 확인을 하지 않고 바로 또 send()를 호출하게 된다. ( 이방식에서는 Send Thread에서 send packet list에 있는 팩킷들을 열나게 send()를 하다가 그만보내라는 신호가 발생되면 그 상태가 풀릴때까지 기다렸다가 또 열나게 보내는 방식이다. – 무식하다 )
이때 그만보내라고 발생되는 send() 오류가 WSAEWOULDBLOCK 이것인데 , 오류라고 보기보다는 경고라고 봐야 할것이다. 이것이 발생되면 해당 유저의 소켓에 대해 송신을 정지시키고 FD_WRITE가 발생될때까지 더 이상 팩킷을 보내지말고 기다린다. FD_WRITE가 발생되면 해당 유저 소켓의 남아있는 데이터를 전송하면 되는것이다.
FD_READ 이벤트
FD_READ의 처리는 WSAAsyncSelect를 이용해서 고성능 서버를 만드는데 있어서 제일 문제가 된다.
이 방식에서 기본적 로직은 FD_READ신호를 받으면 ioctlsocket()함수를 호출해서 읽어야할 데이터 크기를 확인한후에, recv()함수를 호출해서 모든 데이터를 읽어들일때까지 반복적으로 recv()함수를 호출하면 된다는것이다.
그런데 문제는 읽어할 내용이 생기면 윈도우 메시지 큐로 FD_READ 신호가 들어가게 된다는 것이다. 즉 유저들의 접속이 많아서 반복적으로 유저들로부터 어떤 데이터가 들어오면 윈도우 메시지큐가 가득차게되어 더 이상 FD_READ신호가 발생되지않는 매우 골치아픈 상황이 생긴다는 점이다. 물론 빨리 빨리 recv()를 해서 메시지큐가 가득차지않게 하면 되지만 서버가 유저들의 요청을 처리하다보면 그럴수없는 상황이 생기기도 한다. 서버에 1000명 가량 붙어서 많은 요청이 들어오면 1~2주에 한번씩 그런 일이 발생하는 것을 보았다. 이것은 반대로 서버에서 클라이언트로 신호를 보낼때도 발생될수있다. 한꺼번에 서버에서 많은량의 팩킷을(작은사이즈로 많은횟수) 발송하면 클라이언트가 더 이상 팩킷을 받지못하는 불상사가 생길수있다. 기회가 되는 사람은 실험을 통해서도 쉽게 만들어 낼수가 있다.
해결 방법은 있다. 하지만 이 방법은 다른 문제를 발생시킨다. 다음을 보자.
처음 FD_READ신호를 받으면 ioctlsocket()를 호출하고 바로 다음과 같은 호출을한다.
WSAAsyncSelect(m_Sock,m_hWnd,m_RecvMsg,FD_WRITE| FD_CLOSE)
즉 다시 FD_READ신호를 받지않는다는것이다.
대신 루프를 돌면서 계속 ioctlsocket()를 체크해서 더 이상 읽어들일것이 없을 때 까지 모든 내용을 다 읽고 , 더 이상 내용이 없으면 반복루프를 빠져나가고 다음의 함수를 호출한다.
WSAAsyncSelect(m_Sock,m_hWnd,m_RecvMsg,FD_WRITE| FD_READ|FD_CLOSE)
다시 FD_READ 신호를 받는다는것이다.
그리고 함수를 빠져나간다.
이렇게 하면 더 이상 recv()할것이 없을때까지 계속 팩킷을 읽어들이게 되어서 , 따로 FD_READ신호를 받을필요도 없고, 메시지큐에 FD_READ 가 넘칠때까지 쌓이는 문제도 피할수있다.
하지만 여기서도 문제는 있다. 위의 FD_WRITE 가 발생되는 조건에 대한 이야기중에서 WSAAsyncSelect 에서 FD_WRITE가 지정 될 때 즉 소캣과 매핑이 될 때 라는 이야기가 있다. 다음과 같은 상황을 생각해 보자.
a) 서버에서 어떤 클라이언트에게 팩킷을 발송한다.
b) 클라이언트까지의 네트웍이 느리거나 , 클라이언트가 recv()하는것이 느려서 서버가 제대로 보내지못하고 결국 WSAEWOULDBLOCK 이 발생되었다.
c) 정상적이라면 WSAEWOULDBLOCK이 풀리면 FD_WRITE 가 발생되어야 한다. 하지만 그 유저에 대한 FD_READ 할것이 있어서 WSAAsyncSelect()를 호출하다가 보면 정상적으로 WSAEWOULDBLOCK이 풀려서 FD_WRITE가 발생되어야하는데 그전에 위의 WSAAsyncSelect() 함수 호출에 의해서 FD_WRITE가 신호가 발생되는 불상사가 생기게 된다.
d) 서버는 해당 클라이언트에대한 FD_WRITE가 발생된 것을 보고 우드 블록이 풀렸는지 알고 더 팩킷을 보내게 되고, 결국 또 우드블럭이 발생되는 문제가 생길수있다.
결국 WSAAsyncSelect 방식에서는 고성능 서버를 만들기위해서는 우드블럭 문제가 생겼을 때 문제처리를 어떻게 해줘야하는지가 관건이 될수있다.
Select방식에서는 보낼수있는지 검사를한뒤에 보내기 때문에 속도 자체는 느리지만 우드블럭이생기는 것은 줄일수있지만, 이 방식에서는 보낼수있는 상태를 검사하지않기 때문에 문제가 될수있는것이다. (WSAAsyncSelect를 사용하면서 send()를 하기전에 select를 써서 보낼수있는지 상태를 검사한뒤에 보내는방법은 적용해 보지않았다. 관심있는사람은 한번 해보시길…)
- select 방식을 사용한다고 해도 우드블럭이 전혀 생기지않는 것은 아니다 , 다만 보내기전에 항상 루틴이 있기 때문에 그 발생빈도를 줄일수있는 잇점이 있다.
나의 경우는 서버의경우. FD_WRITE를 이용해서 우드블럭이 풀리는 시점을 검사하는 것을 포기하고 , 최초 우드블럭이 발생되면 더 이상 해당 소켓으로 팩킷을 전송하지않고 일정한 시간동안 대기한뒤에 다시 팩킷을 발송한다 . 그래도 우드블럭이 풀리지않으면 그 팩킷은 지워버린다. (헉!)
클라이언트의 경우는 서버로 보낼 때 클라이언트에서 우드블럭이 발생되었다는 것은 서버가 바쁘다는것이므로 FD_WRITE를 이용해 우드블럭 풀림을 검사해서 작업을 하는것이좋다.
끝으로 생각해볼것이 있다.
만약 TCP단의 send 버퍼가 다 차서 더 이상 보낼수없는 상태 , 즉 우드블럭 상태를 어플리케이션단에 감지할수있다면 어떨까 ?
그러면 어플리케이션단에 send 버퍼를 두고 그 내용이 확실히 전송되었는지 확인되면 보낸만큼 버퍼에서 지우고 , 다시 send 버퍼에 보낼 내용을 복사해주면 되기때문이다.
IOCP가 해결책을 제시해준다.
참고) WSAEWOULDBLOCK
우드블럭 에러라고 하지만 실제로 에러로 보기는 문제가있다. 그냥 시스템의 상태를 알려준다고 생각하자.
Send()에서 우드블럭메시지는 논블럭킹 소켓에서 자주 발생이 된다. Send()를 하고 그 결과를 기다리지않기 때문에 계속해서 TCP로 팩킷을 보내게 되고 TCP단에 버퍼를 초과하게되면 결국 WSAEWOULDBLOCK메시지가 발생하게 되는것이다.
6. WSAEventSelect 방식
많은 부분 WSAAsyncSelect 방식과 유사하다. 다른점은 IO 검출의 통지를 윈도우 메시지를 통해서 하지않고 유저가 만들어준 이벤트 동기화 개체를 이용한다는 점이다.
(사실 이방법으로 대용량 서버를 운영해 보지않았기 때문에 내용을 적는다는 것이 지나친 자만이겠지만 그냥 주관적인 나의 생각을 적어보겠다. )
WSAEventSelect방식은 하나의 소켓이 생성될때마다 이벤트를 하나씩 만들어서 해당 소켓과 이벤트를 맵핑시키는 개념이고 , 그 매핑을 시키는 함수가 WSAEventSelect이다. 위의 WSAAsyncSelect와 달리 윈도우를 만들지 않아도 되기 때문에 더 깔끔(?)한 프로그래밍을 할수있다는 장점도 있지만 다음의 불편한 점이 있다.
l WSAAsyncSelect 방식에서는 IO의 검출을 윈도우 시스템이 해서 알려주지만 이 방식에서는 WSAWaitForMultipleEvents()함수와 같은 이벤트 검출 함수를 이용해서 항상 감시상태로 있어야한다.
l IO의 상태가 변했을 때 이벤트가 발생을 하지만 , 이벤트를 받은후에 소켓에 대한 어떤 IO 가 발생한것인지 구별해 내는 루틴이 필요하다. ( WSAEnumNetworkEvents 함수 이용 )
l WSAWaitForMultipleEvents 이용할때 동시에 검사할 수 있는 이벤트의 최대값은 64이다. 즉 한번에 64유저의 IO 상황을 체크할수있다는 것이다. 결국 한쓰래드에서 64개만 체크할수있기 때문에 더 많은 유저를 받아들이려면 쓰래드의 수를 늘려야한다는 문제가 있다.
l WSAAsyncSelect 방식에서 발생된 FD_WRITE의 문제는 여전히 남아있다.
1000명의 유저를 받는 서버를 만든다고 할 때 16개의 쓰래드가 필요하게 된다. 멀티 쓰래드에서 검출을 할 수는 있지만 , 미리 쓰래드 풀을 만들어서 관리해야하고 많은 유저를 받을 때 쓰래드수가 지나치게 많아지는 문제가 있다.
물론 WSAWaitForMultipleEvents()를 사용하지않고 , WaitForMultipleObjects()를 사용할수도 있다. 하지만 이것을 사용하게 되면 발생된 이벤트가 어떤 소켓에 의해서 발생된것인지 한번더 검사하는 루틴이 들어가게되므로 유저가 많이 접속하는 고성능서버에서 Receive신호가 많다고하면 매우 많은 효율의 낭비를 보게된다.
몇백명 안쪽에서 유저를 받는다면 WSAAsyncSelect 방식보다 좋은 효율을 볼것으로 기대가 된다.
7. Overlapped IO 방식
Overlapped IO , 한글로 억지로 번역한다면 중첩입출력, 이것에대한 개념은 IOCP로 연결이 되기 때문에 파일 IO를 기준으로 해서 설명을 간단히 해보도록하겠다.
l 파일 블록을 읽을 때 IO가 하나의 핸들을 열어서 여러구간을 읽기가 가능하다.
A C
B
위의 그림처럼 하나의 핸들을 열어서 동시에 3군데에서 Read/Write가 가능하다.
물론 위와 같은 방식은 구버젼 IO함수에서도 구현할 수는 있다. 그러나 ! 구현해 보면 알겠지만 여간 귀찮은 일이 아니다.
l 파일 블록을 중첩해서 IO가 가능하다.
A C
B
이것은 매우 막강한 기능이다. 서버에서 중첩된 읽기가 가능한 파일 IO를 구현한다고했을 때 결국은 시스템의 효율을 더욱 더 높이는 일이 될수있기때문이다. 주의할점은 Read만 하는 시스템에서는 A,B,C 구간 작업을할 때 Lock을 할 필요가 없지만 Write 작업시에는 하나의 기록이 다른 하나에 영향을 줄수있기 때문에 Lock을 걸어주는 것이 필요하다.
그럼 어떤 사람이 묻는다.
A,B작업 같이 write 할 때 Lock을 건다는 것은 두개의 쓰래드가 같이 갈수없으니까 결국 속도면에서보면 이전 방식과 똑같은거 아닌가요 ?
니말이 맞다. 그렇지만 A,C 작업을할때는 이익을 볼수있잖아. 그리고 Read 작업시에는 Lock에 신경을 쓰지않아도 되서 많은 이익을 볼수있으니깐 결국 사용해야 되지 않겠어 ? ( 역시 지극히 주관적인 생각이다. 이견이 있다면 할말 없음 )
l 서버가 IO 작업을 걸어두고 다른 작업을 진행시킬수있다.
이것은 Overlapped IO라기보다는 Overlapped IO를 사용해서 생기는 부수적 효과라고 봐야한다. 중첩 입출력을 해야하기 때문에 당연히 IO의 완료가 되었음을 바로 알수있는 블록킹모드가 아니고 논 블록킹모드로 가야하고 , 그렇기 때문에 IO가 끝나기 전에 다른작업을 할수있게 되는것이다. 역시 서버에서 이 기능은 막강하다.
앞에 5페이에서 이야기를 했지만 Overlapped IO는 IO의 통지를 두가지 형태 모두 지원한다고 했다. 어떻게 보면 IOCP라는 방식이있는 상황에서 어정쩡한 느낌도 있지만 어쨌든 Overlapped IO방식을 설명해야 하는 필요성이있는 관계로 간단하게 이야기를 해보겠다.
l 이벤트를 이용해서 어떤 IO 상태인지 검사를 해내는 방식
l 완료 함수를 이용해서 알아내는 방식
1) 이벤트를 이용해서 어떤 IO 상태인지 검사를 해내는 방식
이 방식은 WSAEventSelect 방식과 매우 유사하다.
유저의 접속이 이루어지면 이벤트를 하나 만들어두고 , 추후에 IO 상태 변경상황이 발생되면 이벤트가 발생된 것을 보고 어떤 IO가 변동되었는지 확인을 해서 각각에 맞는 처리를 해준다는것이다. WSAEventSelect 방식에서 WSAEnumNetworkEvents() 역할을 했던 것이 여기서는 WSAGetOverLappedResult()함수가 된다.
다만 다른 것은 WSAEnumNetworkEvents()에서 FD_READ 가 발생되었다면 recv()함수를 호출해야된다는것이고, WSAGetOverLappedResult()에서 Read를 알았다면 내가 Read를 해 왔으니 가져가서 사용하라고 알려준다는 차이다. 이것은 Overlapped IO와 IOCP 동시에 해당되는 이야기이기 때문에 IOCP를 설명하는 부분에서 자세히 설명하겠다.
2) 완료 함수를 이용해서 알아내는 방식
쉽게 생각해서 IO의 상태 변화를 시스템에서 알려주는데 , 알려주는 방법을 유저가 만든 함수를 연결시켜주면 그 함수를 통해서 알려준다는 것이다. 함수포인터를 이용하는 방법으로 SetTimer함수의 마지막 파라메터를 셋팅하는것과 같은 개념이라 할수있겠다.
( 대부분 NULL값을 넣겠지만 그곳에 함수를 연결해주면 지정된 시간에 그 함수가 호출되는 편리한 상황이 연출된다. -.- 뭐 사실 그렇게 편하지는 않지만 )
내 경우는 이런 두가지 기능을 M$에서 제공받는다고는 하지만, 그래도 IOCP가 있는데 Overlapped IO를 사용하는 것은 무모한 짓이라는 판단이 들어 과감하게 접어버렸다.
기본적으로 IO의 검출을 멀티쓰래드로 만든다는 큰 그림 안에서 다음과 같은 불편한 문제가 생기기 때문이다. ( 물론 직접 만들어보지않았기 때문에 머리속에서만 계산이 돌아갔다. 역시 극히 주관적인 생각임을 밝힌다. )
1) 번의 방식은 WSAWaitForMultipleEvents(), WaitForMultipleObjects() 둘중에 어떤 것을 사용할지에대한 고민이다. 전자는 64라는 것에 대한 문제가 있고 후자는 해당 소켓과 IO에대한 검출을 할 때 관리의 불편함에 대한 걱정이 있다. WSAEventSelect를 사용할 때 발생되었던 문제점과 유사하다.
2) 완료 함수를 사용하게 되면 , IO 검출에 대해서 해당함수가 불려지는데 유저가 많이 붙고 , 많은 팩킷이 서버에 유입된다고 할 때 함수의 호출이 지나치게 많아지면 큐의 증가에의한 메모리 관리와 CPU 관리에 대한 제어가 되지 않아 통제 불능의 상황에 빠질지도 모른다는 걱정이 있다.
(주의할것은 내가 이런 방식으로 대용량 서버를 구현해 보지않았기 때문에 지나친 걱정일수도 있고, 이 방식으로 서버를 운영하려고 했던 사람에게 잘못된 정보를 줄수도 있다는것이다. 이 문서는 절대적인 자료가 아니다. )
이 방식에서는 APC Queue (Asynchronous Procedure Call Queue, 비동기적 함수 콜 큐)를 이용하는데 , 이것은 운영체제의 커널에서 운영되는 것으로 여기서는 중첩입출력에 대한 완료 함수를 저장하는 큐로 사용이 된다. ( 우리가 만드는 쓰래드는 사용자가 만드는 APC라고 해서 user mode APC라고 한다. 즉 쓰래드를 만들고 함수를 등록해 주면 우리가 그 함수를 호출하지않아도 자동적으로 그 함수가 호출되는데 이 역시 APC라고 할 수 있는것이다.)
여하튼 커널에서 APC Queue를 운영하면서 내부적으로 멀티쓰래드를 구성해서 완료 함수의 호출이 이루어지기 때문에 통제 불가능의 함수호출은 이루어지지 않을거라고 생각이 든다. ( 역시 공부가 더 필요한 부분이다. )
하지만 .
IOCP를 이용하게 되면 이러한 걱정들이 많은 부분 사라지게 된다. 자신의 멀티쓰래드 될것의 개수를 지정하고 , 커널은 그 개수의 범위안에서만 쓰래드를 관리하기 때문에 컨텍스트 스위칭이나 , 시스템의 지나친 사용 , IO가 발생했을 때 어떤 소켓에 대해 발생했는지 등등에 대한 관리가 매우 편하게 이루어지기 때문이다.
이제 IOCP로 넘어가자.
8. IOCP 방식
IOCP는 I/O completion port의 준말이다.
I/O의 통지를 특정한 Port를 통해서 알려준다는 말이다. Port는 왜 포트라는 말을 M$에서 붙였는지 모르겠다. 뭔가 특정한 소켓에서의 포트가 있는거처럼 보여서 혼동을 유발시킨다. 포트는 없다.
GetQueuedCompletionStatus()를 이용해서 현재 IO 상태를 판독해 내는 것이 끝이다.
윈도우에는 메시지 큐라는것이있고, GetMessage()라는 함수가 있다. 메시지큐에서 이벤트를 하나 읽어오는 함수다. 이벤트는 운영체제가 넣어준다. 개발자는 큐에서 메시지를 꺼내와 어떤 것인지 판독을 해서 처리해주면 되는거다. 윈도우에서 사용되는 모든 Event driven 방식은 큐와 큐에서 꺼내오는 함수만 이해를 하면 상황끝이다.
시스템의 커널에서는 IO의 상태 변화 통지를 IO Completion Queue에 계속해서 넣어주고 GetQueuedCompletionStatus()함수는 순서대로 꺼내오는 역할을 하는것이다.
GetQueuedCompletionStatus() 함수는 이전의 WSAWaitForMultipleEvents() , WaitForMultipleObjects()과 같은 역할을 하지만 단순히 어떤 소켓에서 발생되었는지 , 또는 어떤 이벤트에서 발생되었는지만 알아내는 것이 아니고 , 어떤 소켓 개체에서 발생되었는지 , 어떤 IO의 변동이 있었는지 , 얼마큼의 데이터 변동이 있었는지 상황을 알려주기 때문에 WSAWaitForMultipleEvents(),WaitForMultipleObjects() 때보다 개발자가 프로그래밍을 해야되는 내용들이 많이 줄어든다.
IOCP의 장점은 지금까지 오면서 말했던 여러가지 방식들의 단점이 없다는 것이 장점이라고 할수있고 , 단점은 NT , 2000, XP 계열에서만 사용할수있다는것이다. 98이하 버전에서는 이 방식을 사용할수없다. 즉 클라이언트용 소켓 라이브러리에서는 적합하지 않은 방식이다.
그러면 IOCP를 사용하는 방식을 간단하게 짚어보자.
일단 처음에 유저의 접속이 이루어지면 Accept를 하고 유저와 관련된 변수 / 버퍼의 메모리등 개체를 할당하고, WSARecv()함수를 이용해서 읽어들일 버퍼를 한번 걸어줘야한다.
select, WSAAsyncSelect, WSAEventSelect 방식에서는 어떤 IO가 이루어졌는지 확인을하고 , 해당 IO 검출에 따라서 recv() / send()함수를 사용했다. 하지만 Overlapped IO, IOCP 방식에서는 먼저 읽어야할 공간을 운영체제 커널에 알려주는 작업을 해야한다.
그러면 커널은 해당 유저의 소켓에 Receive한 내용을 채워서 다시 통지를 해주는것이다.
우물에서 물동이를 넣어서 힘들게 끌어올린뒤에 그 물을 나누어 가져가는 것이 아니고 , 물동이만 우물에 넣어주면 신령이 나타나서 물동이를 채워서, 꺼내서, 던져주는 것이다.
계속 Receive를 받고 싶으면 WSARecv()함수를 이용해서 물동이를 계속 우물에 넣어주면 된다. 주의할것은 WSARecv()함수로 읽어들일 버퍼를 걸어줄 때 이전에 걸었던 부분과 중복되는 부분이 있으면 안된다는 것이다. 당연한 이야기겠지만 시스템은 날라온 버퍼가 이전 버퍼 부분과 중복이 되는지 확인을 하지 않는다는것이다.
중첩 입출력이라고 중첩된 버퍼를 보내도 알아서 채워준다는 것은 아니다. 파일을 읽어들일 때 중첩되는 구간을 읽는것과 , 버퍼를 보내주고 그곳에 채워달라고 요청하는것에 대한 혼동이 없기를 바란다.
Send할때는 더 재미가 있다.
다른 IO방식에서는 현재 송신이 가능한지 그 상태를 알아내는 것이 매우 불편했다.
IOCP에서는 송신을 할 때 WSASend()함수를 통해서 보내야할 데이터를 걸어주면 된다.
그러면 GetQueuedCompletionStatus()함수에서 데이터를 얼마나 보냈는지 알려주게 된다. 개발자는 전체 보내야될 데이터 중에 안보낸만큼만 더 걸어주고 , 또 다른 팩킷을 걸어주면 되는것이다.
즉 커맨드 처리함수에서 유저의 요청에대한 보내야될 팩킷 리스트를 만들고, 작동중인 Send Thread에서는 순차적으로 유저의 Send Buffer에 넣어주고 , WSASend()함수를 호출하면 된다. 이때 Send Buffer의 남아 있는 공간보다 보내야할것이 더 많다면 조금후에 보내는 루틴을 만들면되는것이다. 이렇게 하면 송신중에서 생길수있는 최대 문제인 WSAEWOULDBLOCK 문제를 해결할수있게된다.
추가로 IOCP로 라이브러리를 구성하면 다음의 것들을 생각해야 한다.
l 메모리 풀을 만들어 주는 것이 좋다. 특히 유저 소켓에 대한 메모리 풀 (또는 커넥션 풀이라고 하자)을 만들어주는 것이 좋다.
커넥션 풀에는 유저 소켓별 변수들과 수신버퍼, 송신버퍼들이 들어 있다고 하자. (커넥션 풀을 1000으로 잡는다면, 유저 접속을 1000명으로 제한한다는 것이다. )
일단 커넥션풀을 사용하지 않는 상황을 생각해 보자.
멀티쓰래드에서 GetQueuedCompletionStatus()를 통해 IO 신호가 검출되기 때문에 만약 4개의 쓰래드가 있고 , 동시에 두개의 Receive와 한 개의 Close 신호가 들어왔다면 상황에 따라서 문제가 생길수가 있다.
만약 Receive 두개가 먼저 처리되고 Close가 처리된다면 문제가없다.
하지만 Close신호가 먼저 처리되고 Receive가 처리되면 문제가 된다. Close에서 해당 소켓의 메모리를 삭제했기 때문에, 리시브 버퍼 포인터가 이미 지워 졌기 때문이다. 물론 해결할 수 있는 방법도 있다. 각각의 쓰래드 진입시에 Lock을해서 소켓 오브젝트의 생성 삭제를 감시하면 되겠지만, 속도에서 많은 부분 손실을 보게된다.
그래서 커넥션 풀을 만들고 , 지금 Close가 되었는지 아닌지 플래그를 만들고, 그 플래그를 통해서 위의 상황을 제어하면 아주 좋다.
l Receive buffer, Send buffer의 링버퍼 운영에 주의해야 한다.
(미안하지만 링버퍼를 만들고 운영하는 방법들은 여러 책들을 참고하기 바란다.)
각각의 버퍼 구간들이 서로 꼬이지 않게 Lock을 잘걸어주면서 작업을 해야한다. 특히 Send buffer의 경우 내가 보내는 중에 링버퍼의 끝까지와서 버퍼의 위치 바꾸는 작업을할 때 , 아직 보내지않은 것을 보내는 루틴과 섞이는 문제가 생길수있다.
A
B C
A : 지금 보내려고 버퍼에는 들어있지만 WSASend에 걸지 않은 구간.
B : WSASend에 걸어서 , 이미 보내졌지만 보냈다는 통지가 오지 않은 구간.
C : 새로 보내야 될 팩킷 , 링버퍼에 넣어야 하는데 버퍼공간이 부족하다.
이 경우 C를 버퍼에 걸기 위해서는 A 부분을 맨앞으로 옮기고 , 그 다음부터 C를 버퍼에 복사하면 된다. 하지만 B가 있다.
B의 데이터중에 얼마만큼의 데이터가 실제로 보내졌는지 알수가 없기 때문에 단순히 A 부분부터 복사를 하면 안되는것이다. B의 구간중 일부도 복사를 할 필요가 있기때문이다.
C를 버퍼에 넣기전에 B구간에대한 처리가 완료될때까지 기다리는 부분을 잘 구성해야 할것이다.
정작 IOCP에 대한 이야기를 하려고 했더니 할말이 별로 없다. 생각했던거 보다는 많이 부족한 문서가 되어 버렸다.
IO의 검출은 통신 라이브러리에서 제일 기본적인 부분이고 코어에 해당된다. 하지만 통신 라이브러리는 이것이 끝이 아니다.
파싱 쓰래드를 둘것인가 말것인가, 그렇다면 싱글로 아니면 멀티로 , 인코딩 / 디코딩은 어떻게 할것인가 , 팩킷 시리얼링은 어떻게 할것인가 , 클라이언트의 접속 관리는 어떻게 할것인가 , 메로리풀과 버퍼 관리는 어떻게 하는 것이 좋을까, DB풀과 쓰래드 풀은 어떻게 할까 …….
생각할것이 끝이없다.
앞에 이상적인 IO 통지 구조에서 이야기했던 여러가지 조건들을 다시 한번 생각해 보기 바란다. 개인의 프로그래밍 스타일과 쓰래드 , 이벤트 운영에 대한 취향 그리고 이상향이 다르기 때문에 정도는 없다.
다만 최대다수의 최대행복에대한 좋은 가이드라인을 제시했다면 거기서 만족하련다.
출처 : 변해룡 님
댓글을 달아 주세요