책정리/열혈 TCP,IP

9장 소켓의 다양한 옵션

GONII 2015. 8. 5. 16:13
  • 소켓의 옵션과 입출력 버퍼의 크기
    • 소켓의 다양한 옵션

Protocol Level

Option Name

Get

Set

SOL_SOCKET

SO_SNDBUF
SO_RCVBUF
SO_REUSEADDR
SO_KEEPALIVE
SO_BROADCAST
SO_DONTROUTE
SO_OOBILINE
SO_ERROR
SO_TYPE

O
O
O

O

O

O

O

O

O

O
O
O
O
O
O
O
X
X

IPPROTO_IP

IP_TOS
IP_TTL
IP_MULTICAST_TTL
IP_MULTICAST_LOOP
IP_MULTICAST_IF

O

O

O

O

O

O
O
O
O
O

IPPROTO_TCP

TCP_KEEPALIVE
TCP_NODELAY
TCP_MAXSEG

O

O

O

O
O
O

소켓의 옵션은 계층별로 분류된다. IPPROTO_IP 레벨의 옵션들은 IP프로토콜에 관련된 사항들이며, IPPROTO_TCP 레벨의 옵션들은 TCP 프로토콜에 관련된 사항들이다. SOL_SOCKET레벨의 옵션들은 소켓에 대한 가장 일반적인 옵션들로 생각하면 된다.

  • getsockopt & setsockopt

    거의 모든 옵션은 설정상태의 참조(Get) 및 변경(Set)이 가능하다. 그리고 옵션의 참조 및 변경에는 다음 두 함수를 사용한다.

#include <sys/socket.h>

   

int getsockopt(int sock, int level, int optname, void* optval, socklen_t* optlen);

// 성공 : 0

// 실패 : -1

sock : 옵션 확인을 위한 소켓의 파일 디스크립터

level : 확인할 옵션의 프로토콜 레벨

optname : 확인할 옵션의 이름

optval : 확인결과의 저장을 위한 버퍼의 주소 값

optlen : optval로 전달된 주소 값의 버퍼크기를 담고 있는 변수의 주소 값, 함수호출이 완료되면 이 변수에는 네 번째 인자를 통해 반환된 옵션정보의 크기가 바이트 단위로 계산되어 저장됨

   

#include <sys/socket.h>

   

int setsockopt(int sock, int level, int optname, const void* optval, socklent_t optlen);

// 성공 : 0

// 실패 : -1

sock : 옵션 변경을 위한 소켓의 파일 디스크립터

level : 변경할 옵션의 프로토콜 레벨

optname : 변경할 옵션의 이름

optval : 변경할 옵션정보를 저장한 버퍼의 주소 값

optlen : optval로 전달된 옵션 정보의 바이트 단위 크기

  • sock_type.c

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/socket.h>

   

void error_handling(char* message);

   

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

{

int tcp_sock, udp_sock;

int sock_type;

socklent_t optlen;

int state;

   

optlen = sizeof(sock_type);

tcp_sock = socket(PF_INET, SOCK_STREAM, 0);

udp_sock = socket(PF_INET, SOCK_DGRAM, 0);

printf("SOCK_STREAM: %d\n", SOCK_STREAM);

printf("SOCK_DGRAM: %d\n", SOCK_DGRAM);

   

state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);

if( state )

error_handling("getsockopt() error");

printf("Socket type one : %d\n", sock_type);

   

state = getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen);

if( state )

error_handling("getsockopt() error");

printf("Socket type two: %d\n", sock_type);

   

return 0;

}

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • SO_SNDBUF & SO_RCVBUF

    소켓이 생성되면 기본적으로 입력버퍼와 출력버퍼가 생성된다고 하였다.

    SO_RCVBUF는 입력버퍼의 크기와 관련된 옵션이고, SO_SNDBUF는 출력버퍼의 크기와 관련된 옵션이다. 즉, 이 두 옵션을 이용해서 입출력 버퍼의 크기를 참조할 수 있을 뿐만 아니라, 변경도 가능하다.

    • get_buf.c

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/socket.h>

   

void error_handling(char* message);

   

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

{

int sock;

int snd_buf, rcv_buf, state;

socklent_t len;

   

sock = socket(PF_INET, SOCK_STREAM, 0);

len = sizeof(snd_buf);

state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);

if( state )

error_handling("getsockopt() error");

   

len = sizeof(rcv_buf);

state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);

if( state )

