책정리/윈도우 네트워크 프로그래밍

7장 소켓 옵션

GONII 2015. 4. 23. 14:12
  • 개요

    소켓 프로그래밍이란 아래 그럼에서 보는 것과 같이 소켓을 통해 간접적으로 TCP, IP등과 같은 하부 프로토콜을 다룸으로써 애플리케이션을 작성하는 것을 의미한다. 이와 같은 관점에서 보면, 애플리케이션은 소켓 코드가 제공하는 인터페이스인 소켓 함수를 호출함으로써 하부 프로토콜이 제공하는 다양한 기능을 사용하는 것이다.

    지금까지 다룬 모든 소켓 애플리케이션은 특별한 변경 없이 소켓 함수를 그대로 호출함으로써 구현하였다. 그러나 다양한 애플리케이션을 작성하다보면, 동일한 소켓 함수를 호출하더라도 소켓 코드나 프로토콜 구현 코드의 동작이 달라지도록 할 필요가 생긴다. 소켓 옵션(socket option)을 적용하면 소켓 함수의 기본 동작을 변경할 수 있으며, 이를 이용하여 애플리케이션은 소켓 코드와 프로토콜 구현 코드에 대한 좀 더 세부적인 제에를 할 수 있게 된다.

    소켓 옵션은 처리하는 코드의 위치에 따라 크게 두 종류로 구분할 수 있다.

    • 소켓 코드가 담당하는 부분

      옵션을 설정하면 소켓 코드에서 해석하고 처리한다. 프로토콜 독립적인 성격이 있으나 옵션의 실제적인 적용은 프로토콜의 종류에 따라 달라진다.

    • 프로토콜 구현 코드가 담당하는 부분

      옵션을 설정하면 프로토콜 구현 코드에서 해석하고 처리한다. 프로토콜 의존적인 성격이 있으므로 프로토콜의 종류에 따라 옵션도 달라진다.

    소켓 옵션을 설정하려면 setsockopt( )함수를 호출한다.

    int setsockopt(

    SOCKET s,

    int level,

    int optname,

    const char* optval,

    int optlen

    ); // 성공:0, 실패:SOCKET_ERROR

    • s

      옵션을 적용할 소켓이다

    • level

      옵션을 해석하고 처리하는 코드를 나타내며, 옵션 레벨(option level)이라 부른다. 소켓 코드가 처리하면 SOL_SOCKET, IP프로토콜 코드가 처리하면 IPPROTO_IP, TCP프로토콜 코드가 처리하면 IPROTO_TCP를 사용한다

    • optname

      구체적인 옵션 이름이다.

    • optval

      설정할 옵션값을 담고 있는 버퍼의 주소다. 옵션값으로는 대부분 정수형을 사용하지만 구조체형을 사용하는 경우도 있다.

    • optlen

      optval이 가리키는 버퍼의 크기다

    현재 설정된 소켓 옵션값을 얻으려면 getsockopt( )함수를 호출한다

    int getsockopt(

    SOCKET s,

    int level,

    int optname,

    char* optval,

    int* optlen

    ); // 성공:0, 실패:SOCKET_ERROR

    • s

      옵션값을 얻을 소켓이다.

    • level

      옵션을 해석하고 처리하는 코드를 나타내며, 옵션 레벨(option level)이라 부른다. 소켓 코드가 처리하면 SOL_SOCKET, IP프로토콜 코드가 처리하면 IPPROTO_IP, TCP프로토콜 코드가 처리하면 IPPROTO_TCP를 사용한다.

    • optname

      구체적인 옵션 이름이다.

    • optval

      옵션값을 저장할 버퍼의 주소다. 옵션값으로는 대부분 정수형을 사용하지만 구조체형을 사용하는 경우도 있다.

    • optlen

      값-결과(value-result)인자다. 함수 호출 전에는 optval이 가리키는 버퍼의 크기로 초기화한다. 함수 호출 후에는 옵션값의 크기가 저장된다.

  • SOL_SOCKET

optname

optval타입

get

set

설명

SO_BROADCAST

BOOL

.

.

브로드캐스팅 허용

SO_DONTROUTE

BOOL

.

.

데이터 전송 시 라우팅 테이블 참조 과정 생략

SO_KEEPALIVE

BOOL

.

.

주기적으로 연결 여부 확인

SO_LINGER

linger{}

.

.

보낼 데이터가 있을 경우 closesocket( )함수 리턴 지연

SO_SNDBUF

SO_RCVBUF

int

.

.

소켓 송/수신 버퍼 크기 설정

SO_SNDTIMEO

SO_RCVTIMEO

int

.

.

send( ), recv( )등의 함수에 대한 타임아웃(timeout) 설정

SO_REUSEADDR

BOOL

.

.

