여러 클라이언트가 동시에 접속할 수 있도록 멀티 프로세스를 만들고 소켓 프로그래밍과 연결한다.
[ TCP ]
TCP는 TCP 상위 레이어에 신뢰성 제공을 위해 패킷을 잃어버릴 경우 재전송하거나, 여러 패킷이 동시에 전송될 경우
패킷 순서를 조합한다. 신뢰성 제공을 하다보니 ACK에 의한 시간 지연이 생기게 된다.
반면에 UDP는 상대적으로 시간 지연이 TCP보다 적다. 시간 지연에 제일 영향을 받는 것은 streaming 서비스이다.
실시간 멀티미디어 데이터 통신 서비스는 streaming으로 계속 날라가 줘야하는 서비스인데 재 전송 요청에 따른 서비스
지연이 발생해서는 안된다.
또한 TCP는 신뢰성 제공을 위해 연결( connection )을 관리하지만 overhead가 발생한다.
[ UDP의 특징과 장점 ]
UDP( User Datagram Protocol )은 전송 패킷에 대한 응답을 기다리지 않는다. 그러다보니 UDP는 패킷이 제대로
전송이 됐는지 오류가 없는지 확인할 수 없어 신뢰할 수 없다. 대신에 신뢰성을 희생해서 높은 성능을 확보한다.
즉, 품질과 신뢰성을 중시하는 서비스는 TCP를 이용하고, 품질과 신뢰성이 그다지 중요하지 않은 서비스이고, 연속성을
중시하는 서비스는 UDP를 사용하는 것이 좋다.
[ TCP로 멀티미디어 데이터 다루기 ]
UDP에서만 멀티미디어 데이터가 다뤄지는 것은 아니다. 품질과 연속성을 모두 다루기 위해서는 TCP에서 멀티미디어를
다루는 기법을 사용할 때도 있다. → 버퍼링
버퍼링이 TCP로 멀티미디어 데이터를 다루는 방식이다. 멀티미디어 데이터를 관리하는 버퍼( 응용 프로그램 내의 사용자 버퍼 )를 운영한다. 대략적인 지연 시간을 계산해서 예상되는 지연 시간 만큼의 데이터를 서비스 초기에 미리
받아놓는다.
* 버퍼는 OS 내부의 버퍼와 응용프로그램 내부의 사용자 버퍼가 존재한다. 멀티미디어 데이터는 인터넷에서 바로
데이터 버퍼로 받아오는 것이 아니라 OS 내의 버퍼로 먼저 들어가고 데이터 버퍼로 들어가게 된다
( OS 버퍼만 사용할 경우 굉장히 끊김이 심하기 때문에 완충 역할을 하는 데이터 버퍼를 사용자 버퍼에 둬서 사용한다. )
버퍼 사이즈가 크면 클수록 끊어지는 확률은 적지만 버퍼의 처음부터 끝까지 latency(지연 시간)가 커지게 된다.
→ tradeoff가 존재한다.
[ UDP 소켓 만들기 ]
socket( AF_INET, SOCK_DGRAM, 0 );
socket( AF_INET, SOCK_DGRAM, IPPROTO_UDP );
* SOCK_DGRAM : UDP 통신
SOCK_STREAM : TCP 통신
* 0은 default를 의미한다. SOCK_DGRAM일 때의 default protocol은 IPPROTO_UDP이다.
[ UDP 서버 프로그램 흐름 ]
UDP는 connection이 없다는 것이 핵심이다. connection이 필요없기 때문에 listen, accept 가 필요 없고 socket과
bind 함수만 필요하다.
* socket : UDP 소켓 생성
* bind : socket을 port number, IP address에 묶어준다.
* listnen : 바로 통신할 수 없는 들어오는 클라이언트를 FIFO queue에 넣어준다.
* accept : 기다리다가 연결이 클라이언트로 부터 들어오면 받아들인다.
즉, UDP는 bind한 다음에 바로 보내고 받는다. 하지만 connection 과정이 없기 때문에 보내는 packet이 어디로
가는지, 오는 packet이 어디서부터 온 것인지 알 수 없다. 그래서 sendto()와 recvfrom()이라는 시스템 콜을 별도로
만든다.
write() 대신에를 사용하고 read() 대신에 recvfrom()을 사용한다. sendto()와 recvfrom()은 패킷마다 주소가
들어가는 전제로 하고 있다. 주소가 있어야 데이터를 클라이언트한테 전달을 할 수 있고 데이터를 클라이언트에게
받을 수 있다.
[ UDP 통신 함수 사용하기 ]
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(1234);
state = bind(sockfd, (struct sockaddr *)&serveraddr,
sizeof(serveraddr));
bind 함수의 목적은 port 번호와 기다릴 IP 주소를 bind하므로 TCP 소켓의 bind와 사용 방법에 차이가 없다.
recvfrom(sockfd, buf, MAXLINE, 0,
(struct sockaddr *)&cliaddr, &clilen);
sendto(sockfd, buf, strlen(buf), 0,
(struct sockaddr *)&cliaddr, sizeof(cliaddr));
connection이 없기 때문에 모든 데이터 송수신에 있어 IP 주소와 port 번호가 들어가 있는 구조체가 필요하다.
sendto()의 구조체는 패킷을 해당 주소로 보내겠다는 의미이고, recvform()의 구조체는 패킷을 받고 보니까
이 주소이다 라는 의미이다.
즉, IP 주소 정보를 위해서 sockaddr 구조체를 사용한다.
[ UDP 클라이언트 프로그램 흐름 ]
서버에는 listen()과 accpet()를 사용하지 않으면 연결을 맺지 않으므로 클라이언트에서도 connect 함수를 사용하지
않아도 된다. 클라이언트 입장에서는 socket()에서 sendto(), recvfrom()으로 바로 넘어가면 된다.
하지만 데이터를 매번 보낼 때 마다 패킷 마다 주소를 전달해줘야 하는 번거로움이 있어 connect 함수가 사용되기도
한다. 실제로 connect 함수를 연결하기 위해 사용하는 것이 아니다. UDP에서 connect 함수를 사용한다는 것은 패킷을
보낼 서버의 주소를 명시하기 위해 사용하는 것이다.
* connect() : 클라이언트가 서버로 접속하기 위해 사용하는 시스템 콜
서버의 주소를 명시해놓는다면 sendto()에 NULL을 전달해주면 된다.
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr("111.111.111.111");
addr.sin_port = htons(8081);
connect(sockfd, &addr, sizeof(addr));
// 111.111.111.111:8081로 데이터를 전송
sendto(sockfd, buf, strlen(buf), 0, NULL, len);
IP 주소와 port 번호를 미리 명시해놓고 connect를 해놓으면 주소 필드에 NULL을 전달해줘도 연결이 된다.
[ calc_linux_server.c와 calc_linux_cli.c 확인하기 ]
1. calc_linux.server.c
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT_NUM 3800
#define MAXLEN 256
struct cal_data
{
int left_num;
int right_num;
char op;
int result;
short int error;
};
int main(int argc, char **argv)
{
int sockfd;
socklen_t addrlen;
int cal_result;
int left_num, right_num;
struct sockaddr_in addr, cliaddr;
struct cal_data rdata;
if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
return 1;
}
memset((void *)&addr, 0x00, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(PORT_NUM);
addrlen = sizeof(addr);
if(bind(sockfd, (struct sockaddr *)&addr, addrlen) == -1)
{
return 1;
}
while(1)
{
addrlen = sizeof(cliaddr);
recvfrom(sockfd, (void *)&rdata, sizeof(rdata), 0,
(struct sockaddr *)&cliaddr, &addrlen);
#if DEBUG
char *ptr = (char *) &rdata;
for(int i=0; i< sizeof(rdata); i++)
printf("%02x ", *(ptr+i) & 0xFF);
printf("\n");
printf("Client Info : %s (%d)\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
printf("%02x %c %02x\n", ntohl(rdata.left_num), rdata.op, ntohl(rdata.right_num));
#endif
left_num = ntohl(rdata.left_num);
right_num = ntohl(rdata.right_num);
switch(rdata.op)
{
case '+':
cal_result = left_num + right_num;
break;
case '-':
cal_result = left_num - right_num;
break;
case '*':
cal_result = left_num * right_num;
break;
case '/':
if(right_num == 0)
{
rdata.error = htons(2);
break;
}
cal_result = left_num / right_num;
break;
}
rdata.result=htonl(cal_result);
#if DEBUG
for(int i=0; i< sizeof(rdata); i++)
printf("%02x ", *(ptr+i) & 0xFF);
printf("\n");
#endif
sendto(sockfd, (void *)&rdata, sizeof(rdata), 0,
(struct sockaddr *)&cliaddr, addrlen);
}
return 1;
}
recvfrom()의 주소 필드인 ( struct sockaddr * )& cliadder은 패킷을 받고 보니 이 주소였다는 의미이고, sendto()의
주소 필드인 ( struct sockaddr * )& cliadder은 패킷을 보낸 클라이언트에 다시 데이터를 전송해주는 주소이다.
#if DEBUG
char *ptr = (char *) &rdata;
for(int i=0; i< sizeof(rdata); i++)
printf("%02x ", *(ptr+i) & 0xFF);
printf("\n");
printf("Client Info : %s (%d)\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
printf("%02x %c %02x\n", ntohl(rdata.left_num), rdata.op, ntohl(rdata.right_num));
#endif
소프트웨어는 개발 버전과 배포 버전이 다르다. 배포 버전에서는 개발 과정에서 필요했던 코드를 제외를 하고 배포를
해야하는 경우들이 존재한다.
→ 전처리 과정을 통해서 #if DEBUG와 #endif 사이에 있는 코드를 보고자 할 때는 컴파일할 때 -DDEBUG를
작성해줘야 한다.
* 전처리( pre-processing ) : 컴파일 이전에 수행 ( #define, #include )
gcc ... -D[프로그램이름]
컴파일 할 때 -D 옵션이 있으면 #if와 #endif 사이에 있는 코드를 실행하는 것이고 -D 옵션이 없다면 해당 코드가 없는
식으로 처리가 된다. 코드의 내용은 수정되지 않는다.
2. calc_linux_cli.c
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define PORT_NUM 3800
#define MAXLEN 256
struct cal_data
{
int left_num;
int right_num;
char op;
int result;
short int error;
};
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in addr;
struct cal_data sdata, recvaddr;
char msg[MAXLEN];
int left_num;
int right_num;
socklen_t addrlen;
char op[2];
if (argc != 2)
{
printf("Usage : %s [ipaddress]\n", argv[0]);
return 1;
}
if ( (sockfd = socket(AF_INET, SOCK_DGRAM, 0)) == -1 )
{
return 1;
}
memset((void *)&addr, 0x00, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = inet_addr(argv[1]);
addr.sin_port = htons(PORT_NUM);
while(1)
{
printf("> ");
fgets(msg, MAXLEN-1, stdin);
if(strncmp(msg, "quit\n",5) == 0)
{
break;
}
sscanf(msg, "%d%[^0-9]%d", &left_num, op, &right_num);
memset((void *)&sdata, 0x00, sizeof(sdata));
sdata.left_num = htonl(left_num);
sdata.right_num = htonl(right_num);
sdata.op = op[0];
addrlen = sizeof(addr);
sendto(sockfd, (void *)&sdata, sizeof(sdata), 0,
(struct sockaddr *)&addr, addrlen);
recvfrom(sockfd, (void *)&sdata, sizeof(sdata), 0,
(struct sockaddr *)&recvaddr, &addrlen);
printf("%d %c %d = %d\n", ntohl(sdata.left_num), sdata.op, ntohl(sdata.right_num), ntohl(sdata.result));
}
close(sockfd);
}
무한 루프에서 quit가 입력될 때까지 서버로 데이터를 요청하고 받는다. 데이터를 받으면 sendto()해서 서버로
데이터를 보낸다. 데이터를 받을 때는 recvfrom을 사용한다.
< 실습하기 >
1. 서버 실행하기
2. 클라이언트 실행하기
1과 2는 서버로 4바이트씩 전송이 되고 +가 2b로 전송이 되고 계산 결과도 3으로 4바이트로 전송이 되고 나머지는
에러이다. 지금은 에러가 없으므로 00 00 00 00이다. +가 2b로 전송되면 3 바이트가 남는데 이 3바이트는 구조체가
몇 바이트가 될 지 모르기 때문에 전송되는 값이다.
→ 컴퓨터 구조 상으로 메모리에 주소를 지정할 때 바이트 단위로 지정할 수 있지만 word 단위로도 지정할 수 있다.
즉 word 개념으로 서로 떨어져서 주소를 저장하는 것이 일반적이다.
* -D 옵션 없이 서버 소스코드를 컴파일 한다면 클라이언트로 부터 전달되는 값에 대한 메모리 주소를 확인할 수 없다.
'2CHAECHAE 학교생활 > OSNW실습' 카테고리의 다른 글
[ OS/NW 실습 ] 8주차 - 멀티 프로세스 소켓 프로그래밍 ② (1) | 2022.10.31 |
---|---|
[ OS/NW 실습 ] 8주차 - 멀티 프로세스 소켓 프로그래밍 ① (1) | 2022.10.31 |
[ OS/NW 실습 ] 7주차 - TCP 소켓 프로그래밍 (0) | 2022.10.30 |
[ OS/NW 실습 ] 7주차 - 바이트 순서( Byte Order ), 인터넷 주소와 도메인 (0) | 2022.10.29 |
[ OS/NW 실습 ] 6주차 - 소켓 네트워크 프로그램 이해 (0) | 2022.10.29 |