본문 바로가기

프로그래밍

[펌] 소켓 프로그래밍에 관한 주의 및 Q&A


[소켓 프로그래밍시의 일반적인 주의점]
*) CSocket을 가능하면(절대라고 표현해도 될 정도죠^^) 사용하지 마세요.
- CSocket을 이용하여 프로그램 하시는 분이 있다면 존경받아 마땅한 분이죠...^^
  왜냐면 CSocket은 Blocked Call이기 때문에 제대로 돌아가려면 Thread를 이용한 코딩이 되어야 하는데
  거의 웬만한 경우에 배보다 배꼽이 크게 만듭니다...
  옛날에는 유닉스 기반한 Telnet서버 등에서 이런 식의 Blocked Call을 Thread와 같이 사용했는데
  실제적으론 문제가 많습니다. 가장 큰 문제는 시스템 리소스를 무지하게 잡아먹는다는 거죠
  메모리 사용보다 가장 큰 문제는 Thread switching 오버헤드와 Thread 갯수 제한이죠...
  1000명의 사용자가 접속하는 Telnet서버를 만들기 위해선 최소 1000개의 Thread를 만들어야 하는데 안되죠^^
  1000개 Thread를 무리없이 돌릴 수 있는 프로그램이 몇 개나 될까요?
  된다 치더라도 만약 각 Thread간의 Data Sync가 필요한 상황이 된다면 어찌될까요? 끔찍하겠죠?
 
  - 이야기를 바꾸면 가능하면 Thread를 이용한 통신을 하지 말라는 말과도 같습니다... Thread는 필수일 경우에만 쓰시면 됩니다.
  - 실제로 Thread를 이용한 통신을 하게 되더라도 결국엔 OS나 시스템 성능 때문에 대형 시스템에서 조차
  - Thread하나에 여러 개의 접속을 Async로 처리하게 될 수 있습니다.
  - 시스템의 성능을 한계까지 발휘했을 때 모든 처리를 각각의 Thread기반으로 처리했을 때와 한 개의 Thread에서 Async로 처리했을 때
  당연 후자가 시스템을 가장 제대로 활용하게 될 수 밖에 없습니다.
 
==> CSocket가 Blocked Call이기 때문에 쓰레드를 꼭 써야하는것도 아니죠....ui에 신경을 쓸필요가 없거나 소켓 옵션에 timeout을 걸어둘수도 있는 문제구욤....꼭 blockedcall이 나쁜건 아니죠..... 그리고 쓰레드을 이용한 코딩이 오브헤드가 그렇게 많지 않답니다. 그리고 telnet데몬들은 1user1thread로 작성된게 없지 않고 잘돌아 갑니다. 그리고 data sync도 따지고 보면 blocked나 non-blocked나 둘다 비슷할텐데요 ^^;
 
 
*) CAsyncSocket을 그대로 사용하지 마세요.
- Non Blocked Call이긴 하지만 그냥 사용해서 프로그램을 작성하면 끔찍한 프로그램 밖엔 만들 수가 없습니다.
  일반적으로 CAsyncSocket을 사용하시는 분들의 특징이 데이터 수신은 받고 싶은 만큼만 받으면 되니 문제가 없는데
  데이터를 송신할 때 문제를 야기시킵니다. 보내고 싶은 데이터를 그때 그때 바로 보내려고 시도하는데
  문제는 OSI 세븐 레이어인지 하는 계통을 따라 데이터를 보내게 되는 통신 과정상 어디선가는 인터널 버퍼 오류 등의
  여러가지 상황이 발생할 수 있습니다. 이때마다 .Send함수에 대한 예외 처리(혹은 그자리서 무식하게 메시지 루프를 돌면서
  데이터가 전부 전달될 때까지 배째기)를 해야 하는데 프로그램이 커지면 수십 군데의 오류처리부가 존재하게 됩니다.
  결국 무지하게 지저분한 코드가 될 수 밖에 없죠...
 
  한 개의 함수 안에서 Receive와 Send가 번갈아 서너번 처리되어야 한다고 생각해보죠... 이중에 어디서 오류가 날지 누가 알겠습니까?
  결국 개발자는 모든 Receive와 Send에 대하여 오류처리를 해야하죠.. 지금껏 정상이라고 믿고 작업했던 내용도 되돌려야 하죠^^
 
  하지만 깊게 생각하셔야 할게 있습니다. .Send함수가 성공을 리턴했다고
  상대방 프로그램이 그 데이터를 제대로 받았을까요? 혹은 받아서 처리하다가 죽었을 수도 있죠? 혹은 제대로 처리해서 결과를
  리턴하는 도중에 인터넷 라인이 끊어지는 경우도 있을 수 있죠?
  그런데도 수 십 개의 .Send함수 호출 바로 뒤에서 오류처리를 하실 겁니까?^^
  진짜로 오류가 있는지 없는지는 내가 보낸 데이터에 대한 결과가 도착하기 전에는 알 수가 없는 거죠.
 
  - 뒷부분에 자세한 이야기를....
 
*) 가능하면 BSD소켓 API만을 사용하여 코딩하십시오...
 
==> 멀티 플래폼이 아닌 socket프로그램이라면 굳이 bsd소켓 api만 사용하지 않아도 될것입니다. 포팅을 고려한다면...물론 BSD소켓만 사용하시는게 좋겠죠....
윈속2.0 api의 경우 많은 기능과 더 향상된 성능(미미하겠지만)을 발휘한답니다.
 