지역 주소(IP 주소, 포트 번호) 재사용 허용

  • IPPROTO_IP

optname

optval 타입

get

set

설명

IP_HDRNCL

BOOL

.

.

데이터를 보낼 때 IP 헤더를 포함

IP_TTL

int

.

.

IP패킷의 TTL(time-to-live)

IP_MULTICAST_IF

IN_ADDR{ }

.

.

멀티캐스트 패킷을 보낼 인터페이스 설정

IP_MULTICAST_TTL

int

.

.

멀티캐스트 패킷의 TTL 변경

IP_MULTICAST_LOOP

BOOL

.

.

멀티캐스트 패킷의 루프백 여부 설정

IP_ADD_MEMBERSHIP

IP_DROP_MEMBERSHIP

ip_mreq{ }

  

.

멀티캐스트 그룹 가입과 탈퇴

  • IPPROTO_TCP

optname

optval타입

get

set

설명

TCP_NODELAY

BOOL

.

.

Nagle 알고리즘 작동 중지

  • SOL_SOCKET

    SOL_SOCKET 레벨 옵션은 소켓 코드에서 해석하여 처리하므로 프로토콜 독립적이다. 그러나 여기에 속한 옵션을 모든 프로토콜에 적용할 수 있는 것은 아니므로 주의해야 한다.

    SO_BROADCAST

    SO_BROADCAST 옵션을 설정하면 해당 소켓을 이용하여 브로드캐스트 데이터를 보낼 수 있다. 프로토콜의 특성상 TCP 소켓에는 사용할 수 없고 UDP 소켓에만 사용할 수 있다.

    SO_DONTROUTE

    TCP/IP 애플리케이션이 보내는 데이터는 라우팅 테이블(routing table 또는 route table)이라 부르는 정보를 참조하여 하부의 IP프로토콜이 보내게 된다. SO_DONTROUTE옵션을 설정하면 데이터 전송 시 라우팅 테이블 참조를 생략하고, 곧바로 bind( )함수로 설정한 네트워크 인터페이스로 모든 데이터를 보내게 된다.

    SO_DONTROUTE옵션 사용 예는 다음과 같다

BOOL optval = TRUE;
if(setsockopt(listen_sock, SOL_SOCKET, SO_DONTROUTE, (char*)&optval, sizeof(optval)) == SOCKET_ERROR)
{

err_quit("setsocketopt( )");

}

SO_DONTROUTE옵션은 TCP와 UDP 소켓에 모두 사용할 수 있으며, 특히 TCP 서버의 연결 대기 소켓(listening socket)에 대해 이 옵션을 설정해두면 accept( )함수가 생성하는 새로운 소켓도 자동으로 동일한 옵션이 설정된다.

SO_KEEPALIVE

SO_KEEPALIVE 옵션을 설정해두면, TCP프로토콜 수준에서 연결 여부를 확인하기 위해 상대 TCP에 주기적으로(약 2시간 간격) TCP 패킷을 보낸다. 이때 상대 TCP의 반응에 따라 다음과 같이 두 경우로 나눌 수 있다.

  • 상대 TCP가 정해진 시간 이내에 응답을 하는 경우

    TCP연결에 문제가 없는 경우므로 애플리케이션은 이를 알아차리지 못한다.

  • 상대 TCP가 시간 이내에 응답을 하지 않거나 RST(reset) 패킷으로 응답할 경우

    TCP 연결에 문제가 있는 경우므로 자동으로 소켓을 닫는다. 이 경우 애플리케이션이 해당 소켓으로 데이터 통신을 하려고 하면 오류가 발생한다.

TCP 연결은 물리적인 연결이 아닌 양쪽에서 상태 정보를 유지하는 가상적인 연결이다. 따라서 데이터 교환이 없다면 상대 호스트가 다운되거나 전원이 나간 경우를 감지하지 못한다. 한편, TCP서버는 접속한 클라이언트 수에 비례하여 소켓을 생성하며 이로 인해 시스템 자원 소모도 비례해서 증가하므로 연결이 끊어진 소켓을 그때그때 닫아줄 필요가 있다. 그러나 TCP 연결의 특성상 연결이 끊어진 사실을 서버가 감지할 수 없는 경우가 발생한다. 이때 SO_KEEPALIVE옵션을 설정해두면 TCP프로토콜 수준에서 주기적으로 끊어진 연결을 감지하여 불필요하게 열린 소켓을 닫을 수 있게 된다.

SO_KEEPALIVE 옵션 사용 예는 다음과 같다

BOOL optval = TRUE;

if( setsockopt(listen_sock, SOL_SOCKET, SO_KEEPALIVE, (char*)&optval, sizeof(optval)) == SOCKET_ERROR)

{

err_quit("setsockopt( )");

}

