본문 바로가기
컴퓨터 기초/TCP&IP

12.멀티플렉싱

by 인생여희 2020. 8. 14.

#.멀티플렉싱 기반의 서버


멀티프로세스 서버의 단점과 대안


이전 Chapter에서는 다중접속 서버의 구현을 위해 클라이언트의 연결요청이 있을 때마다 새로운 프로세스를 생성하였다. 이는 실제 사용되는 방법이지만 문제가 전혀 없는 방법은 아니다. 프로세스의 생성에는 상당히 많은 대가를 지불해야 하기 때문이다. 많은 양의 연산이 요구되며, 필요한 메모리 공간도 비교적 큰 편이다. 또한 프로세스마다 별도의 메모리 공간을 유지하기 때문에 상호간에 데이터를 주고받으려면 다소 복잡한 방법을 택할 수밖에 없다.
프로세스의 생성을 동반하지 않으면서 다수의 클라이언트에게 서비스를 제공할 수 있는 방법으로 IO멀티플렉싱 서버가 있다. 하지만 이 모델은 구현하고자 하는 서버의 특성에 따라서 구현방법이 달리 결정되어야 한다. 즉 이 방법이 모든 경우에 있어서 최선은 아니다.

 

#.멀티플렉싱이라는 단어의 이해

• 하나의 통신 채널을 통해서 둘 이상의 데이터(시그널)를 전송하는데 사용되는 기술

• 물리적 장치의 효율성을 높이기 위해서 최소한의 물리적인 요소만 사용해서 최대한의 데이터를 전달하기 위해 사용되는 기술

• 멀티플렉싱의 개념을 서버에 적용하기

 

서버에 멀티플렉싱 기술을 도입해서 필요한 프로세스의 수를 줄일 수 있다.

 

 

• 위 모델에 멀티플렉싱 기술을 적용하면 다음과 같이 프로세스의 수가 줄어든다.

 

 

#.select 함수의 이해와 서버의 구현
select 함수의 기능과 호출 순서
select 함수를 사용하면 한곳에 여러 개의 파일 디스크립터를 모아놓고 동시에 이들을 관찰할 수 있다. 이때 관찰할 수 있는 항목은 다음과 같다.

 

수신한 데이터를 지니고 있는 소켓이 존재하는가?

• 블로킹되지 않고 데이터의 전송이 가능한 소켓은 무엇인가?

• 예외 상황이 발생한 소켓은 무엇인가?

 

select 함수의 호출방법과 순서는 다음과 같다.

 

 

#.파일 디스크립터의 설정
select 함수를 사용하면 여러 개의 파일 디스크립터를 동시에 관찰할 수 있다고 하였다. 물론 파일 디스크립터의 관찰은 소켓의 관찰로 해석할 수 있다. 그렇다면 먼저 관찰하고자 하는 파일 디스크립터를 모아야 한다. 모을 때도 관찰항목에 따라서 구분해서 모아야 한다.
파일 디스크립터를 세 묶음으로 모을 때 사용하는 것이 fd_set형 변수이다. 이는 다음 그림에서 보이듯이 0과 1로 표현되는, 비트단위로 이뤄진 배열이라고 생각하면 된다.

 

 

이 비트가 1로 설정되면 해당 파일 디스크립터가 관찰의 대상임을 의미한다. fd_set형 변수에 값을 등록하거나 변경하는 등의 작업은 다음 매크로 함수들의 도움을 통해 이뤄진다.

 

- FD_ZERO(fd_set* fdset)

인자로 전달된 주소의 fd_set형 변수의 모든 비트를 0으로 초기화

 

FD_SET(int fd, fd_set* fdset)

매개변수 fdset으로 전달된 주소의 변수에 매개변수 fd로 전달된 파일 디스크립터 정보를 등록

 

FD_CLR(int fd, fd_set* fdset)

매개변수 fdset으로 전달된 주소의 변수에서 매개변수 fd로 전달된 파일 디스크립터 정보를 삭제

 

FD_ISSET(int fd, fd_set* fdset)

매개변수 fdset으로 전달된 주소의 변수에 매개변수 fd로 전달된 파일 디스크립터 정보가 있으면 양수를 반환

위의 함수들 중에서 FD_ISSET은 select 함수의 호출결과를 확인하는 용도로 사용된다.

 

 

 

#.검사(관찰)의 범위지정과 타임아웃의 설정

 

#include <sys/select.h>
#include <sys/time.h>
int select( int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, const struct timeval* timeout);

 

- 성공 : 0 반환

- 실패 : -1 반환

 

• int maxfd
검사 대상이 되는 파일 디스크립터의 수

 

• fd_set* readset
fd_set형 변수에 '수신된 데이터의 존재여부'에 관심 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달

 

• fd_set* writeset
fd_set형 변수에 ‘블로킹 없는 데이터 전송의 가능여부'에 관심 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달

 

• fd_set* exceptset
fd_set형 변수에 '예외상황의 발생여부'에 관심이 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소 값을 전달

 

• const struct timeval* timeout
select 함수 호출 이후에 무한정 블로킹 상태에 빠지지 않도록 타임아웃(time-out)을 설정하기 위한 인자를 전달

 

