9.소켓의 다양한 옵션

 

소켓에는 다양한 특성이 존재하는데, 이러한 특성은 소켓의 옵션변경을 통해서 변경이 가능하다. 

 

 

 #.Protocol Level 

 

소켓의 옵션은 계층별로 분류된다. 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 - 예제

 

#.SO_SNDBUF & SO_RCVBUF

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

 

 #.get_buf.c - 예제

 #.set_buf.c - 예제

결과

 

 

#.SO_REUSEADDR 와 주소할당 에러(Binding Error) 발생

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

 

 #.reuseadr_eserver.c - 예제

#.echo_client.c - 예제

 

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

 

"서버와 클라이언트가 연결된 상태에서 서버 측 콘솔에서 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);

 

 

 

 

 

[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어