SO_KEEPALIVE 옵션은 TCP소켓에만 사용할 수 있다.

SO_LINGER

TCP 소켓에 대한 closesocket( )함수 호출은 다음과 같이 두 가지 기능을 한다

  • TCP프로토콜 수준에서 연결 종료 절차(connection termination sequence)를 작동한다. 즉 TCP연결 종료를 위한 패킷 교환이 발생한다.
  • 소켓과 관련된 리소스(송,수신 버퍼를 위한 메모리, ...)를 운영체제에 반환한다. closesocket( )함수 호출이 끝나면 해당 소켓은 통신을 위해 재사용할 수 없게 된다.

그런데 closesocket( )함수 호출 시, send( )함수로 보내려고 했던 데이터가 아직 송신 버퍼에 남아 있을 경우 다음과 같은 질문을 생각해볼 수 있다.

Q1. closesocket( )함수는 언제 리턴하는가?

Q2. 송신 버퍼의 데이터를 보낼 것인가, 삭제할 것인가?

Q3. TCP연결 종료는 어떤 방식으로 할 것인가?(정상 종료? 강제 종료?)

위의 질문에 대한 응답으로 다음과 같은 다양한 동작 방식을 생각해 볼 수 있다.

  • closesocket( )함수는 곧바로 리턴하고 송신 버퍼의 데이터는 백그라운드로 보낸 후 TCP연결을 정상 종료한다.
  • closesocket( )함수는 곧바로 리턴하고 송신 버퍼의 데이터는 삭제한 후 TCP연결을 강제 종료한다.
  • 송신 버퍼의 데이터를 모두 보내고 TCP 연결을 정상 종료한 후 closesocket( )함수가 리턴한다. 만약 일정 시간 내에 송신 버퍼의 데이터를 모두 보내지 못하면 TCP 연결을 강제 종료한 후 closesocket( )함수가 리턴한다. 이때 송신 버퍼에 남은 데이터는 삭제한다.

SO_LINGER옵션은 closesocket( )함수의 이중적인 기능을 세부적으로 제어하기 위한 소켓 옵션이다. 따라서 이를 이용하면 closesocket( ) 함수 호출 시 1~3 중 한 방식으로 동작하도록 할 수 있다.

SO_LINGER 옵션값으로 다음과 같은 구조체를 사용한다.

struct linger {

u_short l_onoff;        /* option on/off */

u_short l_linger;        /* linger time */

};

typedef struct linger LINGER;

  • l_onoff

    이 값이 0이면 closesocket( )함수는 곧바로 리턴한다. 0이 아니면 l_linger로 설정한 시간 동안 리턴하지 않고 대기한다

  • l_linger

    closesocket( )함수가 대기할 시간을 초(seconds) 단위로 설정한다. 0 또는 양수 값을 사용한다.

SO_LINGER 옵션 사용 예는 다음과 같다

LINGER optval;

optval.l_onoff = 1; // linger on

optval.l_linger = 10; // linger time = 10초

if(setsockopt(sock, SOL_SOCKET, SO_LINGER,

(char*)&optval, sizeof(optval)) == SOCKET_ERROR)

{

err_quit("setsockopt( )");

}

옵션값에 따른 closesocket( )함수의 동작을 요약하면 다음과 같다

l_onoff

l_linger

closesocket( )함수 동작

추가 설명

0

사용 안함

1번과 동일

closesocket( )함수의 디폴트 동작

1

0

2번과 동일

  

1

양수

3번과 동일

  

SO_LINGER옵션은 TCP소켓에만 사용할 수 있으며, 특히 TCP서버의 연결 대기 소켓(listening socket)에 대해 이 옵션을 설정해두면 accept( )함수가 생성하는 새로운 소켓도 자동으로 동일한 옵션이 설정된다.

SO_SNDBUF, SO_RCVBUF

SO_SNDBUF와 SO_RCVBUF 옵션을 이용하면 소켓의 송신 버퍼와 수신 버퍼 크기를 변경할 수 있다. 두 옵션 사용 방법은 동일하다. 다음 코드는 TCP서버에서 연결 대기 소켓(listening socket)의 수신 버퍼 크기를 두 배로 늘리는 방법을 보여준다. 버퍼 크기를 무한정 늘릴 수 없으므로 버퍼 크기가 제대로 설정됐는지 확인하기 위해 getsockopt( )함수를 다시 호출한다는 점을 주목해야 한다.

// 수신 버퍼 크기를 얻는다

int optval;

int optlen = sizeof(optval);

if( getsockopt(listen_sock, SOL_SOCKET, SO_RCVBUF,

(char*)&optval, &optlen) == SOCKET_ERROR)

{

err_quit("getsockopt( )");

}

printf("수신 버퍼 크기 = %d\n", optval);

   

// 수신 버퍼 크기를 두 배로 늘린다