- 뭐 여러가지 구질구질하게 이야기할 것 없이. BSD API만을 이용해서도 원하는 모든 것을 할 수 있습니다.^^
  윈도우에서 잘 돌아가던 프로그램(예를 들어 ActiveX)을 유닉스(CGI프로그램으로 변경하여)에서 실행해야 한다고 생각해보죠.
  MS의 API Extension은 꼭 필요한 경우에만 쓰시면 좋을 듯 싶군요. 예를 들어 오버랩드 IO정도 겠네요.
  실제로 저같은 경우에 windows/free BSD/linux/solaris 등등에서 돌아가는 프로그램을 작성한 기억이 있는데 애초에 거의 BSD API만을
  이용해 짰는데도 미묘한 API차이 때문에 고생을 많이 했습니다.
  개인적으로 최소한 통신 API만큼은 MS API Extension들이 잘만들었다곤 생각지 않습니다.
 
- 윈도우즈 환경에서 기본적으로 사용해야 하는 MS구현 함수들
  WSAStartup/WSACleanup/WSAGetLastError
- 오버랩드 IO를 사용할 때 써야하는 MS제공 함수들...
  WSASocket(==socket)/WSAConnect(==connect)/WSAAccept(==accept)/WSASend(==send)
- 일반적인 BSD소켓 API함수들...
  socket/connect/accept/send/recv/bind/listen/select/setsockopt/shutdown/closesocket
  gethostbyname/inet_addr/inet_ntoa/ntohs/htons/FD_... 등의 utility함수 혹은 매크로...
- IOCTL함수 구현 - Win32(ioctl)/Unix계열(fcntl)
 
[소켓 코딩시 실제적으로 주의해야할 사항]
*) connect함수를 호출한 다음 언제 상대방이 제대로 접속이 이루어 진 것인가요?
 
==> 굳이 select을 사용해서 체크할 필요 없을듯합니다 connect의 리턴 값만 체크하는것만으로도 충분하다고 생각됩니다.
이부분은 왜 굳이 select을 사용해서 체크하는지 이유가 있다면^^ 알려주세욤^^ (전 그냥 connect의 리턴값 체크로도 잘되었습니당)
 
  const struct timeval time_out = {0L, 0L}; // t.tv_sec, t.tv_usec
  select(1, NULL, &fd_set, NULL, &time_out)의 결과로서 데이터 send가 가능하다고 된 최초의 시점에...
  즉 connect(hSocket, ...)한 다음에
  fd_set에 hSocket한 개만 넣고 위와 같이 했을 때 쓸 수 있다고 되면 최초의 쓰기 가능한 상태인지 보아서 최초였으면 OnConnect virtual함수를 호출하면 되겠죠.
  따로 위의 함수를 connect되었는지 여부를 체크하기 위해 호출하기 보다는 전체 프로그램의 Idle(혹은WM_TIMER)타임에서 어차피 데이터 전송을 위해
  체크해야 하기 때문에 그때 체크해서 해당 소켓 핸들에 대한 최초의 쓰기 가능인지 여부만 검사하면 됨...
 
  - 물론 만약을 위해 데이터 리딩을 시도했을 때 데이터가 이미 와 있다면 이미 connect된 경우겠지요.
  - 가능하면 여러 개의 소켓 핸들 전체에 대하여 한 번에 select함수를 호출해야함...
 
  자세한 내용은 select함수 도움말을 보세요^^
 
- 언제 소켓 접속이 끊어진 것입니까?
  당연 send/recv 등등의 함수들을 호출 했을 때 오류를 리턴할 경우이겠죠^^...
  접속이 끊어지는 경우는 크게 세가지가 있습니다.
 