error_handling("getsockopt() error");

   

printf("Input buffer size: %d\n", rcv_buf);

printf("Output buffer size: %d\n", snd_buf);

   

return 0;

}

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • set_buf.c

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <sys/socket.h>

   

void error_handling(char* message);

   

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

{

int sock;

int snd_buf = 1024 * 3;

int rcv_buf = 1024 * 3;

int state;

socklent_t len;

   

sock = socket(PF_INET, SOCK_STREAM, 0);

state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, sizeof(rcv_buf));

if( state )

error_handling("setsockopt() error");

   

state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));

if( state )

error_handling("setsockopt() error");

   

len = sizeof(snd_buf);

state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);

if( state )

error_handling("getsockopt() error");

   

len = sizeof(rcv_buf);

state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, &len);

if( state )

error_handling("getsockopt() error");

   

printf("Input buffer size: %d\n", rcv_buf);

printf("Output buffer size: %d\n", snd_buf);

   

return 0;

}

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

  • SO_REUSEADDR
    • 주소할당 에러(Binding Error) 발생

      SO_REUSEADDR 옵션에 대한 이해에 앞서 Time-wait 상태를 먼저 이해하는 것이 순서이다.

      • reuseadr_eserver.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <unistd.h>

#include <arpa/inet.h>

#include <sys/socket.h>

   

#define TRUE 1

#define FALSE 0

   

void error_handling(char* message);

   

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

{

int serv_sock, clnt_sock;

char message[30];

int option, str_len;

socklen_t optlen, clnt_adr_sz;

struct sockaddr_in serv_adr, clnt_adr;

   

if( argc != 2 )

{

printf("Usage : %s <port>\n", argv[0]);

exit(1);

}

   

serv_sock = socket(PF_INET, SOCK_STREAM, 0);

if( serv_sock == -1 )

error_handling("socket() error");

   

/*optlen = sizeof(option);

option = TRUE;

setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&option, optlen);*/

   

memset(&serv_adr, 0, sizeof(serv_adr));

serv_adr.sin_family = AF_INET;

serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);

serv_adr.sin_port = htons(atoi(argv[1]));

   

if( bind(serv_sock, (struct sockadrr*)&serv_adr, sizeof(serv_adr)))

error_handling("bind() error");

if( listen(serv_sock, 5) == -1 )

error_handling("listen() error");

clnt_adr_sz = sizeof(clnt_adr);

clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);

   

while( (str_len = read(clnt_sock, message, sizeof(message))) != 0 )

{

write(clnt_sock, message, str_len);

write(1, message, str_len);

}

   

close(clnt_sock);

close(serv_sock);

   

return 0;

}

   

void error_handling(char* message)

{

fputs(message, stderr);

fputc('\n', stderr);

exit(1);

}

클라이언트가 먼저 연결종료를 요청하는 경우는 일반적인 상황이기 때문에 별다른 일이 발생할 것이 없다. 그러나 다음과 같이 프로그램을 종료하면 이야기가 달라진다.

"서버와 클라이언트가 연결된 상태에서 서버 측 콘솔에서 CTRL+C를 입력한다. 즉, 서버 프로그램을 강제 종료한다."

이는 서버가 클라이언트 측으로 먼저 FIN메시지를 전달하는 상황의 연출을 위한 것이다. 그런데 이렇게 서버를 종료하고 나면 서버의 재실행에 문제가 생긴다. 동일한 PORT번호를 기준으로 서버를 재실행하면 "bind() error" 메시지가 출려될 뿐 서버는 실행되지 않는다. 그러나 이 상태에서 약 3분 정도 지난 다음 재실행을 하면 정상적인 실행을 확인할 수 있다.