반환 값
오류 발생시에는 -1 반환
타임 아웃에 의한 반환 시에는 0이 반환

 

관심 대상으로 등록된 파일 디스크립터에 해당 관심에 관련된 변화가 발생하면 0보다 큰 값이 반환되는데 이 값은 변화가 발생한 파일 디스크립터의 수를 의미한다

 

select 함수는 세가지 관찰항목의 변화를 확인하는데, 이 세가지 관찰항목별로 fd_set형 변수를 선언해서 파일 디스크립터 정보를 등록하고, 이 변수의 주소 값을 위 함수의 두 번째, 세 번째 그리고 네 번째 인자로 전달하게 된다. 그런데 이에 앞서 다음 두 가지를 먼저 결정해야 한다.

 

• 파일 디스크립터의 관찰(검사) 범위는 어떻게 되는지?

• select 함수의 타임아웃 시간을 어떻게 할지?

 

이 중 첫 번째 파일 디스크립터의 관찰(검사) 범위는 select 함수의 첫번째 매개변수와 관련이 있다. 따라서 fd_set형 변수에 등록된 파일 디스크립터의 수를 확인할 필요가 있는데 파일 디스킯터의 값은 생성될 때마다 1씩 증가하기 때문에 가장 큰 파일 디스크립터의 값에 1을 더해서 전달하면 된다.

 

두 번째 select 함수의 타임아웃 시간은 select 함수의 마지막 매개변수와 관련이 있는데 매개 변수 선언에 보이는 자료형 timeval 구조체 기반의 자료형으로 다음과 같이 정의되어 있다.

 

struct timeval

{

long tv_sec; // second

long tv_usec; // microseconds

}

 

select 함수는 관찰중인 파일 디스크립터에 변화가 생겨야 반환을 한다. 때문에 변화가 생기지 않으면 무한정 블로킹 상태에 머물게 된다. 바로 이러한 상황을 막기 위해서 타임아웃을 지정하는 것이다. 타임아웃을 설정하고 싶지 않을 경우 NULL을 인자로 전달하면 된다.

 

#.select 함수호출 이후의 결과확인

0이 아닌 양수가 반환이 되면 그 수만큼 파일 디스크립터에 변화가 발생했음을 의미한다.

select 함수가 양의 정수를 반환한 경우, 변화가 발생한 파일 디스크립터는 어떻게 알아낼 수 있을까? select 함수의 두 번째, 세 번째 그리고 네 번째 인자로 전달된 fd_set형 변수에 다음 그림에서 보이는 변화가 발생하기 때문에 어렵지 않게 알아낼 수 있다.

 

 

select 함수 호출이 완료되고 나면 select 함수의 인자로 전달된 fd_set형 변수에 변화가 생긴다. 1로 설정된 모든 비트가 다 0으로 변경되지만 변화가 발생한 파일 디스크립터에 해당하는 비트만 그대로 1로 남아있게 된다. 때문에 1로 남아있는 위치의 파일 디스크립터에서 변화가 발생했다고 판단할 수 있다.

 

select 함수를 호출하는 예제의 확인

#include <stdio.h>
#include <unistd.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 30
int main(int argc, char *argv[])
{
fd_set reads, temps;
int result , str_len;
char buf[BUF_SIZE];
struct timeval timeout;
//fd_set 형 변수의 모든 비트를 0으로 초기화
FD_ZERO(&reads);
//표준입력을 관심을 가지고 보겠다는뜻.
//매개변수 fd_set으로 전달된 주소의 변수에 배개변수 0 으로 전달된 파일 디스크립터 정보를 등록한다.
// 0 : 표준 입력
FD_SET(0,&reads);
while (1) {
temps = reads;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
//파일 디스크립터에 변화가 발생하지 않아도 지정된 시간이 지나면 함수가 반환을 하는데, 반환값은 0 이다.
result = select(1, &temps, 0, 0, &timeout);
if (result == -1) {
puts("select error!");
break;
}else if(result == 0){
puts("time out!");
//콘솔로 부터 데이터가 입력되면 0보다 큰 수가 반환된다.
//변화를 보인 파일 디스크립터가 표준입력이 맞는지 확인하고,
//맞으면 표준입력으로 부터 데이터를 읽어서 콘솔로 데이터를 출력하고 있다.
}else{
int check = FD_ISSET(0, &temps);
printf("check : %d \n" , check);
if (check) {
str_len = read(0,buf,BUF_SIZE);
buf[str_len] = 0;
printf("message from console : %s" , buf);
}
}
}
return 0;
}
view raw select.c hosted with ❤ by GitHub

결과

 

 

 

 

#.멀티플렉싱 서버의 구현

 