==> 어느쪽이든 끊고 싶은쪽에서 closesocket해버려도 상관없을듯한데요 만약에 보내구 있던중 데이터의 확실한 전송을 원하시면 linger 을 on해주면 .....음.....타임아웃 값을 조금 늘려주든지 하면 될겁니다.
 
  1 - 이쪽에서 접속을 끊은 경우
  2 - 상대방에서 접속을 끊은 경우
  3 - 네트웍 서브시스템(혹은 인터넷 망사업자) 등등의 오류에 의한 강제 종료 <- 이 경우는 뒤에...
 
  1과 2의 경우는 실제적으론 동일할 경우이겠죠? 아래의 글을 읽어보시면 아시겠지만 둘이 동시에 접속 종료를 요구한 경우도 동일합니다...
  제대로 접속을 종료하는 방식은 다음과 같습니다.
 
  a. 먼저 접속을 종료해야 겠다고 마음 먹은 측에서 데이터를 전부 보냈는지 확인(당연 서로 주고 받을게 없을 때 접속을 끊겠죠 ....^^;;)
  b. 더이상 데이터를 보낼게 없거나 통신/프로그램 오류가 발생하여 접속을 종료해야 하는게 확실한 시점에서 shutdown(hSocket, SEND?)를 호출....
     이후 이쪽에서 데이터를 보내려는 어떠한 시도도 send함수는 오류를 리턴함...
  c. 그래도 상대측에선 데이터를 계속해서 보내고 있을 수 있으므로 recv 함수를 계속 호출해야함...
  d. 상대측에서는 아직 이쪽에서 데이터 전송을 그만 둔지 모르기 때문에 recv를 계속 호출하고 있을 것임
  e. 이쪽(접속을 종료하겠다고 먼저 마음 먹은 쪽)에서 shutdown(hSocket, SEND?)를 통해 더이상 데이터를 안보내겠다고 한 사실이 상대측에 도착하면 상대측 recv 함수가 0을 리턴함...
  f. 위 e. 이후에 상대측에선 나머지 데이터를 다 보내거나 그만 둔 다음 적당한 시점에 shutdown(hSocket, BOTH? 혹은 SEND?)를 호출
     어차피 상대측에서 SEND를 그만둔 상태이므로 이쪽에서 받든 안받든 차이가 없으므로 BOTH/SEND 아무거나 상관없음...
     상대측에선 이 시점에 closesocket()를 불러서 socket핸들을 free하면 됨...
  g. 이쪽에선 c. 상태가 계속 되다가 recv 함수가 0을 리턴하면 상대방도 접속 종료에 동의한 상태 이므로
     이 때 closesocket()를 불러서 소켓 핸들을 free하면 됨...
  - 위의 a~g까지의 단계를 보면 접속을 먼저 종료하겠다고 한 측은 shutdown(SEND?)를 불러서 상대방에 알리고
  상대방도 shutdown(SEND?)를 통해 동의하면 그때 closesocket를 부르면 됨
 
  - 상대방이 접속을 종료하겠다고 먼저 shutdown(SEND?)를 호출한 것을 recv의 return값이 0인 것을 확인해서 알았다면
  shutdown(SEND?)한 다음 바로 closesocket를 불러도 됨.
 
  * 위와 같이 shutdown함수를 closesocket함수 호출 이전에 불러줘야 제대로된 통신 종료가 되는 것인데
  이러한 과정을 graceful shutdown이라고 합니다.
 
*) 만약 위의 과정(graceful shutdown)을 거치지 않고 그냥 접속 종료하고 싶다고 바로 closesocket()을 호출하면 어찌되나요?
  상대측의 recv함수가 0을 리턴하지 않고 SOCKET_ERROR값을 리턴함(물론 운이 좋으면이죠... 대개는 그냥 아무일 없다고 나오죠...)
  이러한 경우는 aborty shutdown이라고 해서 접속 라인 상태 이상, 상대방 프로그램/시스템 down 등등과 구별하기가 어렵습니다...
  물론 이정도만이라면 그냥 그렇게 쓰셔도 되겠죠. 어차피 서로 빠빠이 하는 상태인데요 뭘...
  하지만 당연 이렇게 처리하시면 문제가 발생할 소지가 있습니다.
  이러한 경우 상대방 TCP/IP 처리부에서 이쪽에서 접속 종료한 사실을 모르기 때문에 계속 접속이 살아있게 되는 경우가 많습니다.
  - 자세한 내용은 모르겠지만 TCP/IP FIN(FINE==끝?) CODE인가를 서로 송수신해야 하는데 그게 전달이 제대로 안된다는 듯...
  - 그래서 결론은 바로 다음번 접속 시도때 서로 접속이 안되는 경우가 발생하게 됩니다 ^_^
  - 이러한 경우 대개는 시스템을 리부트 해야만 접속이 다시 됩니다. 그전엔 벼라별 방법을 써도 잘 안되더군요.
  - 혹시 이러한 경우에 TCP/IP Keep Alive 옵션을 켜주면 제대로 접속 종료가 될지는 확인 못했네요... 한 번도 써본 적이 없는 옵션...
 
 
*) 네트웍 서브시스템(혹은 인터넷 망사업자) 등등의 오류에 의한 강제 종료는 어찌 처리합니까?
 
==> 이럴경우에 보통 감지 되지 않습니다. ping/pong으로 체크를 하셔서 종료처리를 서버쪽혹은 클라이언트쪽에서 ...하는거시^^
 
  뭐 천재지변이니 어쩔 수 없습니다. 바로 closesocket부르고 그만두면 됩니다.
  제대로된 오류 처리를 하고 싶으시다면 closesocket 바로 뒤에 다시 socket 만들어서 connect 때려서 연결될 때까지 기다려서
  마치 오류가 없었던 듯이 행동하면 되겠죠 ^^;;;
 
*) 위의 graceful shutdown과정을 순서대로 밟고 제대로 처리했는데 바로 다시 재접속을 하면 접속이 안돼요. 어찌된 것인가요?
 
