본문 바로가기

서버운영 (TA, ADMIN)/네트워크

[네트워크] 소켓프로그래밍 기초

01. IP 주소와 포트 번호

TCP/IP 프로토콜을 이용해 통신하려면 IP 주소가 있어야 합니다. 또한 인터넷에서 동작하는 각종 서비스를 구분하기 위한 포트 번호를 지정해야 합니다. 이 절에서는 IP 주소와 포트번호의 기본 개념을 살펴보고, 관련 함수의 사용법을 배웁니다.



IP 주소와 호스트명


IP 주소는 인터넷을 이용할 때 사용하는 주소로, 점(.)으로 구분된 32비트 숫자로 표시합니다. (예: 192.168.100.51) IP 주소를 네트워크 주소(인터넷 주소) network address(internet address)라고도 합니다. IP 주소는 A~C 클래스로 구분됩니다.


시스템은 주소를 숫자로 구분하는 것이 효율적이지만, 사람은 주소를 이름으로 구분하는 것이 더 편합니다. 따라서 시스템에는 IP 주소 외에 호스트명을 지정합니다. 예를 들어, 호스트명이 www.hanb.co.kr인 시스템의 IP 주소는 218.237.65.4입니다. 인터넷에서 사용하는 호스트명은 '호스트명 + 도메인명' 형태로 구성됩니다. www.hanb.co.kr의 경우 www는 호스트명, hanb.co.kr은 도메인명이 됩니다. 도메인명은 도메인을 관리하는 기관에 등록하고 사용해야 합니다. 국내에서는 한국인터넷진흥원에서 kr 도메인을 관리하고 있습니다. 호스트명은 같은 도메인 안에서 중복되지 않게 시스템 관리자가 정해 사용하면 됩니다. 호스트명과 도메인명을 관리하는 시스템을 DNS(Domain Name System)이라고 합니다.



호스트명과 IP 주소 변환


호스트명과 IP 주소를 등록해놓은 파일이나 데이터베이스를 검색해 호스트명이나 IP 주소를 찾을 수 있습니다. 이와 관련된 파일은 /etc/hosts이며, 데이터베이스로는 제공하는 서비스에 따라 DNS일 수도 있고, NIS(Network Information Service)일 수도 있습니다. /etc/nsswitch.conf 파일에 어떤 데이터베이스를 어떤 순서로 활용하는지 지정하고 있습니다. 예를 들어, /etc/nsswitch.conf에 지정된 값이 다음과 같다고 하겠습니다.


hosts: files dns


이 설정의 의미는 호스트명과 IP 주소를 먼저 파일에서 찾고, 파일에서 찾지 못하면 DNS 서비스를 이용한다는 의미입니다. 여기서 파일이란 /etc/hosts 파일을 의미합니다. 유닉스에서는 호스트명과 IP 주소를 변환하는 함수를 여러 가지 형태로 제공합니다.


호스트명과 IP 주소 읽어오기 : gethostent(3), sethostent(3), endhostent(3)

#include <netdb.h>

struct hostent *gethostent(void);
// stayopen : IP 주소 데이터베이스를 열어둘지 여부를 나타내는 값
int sethostent(int stayopen);
int endhostent(void);

gethostent ,sethostent, endhostent 함수는 호스트명과 IP 주소를 차례로 읽어옵니다. gethostent 함수는 호스트명과 IP 주소를 읽어서 hostent 구조체에 저장하고 그 주소를 리턴합니다. sethostent 함수는 데이터베이스의 현재 읽기 위치를 시작 부분으로 재설정합니다. sethostent 함수는 gethostent 함수를 처음 사용하기 전에 호출해야 합니다. sethostent 함수의 인자인 stayopen 값이 0이 아니면 데이터베이스가 열린 채로 둡니다. endhostent 함수는 데이터베이스를 닫습니다. gethostent 함수는 데이터베이스의 끝을 만나면 널을 리턴합니다. sethostent 함수와 endhostent 함수는 수행을 성공하면 0을 리턴합니다.


hostent 구조체는 다음과 같으며 <netdb.h>에 정의되어 있습니다.

struct hostent {
    char *h_name;       // 호스트명을 저장
    char **h_aliases;   // 호스트를 가리키는 다른 이름들을 저장
    int  h_addrtype;    // 호스트 주소의 형식을 지정
    int  h_length;      // 주소의 길이를 저장
    char **h_addr_list; // 해당 호스트의 주소 목록 저장
};

gethostent 함수를 사용해 /etc/hosts 파일의 내용을 읽어 출력하는 간단한 프로그램을 작성해보겠습니다.

#include <netdb.h>
#include <stdio.h>

int main(void) {
    struct hostent *hent;
    // 호스트 파일의 처음으로 읽기 위치를 지정
    sethostent(0);
    // 호스트 파일에서 차례로 읽어 호스트명(h_name)을 출력
    while((hent = gethostent()) != NULL)
        printf("Name=%s\n", hent->h_name);
    // 호스트 파일을 닫는다.
    endhostent();

    return 0;
}

[root@dev-web ch11]# cat /etc/hosts
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6
[root@dev-web ch11]# ./ex11_1.out
Name=localhost
Name=localhost



호스트명으로 정보 검색: gethostbyname(3)

#include <netdb.h>
// name: 검색하려는 호스트명
struct hostent *gethostbyname(const char *name);

gethostbyname 함수는 호스트명을 인자로 받아 데이터베이스에서 해당 항목을 검색해 hostent 구조체에 저장하고 그 주소를 리턴합니다.



IP 주소로 정보 검색: gethostbyaddr(3)

#include <netdb.h>
// addr: 검색하려는 IP 주소 & len: addr 길이 & type: IP 주소 형식
struct hostent *gethostbyaddr(const char *addr, int len, int type);

