Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

Contents

입출력 다중화를 이용한 다중연결처리 서버 제작

우리는 멀티 프로세스 기반 다중 클라이언트 처리 프로그램을 알아보았다. 이번에는 입출력 다중화를 이용, 다수의 연결을 처리하는 소켓 서버 프로그래밍 기법에 대해서 알아보도록 하겠다. 먼저 파일 입출력 다중화문서를 읽어 보기 바란다.

select(2) 를 통해서 다중연결서버를 만들경우 fork(2)를 이용한 멀티 프로세스에 비해서 몇가지 장점이 있다. 프로세스 생성은 매우 많은 비용이든다. 입출력 다중화는 프로세스 생성비용을 줄일 수 있다. 멀티 프로세스 방식 프로그램의 경우 독립된 프로세스로 실행되므로 프로세스간 통신을 위해서 IPC(:12)를 사용해야 한다. IPC(:12)는 제어하기 까다롭기로 유명하다. select 로 구성할경우 단일프로세스로 모든 작업이 이루어지므로 굳이 IPC를 사용할 필요가 없다라는 점이다.

select의 단점은 파일 입출력 다중화문서를 참고하기 바란다. 소켓 네트워크 프로그램에서도 fd_set을 매번 복사해야 하며, 전체 fd_set을 모두 순환해야 하는 문제를 가진다. epoll(:12)을 이용하면 입출력 다중화의 문제를 해결할 수 있다.

그럼 fork 버젼의 서버를 select 버젼으로 재작성해보도록 하자. 클라이언트는 기존의 것을 수정하지 않고 쓰도록 한다. 전형적인 에코 서버다.
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#include <sys/socket.h>
#include <sys/types.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <stdio.h>
#include <string.h>

#define MAXLINE 1024 
#define PORTNUM 3600
#define SOCK_SETSIZE 1021

int main(int argc, char **argv)
{
	int listen_fd, client_fd;
	socklen_t addrlen;
	int fd_num;
	int maxfd = 0;
	int sockfd;
	int readn;
	int i= 0;
	char buf[MAXLINE];
	fd_set readfds, allfds;

	struct sockaddr_in server_addr, client_addr;

	if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
	{
		perror("socket error");
		return 1;
	}   
	memset((void *)&server_addr, 0x00, sizeof(server_addr));
	server_addr.sin_family = AF_INET;
	server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	server_addr.sin_port = htons(PORTNUM);
	
	if(bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
	{
		perror("bind error");
		return 1;
	}   
	if(listen(listen_fd, 5) == -1)
	{
		perror("listen error");
		return 1;
	}   
	
	FD_ZERO(&readfds);
	FD_SET(listen_fd, &readfds);

	maxfd = listen_fd;
	while(1)
	{
		allfds = readfds;
		printf("Select Wait %d\n", maxfd);
		fd_num = select(maxfd + 1 , &allfds, (fd_set *)0,
					  (fd_set *)0, NULL);

		if (FD_ISSET(listen_fd, &allfds))
		{
			addrlen = sizeof(client_addr);
			client_fd = accept(listen_fd,
					(struct sockaddr *)&client_addr, &addrlen);

			FD_SET(client_fd,&readfds);

			if (client_fd > maxfd)
				maxfd = client_fd;
			printf("Accept OK\n");
			continue;
		}

		for (i = 0; i <= maxfd; i++)
		{
			sockfd = i;
			if (FD_ISSET(sockfd, &allfds))
			{
				if( (readn = read(sockfd, buf, MAXLINE-1)) == 0)
				{
					printf("close\n");
					close(sockfd);
					FD_CLR(sockfd, &readfds);
				}
				else
				{
					buf[readn] = '\0';
					write(sockfd, buf, strlen(buf));
				}
				if (--fd_num <= 0)
					break;
			}
		}
	}
}

모아서 처리하기

select는 소켓 버퍼에 데이터가 남아 있으면 fd_set을 1로 둔다. 그러므로 read 함수로 소켓 버퍼를 비우지 못하면, 여러 번 select를 호출하게 된다. MAXLINE의 크기를 작게 한 다음 테스트 해보기 바란다. select를 여러번 호출하는 것은 그다지 좋은 방법은 아니다. 차라리 버퍼에 있는 내용을 다 가져올 때까지 read를 호출하는게 더 나을 것이다.
while( (readn = read(sockfd, buf, MAXLINE-1)) > 0)
{
	write(sockfd, buf, readn);
}
그러나 이 방법은 버퍼를 비우고 나서, read에서 봉쇄되어버린다는 문제가 있다. 이 문제를 해결하기 위해서는 소켓을 비봉쇄로 해야 한다.

비봉쇄 입출력 적용

모아서 처리하기는 좋은 방법이지만, 제대로 활용하기 위해서는 비봉쇄 소켓으로 만들어야 한다. 다음은 매개 변수로 넘어온 파일을 비봉쇄로 만드는 함수다. accept(:2)함수가 가져오는 연결 소켓을 아래 함수를 이용해서 비 봉쇄로 만들면 된다.
void nonblock(int sockfd) 
{ 
    int opts; 
    opts = fcntl(sockfd, F_GETFL); 
    if(opts < 0) 
    { 
        perror("fcntl(F_GETFL)\n"); 
        exit(1); 
    } 
    opts = (opts | O_NONBLOCK); 
    if(fcntl(sockfd, F_SETFL, opts) < 0) 
    { 
        perror("fcntl(F_SETFL)\n"); 
        exit(1); 
    } 

비봉쇄 소켓은 읽을 데이터가 없으면 즉시 반환하는데 이 때 값이 -1이다. 그로므로 에러의 종류를 확인하기 위해서 errno값을 검사해야 한다. 더 이상 읽을 데이터가 없어서 -1을 반환할 경우 errno가 EAGAIN로 설정된다. 그러므로 다음과 같이 코드를 변경하면 된다.
while( (readn = read(sockfd, buf, MAXLINE-1)) > 0)
{
   write(...);
}
if(readn == -1)
{
	if (errno != EAGAIN)
    {
		close(sockfd);
		FD_CLR(sockfd, &readfds);
    }
}