본문 바로가기

별걸다하는 IT/네트워크_소켓_통신

[네트워크/소켓 프로그래밍] setsockopt, getsockopt 소켓옵션 상세설명, 세부 옵션 설정하기, 소켓 버퍼 SO_SNDBUF & SO_KEEPALIVE

안녕하세요! 

오늘은 소켓 세부 설정하는 함수에 대해서 포스팅을 해보려고 합니다.

소켓 세부 설정이 꼭 필요한가요?

통신이 하나의 서버에 하나의 클라이언트만으로 이뤄지는 건 아니죠, 또 하나의 트랜젝션만 발생하는게 아니기 때문에, 네트워크 환경을 모두 예측하기는 힘들어요. 생각지도 못한 상황이 발생할 수도 있는!

보통 기본으로 다 설정이 되어있으니까 세부 설정 할 일이 없을 것 같겠지만, 세부설정을 해줘야만 하는 상황이 꽤 흔하게 온답니다. 오늘은 소켓의 세부사항을 조절하는 함수에 대해 알아볼거예요.

 

소켓 세부 사항 설정하는 함수

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

get으로 시작하는 함수는 소켓의 옵션을 가져와 확인하는 함수이고. set으로 시작하는 setsockopt는 소켓의 세부사항들을 설정하는 함수예요.

 

[헤더]

해당 함수들을 포함하고 있는 헤더는 <sys.socket.h>입니다. 

윈도우의 경우에는 <winsock2.h>

 

[return 값]

둘 다, 성공했을 경우 0을 반환하고 실패하였을 경우 -1을 반환합니다.

 

[인자 값]

해당 인자에 어떤 값이 들어가는 지는 밑에 표로 정리해놨어요. 

sock - 옵션 확인을 위한 소켓의 파일 디스크립터 전달.

level - 확인(getsockopt)또는 변경(setsockopt)할 옵션의 프로토콜 레벨 전달

optname - 확인 또는 변경할 옵션의 이름 전달

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

optlen - 주의 : getsockopt의 경우 포인터 값을 매개변수로 받고, setsockopt의 경우 타입이 socklen_t*가 아닌 socklen_t이다.)

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

(setsockopt) 네 번째 매개변수 optval로 전달된 옵션정보의 바이트 단위 크기 전달

 

프로토콜 레벨에 따른 옵션명 종류

분명 프로토콜 레벨과 옵션명을 넣는 부분은 타입이 int인데, 왜 이렇게 문자열로 되어있을까 생각할 수 있어요.

정수형인자가 맞는데, 사람들이 보기 쉬운 아래와 같은 문자열 값이 시스템에서는 정수형 인자로 매핑되어 있습니다. 즉 시스템에 정의되어 있는 상수값이예요. 

Protocol Level 옵션명 get set 설명
SOL_SOCKET (일반) SO_SNDBUF OK OK 소켓 송신 버퍼의 크기
SO_RCVBUF OK OK 소켓 입력 버퍼의 크기
SO_REUSEADDR OK OK 이미 사용중인 주소나 포트에 대해서도 바인드 허용
SO_LINGER OK OK 소켓을 닫을 때 남은 데이터의 처리 규칙 지정
SO_KEEPALIVE OK OK

keepalive 메세지 활성화

(프로토콜에 구현되어 있을 경우)

SO_BROADCAST OK OK 브로드캐스트 허용
SO_DONTROUTE OK OK 라우팅하지 않고 직접 인터페이스로 전송 
SO_OOBINLINE OK OK OOB 데이터 전송을 설정할 때 일반 입력 큐에서 데이터를 읽을 수 있게 합니다.
SO_SNDLOWAT OK OK 전송할 최소 바이트 수 
SO_RCVLOWAT OK OK recv()가 반환될 수 있는 최소 바이트 수
SO_ERROR OK X 에러상태를 봔환하고 클리어됩니다.
SO_TYPE OK X 소켓의 타입. (ex: SOCK_STREAM) 

IPPROTO_IP

(IP레벨-IPv4)