gethostbyaddr 함수는 IP 주소를 인자로 받아 데이터베이스에서 해당 항목을 검색해 hostent 구조체에 저장하고 그 주소를 리턴합니다. 첫번째 인자인 addr에는 IP 주소가 저장됩니다. addr에 저장되는 주소는 변환을 수행한 것입니다. 두 번째 인자인 len은 addr의 길이입니다. 세 번째 인자인 type에는 주소의 형식으로 <sys/socket.h> 파일에 정의된 주소 형식 중 하나를 지정해야 합니다. <sys/socket.h> 파일에 정의된 주소 형식은 다음과 같으며 이중 AF_UNIX와 AF_INET을 주로 사용합니다.

AF_UNSPEC       0   /* 미지정 */
AF_UNIX         1   /* 호스트 내부 통신 */
AF_INET         2   /* 인터네트워크 통신: UDP, TCP 등 */
AF_IMPLINK      3   /* Arpanet의 IMP 주소 */
AF_PUP          4   /* PUP 프로토콜: BSP 등 */
AF_CHAOS        5   /* MIT의 CHAOS 프로토콜 */
AF_NS           6   /* XEROX의 NS 프로토콜 */
AF_NBS          7   /* NBS 프로토콜 */
AF_ECMA         8   /* 유럽 컴퓨터 제조업체 */
AF_DATAKIT      9   /* DATAKIT 프로토콜 */
AF_CCITT        10  /* CCITT 프로토콜(X.25 등) */
AF_SNA          11  /* IBM SNA 프로토콜 */
AF_DECnet       12  /* DECnet 프로토콜 */
AF_DLI          13  /* 데이터링크 직접 접속 */
AF_LAT          14  /* LAT 프로토콜 */
AF_HYLINK       15  /* NSC 하이퍼채널 */
AF_APPLETALK    16  /* 애플토크 프로토콜*/
AF_NIT          17  /* 네트워크 인터페이스 탭 */
AF_802          18  /* IEEE 802.2 또는 ISO 8802 */
AF_OSI          19  /* OSI 프로토콜 */
AF_X25          20  /* CCITT X.25 프로토콜 */
AF_OSINET       21  /* AFI = 47, IDI = 4 */
AF_GOSIP        22  /* 미국 정부 OSI */
AF_IPX          23  /* 노벨 인터넷 프로토콜 */
AF_ROUTE        24  /* 내부 라우팅 프로토콜 */
AF_LINK         25  /* 링크계층 접속 */
AF_INET6        26  /* IP 버전 6 */
AF_KEY          27  /* 보안 관련 DB 소켓 */
AF_NCA          28  /* NCA 소켓 */
AF_POLICY       29  /* 보안 정책 DB 소켓 */

gethostbyname 함수와 gethostbyaddr 함수의 사용 예는 다음 절에서 살펴보겠습니다.



포트 번호


IP 주소는 데이터가 전송될 목적지 호스트를 알려주는 역할을 합니다. 그런데 목적지 호스트에는 여러 가지 기능을 수행하는 서비스 프로세스들이 동시에 동작하고 있을 수 있습니다. 예를 들면, 웹 서비스, 메일 서비스, FTP 서비스, 텔넷 서비스 등을 수행하는 프로세스들이 동작하고 있는 것입니다. 따라서 전송되어 오는 데이터를 어느 서비스 프로세스에 전달할 것인지 구분할 수 있어야 합니다. 마치 회사 주소로 배달된 우편물을 전산실 김대리에게 전달하려면 회사 주소뿐만 아니라 수신자명이 정확하게 있어야 하는 것과 마찬가지입니다. 인터넷에서도 IP 주소 외에 서비스를 구분하는 다른 정보가 필요합니다. 이때 사용하는 것이 포트 번호 입니다. 포트 번호는 2바이트 정수로 되어 있으므로 0~65535까지 사용할 수 있습니다. 인터넷에서 자주 사용하는 서비스는 이미 포트 번호가 지정되어 있습니다. 이를 잘 알려진 포트(well-known port)라고 하며, 0~1023까지 사용합니다. 대표적인 포트 번호는 텔넷 프로토콜이 23, FTP가 21, HTTP가 80 등입니다. 일반 프로그램에서는 0~1023을 제외한 1024~65535를 사용하면 됩니다. 이미 정해진 포트 번호는 /etc/services 파일에 등록되어 있습니다. 물론 유닉스는 /etc/service 파일에서 정보를 검색하는 함수를 제공합니다.


포트 정보 읽어오기: getservent(3), setservent(3), endservent(3)

#include <netdb.h>

struct servent *getservent(void);
// stayopen: 포트 정보 데이터베이스를 열어둘지 여부를 나타내는 값
int setservent(int stayopen);
int endservent(void);

getservent, setservent, endservent 함수는 포트 정보를 차례로 읽어옵니다. 이 함수들은 gethostent, sethostent, endhostent 함수와 같은 형태로 동작합니다. getservent 함수는 포트 정보를 읽어서 servent 구조체에 저장하고 그 주소를 리턴합니다. setservent 함수는 데이터베이스의 현재 읽기 위치를 시작 부분으로 재설정합니다. setservent 함수는 getservent 함수를 처음 사용하기 전에 호출해야 합니다. setservent 함수의 인자인 stayopen 값이 0이 아니면 데이터베이스를 연 채로 둡니다. endservent 함수는 데이터베이스를 닫습니다. getservent 함수는 데이터베이스의 끝을 만나면 널을 리턴합니다. setservent와 endservent 함수는 성공하면 0을 리턴합니다.


servent 구조체는 다음과 같으며 <netdb.h>에 정의되어 있습니다.

struct servent {
    char *s_name;       // 포트명을 지정
    char **s_aliases;   // 해당 서비스를 가리키는 다른 이름들 저장
    int  s_port;        // 포트 번호를 저장
    char *s_proto;      // 서비스에 사용하는 프로토콜의 종류
};

getservent 함수를 사용해 /etc/services 파일의 내용을 읽어 출력하는 간단한 프로그램을 작성해보겠습니다.


getservent 관련 함수는 socket 라이브러리에 정의되어 있습니다. socket 라이브러리도 /usr/lib 디렉토리에 위치하고 있으며, 파일명은 libsocket.so입니다. 따라서 이번 예제는 컴파일할 때 다음과 같이 -lsocket으로 지정하면됩니다.

#include <netdb.h>
#include <stdio.h>