optval *= 2;

if( setsockopt(listen_sock, SOL_SOCKET, SO_RCVBUF,

(char*)&optval, sizeof(optval)) == SOCKET_ERROR)

{

err_quit("setsockopt( )");

}

   

// 수신 버퍼 크기를 얻는다

optlen = sizeof(optval);

if( getsockopt(listen_sock, SOL_SOCKET, SO_RCVBUF,

(char*)&optval, &optlen) == SOCKET_ERROR)

{

err_quit("getsockopt( )");

}

printf("새로 설정한 수신 버퍼 크기 = %d\n", optval);

SO_SNDBUF와 SO_RCVBUF 옵션 사용 시 주의할 점은, UDP소켓은 언제든 이 옵션으로 소켓 버퍼 크기를 변경할 수 있지만, TCP 소켓은 연결이 이루어지기 전에 변경해야 한다는 점이다. 즉 TCP서버에서는 listen( )함수, TCP클라이언트에서는 connect( )함수 호출 전에 옵션 설정을 해야 한다. TCP서버의 연결 대기 소켓(listen socket)의 경우, 소켓 버퍼 크기를 설정해두면 accept( )함수가 리턴하는 소켓도 자동으로 동일한 크기의 소켓 버퍼가 할당된다.

SO_SNDTIMEO, SO_RCVTIMEO

기본 소켓은 블로킹 소켓이므로 데이터 전송 함수 호출 시 조건이 만족되지 않으면 무한정 블록이 되고, 이로 인해 교착 상태가 발생할 수 있다. 그러나 SO_SNDTIMEO과 SO_RCVTIMEO 옵션으로 타임아웃(timeout)을 설정해두면 데이터 전송 함수(send( ), recv( ), sendto( ), recvfrom( ))가 작업 완료와 상관없이 일정 시간 후 리턴하도록 할 수 있다

SO_SNDTIMEO, SO_RCVTIMEO 옵션값으로는 정수형을 사용하며, 밀리초(milliseconds)단위로 타임아웃을 설정한다 사용 예는 다음과 같다

int optval = 3000;

if(setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO,

(char*)&optval, sizeof(optval)) == SOCKET_ERROR)

{

err_quit("setsockopt( )");

}

SO_SNDTIMEO과 SO_RCVTIMEO 옵션은 TCP와 UDP 소켓에 모두 사용할 수 있다. 이 두 옵션은 교착 상태를 방지하는 용도 외에 통신 상대가 일정 시간 이내에 응답하는지 여부를 알고 싶을 때도 유용하게 사용할 수 있다

SO_REUSEADDR

SO_REUSEADDR옵션을 설정하면 이미 사용중인 IP주소와 포트 번호를 재사용할 수 있다. 즉 이미 사용 중인 IP주소와 포트 번호로 bind( )함수를(성공적으로) 호출할 수 있다. 이 옵션은 TCP와 UDP 소켓에 모두 사용할 수 있으며 매우 유용하다

  • 예제 TCPServer

#include <winsock2.h>

#include <stdlib.h>

#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 512

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER |

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

// 소켓 함수 오류 출력

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER|

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

int main(int argc, char* argv[])

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

{

return -1;

}

   

// socket()

SOCKET listen_sock = socket(AF_INET, SOCK_STREAM, 0);

if(listen_sock == INVALID_SOCKET)

{

err_quit("socket()");

}

   

// SO_REUSEADDR 소켓ㅋ 옵션 설정

BOOL optval = TRUE;

retval = setsockopt(listen_sock, SOL_SOCKET,

SO_REUSEADDR, (char*)&optval, sizeof(optval));

if( retval == SOCKET_ERROR )

{

err_quit("setsockopt()");

}

   

// bind()

SOCKADDR_IN serveraddr;

ZeroMemory(&serveraddr, sizeof(serveraddr));

serveraddr.sin_family = AF_INET;

serveraddr.sin_port = htons(9000);

serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);

retval = bind(listen_sock, (SOCKADDR*)&serveraddr,

sizeof(serveraddr));

if( retval == SOCKET_ERROR )

{

err_quit("bind()");

}

   

//listen()

retval = listen(listen_sock, SOMAXCONN);

if(retval == SOCKET_ERROR)

{

err_quit("listen()");

}

   

// 데이터 통신에 사용할 변수

SOCKET client_sock;

SOCKADDR_IN clientaddr;

int addrlen;

char buf[BUFSIZE+1];

   

while(1)