==> ??? 재접속을 하면 안되는 이유는 다른데 있는듯합니다...
 
  - 주로 클라이언트 쪽 보단 서버에서 listen소켓을 즉시 다시 만드는데 실패하는 경우임.
 
  먼저 linger옵션에 대해서 이해를 하셔야 합니다.
  어느쪽이든지 closesocket을 호출했다고 생각을 해보죠. 그렇다고 TCP/IP 처리부에서 바로 소켓 핸들을 날려버리면 문제가 생길 수도 있습니다.
  왜냐면 소켓 핸들에는 보내야 할 데이터 정보가 달려 있는데 closesocket부르기 전에 send한 데이터들이
  아직 네트웍에 부하 혹은 모종의 이유로 처리가 안된 경우가 있을 수 있습니다. 이 때 개발자 측에서는 이미 소켓을 날려버렸지만
  실제 하부 시스템에선 아직 살아있을 수가 있습니다. 그 상태에서 개발자가 바로 socket을 다시 만들고 같은 주소 같은 port로 바인드를
  시도하면 당연 오류가 발생하죠...
 
  즉 closesocket함수를 호출해도 일정시간 하부시스템에서 핸들이 살아있게 되는데 이 시간이 linger타임입니다.
  이러한 옵션이 편하긴 하지만 방금처럼 바로 재접속 시도를 하는 경우엔 예기치 않은 오류메시지를 출력해야만 하는 경우가 될 수도 있습니다.
 
  따라서 제대로 처리를 하기 위해선 이 linger타임을 0로 설정하고 closesocket 호출을 그 시간 만큼 지연해서 하는 겁니다.
  즉 linger타임 액션을 하부 시스템에 맞기지 않고 직접 처리하는 것이죠.
  linger타임이 0이면 closesocket를 호출하는 순간 바로 socket핸들이 시스템에서 사라집니다.
  하지만 마지막에 보낸 데이터들이 충분히 전송될 시간 정도는 두고서 closesocket를 불러주어야 하겠죠...
 
  가장 제대로된 방법은 제일 마지막 패킷은 서로 접속종료를 확인하는 과정을 거치는 것이죠 그래서 상대방에게서 동의가 오면
  바로 언제든지 접속을 종료해도 되기 때문에 문제가 되지는 않겠지요....
 
  아래 두 줄이 linger타임을 설정하는 방법입니다.
 
  struct linger opt_ling = { 0, 0 }; // ling.l_onoff, ling.l_linger
  err = setsockopt(hSocket, SOL_SOCKET, SO_LINGER, (const char*)&opt_ling, sizeof(opt_ling));
 
*) socket함수를 불러 소켓을 만들고 함수를 부르면 블럭이 됩니다. 어떻게 non-blocked call을 할 수 있죠?
  클라이언트나 listen소켓 쪽에선 socket() 함수로 소켓을 만들자 마자 설정하면 됩니다.
  당연 서버측에선 accept 혹은 WSAAccept 함수를 통해 얻은 소켓 핸들에 대하여 설정하면 되겠죠
  뭐 단순히 소켓 핸들이 만들어지자 마자 항상 linger이나 non-blocked-IO 상태 등을 설정하시면 됩니다.
 
  #ifdef WIN32
    u_long s_ulOPT = 1;
    err = ioctl(hSocket, FIONBIO, &s_ulOPT);
    if (err == 0) { // don't use SOCKET_ERROR
        return TRUE;
    }
  #else // !WIN32 - 유닉스 계열 OS들에서...
    int flags = fcntl(hSocket, F_GETFL);
    flags |= O_NONBLOCK;
    err = fcntl(hSocket, F_SETFL, flags);
    if (err != -1) { // don't use SOCKET_ERROR
        return TRUE;
    }
  #endif /* WIN32 */
 
*) OVERLAPPED IO가 잘 동작하지 않습니다.
  OVERLAPPED IO 사용시 Write하는 쪽만 사용하면 됩니다. Read하는 쪽까지 사용하기엔 프로그램 구조에 문제가 많습니다.
  Read야 시간 남을 때 정해진 버퍼 사이즈안에서 계속 무작정 받아 두면 되고, 패킷 처리부에서 시간 날 때마다 가져다 처리하면 됩니다.
 
==> 현재 서버구현에서 가장 많이 쓰이는게 iocp(windows nt기반에서)을 이용해서 overlapped i/o입니당..-,-a 그리고 보통 overlapped로 read/write한다구 해서 문제가 되는건 아님니당....오히려 성능이 좋다고 설명되어있습니다(win32네트워크프로그래밍;직접 벤치마킹해보진 않았음^^) 님이 말씀하신건 혹시 리눅스의 aio_의 단점이긴합니당^^;
 
 
  Q) OVERLAPPED IO결과가 성공이긴 한데 요구 되어진게 100 바이트 인데 50바이트만 보내고 50바이트는 못 보냈다고 나올 수 있나요?
  A) 현재까진 그런 경우는 본 적이 없습니다. 도큐먼트상으론 가능한데 All Or Nothing로 구현된듯 하네요...
 
==> 이건 저두 확실히 알지 못하지만 all Or Nothing라구 하긴 문제가 있지 않을까요 (10M~100M)을 보내보셍 그럼 100M가 가야하는데 그렇지 않을듯.....저건 스트리밍i/o인 tcp의 특성인거 같기도 하고.....이부분은 저도 확실하지 않습니당.
 
 
  Q) select(fd_count, NULL, &fd_set, NULL, ...)을 이용하여 제대로 쓰기 가능 상태를 체크하는데 제대로 되지 않습니다.
  마치 OVERLAPPED IO처리가 조금씩 지연되고 있는듯 보입니다. 어찌해야 하나요?
  A) select(...) 호출 바로 앞에서 SleepEx(0, TRUE); 를 호출하여 OVERLAPPED IO결과가 프로그램에 전달될 수 있도록 해야 합니다.
  필요한 경우 SleepEx() 함수 안에서 바로 OVERLAPPED IO콜백 함수가 불립니다.
 
