수업/Network Programming

[Network Programming] Socket의 옵션들

hw-ani 2023. 5. 28. 20:50

말고도 더 있는데 그 중 일부만 보여줌

 

이렇게 다양한 option들이 존재하고 이걸 이용해 소켓의 정보를 get 하거나 set 할 수 있다.

표에서 볼 수 있듯이 소켓의 옵션들은 계층별로 분류된다. SOL_SOCKET 레벨의 옵션들은 소켓에 대한 가장 일반적인 옵션들이고, IPPROTO_IP 레벨의 옵션들은 IP protocol에 관련된 사항들이며, IPPROTO_TCP 레벨의 옵션들은 TCP protoocl에 관련된 사항들이다.

우측은 get만 할 수 있는 정보인지 set만 할 수 있는 정보인지 둘 다 할 수 있는지를 보여준다. 예를들어 소켓의 타입을 알 수 있는 SO_TYPE 옵션으로는 Set은 할 수 없고 Get만 할 수 있다. 즉, SO_TYPE은 확인만 가능하고 변경이 불가능한 옵션이다.

 

 

 

 

getsockopt

#include <sys/socket.h>

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

함수 이름에서 알 수 있듯 옵션 정보를 가져오는데 사용하는 함수이다.

첫번째 인자로 옵션을 확인할 socket의 file descriptor를 전달한다.

두번째 인자로 확인할 옵션의 프로토콜 레벨을 전달한다.

세번째 인자로 확인할 옵션의 이름을 전달한다.

네번째 인자로 확인 결과를 저장할 버퍼의 주소 값을 전달한다.

다섯번째 인자로 네번째 인자로 전달된 버퍼의 크기를 담고있는 변수의 주소 값을 전달한다. 함수 호출이 완료되면 이 인자에는 반환된 옵션 정보의 크기가 byte 단위로 저장된다. (accept() 함수의 마지막 인자랑 같은 역할)

 

성공 시 0, 실패 시 -1 을 반환한다.

 

 

 

setsockopt

#include <sys/socket.h>

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

함수 이름에서 알 수 있듯 옵션 정보를 설정하는데 사용하는 함수이다.

첫번째 인자로 옵션을 변경할 socket의 file descriptor를 전달한다.

두번째 인자로 변경할 옵션의 프로토콜 레벨을 전달한다.

세번째 인자로 변경할 옵션의 이름을 전달한다.

네번째 인자로 변경할 옵션 정보를 저장할 버퍼의 주소 값을 전달한다.

다섯번째 인자로 네번째 인자로 전달된 옵션 정보의 크기를 byte 단위로 전달한다.

 

성공 시 0, 실패 시 -1 을 반환한다.

 

 

 

 

 

예제 1 : Socket type 확인

//TCP UDP 소켓 생성
tcp_sock = socket(PF_INET, SOCK_STREAM, 0);
udp_sock = socket(PF_INET, SOCK_DGRAM, 0);

//SOCK type 상수 출력해보기(나중에 실제로 구해본거랑 비교하려고)
printf("SOCK_STREAM : %d\n", SOCK_STREAM);
printf("SOCK_DGRAM : %d\n", SOCK_DGRAM);

//SO_TYPE 옵션으로 sock_type 변수에 소켓 타입 정보 담아오기
state = getsockopt(tcp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &potlen);
if (state)  error_handling("getsockopt() error!");
printf("Socket type one: %d\n", sock_type);

//SO_TYPE 옵션으로 sock_type 변수에 소켓 타입 정보 담아오기
state = getsockopt(udp_sock, SOL_SOCKET, SO_TYPE, (void*)&sock_type, &potlen);
if (state)  error_handling("getsockopt() error!");
printf("Socket type two: %d\n", sock_type);

//출력 결과
//SOCK_STREAM: 1
//SOCK_DGRA: 2
//Socket type one: 1
//Socket type two: 2

소켓 타입 확인  →  SOL_SOCKET의 SO_TYPE

(SO_TYPE은 소켓 타입 확인만 되고 변경(setsockopt())은 안됨.)

 

 

 

 

예제 2 : Socket 입출력 버퍼 크기 확인

//tcp socket 생성
sock = socket(PF_INET, SOCK_STREAM, 0);

//출력 버퍼 크기를 getsockopt의 SO_SNDBUF 옵션으로 가져온다.
len = sizeof(snd_buf);
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
if (state) error_handling("getsockopt() error");

//입력 버퍼 크기를 getsockopt의 SO_RCVBUF 옵션으로 가져온다.
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);

소켓의 입출력 버퍼 크기 확인/변경  →  SOL_SOCKET의 SO_RCVBUF와 SO_SNDBUF

 

 

 

 

예제 3 : Socket 입출력 버퍼 크기 변경

//tcp socket 생성
sock = socket(PF_INET, SOCK_STREAM, 0);

//입출력 버퍼의 크기를 SO_RCVBUF와 SO_SNDBUF 옵션으로 수정한다.
state = setsockopt(sock, SOL_SOCKET, SO_RCVBUF, (void*)&rcv_buf, sizeof(rcv_buf));
state = setsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, sizeof(snd_buf));

//출력 버퍼 크기를 getsockopt의 SO_SNDBUF 옵션으로 가져온다.
len = sizeof(snd_buf);
state = getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (void*)&snd_buf, &len);
if (state) error_handling("getsockopt() error");

//입력 버퍼 크기를 getsockopt의 SO_RCVBUF 옵션으로 가져온다.
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);

소켓의 입출력 버퍼 크기 확인/변경  →  SOL_SOCKET의 SO_RCVBUF와 SO_SNDBUF

(확인이든 변경이든 옵션명은 같음)

 

