소켓 프로그램에서 송수신으로 가장 많이 사용되는 것은 recv,send 와 recvfrom, sendto 일 것이다. 전자를 TCP 용 후자를 UDP 용으로 흔히 알고 사용하는데, 그 이유는 연결지향형이 아닌 경우 매 패킷마다 주소지가 다른 데이터가 올라 올 수 있기 때문에 주소를 받거나 보낼 수 있는 시스템 콜을 사용하게 된다.

UDP의 경우 기대되는 데이터의 흐름은 다음과 같다.

서버

1. UDP 서버가 대기
2. 패킷 및 클라이언트 주소 수신
3. 클라이언트 주소로 응답 송신

클라이언트
1. 서버주소 및 요구사항 송신
2. 패킷 및 서버 주소 수신

여기서 주의할 점이 클라이언트가 데이터를 송신하고서 recvfrom 으로 대기하고 있다면, 서버가 아닌 다른 누군가가 패킷을 쏜다면 (이것은 네트웍 중간에 있는 녀석이 충분히 만들어낼 수 있다.) 그것을 자료인 양 수신하게 된다. 이 경우 클라이언트쪽에서 받은 응답만 보는 것이 아니라 udp 주소를 보고 과연 내가 보낸 송신자로부터 왔는지 확인한 후 잘못된 곳에서 왔다면 다시 대기하여야한다. UDP 포트에 아무데이터나 쏘고 없을때 돌아오는 ICMP 를 확인하는 수준의 포트스캐너가 순간 다녀간다면, 오동작할 것이 뻔하다. 환장한다.

이런 경우를 대비해서 UDP 클라이언트는 connect, send, recv를 사용하여 프로그램하는 것이 올바른 방법이다. 책을 보면, udp 소켓에서의 connect는 실제 접속을 시도하는 것이 아니라 이후에 올 send에 대해서 전송할 주소를 설정해 두는 것이고, recv에 대해서 그 주소외에서 오는 응답은 필터링을 하는 역할을 한다. 그렇다면, 당연히 UDP 클라이언트 프로그램은 connect를 사용해야하는 것이 정답이다. 예외가 없다.

정리하자면, UDP 프로그램의 서버는 sendto, recvfrom 조합을 클라이언트는 connect, recv, send를 쓰는 것이 일반적이라는 것이다.

신고
크리에이티브 커먼즈 라이선스
Creative Commons License
  1. kukuta 2007.03.16 20:45 신고

    "udp 소켓에서의 connect는 실제 접속을 시도하는 것이 아니라 이후에 올 send에 대해서 전송할 주소를 설정해 두는 것이고, recv에 대해서 그 주소외에서 오는 응답은 필터링을 하는 역할을 한다."

    라는 것에서 만일 서버의 아이피가 여러개일 경우에는 connect로 설정되는 주소가 어떻게 되는건가요?
    저런식으로 하면 서버의 아이피모두를 골라 줄수 있는것인가요??

    kukuta@gmail.com

    • 최호진 2007.03.16 23:17 신고

      connect 대상은 packet을 전송할 주소만 저장하는 것이고, 실제 multi-homed nic의 경우 여러 IP 중 하나가 선택되는 것은 어떻게 bind 했는지에 따라 선택되거나 전송되는 순간 routing table에 따라 결정됩니다.

      따라서 connect는 서버에 여러 IP중 하나와 연결시키는 것과는 아무 상관이 없습니다. 제 글 중 recv 에 대해서 필터링을 한다는 얘기는 recv 상태에서는 모든 UDP 패킷을 받을 수 있으나 송신지가 connect 해둔 주소에서 오는 것외에는 필터링을 한다는 말이었습니다.

  2. 백탄왕 2007.05.08 18:40 신고

    질문하나 해도 될런지요?
    connect를 호출하고 난 후에는 connect 호출된 클라이언트 이외의 패킷들은 recv에서 필터링 된다는 글인것 같은데, 맞습니까? 그렇다면 지금 설명하신게 리눅스 기반에서 말씀하신 듯 한데, 윈도우에서도 똑같이 적용되는지 혹시 아시는지요? (물론 윈속이 아닌 경우를 말씀드리는 것입니다.)
    :) 답변 기다리겠습니다. 감사합니다..

    • 최호진 2007.05.08 20:32 신고

      윈도우에서는 테스트 해보진 않았지만 (환경이 안돼서리..) 버클리 소켓의 기본적인 행동이므로 동작하리라 예상됩니다.