{

// accept()

addrlen = sizeof(clientaddr);

client_sock = accept(listen_sock,

(SOCKADDR*)&clientaddr, &addrlen);

if( client_sock == INVALID_SOCKET)

{

err_display("accept()");

continue;

}

printf("\n[TCP서버]클라이언트접속:IP 주소=%S, 포트 번호=%d\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port));

   

// 클라이언트와 데이터 통신

while(1)

{

// 데이터 받기

retval = recv(client_sock, buf, BUFSIZE, 0);

if(retval == SOCKET_ERROR)

{

err_display("recv()");

break;

}

else if(retval == 0)

{

break;

}

// 받은 데이터 출력

buf[retval] = '\0';

printf("[TCP/%s:%d] %s\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port), buf);

   

// 데이터 보내기

retval = send(client_sock, buf, retval, 0);

if(retval == SOCKET_ERROR)

{

err_display("send()");

break;

}

}

   

// closesocket()

closesocket(client_sock);

printf("[TCP서버]클라이언트 종료: IP주소=%s, 포트번호=%d\n",

inet_ntoa(clientaddr.sin_addr),

ntohs(clientaddr.sin_port));

}

   

//closesocket()

closesocket(listen_sock);

   

// 윈속 종료

WSACleanup();

   

return 0;

}

  • TCPServer를 두 개 실행한다 SO_RESUSEADDR옵션을 설정하지 않았다면 bind()함수 오류가 발생할 것이다.
  • TCPClient실행과 종료를 해보면 한 TCP서버만 클라이언트 접속을 받아들이는 것이다.
  • TCPServer.exe파일을 TCPServer2.exe로 이름을 바꾸고 serveraddr.sin_addr.s_addr = htons(INADDR_ANY);를 serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");로 수정한다. 이렇게 되면 INADDR_ANY 대신 특정 IP주소로 바인딩하도록 바꾼 것이다. 이렇게 하면 두 TCP서버가 동일한 지역 포트 번호(9000)을 사용하되 지역 IP주소는 다른 상황이 된다.
  • TCPServer와 TCPServer2를 실행한 후 TCPClient 실행과 종료를 반복해보면 클라이언트가 "127.0.0.1" 주소로 접속하므로 "127.0.0.1" 주소로 바인딩한 TCPServer가 INADDR_ANY주소로 바인딩한 TCPServer2보다 우선권을 갖는다

SO_REUSEADDR 옵션을 설정하는 목적을 요약하면 다음과 같다

  • 서버 종료 후 재실행 시 bind( )함수에서 오류가 발생하는 것을 방지한다.
  • 두 개 이상의 IP주소를 가진 호스트에서 각 IP주소별로 서버를 따로 운용할 수 있도록 한다
  • 멀티캐스팅 애플리케이션이 동일한 포트 번호를 사용할 수 있도록 한다.
  • IPPROTO_IP

    IPPROTO_IP 레벨 옵션은 IP 프로토콜 코드에서 해석하여 처리한다. 따라서 AF_INET주소 체계를 사용하는 소켓에 대해서만 적용할 수 있다.

    멀티캐스팅은 UDP에 대해서만 적용되므로 앞으로 소개할 모든 소켓 옵션은 UDP소켓에만 적용된다.

    멀티캐스팅 개념

    멀티캐스팅(multicasting)을 사용하면 하나 또는 여러 네트워크에 속한 선별된 개체끼리 통신할 수 있다. 멀티캐스팅을 하려면 예약된 IP주소를 사용해야 하며, 이를 이용하여 특정 그룹을 지정할 수 있다

    멀티캐스팅을 위한 IP주소는 다음과 같이 정의되어 있다.

    이 주소는 224.0.0.0 ~ 239.255.255.255 범위값을 가질 수 있다. 고정된 상위 4비트를 제외한 28비트를 멀티캐스트 그룹ID라 부르며, 이 값이 특정 그룹을 지정하는 용도로 사용된다. 애플리케이션은 멀티캐스트 데이터를 수신하기 위해 멀티캐스트 그룹에 자유롭게 가입(join)할 수 있고 탈퇴(leave)할 수도 있다. 그룹 가입과 탈퇴는 소켓 옵션을 이용하여 할 수 있다.

    인터넷의 멀티캐스팅은 다음과 같은 특징을 지닌다.

    • 그룹 가입과 탈퇴는 자유롭고, 그룹 구성원 모두 평등하다 아래 그림처럼 A,B 두 호스트는 언제든지 그룹 구성원에게 멀티캐스트 데이터를 보낼 수 있다.

    • 멀티캐스트 데이터를 받으려면 반드시 그룹에 가입해야 한다
    • 멀티캐스트 그룹에 가입하지 않은 호스트도 언제든지 특정 멀티캐스트 그룹에 데이터를 보낼 수 있다.

    [실습] 멀티캐스팅 예제

    6장에서 다룬 브로드캐스팅 예제를 멀티캐스팅을 사용하도록 변경한다. 애플리케이션의 동작 방식은 다음과 같다

    • 송신자 : 사용자가 키보드로 입력한 문자열을(fgets) 멀티캐스트 주소로 보낸다(sendto)
    • 수신자 : 멀티캐스트 그룹에 가입한다. 멀티캐스트 데이터를 받아(recvfrom), 이를 문자열로 간주하여 무조건 화면에 출력한다(printf)

    • 예제 MulticastSender

#include <winsock2.h>

#include <ws2tcpip.h>

#include <stdlib.h>

#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE        512

   

// 소켓 함수 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER|

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

// 소켓 함수 오류 출력

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER|

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

int main(int argc, char* argv[])

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if(WSAStartup(MAKEWORD(2,2), &wsa) != 0)

{

return -1;

}

   

// socket()

SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);

