공부/Server

소켓 옵션 정리

sudo 2021. 8. 1. 21:08

getsockopt & setsockopt 함수

이름에서부터 알 수 있듯이 socket의 옵션정보들을 얻거나 바꿀 수 있는 함수들이다. 

#include <winsock2.h>
int getsocket(SOCKET sock, int level, int optname, char* optval, int* optlen)
// 성공 시 0, 실패 시 SOCKET_ERROR 반환
  • sock : 옵션 정보를 얻을 소켓
  • level : 확인할 옵션의 프로토콜 레벨(ex. SOL_SOCKET, IPPROTO_IP, IPPROTO_TCP)
  • optname : 확인할 옵션의 이름(ex. SO_RCVBUF, SO_SNDBUF, SO_TYPE 등)
  • optval : 확인 결과 저장을 위한 버퍼의 주소값
  • optlen : optval이 가리키고 있는 타입의 크기를 갖고 있는 변수의 주소값
#include <winsock2.h>
int setsockopt(SOCKET sock, int level, int optname, const char* optval, int optlen)
// 성공 시 0, 실패 시 SOCKET_ERROR 반환
  • sock : 옵션 변경할 소켓
  • level : 변경할 옵션의 프로토콜 레벨
  • optname : 변경할 옵션의 이름
  • optval : 변경할 옵션 정보를 저장한 버퍼의 주소
  • optlen : 네번째 매개변수 optval의 사이즈

SO_SNDBUF & SO_RCVBUF 옵션

setsockopt로 입출력 버퍼의 크기를 바꿔줄 수 있는데 이때 setsockopt의 인자 optname에 들어가는 것이 SO_SNDBUF와 SO_RCVBUF이다. SO_RCVBUF가 입력 버퍼, SO_SNDBUF가 출력 버퍼를 의미한다. 다음과 같이 입출력 버퍼 사이즈를 바꾸려고 한다면 내가 바꾸려고 한 사이즈만큼 정확하게 바뀌지 않는 것을 확인할 수 있다.

#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, rcv_buf=1024*3;
	int state;
	socklen_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)
{
// 생략
}

이유는 입출력 버퍼를 우리가 원하는 크기대로 완전히 바꿀 수 있다면, 만약에 입출력 버퍼 사이즈를 0으로 만들려는 시도가 있다면 문제가 생길 수 있기 때문이다. 따라서 우리의 의도를 100% 반영해주진 않고 어느 정도만 반영을 해서 사이즈를 바꿔준다.

 

time_wait와 SO_REUSEADDR

서버와 클라이언트 연결중, 클라이언트 쪽에서 ctrl + c와 같은 방법으로 종료 시키려고 한다면 종료 직후 바로 다시 연결 하려고 bind()를 호출 해도 문제가 없다. 하지만 서버 쪽에서 연결을 종료하고 바로 다시 연결을 하기 위해 bind()를 호출하면 실패하고 SOCKET_ERROR를 리턴한다. 약 3분정도 기다린 후에 다시 서버가 연결 요청을 하면 정상적으로 연결된다. 이런 현상이 일어나는 이유를 알기 위해서는 time_wait 상태를 이해해야 한다. 

아래 그림처럼 TCP 소켓 연결을 종료할 때는 4-way handshaking 과정을 거치는 것을 우리는 이미 알고 있다. 호스트 A(위의 예시에서는 서버에 해당)가 마지막으로 ACK패킷을 보내고 호스트 A는 time-wait라는 타이머를 동작시키고 이 타이머가 종료됐을 때 연결이 종료된다. time-wait동안에는 해당 소켓의 PORT번호가 사용중인 상태이다. 그래서 위의 경우 서버가 먼저 종료 요청을 하고 직후에 다시 연결이 안됐던 것이다. 하지만 클라이언트가 먼저 종료 요청을 했을 때는 종료 후 바로 연결 요청을 해도 가능한 이유는 connect()함수에서 배웠듯이 클라이언트 소켓의 PORT번호는 임의로 할당되기 때문이다. 

 

그렇다면 time-wait는 왜 있는 걸까? 만약에 time-wait가 없이 위의 경우에서 호스트 A가 마지막 ACK패킷을 보내고 바로 소켓을 소멸시켰다고 가정해보자. 그런데 이 마지막 ACK패킷이 호스트 B에게 전달되지 못하고 도중에 소멸된다면 호스트 B는 자신이 이전에 보냈던 FIN 패킷이 호스트 A에게 전달되지 못했다고 생각해서 FIN패킷을 보내려고 할것이다. 하지만 호스트 A의 소켓은 이미 소멸돼서 아무리 재전송해도 호스트 A는 수신하지 못한다. 따라서 이런 상황을 막기 위해 time-wait를 두는 것이다.

 

하지만 우리는 SO_REUSEADDR옵션을 true로 바꿈으로써 time-wait상태에 있는 소켓의 PORT번호를 새로 시작하는 소켓에 할당하게 해줄 수 있다. SO_REUSEADDR은 디폴트로 false이다. 아래와 같은 코드를 사용하면 SO_REUSEADDR을 true로 바꿀 수 있다.

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

TCP_NODELAY

TCP상에 적용되는 Nagle 알고리즘이란것이 있다. Nagle 알고리즘은 앞서 전송한 데이터에 대한 ACK데이터를 받아야만, 다음 데이터를 전송하는 알고리즘이다. 그림으로 표현하면 아래와 같다.

Nagle 알고리즘을 적용하면 ACK이 수신될 때 까지 최대한 버퍼를 채워서 데이터를 전송한다. Nagle을 적용했을 때는 4개의 패킷이 오갔지만 Nagle을 적용하지 않았을 경우 10개의 패킷이 오갔음을 확인할 수 있다. 따라서 Nagle 알고리즘을 적용하지 않았을 때 트래픽에 부정적인 영향을 끼칠 수 있다. 1바이트를 전송하더라도 패킷에 포함된 헤더정보 크기가 수십바이트에 이르기 때문이다. 

 

하지만 Nagle 알고리즘을 적용하는 것이 항상 좋은 것은 아니다. 용량이 큰 파일 데이터 전송을 할때는 Nagle 알고리즘을 적용하는 것이 더 빠르다. 파일을 출력 버퍼로 밀어 넣는 작업은 시간이 걸리지 않기 때문에 출력 버퍼를 매번 거의 꽉 채운 상태로 패킷을 전송하게 된다. 따라서 패킷 수도 크게 증가하지도 않고 굳이 ACK패킷을 기다리지 않고 데이터를 전송하니 속도도 빨라진다. 따라서 Nagle 알고리즘도 상황에 맞게 쓰거나 중단시킬 필요가 있다.

 

Nagle 알고리즘은 마찬가지 setsockopt()함수로 사용 여부를 바꿀 수 있다.

int opt_val = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&opt_val, sizeof(opt_val));

Nagle 알고리즘을 사용하면 delay가 있는 것이므로 Nagle 알고리즘이 사용중이라면 opt_val이 0이 할당될 것이고, 사용중이지 않다면 opt_val에 1이 할당될 것이다.