앞서 보인 두 가지 실행방식에 있어서의 유일한 차이점은 FIN메시지를 누가 먼저 전송했는지에 있다.

  • Time-wait 상태

    위 그림에서 호스트A를 서버라고 보면, 호스트A가 호스트B로 FIN 메시지를 먼저 보내고 있으니 서버가 콘솔상에서 CTRL+C를 입력한 상황으로 볼 수 있다. 그런데 여기서 주목할 점은 연결의 해제 과정인 Four-way handshaking 이후에 소켓이 바로 소멸되지 않고 Time-wait 상태라는 것을 일정시간 거친다는 점이다. 물론 Time-wait 상태는 먼저 연결의 종료를 요청한(먼저 FIN메시지를 전송한)호스트만 거친다. 때문에 서버가 먼저 연결의 종료를 요청해서 종료하고 나면, 바로 이어서 실행을 할 수 없는 것이다. 소켓이 Time-wait 상태에 있는 동안에는 해당 소켓의 PORT번호가 사용중인 상태이기 때문이다.

    ※ 서버와 달리 클라이언트 프로그램은 실행될 때마다 PORT번호가 유동적으로 할당되기 때문에 Time-wait상태에 대해 신경쓰지 않아도 된다.

       

    그렇다면 Time-wait 상태는 무엇 때문에 존재하는 것일까? 위 그림에서처럼 호스트A가 호스트B로 마지막 ACK메시지(SEQ 5001, ACK 7502)를 전송하고 나서 소켓을 바로 소멸시켰다고 가정해보자. 그런데 이 마지막 ACK 메시지가 호스트 B로 전달되지 못하고 중간에 소멸되어 버렸다. 그렇다면 어떤 일이 일어나겠는가? 아마도 호스트B는 자신이 좀 전에 보낸 FIN메시지(SEQ 7501, ACK 5001)가 호스트A에 전송되지 못했다고 생각하고 재 전송을 시도할 것이다. 그러나 호스트 A의 소켓은 완전히 종료된 상태이기 때문에, 호스트B는 호스트A로부터 영원히 마지막 ACK메시지를 받지 못하게 된다. 반면 호스트A의 소켓이 Time-wait상태로 놓여있다면 호스트B로 마지막 ACK 메시지를 재전송하게 되고, 호스트B는 정상적으로 종료할 수 있게 된다. 이러한 이유로 먼저 FIN 메시지를 전송한 호스트의 소켓은 Time-wait 과정을 거치는 것이다.

  • 주소의 재할당

    듣고 보니 Time-wait는 매우 중요한 것으로 생각된다. 그러나 이러한 Time-wait가 늘 반가운 것은 아니다. 시스템에 문제가 생겨 서버가 갑작스럽게 종료된 상황을 생각해보자. 재빨리 서버를 재가동시켜서 서비스를 이어가야 하는데, Time-wait 상태 때문에 몇 분을 기다릴 수 밖에 없다면 이는 문제가 될 수 있다. 따라서 Time-wait의 존재가 늘 반가울 수만은 없다. 또한 Time-wait상태는 상황에 따라서 더 길어질 수 있어서 더 큰 문제로 이어질 수 있다. 다음 그림은 종료과정인 Four-way handshaking 과정에서 Time-wait의 상태가 길어질 수밖에 없는 문제의 상황을 보여준다.

    호스트A가 전송하는 Four-way handshaking 과정에서 마지막 데이터가 손실이 되면, 호스트B는 자신이 보낸 FIN메시지를 호스트A가 수신하지 못한 것으로 생각하고 FIN메시지를 재전송한다. 그러면 FIN메시지를 수신한 호스트A는 Time-wait타이머를 재 가동한다. 때문에 네트워크의 상황이 원할하지 못하다면 Time-wait 상태가 언제까지 지속될지 모르는 일이다.

    소켓 옵션 중에서 SO_REUSEADDR의 상태를 변경하면 Time-wait 상태에 있는 소켓에 할당되어 있는 PORT번호를 새로 시작하는 소켓에 할당하게끔 할 수 있다. SO_REUSEADDR의 디폴트 값은 0(FALSE)인데, 이는 Time-wait 상태에 있는 소켓의 PORT번호는 할당이 불가능함을 의미한다. 따라서 이 값을 1(TRUE)로 변경해줘야 한다.

  • TCP_NODELAY

    Nagle알고리즘

    Nagle 알고리즘은 네트워크상에서 돌아다니는 패킷들의 흘러 넘침을 막기 위해 1984년에 제안된 알고리즘이다. 이는 TCP상에서 적용되는 매우 단순한 알고리즘으로써, 이의 적용여부에 따른 데이터 송수신 방식의 차이는 다음과 같다

    Nagle 알고리즘은 앞서 전송한 데이터에 대한 ACK 메시지를 받아야만 다음 데이터를 전송하는 알고리즘이다.

    기본적으로 TCP소켓은 Nagle 알고리즘을 적용해서 데이터를 송수신한다. 때문에 ACK가 수신될 때까지 최대한 버퍼링을 해서 데이터를 전송한다. 위 그림의 왼편에서 이러한 상황을 보여준다.

    Nagle 알고리즘을 적용하지 않으면 네트워크 트래픽(Traffic: 네트워크에 걸리는 부하나 혼잡의 정도를 의미함)에는 좋지 않은 영향을 미친다.

    그러나 Nagle 알고리즘이 항상 좋은 것은 아니다. 전송하는 데이터의 특성에 따라서 Nagle 알고리즘의 적용 여부에 따른 트래픽의 차이가 크지 않으면서도 Nagle 알고리즘을 적용하는 것보다 데이터의 전송이 빠른 경우도 있다.

    일반적으로 Nagle 알고리즘을 적용하지 않으면 속도의 향상을 기대할 수 있으나, 무조건 Nagle 알고리즘을 적용하지 않을 경우에는 트래픽에 상당한 부담을 주게 되어 더 좋지 않은 결과를 얻을 수 있다. 따라서 데이터의 특성을 정확히 판단하지 않은 상태에서 Nagle 알고리즘을 중지하는 일은 없어야 한다.

    Nagle 알고리즘의 중단

    "Nagle 알고리즘의 적용 여부에 따른 트래픽의 차이가 크지 않으면서도 Nagle 알고리즘을 적용하는 것보다 데이터의 전송이 빠른 경우"

    소켓 옵션 TCP_NODELAY를 1(TRUE)로 변경해주면 된다.

    int optVal = 1;

    setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optVal, sizeof(optVal));

    Nagle 알고리즘의 설정상태를 확인은 TCP_NODELAY에 설정된 값을 확인하면 된다.

    int optVal;

    sockLen_t optLen;

    getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&optVal, &optLen);

  • 윈도우 기반으로 구현하기