if(sock == INVALID_SOCKET)

{

err_quit("socket()");

}

   

// SO_REUSEADDR 옵션 설정

BOOL optval = TRUE;

retval = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR,

(char*)&optval, sizeof(optval));

if(retval == SOCKET_ERROR)

{

err_quit("socketopt()");

}

   

// bind()

SOCKADDR_IN localaddr;

ZeroMemory(&localaddr, sizeof(localaddr));

localaddr.sin_family = AF_INET;

localaddr.sin_port = htons(9000);

localaddr.sin_addr.s_addr = htonl(INADDR_ANY);

retval = bind(sock, (SOCKADDR*)&localaddr, sizeof(localaddr));

if(retval == SOCKET_ERROR)

{

err_quit("bind()");

}

   

// 멀티캐스트 그룹 가입

struct ip_mreq mreq;

mreq.imr_multiaddr.s_addr = inet_addr("235.7.8.9");

mreq.imr_interface.s_addr = htonl(INADDR_ANY);

retval = setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP,

(char*)&mreq, sizeof(mreq));

if(retval == SOCKET_ERROR)

{

err_quit("setsockopt()");

}

   

// 데이터 통신에 사용할 변수

SOCKADDR_IN peeraddr;

int addrlen;

char buf[BUFSIZE+1];

   

// 멀티캐스트 데이터 받기

while(1)

{

// 데이터 받기

addrlen = sizeof(peeraddr);

retval = recvfrom(sock, buf, BUFSIZE, 0,

(SOCKADDR*)&peeraddr, &addrlen);

if( retval == SOCKET_ERROR )

{

err_display("recvfrom()");

continue;

}

   

// 받은 데이터 출력

buf[retval] = '\0';

printf("[UDP/%s:%d] %s\n",

inet_ntoa(peeraddr.sin_addr),

ntohs(peeraddr.sin_port), buf);

}

   

// 멀티캐스트 그룹 탈퇴

retval = setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP,

(char*)&mreq, sizeof(mreq));

if( retval == SOCKET_ERROR )

{

err_quit("setsockopt()");

}

   

// closesocket()

closesocket(sock);

   

// 윈속 종료

WSACleanup();

   

return 0;

}

  • 예제 MulticastSender

#include <winsock2.h>

#include <ws2tcpip.h>

#include <stdlib.h>

#include <stdio.h>

#pragma comment(lib, "ws2_32.lib")

   

#define BUFSIZE 512

   

// 소켓 오류 출력 후 종료

void err_quit(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER|

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

MessageBox(NULL, (LPCTSTR)lpMsgBuf, msg, MB_ICONERROR);

LocalFree(lpMsgBuf);

exit(-1);

}

   

// 소켓 함수 오류 츨력

void err_display(char* msg)

{

LPVOID lpMsgBuf;

FormatMessage(

FORMAT_MESSAGE_ALLOCATE_BUFFER|

FORMAT_MESSAGE_FROM_SYSTEM,

NULL, WSAGetLastError(),

MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),

(LPTSTR)&lpMsgBuf, 0, NULL);

printf("[%s] %s", msg, (LPCTSTR)lpMsgBuf);

LocalFree(lpMsgBuf);

}

   

int main(int argc, char* argv[])

{

int retval;

   

// 윈속 초기화

WSADATA wsa;

if( WSAStartup(MAKEWORD(2,2), &wsa) != 0 )

{

return -1;

}

   

// socket()

SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);

if(sock == INVALID_SOCKET)

{

err_quit("socket()");

}

   

// 멀티캐스트 TTL 설정

int ttl = 2;

retval = setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL,

(char*)&ttl, sizeof(ttl));

if(retval == SOCKET_ERROR)

{

err_quit("setsockopt()");

}

   

// 소켓 주소 구조체 초기화

SOCKADDR_IN remoteaddr;

ZeroMemory(&remoteaddr, sizeof(remoteaddr));

remoteaddr.sin_family = AF_INET;

remoteaddr.sin_port = htons(9000);

remoteaddr.sin_addr.s_addr = inet_addr("235.7.8.9");

   

