모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!
# 멀티프로세스 서버의 단점
- Ch 11에서는 클라이언트의 연결요청이 있을 떄마다 새로운 프로세스를 생성하였음
- 프로세스 생성에는 상당히 많은 대가를 지불함(많은 양의 연산이 요구되며, 필요한 메모리 공간도 비교적 큼)
- 프로세스마다 별도의 메모리 공간을 유지하기 때문에 상호간에 데이터를 주고받으려면 다소 복잡한 방법이 필요(IPC)
# 멀티프로세스 서버의 대안 : 멀티플렉싱
- 프로세스의 생성을 동반하지 않으면서 다수의 클라이언트에 서비스를 제공할 수 있는 방법
# 구현하고자 하는 서버의 특성에 따라서 구현방법은 달리 결정되어야 한다. 즉, 모든 경우에 있어서 최선인 방법은 없다.
# 멀티플렉싱
- "하나의 통신채널을 통해서 둘 이상의 데이터(시그널)를 전송하는데 사용되는 기술"
- "물리적 장치의 효율성을 높이기 위해서 최소한의 물리적인 요소만 사용해서 최대한의 데이터를 전달하기 위해 사용되는 기술"
# 멀티플렉싱의 개념을 서버에 적용하기
- 접속해있는 클라이언트의 수에 상관없이, 서비스를 제공하는 프로세스의 수는 "딱 하나"
- IO 멀티플렉싱 서버에서는 (서버)프로세스가 데이터가 수신된 소켓이 있는지 확인하고,
데이터를 수신한 소켓이 있다면 그 소켓을 통해서 전송된 데이터를 수신하게 된다.
# select 함수를 이용하는 것이 멀티플렉싱 서버의 구현에 있어서 가장 대표적인 방법. 윈도우에도 이와 동일한 이름으로 동일한 기능을 제공하는 함수가 있어 이식성에 있어서도 좋음.
# select 함수의 기능
- 기능 : 한곳에 여러개의 파일 디스크립터를 모아놓고 동시에 관찰 가능
- 관찰 가능한 항목(이벤트, event)
1) 수신한 데이터를 지니고 있는 소켓이 존재하는가?
2) 블로킹되지 않고 데이터의 전송이 가능한 소켓은 무엇인가?
3) 예외상황이 발생한 소켓은 무엇인가?
# select 함수의 호출방법과 순서
step 1)
1. 파일 디스크립터의 설정
- 관찰하고자 하는 파일 디스크립터를 관찰항목(수신, 전송, 예외)에 따라 구분해서 모아야 함
- 파일 디스크립터를 바로 위에서 언급한 세 묶음으로 모을 때 사용되는 것이 "fd_set" 변수
(0과 1로 표현되는, 비트단위로 이뤄진 배열임)
- fd_set의 가장 왼쪽 비트는 파일 디스크립터 0을 나타내며, 이 비트가 1로 설정되면 해당 파일 디스크립터가 관찰대상임을 의미
- fd_set형 변수에 값을 등록하거나 변경하는 등의 작업은 다음 매크로 함수들의 도움을 통해 이루어짐
FD_ZERO(fd_set *fdest) | 인자로 전달된 주소의 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 함수의 호출결과를 확인(step 3)하는 용도로 사용됨
# 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 반환
- maxfd : 검사 대상이 되는 파일 디스크립터의 수
- readset : fd_set형 변수에 "수신된 데이터의 존재여부"에 관심 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소값을 전달
- writeset : fd_set형 변수에 "블로킹 없는 데이터 전송의 가능여부"에 관심 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소값을 전달
- exceptset : fd_set형 변수에 "예외상황의 발생여부"에 관심 있는 파일 디스크립터 정보를 모두 등록해서 그 변수의 주소값을 전달
- timeout : select 함수호출 이후에 무한정 블로킹 상태에 빠지지 않도록 타임 아웃(time-out)을 설정하기 위한 인자를 전달
- 반환값 : 오류발생시에는 -1이 반환되고, 타임 아웃에 의한 반환시에는 0이 반환된다. 그리고 관심대상으로 등록된 파일 디스크립터에 해당 관심에 관련된 변화가 발생하면 0보다 큰 값이 반환되는데, 이 값은 변화가 발생한 파일 디스크립터의 수를 의미한다.
2. 검사의 범위 지정
- select 함수의 첫번째 매개변수(maxfd)와 관련이 있음
- 리눅스에 파일 디스크립터의 값은 생성될 때마다 1씩 증가하기 때문에 가장 큰 파일 디스크립터의 값에 1을 더해서 maxfd 인자로 전달하면 됨(파일 디스크립터의 값이 0부터 시작하기 때문)
3. 타임아웃의 설정
- select 함수의 마지막 매개변수(timeout)와 관련이 있음
struct timeval
{
long tv_sec; // seconds
long tv_usec; // microseconds
}
- 원래 select 함수는 관찰중인 파일 디스크립터에 변화가 생겨야 반환을 하기 때문에, 변화가 생기지 않으면 무한정 블로킹 상태에 놓이게 됨 => 이러한 상황을 막기 위해 타임아웃을 지정
- timeval 변수의 주소값을 select 함수의 마지막 인자로 전달하면, 파일 디스크립터에 변화가 발생하지 않아도 지정한 시간이 지나면 함수가 "0"을 반환함
- 타임아웃을 설정하고 싶지 않으면 NULL을 인자로 전달
step 2) : select 함수의 호출
step 3) : 호출결과 확인
1. 오류발생시 -1 반환
2. 타임아웃에 의한 반환시 0 반환
3. 관심대상 파일 디스크립터의 변화에 의한 양수 반환
- 변화가 발생한 파일 디스크립터를 확인하는 방법 :
select 함수호출이 완료되고 나면 select 함수의 인자로 전달된 fd_set형 변수의 모든 비트가 다 0으로 변경되지만, 변화가 발생한 파일 디스크립터에 해당하는 비트만 그대로 1로 남아 있게 됨
# select 함수를 호출하는 예제의 확인
select.c
#include <stdio.h>
#include <unistd.h>
#include <stdbool.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_ZERO(&reads); // 모두 0으로 초기화
FD_SET(0, &reads); // 파일 디스크립터 0은 표준 입력(콘솔), 표준입력의 변화에 관심을 가지겠다는 의미
while(true)
{
temps=reads;
/* select 함수 호출이 완료되면
관심 대상 중 변화한 파일 디스크립터 위치만 제외하고
모두 모든 비트가 0으로 변경되므로 원본이 손실될 수 있다.
따라서 원본이 아닌 복사본으로 select 함수를 호출한다.
*/
timeout.tv_sec=5; // 5초
timeout.tv_usec=5000; // 0.5초
/*
타임아웃의 설정시간은 select 함수 호출 이전에 매번 새롭게 해주어야 한다.
select 함수호출 이후에는, 타임아웃이 발생하기까지 남았던 시간으로 변화하기 때문이다.
*/
result=select(1,&temps,0,0,&timeout); // 표준입력의 수신 여부에 관심
if(result==-1) // 오류발생시
{
puts("select() error!");
break;
}
else if(result==0) // 타임아웃 발생시
{
puts("Time-out!");
}
else // 관심있는 대상의 파일디스크립터가 변화된 경우
{
if(FD_ISSET(0, &temps)) // 표준 입력이 변화됐는지 확인
{
str_len=read(0,buf,BUF_SIZE); // 표준입력으로부터 읽어들임
buf[str_len]='\0';
printf("message from console: %s",buf);
}
}
}
return 0;
}
# 멀티플렉싱 서버의 구현
- select 함수의 사용법을 바탕으로 멀티플렉싱 서버를 구현
echo_selectserv.c : 여태까지 구현했던 어떤 에코 클라이언트와 같이 실행해도 제대로 동작합니다!
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.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 *message);
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) // 실행파일 경로/PORT번호 를 입력으로 받아야 함
{
printf("Usage : %s <port> \n", argv[0]);
exit(EXIT_FAILURE);
}
serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
/* 서버 주소정보 초기화 */
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;
while(true)
{
cpy_reads=reads; // 원본 손실 방지를 위해 복사본으로 select함수 호출 진행
/* 타임 아웃 시간 설정 */
timeout.tv_sec=5;
timeout.tv_usec=5000;
if((fd_num=select(fd_max+1,&cpy_reads,0,0,&timeout))==-1) // -1 반환시 오류 발생, 데이터 수신과 관련하여 관심을 가짐
break;
if(fd_num==0) // 0 반환시 타임아웃
continue;
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 // 클라이언트의 메시지를 실제로 수신하는 소켓에 대해(accept 함수호출로 생성된 소켓)
{
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); // 수신한 문자열을 다시 클라이언트로 에코
}
}
}
}
close(serv_sock);
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE); // -1
}
# 윈도우 기반 select 함수의 호출
- 윈도우에서도 select 함수를 제공하며, 모든 인자는 리눅스 기반의 select 함수와 동일
- 단, 윈도우의 select 함수의 첫번째 인자는 리눅스를 포함하는 유닉스 계열의 운영체제와의 상호호환성을 위해 존재하는 것일 뿐 별다른 의미는 없음
- timeval 구조체의 기본적인 정의형태는 리눅스와 동일하나 typedef 선언을 포함함
typedef struct timeval
{
long tv_sec; // seconds
long tv_usec; // microseconds
} TIMEVAL;
- 윈도우 기반으로 예제를 변경(혹은 구현)하는데 있어서 주의해야 할 부분은,
윈도우의 fd_set은 리눅스와 같이 비트의 배열로 구성되어 있지 않다는 것이다.
typedef struct fd_set
{
u_int fd_count;
SOCKET fd_array[FD_SETSIZE];
} fd_set;
- fd_count : 저장된 소켓의 핸들 수
- fd_array : 소켓의 핸들 저장을 위한 멤버
=> 왜 이렇게 정의되었을까?
- 리눅스의 파일 디스크립터는 0부터 시작해서 값이 하나씩 증가하는 구조를 지니지만, 윈도우 기반의 소켓 핸들은 어떠한 규칙성도 없기 때문이다. 다행히 fd_set형 변수의 조작을 위한 매크로 함수는 이름, 기능 및 사용방법이 리눅스와 동일
[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어
'Programming > 열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)' 카테고리의 다른 글
Ch 13. 다양한 입출력 함수들 (0) | 2020.08.09 |
---|---|
Ch 12. 내용 확인문제 (0) | 2020.08.09 |
Ch 11. 내용 확인문제 (0) | 2020.08.09 |
Ch 11. 프로세스간 통신(Inter Process Communication) (0) | 2020.08.09 |
Ch 10. 내용 확인문제 (0) | 2020.08.08 |