TCP/IP 서버를 만들때는 다음과 같은 방법으로 만들게 된다.

1. 소켓 생성
2. bind
3. listen
4. accept 로 클라이언트 소켓 생성

소켓 프로그램을 처음하는 사람들이 겪게 되는 의문 중의 하나는 무엇에다가 묶고(bind), 듣기전까지는 어떤 일이 일어나길래 들어야(listen)하는가인데, 여기에는 발상의 전환이 필요하다. 일반적으로 파일을 열고 파일에서 읽는 과정을 생각해보면, 이미 경로라는 구별되어 있는 개체가 존재하고 그것을 다루기 위한 기술자(descriptor)를 만들어 기술자를 모든 파일 관련 입출력 함수에 인자로 넘기게 되는데, 소켓 프로그램은 그 반대라는 것이 중요하다. 기술자(socket)를 먼저 만들고, 그 기술자를 구별될 수 있는 경로 혹은 주소에 가져다가 붙이는 일을 한다.

그 이유는 기술자 혹은 소켓의 개념과 주소의 개념이 각각 내부적인 것과 외부적인 것을 대표하는 개념이 이미  OS 초창기부터 존재해 왔고 , 기존의 풍부한 기술자 기반의 시스템 호출과 유사하게 가져가려는 심미적인 설계 때문에 생긴 것이라 할 수 있다.

listen 이라는 것은 쉽게 이야기하면, 이제부터 외부의 접속을 듣겠다, 받아들일 준비 상태로 만들어라는 뜻이겠지만, 보다 기술적인 얘기를 하자면, listen 상태에 들어 있는 소켓은 응용 프로그램이 accept를 하는 것과 상관없이 커널에서 접속에 관계된 3 way handshaking을 구동시키라는 뜻이된다. 중요한 것은 accept와 상관없이 일어나는 것이며, accept는 그렇게 접속과정을 끝낸 소켓을 큐에서 꺼내어 특정 주소에 bind 되어 있는 소켓을 만들어 내는 일을 하는 것이다.

그러면, 일반적으로 서버 소켓을 만들때 사용하는 재사용 가능한 소켓은 언제 설정해 주어야 하는가?

1. 소켓 생성
2. setsockopt( s, SOL_SOCKET, SO_REUSEADDR ... );
3. bind
4. listen
5. accept 로 클라이언트 소켓 생성

bind 작업에 들어갈 때, 묶어 주는 주소에 대해 서버 소켓이 listen 상태에서 벗어나는 순간 바로 (주소에 해당하는 ESTABLISHED 혹은 TIME_WAIT 상태로 남아 있는)가 있어도) 다시 접속가능한 bind 를 지정하기 위해서 소켓 옵션의 설정은 bind 이전에 해야하는 것이 옳다. 재사용가능 한 것이 소켓 옵션이 아니라 주소에 대한 것이므로 소켓에 옵션을 실어 bind 시스템 호출을 해서 넘기는 것이다. 즉, 다음은 올바른 사용법이 아니라는 것이다.

1. 소켓 생성
2. bind
3. setsockopt( s, SOL_SOCKET, SO_REUSEADDR ... );
4. listen
5. accept 로 클라이언트 소켓 생성

물론 구현에 따라 위 순서로 해도 동작가능할 수 있다. 하지만, 의미상 그렇지 않다는 것을 알고 사용하자.

신고
크리에이티브 커먼즈 라이선스
Creative Commons License

http://bbs.kldp.org/viewtopic.php?t=768
여기에 썼던글이다. (drupal로 이관하면서 이관된 문서를 찾으려 했으나 역부족.)
본 글은 IP 주소를 얻는 방법에 대해 쓴 것이 아니라, 그 이면에 있는 드라이버와의 통신에 대해 다룬다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stropts.h>

#if defined(sun)
#include <sys/sockio.h>
#endif

#include <net/if.h>
#if defined(linux)
#include <linux/sockios.h>
#endif

#define BUFFERSIZE 1024
const char * localip = "0.0.0.0";