==> select와 overlapped i/o와 아무 상관이 없으며 SleepEx을 쓰는 이유도 타당해보이지 않는뎅....^^;;
 
 
음 그리고 버퍼에 넣는 이유는 오류처리의 불편함도 있을수 있겟지만  tcp가 스트리밍io 여서이기도 하고 버퍼링(똑같은 말이네...-..-;모아서 보내면 자잘히 많이 보내는것보다 좋겠져...-,-;)과 동기화등 다른 이유가 더 큰거 같은뎅^^;
  Q) OVERLAPPED IO의 사용에 가장 큰 장점이 있다면 무엇?
  A) 각 소켓 연결마다 필요한 고정 사이즈의 중간 Send용 버퍼의 크기제한/낭비가 없어지며 속도가 빠르다...
 
 
*) 자기만의 TCP/IP 용 라이브러리를 설계하고자 하는 사람이 반드시 알아야 할 한가지가 있다면?
  위에서 CAsyncSocket을 그대로 사용하지 마세요.란 제목으로 된 글의 보충
 
  일반적으로 통신 프로그램은 핑퐁시스템입니다. 이쪽에서 데이터를 보내면 상대방이 응답하죠. 바꾸어 말해도 같은 이야기고요.
  그런데 이 와중에 개발자는 항상 Send와 Recv 함수를 무수하게 사용하게 됩니다. 그리고 각각의 함수의 실행 결과에 대하여서는
  항상 예외 처리를 해야 하고요. 이 때 위에서 예를 들었다 시피 어차피 .Send함수를 사용해서 성공했다고 했도 실제론 실패했을 수도 있기 때문에
  프로그램에선 .Send가 성공했다고 모두가 성공적인 실행이라고 가정할 수 없습니다. 그러니 아예 모든 .Send문이 성공했다고
  가정하는 것이죠. 즉 받은 데이터 패킷에 대하여 결과를 리턴한 내용들이 일단 무조건 성공했다고 가정합니다.
  .Send함수는 TCP/IP의 send함수를 바로 부르지 않고 그냥 정해진 버퍼에 쌓아 두기만 하는 거죠.
  그다음 프로그램의 Idle 타임에 실제 send() 함수를 호출하여 데이터를 전송하면 되는거죠.
  만약 이때 오류가 발생 한다면 송신부는 여기밖에 없기 때문에 단 한 곳의 오류 처리만 하면 됩니다. 더군다나
  Idle루틴의 특성상 깊숙이 들어간 함수 호출이 아니기 때문에 상대적으로 오류처리도 쉽습니다.
  송신할 데이터는 얼마가 될지 아무도 장담할 수가 없습니다. 그래서 가변 버퍼(혹은 OVERLAPPED IO)를 쓰면 좋을 듯 하고요.
  
  수신부는 고정크기의 버퍼를 가지면 됩니다. Idle루틴에서 항상 버퍼가 비어있는 만큼 읽으려 시도해서 가능하면 많이 읽어서 쌓아두면
  패킷 처리부에서는 버퍼의 제일 앞에서부터 읽어보아서 처리할 만한 양의 데이터(일반적으로 한 개 이상의 완료된 패킷)가 다 도착한 다음에서야
  읽기 버퍼에서 읽어서 지우면 됩니다. 읽기는 버퍼가 가득차면 더 이상 읽어가지 않으면 되기 때문에
  버퍼 크기를 적당히 알아서 지정하면 되겠죠...
 
  이러한 시스템의 장점은 프로그램 전체를 통틀에서 실제 send/recv 함수 호출이 딱 한 군데라는 것이죠...
  오류처리가 무지하게 편해집니다.^^
 
  다만 Idle루틴은 여러가지 경우를 보아 윈도우에 Timer를 설정하여 WM_TIMER를 받아서 처리하는게 좋을 듯 싶군요.
  시간은 5-100밀리 세컨드 정도 편하신데로 하시면 됩니다. 어차피 늦게 보면 그만큼 많은 데이터가 처리 되어있을 것이기 때문에 문제 없겠네요
 
 
  CWinApp::OnIdle() 이 편하다고 생각하실지 모르지만 문제가 몇가지 있습니다.
  1 - CPU 점유율이 거의 항상 100%가 됩니다. 대부분... Timer를 5밀리 세컨으로 잡아도 한자리 수 단위로 떨어집니다.^^
  2 - Modal 대화상자가 뜨게 되었을 때 Idle루틴을 돌 수가 없어서 통신이 잠시 멈추게 됩니다.^^;;;
 
