모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!
# 소켓 프로그래밍에서 중요한 것
1) 데이터의 송수신
2) 소켓이 지니는 다양한 특성을 파악 후, 필요에 맞게 특성을 변경하는 것
# 소켓의 다양한 옵션은 계층별로 분류됨
Protocol Level
1) SOL_SOCKET : 가장 일반적인 옵션
2) IPPROTO_IP : IP 프로토콜과 관련
3) IPPROTO_TCP : TCP 프로토콜과 관련
# 소켓의 거의 모든 옵션은 설정상태의 참조(Get) 및 변경(Set)이 가능
- 물론 참조만, 또는 설정만 가능한 가능한 옵션도 존재함
# 옵션의 참조(Get)
#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로 전달된 주소값의 버퍼크기를 담고있는 변수의 주소값,
함수호출이 완료되면 이 변수에는 네번째 인자를 통해 반환된 옵션정보의 크기가 바이트 단위로 계산되어 저장됨
# 옵션의 변경(Set)
#include <sys/socket.h>
int setsockopt(int sock, int level, int optname, const void* optval, socklen_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 *meesage);
int main(int argc, char *argv[])
{
int tcp_sock, udp_sock;
int sock_type;
socklen_t optlen;
int state;
optlen=sizeof(sock_type);
tcp_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
udp_sock=socket(PF_INET,SOCK_DGRAM,0); // UDP 소켓 생성
printf("SOCK_STREAM: %d \n",SOCK_STREAM); // 상수 1 출력
printf("SOCK_DGRAM: %d \n",SOCK_DGRAM); // 상수 2 출력
state=getsockopt(tcp_sock,SOL_SOCKET,SO_TYPE,(void*)&sock_type,&optlen);
// TCP 소켓의 소켓 타입 정보 확인
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);
// UDP 소켓의 소켓 타입 정보 확인
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(EXIT_FAILURE);
}
# 소켓의 타입정보 확인을 위한 "옵션 SO_TYPE"은 확인(Get)만 가능하고 변경(Set)은 불가능한 대표적인 옵션
=> "소켓의 타입"은 소켓 생성시 한번 결정되면 더이상 변경이 불가능하다
# 소켓이 생성되면, 기본적으로 입력버퍼와 출력버퍼가 생성됨
1) SO_RCVBUF : 입력버퍼 크기와 관련된 옵션
2) SO_SNDBUF : 출력버퍼 크기와 관련된 옵션
- 두 옵션을 통해 입출력버퍼의 크기 참조 및 변경 가능
- setsockopt 함수호출을 통해 입출력버퍼의 크기를 우리가 완벽하게 조정은 할 수 없지만, 어느정도 반영은 된다.
# set_buf.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/socket.h>
void error_handling(char *meesage);
int main(int argc, char *argv[])
{
int sock;
int snd_buf=1024*3, rcv_buf=1024*3, state;
socklen_t len;
sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
state=setsockopt(sock,SOL_SOCKET,SO_SNDBUF,(void*)&snd_buf,sizeof(snd_buf));
// 출력버퍼 크기에 대한 요구사항을 전달
if(state)
error_handling("setsockopt() error");
state=setsockopt(sock,SOL_SOCKET,SO_RCVBUF,(void*)&rcv_buf,sizeof(rcv_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("setsockopt() error");
len=sizeof(rcv_buf);
state=getsockopt(sock,SOL_SOCKET,SO_RCVBUF,(void*)&rcv_buf,&len);
// 입력버퍼 크기에 대한 정보를 참조
if(state)
error_handling("setsockopt() 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(EXIT_FAILURE);
}
# 소켓이 FIN 메시지를 전달하는 경우
1) close() 함수를 호출하는 경우
2) CRTL+C 를 입력하는 경우
3) 프로그램을 강제종료하는 경우
# 연결지향형 소켓의 연결 해제과정인 four-handshaking 과정에서
(먼저 FIN 메시지를 보낸) 소켓은 바로 소멸되는 것이 아니라, time-wait 상태라는 것을 일정시간 거치게 됨
# four-handshaking
1) A소켓이 연결을 종료하겠다고 알림
2) B소켓이 이에 대해 알겠다고 알림
3) B소켓이 본인도 연결을 종료하겠다고 알림
4)
# time-wait 상태
연결의 종료를 요청한 해당 소켓은 반드시 time-wait 상태를 거친다.
그러나, 서버와 다르게 클라이언트의 time-wait 상태는 신경써주지 않아도 되는 이유는,
클라이언트 소켓의 PORT번호는 임의로 할당되기 때문이다.
# time-wait 상태의 존재 이유
1) FIN 메시지를 먼저 보낸 소켓(A)이 보낸, 마지막 ACK 메시지가 어떠한 이유로 B소켓에 제대로 전달되지 않으면
2) 이에 B소켓은 자신이 보낸 FIN 메시지가 A소켓에 제대로 전달되지 않았다고 여기고, 다시 FIN 메세지를 전송
3) 이때 A소켓이 이미 소멸해버렸다면, A소켓이 이 메시지를 받지 못하므로 B소켓은 영영 마지막 ACK 메시지를 받지 못하게 됨 => 이러한 경우를 방지하기 위해 A소켓의 소멸시간을 뒤로 미루게 됨(time-wait)
4) time-wait 타이머 시간 안에 B소켓으로부터 다시 FIN 메시지를 A소켓이 받으면,
타이머를 재가동시키고 다시 마지막 ACK 메시지를 B소켓으로 보낸다.
5) A소켓의 마지막 ACK 메시지를 받으면 B소켓이 소멸하고, 타이머 시간이 되면 A소켓이 소멸한다.
# time-wait의 장점도 있지만, 단점도 있다.
- 비정상적인 서버 종료 이후에, 서버를 곧바로 재가동시킬 수 없기 때문(해당 포트의 서버 소켓이 살아있으므로)
# time-wait 시간은 네트워크 상황에 따라 얼마든지 더 길어질 수 있다.
- 이를 해결하려면 "SO_REUSEADDR의 상태를 변경" 하면 된다.
- SO_REUSEADDR의 DEFAULT = 0(FALSE) : time-wait 상태에 있는 소켓의 PORT번호는 할당이 불가능함을 의미
- SO_REUSEADDR = 1(TRUE) 로 변경하면 해결책이 된다.
# reuseadr_eserver.c
- Ch04의 echo_client.c 와 같이 실행하시면 됩니다.
- 옵션변경하는 부분을 주석처리하고 먼저 실행해보고, 그다음 주석해제 후 실행하면 차이점이 보입니다.
- 단, 서버 쪽에서 먼저 CTRL+C 를 눌러 FIN 메시지를 전송하게 해야 차이점을 확인할 수 있습니다.
#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 *meesage);
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) // 실행파일의 경로/PORT번호 를 인자로 받아야 함
{
printf("Usage : %s <port> \n",argv[0]);
exit(EXIT_FAILURE);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
if(serv_sock==-1)
error_handling("socket() error");
// time-wait 상태의 소켓의 포트번호도 할당가능하게 변경
// 이 과정을 해주지 않으면 서버쪽에서 먼저 FIN 메시지를 전달하게 하는 경우
// 한동안 해당 포트번호로 서버소켓을 생성할 수 없게 됨
optlen=sizeof(option);
option=TRUE; // TRUE = 1 로 매크로 설정함
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 sockaddr*)&serv_adr,sizeof(serv_adr)))
error_handling("bind() error");
/* 진정한 서버소켓이 되는 과정
연결요청 대기큐가 생성되고, 이 함수 호출 이후부터
클라이언트의 연결요청(connect 함수 호출)이 가능해짐
*/
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); // 여기서 1은 표준출력의 파일 디스크립터, 즉 모니터에 출력을 의미
}
close(clnt_sock);
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE);
}
# Nagle 알고리즘
- 네트워크 상에서 돌아다니는 패킷들의 흘러 넘침을 막기 위해 1984년에 제안된 알고리즘
- TCP 상에서 적용되는 매우 단순한 알고리즘
- 앞서 전송한 데이터에 대한 ACK 메시지를 받아야만, 그 다음 데이터를 전송하는 알고리즘
- TCP는 기본적으로 Nagle 알고리즘 적용하여 데이터 송수신
- ACK가 수신될 때까지 최대한 버퍼링을 해서 데이터를 전송함
- Nagle 알고리즘을 적용하지 않으면 네트워크 트래픽에 좋지 않은 영향을 미치므로,
(1바이트를 전송하더라도 패킷에 포함되는 헤더정보의 크기는 수십바이트에 이른다)
효율적인 네트워크 사용을 위해서는 Nagle 알고리즘을 반드시 적용해야 함
# Nagle 알고리즘의 단점?
- 전송하는 데이터 특성에 따라, Nagle 알고리즘의 적용여부에 따른 트래픽의 차이가 크지 않으면서
Nagle 알고리즘을 적용하는 것보다 데이터의 전송이 빠른 경우도 있음
e.g. "용량이 큰 파일 데이터의 전송"
파일 데이터를 출력버퍼로 밀어넣는 작업은 시간이 거의 걸리지 않으므로,
Nagle을 적용하지 않아도 출력버퍼를 거의 꽉채운 상태에서 패킷을 전송하게 됨.
=> 패킷 수가 크게 증가하지도 않고, ACK를 기다리지 않고 연속해서 데이터를 전송하여 전송속도도 크게 향상함
# 일반적으로 Nagle 알고리즘을 적용하면 속도의 향상을 기대할 수 있으나,
데이터의 특성을 정확히 판단후, 더 좋은 방법이 있다면 중단한다.
# Nagle 알고리즘의 중단
- "Nagle 알고리즘의 적용 여부에 따른 트래픽의 차이가 크지 않으면서도,
Nagle 알고리즘을 적용하는 것보다 데이터 전송이 빠른 경우" 중단
- Nagle 알고리즘 중단 방법
int opt_val = 1(TRUE);
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);
// opt_val = 0(FALSE) 이면, Nagle 알고리즘 설정상태
// opt_val = 1(TRUE) 이면, Nagle 알고리즘 해제상태
[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어
'Programming > 열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)' 카테고리의 다른 글
Ch 10. 멀티프로세스 기반의 서버구현 (0) | 2020.08.08 |
---|---|
Ch 09. 내용 확인문제 (0) | 2020.08.08 |
Ch 08. 내용 확인문제 (0) | 2020.08.08 |
Ch 08. 도메인 이름과 인터넷 주소 (0) | 2020.08.08 |
Ch 07. 내용 확인문제 (0) | 2020.08.08 |