From YYpBD's MediaWiki
1. socket의 사용 방식에 대한 이해
a. blocking/non-blocking 방식
blocking mode: function 호출시 그 function이 해당 기능을 완전히 수행하였거나, 실패하였을 경우에만 return되는 방식.
수행이 완료될때까지 대기하게 되므로 blocking방식이라고 부른다.
장점 : 결과를 바로 알수 있으므로 쓰기가 편하다. 누구나 쉽게 짤 수 있다.
단점 : 네트웍에 이상이 생기거나, 클라이언트를 남이 짰는데 패킷 길이가 틀리면 골때린다.
에러 처리하기가 지-랄인데다가, 기둘리는 동안 딴짓을 못해서 비효율적이다.
이런걸로 서버짰다가는 잘못하면 클라이언트 백개도 못받는다. 아니면 thread가 백개가 넘어간다.!
보통 초보자용 혹은, 대충 돌게만 만드는 클라이언트용으로 사용한다.
까놓고 말하면 unix에서는 대개 이런걸로 서버를 만든다. 근데.. 그래도 잘돌아간다... unix가 좋다.!!
사용되는 function : 각종 winsock function들. default가 blocking mode로 생성되므로 그냥 쓰면 된다.
주로 select를 이용해서 사용하게 된다.
non-blocking mode : function이 즉시 수행가능할 경우에만 수행을 하고 아닐 경우는 실패로 return하는 방식.
connect를 제외한 나머지 function에서 WSAEWOULDBLOCK이 날 경우 retry를 하여야 한다.
장점 : ... 이런게 장점이 있나.. 그래도 이건 event 혹은 message driven방식으로 코딩을 해야 되므로
좀 있어보인다. 이정도만 돼도 잘 돌아가는 서버를 짤 수 있다. 밑에 쓰는 방식에 비해서는 무지 쉽다.
thread 한개로도 서버가 만들어진다.
단점 : 진짜 thread 한개로 서버 만들면 무쟈게 느리고 욕먹는 서버가 나올 확률이 크다.
밑에 나오는 것들에 비해 없어보인다.
맨날 나오는 WSAEWOULDBLOCK 처리하기가 귀찮다.
사용되는 function: 위와 동일하고, WSAEventSelect/WSAEnumNetworkEvents나
WSAAsyncSelect/WSAGETSELECTERROR/WSAGETSELECTEVENT 를 사용한다.
blocking mode와 non-blocking mode의 전환 : default로 socket은 blocking mode로 생성이 된다.
WSAAsyncSelect, WSAEventSelect를 사용하면 자동적으로 non-blocking mode로 전환이 되며, ioctlsocket,
WSAIoctl 등으로 blocking/non-blocking의 전환이 가능하다.
b. overlapped i/o 방식 (Asynchronous 방식)
overlapped i/o란 function 호출과 그 결과의 확인이 분리되어있는 형태를 말한다. 즉, function의 호출은 시스템에 대한 요청
으로 간주되며 요청이 접수된 즉시 그 결과에 상관없이 return하게 된다. 이때 function의 기능이 수행가능한 상태에
있을 경우 그 결과를 즉각 알 수 있지만, 그렇지 않을 경우 추후에 결과를 확인하게 된다.
이렇게 결과를 확인하는 방식에 따라 다음과 같이 구분되기도 한다.
사용 function: socket을 만들때 overlapped flag을 set해서 만든다.
각종 socket function을 사용할때 overlapped 구조체나 callback function을 넘겨줘서 사용한다.
overlapped 구조체를 사용할 경우는 넘겨준 구조체에 해당하는 I/O가 끝났는지를 HasOverlappedIoCompleted를
통해서 확인후 지우던지 해야 한다.
polling 방식 : 결과가 나올때까지 결과를 확인하는 function을 호출하는 방식. 이때 계속해서 WSAGetOverlappedResult function을 사용한다.
만일 결과가 나올때까지 대기하도록 설정하면 block해서 대기하는 방식과 비슷하다.
장점 : ... 몰르겠다... 아무리 생각해봐도 생각이 안난다. 아.. 혹시 context switching을 방지하기 위해서 쓸수도 있겠다.
단점 : 일단 무식해보인다. thread 한개로 여러개 결과 확인하기가 애매모호하다.. 언제 그걸 다 불러보고 있냐..
사용 function : WSAGetOverlappedResult를 결과 확인에 사용.
block 해서 대기하는 방식 : Overlapped 구조체에 사용한 event를 WaitForMultipleObjects나 WaitForSingleObject로 대기하는 방식.
주의 : 이때 사용된 event는 manual reset으로 생성된 event여야만 한다.
장점 : 그래도 이정도 되면 상당히 있어보인다. thread도 여러개 쓰고.. 잘 짜면 아무데서나 호출하고 thread 한개로 결과
를 받아볼 수 있다. 조금 신경써서 thread pool 이라도 만들어놓으면 남들이 코딩 잘한다고 생각한다. IOCP만 빼고는
최강의 solution이 될 확률이 무지 크다. IOCP보다는 조금 생각할게 적어질 수 있다.
단점 : 암만해도 쪼간 어렵다. function 한개가지고 기다릴수 있는 event가 64개로 제한되어 있으니깐 thread 한개로
처리하기는 무리다. 소스가 복잡해지기 십상이라.. 좀만 지나면 자기도 어떻게 짰는지 기억이 안난다..comment 암만
잘해봐라..기억나나..
사용 function : overlapped 구조체에 넘겨준 event를 대기하는 function들을 사용.
I/O completion routine을 사용하는 방식 : overlapped구조체에 completion function을 지정하여 callback을 받는 방식. 이때
callback은 APC가 사용되고 APC는 calling thread의 context를 사용하기 때문에 calling thread는 alertable wait state에 있어야 한다.
참고 : alertable state로 들어가기 위해서 사용되는 function에는 SleepEx, SignalObjectAndWait,
MsgWaitForMultipleObjectsEx, WaitForMultipleObjectsEx, or WaitForSingleObjectEx function등이 있다.
참고 : overlapped구조체에 event와 completion routine을 동시에 제공했다면 completion routine만 호출된다.
주의 : completion routine을 사용하는 I/O에 대해서는 WSAGetOverlappedResult 를 사용하면 안된다.
장점 : 웬 장점..난 도대체가 APC란 놈이 희안하다.. 이게 진짜 쓸모가 있기는 있는건가..어쨌든 IO를 여러개 걸고 나중에
결과를 받을 수 있기는 하다.
단점 : 이런걸로 서버를 짜는 사람이 신기하다. 내가 혹시 바본가.. 도대체 I/O한 thread에서 무조건 기다려야 되면 코딩
할 수 있는 방법이 제한되지 않나! 더이상은 얘기하기 싫다.
사용 function : WSAOVERLAPPED_COMPLETION_ROUTINE 을 사용하고, alertable wait state로 빠질수 있는 function들을 사용.
I/O completion port를 사용하는 방식 : I/O completion port란 일종의 notification message queue이다. 즉 결과가 message queue
처럼 port로 들어오고, 그 port에 대기하고 있는 thread에서 그 결과를 처리하는 방식이다. 모든 socket에서 발생하는 event를
port에서 받아보고 처리하는 방식이다.
장점 : 현존하는 window platform에서의 최강 서버모델이다. 이걸로 서버짜면 남들이 우러러본다. 소켓으로는 raw socket빼고는
갈데까지 다 가본사람이라고 인정받는다. (물론 driver 까지 가는거 빼고.. application으로만). 실제 엔간히만 짜놔도
진짜 잘돌아간다. 클라이언트 무쟈게 붙여도 끄덕없다..훌륭하다.
단점 : 장점이 장점인만큼 짜기가 괴롭다. 한번 벅나면 다시짜고 싶어진다. 좀 지나서 새로운 기능 넣을려면 진짜 새로짜고 만다.
같은 말이지만 한참 지나면 아무생각도 안난다. 이거 내가짠거 맞어?라고 남들에게 물어봐야된다.
95/98/ME에서는 절대 안돌아간다.(음.. 이건 별로 단점이 아닌데..)
사용 function : CreateIoCompletionPort, GetQueuedCompletionStatus, PostQueuedCompletionStatus 등을 사용.
문서에 나온 장점 : 1. runnable thread count를 제한한다. port에 연결된 runnable thread의 갯수가 concurrency value에 도달하면
system은 더이상의 runnable thread가 없도록 block한다. 즉 port에 event가 queue되더라도, GetQueuedCompletionStatus로 대기하고
있는 thread를 깨우지 않는다는 뜻이다. 이러한 방식을 통해 context switching을 최소한도로 제한할 수 있다. 예외적으로 concurrentcy
value를 일시적으로 넘어가는 경우가 생기는데 이때는 runnable thread가 다른 io 혹은 기타 이유때문에 (GetQueuedCompletionStatus말고)
block됐을 경우이다. 이럴 경우는 system이 다음에 대기중인 thread를 runnable로 만들고, 추후에 concurrency value를 맞추게 된다.
2. GetQueuedCompletionStatus로 thread들이 대기할때 그 대기되는 방식이 LIFO(Last In First Out)방식을 취한다. 이렇게 함으로써
일을 많이하는 thread는 계속 일을 하게 되고 (역시 context switching이 적어질 확률이 높아짐) 또 노는 thread는 계속 놀게 됨으로써
system이 그 thread가 차지하고 있는 resource를 page out할수 있게 된다.
3. 여러개의 I/O에 관련된 event를 하나의 object를 통해서 받아볼 수 있게 됨으로써 thread pool의 사용을 쉽게 만들었다.
2. socket 서버를 짤때 신경쓸 것들.
a. send buf/recv buf size를 0로 놓는 방법 : 기본적으로 transport에서는 buffer가 제공되고, send할때는 user buffer가 이 transport
buffer 로 copy 된 후에 발송이 되며, recv할 때도, transport buffer에 우선 data가 찬 이후에 user buffer로 copy가 된다. 이 copy
를 줄여보기 위해서 transport의 send buffer를 0로 놓으면 user가 제공한 buffer를 사용해서 직접 발송을 하게 되므로 약간의 시간을
줄여볼 수 있다. 단 이때 transport가 user buffer를 사용해야 하기 때문에 전송이 다 끝나기 전까지는 이 buffer에 어떠한 user 작업도
가해져서는 안되며 이때 사용되는 memory page는 lock이 걸리게 되고, 많은 수의 send가 동시에 진행될 경우 page lock limit에
도달할 가능성이 생기게 된다. recv buffer를 0로 놓는 경우는 미리 충분한 숫자의 buffer를 제공해 놓지 않을 경우, 즉 data가 들어오는데
사용할 buffer가 없을 경우는 transport의 밑단에서 buffer로 받아놓게 되므로 다시 user buffer로 copy가 발생하게 된다. 결국 IOCP방식을
쓰면서 recv buffer를 미리 잔뜩 걸어놓는 방식이 아니고서는 굳이 사용할 이유가 없게 되며, send buffer를 0으로 놓을 경우와 마찬가지로
page lock 등에 신경을 써야 하므로 그다지 자주 사용되지 않는 방법이다.
b. page lock limit과 non-paged pool limit : a에서 밝혔듯이 Send buffer나 receive buffer를 0로 놓았을 경우, user buffer 의 memory
가 physical memory에 lock 된다. 따라서 concurrent한 send/receive가 많이 발생할 경우 ERROR_INSUFFICIENT_RESOURCES가 발생할 수 있다.
이런 상황을 방지하기 위해서 user buffer를 되도록 page boundary에 맞도록 만들 필요가 있다.
socket의 creation이나, binding, connecting이 일어날때 OS는 non-paged pool에서 일정량의 memory를 할당한다. 또한 I/O 자체도 약간의
memory를 소비한다. 이 메모리 할당이 어느 수준을 넘어가면 문제가 생기게 된다.
c. 몇개의 thread를 쓸 것인가 : 가장 이상적인 case는 thread가 쉬지 않고 계속 돌도록 만드는 것이다. 보통 권장되는 thread갯수는 CPU 당 한개가 권장된다.
그러나 실제로는 thread가 요청을 받아서 그 요청을 처리하는 시간과, 요청이 들어오는 빈도수를 같이 고려해야만 한다. thread가 요청을 처리하는데
1초의 시간이 걸리고, 요청이 1초 마다 한개씩 들어올 경우와, 요청이 1초 마다 두개씩 들어올 경우의 thread갯수가 같을 수는 없다. 결국 적합한 갯수라는
것은 경험치를 적용할 수 밖에 없게 되며, thread갯수를 바꿔가면서 performance를 측정하는 방법밖에 없다.
d. 안좋은 서버가 가지는 몇가지 경향 : 서버가 작은 packet을 자주 보내는 경우. 아무리 작은 packet이라도 기본적인 protocol overhead가 있고
그러한 overhead가 많은 수의 packet으로 증폭이 되면 상당히 느린 performance의 원인이 된다. 가능하다면 작은 packet들을 모아서 한번에 처리하는 것이
좋다. 또다른 경우는 서버와 클라이언트간에 불필요한 serialization이 발생하는 경우가 있다. 예로 file을 send하는데 매번 packet하나를 보낼때마다
reply를 받게 된다면 쓸모없는 packet과 그 packet의 전송/처리에 드는 시간이 발생하므로 그다지 효용성이 없게 된다. 그냥 쭉 보내버리든가, 꼭 필요하다면
몇개마다 한번씩 받도록 바꾸는 것이 좋다. 또 한가지 경우로 쓸데없이 큰 packet을 보내는 경우가 있겠다. 뭐 대단한 압축 루틴을 사용하라는 것이 아니라
완전히 불필요하게 packet의 size를 늘리는 것은 network의 overhead를 증가시켜서 또다른 문제를 발생시킨다. byte 하나를 쓸수 있는 내용을
string으로 쭉 늘어서 쓴다든지 하는 방법이 그렇다. 최소한 packet structure에 불필요한 부분이 없는지 정도는 검증을 하도록 하자.
하나 더 쓰자면 transaction마다 connetion을 새로 맺고 끊는 방식이다. tcp가 slow-start방식을 취하고 있으므로, connection을 새로 맺는
데는 그만한 부하가 걸린다. 또한 close를 한 후에도 tcb가 TIME_WAIT상태로 들어가서 system resource를 잡고 있으므로 이것도 부담이 된다. 물론 persistent
connection을 사용하는것보다 짜기가 쉽고, 에러처리하기도 쉽다는 장점이 있지만, 많은 수의 클라이언트를 상대해야 될 경우 오히려 서버에 안좋은 영향이 끼쳐지므로
약간 부담이 되더라도 connection을 유지하도록 하는것이 좋다.
3. 기타 참고 사항
a. Nagle algorithm : nagle algorithm은 slow network 상에서의 작은 packet들에 대한 문제를 해결하기 위해 만들어진것으로, 하나의 connection에서
작은 packet이 한개만 존재할 수 있도록 만드는 algorithm이다. ethernet상에서 작다는 말은 대개 1500byte이하의 packet을 말한다.
b. CAsyncSocket과 CSocket : MFC에서 제공되는 CAsyncSocket은 위에서 본 non-blocking socket을 class로 wrapping한 것이다. 이때 내부적으로 MFC하에서
돌도록 만들기 위해서 hidden window가 존재하게 되며, 기타 많은 내부 변수들을 갖게 된다. CSocket은 CAsyncSocket을 좀 더 쓰기 편하게 만든것으로 위에서 본
blocking mode와 비스므리하게 동작하는데, 실제 blocking mode를 사용하는 것은 아니고, 내부에서 loop를 돌면서 대기중 window message의 처리 및 blocking
mode처럼 작동하도록 하는 기능을 해준다. 사용하기는 편리하지만, Winsock 1.1을 사용하므로 고급 기법을 전혀 사용할 수 없고, 내부적으로 처리하는 부분이 많기 때문에
무겁다. 또한 thread를 넘나들때는 결국 HANDLE을 이용해야하므로, 일반적으로 고수라 불리는 사람들은 이것들을 쓰는걸 회피하기 마련이다. 단 클라이언트를 간단히 만들어
사용하거나, MFC의 내부를 들여다 보고 싶은 사람, API를 어떤식으로 class로 만들었나 보고 싶은 사람들에게는 참고할만한 소스가 되기도 한다.
c. APC란? (Asynchronous procedure call) : APC란 thread context내에서 asynchronous하게 돌아가는 function이다. 각 thread는 각자의 APC queue를 가지고
있으며, 이 queue에 들어간 function은 thread가 alertable wait state로 들어갈때 수행된다.
APC를 queue하기 위해 사용되는 function은 QueueUserAPC 이다.
d. graceful shutdown ? : socket을 close하는 방식은 크게 두가지로 나뉘는데 그것이 바로 graceful shutdown과 abortive shutdown이다. 두가지 방식의 차이는
안보내진 data가 있을경우의 처리가 다른것인데, graceful shutdown을 사용하면 마지막 데이터까지 서로 주고받을수 있도록 해주는 반면, abortive shutdown을 사용할
경우는 안보내진 data는 없어진다. graceful shutdown의 순서는 다음과 같다.
1. side A 는 shutdown(SD_SEND)를 수행.
2. side B는 FD_CLOSE를 수신.
3. side B는 나머지 보낼 데이터를 보냄
5'. side A는 데이터를 수신. 4. side B는 shutdown(SD_SEND)를 수행.
5. side A 는 FD_CLOSE를 수신. 4'. closesocket 수행
6. closesocket 수행.
위와 같은 방법을 직접 사용하지 않는 경우 SO_LINGER option을 사용해서 closesocket을 직접 부름으로써, closesocket 내부에서 shutdown처리 및 graceful
shutdown 처리를 하게 할 수있다. 권장되는 방법은 위의 순서를 사용하는 방법이다.