// 데이터 통신에 사용할 변수

char buf[BUFSIZE+1];

int len;

   

// 멀티캐스트 데이터 보내기

while(1)

{

// 데이터 입력

printf("\n[보낼데이터]");

if(fgets(buf, BUFSIZE+1, stdin) == NULL)

{

break;

}

   

// '\n' 문자 제거

len = strlen(buf);

if(buf[len-1] == '\n')

{

buf[len-1] = '\0';

}

if(strlen(buf) == 0)

{

break;

}

   

// 데이터 보내기

retval = sendto(sock, buf, strlen(buf), 0,

(SOCKADDR*)&remoteaddr, sizeof(remoteaddr));

if(retval == SOCKET_ERROR)

{

err_display("sendto()");

continue;

}

printf("%d바이트 보냄\n", retval);

}

// closesocket()

closesocket(sock);

   

// 윈속 종료

WSACleanup();

   

return 0;

}

코드 분석

  • IP_MULTICAST_IF

    IP_MULTICAST_IF 옵션은 두 개 이상의 IP주소를 가진 호스트에서 데이터를 보낼 네트워크 인터페이스를 설정할 때 사용한다. 다음 코드는 특정 IP주소(147.46.114.70)로 멀티캐스트 데이터를 보내도록 설정한 것이다.이 코드를 수행하면, 147.46.114.70 주소를 할당받은 네트워크 인터페이스를 통해 멀티캐스트 데이터가 전송된다. IP_MULTICAST_IF 옵션을 설정하지 않으면, IP라우팅 기능에 의해 각 패킷별로 적절한 네트워크 인터페이스를 선택하여 보내게 된다.

IN_ADDR localaddr;

localaddr.s_addr = inet_addr("147.46.114.70");

if(setsockopt(sock, IPPROTO_IP, IP_MULTICAST_IF,

(char*)&localaddr, sizeof(localaddr)) == SOCKET_ERROR)

{

err_quit("setsockopt()");

}

  • IP_MULTICAST_TTL

    멀티캐스트 패킷은 IP헤더, UDP헤더 그리고 애플리케이션 데이터로 구성된다.

    멀티캐스트 패킷이 생성될 때 헤더의 TTL(time-to-live) 필드는 디폴트값 1로 설정된다. 이것은 멀티캐스트 패킷이 라우터 경계를 넘어갈 수 없음을 뜻한다. 라우터 경계를 넘어 특정 범위까지 멀티캐스트 패킷을 보내려면 IP_MULTICAST_TTL 옵션을 이용하여 TTL값을 변경해야 한다.

// 멀티캐스트 TTL 설정

int ttl = 2;

retval = setsockopt(sock, IPPROTO_IP, IP_MULTICAST_TTL,

(cahr*)&ttl, sizeof(ttl));

if(retval == SOCKET_ERROR) err_quit("setsockopt()");

  • IP_MULTICAST_LOOP

    멀티캐스트 그룹에 가입한 애플리케이션이 멀티캐스트 데이터를 보내면 이 데이터를 자신도 받는다(디폴트 동작). 다음과 같이 IP_MULTICAST_LOOP옵션을 이용하면 디폴트 동작을 바꿀 수 있다.

    BOOL optval = FALSE; // 자신이 보낸 데이터는 받지 않는다

if( setsockopt(sock, IPPROTO_IP, IP_MULTICAST_LOOP,

(char*)&optval, sizeof(optval)) == SOCKET_ERROR)

{

err_quit("setsockopt()");

}

  • IP_ADD_MEMBERSHIP, IP_DROP_MEMBERSHIP

    멀티캐스트 데이터를 받기 위해서는 반드시 멀티캐스트 그룹에 가입해야 한다. IP_ADD_MEMBERSHIP옵션은 그룹에 가입할 때, IP_DROP_MEMBERSHIP옵션은 그룹에서 탈퇴 할때 사용하다. 두 옵션 모두 ws2tcpip.h파일에 선언된 ip_mreq구조체를 사용한다.

#include <ws2tcpip.h>

struct ip_mreq{

struct in_addr imr_multiaddr; // IP multicast address or group

struct in_addr imr_interface; // local IP address of interface

};

imr_multiaddr

가입하거나 탈퇴할 멀티캐스트 그룹 주소

imr_interface

멀티캐스트 패킷을 받을 네트워크 인터페이스를 나타낸다. INADDR_ANY를 사용하면 자동으로 설정된다.

IP_ADD_MEMBERSHIP 옵션을 이용하여 특정 네트워크 인터페이스(147.46.114.70)를 멀티캐스트 그룹(235.7.8.9)에 가입시키는 코드는 다음과 같다. 이 코드를 수행하면 147.46.114.70 주소를 할당받은 네트워크 인터페이스가 멀티캐스트 데이터를 받도록 세팅된다.