IP_TOS OK OK 패킷의 우선순위 설정 (Type Of Service)
IP_TTL OK OK 유니캐스트 시 TTL 값 설정
IP_MULTICAST_TTL OK OK 멀티캐스트 시 TLL 값 설정
IP_MULTICAST_LOOP OK OK 멀티캐스트 소켓이 자신이 보낸 패킷을 수신토록 합니다.
IP_MULTICAST_IF OK OK 멀티캐스트 패킷을 보낼 인터페이스 설정
IPPROTO_TCP (TCP레벨) TCP_KEEPALIVE OK OK  keep alive 시간 간격 지정
TCP_NODELAY OK OK 데이터를 합치기 위해 Nagle's 알고리즘 사용시 지연을 허용 안함
TCP_MAXSEG OK OK TCP 최대 세그먼트 지정

대게 많죠? 사실 이거보다 더 많습니다. 

더 많고 자세한 내용을 참조하고 싶으신 분은 이 링크를 참조해주세요 

주로 SOL_SOCKET과 IPPROTO_TCP를 많이 사용하시게 될거예요.

IPROTO_IP는 IP프로토콜을 의미하고, IPROTO_TCP레벨의 옵션들은 TCP프로토콜에 관련된 옵션들입니다. 그리고 SOL_SOCKET레벨의 옵션들은 소켓에 대한 가장 일반적인 옵션들이예요. 이 외에 IPPROTO_IPV6가 있어요 ㅎㅎ 

어떤 옵션들은 프로토콜 독립적이여서 SOL_SOCKET라는 이름으로 소켓 계층 자체에서 다루어지고, 어떤 옵션들은 전송 프로토콜에 특화되어 IPPROTO_TCP레벨로 불리며, 또 다른 어떤 옵션들은 IPPROTO_IP라는 이름으로 인터넷 프로토콜 계층에서 다뤄진다.
[마이클 도나후, 케네스 칼버트 저 TCP/IP 소켓 프로그래밍]

오늘 다 살펴보기는 어려우니 SOL_SOCKET에서 대표적인 몇 가지를 먼저 살펴보도록 할게요.

1. SO_SNDBUF, SO_RCVBUF - 버퍼 크기

소켓의 버퍼 크기를 지정해줄때, 또는 소켓의 버퍼 크기를 확인하고 싶을 때, 3번째 인자 optname에 SO_SNDBUF나 SO_RCVBUF를 넣어주시면 됩니다.

SO_SNDBUF는 전송소켓에서 사용할 최대 버퍼의 크기를 설정합니다. (출력버퍼)

SO_RCVBUF는 수신소켓에서 사용할 최대 버퍼의 크기를 설정합니다 (입력버퍼) 

 

근데 버퍼를 지정하고 확인해보면 내가 지정했던 값이 아니예요! 왜냐 버퍼는 상당히 주의깊게 다뤄여야 하는 영역이라 우리의 요구대로 크기가 딱 맞춰지지는 않습니다. 다만 우리는 setsockopt함수 호출을 통해 버퍼 크기에 대한 요구사항을 전달할 뿐이예요. 보통 커널은 setsockopt로 설정한 값의 2배로 버퍼 크기를 잡습니다. 

즉: 항상 소켓 버퍼크기가 내가 지정한 새로운 크기로 적용되지는 않는다. 단 참조를 한다.

 

소스코드 C언어

[소켓 버퍼 크기 읽어와서 출력하기 - getsockopt]

int rcv_buffer_size;
int sockopt_size = sizeof(rcv_buffer_size);
if(getsockopt(sock, SOL_SOCKET, SO_RCVBUF, &rcv_buffer_size, &sockopt_size) < 0)
	error_handling("getsockopt failed");
printf("rcv buffer size :%d\n", rcv_buffer_size);

[전체 소스코드]

#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_buffer, rcv_buffer;
    socklen_t len;
    
    sock = socket(PF_INET, SOCK_STREAM, 0);
    len = sizeof(snd_buffer);
    if(getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buffer, &len) <0)
    	error_handling("getsockopt error");
    len = sizeof(rcv_buffer);
    if(getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buffer, &len) <0)
    	error_handling("getsockopt error");
        
    printf("input buffer size :%d\n", rcv_buffer);
    printf("output buffer size :%d\n", snd_buffer);
    return 0;
}
void error_handling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

[소켓 버퍼 설정하기 - setsockopt]

이번에는 소켓버퍼를 설정해볼건데요~~ 함수로 만들어볼게요.