int main(void) {
    struct servent *port;
    int n;

    setservent(0); // 포트 정보 데이터베이스에서 현재 읽기 위치를 시작으로 이동시킴.

    // 처음 5개의 포트 정보를 차례로 읽어서 출력한다.
    for(n=0; n < 5; n++) {
        port = getservent();
        printf("Name=%s, Port=%d\n", port->s_name, port->s_port);
    }
    // 포트 정보 데이터베이스를 닫는다.
    endservent();

    return 0;
}

[root@dev-web ch11]# ./ex11_2.out
Name=tcpmux, Port=256
Name=tcpmux, Port=256
Name=rje, Port=1280
Name=rje, Port=1280
Name=echo, Port=1792


실행 결과와 /etc/services 파일의 내용을 비교한다면, 포트 번호가 다를 것입니다. 이는 servent 구조체에 저장되는 포트 번호의 바이트 순서가 다르기 때문입니다. 바이트 순서 관련 내용은 다음 절에서 다루므로 결과가 이상해도 넘어가겠습니다.


서비스명으로 정보 검색: getservbyneme(3)

#include <netdb.h>

// name : 검색할 포트명, proto: "tcp" 또는 "udp"
struct servent *getservbyname(const char *name, const char *proto);

getservbyname 함수는 포트명을 인자로 받아 데이터베이스에서 해당 항목을 검색해 servent 구조체에 저장하고 그 주소를 리턴합니다. 두 번째 인자인 proto에는 "tcp"나 "udp" 또는 NULL을 지정합니다. 같은 서비스 포트가 TCP 서비스를 위한 번호화 UDP 서비스를 위한 번호로 구분되기 때문입니다.



포트 번호로 정보 검색: getservbyport(3)

#include <netdb.h>
// port : 검색할 포트 번호, proto: "tcp" 또는 "udp"
struct servent *getservbyport(int port, cosnt char *proto);

getservbyport 함수는 포트 번호를 인자로 받아 데이터베이스에서 해당 항목을 검색해 servent 구조체에 저장하고 그 주소를 리턴합니다. 두번째 인자인 proto에는 "tcp"나 "udp" 또는 NULL을 지정합니다.



02. 소켓 프로그래밍 기초

소켓은 응용 계층과 전송 계층을 연결하는 기능을 제공하는 프로그래밍 인터페이스입니다. 소켓을 이용해 TCP/IP 환경에서 프로그래밍할 경우 TCP나 UDP 프로토콜의 세부적인 내용에 대한 지식이 없어도 통신 프로그램을 작성할 수 있습니다. 이 절에서는 소켓 프로그래밍을 할 때 알아야할 구조체와 함수 등 기본적인 사항을 살펴보겠습니다.



소켓의 종류


소켓은 크게 두 가지로 구분하는데, 같은 호스트에서 프로세스 사이에 통신할 때 사용하는 유닉스 도메인 소켓(unix domain socket)인터넷을 통해 다른 호스트와 통신할 때 사용하는 인터넷 소켓(internet socket)이 있습니다. 이 소켓들을 표시하는 이름으로는 <sys/socket.h>에 정의되어 있는 주소 패밀리명을 사용합니다.

  • AF_UNIX: 유닉스 도메인 소켓
  • AF_INET:  인터넷 소켓


소켓의 통신 방식


TCP/IP 프로토콜에서 전송 계층에서 사용하는 프로토콜로는 TCP와 UDP가 있습니다. 소켓을 이요할 때도 하부 프로토콜로 TCP를 사용할 것인지, UDP를 사용할 것인지 지정해야 합니다. 이는 미리 정의되어 있는 상수를 사용해 지정합니다.


  • SOCK_STREAM: TCP 프로토콜 사용
  • SOCK_DGRAM: UDP 프로토콜 사용


따라서 소켓을 이용할 때는 소켓의 종류와 통신 방식에 따라 4가지 통신 유형이 나타나게 됩니다.


  • AF_UNIX - SOCK_STREAM
  • AF_UNIX - SOCK_DGRAM
  • AF_INET - SOCK_STREAM
  • AF_INET - SOCK_DGRAM


소켓 주소 구조체


소켓을 이용한 프로그래밍에서는 소켓의 종류와 IP 주소, 포트 번호 등을 지정하기 위한 구조체를 사용합니다. 소켓 구조체는 유닉스 도메인 소켓과 인터넷 소켓에서 각기 다른 형태를 사용합니다.


유닉스 도메인 소켓의 주소 구조체

유닉스 도메인 소켓에 사용하는 주소 구조체는 다음과 같습니다. sockaddr_un 구조체는 <sys/un.h>에 정의되어 있습니다. sockaddr_un 구조체에는 주소 패밀리명과 경로명이 들어있습니다. sun_family에는 AF_INET을 지정합니다.

struct sockaddr_un {
    sa_family_t     sun_family;
    car             sun_path[108];
};


인터넷 소켓의 주소 구조체

인터넷 소켓에 사용하는 주소 구조체는 다음과 같습니다. 주소 패밀리명과 포트 번호, IP 주소가 구조체 항목으로 들어있습니다.

struct sockaddr_in {
    sa_family_t     sin_family;
    in_port_t       sin_port;
    struct in_addr  sin_addr;
};

struct in_addr {
    in_addr_t       s_addr;     /* 32비트 IP주소(long) */
};



바이트 순서 함수


컴퓨터에서 정수를 저장하는 방식(바이트 순서:byte ordering)은 두 가지로, 각각 바이트를 순서대로 저장하는 빅 엔디언(big endian)과 거꾸로 저장하는 리틀 엔디언(little entian)입니다.


빅 엔디언 방식은 메모리의 낮은 주소에 정수의 첫 바이트를 위치시킵니다. 예를 들어, 0x1234를 저장할 경우 빅 엔디언은 0x12, 0x34의 순서대로 저장합니다.(최상위 바이트 우선: most significant byte first). 반면 리틀 엔디언의 경우 메모리의 높은 주소에 정소의 첫 바이트를 위치시킵니다. 따라서 0x1234를 저장하면 0x32, 0x12의 순서와 같이 거꾸로 저장합니다.(최하위 바이트 우선: least significant byte first). 인텔 계열(펜티엄)은 리틀 엔디언 방식을, 모토롤라(680x0)와 썬 SPARC는 빅 엔디언 방식을 사용합니다.