echo_selectserv.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
#define BUF_SIZE 100
void error_handling(char *buf);
int main(int argc, char *argv[])
{
int serv_sock, clnt_sock;
struct sockaddr_in serv_adr, clnt_adr;
struct timeval timeout;
fd_set reads, cpy_reads;
socklen_t adr_sz;
int fd_max, str_len, fd_num , i;
char buf[BUF_SIZE];
//실행파일 경로 /port 번호를 입력받아야 함.
if (argc != 2) {
printf("usage : %s <port>\n" , argv[0]);
exit(1);
}
//IPv4, TCP 소켓 생성
serv_sock = socket(PF_INET, SOCK_STREAM,0);
//서버 주소 정보 초기화
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)) == -1) {
error_handling("bind error");
}
//클라이언트로부터 연결 요청을 수락할 준비 완료 (진정한 서버 소켓이됨)
if(listen(serv_sock , 5) == -1){
error_handling("listen error");
}
FD_ZERO(&reads); //fd_set형 변수를 모두 0으로 초기화
FD_SET(serv_sock, &reads); //서버소켓을 관심 대상으로 설정
fd_max = serv_sock;
printf("fd_max : %d \n" , fd_max);
while (1) {
cpy_reads = reads; //원본 손실 방지를 위해 복사본으로 select 함수 호출 진행
//타임아웃 시간 설정 - 무한 블락킹 방지
timeout.tv_sec = 5;
timeout.tv_usec = 5000;
// -1 반환시 오류 발생, 데이터 수신과 관련하여 관심을 가짐
if ((fd_num = select(fd_max+1 , &cpy_reads , 0,0, &timeout)) == -1) {
break;
}
//0반환시 타임아웃
if (fd_num == 0) {
printf("time out.... \n");
continue;
}
//관찰대상의 모든 파일 디스크립터에 대해
for (i = 0; i < fd_max+1; i++) {
//해당 파일 디스크립터가 데이터를 수신하였는지 확인
//매개변수 &cpy_reads 으로 전달된 주소의 변수에
//매개변수 i로 전달된 파일 디스크립터 정보가 있으면 양수를 반환
if (FD_ISSET(i, &cpy_reads)) {
if (i == serv_sock)
{
printf("서버 소켓 부분 : %d \n" , i);
/*
클라이언트의 연결요청도 데이터 전송을 통해 이루어지므로
서버소켓에 수신된 데이터가 존재한다는 것은 클라이언트의 연결요청이 있었다는 의미
*/
printf("클라이언트 연결 대기.... \n");
adr_sz = sizeof(clnt_adr);
//클라이언트의 연결 수락
clnt_sock = accept(serv_sock , (struct sockaddr*)&clnt_adr,&adr_sz);
printf("연결된 clnt_sock : %d \n" , clnt_sock);
//클라이언트에게 서비스를 제공하기 위해 생성된 소켓의 파일 디스크립터 또한
//관심 대상에 추가!
FD_SET(clnt_sock, &reads);
//fd_max는 서버 소켓 디스크립터 번호이다. 보통 3 이고 클라이언트 소켓 디스크립터가 뒤에 생성되었으므로 더 크다.
if (fd_max < clnt_sock) {
fd_max = clnt_sock;
printf("connected client : %d \n" , clnt_sock);
}
}else{
//클라이언트의 메시지를 실제로 수신하는 소켓에 대해
//accept 함수로 생성된 소켓
printf("accept 함수로 생성된 소켓 부분 : %d \n" , i);
//클라이언트로 부터 데이터 수신
str_len = read(i , buf, BUF_SIZE);
//수신한 데이터가 없으므로, 클라이언트의 연결 종료를 의미함.
if (str_len == 0) {
//클라이언트가 종료했으므로 이 소켓 또한 관심대상에서 제외하고
FD_CLR(i,&reads);
//이 소켓의 연결도 종료
close(i);
printf("close client %d \n" , i);
}else{
//수신한 문자열을 다시 클라이언트로 에코
write(i, buf, str_len);
}
}
}//if (FD_ISSET(i, &cpy_reads)) -- end
}
}
close(serv_sock);
return 0;
}
//에러처리
void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
view raw s.c hosted with ❤ by GitHub

client.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void error_handling(char *message);
int main(int argc, char *argv[])
{
int sock;
char message[BUF_SIZE];
int str_len;
struct sockaddr_in serv_adr;
if(argc!=3) {
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
sock=socket(PF_INET, SOCK_STREAM, 0);
if(sock==-1)
error_handling("socket() error");
memset(&serv_adr, 0, sizeof(serv_adr));
serv_adr.sin_family=AF_INET;
serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
serv_adr.sin_port=htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
error_handling("connect() error!");
else
puts("Connected...........");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
break;
write(sock, message, strlen(message));
str_len=read(sock, message, BUF_SIZE-1);
message[str_len]=0;
printf("Message from server: %s", message);
}
close(sock);
return 0;
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
view raw c.c hosted with ❤ by GitHub

결과

 

 

 

 

 

 

  [출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어

'컴퓨터 기초 > TCP&IP' 카테고리의 다른 글

14.멀티캐스트 & 브로드캐스트  (0) 2020.08.18
13.다양한 입출력 함수들  (0) 2020.08.18
11.프로세스간 통신  (0) 2020.08.14
10.멀티프로세스 기반의 서버구현  (0) 2020.08.13
9.소켓의 다양한 옵션  (0) 2020.08.12