Computer Science 기본 지식/소켓 프로그래밍

[TCP/IP 소켓 프로그래밍] (6) IP주소와 도메인 이름 변환 / 소켓 옵션

로파이 2021. 3. 29. 13:45

출처 : 열혈 TCP/IP 소켓 프로그래밍 윤성우 저

 

IP 주소를 명명하는 도메인 이름을 통해 네트워크를 접속할 수 있다.

 

DNS 서버

도메인 이름에 대한 IP 주소를 알려주는 서버로 호스트가 속한 네트워크에 보통 디폴트(기본) DNS 서버가 자동 설정되어 있다. 호스트는 인터넷 접속에 필요한 IP 주소를 도메인 이름을 통해 로컬 DNS 서버에 질의하고 로컬 DNS 서버는 상위 DNS 서버로 IP 주소를 요청한다. 상위 DNS 서버로 부터 IP 주소를 얻은 로컬 DNS 서버는 호스트에게 전달하여 인터넷에 접속할 수 있게된다.  

 

도메인 이름과 관련된 정보 hostent 구조체

struct hostent
{
    char * 	h_name;        // official name
    char **	h_aliases;     // alias list
    int 	h_addrtype;    // host address type
    int		h_length;      // address length
    char **	h_addr_list;   // address list
}
  • h_name: 공식 도메인 이름
  • h_aliases: 공식 도메인 이름 외에 대체가능한 도메인 이름
  • h_addrtype: IPv4(AF_INET), IPv6(AF_INET6)
  • h_length: IPv4(4바이트), IPv6(16바이트)
  • h_addr_list: 도메인 이름에 대한 IP주소가 정수의 형태로 반환

h_addr_list는 char**로 각 엔트리는 IP 주소 정보를 담고 있으며 char*에서 in_addr*(IPv4 주소: 4 바이트) 혹은 in6_addr*(IPv6 주소: 16 바이트)로 변환하여 사용한다.

 

도메인 이름을 이용하여 IP주소 얻기

struct hostent* gethostbyname(const char* name);
  • 성공 시 hostent 구조체 주소, 실패 시 NULL 포인터

도메인 이름으로 IP 주소 정보를 받을 수 있다.

 

IP 주소를 이용하여 도메인 정보를 얻기

struct hostent* gethostbyaddr(const char* addr, socklen_t len, int familiy);
  • 성공 시 hostent 구조체 주소, 실패 시 NULL 포인터
  • addr: IP 주소 정보를 담은 in_addr*를 char*로 변환하여 전달
  • len: IPv4일 경우 4, IPv6일 경우 16
  • family: AF_INET / AF_INET6

예제

int main(int argc, char* argv[])
{
	const char* siteName = "www.google.com";
	// 윈속 라이브러리 초기화
	WSADATA wsaData;
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		ErrorHandling("WSAStartup() error");

	hostent* host = gethostbyname(siteName);
	if (!host)
	{
		ErrorHandling("get host error");
	}

	// 공식 도메인 이름
	printf("Official name : %s\n", host->h_name);
	// 명명 도메인 이름
	for (int i = 0; host->h_aliases[i]; ++i)
	{
		printf("Aliases %d: %s\n", i + 1, host->h_aliases[i]);
	}
	// 주소 타입
	printf("Address type: %s\n", (host->h_addrtype == AF_INET) ? "AF_INET" : "AF_INET6");
	// 주소 리스트
	for (int i = 0; host->h_addr_list[i]; ++i)
	{
		printf("IP addr %d: %s\n", i + 1, inet_ntoa(*(PIN_ADDR)host->h_addr_list[i]));
	}
	// 윈속 라이브러리 해제
	WSACleanup();

	return 0;
}

 

결과

소켓 옵션

소켓의 입출력 버퍼 사이즈를 설정하거나 TCP 프로토콜에 관한 설정 혹은 IP에 관한 설정을 할 수 있다.

 

getsockopt & setsockopt

int getsockopt(int sock, int level, int optname, void *optval, socklen_t* optlen);
  • 성공 시 0, 실패 시 -1
  • sock: 소켓의 핸들
  • level: 확인할 프로토콜의 레벨
  • optname: 확인할 옵션의 이름
  • optval: 확인 결과의 저장을 위한 버퍼의 주소 값 전달
  • optlen: optval로 전달된 주소 값의 버퍼 크기를 담고 있는 변수의 주소값 전달
int setsockopt(int sock, int level, int optname, const void *optval, socklen_t optlen);
  • 성공 시 0, 실패 시 -1
  • sock: 소켓의 핸들
  • level: 변경할 프로토콜의 레벨
  • optname: 변경할 옵션의 이름
  • optval:  변경할 옵션정보를 저장한 버퍼의 주소 값 전달
  • optlen: optval의 바이트 크기를 저장한 변수의 주소값 전달

 

SO_SNDBUF & SO_RCVBUF

해당 옵션을 optname으로 전달하여 입력 및 출력버퍼의 사이즈를 얻거나 설정할 수 있다.

int snd_buf, rcv_buf;
SOCKET sock = socket(PF_INET, SOCK_STREAM, 0);
int len = sizeof(snd_buf);
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&snd_buf, &len);