컴퓨터마다 바이트를 저장하는 순서가 다르기 때문에 네트워크를 이용한 통신에서 바이트 순서는 주요 문제가 됩니다. 데이터를 보내는 컴퓨터와 받는 컴퓨터의 정수 저장 방식이 다르면 같은 값을 서로 다르게 해석하기 때문입니다. 따라서 TCP/IP에서는 데이터를 전송할 때 무조건 빅엔디언을 사용해 데이터를 전송하기로 결정했습니다. 이를 네트워크 바이트 순서(NBO, Network Byte Order)라고 합니다.


반면 호스트에서 사용하는 바이트 순서는 호스트 바이트 순서(HBO, Host Byte Order)라고 합니다. 시스템에서 통신을 통해 데이터를 내보낼 때는 HBO에서 NBO로 순서를 바꿔서 전송하고, 데이터를 받으면 NBO에서 HBO로 데이터 순서를 변환한 후 처리해야 합니다. NBO와 HBO 간에 바이트 순서를 변환해주는 함수를 사용해 이 작업을 수행할 수 있습니다.

#include <sys/types.h>
#include <netinet/in.h>
#include <inttypes.h>

// hostlong, hostshort: 호스트 바이트 순서로 저장된 값
// netlong, netshort: 네트워크 바이트 순서로 저장된 값
uint32_t htonl(unit32_t hostlong);
uint16_t htons(unit16_t hostshort);
uint32_t ntohl(unit32_t netlong);
uint16_t ntohs(unit16_t netshort);

htonl 함수는 32비트 HBO를 32비트 NBO로 변환합니다. htons 함수는 16비트 HBP를 16비트 NBO로 변환합니다. ntohl 함수는 32비트 NBO를 32비트 HBO로 변환하고, ntohs는 16비트 NBO를 16비트 HBO로 변환합니다. 이 함수들은 socket 라이브러리에 정의되어 있습니다.


servent 구조체는 통신과 관련된 정보를 저장하는 구조체이므로 포트 번호를 NBO 형태로 저장합니다. 따라서 호스트에서 이 값을 출력하려면 nthos 함수로 변환해야 합니다. ntohs를 사용하는 이유는 포트 번호가 16비트 정수(short)기 때문입니다.


아래는 NBO를 HBO로 변환하는 소스코드 예제입니다.


#include <netdb.h>
#include <stdio.h>

int main(void) {
    struct servent *port;
    int n;

    setservent(0);

    for(n=0; n<5; n++) {
        port = getservent();
        printf("Name=%s, Port=%d\n", port->s_name,
                ntohs(port->s_port));
    }

    endservent();

    return 0;
}

[root@dev-web ch11]# ./ex11_3.out
Name=tcpmux, Port=1
Name=tcpmux, Port=1
Name=rje, Port=5
Name=rje, Port=5
Name=echo, Port=7


실행결과를 보면 /etc/services 파일에 정의되어 있는 값과 동일함을 알 수 있습니다.


gestservbyname과 getservbyport 함수를 사용해 포트 정보를 검색하는 예제를 살펴보겠습니다. 포트 번호를 정확하게 처리하려면 NBO와 HBO 사이의 변환이 필요합니다.


다음은 HBO를 NBO로 변환하는 예제입니다.

#include <netdb.h>
#include <stdio.h>

int main(void) {
    struct servent *port;

    // 포트명을 검색하려면 getservbyname 함수에 서비스명을 인자로 지정한다.
    port = getservbyname("telnet", "tcp");
    // ntohs 함수를 사용해 검색 결과를 출력한다.
    printf("Name=%s, Prot=%d\n", port->s_name, ntohs(port->s_port));

    // 포트 번호로 검색하려면 포트 번호를 NBO로 지정해야 한다.
    // getservbyport 함수의 첫번째 인자는 htons 함수를 사용해 포트 번호를
    // HBO에서 NBO로 변환해 지정한다.
    port = getservbyport(htons(21), "tcp");
    // ntohs 함수를 사용해 검색 결과를 출력한다.
    printf("Name=%s, Port=%d\n", port->s_name, ntohs(port->s_port));

    return 0;
}

[root@dev-web ch11]# ./ex11_4.out
Name=telnet, Prot=23
Name=ftp, Port=21


실행 결과를 보면 텔넷 서비스는 23번 포트를, FTP 서비스는 21번 포트를 사용하고 있음을 알 수 있습니다.



IP 주소 변환 함수


IP 주소는 192.168.10.1과 같이 점으로 구분되는 형태입니다. 이 주소를 저장할 때 두 가지 방법이 있습니다. 시스템 내부적으로는 앞서 같은 형태의 주소를 이진값으로 바꿔서 저장합니다. 외부적으로 사용할 때는 문자열로 사용합니다. 따라서 이진값과 문자열로 표시되는 IP주소를 서로 변환할 수 있는 함수를 제공합니다.



문자열 형태의 IP 주소를 숫자 형태로 변환: inet_addr(3)

#include <sys/types.h>
#include <sys/scoket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// cp: 문자열 형태 IP 주소
in_addr_t inet_addr(const char *cp);

inet_addr 함수는 IP 주소를 문자열로 받아 이를 이진값으로 바꿔서 리턴합니다. in_addr_t는 long 형입니다.



구조체 형태의 IP 주소를 문자열 형태로 변환: inet_nota(3)

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

// in_addr 구조체 형태 IP 주소
char *inet_ntoa(const struct in_addr in);

inet_ntoa 함수는 IP 주소를 in_addr 구조체 형태로 받아 점으로 구분된 문자열로 리턴합니다.


IP 주소 변환 함수인 gethostbyaddr 함수를 사용해 호스트 정보를 검색하는 예제를 살펴보겠습니다.

