Network/Windows

[IOCP] AcceptEx 관련

Binceline 2013. 1. 16. 14:41

출처 : 

http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=50&MAEULNo=20&no=463918&ref=463900


-----


안녕하세요.

 

일단은 POSA(Pattern Oriented Software Architecture) 2 권에 등장하는 용어를 인용해 설명 하자면...

Proactor 와 Reactor 라는 녀석의 개념부터 짚어보는 것이 좋을 듯 합니다.

 

Proactor --> '선행 하는 넘'

Reactor  --> '후행(반응) 하는 넘'

 

대충... 짧은 영어 실력이지만 위와 같이 해석이 가능할 듯 합니다.

 

자... 그럼 IOCP를 공부하셨다고 언급하셨으니까  Overlapped I/O 에 대한 기본 개념은 알고 계시다고

생각하겠습니다.

 

IOCP 쓰건 그렇지 않건 간에...

 

Overlapped I/O --> 이넘 전형적인 Proactor 패턴의 형태를 가지는 녀석이죠...

말 그대로 어떤 오퍼레이션을 선행해서 처리한다는 거죠...

자 그럼 대표적인 Reactor 라 할 수 있는 select() 함수를 이용한 blocking socket을

사용하는 경우와 비교를 해 보겠습니다.

 

recv 를 가지고 비교를 해보죠...

 

Overlapped I/O를 사용하는 경우

우리는 미리 recv 를 posting 해 둡니다...

현재 수신된 데이터가 소켓 서브시스템에 buffering이 되어있는 지 여부와는 상관이 없이 말이죠..

말 그대로 '선행' 해서 미리 걍 recv를 해 두는 거죠...

그런 후에 실제로 수신된 데이터가 있을 경우... 앞서 우리가 posting 했던 recv 함수를 통해

넘어간 buffer로 그 데이터가 복사된 후 다양한 overlapped i/o의 통지 메커니즘을 통해서

우리는 그 사실을 알게 되는 (iocp의 경우는  GQCS(GetQueuedCompletionStatus)가 되겠죠)

형태이죠...

 

반면... Blocking Socket의 경우...

우리는 select() 함수를 통한 polling을 통해서 들어온 데이터가 있는 지 없는지 여부를 계속해서

감시해야 합니다. 이렇게 하지 않고 그냥 recv() 함수를 호출해 버리면 실제로 수신된 데이터가

있을때까지 thread는 언제까지고 blocking이 되어버리니까요...

그런 이유로 주로 별도의 thread를 하나 생성하고 그녀석이 select()함수를 호출해서 네트웍 이벤트를

감시하게 되죠... 그러다가 수신된 데이터가 있음이 감지가 되면 그제서야...

그에대한 반응 으로 recv를 호출해서 수신된 데이터를 application으로 가져가게 되는 형태이죠..

 

대충 감이 잡히시죠?

 

자... 그래서 overlapped i/o 방식을 사용할 경우 send/recv 까지는 좋았는데...

서버입장에서 클라이언트의 접속요청을 받아들이려 하려니까...

이런 된장... listen 하고 accept 를 해야되더라 이거죠... 이것 때문에

Proactor 와 Reactor 가 짬뽕된... 애매한 녀석이 되었는데요...

 

그러다가 아시다시피 Winsock Extension 함수군이 등장했고 그 중에 AcceptEx 란 녀석이 포함되어 있습니다.

이녀석을 맞이함으로 인해 비로소 완벽한 Proactor가 될 수 있었는데요...

 

기존에 listen , accept를 사용할 경우... bsd socket 을 그대로 계승한 전형적인 reactor 죠?

listen 하고 있다가 connect 요청이 있으면 accept 해서 내부적으로 소켓 생성해서 받아들이 클라이언트를 그넘과

bind해서 리턴해 주죠...

다시 listen 하다가 connect 있으면 accept 하고, 한방에 하나씩!...

 

반면 AcceptEx 이넘... 앞선 recv 나 send의 경우와 마찬가지로...

미리 소켓을 N개 (원하는 만큼? 혹은 시스템이 수용할 만큼) 만든 후에

만들어진 N개의 소켓을 AcceptEx 를 이용해서 전부 posting해 둡니다...

질문하신 영어문서에 나오듯... outstanding 상태로 만들어 두는 것이죠...

이렇게 되면 이넘들이 대기하고 있다가 클라이언트 연결 요청이 들어오게 되면

바로 binding 시켜 역시나 GQCS()를 통해 app로 알려주게 되는 것이죠...

이렇게 함으로써, accept()를 사용했을때, 새로 소켓 생성하고 kernel 내에 메모리 할당 등의

내부적 처리의 과정이 생략되어 클라이언트 연결 요청에 대해 즉각적으로 App에서 반응 할 수

있다는 잇점이 있는 것입니다. 또한, 복수개의 thread로 Accept 처리를 하도록 했다면 수많은

동시접속 요청에 대해 더욱 빠른 응답을 할 수 있겠죠...

 

그럼 과연 그정도까지 accept 응답 성능에 신경쓸 필요가 있는가? 하는 의문이 들 수 있을 것입니다.