참고로, 입출력 버퍼의 크기를 변경하는 위 코드는 완벽하게 원하는 바대로 작동하진 않는다. 입출력 버퍼는 상당히 주의깊게 다뤄지는 영역이라 우리의 요구를 어느 정도는 반영해주지만 100% 정확하게 반영하진 않는다. setsockopt()로 버퍼 크기를 3000으로 변경해도 실제로 값을 찍어보면 6000으로 변경됐을 수도 있다.

 

 

 

 

예제 4 : SO_REUSEADDR 옵션

이 옵션을 이해하기 위해선 우선 Time-wait를 이해할 필요가 있다.

 

위 그림은 TCP의 연결이 종료될 때 4-way handshaking을 나타낸 것이다.

TCP는 데이터 손실에 대응하도록 돼있다, 패킷이 제대로 도달하지 않으면 이를 다시 보낸다. 예를들어 첫번째 FIN은 ACK로 답장이 안오면 다시 보내야 하고, 두번째 ACK가 host A에 제대로 도달하지 않았다면 host A에서 다시 FIN을 보낼 것이다 그러니 host A에서 FIN이 다시 온다면 ACK를 다시 보내야 한다. 세번째 FIN은 마지막 ACK가 도달하지 않는다면 다시 보내야 한다.

그럼 마지막에 host A가 보내는 ACK가 중간에 사라진다면 어떻게 해야할까? 그럼 host B에서 다시 FIN을 보낼 것이고, 이 신호를 보고서 다시 ACK를 보내야한다.

근데 그러려면 host A는 ACK를 보낸 뒤 무작정 연결을 종료하면 안된다. ACK를 보낸 후, 이를 제대로 전달 받지 못 한 host B에서 다시 FIN을 보내는지 잠깐 기다려야한다. 이게 Time-wait이다.

즉, 4-way handshaking의 첫번째 메시지를 보낸 host는 마지막 ACK를 보낸 후 일정 시간 Time-wait 상태이다.

Time-wait 상태의 문제점은, 그 시간동안 해당 소켓이 소멸하지 않아서 할당 받은 PORT를 다른 소켓이 사용할 수 없다는 점이다.

 

Time-wait는 필요하나, 실 서비스 중인 서버에선 문제가 될 수 있다.

SO_REUSEADDR option을 1로 설정하면, Time-wait를 무시하고 해당 소켓에 할당된 PORT를 할당 가능하게 만든다.(그렇다고 time-wait을 없애고 바로 종료해버리는 건 아니고, 일단 할당 가능하게 해놓고 혹시나 time-wait 기간에 상대방으로부터 다시 FIN이 날아오면 다시 ACK 보내든동 알아서 잘 처리하나 봄)

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

 

찾아보니까 이건 server socket에 주로 해당되는 말이더라.
사실 위 그림에서 host B도 ACK를 기다리긴해야하므로 양쪽 다 어느 정도 기다리긴 한다.
하지만 주로 server side에서만 Time-wait를 신경쓴다. 왜냐하면 client는 보통 해제된 port 번호를 랜덤으로 가져와서 쓰기 때문이다. 하지만 서버는 정해진 포트가 있어야 client에서 연결 요청을 할 것이므로 아무 포트나 쓰는게 안된다. 그래서 SO_REUSEADDR을 1로 설정하는건 server side에서나 큰 의미가 있다고들 함.

 

 

 

 

예제 5 : TCP_NODELAY 옵션

이 옵션을 이해하기 위해선 우선 Nagal Algorithm을 이해할 필요가 있다.

 

우측처럼 ACK를 받지 않고 상대 window size(버퍼 사이즈)만 넘지 않게 일단 데이터를 막 보낼 수 있다. 그럼 host B에선 데이터를 빠르게 받아볼 수 있다.

하지만 이렇게 하면 인터넷에 과도한 트래픽이 생기고 그로인한 전송속도 저하가 발생할 수 있다.

따라서 소켓은 기본적으로 좌측처럼 Nagle 알고리즘을 이용해 ACK가 와야지만 data를 보낼 수 있도록 한다.

이 Nagle 알고리즘을 확인하거나 ON/OFF 할 수 있는 옵션이 TCP_NODELAY 옵션이다.

 

Q. 위 예제에서 왜 트래픽이 증가하는지 모르겠다, 결국 Nagle을 적용해도 ACK는 다 받아야하지않나?
A. 우측과 좌측을 비교해보면 같은 시간이 흘러도 우측의 경우가 훨씬 많은 패킷이 오간다는 것을 알 수 있다. 특정 시간에 좌측의 경우 4개의 패킷만 오가는 반면 우측은 10개나 오가니 트래픽이 증가하는 것이다.

※ 데이터 특성에 따라 Negal algorithm을 적용하는게 좋은지 안 좋은진 모른다.
일반적인 경우 적용하지 않으면 데이터 전송속도 향상엔 좋지만, 네트워크 트래픽엔 좋지 않다. 무작정 Negal을 중지하는건 트래픽에 큰 부담을 주므로 좋지 않은 결과를 얻을 수 있다.
하지만 예를들어 용량이 큰 파일 데이터의 경우 Negal을 적용하지 않는게 패킷 수도 크게 증가시키지 않아서 트래픽이 많이 나빠지지도 않고, 데이터 전송속도는 좋아진다.

 

 

//Nagle 알고리즘 중단  //NODELAY를 true로 하는거니까 Nagle은 중단되는 것
int opt_val = 1;
setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&opt_val, sizeof(opt_val));

//Nagle 알고리즘 설정 상태 확인
int opt_val;
socklen_t opt_len;
opt_len = sizeof(opt_val);
getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void*)&opt_val, &opt_len);