struct ip_mreq mreq;

mreq.imr_multiaddr.s_addr = inet_addr("235.7.8.9);

mreq.imr_interface.s_addr = inet_addr("147.46.114.70");

if( setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP,

(char*)&mreq, sizeof(mreq)) == SOCKET_ERROR)

{

err_quit('setsockopt()");

}

멀티캐스트 그룹에 가입하고 탈퇴하는 코드는 다음과 같다

// 멀티캐스트 그룹 가입

struct ip_mreq mreq;

mreq.imr_multiaddr.s_addr = inet_addr("235.7.8.9");

mreq.imr_interface.s_addr = htonl(INADDR_ANY);

retval = setsockopt(sock, IPPROTO_IP, IP_ADD_MEMBERSHIP,

(char*)&mreq, sizeof(mreq));

if(retval == SOCKET_ERROR) err_quit("setsockopt()");

// 멀티캐스트 그룹화

retval = setsockopt(sock, IPPROTO_IP, IP_DROP_MEMBERSHIP,

(char*)&mreq, sizeof(mreq));

if(retval == SOCKET_ERROR) err_quit("setsockopt()");

  • IPPROTO_IP

    IPPROTO_TCP레벨 옵션은 TCP 프로토콜 코드에서 해석하여 처리한다. 따라서 TCP소켓에 대해서만 적용할 수 있다. 여기서는 TCP_NODELAY옵션만 살펴본다

    TCP_NODELAY옵션은 Nagle 알고리즘 작동을 중지하는 역할을 한다. Nagle알고리즘은 TCP의 성능을 향상시키고 네트워크 트래픽(traffic)을 감소시키는 효과가 있기 때문에 디폴트로 동작한다. 따라서 대부분의 경우 TCP_NODELAY 옵션을 설정하지 않는 것이 바람직하지만 몇 가지 이유로 인해 TCP_NODELAY 옵션을 사용하는 경우가 있다.

    Nagle알고리즘을 이해하기 위해서는 TCP의 동작 원리를 먼저 알아야 한다. TCP는 신뢰성 있는 데이터 전송을 위한 방법 중 하나로 ACK(acknowlegement)에 기반한 데이터 재전송을 한다. 즉 데이터를 보낸 후 상대방이 잘 받았는지 확인하고 그렇지 않을 경우 재전송하는 것이다.

    그러나 TCP패킷을 보낼 때마다 ACK가 도착할 때까지 대기하게 되면 네트워크 자원 활용도가 낮아지므로, 슬라이딩 윈도우(sliding iwndow)라는 방식을 사용한다. 이 방식에서는 ACK를 받지 못했더라도 윈도우 크기(windwos size)만큼 데이터를 계속 보낼 수 있으므로 성능을 높일 수 있다. 여기에 추가적으로 모든 TCP패킷마다 상대편에 ACK를 보낼 것을 요구할 경우 성능이 저하되기 때문에, 마지막으로 성공적으로 받은 데이터에 대해서만 ACK를 보내도록 한다.

    Nagle 알고리즘의 동작 방식을 요약하면 다음과 같다

    • 보낼 데이터가 MSS(maximum segment size)로 정의된 크기만큼 쌓이면, 상대편에 무조건 보낸다.

      이 경우, 슬라이딩 윈도우 방식으로 계속 데이터를 보낼 수 있게 된다.

    • 보낼 데이터가 MSS보다 작을 경우, 이전에 보낸 데이터에 대한 ACK가 오기를 기다린다.

      ACK가 도달하면 보낼 데이터가 MSS보다 작더라도 상대편에 보낸다.

      이 경우, TCP패킷 하나에 대해 ACK를 기다리고, ACK를 받으면 다시 TCP 패킷 하나를 보내는 동작이 반복된다.

    Nagle 알고리즘의 장단점은 다음과 같다

    장점 : 작은 패킷이 불필요하게 많이 생성되는 것을 미연에 방지함으로써 네트워크 트래픽을 감소시킨다

    단점 : 데이터가 충분히 쌓일 때까지 또는 ACK가 도달할 때까지 대기하는 시간 때문에 애플리케이션의 반응 시간(response time)이 길어질 가능성이 있다.

    TCP_NODELAY옵션은 Nagle알고리즘의 장점을 포기하는 대신 애플리케이션의 반응 속도를 빠르게 하고자 할 때 사용한다. TCP_NODELAY옵션 설정 예는 다음과 같다

BOOL optval = TRUE; // Nagle 알고리즘 작동 중지

if(setsockopt(sock, IPPROTO_TCP, TCP_NODELAY,

(char*)&optval, sizeof(optval)) == SOCKET_ERROR)

{

err_quit("setsockopt()");

}

   

반응형