const char * myip()
{
        const int MAX_NIC = 10;
        struct ifconf ifc;
        struct ifreq ifr[MAX_NIC];

        int s;
        int nNumIFs;
        int i;
        int count;
        int max=2;
        static char ip[BUFFERSIZE];
        int cmd = SIOCGIFCONF;

        max++;

        ifc.ifc_len = sizeof ifr;
        ifc.ifc_ifcu.ifcu_req = ifr;

        if( (s=socket(AF_INET,SOCK_STREAM,0)) < 0)
        {
                perror("socket");
                exit(1);
        }

#if defined(_AIX)
        cmd = CSIOCGIFCONF;
#endif

        if( ioctl(s, cmd, &ifc) < 0)
        {
                perror("ioctl");
                exit(1);
        }
        close(s);

        nNumIFs = ifc.ifc_len / sizeof ( struct ifreq );
        count = 0;
        strcpy( ip, localip );
        for( i=0; i<nNumIFs; i++ )
        {
                struct in_addr addr;
                if( ifc.ifc_ifcu.ifcu_req[i].ifr_ifru.ifru_addr.sa_family != AF_INET)
                {
                        continue;
                }

                addr = ((struct sockaddr_in *) & ifc.ifc_ifcu.ifcu_req[i].ifr_ifru.ifru_addr)->sin_addr;
                if( addr.s_addr == htonl( 0x7f000001 ) )
                {
                        continue;
                }
                strcpy( ip, inet_ntoa( addr ) );
                printf( "IP: %s\n", ip );
        }
        return ip;
}

int main()
{
        printf("One of my IP is %s\n", myip() );
        return 0;
}

이 함수를 인용하는 이유는, 원리가 특이하다 생각할 수 있기 때문이다. 자세히 들여다보면, myip 함수에서 socket 함수를 사용하여 소켓하나를 만들고 정작 우리가 흔히 이용하는 대로 bind하거나 connect하지 않는다.
위 함수가 Windows에서 돌아가는지조차 테스트 해보지 않아서 더더욱 이식성이라도 있는지 모르겠다.

Windows 같으면 Registry를 뒤져가며, 네트웍 인터페이스 카드(NIC)에 할당되어 있는 DHCP IP건 Auto IP건 말그대로 Static IP건 찾는대로 보여줄텐데, 이 녀석은 그렇지 않다.

IP 주소는 무엇인가? 커널의 어떤 녀석이 그 주소 정보를 가지고 있는것인가? SIOCGIFCONF 라는 옵션은 인터페이스 구성에 대한 것을 되돌려 받는 것 같은데, 그것이 커널내의 어떤 녀석에게 물어 본다는 것인가? 상상을 하자면 끝이 없다.

모든 I/O 관련 시스템콜이 그렇지만, 간과해서는 안 될 것이, I/O 관련 시스템콜은 사실 파일 기술자 혹은 소켓이 어디 소속인지를 보고 소속 드라이버의 실제 시스템콜을 호출하는 것에 불과하다는 것이다. 드라이버를 제작하다보면, 그 드라이버와 통신하기 위해 몇가지 방법이 존재하는데 그 중 한 방법이 ioctl이다. (디바이스 드라이버나 가상 파일 시스템을 만들수도 있고, 시스템 콜을 추가할 수도 있다.)

위의 예제는 네트웍 인터페이스 카드의 주소를 설정하기 위해, 해당 드라이버와 통신하기 위한 소켓을 만들었을 뿐, 그 이상도 이하도 아니다. 그 소켓은 실제 통신용이 아니라는 얘기이다. 만일, 실제 통신용이 존재한다면, 그것을 그대로 이용해도 무방하다. 즉 따로 IP를 구하기 위해 소켓을 만들지 말라는 얘기이다.

아니 정말, 소켓을 만든이유가 단지 그 드라이버와의 통신을 위한 것이란 말이오? 라고 반문할 수 있겠지만, 요즈음에 사용되는 /proc 파일 시스템같은 것들이 소개되기 훨씬이전부터 있어 왔던 방식이다라고 말할 수 밖에 없다.

좀더 생각해 볼 것은, read가 recv와 같은 역할을 하는 이유가 read, recv 들은 사실 wrapper일 뿐 실제적인 녀석은 따로 있기 때문이다. 둘 다 소켓을 보고 해당 드라이버의 recv 용 함수를 부르게된다.
신고
크리에이티브 커먼즈 라이선스
Creative Commons License