#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int main(void) {
    in_addr_t addr;
    struct hostent *hp;
    struct in_addr in;

    // 문자열로 된 IP 주소를 이진 형태로 변환.
    if((addr = inet_addr("127.0.0.1")) == (in_addr_t)-1) {
        printf("Error: inet_addr(127.0.0.1)\n");
        exit(1);
    }

    // gethostbyaddr 함수에 정수형으로 변환된 IP 주소를 인자로 지정하고
    // 호스트 정보를 검색한다. gethostbyaddr 함수의 첫번째 인자는 char *이므로
    // 형변환을 수행해 지정한다. 주소는 4바이트 크기고, 인터넷 주소이므로 AF_INET을 지정한다.
    hp = gethostbyaddr((char *)&addr, 4, AF_INET);
    if(hp == NULL) {
        (void)printf("Host information not found\n");
        exit(2);
    }

    // 검색된 호스트명을 출력한다.
    printf("Name=%s\n", hp->h_name);

    // hostent 구조체의 항목으로 리턴된 IP 주소를 출력한다.
    // 이 IP 주소는 이진값으로 hostent 구조체의 항목에서 in_addr 구조체로 복사한 후
    // 문자열 형태로 변환해야 한다. in_add 구조체에 값을 지정하고,
    // inet_ntoa 함수로 in_addr 구조체를 인자로 받아 문자열로 변환하고 출력한다.
    (void)memcpy(&in.s_addr, *hp->h_addr_list, sizeof(in.s_addr));
    printf("IP=%s\n", inet_ntoa(in));

    return 0;
}

[root@dev-web ch11]# ./ex11_5.out
Name=localhost
IP=127.0.0.1


실행 결과를 보면 호스트명과 IP 주소가 출력됨을 확인할 수 있습니다.



03. 소켓 인터페이스 함수

소켓도 특수 파일의 하나입니다. 따라서 소켓을 이용해 네트워크 프로그래밍을 할 때는 소켓을 생성해 IP 주소와 연결한 후 서버와 클라이언트가 연결되면 소켓을 통해 읽고 쓰면 됩니다. 소켓을 이용해 데이터를 주고받으려면 다양한 소켓 관련 함수가 필요하며, 이 함수들을 순서에 맞게 호출해야 합니다. 이 절에서는 소켓 인터페이스 함수의 종류와 호출 순서를 살펴보겠습니다.



소켓 인터페이스 함수


소켓을 이용해 네트워크 프로그래밍을 할 때 필요한 함수는 다음과 같습니다.


  • socket: 소켓 파일 기술자 생성
  • bind: 소켓 파일 기술자를 지정된 IP 주소/포트 번호와 결합(bind)
  • listen: 클라이언트의 접속 요청 대기
  • connect: 클라이언트가 서버에 접속 요청
  • accept: 클라이언트의 접속 허용
  • recv: 데이터 수신(SOCK_STREAM)
  • send: 데이터 송신(SOCK_STREAM)
  • recvfrom: 데이터 수신(SOCK_DGRAM)
  • sendto: 데이터 송신(SOCK_DGRAM)
  • close: 소켓 파일 기술자 종료


이들 함수 중 bind, listen, accept는 서버측에서만 사용합니다. 반면 connect 함수는 클라이언트측에서만 사용합니다. 나머지 socket, recv, send, recvfrom, sendto, close 함수는 서버와 클라이언트 모두 사용합니다. 각 함수의 형식과 사용 방법을 알아보겠습니다.



소켓 생성하기: socket(3)

#include <sys/types.h>
#include <sys/socket.h>

// domain: 소켓 종류(유닉스 도메인 또는 인터넷 소켓)
// type: 통신 방식(TCP 또는 UDP)
// protocol: 소켓에 이용할 프로토콜
int socket(int domain, int type, int protocol);

socket 함수는 domain에 지정한 소켓의 형식과 type에 지정한 통신 방식을 지원하는 소켓을 생성합니다. protocol은 소켓에서 이용할 프로토콜로, 보통은 0을 지정합니다. 이 경우 시스템이 protocol 값을 결정합니다. 이 경우 시스템이 protocol 값을 결정합니다. domain에는 도메인 또는 주소 패밀리를 지정합니다. 유닉스 도메인 소켓을 생성할 경우 AF_UNIX를 지정하고, 인터넷 소켓을 생성할 경우 AF_INET을 지정합니다. type에는 통신 방식에 따라 SOCK_STREAM이나 SOCK_DGRAM을 지정합니다. socket 함수는 성공하면 소켓 기술자를, 실패하면 -1을 리턴합니다.


다음은 인터넷 소켓을 생성하는 예입니다.

int sd;
sd = socket(AF_INET, SOCK_STREAM, 0);


소켓에 이름 지정하기: bind(3)

#include <sys/types.h>
#include <sys/socket.h>

// s: socket 함수가 생성한 소켓 기술자
// name: 소켓의 이름을 표현하는 구조체
// namelen: name의 크기
int bind(int s, const struct sockaddr *name, int namelen);

socket 함수로 생성한 소켓을 사용하려면 소켓을 특정 IP 및 포트 번호와 연결해야 합니다. bind 함수는 socket 함수가 생성한 소켓 기술자 s에 sockaddr 구조체인 name에 지정한 정보를 연결합니다. sockaddr 구조체에 지정하는 정보는 소켓의 종류, IP 주소, 포트 번호입니다. bind 함수는 수행을 성공하면 0을, 실패하면 -1을 리턴합니다.


다음은 bind 함수로 소켓에 이름을 붙이는 예입니다. IP 주소를 192.168.100.1로 지정하고 포트 번호를 9000번으로 지정했습니다.

int sd;
struct sockaddr_in sin;