int set_sobuffer (int sockfd, int size)
{
    int rc, rbuffer, wbuffer;
    rbuffer = wbuffer = size;
    rc = setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, (char*)&rbuffer, sizeof(rbuffer));
    rc = setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, (char*)&wbuffer, sizeof(wbuffer));
    return rc;
}

2. SO_KEEPALIVE - 세션 상태를 위한 패킷전송

그 다음 SO_KEEPALIVE 옵션명에 대해 살펴볼게요 ㅎㅎ 사실 순서에 규칙은 없습니다. 그냥 제가 작성하고 싶은 순서대로... REUSEADDR처럼 좀 길어질 거같은건 2편으로 빼려고요 ㅎㅎ

 

'살아있게 유지해라'로 번역이 되죠?? 이 옵션을 세팅하면 중간중간 세션이 끊어졌는지 안끊어졌는지 확인을 위한 패킷을 보내서 체크해주는 거예요. 이런 옵션들이 꽤 중요합니다. 좀비세션을 막기 위한 방책중 하나로 사용되기도 합니다. 

 

좀비세션은 서버나 클라이언트가 정상적으로 종료하지 않았는데 (보통 렌선이 뽑혔다다거나 방화벽에서 일부로 세션을 종료시켰다던가 등)도 불구하고 종료되어 서버나 클라이언트는 세션이 종료되었는지 모르는 상태의 세션을 말해요.

이렇게 종료가 정상적으로 사라지지 않으면 시스템의 중요한 리소스를 차지하게 되고 시스템에 큰 부담이 되게 됩니다. 좀비프로세스가 생기지 않도록 소켓프로그래밍을 하는 것도 중요하지만, 또 방어책으로 설정을 해놓는 것도 중요해요. KEEPALIVE 설정을 해 주기적으로 확인 패킷을 보내는데 응답이 없으면 잘못된 소켓이라 판단해 소켓을 종료합니다. 이렇게 죽이는 것 외에, 중간 중간 패킷을 보내서 세션을 유지시키는 역할을 하기도 합니다. 

 

KEEPALIVE 설정을 하면 시스템 레지스트리 값을 변경하므로 모든 소켓의 KEEPALIVE 속성이 변하게 됩니다. 디폴트는 2시간이예요. 각각 소켓별로 KEEPALIVE 속성을 세팅하고 싶으시면 SIO_KEEPALIVE_VALAS를 사용해주시면 됩니다.

 

소스코드 C언어

[KEEPALIVE 세팅하기 - setsockopt]

int set_sokeepalive(int sockfd)
{
    int switch = 1;
    setsockopt(sockfd, SOL_SOCKET, SO_KEEPALIVE, (char *)&switch, sizeof(switch));
    return 0;
}

2. SO_TYPE - 소켓 타입정보 확인

그 다음은~~~ 소켓의 타입을 확인할 수 있는 SO_TYPE입니다. 이 옵션은 set이 없고 get만 있어요. 즉 소켓 타입은 소켓 생성시 한 번 결정되면 변경이 불가능.

소켓타입이 뭐냐 우리 소켓 생성할 때 인자로 타입을 정해주잖아요. SOCK_STREAM, SOCK_DGRAM 이런거, 

이런거를 확인할 수 있게 해주는 거예요.

 

1이 나오면 TCP 소켓이라는거 (SOCK_STREAM)

2가 나오면 UDP 소켓이라는거 (SOCK_DGRAM)

 

소스코드 C언어

[SO_TYPE 확인 - getsockopt]

#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;
    
    socklen_t optlen = sizeof(sock_type);
    tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
    udp_sock = socket(PF_INET, SOCK_DGRAM, 0);
    
    if(getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen)<0);
    	error_handling("getsockopt error");
    printf("socket type one :%d", sock_type);
    
    if(getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &optlen)<0);
    	error_handling("getsockopt error");
    printf("socket type two :%d", sock_type);
    return 0;
}
void error_handling(char* message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

오늘은 일단 3개까지만 알아봤어요 ㅎㅎ 나머지 것들은 다음편에 이어서 알아보도록 해요!

오늘 고생하셨습니다~ 다음 포스팅에서 또 뵙기를 바랄게요 :)