맞는 얘기죠 사실... ^^;

그런데 실제로 online 게임 같은 경우 서비스 점검 후 다시 서버를 open하게 되면 순간적으로

엄청난 수의 connection 요청이 들어오는데, 이게 원활한 처리를 해내지 못하고 유저에게

'로그인 시도중이니 잠시 기다려 주세요' 라는 창을 보여야 한다면 프로그래머 입장에서 좀 그렇겠죠?

암튼 수많은 동시접속 요청을 매끄럽게 수용해야 하는 server app를 제작해야 하는 입장이라면

AcceptEx는 놓치기 힘든 유혹이죠... :)

 

자... 그럼 여기서 의문이 하나 생길 수 있는데요...

그럼 posting 해둔 socket들이 모두 accept 된 다음에는 어떻게 되는거야? 라고 하는 것이죠...

새로 Accept를 posting하기 위해서 어차피 socket을 생성해야 하는데... 그럼 결국 똑같은 거

아닌가? 라는 것이 될텐데요... 사실 일부 맞는 얘기죠... 그러나! 그러나!

므흣~

DisconnectEx 를 아시죠? 이녀석을 이용하면... 커넥션은 끊고 socket 핸들과 그와 함께 할당된

모든 시스템 자원은 reusable 상태가 된답니다.  정말 멋지지 않은가요? 즉, 굳이 socket() 함수를

호출해서 새로운 socket descripter를 allocate할 필요 없이 방금 끊은 그 녀석을 재활용 할 수 있다는 것이죠.

비로소 온전한 socket pool 을 application에서 활용할 수 있게 되었습니다... :)

그런데 버뜨! 망할 DisconnectEx는 WinXP 계열에서만 사용이 가능하답니다... -_-;

 

그러나 아직도 희망은 있습니다.

 

TransmitFile() <-- 이 녀석을 아시죠... 이 녀석을 이용해서 DisconnectEx와 동일한 작업을 할 수가 있답니다.

좀 어색하죠? 함수이름하고 사용하는 용도가 잘 매치가 안되죠? 물론 이넘의 주목적은 함수명 그대로

화일 전송이지만... 지금 우리가 필요한 용도로도 사용이 가능하다는 것이죠.

음... 지금 MSDN이 없어서 플래그가 잘 기억이 안나는데... (TF_DISCONNECT | TF_REUSE_SOCKET)

이렇게 인자로 주게 되면 (물론 Buffer는 NULL로 하셔야 겠죠) 전송할 데이터가 없으니 즉시 소켓 디스커넥트 하고

socket descriptor와 그와 연결된 모든 내부 자료구조는 유지가 되는 reusable 상태가 된답니다.

 

그럼... 이런 상황을 생각해 보죠...

어떤 서버가 있고 평균 concurrent user가 약 1000명 정도 된다고 가정하죠...

AcceptEx를 사용하고 DisconnectEx (혹은 TransmitFile) 을 통해 소켓 pooling을 해서 재활용 한다고 하면

최초 1000 개의 소켓이 생성될 때 까지는 accept를 쓰는 것과 socket descripter 생성을 위한

overhead는 동일하게 가지고 가겠죠...

그러나! 일단 약 1000 개의 소켓이 생성되고 난 후에는 계속해서 그 1000 개의 소켓을 돌아가며 재활용 하게 되겠죠...

즉, 이후부터는 AcceptEx 호출을 통해 posting 하는 것과 session establish 된 녀석의 내부적인 데이터 세팅

정도만 필요하게 되겠죠...

accept() 를 사용했을때엔 꿈도 못꾸는 일이죠... 정말 매력적이지 않나요? ^^;

소켓 pooling이라.. :)

 

음... 그런데 AcceptEx 와 ConnectEx를 통해서 부가적인 기능으로 초기 데이터를 주고 받을 수 있는 거 아시죠?

커넥션 완료와 함께 최초의 데이터를 주고 받을 수 있는...

이 기능을 활용하실때엔 주의 하셔야 하는 부분이 있는데요...

client - side 에서 만약 악한 마음을 먹고 DoS 를 할  수가 있다는 것입니다...

어찌보면 예전에 많이 했던 SYN Flooding 이라는 DoS 기법과도 비슷한데...

정상적인 클라이언트가 아닌 어떤 malicious user가 자신이 만든 임의의 application을 통해

server 로 connect만 한 후에 그냥 두는 것이죠... 이후 아무 작업도 안하구요...

이런식으로 그 application 에서 무작정 계속 해서 connect를 호출해 버리면 AcceptEx를 통해

초기 데이터를 수신하게 되어있는 server는 아무 데이터가 수신되지 않기 때문에 무작정 기다리게 된답니다.

결국 backlog가 가득차게 되서 이후 정상적인 유저의 connect 요청에 응답할 수가 없게 되는 것이죠...

으~ -_-+

전형적인... 그리고 손쉬운 DoS가 가능하죠?

 

뭐 암튼 이런 관계로... 주의할 점도 있다... 하는 것입니다.

 

으아~ 써놓고 보니 엄청 길어졌는데요...

암튼 도움 되셨기 바랍니다.

반응형