====================================================================================
* 위의 글에서 소켓 코딩은 그냥 TCP/IP(UDP 제외한...)를 이용한 코딩을 의미합니다.
* 최소한 CSocket/CAsyncSocket은 써보고 대안을 찾는 사람을 대상으로 한 글입니다.^^
* 소켓코딩을 해서 실제 프로그램을 제작했는데 문제가 생긴 경우 혹시 프로그램상의 문제가 있다면 오류를 찾는데 도움을 드리기 위한 겁니다.
* 그래서 대부분의 글 내용이 BSD소켓 API를 이용한 async(non-blocked) call에 관한 내용입니다. 물론 win32 환경에서는 오버랩드 I/O를 사용하고 있다고 생각하시면 됩니다.
* 그리고 개인적으로 온라인 게임 서버/클라이언트, 메신저의 다자간 채팅 서버/클라이언트, 웹 서비스 회사의 ActiveX/CGI/인증서버 등등 많은 소켓 응용 프로그램을 제작한 경험상, 대부분의 상황이 접속 요구가 굉장히 빈번하고 수많은 오류 처리를 해야하는 상황하의 코딩들만 해보았기 때문에 CSocket/CAsyncSocket을 이용하는 경우나, 구조적 안정성이나 프로그래밍적인 편리성 등이 필요없는 상황(그저 대충대충해도 뭐든지 가능한^^;;)은 솔직히 전혀 생각해 본 적이 없었더군요...
=================
---------------------------------------------------------------------------------
논점1: blocked call / timeout / 오류처리
---------------------------------------------------------------------------------
 - 1:1접속이 아닌 1:다 의 경우 mmorpg가 아닌경우엔 순간전송량이 많고 접속자수가 그리 많지않은(ftp나 file 관리서버)같은경우엔
blocked call이 훨씬 좋을수 있습니다.  
 - 그리고 CSocket이 recv/send을 남발한다구 했는데 그건 asyncsocket두 마찬가지아닌가요 이건 어떻게 소켓을가지고 프로그래밍 했느냐의 문제일듯...
 - timeout에 대해서.... 실제루 그렇게 사용되구 있구요(ftp서버나 telnetd, httpd) 그리소 송수신 호출부에 타임아웃을 거는게 아니고 소켓의 옵션을 말했던겁니다.
 
 
1:1 이라고 한 의미를 확장한다면 위 원문에서 말씀드렸다시피 한 서버에 접속한 사용자들끼리 서로 데이터 동기화가 없다면 그것도 1:1 이라고 볼 수 있겠죠...
제가 궁극적으로 CSocket에 대한 단점을 말씀드리는 요지는 효율성/특수목적에의 적합성(당연 CSocket이 맞는 경우엔 CSocket을 써야죠...) 이런 것을 떠나서 프로그래밍 습관에 관한 이야기 입니다.
CSocket은 send/recv 남발이고 Async인 경우엔 그렇지 않다는 뜻은 CSocket은 아무데서나 자기가 원하는 때에 원하는 통신을 할 수 있기 때문에 구조적으로 잘 정리된 프로그래밍이 태생적으로 요구되지 않는다는 것입니다. 그에 비해 Async콜인 경우엔 다들 아시다시피 이벤트 기반으로 프로그램들이 돌아갈 수 밖에 없고 당연 개발자는 상태머신 유지라든지 오류처리라든지 하는 부분에서 좀더 고민하고 프로그래밍할 수 밖에 없게 된다는 겁니다. 즉 CSocket이 쉽게 가능한 것들이 Async콜에선 가능하지 않은게 많기 때문에 Async콜들이 당연히 좀더 구조화 되게 되고 결과적으로 개발자에게 도움이 될 수 있다는 거죠... 결국 제대로된 Async콜에 대한 경험과 개인적인 라이브러리들이 생기게 될텐데 그럼 CSocket에 관한 필요성이 없어지거든요. 뭐 깔끔하기로 말하면 CSocket이 월등하게 깔끔한 코드를 만들어 낼 수는 있을겁니다. Async콜들은 깔끔하다기 보단 무지 복잡하고 지저분해질 수는 있죠... 결국 제 개인적 생각으로는 CSocket과 Async콜 둘 다를 써먹어야 한 다면 Async콜 쪽에 더 무게를 두고 싶다는 것입니다. CSocket을 쓰기 시작하면 별로 가능하지 않은 상황에서도 CSocket을 쓰려고 억지를 부리게 될 가능성도 있습니다. 실제 통신 초보자들이 많이들 Async라면 애초에 고생할 필요가 없는 문제들에 대해서조차 그렇게 고생들을 하죠..
 
---------------------------------------------------------------------------------
논점2: winsock2을 사용할것인지 bsd호환 api을 사용할것인지?
---------------------------------------------------------------------------------
전적으로 동의합니다... 하지만 엄청난 성능 향상(오버랩드 I/O는 정말 쓸만하죠^^)이 이루어지는 것이 아닌데 굳이 윈도우즈에서만 사용되는 API만을 사용하면 웬지 이상하지 않느냐는 정도로 이해하시면 될듯...
 
엄청난 성능향상은 없지만 제가 알기론 overlapped I/O을 사용하는 WSASend/Recv의 경우엔 30%의 성능향상이 있으며...(network programming for mcrosoft windows와 개인적인 테스트)
iocp나 기타 winsock2에서만 지원하는 각각의 객체들도 winsock1대에서는 사용할수 없기때문에 성능상으로 따지고 보면 많은 향상이 있다는것입니다. 실제로 예전 테스트에서 select을 사용하는것보다 winsock2에서 제일 비슷한 eventselect가 성능상 우위에 있었습니다.
 
