열렬히.뛰기

11. 멀티플랙싱 : 개요

네트워크 > 소켓 프로그래밍 > 소켓 프로그래밍 > 11. 멀티플랙싱 : 개요

멀티서버

왜 멀티서버인가?

  • 전체적인 서비스 제공시간이 늦어져도 모든 클라이언트에게 서비스를 제공하기 위해
  • cpu의 연산을 필요로 하지 않는 데이터의 송수신 시간이 큰 비중을 차지하므로, 이를 효율적으로 쓰기 위해

멀티서버 구현

  1. 멀티프로세싱 : 클라이언트 수만큼 프로세스 생성
  2. 멀티쓰레딩 : 클라이언트 수만큼 쓰레드 생성
  3. 멀티플랙싱 : 입출력 대상을 묶어서 관리

i/o 멀티플랙싱

어떻게 한 개의 프로세스만을 가지고 여러개의 fd를 다룰 수 있는가?

이를 i/o 멀티플랙싱이라고 하며, i/o 멀티플랙싱을 위한 여러가지 함수가 있다.

c
select(), poll(), epoll(), kqueue(), ...

여기서는 가장 기초적인 함수인 select를 알아보자.

select 함수

작업할 준비가 된 파일에 대해서만 작업을 하고, 나머지 파일은 막아두면 된다.

c
int select(int maxfd, fd_set *readset, fd_set *writeset,
						fd_set *exceptset, const struct timeval *timeout);
  • maxfd : 검사 대상이 되는 파일 디스크립터 수
  • readset : '읽기'가 가능한지 확인
  • writeset : '쓰기'가 가능한지 확인
  • exceptset : 예외가 발생했는지 확인
  • timeout : 무한정 블로킹 방지용 시간 설정
  • 반환 값 : 오류발생시 -1, 타임아웃 0, 0보다 큰 경우 = 변화가 발생한 fd 갯수

select 함수의 특징

  1. 시간을 설정해야 한다.
  • select 함수는 변화가 발생할 때까지 계속 가만히 있는다.
  • 변화가 생기지 않으면 무한정 그 상태에 머물기 때문에 timeout을 지정해야 한다.
  • timeout이 생긴 경우 0을 반환값으로 준다.
  • 변화가 생기는 경우에는 파일 디스크립터가 다음과 같이 변한다.
c
<변화 전 : fd 0번 ~ 8번까지 등록>
1 1 1 1 1 1 1 1 1

- fd 0번과 2번이 변화 -

<변화 후 : 변화한 fd만 계속 1이다.>
1 0 1 0 0 0 0 0 0

fd 0번이 변화했다는 것 = fd 0번으로 수신한 데이터가 존재.
  • 이 경우 파일 디스크립터는 2을 반환한다. (변화가 있는 데이터가 2개라는 뜻)
  1. 특정 항목은 감시하지 않게 할 수 있다.
  • 만약 select의 매개변수에 NULL 또는 0을 주면 해당 이벤트는 감시하지 않는다.
c
result = select(1, &temps, 0, 0, &timeout);

// write와 except를 감시하지 않는다.
  1. fd를 어디에 담는가?
  • 그 많은 fd를 어디에 담을 것인가?
  • 이를 위해서 나온 특수한 구조체가 바로 fd_set이다.

fd_set 구조체

이를 위해 나온 구조체가 바로 fd_set이다.

fd를 저장하기 위한 구조체이며, 내부는 0, 1로 이뤄진 배열로 되어 있다.

만약 여기에 특정 fd를 등록하면 해당 index의 값이 1로 세팅된다.

c
<처음 생성 및 초기화 시>
0, 0, 0, 0, 0, 0, 0, ...

<fd 1번 등록 시>
0, 1, 0, 0, 0, 0, 0, ...

fd_set 관련 함수

fd_set의 생성 및 초기화는 다음과 같이 한다.

c
fd_set set;     // fd_set 생성
FD_ZERO(&set);  // fd_set 초기화

fd_set2번 fd와 5번 fd를 등록한다고 해보자.

c
FD_SET(2, &set);
FD_SET(5, &set);

fd_set에서 3번 fd를 삭제할 수도 있다. (즉, 1 → 0으로 값을 바꾼다는 이야기)

c
FD_CLR(3, &set);

fd_set 에서 4번 fd가 1인지(즉 값이 등록 되었는지) 확인하고 싶다면 다음과 같이 사용

c
FD_ISSET(4, &set);

select로 만드는 멀티서버

c
#include "../../socket_header.h"
#include "../../socket_header.c"
#define BUF_SIZE2 100

int main(int ac, char **av)
{
	int serv_sock, clnt_sock, res;
	struct sockaddr_in serv_adr, clnt_adr;

	struct timeval timeout;
	fd_set reads, cpy_reads;

	socklen_t adr_size;
	int fd_max, str_len, fd_num;
	char buf[BUF_SIZE2];

	if (ac != 2)
	{
		printf("usage : %s\n", av[0]);
		exit(0);
	}

	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(av[1]));

	res = bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr));
	if (res == -1)
		error_handling("bind() error");
	res = listen(serv_sock, 5);
	if (res == -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;

		// 현재 fd번호에 1개를 추가해야 전체 갯수가 나온다.
		if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
			break ;
		if (fd_num == 0)
			continue ;

		// 여기서부턴 select의 반환값이 1 이상일때만 실행된다.
		for (int i = 0; i < fd_max + 1; i++)
		{
			// cpy_reads 중 i번째 fd가 그대로 1인지 검사. (= 변화가 있는 fd 잡아내기)
			if (FD_ISSET(i, &cpy_reads))
			{
				// i가 서버의 소켓이라면
				if (i == serv_sock)
				{
					adr_size = sizeof(clnt_adr);
					clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_size);
					// reads에 클라이언트 소켓 등록.
					FD_SET(clnt_sock, &reads);

					// 만약 클라이언트 소켓이 fd_max보다 크다면 fd_max값 바꿔주기.
					if (fd_max < clnt_sock)
						fd_max = clnt_sock;
					
					// 몇번 소켓인지 표기.
					printf("connected client : %d\n", clnt_sock);
				}
				// i가 서버의 소켓이 아니라면 (즉 클라이언트 소켓이라면)
				else
				{
					// 일단 read로 buf에 그 내용을 쓴다.
					str_len = read(i, buf, BUF_SIZE2);
					// 만약 EOF(길이가 0)이면 해당 클라이언트 소켓을 닫을 준비를 한다.				
					if (str_len == 0)
					{
						FD_CLR(i, &reads);	// reads에서 해당 소켓 삭제
						close(i);			// 소켓 닫기
						printf("closed client : %d\n", i);
					}
					// 아니면 buf를 출력한다.
					else
						write(i, buf, str_len);
				}
			}
		}
	}

	// 서버 소켓을 닫는다.
	close(serv_sock);
	return 0;
}