len = sizeof(rcv_buf);
getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&rcv_buf, &len);

printf("Input Buffer Size : %d\n", rcv_buf);
printf("Output Buffer Size : %d\n", snd_buf);

// 입출력 버퍼 사이즈 설정
snd_buf = 1024 * 4, rcv_buf = 1024 * 4;
setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&snd_buf, sizeof(snd_buf));
setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&rcv_buf, sizeof(rcv_buf));

len = sizeof(snd_buf);
getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&snd_buf, &len);
len = sizeof(rcv_buf);
getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&rcv_buf, &len);

printf("Input Buffer Size : %d\n", rcv_buf);
printf("Output Buffer Size : %d\n", snd_buf);

 

Time-Wait 상태

정상 종료, Ctrl+C로 콘솔을 종료, 컴퓨터를 강제 종료 등에 소켓을 닫게 되면 소켓을 닫은 쪽, 즉 전송자로부터 FIN 메세지를 보내고 수신자가 FIN을 전송해오면 전송자쪽은 Time-Wait 상태에 진입하게 된다.

4-way handshaking을 통한 연결 종료

Time-Wait의 필요성

User A가 만약 User B로 부터 연결 종료 FIN을 받고 마지막 ACK (z+1) 메세지를 보낸다. User A는 Time-Wait 상태없이 소켓을 닫아버렸고 User B는 ACK (z+1)를 받아야하는 상황이다. 그런데 이 메세지가 중간에 소실되었다면 User B는 영원히 이 ACK 메세지를 받을 수 없게 되어 자신의 소켓을 닫을 수 없는 상황이 된다.

 

Time-Wait 상태가 있었더라면, User B는 응답을 받지 못 했으므로 다시 한번 FIN 메세지를 보내게 되고 Time-Wait 상태로 아직 소켓을 닫지 않은 User A는 FIN 메세지를 받았으므로 Time-Wait 상태의 타이머를 재설정하고 ACK (z+1)를 보내게 된다. 정상 도착하였다면 User A는 타임 아웃으로 자동 종료할 수 있게 된다.

 

Time-Wait와 서버의 강제 종료

서버에서는 특정 포트 번호를 사용하고 있다. 만약 서버가 부득이한 오류로 강제종료를 하였고 빠른 복귀로 서비스를 재개하기 위해서는 소켓을 다시 열어야한다. 그런데 해당 포트 번호는 Time-Wait 상태로 남아있게 되고 타임 아웃될 때 까지 해당 포트 번호를 재사용할 수 없게 된다.

 

SO_REUSEADDR

위 소켓 옵션으로 타임 아웃 상태에 있는 소켓을 재할당하여 새로 시작하는 소켓에 할당할 수 있다. 기본값은 FALSE(0)이고 Time-Wait상태에 있는 소켓의 포트 번호를 사용할 수 없다는 의미이다. 이를 TRUE로 설정하여 사용한다.

int optlen = sizeof(option);
int option = TRUE;
setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (char*)&option, optlen);

 

TCP_NODELAY

이미지 출처 : TCP/IP 소켓프로그래밍 (윤성우 저)

Nagle 알고리즘

Nagle 알고리즘은 네트워크 상에서 돌아다니는 패킷의 흘러 넘침을 막기 위해 1984년에 제안된 알고리즘으로 앞서 전송한 데이터에 대한 ACK 메세지를 받아야만, 다음 데이터를 전송하는 알고리즘이다. 

 

Nagle 알고리즘 기능이 꺼져있는 상태라면 TCP의 전송은 오른쪽 그림과 같이 "Nagle" 문자열을 보내기위해 일정 시간간격으로 한 문자만을 보낸다고 했을 때, 10개의 패킷을 주고 받아야한다. 이는 네트워크 트래픽을 증가시키고 지연을 유발할 수 있다.

 

Nagle 알고리즘은 출력 버퍼에 담긴 데이터 용량을 최대로 끌어올린 후 한꺼번에 보내는 데 의의가 있다. 하지만 만약 용량이 큰 파일을 전송하는 경우 보통 한 번에 파일을 출력버퍼로 보내기 때문에 Nagle 알고리즘이 없는게 낫다.

 

따라서 알고리즘 적용 여부와 상관 없이 트래픽 차이가 크지 않고 적용하는 것보다 전송이 빠른 상황이 가능하기 때문에 TCP_NODELAY 옵션을 TRUE로 설정하여 Nagle 알고리즘을 사용하지 않음으로 설정할 수 있다.

int optlen = sizeof(option);
int option = TRUE;
setsockopt(serv_sock, SOL_SOCKET, TCP_NODELAY, (char*)&option, optlen);

 

Nagle 알고리즘을 해제하는 경우는?

알고리즘 적용 여부에 따른 트래픽 차이가 크지 않으면서 한 세션내에서 전송하는 데이터 양이 많은 경우 사용한다. 데이터 양이 많은 경우 출력 버퍼가 충분히 버퍼링 되어 전송되기 때문에 Nagle 알고리즘을 사용한 것과 비슷하다.