#include <winsock2.h>

int getsockopt(SOCKET sock, int level, int optname, char* optval, int* optlen);

  • sock

    옵션 확인을 위한 소켓의 핸들

  • level

    확인한 옵션의 프로토콜 레벨 전달

  • optname

    확인할 옵션의 이름 전달

  • optval

    확인 결과의 저장을 위한 버퍼의 주소 값 전달

  • optlen

    optval로 전달된 주소 값의 버퍼 크기를 담고 있는 변수의 주소값

#include <winsock2.h>

int setsockopt(SOCKET sock, int level, int optname, const char* optval, int oplen):

  • sock

    옵션 확인을 위한 소켓의 핸들

  • level

    변경할 옵션의 프로토콜 레벨 전달

  • optname

    변경할 옵션의 이름 전달

  • optval

    변경할 옵션 정보를 저장한 버퍼의 주소 값 전달

  • optlen

    optval로 전달된 옵션정보의 바이트 단위 크기 전달

   

  • 예제 buf_win.c

#include <stdio.h>

#include <stdlib.h>

#include <string.h>

#include <winsock2.h>

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

   

void errorHandling(char* msg);

void showSocketBufSize(SOCKET sock);

   

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

{

WSADATA wsaData;

SOCKET hSock;

int sndbuf, rcvbuf, state;

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

errorHandling("WSAStartup() error");

   

hSock = socket(PF_INET, SOCK_STREAM, 0);

showSocketBufSize(hSock);

   

sndbuf = 1024 * 3;

rcvbuf = 1024 * 3;

   

state = setsockopt(hSock, SOL_SOCKET, SO_SNDBUF, (char*)&sndbuf, sizeof(sndbuf));

if( state == SOCKET_ERROR )

errorHandling("setsockopt() error");

   

state = setsockopt(hSock, SOL_SOCKET, SO_RCVBUF, (char*)&rcvbuf, sizeof(rcvbuf));

if( state == SOCKET_ERROR )

errorHandling("setsockopt() error");

   

showSocketBufSize(hSock);

   

closesocket(hSock);

WSACleanup();

return 0;

}

   

void errorHandling(char* msg)

{

fputs(msg, stderr);

fputc('\n', stderr);

exit(1);

}

void showSocketBufSize(SOCKET sock)

{

int sndbuf, rcvbuf, state, len;

   

len = sizeof(sndbuf);

state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&sndbuf, &len);

if( state == SOCKET_ERROR )

errorHandling("getsockopt() error");

   

len = sizeof(rcvbuf);

state = getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&rcvbuf, &len);

if( state == SOCKET_ERROR )

errorHandling("getsockopt() error");

   

printf("Input buffer size: %d\n", rcvbuf);

printf("Output buffer size: %d\n", sndbuf);

}

   

반응형