에 저도 overlapped I/O(Writing부분만^^)를 윈도우즈 환경에서 작업할 때는 반드시 사용합니다...^^ 실제 다른 event기반한 select문들을 사용하지 않는 것은 단순히 제 통신라이브러리의 구조상 bsd와 winsock 둘 다를 구조적으로 지원할 수 없기 때문입니다. 물론 고민을 하면 가능하겠지만 저처럼 unix 계열들에서도 돌아가야 한다면 우선적으로 bsd api코딩을 할 수 밖에 없고 당연 이후에 winsock에 특화된 루틴(특히 event기반한)을 conditional compile을 통해 지원할지 말지는 전적으로 개발자 개인의 마음이겠죠... 음 언제 bsd api 기반한 통신 라이브러리들에서 winsock(특히 2.x) 특화 함수들을 지원하는 문제에 대하여 고민해보아야 겠습니다.^^ 성능향상 문제에 있어서는 오버랩드 I/O를 제외한 다른 부분에 대해서는 실제적으로 경험한 바가 없다고 해야겠습니다.^^
 --------------------------------------------------------------------------------
논점3: overlapped i/o
---------------------------------------------------------------------------------
소켓 프로그램 구조적으로 write는 오버랩드 I/O에 아주 잘 맞습니다. 하지만 문제는 read쪽인데 프로그램에서 read할 데이터의 사이즈가 명백한 상황에서는 오버랩드 I/O를 이용한 코딩이 아주 도움이 되겠죠.. 하지만 대개의 경우 소켓코딩에서는 읽어야할 데이터의 수가 미리 고정되어 있지 않습니다. 그저 상대방이 보내주는 데이터를 읽어야 하는데 패킷의 크기도 가변이 태반이기 때문에 미리 사이즈를 주고 읽어야 하는 오버랩드 I/O와는 맞지 않는다는 겁니다. 고작 고정 사이즈의 작은 패킷 헤더를 읽기 위해 쓰기엔 아깝죠... 다만 파일처리쪽에선 많이 다르겠죠. 파일처리시엔 이미 데이터의 크기를 알고 있기 때문에 사용하면 무지 편하겠죠...
 
참고로 overlapped i/o는 제가 님의 말씀을 듣고 찾아본결과 현재 수면위로 올라온 버그는 없습니다. 그리고 커맨드가 가변일경우에 아깝다구 했는데....왜 그런지 이해하기 힘들구 제 생각에 님 기법이 데이타 사이즈을 recv 그후에 데이터를 recv해서 파씽하시나 본데 그렇게 하지 않고 그냥 일정 버퍼크기만큼을 받아서 파씽하는 방법을 보통 사용하죠 (overlapped i/o시)
 
저같은 경우도 읽기시에 가능하면 현재까지 들어온 데이터를 다 읽어서 하나 혹은 그 이상의 완결된 패킷인지 검사하여 완결되었으면 처리하고 아니면 그냥 쌓아두었다가 다음에 온 데이터와 합해서 처리하게됩니다. 제가 리드시에 오버랩드 I/O가 쓸모가 좀더 적다고 한 이유는 다음과 같은 경우때문입니다.
 
* 만약 오버랩드 I/O를 통해 데이터를 읽는다고 가정해보죠. 그리고 이번에 들어올 패킷의 크기는 알 수 없고요. 대략 1024 byte미만이라고 하죠.
* 먼저 오버랩드 I/O 리딩을 통해 나타나는 결과는 세 가지 입니다.(1024 byte를 다 읽든지 아님 중간에 타임아웃을 두든...)
1. 정확히 한 개의 패킷을 읽은 경우, 2. 한 개의 패킷에서 데이터가 조금 모자라는 경우, 3. 한 개의 패킷이 완결되고 다음 패킷 중간까지 읽은 경우
* 제가 지적하고자 하는 문제는 위의 2,3번의 경우입니다. 결국 2,3번은 같은 이야기겠죠. 원래대로라면 대부분의 패킷은 패킷 단위로 같이 도착하게 되는데 위에서처럼 오버랩드 I/O를 통해서 읽은 경우엔 패킷이 중간에 잘릴 수 있다는 겁니다(recv 루핑도 같죠^^). 이미 패킷들은 완료된 형태로 전달되어 있는데도 말입니다. 즉 오버랩드 I/O로 읽혀진 부분과 이쪽에 전달은 되었지만 읽혀지지 않은 부분 이렇게 두 부분이 되게 되죠... 결국 나뉘어진 패킷들을 연결해서 처리하기 전엔 오버랩드 I/O를 통해 읽은 데이터들을 그 자체로 처리하고 메모리를 free할 수 없는 상황이 생긴다는 거죠... 결국 위에 대한 해결책은 serial하게 오버랩드 I/O를 통해 읽은 데이터들을 재구성할 버퍼를 만들고 그곳에 순차적으로 복사를 하는 것이겠죠. 실제로는 메모리를 복사하는 방식을 취하지 않고 고정크기의 버퍼를 두고 오버랩드 I/O의 대상 버퍼의 위치를 조금씩 이동하면서 바로 바로 사용하겠죠. 어쨌든 중요한 것은 오버랩드 I/O를 사용하든 안하든 읽기버퍼를 두어야만 한다는 것입니다. 버퍼의 크기/용도/지속성 등 모든 경우에 오버랩드 I/O와 recv루핑을 통해 읽는 경우 모두 동일한 것이죠.
* 즉 오버랩드 I/O 리딩과 recv루핑을 통해 읽는 경우가 다른 점은 유일하게 읽기 버퍼를 채우는 방식일 뿐입니다.
* 그렇다면 문제시 되는 것은 유일하게 효율성입니다. 실행상의 효율성 혹은 코딩상의 효율성인지 애매하긴 하지만 말입니다.^^
* 실행상의 효율은 분명 오버랩드 I/O에 있습니다. winsock시스템의 임시 메모리 부하도 줄일 수 있고요.
* 제가 보기에 오버랩드 I/O의 Reading/Writing의 차이는 아무래도 총 버퍼크기를 예측할 수 있느냐 없느냐의 차이인 듯 싶습니다.
  예측이 불가능한 Writing시엔 버퍼를 가변으로 해서 메모리를 realloc하거나 고정크기의 버퍼를 리스트로 연결하거나 해야하는데 이런 복잡함 보다는 오버랩드 I/O를 사용하는게 훨씬 간단하고 편할 수 있겠죠. 즉 실제적인 필요성(코드의 간단함, 메모리의 최소 사용,메모리 복사 등에 의한 실행효율의 낭비방지)이 크다는 거죠. 실제 오버랩드 I/O가 지원되지 않는 유닉스 계열에서 저같은 경우는 그냥 고정크기 버퍼를 잡고 버퍼가 넘치면 사정없이 접속을 종료합니다. 버퍼가 넘치는 상황처리가 귀찮아서죠^^;;;