memset((char *)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(9000);
sin.sin_addr.s_addr = inet_addr("192.168.100.1");
memset(&(sin.sin_zero), 0, 8);

bind(sd, (struct sockaddr *)&sin, sizeof(struct sockaddr));


클라이언트 연결 기다리기: listen(3)

#include <sys/types.h>
#include <sys/socket.h>

// s: socket 함수가 생성한 소켓 기술자.
// backlog: 최대 허용 클라이언트 수
int listen(int s, int backlog);

listen 함수는 소켓 s에서 클라이언트의 연결을 받을 준비를 마쳤음을 알립니다. 접속이 가능한 클라이언트 수는 backlog에 지정합니다. listen 함수는 소켓이 SOCK_STREAM 방식으로 통신할 때만 필요합니다. 다음은 클라이언트의 연결 요청을 받아들일 준비를 마쳤고, 최대 10개까지만 연결을 받겠다고 표현한 예입니다.

listen(sd, 10);


연결 요청 수락하기: accept(3)

#include <sys/types.h>
#include <sys/socket.h>

// s: socket 함수가 생성한 소켓 기술자
// addr: 접속을 수락한 클라이언트의 IP 정보
// addrlen: addr 크기
int accept(int s, struct sockaddr *addr, socklen_t *addrlen);

accept 함수는 클라이언트의 연결 요청을 수락합니다. 서버는 accept 함수를 사용해 소켓 s를 통해 요청한 클라이언트와의 연결을 수락합니다. 이때 addr에 클라이언트의 주소가 저장됩니다. addrlen에는 addr의 크기가 저장됩니다. 클라이언트의 연결 요청이 오면 새로운 소켓 기술자를 리턴합니다. 서버는 이 새로운 소켓 기술자를 사용해 클라이언트와 데이터를 주고받을 수 있습니다. s가 가리키는 소켓 기술자는 추가 연결 요청을 기다리는 데 사용합니다.


다음은 accept 함수를 사용하는 예입니다. 기존 소켓 기술자 sd를 통해 연결이 수락되면 새로운 기술자 new_sd가 리턴됩니다. clisin에는 클라이언트 주소가 저장됩니다.

int sd, new_sd;
struct sockaddr_in sin, clisin;

new_sd = accept(sd, &clisin, &sizeof(struct sockaddr_in));


서버와 연결하기: connect(3)

#include <sys/types.h>
#include <sys/socket.h>

// s: socket 함수가 생성한 소켓 기술자
// name: 접속하려는 서버의 IP 정보
// namelen: name의 크기
int connect(int s, const struct sockaddr *name, int namelen);

connect 함수는 클라이언트가 서버에 연결을 요청할 때 사용합니다. connect 함수는 소켓 s를 통해 name에 지정한 서버에 연결을 요청합니다. SOCK_STREAM 방식으로 통신할 때만 필요합니다. connect 함수는 첫번째 인자인 s가 가리키는 소켓을 두번째 인자인 name이 가리키는 주소로 연결합니다. 연결에 성공하면 0을, 실패하면 -1을 리턴합니다. 다음은 IP 주소가 192.168.100.1인 서버에 9000 포트로 연결하는 예입니다.

int sd;
struct sockaddr_in sin;

memset((char *)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(9000);
sin.sin_addr.s_addr = inet_addr("192.168.100.1");
memset(&(sin.sin_zero), 0, 8);

connect(sd, (struct sockaddr *)&sin, sizeof(struct sockaddr));


데이터 보내기: send(3)

#include <sys/types.h>
#include <sys/socket.h>

// s: socket 함수가 생성한 소켓 기술자
// msg: 전송할 메시지를 지정한 메모리 주소
// len: 메시지의 크기
// flags: 데이터를 주고받는 방법을 지정한 플래그
ssize_t send(int s, const void *msg, size_t len, int flags);

send 함수는 소켓 s를 통해 크기가 len인 메시지 msg를 flags에 지정한 방법으로 전송합니다. 마지막 인자인 flags에는 데이터를 주고받는 방법을 지정합니다. 이 플래그에 지정할 수 있는 값은 다음과 같습니다.


  • MSG_OOB: 영역 밖의 데이터(out-of-band data)로 처리한다. 이는 SOCK_STREAM에서만 사용할 수 있다. 영역 밖의 데이터란 중요한 메시지가 아니라는 의미다. 이 플래그를 설정한 메시지를 보내고 수신 확인을 받지 않아도 다른 메시지를 계속 전송한다.
  • MSG_DONTROUTE: 데이터의 라우팅 설정을 해제한다. 이 플래그는 진단 프로그램이나 라우팅 프로그램에서 사용한다.


send 함수는 실제로 전송한 데이터의 바이트 수를 리턴합니다. 리턴값이 지정한 크기보다 작으면 데이터를 모두 보내지 못했음을 의미합니다. send 함수의 리턴값이 -1이면 데이터 전송 자체를 실패했다는 의미입니다. 다음은 간단한 메시지를 전송하는 예입니다.

char *msg = "Sen Test\n";
int len = strlen(msg) + 1;

if(send(sd, msg, len, 0) == -1) {
    perror("send");
    exit(1);
}


데이터 받기: recv(3)

#include <sys/types.h>
#include <sys/socket.h>

// s: socket 함수가 생성한 소켓 기술자
// buf: 전송받은 메시지를 저장할 메모리 주소
// len: buf의 크기
// flags: 데이터를 주고받는 방법을 지정한 플래그
ssize_t recv(int s, void *buf, size_t len, int flags);

recv 함수는 소켓 s를 통해 전송받은 메시지를 크기가 len인 버퍼 buf에 저장합니다. 마지막 인자인 flags는 send 함수에서 사용하는 플래그와 같습니다. recv 함수는 실제로 수신한 데이터의 바이트 수를 리턴합니다. 다음은 버퍼의 크기를 80으로 설정하고 데이터를 수신하는 예입니다. 실제 수신한 데이터의 크기는 rlen 변수에 저장됩니다.

char buf[80];
int len, rlen;

if((rlen = recv(sd, buf, len, 0)) == -1) {
    perror("recv");
    exit(1);
}



데이터 보내기: sendto(3)

#include <sys/types.h>
#include <sys/socket.h>

// s: socket 함수가 생성한 소켓 기술자
// msg: 전송할 메시지를 저장한 메모리 주소
// len: 메시지의 크기
// flgas: 데이터를 주고받는 방법을 지정한 플래그
// to: 메시지를 받을 호스트의 주소
// tolen: to의 크기
ssize_t sendto(int s, const void *msg, size_t len, int flags,
                const struct sockaddr *to, int tolen);

sendto 함수는 UDP 프로토콜로 데이터를 전송하는 함수입니다. 따라서 목적지까지 미리 경로를 설정하지 않고 데이터를 전송합니다. 데이터그램 기반 소켓(SOCK_DGRAM)으로 통신을 할 때는 listen이나 accept 함수를 호출하지 않습니다. 첫 번째 인자인 s로 지정한 소켓을 통해 msg가 가리키는 데이터를 to가 가리키는 목적지의 주소로 전송합니다. send 함수와 달리 매번 목적지 주소를 지정해야 합니다. sendto 함수는 실제로 전송한 데이터의 바이트 수를 리턴합니다. 다음은 IP 주소가 192.168.10.1인 서버로 데이터를 전송하는 예입니다.

char *msg = "Send Test\n";
int len = strlen(msg) + 1;
struct sockaddr_in sin;
int size = sizeof(struct sockaddr_in);

memset((char *)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(9000);
sin.sin_addr.s_addr = inet_addr("192.168.10.1");
memset(&(sin.sin_zero), 0, 8);

if(sendto(sd, msg, len, 0, (struct sockaddr *)&sin, size) == -1) {
    perror("sendto");
    exit(1);
}


데이터 받기: recvfrom(3)

#include <sys/types.h>
#include <sys/socket.h>

// s: socket 함수가 생성한 소켓 기술자
// buf: 전송받은 메시지를 저장할 메모리 주소
// len: 메시지의 크기
// flags: 데이터를 주고받는 방법을 지정한 플래그
// from: 메시지를 보내는 호스트의 주소
// fromlen: from의 크기
ssize_t recvfrom(int s, void *buf, size_t len, int flags,
                struct sockaddr *from, int *fromlen);

recvfrom 함수는 UDP 프로토콜로 전달된 데이터를 수신하는데 사용합니다. 따라서 어디에서 메시지를 보내온 것인지 주소 정보도 함께 전달받습니다. 다섯번째 인자인 from에는 메시지를 발신한 시스템의 주소 정보가 저장됩니다. recvfrom 함수는 실제로 읽어온 데이터의 바이트 수를 리턴합니다. 다음은 서버에서 전송한 데이터를 읽어오는 예입니다.

char buf[80];
int len, size;
struct sockaddr_in sin;

if(recvfrom(sd, buf, len, 0, (struct sockaddr *)&sin, &size) == -1) {
    perror("recvfrom");
    exit(1);
}


소켓 함수의 호출 순서

소켓 관련 함수를 호출하는 일반적인 순서는 아래와 같습니다. 서버측에서는 socket 함수로 먼저 소켓을 생성한 후 bind 함수를 사용해 특정 포트와 연결합니다. 그 후 클라이언트에서 오는 요청을 받을 준비를 마쳤다는 사실을 listen 함수를 통해 운영체제에 알리고, 요청이 들어오기를 기다립니다. 클라이언트의 요청이 오면 accept 함수로 요청을 받고, send와 recv 함수를 통해 데이터를 주고받습니다.


클라이언트의 경우 socket 함수로 소켓을 만든 뒤 connect 함수로 서버와 연결을 요청합니다. 서버에서 연결 요청을 받아들이면 send와 recv 함수로 데이터를 주고받습니다. 서버와 클라이언트 모두 close 함수를 사용해 통신을 종료합니다.




04. 소켓 프로그래밍 예제

이 절에서는 소켓을 이용한 간단한 예제 프로그램을 작성해보겠습니다. 소켓에는 같은 시스템에 있는 프로세스끼리 데이터를 주고받을 때 사용하는 유닉스 도메인 소켓과 다른 시스템의 프로세스와 통신을 하는 인터넷 소켓이 있습니다. 이 절에서는 예제를 통해 각각의 사용 방법을 알아보겠습니다.



유닉스 도메인 소켓 예제


유닉스 도메인 소켓은 같은 시스템에서 통신이 일어나므로 TCP/IP 프로토콜을 직접 이용할 필요가 없습니다. 따라서 유닉스 도메인 소켓에서 사용하는 소켓 주소 구조체의 항목도 IP주소가 아닌 경로명을 지정하도록 되어 있습니다. 이는 파이프나 시스템 V IPC에서 특수 파일을 통신에 사용하는 것과 같다고 생각하면 됩니다. 소켓 주소 구조체의 항목이 다른 것을 제외하면 유닉스 도메인 소켓이든 인터넷 소켓이든 소켓 함수를 사용하는 방식은 동일합니다.


유닉스 도메인 소켓 예제는 서버와 클라이언트 프로그램으로 나뉩니다. 아래 예제에 나타낸 서버 프로그램은 "hbsocket"이라는 이름으로 소켓을 생성한 후 클라이언트의 접속을 기다리다가, 클라이언트가 접속하면 보내온 메시지를 읽어 출력하는 간단한 구조로 되어 있습니다.

#include <sys/un.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define SOCKET_NAME     "hbsocket"

int main(void) {
    char buf[256];
    struct sockaddr_un ser, cli;
    int sd, nsd, len, clen;

    // 소켓을 생성하고 소켓 주소 구조체에 값을 지정한다.
    if((sd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    memset((char *)&ser, 0, sizeof(struct sockaddr_un));
    // 소켓 패밀리를 AF_UNIX로 지정하고 소켓 경로명을 지정한다.
    ser.sun_family = AF_UNIX;
    strcpy(ser.sun_path, SOCKET_NAME);
    len = sizeof(ser.sun_family) + strlen(ser.sun_path);

    // bind 함수로 소켓 기술자를 소켓 주소 구조체와 연결해 이름을 등록한다.
    if(bind(sd, (struct sockaddr *)&ser, len)) {
        perror("bind");
        exit(1);
    }

    // listen 함수를 호출해 통신할 준비를 마쳤음을 알린다.
    if(listen(sd, 5) < 0) {
        perror("listen");
        exit(1);
    }

    printf("Waiting...\n");
    // accept 함수를 호출해 클라이언트의 접속 요청을 수락하고,
    // 새로운 소켓 기술자를 생성해 nsd에 저장한다.
    if((nsd = accept(sd, (struct sockaddr *)&cli, &clen)) == -1) {
        perror("accept");
        exit(1);
    }

    // 클라이언트가 보낸 메시지를 recv 함수로 받아서 출력한다.
    if(recv(nsd, buf, sizeof(buf), 0) == -1) {
        perror("recv");
        exit(1);
    }

    printf("Received Message: %s\n", buf);

    // 출력을 완료 했으므로 소켓을 닫는다.
    close(nsd);
    close(sd);

    return 0;
}


다음 예제에 나타난 클라이언트도 "hbsocket"이라는 이름으로 소켓을 생성한 후 서버측과 연결해 메시지를 전송하는 프로그램입니다.

#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define SOCKET_NAME     "hbsocket"

int main(void) {
    int sd, len;
    char buf[256];
    struct sockaddr_un ser;

    // 소켓을 생성한다.
    if((sd = socket(AF_UNIX, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    // 소켓 주소 구조체에 값을 지정한다.
    memset((char *)&ser, '\0', sizeof(ser));
    ser.sun_family = AF_UNIX;
    strcpy(ser.sun_path, SOCKET_NAME);
    len = sizeof(ser.sun_family) + strlen(ser.sun_path);

    // 소켓 주소 구조체에 지정한 서버로 connect 함수를 사용해 연결을 요청한다.
    if(connect(sd, (struct sockaddr *)&ser, len) < 0) {
        perror("bind");
        exit(1);
    }

    // 서버로 메시지를 전송한다.
    strcpy(buf, "Unix Domain Socket Test Message");
    if(send(sd, buf, sizeof(buf), 0) == -1) {
        perror("send");
        exit(1);
    }
    close(sd);

    return 0;
}

[root@dev-web ch11]# ./ex11_6s.out&

[1] 31031

[root@dev-web ch11]# Waiting...


[root@dev-web ch11]#

[root@dev-web ch11]#

[root@dev-web ch11]# ./ex11_6c.out

Received Message: Unix Domain Socket Test Message

[1]+  Done                    ./ex11_6s.out



인터넷 소켓 예제


인터넷 소켓(internet socket)은 서로 다른 시스템 사이에 통신하므로 TCP/IP 프로토콜을 직접 이용합니다. 따라서 소켓 주소 구조체의 항목에 IP 주소와 포트 번호를 지정해야 합니다. 인터넷 소켓으로 간단한 메시지를 주고받는 프로그램을 작성해보겠습니다. 이번에는 서버에서 클라이언트로 메시지를 보내도록 하겠습니다. 메시지의 내용은 서버에 접속한 클라이언트의 주소를 알려주는 것입니다.


서버 프로그램은 소켓을 생성한 후 IP 주소와 연결하고 클라이언트의 접속을 기다리다가, 클라이언트가 접속하면 클라이언트의 주소를 읽어 메시지를 작성한 후 클라이언트네 전송하는 프로그램입니다. 아래 예제는 서버 시스템의 IP 주소는 172.16.15.42이고, 클라이언트 시스템의 IP 주소는 172.16.15.43이라고 가정합니다.

#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define PORTNUM 9000

int main(void) {
    char buf[256];
    struct sockaddr_in sin, cli;
    int sd, ns, clientlen = sizeof(cli);

    // socket 함수의 인자로 AF_INET과 SOCK_STREAM을 지정해 소켓을 생성한다.
    if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        exit(1);
    }

    // 서버의 IP 주소를 지정하고, 포트 번호는 9000으로 지정해 소켓 주소 구조체를 설정한다.
    memset((char *)&sin, '\0', sizeof(sin));
    sin.sin_family  = AF_INET;
    sin.sin_port    = htons(PORTNUM);
    sin.sin_addr.s_addr = inet_addr("172.16.15.42");

    // bind 함수로 소켓의 이름을 정하고 접속 요청을 받을 준비를 마쳤음을 알린다.
    if(bind(sd, (struct sockaddr *)&sin, sizeof(sin))) {
        perror("bind");
        exit(1);
    }

    if(listen(sd, 5)) {
        perror("listen");
        exit(1);
    }

    // accept 함수로 클라이언트의 요청을 수락한다.
    if((ns = accept(sd, (struct sockaddr *)&cli, &clientlen)) == -1) {
        perror("accept");
        exit(1);
    }

    // 접속한 클라이언트의 IP 주소를 읽어 메시지를 작성한다.
    sprintf(buf, "Your IP address is %s", inet_ntoa(cli.sin_addr));
    // send 함수로 메시지를 전송한다.
    if (send(ns, buf, strlen(buf) + 1, 0) == -1) {
        perror("send");
        exit(1);
    }
    // 사용을 마친 소켓을 모두 닫는다.
    close(ns);
    close(sd);

    return 0;
}


#include <sys/types.h>
#include <sys/socket.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>

#define PORTNUM 9000

int main(void) {

        int sd;
        char buf[256];
        struct sockaddr_in sin;

        // 서버와 마찬가지로 소켓을 생성한다.
        if((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
                perror("socket");
                exit(1);
        }

        // 소켓 주소 구조체를 설정한다.
        // 이때 IP 주소는 서버의 주소로 설정해야 한다.
        memset((char *)&sin, '\0', sizeof(sin));
        sin.sin_family          = AF_INET;
        sin.sin_port            = htons(PORTNUM);
        // sin.sin_addr.s_addr     = inet_addr("172.16.15.42");
        sin.sin_addr.s_addr     = inet_addr("127.0.0.1");

        // connect 함수로 서버에 연결을 요청한다.
        if(connect(sd, (struct sockaddr *)&sin, sizeof(sin))) {
                perror("connect");
                exit(1);
        }

        // 연결되면 서버에서 오는 메시지를 받는다.
        if(recv(sd, buf, sizeof(buf), 0) == -1) {
                perror("recv");
                exit(1);
        }
        close(sd);
        printf("From Server : %s\n", buf);

        return 0;
}