멀티플렉싱이란 하나의 통신채널을 통해 둘 이상의 데이터를 전송하는데 사용되는 기술입니다. 앞서 사용한 멀티프로세스와 달리 프로세스를 생성하지 않고 다수의 클라이언트에게 서비스를 제공할 수 있습니다.
멀티플렉싱 서버를 구현함에 있어서 가장 대표적인 방법으로는 select 함수를 사용하는 것입니다. select 함수를 호출해서 결과를 얻기까지의 과정을 간단하게 정리하면 다음과 같습니다.
1-1 파일 디스크립터의 설정
1-2 검사의 범위 지정
1-3 타임아웃의 설정
2-1 select 함수의 호출
3-1 호출 결과 확인
위 과정을 순서대로 소개하겠습니다.
1-1 파일 디스크립터 설정
select 함수를 사용하면 여러 개의 파일 디스크립터를 모와서 동시에 이들을 관찰할 수 있고, 관찰할 수 있는 항목(=이벤트)는 다음과 같습니다.
- 수신한 데이터를 지니고 있는 소켓이 존재?
- 블로킹되지 않고 데이터의 전송이 가능한 소켓은?
- 예외상황이 발생한 소켓은?
즉 관찰항목은 수신, 전송, 예외 세 가지가 있고 관찰을 하기 위해선 파일 디스크립터를 관찰항목에 따라 구분해서 모아야 합니다. 이때 파일 디스크립터를 세 묶음으로 모을 때 사용하는 것이 fd_set형 변수입니다. fd_set형 변수는 비트단위로 이뤄진 배열입니다. 각 인덱스의 비트가 1로 설정되면 그 인덱스에 해당하는 파일 디스크립터는 관찰대상이 되는 것이고, 0이면 관찰대상에서 제외되는 것입니다.
fd_set형 변수는 다음 매크로 함수를 통해 조작 할 수 있습니다.
FD_ZERO(fd_set * fdset) : fdset의 모든 비트를 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 파일디스크립터 정보가 있으면 양수를 반환
fdset에 정보가 등록되면 해당 비트가 1로 바뀌고, 정보가 제거되면 0으로 바뀌게 됩니다. fd_set 변수를 통해 1-1의 파일 디스크립터 설정을 마칠 수 있습니다.
1-2 검사의 범위 지정
관찰대상이 되는 파일 디스크립터의 개수를 뜻합니다. 파일 디스크립터의 값은 0부터 시작해서 생성될 때마다 1씩 증가하기 때문에 가장 큰 파일 디스크립터의 값에 1을 더해주면 됩니다. (참고: 0번 파일 디스크립터는 표준입력과 관련되어 있습니다.)
1-3 타임아웃의 설정
struct timeval{
long tv_sec; //seconds
long tv_usec; //microseconds
}
timeval은 select함수의 인자에 들어가는 구조체입니다. select 함수의 블로킹 상태가 일정 시간 이상 지속되지 않도록 타임아웃의 시간을 정해주는 용도입니다. 만약 타임아웃을 설정하고 싶지 않을 땐 NULL을 인자로 전달하면 됩니다.
2-1 select 함수의 호출, 3-1 호출 결과 확인
#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);
maxfd: 검사 대상이 되는 파일 디스크립터의 수
readset: 수신된 데이터의 존재여부에 관심있는 모든 파일 디스크립터 정보를 담은 fd_set형 변수의 포인터
writeset: 블로킹 없는 데이터 전송의 가능여부에 관심있는 모든 파일 디스크립터 정보를 담은 fd_set형 변수의 포인터
exceptset: 예외상황의 발생여부에 관심이 있는 모든 파일 디스크립터 정보를 담은 fd_set형 변수의 포인터
timeout: select함수 호출 이후 블로킹상태가 길어지지 않도록 타임아웃을 설정하기 위한 인자 전달
return값: 오류 발생 시 -1, 타임 아웃시 0, 정상 실행시 변화가 발생한 파일 디스크립터의 수
select 함수를 호출하면 변경된 파일 디스크립터의 개수를 반환해주고, 인자로 전달된 fd_set형 변수는 변경되지 않은 파일 디스크립터에 해당하는 비트는 0으로, 변경된 파일 디스크립터의 비트만 1로 남아있게 됩니다. 따라서 1로 변경된 파일 디스크립터만 변화가 발생했다고 볼 수 있습니다.
다음 예제 코드는 select 함수를 이용한 멀티플렉싱 기반 에코 서버입니다. 에코 클라이언트는 https://pupuduck.tistory.com/70에 작성된 예제 코드를 사용하면 됩니다.
#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];
if(argc!=2){
printf("Usage : %s <port>\n",argv[0]);
exit(1);
}
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(serv_sock, &reads);
fd_max=serv_sock;
while(1){
cpy_reads=reads;
timeout.tv_sec=5;
timeout.tv_usec=5000;
//select함수의 3번째, 4번째 인자가 0으로 채워져있는데, 이는 관찰의 목적에 맞게 reads만 사용한 것
if((fd_num=select(fd_max+1,&cpy_reads,0,0,&timeout))==-1)
break;
if(fd_num==0)
continue;
//select 함수가 1 이상 반환했을 때 실행되는 반복문
for(i=0; i<fd_max+1; i++){
// 수신된 데이터가 있는 소켓의 파일 디스크립터 탐색
if(FD_ISSET(i, &cpy_reads)){
//서버 소켓에서 변화가 있었는지 확인
//서버 소켓에서 변화가 있었을 시 연결요청에 대한 수락 과정 진행
if(i==serv_sock){
adr_sz=sizeof(clnt_adr);
clnt_sock=accept(serv_sock,(struct sockaddr*)&clnt_adr, &adr_sz);
FD_SET(clnt_sock, &reads);
if(fd_max<clnt_sock)
fd_max=clnt_sock;
printf("connected client: %d\n",clnt_sock);
}
//수신할 데이터가 있는 경우 실행
else{
str_len=read(i,buf,BUF_SIZE);
//EOF일 시 연결종료
if(str_len==0){
FD_CLR(i, &reads);
close(i);
printf("closed client: %d\n", i);
}
//데이터 에코시킴
else{
write(i,buf,str_len);
}
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *buf){
fputs(buf, stderr);
fputc('\n',stderr);
exit(1);
}
관찰의 대상에 서버 파일 디스크립터를 포함시키고, 만약 서버 파일 디스크립터에 변화가 있으면 연결 요청이 있는 것이므로 연결 요청의 수락을 진행해줍니다. 그리고 수신할 데이터가 있는 경우 EOF일 때와 아닌 경우를 구분해서 처리해주면 됩니다.
'현생 > TCP 소켓 프로그래밍' 카테고리의 다른 글
14. 멀티캐스트와 브로드캐스트 [TCP/IP][C][LINUX] (0) | 2022.02.11 |
---|---|
13. 다양한 입출력 함수 [TCP/IP][C][LINUX] (0) | 2022.02.05 |
11. 파이프 기반의 프로세스간 통신 [TCP/IP][C][LINUX] (0) | 2022.01.31 |
10-7 TCP의 입출력 루틴 분할 [TCP/IP][C][LINUX] (0) | 2022.01.30 |
10-6 멀티프로세스 기반 다중접속 서버 구현 [TCP/IP][C][LINUX] (0) | 2022.01.30 |