반응형
http://wiki.kldp.org/wiki.php/CSocketFAQ
C 소켓 프로그래밍 FAQ
김한수 iryna7@yahoo.com
2003.11.30
Unix/Linux 프로그래밍을 시작하시는 분들이 가장 많이 관심을 갖는 분야는 아무래도 소켓 통신 분야가 아닐까 생각이 됩니다. 그러나 훌륭한 교과서와도 같은 서적들이 존재함에도 불구하고 경험자들만이 알고 있는 이런 저런 문제 해결책들을 처음 시작 하는 분들에게 제공해드리고자 C 소켓 프로그래밍 FAQ를 제작합니다. 이것은 어느 누구 한명의 저작물이 아닌 여러 모두의 공동 저작물입니다. 부디 이 작은 시작으로 부터 커다란 Unix/Linux 공동체를 키워나가시길 기원합니다.
-
- 1 일반 정보
- 2 TCP/IP 통신
- 3 C/S 프로그래밍
-
- 3.1 SUN Solaris에서 컴파일은 제대로 되나 링크시 에러가 납니다.
- 3.2 송/수신시 데이터가 다르게 나타납니다.
- 3.3 일정시간이 지나면 접속이 자동으로 끊깁니다.
- 3.4 bind() 호출시 "address already in use" 에러가 발생한 합니다.
- 3.5 제 서버를 데몬 모드로 동작시키고 싶습니다.
- 3.6 리모트 시스템의 MAC 주소를 알고 싶습니다.
- 3.7 특정 서버에 접속시 "connection refused"라는 에러 메시지가 나옵니다.
- 3.8 개별 사용자(접속)에 대한 관리에 대한 개념도는 어떻게 그리시나요.?
- 3.9 서버 프로그램 제작에 대한 사례를 들어 주세요.
- 4 서버 모델(SELECT/POLL/FORK/THREAD)
-
- 4.1 select()는 무엇인가요.?
- 4.2 select()를 이용한 다중서버 구성은 어떻게 하나요.?
- 4.3 select()가 블락(Block) 당합니다.
- 4.4 select()에서 설정한 struct timeval *timeout 값이 변경되는것 같습니다.
- 4.5 select()에서 상대편의 접속 해제는 어떻게 판단할수가 있나요.?
- 4.6 connect() 호출시 서버가 응답하지 않을 경우 일시적인 지연이 발생합니다.
- 4.7 위의 방법을 데이터 송/수신시에도 적용이 가능한가요.?
- 4.8 send()를 호출하여 데이터 전송시 PIPE 시그널이 발생합니다.
- 4.9 select()를 이용했는데 CPU 사용량이 걱정됩니다.
- 4.10 select()를 이용하는데 가끔 CPU가 100%에 이릅니다.
- 4.11 select()가 수용가능한 최대 fd값은 얼마입니까.?
- 4.12 select()를 이용해 시리얼 프로그래밍이 가능한가요.?
- 4.13 select()를 이용한 메시지큐가 가능한가요.?
- 4.14 서버 프로그램에 좀비 프로세서가 생깁니다.
- 5 HTTP 프로토콜
- 6 이기종간의 데이터 송/수신
- 7 보안프로그래밍
- 8 UDP
1.1 C 소켓 프로그래밍 FAQ는 어떻게 관리 되나요? ¶
C 소켓 프로그래밍 FAQ는 여러 자원봉사자들에 의해 유지 보수되어 집니다. 따라서 특별한 관리자는 존재하지 않습니다. 이 FAQ는 KLDP WIKI에서 관리되어 지며 불특정한 시기에 한번씩 버전을 내놓도록 하겠습니다. 최신 버전은 http://wiki.kldp.org/wiki.php/CSocketFAQ에서 확인하실수 있습니다.
1.2 본 FAQ는 누구를 위한 것입니까? ¶
먼저 C 소켓 프로그래밍 FAQ는 제목에서 볼수있듯이 C언어 사용자를 위해서 제작되었습니다. 따라서 자바/WINDOWS 프로그래머분들께서는 다른 FAQ를 참조하시기 바랍니다. 또한 본 FAQ는 C언어를 처음 배운 개발자, 좀더 자세한 정보를 얻기 원하는 사용자 혹은 책을 읽어 보았으나 (누구 책 어디 찾아 보시면 됩니다 하는 대답은 이제 듣기 싫은 사용자) 잘 이해를 하지 못하는 대부분의 사용자를 대상으로 하고 있습니다.
1.3 본 FAQ를 이용하는 저작권은 어떻게 됩니까? ¶
어느 누구도 어떠한 목적으로도 사용이 가능합니다. 즉, 어느 누구나 수정, 출판및 재 배포등 어떠한 용도로도 사용이 가능합니다.
2.1 TCP/IP 통신의 가장 커다란 특징은 무엇입니까. ¶
프로그래머로서 TCP/IP의 여러 측면을 알아 보는것은 좋으나 가장 중요한 몇가지가 특성이 있습니다. 그것은
- TCP/IP 데이터의 전달을 보장합니다.
- 언제 전달될지는 보장되지 않습니다.
2.2 TCP/IP 프로그래밍 초보자입니다. 책이나 사이트 추천 부탁드립니다. ¶
TCP/IP 프로그래밍을 처음 시작하신다면 서점에서 어떠한 UNIX TCP/IP 프로그래밍 서적을 구입하셔도 훌륭 하다고 보증할수 있습니다. 모든 서적이 잘 기획되고, 만들어졌습니다. 그래서 하나를 추천해달라고 부탁하면 Richard Stevens씨의 책을 권합니다. 다만 한글로 번역된 어떠한 Richard Stevens의 책도 구입하지 마시기 바랍니다. 일반적으로 사용되는 컴퓨터 용어들을 무리하게 한글화하는 바람에 읽는데 김치하용어사전(현재 서비스 제공중지)이라는 문서의 도움없이는 단 한줄도 읽을수 없을겁니다. 한때 용어 사전이 인터넷에 존재했는데 지금은 서비스되지 않습니다.
책 :
- UNIX Network Programming by W. Richard Stevens
- TCP/IP Illustrated by W. Richard Stevens
3.1 SUN Solaris에서 컴파일은 제대로 되나 링크시 에러가 납니다. ¶
Linux에서는 기본적으로 링크해야할 소켓라이브러리가 기본적으로 있는 반면에 SUN Solaris에서는 별도로 링크를 해주어야만 합니다. 다음과 같은 옵션을 주어 라이브러리를 링크해주시기 바랍니다.
-lsocket -lnsl
3.1.1 소켓의 접속해제는 어떻게 알수가 있습니까.? ¶
대부분의 경우 recv()에서 반환값이 0인 경우에는 상대편에서 접속을 해제한것으로 판단하시면 됩니다.
recv()가 반환하는 값은
- recv() = 0 상대편의 접속 해제
- recv() > 0 recv()가 읽어 들인 바이트
- recv() = -1 에러
3.2 송/수신시 데이터가 다르게 나타납니다. ¶
데이터 송/수신시 데이터가 다르게 나타나는 이유는 여러가지로 추측해볼수 있는데
- 바이트 패딩 오류(x86및 기타 다른 플렛폼)
- 바이트 오더 차이(x86및 기타 다른 플렛폼)
- 데이터 송/수신 구분
이경우 또 한가지 추측해볼수 있는것은 전송하는 측에서 1,000 바이트를 전송했다고 생각했지만 send()의 리턴값을 프린트 해보면 512 바이트만 출력되는 경우가 있습니다. 이것은 나머지 정보는 송신 버퍼에서 대기열에 존재하는것입니다. 따라서 원하는 데이터를 한번에 모두 전송하기를 원할경우에는
int sendall(int s, char *buf, int *len) { int total = 0; // how many bytes we've sent int bytesleft = *len; // how many we have left to send int n; while(total < *len) { n = send(s, buf+total, bytesleft, 0); if (n == -1) { break; } total += n; bytesleft -= n; } *len = total; // return number actually sent here return n==-1?-1:0; // return -1 on failure, 0 on success }
와 같은 코드를 이용하실수 있습니다(데이터 송신시 위의 코드는 아주 일반적으로 널리 사용되는 코드입니다) 즉, 데이터 송신시 오류가 발생하지 않으면 원하는 데이터 크기만큼 전송이 되어야만 루프를 빠져나오게 만든것입니다.
또한 일반적인 TCP/IP 통신에서는 프로토콜 설계를 할때 데이터의 크기 정보(데이터의 크기가 일반적으로 가변적인 경우)를 전달하기 위해 데이터를 헤더+바디 부분으로 구분해서 전달을 하며 이중 헤더 부분의 크기는 일정하게 고정되어 있는 경우가 보통이며 바디 부분의 크기는 헤더부분의 특정한 위치에 실어서 보내는것이 일반적입니다. 따라서 전달측에서는 보내고자 하는 데이터 바디 부분의 크기를 알아낸후에 데이터 헤더부분에 그 바디의 크기를 실어서 보내면 수신측에서 일정한 크기로 지정되어 있는 헤더부분을 수신한후에 그곳에서 바디 부분의 크기를 읽어 들인 다음에 그 크기 만큼 바디 부분을 수신하는 방법을 채택하시면 됩니다.
아래의 예제는 데이터 전송을 헤더+바디 부분으로 구분핵서 전송하는 예를 들어 보인것으로 송/수신 측에서는 헤더 부분의 크기는 항상 4 바이트라고 규정을 지어 놓아서 수신측에서는 우선 4바이트만 수신을 해보고 데이터 부분을 파싱해보고 바디의 크기가 02 바이트라는것을 알고 재차 3바이트 만큼 수신을 해서 나머지 바디 부분을 수신해서 완료하는 그림입니다. (참고, SOH EOT 부분은 무시해도 좋습니다)
헤 더 바 디 데이터 구분+바디 사이즈+ SOH 가변적인 데이터 + EOT SENDER RECEIVER 1. A03FH ---> A03 2. Header Area Parse 3. ---> FH
참고 : 아래의 개념도는 'A','B','C','D'를 4번 전송한다고 해도 수신측에서 4바이트 수신을 해버리면 수신측에서는 송신측이 4번에 나누어 전송했다고 해도 한번에 송신 한것으로 판단이 됩니다. 즉, 두가지 모양이 모두 같은것으로 판단하시면 됩니다.
SENDER RECEIVER 1. A ---> 2. B ---> 3. C ---> 4. D ---> ABCD --------------------------------------------- SENDER RECEIVER 1. ABCD ---> ABCD
3.3 일정시간이 지나면 접속이 자동으로 끊깁니다. ¶
서버 혹은 클라이언트 측에서 특별한 조치를 취한것도 아닌데 일정한 시간이 지나면 접속이 끊기는것은 사내의 방화벽을 의심해볼 필요가 있습니다. 대부분의 사내 방화벽들이 HTTP 프로토콜만 허용하도록 설정이 된 경우가 많은데 HTTP의 경우 장시간 접속을 유지할 필요가 없기 때문에 일정한 시간 접속이 지속되면 자동으로 접속을 끊어 버리도록 되어 있습니다.
그러나 C/S가 사용자의 인증및 보안시스템이 탑재되어 있다면 한번 접속이 끊기게 되면 새롭게 접속하여 인증을 해야 하는 불편한점이 생기므로 이럴 경우 손쉽게 해결할수 있는 방법은 C/S간에 일정 시간(접속을 끊어버리는 시간) 이내에 CHIT-CHAT(혹은 ) 메시지를 보내도록 만들어 놓는 것입니다. 이것은 클러스터링 시스템에도 유용하게 사용되는것으로 시스템이 서로간의 정상 유지중인지를 판단하는데도 이용될수 있습니다. 대부분의 시스템에서는 의미없는 문자를 주고 받는 경우도 있지만 일련 번호를 서로 전송하는 방법도 사용되어 집니다.
다만 이러한 시스템의 단점은 서버측에서 클라이언트가 일정 시간 사용이 없으면 끊어버리게 만드는(가용 자원의 확보 측면) 방법을 다른쪽에서 구현해 놓아야 한다는것입니다. 대부분의 비즈니스 C/S에서는 클라이언트측 사용자들의 사용 패턴이 한번 서버에 접속해놓고 며칠이고 사용하는 경우가 많다는 점에 착안을 해야 하므로 서버의 가용 자원 확보라는 측면에서 접근을 해야 합니다.
3.4 bind() 호출시 "address already in use" 에러가 발생한 합니다. ¶
대부분의 경우에는 서버를 중단시킨후에 바로 재시작을 했을때 기존의 프로그램 소켓이 아직도 점유하고 사용하기 때문에 발생하는 에러메시지입니다.
이것은 서두에 우리가 정의 했듯이 TCP/IP의 특성 때문에 발생하는 것입니다. 즉, TCP/IP 데이터가 반드시 전송된다는것은 보장하지만 언제 전송이 되는지 보장하지 못하는것입니다. 이부분을 보면 두개의 프로그램이 서로 TCP/IP 통신을 하고 있다가 한쪽이 중단을 하게 되면 서버쪽에서는 TIME_WAIT 상태로 들어 가게 됩니다. 이것은 정말 모든 데이터들이 다 전송이 되고 수신이 되는지 확인하기 위한 절차입니다. RFC 793 문서를 보게 되면
접속 해제시
TCP A TCP B 1. ESTABLISHED ESTABLISHED 2. (Close) FIN-WAIT-1 --> <SEQ=100><ACK=300><CTL=FIN,ACK> --> CLOSE-WAIT 3. FIN-WAIT-2 <-- <SEQ=300><ACK=101><CTL=ACK> <-- CLOSE-WAIT 4. (Close) TIME-WAIT <-- <SEQ=300><ACK=101><CTL=FIN,ACK> <-- LAST-ACK 5. TIME-WAIT --> <SEQ=101><ACK=301><CTL=ACK> --> CLOSED 6. (2 MSL) CLOSED Normal Close Sequence Figure 13.
와 같은 절차를 거치게 됩니다. 이부분은 TCP/IP의 접속, 접속해제에 관한 조금더 복잡한 절차가 존재하므로 차후에 다시 설명 드리도록 하겠습니다. 내용이 조금 깁니다
서버측에서 이와 같은 문제를 해결하기 위해서는 간단히 setsockopt() 함수에서 4번째 인자를 SO_REUSEADDR 값을 줌으로서 해결하실수 있습니다.
int setsockopt(int s , int level , int optname, const void * optval , socklen_t optlen);
3.5 제 서버를 데몬 모드로 동작시키고 싶습니다. ¶
서버를 데몬으로 동작시키는 방법은 자신의 프로세서가 시스템상에서 자식(CHILD) 프로세서가 되어야 하고 자신의 모(PARENT)프로세서는 1번 즉, init 프로세서가 되게 만들면 됩니다. 이때 시그널을 이용하여 여러가지 포어그라운드상에서 동작되는 시그널들을 동작하지 않게 하는것이 좋습니다.
만약 이 서버가 프로세서(fork) 방식의 서버라면 향후 생성되는 자식 프로세서의 번호는 최초 서버 구성시 할당받은(하지만 이것은 데몬 모드로 동작되면서 사라짐) 프로세서 번호 다음부터 생성이 될것입니다.
int init_daemon() { pid_t pid; if ((pid = fork()) < 0) { exit(0); } else if (pid !=0) { exit(0); } setsid(); /* 프로세서를 세션 리더로 설정 */ }
3.6 리모트 시스템의 MAC 주소를 알고 싶습니다. ¶
리모트 시스템의 MAC 주소를 알아내는것은 IP주소를 이용해서 가능하지만 아주 일반적인 상황아 아니면 그것이 별반 의미가 없는 경우가 많습니다. 그것은 요즈음 기업의 인터넷 상황이 대부분 방화벽을 통과하게끔 설계가 되어 있기 때문에 그렇습니다.
int ClientMacAddress(char *ip, char *macaddress) { struct sockaddr_in sin = { 0 }; struct arpreq myarp = { { 0 } }; int sockfd; unsigned char *ptr; sin.sin_family = AF_INET; if (inet_aton (ip, &sin.sin_addr) == 0) { printf ("'%s' address is not valid\n",ip ); return -1; } memcpy (&myarp.arp_pa, &sin, sizeof myarp.arp_pa); strcpy (myarp.arp_dev, "eth0"); if ((sockfd = socket (AF_INET, SOCK_DGRAM, 0)) == -1) { printf ("cannot open socket\n"); return -2; } if (ioctl (sockfd, SIOCGARP, &myarp) == -1) { /* 로컬일 경우 에러가 발생할 가능성이 */ printf(" no entry in arp_cache for '%s'\n",ip); return -3; } ptr = &myarp.arp_ha.sa_data [0]; snprintf(macaddress, 50, "%2.2X:%2.2X:%2.2X:%2.2X:%2.2X:%2.2X", *ptr, *(ptr+1),*(ptr+2), *(ptr+3),*(ptr+4),*(ptr+5)); return 0; }
3.7 특정 서버에 접속시 "connection refused"라는 에러 메시지가 나옵니다. ¶
현재 접속하고자 하는 서버의 주소/포트를 정확히 확인하시기 바랍니다. 만약 정확하다면 그 서버에 원하는 포트가 리스닝(listening)중인지 확인하셔야만 합니다. 그것은 netatat -a 명령어로 확인이 가능하며(예제 화면) 정확히 동작중이라고 하면 방화벽을 의심해보시기 바랍니다. 또한 의심할만한 부분은 접속하고자 하는 서버에서 과도한 접속이 들어와 접속이 거부되는 상황입니다.
3.8 개별 사용자(접속)에 대한 관리에 대한 개념도는 어떻게 그리시나요.? ¶
이것은 프로그램 개발자들의 취향에 따라 다를수가 있습니다만 보통의 경우는 개별 접속을 개별 사용자로 인식하여 하나의 접속이 들어 올때 그 접속에 대한 구조체를 생성하게 됩니다. 그 구조체 안에는 리모트IP, 시간, FD등등을 기록해 두었다가 필요시 활용하게 됩니다. 이때 개별 접속에 대한 가장 중요한 키가 되는 값을 FD입니다.
3.9 서버 프로그램 제작에 대한 사례를 들어 주세요. ¶
서버 프로그램의 제작은 업무 내용과 각 에플리케이션의 특성에 따라 다양하게 제작이 가능합니다. 이중 증권 서버를 기준으로 몇가지만 예로 들어 보겠습니다. 증권 서버는 크게 주문/정보 서버로 구분이 되는데 이중 정보 서버가 가장 커다란 부분을 담당하고 있습니다. 정보 서버는 증권거래소에서 전송되는 가격정보를 수신하는 UDP서버를 기본으로 이 정보를 저장하거나 가공하는 서버들로 구성이 됩니다. 대충 그 구성을 보면 뉴스/챠트/호가/메모리풀/DB/주문 서버들로 구성이 되어 있습니다.
증권사 프로그램의 특성상 표시하고 요청해야 하는 데이터가 많은 관계로 클라이언트는 통신쪽 데이터만 관리하는 통신 핸들러를 별도로 두고 있습니다. 이것은 각 개별 윈도우에서 요청하는 데이터의 종류를 파악하여 서버측에 그에 해당하는 자료를 요청하고 수신하여 개별 윈도우에 전달해주는 역할을 합니다. 따라서 C/S 환경은 Subscribe/Publish 방식이 채택되고 있습니다.
- DB 서버는 개별 서버들에서 직접 엑세스 하지 않고 DB서버에 별도의 데몬을 구동하여 이 데몬을 통하여 SELECT/INSERT/UPDATE를 수행하게 합니다. 또한 빈번한 자료나 신속성을 요하는 정보의 경우에는 새로운 이벤트 발생시 DB 쿼리를 통하지 않고 늘 메모리에 저장하고 있다가 응답을 하게 됩니다.
- 뉴스 서버는 전송되는 데이터의 양이 많지만 정적인 자료가 주를 이루므로 실시간 브로드 케스팅을 하지 않고 클라이언트의 요구에 따라 반응하게 되어 있습니다. 예를 들면 사용자가 뉴스창을 띄었을때 그에 해당 하는 뉴스 제목만 20건 전송해주고 뉴스 내용을 클릭했을때 그 뉴스의 내용에 해당하는 자료를 전송해줍니다. 또한 스크롤바를 클릭하여 20건의 뉴스 제목이 넘어가면 새로운 20건을 전송요청하게 되는 형식입니다.
- 챠트 서버는 정/동적인 데이터를 함께 다룹니다. 즉, 과거 자료를 비교하기 위해서는 정적인 데이터를 필요로 하지만 장중의 가격 정보를 원할경우에는 실시간 데이터를 요청하여 실시간 챠트를 생성하게 합니다.
- 호가서버는 증권전산에서 10차 호가까지 공개가 되므로 데이터의 전송이 가장 빈번하면서도 많습니다. 무엇보다 신속성을 우선으로 하며 증권사에 따라 UDP 프로토콜을 이용하는 서버가 존재합니다. 대부분의 증권사 서버는 호가 서버로 구성이 되는것이 이러한 이유입니다.
- 메모리풀서버는 최초 클라이언트 구성시 순식간에 많은 양의 데이터를 표현해야 하는 관계로 각 서버들에 일일이 요청을 해서 표시하기에는 구동시간이 너무 오래 걸리므로 최초 구동에 필요한 데이터 부분만 따로 가지고 있는 서버입니다.
4.1 select()는 무엇인가요.? ¶
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
SELECT()가 *nix 시스템에 탑재된 이래로 다양한 분야에 쓰이고 있습니다. 가장 자주 빈번하게 사용하는 분야는
- Synchronous I/O Multiplexing
- File Descriptor Read/Writeable
- Timeout
또한 sleep/usleep을 대체하는 함수로도 유용합니다. 아래 참고
int usecsleep(int secs, int usecs) { struct timeval tv; tv.tv_sec = secs; tv.tv_usec = usecs; return select(0, NULL, NULL, NULL, &tv); }
4.2 select()를 이용한 다중서버 구성은 어떻게 하나요.? ¶
select()를 이용한 다중서버 기본적인 구성은 생각보다 쉽게 제작할수 있습니다. 아래 예제를 보시면
와 같이 만들어 낼수 있으며, 만일 thread나 fork를 이용해 개별 접속(가장 일반적인 것이 개별 접속에 대한 분리이다)들에 대해 분리를 원할 경우
int main(void) { socket() setsockopt() address & port set bind() listen() for(;;) { tv value set select(fdmax+1, &read_fds, NULL, NULL, &tv) for(i = 0; i <= fdmax; i++) { if (FD_ISSET(i, &read_fds)) { /* 변화된 fd 감지 */ if (i == listener) { accept() /* 신규 접속 처리 */ } else { recv() /* 데이터 수신 처리 */ } } } } return 0; }
- accept()이후 신규 쓰레드/프로세서 생성
- select를 사용하지 않은 무한 루프속(전통적인 다중서버 구성)에 신규 접속에 대해 쓰레드/프로세서를 생성한이후 개별 접속의 송/수신에 따라 select()를 이용하는 방법
- 1,2번 두가지를 혼합하는 방법
4.3 select()가 블락(Block) 당합니다. ¶
실제적으로 select()가 블락 당하는것 이라기 보다는 I/O(read/write)부분에서 그러한 동작이 일어납니다. 원래 select()는 Synchronous I/O Multiplexing으로 디자인 되어 있기 때문에 select()만 이용해 다중 서버를 구성할때에는 한쪽 fd가 블락되어 있을때 다른쪽에도 영향이 가게 됩니다. 따라서 이것을 해결하는 방법은
- select() 자체만 이용하면서 I/O NON-Blocking 방법을 이용한다.
- select()+thread or select()+fork등을 조합해서 사용한다.
4.4 select()에서 설정한 struct timeval *timeout 값이 변경되는것 같습니다. ¶
이 문제는 플렛폼마다 다르지만 SELECT() 함수의 타임아웃 값은 최초에 설정하지 말고 SELECT() 함수가 속해있는 무한 loop(보통 무한 loop를 사용하죠)안에 설정하는것이 좋습니다. 그렇지 않을 경우에는 시간이 지남에 따라 struct timeval의 값이 점점 줄어 드는것을 확인하실수 있습니다.
4.5 select()에서 상대편의 접속 해제는 어떻게 판단할수가 있나요.? ¶
recv()에서 설명하였듯이 recv()의 반환값을 가지고 판단할수 있습니다. 즉, 반환값이 0일 경우에는 일반적인 접속해제라고 판단이 되고 반환값이 0보다 작을 경우(-1)에는 에러라고 판단이 되는데 이 경우에는 데이터의 전송 도중에 접속이 해제 되거나 접속 자체를 잃어 버리는 경우라고 판단이 되면 됩니다.
4.6 connect() 호출시 서버가 응답하지 않을 경우 일시적인 지연이 발생합니다. ¶
상대측과 접속을 맺을수 없을경우(접속 거부, 서버 다운등등)에 상대측에서 응답을 하지 않는 접속불가의 경우에는 일정한 시간동안 BLOCK 현상이 발생합니다. 이 문제를 해결하기 위해서는 connect() 호출시에 잠시동안 시스템을 NON-BLOCK 모드로 설정한후에 접속을 시도하는것이 좋습니다.
주의 할점은 NON-BLOCK 모드를 이용해 접속을 시도할때에는 connect()가 즉시 에러를 반환하지만 반환값이 아닌 에러값을 가지고 판단해야 합니다. 에러값이 ECONNREFUSED와 같은 계열이 아닌 EINPROGRESS 일 경우에는 정상적인 접속에러가 아닌 접속을 맺기 위해 시도중임을 알수가 있습니다. 그후 SEELCT()를 이용하여 정상적으로 쓰기 가능한 파일 디스크립터를 얻을수 있는지 확인해서 접속을 맺거나 접속 실패에 대한 반환값을 얻어내야만 합니다. 이때 일정 시간동안 접속을 시도할지에 대한 판단은 SEELCT()의 타임아웃 값을 조정하시면 됩니다.
참고 : 특정 플렛폼, 혹은 에플리케이션에 따라 SEELCT()의 타임아웃을 조정할 필요가 있습니다. 즉, 특정 플렛폼에 따라서는 최소한 3초 이상의 타임아웃을 주어야만 접속이 맺어지고 그렇지 않을 경우에는 접속 거부 로 판단되는 경우가 생깁니다. 여러 플렛폼에 탑재한 경험으로는 접속 타임아웃은 5초가 가장 적당한 시간이 아닐까 생각됩니다.(플랫폼/에플리케이션에 따라 이러한 경우가 생기는 이유에 대해 경험이 있으시거나 아시는분은 포스팅 해주시기 바랍니다)
혹은 보다 손쉬운 방법으로는 alarm()을 이용하는것도 있습니다.
socket() set_nonblock rc = connect() if ( (status < 0) && ( (errno == EINPROGRESS) || (errno == EAGAIN) ) ) { while() { select() connect() } } if (status >= 0 ) { /* 정상 접속 */ } else { /* 접속 에러 */ }
signal(SIGALRM, timeout); alarm(1); connect() alarm(0);
4.7 위의 방법을 데이터 송/수신시에도 적용이 가능한가요.? ¶
네, 가능합니다. 접속 시간에 대한 타임아웃을 정하듯이 데이터 송/수신시에도 위와 같이 select()를 적절히 조합해서 타임아웃을 지정할수도 있습니다. 다만 주의 하실점은 select()의 비트 마스크 체크가 read/write 인지 체크하는 부분입니다.
4.8 send()를 호출하여 데이터 전송시 PIPE 시그널이 발생합니다. ¶
PIPE 시그널은 이미 닫혀져있는 디스크립터를 이용하여 데이터를 송신하려고 할때 발생합니다. 즉, 디스크립터 번호는 존재하지만 상대측 혹은 이쪽에 의해 이미 차단이된 쓸모가 없어진 디스크립터입니다. 이것을 방지하기 위해서는 데이터 송신시 select()를 이용하여 쓰기 가능한 파일 디스크립터인지 확인하여(select()의 두번째 인자) 전송을 하며 PIPE 시그널 자체가 별다른 처리를 요하는 시그널이 아니므로 시스템 시작히 PIPE 시그널 자체를 무시하게 만들어 놓는것도 좋은 방법입니다.
참고 : PIPE 시그널을 무시하도록 해놓았아도 디버깅 모드에서는 실제로 파이프 시그널에 의해 세그먼트 폴트 에러가 납니다. 그리고 PIPE 시그널에 대한 처리가 없으면 시그널 발생시 에플리케이션이 크레쉬하는 원인이 되기도 합니다.(실제 크레쉬합니다)
4.9 select()를 이용했는데 CPU 사용량이 걱정됩니다. ¶
select()는 최대 fd를 처음부터 끝까지 회전하면서 변경되는 fd를 감지하기 때문에 일종의 poll 방식을 채택하는것이나 전통적인 프로세스 방식의 다중서버에 비해 CPU 사용량은 지극히 적습니다.
4.10 select()를 이용하는데 가끔 CPU가 100%에 이릅니다. ¶
이것은 여러가지 원인(시그널/조건분기/select 에러등등)일수가 있는데 select()를 이용한 프로그래밍에서 에러처리 미흡으로 발생하는 문제입니다. 서버 시스템을 구동하기 전에 반드시 다양한 환경에서 테스트를 마친후에 설치하는것이 바람직합니다. 또한 대부분의 select()는 일정한 무한루프 안에서 동작하게 되는데 무한루프가 환원되는 싯점에서 1초 이내의 미세한 sleep을 주는것만으로 이러한 문제를 미연에 방지할수는 있으나 근본적인 해결은 아닙니다.
4.11 select()가 수용가능한 최대 fd값은 얼마입니까.? ¶
select()의 fd_set는 비트 마스크 이기 때문에 미리 정의된 크기를 갖고 있으며 그 크기는 플렛폼에 따라서 다르게 설정이 되어 있습니다. 리눅스 시스템의 경우에는 커널을 재 컴파일함으로서 해결할수 있으며 Solaris의 경우에는..
이부분은 예제를 만들어 다시 만들어 올리겠습니다.
4.12 select()를 이용해 시리얼 프로그래밍이 가능한가요.? ¶
네 가능합니다. 이 FAQ에서는 TCP SOCKET만을 다루고 있으므로 자세한것은 시리얼 프로그래밍 관련 문서들을 참조하시기 바랍니다.
4.13 select()를 이용한 메시지큐가 가능한가요.? ¶
메시지큐는 파일 디스크립터를 기반으로 작동되는 시스템이 아니라서 기본적으로는 불가능합니다만, 메시지큐 ID를 파일디스크립터에 대칭시키는 방법으로 가능하게 할수가 있습니다. 아래 링크에 이와 관련된 흥미로운 기사가 있습니다. 참조하시기 바랍니다.
5.2 POLL 4.14 서버 프로그램에 좀비 프로세서가 생깁니다. ¶
프로세서 방식(FORK)의 서버 모델에서 서버 프로세서(자식)의 종료시 exit이나 여타 방법으로 프로세서를 종료하면 이와 같은 좀비 프로세서가 무한정 계속 생겨납니다. 이를 해결하는 방법은 자식 프로세서의 종료시 'SIGCHLD' 신호를 등록하여 waitpid()로 처리해주는 방법입니다.
struct sigaction sigchild; sigchild.sa_handler = sighandler; sigchild.sa_flags = 0; sigemptyset(&sigchild.sa_sigset ); sigaction(SIGCHLD , &sigchild , 0); void sighandler(int signo ) { while ( waitpid( -1 , 0 , WNOHANG ) > 0 ) ; }
5.1 HTTP 프로그램은 어떻게 시작해야 하나요.? ¶
HTTP뿐만 아니라 FTP 프로그램도 RFC를 먼저 읽어 보시는것이 가장 빠른길입니다. 특히아 HTTP/1.0과 HTTP/1.1이 많은 차이를 보이고 있으므로 RFC문서를 필수적으로 보셔야만 합니다. 그후 HTTP는 일반적인 TCP/IP 통신과 거의 유사하므로 특별히 방대한 양의 브라우저의 소스를 확인하시기 보다는 직접 코딩을 하시는것도 좋은 방법입니다.
RFC 1945 - Hypertext Transfer Protocol -- HTTP/1.0
RFC 2068 - Hypertext Transfer Protocol -- HTTP/1.1
가장 일반적인 HTTP 프로토콜을 이용한 통신은 기본적인 TCP/IP통신에 HTTP 프로토콜을 이용하는것이 주를 이루므로
RFC 2068 - Hypertext Transfer Protocol -- HTTP/1.1
가장 일반적인 HTTP 프로토콜을 이용한 통신은 기본적인 TCP/IP통신에 HTTP 프로토콜을 이용하는것이 주를 이루므로
[POST방식] 1. 접속 2. HTTP 헤더 전송[HTTP버전및 전송되는 데이터의 사이즈등등] 3. HTTP 바디(내용) 전송 [주로 질의 내용에 해당] 4. HTTP 헤더 수신[HTTP버전및 전송되는 데이터의 사이즈등등] 5. HTTP 바디(내용) 수신 [질의 결과에 해당] [GET방식] 1. 접속 2. HTTP 헤더 전송[HTTP버전및 전송되는 데이터의 사이즈등등] 3. 4. HTTP 헤더 수신[HTTP버전및 전송되는 데이터의 사이즈등등] 5. HTTP 바디(내용) 수신 [질의 결과에 해당]와 같은 프로시져를 따릅니다. 생각보다 어렵지는 않으나 규약을 따르는것이 무엇보다 중요합니다.
5.1.1 HTTP를 통하여 파일 송수신이 가능한가요.? ¶
네, 가능합니다. 일반 파일 송수신과 동일하게 하는 방법과 BASE64 코드로 인코딩된 자료를 송/수신하는 두가지 방법이 쓰일수가 있습니다. 다만 BASE64 코드로 송/수신시에는 원본의 크기보다 조금더 약 1.6배 늘어나는 단점이 있습니다. 아래는 BASE64 de/encode 예제입니다. 또한 파일의 크기를 위해서 HTTP 헤더에 표시되는 Content-Length를 중요하게 다뤄야만 할것입니다.
/*------ Base64 Encoding Table ------*/ static const char MimeBase64[] = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/' }; /*------ Base64 Decoding Table ------*/ static int DecodeMimeBase64[256] = { -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 00-0F */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 10-1F */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,62,-1,-1,-1,63, /* 20-2F */ 52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1, /* 30-3F */ -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10,11,12,13,14, /* 40-4F */ 15,16,17,18,19,20,21,22,23,24,25,-1,-1,-1,-1,-1, /* 50-5F */ -1,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40, /* 60-6F */ 41,42,43,44,45,46,47,48,49,50,51,-1,-1,-1,-1,-1, /* 70-7F */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 80-8F */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* 90-9F */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* A0-AF */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* B0-BF */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* C0-CF */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* D0-DF */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1, /* E0-EF */ -1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1 /* F0-FF */ }; int base64_decode(char *text, unsigned char *dst, int numBytes ) { const char* cp; int space_idx = 0, phase; int d, prev_d = 0; unsigned char c; space_idx = 0; phase = 0; for ( cp = text; *cp != '\0'; ++cp ) { d = DecodeMimeBase64[(int) *cp]; if ( d != -1 ) { switch ( phase ) { case 0: ++phase; break; case 1: c = ( ( prev_d << 2 ) | ( ( d & 0x30 ) >> 4 ) ); if ( space_idx < numBytes ) dst[space_idx++] = c; ++phase; break; case 2: c = ( ( ( prev_d & 0xf ) << 4 ) | ( ( d & 0x3c ) >> 2 ) ); if ( space_idx < numBytes ) dst[space_idx++] = c; ++phase; break; case 3: c = ( ( ( prev_d & 0x03 ) << 6 ) | d ); if ( space_idx < numBytes ) dst[space_idx++] = c; phase = 0; break; } prev_d = d; } } return space_idx; } int base64_encode(char *text, int numBytes, char **encodedText) { unsigned char input[3] = {0,0,0}; unsigned char output[4] = {0,0,0,0}; int index, i, j, size; char *p, *plen; plen = text + numBytes - 1; size = (4 * (numBytes / 3)) + (numBytes % 3? 4 : 0) + 1; (*encodedText) = malloc(size); j = 0; for (i = 0, p = text;p <= plen; i++, p++) { index = i % 3; input[index] = *p; if (index == 2 || p == plen) { output[0] = ((input[0] & 0xFC) >> 2); output[1] = ((input[0] & 0x3) << 4) | ((input[1] & 0xF0) >> 4); output[2] = ((input[1] & 0xF) << 2) | ((input[2] & 0xC0) >> 6); output[3] = (input[2] & 0x3F); (*encodedText)[j++] = MimeBase64[output[0]]; (*encodedText)[j++] = MimeBase64[output[1]]; (*encodedText)[j++] = index == 0? '=' : MimeBase64[output[2]]; (*encodedText)[j++] = index < 2? '=' : MimeBase64[output[3]]; input[0] = input[1] = input[2] = 0; } } (*encodedText)[j] = '\0'; return 0; }
5.2 HTTP 터널링(TUNNELING)은 무엇인가요.? ¶
이것은 현대에 들어와 방화벽이 사내 네트웍의 주를 이루면서 생겨난 문제입니다. 즉, 많은 회사들이 사내 네트웍에 80(HTTP)포트를 제외한 대부분의 포트를 보안상의 이유(상당부분 방화벽 관리자의 무관심)로 닫아 놓으면서 정작 필요한 포트를 활성화해 사용하기 힘든 어려움을 피하고자 생겨난 일종의 트릭입니다.
특히나 상당수의 회사가 메신저/증권단말기/외부 이메일/게임/기타 포르노사이트등의 피해를 보게 되면서 회사가 규정하는 통신 포트 이외에는 닫아 놓고 필요에 의해서 열게되지만 실제 80포트를 제외한 다른 포트의 개방은 현실적으로 힘든것이 사실입니다. 따라서 주소및 포트 포워딩 기술을 이용한것으로 크게 터널링 클라리언트와 서버로 구성이 됩니다.
클라이언트(일반 에플리케이션)는 TUNNELING CLIENT로 정상적인 접속을 하지만(외부의 에플리케이션 서버가 아님) TUNNELING CLIENT는 방화벽을 통과 하기 위해 포트를 80포트를 사용하도록 교체 하거나 내부 패킷을 검사하는 방화벽이라면 HTTP 헤더를 붙여주거나 방화벽 인증을 요하는 방화벽의 경우 방화벽 인증을 하도록 도와주면서 외부 서버의 방화벽(실제 존재 유무는 무시)까지 도달하게 합니다. 이후 터널링 서버는 내부 에플리케이션에 맞게 포트나 주소등을 포워딩 해주는것입니다.
[ 클라이언트 사내 네트웍 ] CLIENT TUNNELING CLIENT FIREWALL ---> ---> ------------------------------------------------------ [ 서버 사내 네트웍 ] FIREWALL TUNNELING SERVER APPLICATION SERVER ---> --->
하지만 HTTP 터널링에는 사용자 인증이나 접속유지등 보완해야할 문제점이 많이 도사리고 있으므로 이것 자체가 만능은 아닙니다. 또한 주소및 포트를 포워딩 해주는 과정에서 생기는 패킷 자체의 속도 저하를 염두에 두어야만 합니다.
6.1 이기종간의 데이터 송/수신시 이상한 데이타가 확인됩니다. ¶
이기종간의 통신시 데이터의 전송이 char 버퍼가 아닌 int/long/float형등으로 구조체를 이용해 통신하고자 할경우에 플렛폼에 따라서 바이트 오더가 달라서 생기는 문제입니다.
대부분의 x86계열의 CPU를 사용하는 리눅스 시스템은 little endian을 채택하고 있고 AIX/SOLARIS등의 유닉스는 big endian을 채택하고 있습니다. 이것은 편의상 OS 플렛폼을 말씀 드렸으니 실제로는 CPU를 기준으로 판단하셔야만 합니다.
big endian시스템은 메모리에 데이터가 씌여질때 HI --> LOW 순으로 씌여지지만 little endian 시스템은 LOW -- >HI 순으로 데이터가 씌여지면서 발생합니다.
예를 들면 4바이트 정수형 데이터(0x01020304)가 존재할때
Big Endian: 01 02 03 04 Little Endian: 04 03 02 01
와 같이 메모리에 저장이 됩니다.
따라서 C/S 시스템의 경우에는 대부분의 클라이언트가 x86계열을 채택하고 있으므로 서버측에서 엔디안을 변경해주는것이 좋은 방법입니다. 아래는 간단한 바이트 오더 변환 함수입니다.
unsigned long int ChangeEndian(unsigned long int uiInput) { return ( uiInput >> 24 ) | ( ( uiInput >> 8 ) & 0x0000FF00 ) \ | ( ( uiInput << 8 ) & 0x00FF0000 ) | ( uiInput << 24 ); }
6.2 구조체 선언시 크기가 다르게 나옵니다.(바이트 패딩) ¶
이래와 같은 구조체 선언시 컴파일러가 코드를 최적화 하기 위해여 각각의 구조체 멤버들을 4바이트 바운더리로 맞추게 바꾸어 버립니다. 즉, 1바이트짜리 char phase[1]는 뒤에 3개의 패딩 바이트를 덧붙이고 char end[1] 역시 3바이트를 패딩하게 됩니다.
이와 같은 문제는 대부분 sizeof(구조체)와 같은 형식을 이용할때 예상하지 않은 사이즈가 돌출되어 문제가 발생할때 이용하면 유용합니다. 아래 구조체 옵션들은 컴파일시 바이트 패딩을 하지 않고 그대로 이용하도록 만들어 주는것입니다.
#ifdef X86 typedef struct _HEADER_ { char phase[1]; int length; char end[1]; }__attribute__((packed)) _HEADER; #elif AIX #pragma options align=packed typedef struct _HEADER_ { char phase[1]; int length; char end[1]; }_HEADER; #pragma options align=reset #elif HPUX #pragma HP_ALIGN NOPADDING typedef struct _HEADER_ { char phase[1]; int length; char end[1]; }_HEADER; #pragma HP_ALIGN HPUX_NATURAL #endif
반응형