소켓프로그래밍 깊이 보기 1 : 접속이 닫힌 후 읽을 수 있는 버퍼

Quiz 하나를 생각해보자.

클라이언트가 서버에 접속하여 뭔가를 전송하고 있다. 클라이언트는 1000 바이트를 전송하고나서 바로 소켓을 종료하였는데, 서버는 1 byte 씩 읽으면서 행의 끝을 판단하는 구조로 되어 있다. 서버가 10 바이트를 읽었는데, 실상 접속은 종료되었다. 서버쪽 프로그램은 11 바이트를 읽을 때, 접속 종료를 바로 알 수 있을까?

싱겁지만, 답은 서버쪽에서 1001번째를 읽기 시도할 때 비로소 안다는 것이다. 그 이유는 TCP는 데이터의 정확한(?) 전송을 보장하도록 되어 있기 때문인데, 끊어진 클라이언트에게 일단 1000 바이트에 대해 받았다는 신호를 보냈기 때문에, 서버 프로그램에 안정적으로 데이터를 올려 보낸후 접속이 종료되었음을 알려주는 것이다.

아래의 예는 위 문제를 구현한 것은 아니지만, 느린 수신에서 일어나는 현상을 설명하는 것이다.
보내는 쪽은 프로그램은 이미 종료되었지만, 받는 쪽은 계속 진행하고 있다.

$ ./server &
$ ./client
Send: elapsed 0 sec: sent 100 bytes
Send: elapsed 0 sec: sent 100 bytes
Send: elapsed 0 sec: sent 100 bytes
Send: elapsed 0 sec: sent 100 bytes
Send: elapsed 0 sec: sent 100 bytes
Send: elapsed 0 sec: sent 100 bytes
Recv: elapsed 0 sec: received 200 bytes
Send: elapsed 0 sec: sent 100 bytes
Send: elapsed 0 sec: sent 100 bytes
Send: elapsed 0 sec: sent 100 bytes
Send: elapsed 0 sec: sent 100 bytes
End of client
Recv: elapsed 1 sec: received 200 bytes
Recv: elapsed 2 sec: received 200 bytes
Recv: elapsed 3 sec: received 200 bytes
Recv: elapsed 4 sec: received 200 bytes
End of server

$ cat server.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>

void do_hojin2( int sock )
{
       char buf[200];
       int len;
       time_t t = time(0);
       while( ( len = recv( sock, buf, sizeof buf, 0 ) ) > 0 )
       {
               printf("Recv: elapsed %ld sec: received %d bytes\n", time(0) - t, len );
               sleep(1);
       }
}

int main()
{
       struct sockaddr_in addr;
       int sock, worksock;
       int len;
       int val = 1;

       sock = socket( PF_INET, SOCK_STREAM, 0 );

       memset( & addr, 0, sizeof addr );
       addr.sin_family = AF_INET;
       addr.sin_port = htons( 4000 );
       len = sizeof addr;

       setsockopt( sock, SOL_SOCKET, SO_REUSEADDR, & val, sizeof val );
       bind( sock, (struct sockaddr *) & addr, len );
       listen( sock, 5 );

       worksock = accept( sock, (struct sockaddr *) & addr, & len );
       do_hojin2( worksock );
       close( worksock );
       close( sock);
       printf("End of server\n");
       return 0;
}

$ cat client.c
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>

void do_hojin1( int worksock )
{
       int i;
       time_t t = time(0);
       for( i=0; i<10; i++ )
       {
               char buf[100];
               int sent;

               sleep(0);
               memset( buf, 'x', sizeof buf );
               sent = send( worksock, buf, sizeof buf, 0 );
               printf("Send: elapsed %ld sec: sent %d bytes\n", time(0) - t, sent );
               fflush( stdout );
       }
}

int main()
{
       struct sockaddr_in addr;
       int sock, worksock;
       int len;
       int val = 1;

       int buf[32768];

       sock = socket( PF_INET, SOCK_STREAM, 0 );

       memset( & addr, 0, sizeof addr );
       addr.sin_family = AF_INET;
       addr.sin_port = htons( 4000 );
       len = sizeof addr;

       connect( sock, (struct sockaddr *) & addr, len );
       do_hojin1( sock );
       close( sock );
       printf("End of client\n");
       return 0;
}

신고
크리에이티브 커먼즈 라이선스
Creative Commons License

+ Recent posts