왜냐면 버퍼가 넘친다는 것은 상대방이 안가져 간다는 건데 시간을 충분히 주어도 안가져간다면 문제가 있는거죠...  하지만 메모리를 충분히 크게 잡아야 한다는 단점이^^;;
  총 버퍼 크기가 예측이 가능한(이라기 보단 개발자가 그냥 임의의 수치를 정하는 것이지만) Reading시엔 아무래도 복잡한 처리(Writing시에 비해 Reading시의 오버랩드 I/O의 복잡성은 꽤 클 듯...)를 거쳐야하는 오버랩드 I/O보단 논리적으로 단순하고 호환성이 있는 recv 루핑을 선호하게 되는 군요. 물론 절대적인 오버랩드 I/O 리딩의 복잡도가 얼마나 되는지는 구현해보지 않아서 모르겠습니다. 죄송^^;;
 * 음 여기까지 쓰고 보니 특별히 패킷 리딩시에도 오버랩드 I/O를 배제할 딱히 커다란 이유는 없군요 ^^;;;
  당장 구현해 보아야 할 듯 싶군요.
  생각해보니 지금까지는 오버랩드 I/O 리딩에 관한 특별한 처리의 필요성을 느끼지 못하고 있었습니다. 괜시리 고생한다는 생각을 했는데 만약 성능의 차이가 크다면 충분히 활용을 해야할 듯 싶네요. 제 개인적인 생각으로는 다수의 접속을 처리하는 시스템이 아닌 경우엔 논리적인 단순함과 구현이 간편한 recv 루핑을 사용하고 mmorpg처럼 성능이 요구되는 경우엔 오버랩드 I/O를 사용하면 좋겠군요. 물론 루틴이 완성되면 특별히 둘을 구분해 사용할 이유는 없겠죠...
* 어디 성능상의 차이에 대해 측정된 자료같은게 없을까요? winsock 전용 함수들과 bsd api함수들과의 비교데이터 같은게 있으면 좋을 듯 한데요.
* 하여간 제가 간과한 부분에 대해서 깨우쳐 주시느라 수고 많으셨습니다.^^ 좀더 고민을 해보도록 하겠습니다.
 
---------------------------------------------------------------------------------
논점4: socket가 스트리밍i/o라는 말이었는뎅...    
---------------------------------------------------------------------------------
스트리밍 I/O 처럼 속도까지 필요한 경우엔 독립적인 Read/Write 쓰레드 까지 사용될 수 있으므로 동기화 때문에라도 버퍼의 필요가 극대화 되겠죠.^^
 
다들 아시는것처럼 데이터량이 많으나 적으나 버퍼링으로 얻는 이점은 많답니다^^;
 
 * 이 부분은 제가 Audio/Video 스트리밍 처리로 잘 못 알아들었네요...^^ 어쨌든 통신에 있어서의 버퍼링은 필수적일 수 밖에 없다고 생각됩니다...
 
=====================================================================================
마지막 부분에   bsd api 와 윈도우소켓의 성능 비교는 ms press의 network programming for microsoft windows랑 win32네트워크 프로그래밍을 보시면 나와있구욤... 음 오해가 생길수 있겠네요....bsd api로 windows에서 돌린거랑 윈도우즈 소켓으로 overlapped로 돌렸을때의 상황이구요   유닉스 계열의 bsd 소켓과의 성능차이는 비교된 자료가 별루 없네요...^^ 솔라리스와 윈도우즈의 iocp랑 비교한건 본거 같은데... 궁금하실까봐...대충 말하면 소켓의 io의 효율성은 솔라리스가 높았고....iocp의 사용으로 윈도우즈에서도 나쁘지 않은 결과가 나오긴한거 같은데 솔라리스가 조금 앞선걸로 기억되네요^^;
 
출처 : 데브피아 팁게 편집

'프로그래밍' 카테고리의 다른 글

WINVER or _WIN32_WINNT  (0) 2012.08.27
A reusable, high performance, socket server class  (0) 2012.08.16
[펌] ACE 강의(2)  (0) 2012.08.13
[펌] ACE 강의(1)  (0) 2012.08.13
[펌] ACE_Message_Block Pool 만들기  (0) 2012.08.13