이전 글의 fork 를 이용한 다중연결서버 만들기 #1 에 이어서 이번에는 select 를 통한 다수의 연결을 받아들이는 서버프로그래밍 기법에 대해서 알아보도록 하겠다. select 에 대한 내용은 select 를 통한 입출력 다중화를 참고하기 바란다.
소스코드는 다중연결서버 만들기 #1 의 fork 버젼 서버를 select 로 수정하는 방식으로 재 작성하도록 하겠다.
select 를 통해서 다중연결서버를 만들경우 fork 를 이용한 방법에 비해서 몇가지 장점이 있다.
fork 를 통해서 프로세스 생성시 발생하는 overload 를 줄일수 있으며 (접속응답을 빠르게 가져갈수 있다.), fork 를 사용할시 자식프로세스와 부모프로세스간의 통신을 위해서 IPC를 사용해야 하는데, select 로 구성할경우 단일프로세스로 모든 작업이 이루어지므로 굳이 IPC를 사용할 필요가 없다라는 점이다.
또한 fork 의 경우 프로세스를 생성시키는 것이므로, 아무래도 파일지시자 기반의 select 에 비해서 받아들일수 있는 클라이언트의 수에 제한이 있을수 밖에 없다. (생성시킬수 있는 프로세스가, 아무래도 생성시킬수 있는 파일지시자에 비해서 시스템의 제한을 많이 받게 된다).
이제 select 만의 단점에 대해서 알아보도록 하겠다. 가장 큰 단점은 select 에서 넘겨주는 정보가 너무 부실하단 점이다. 단지 이벤트가 발생한 파일지시자의 숫자만 돌려주기 때문이다.
그러므로 select의 부족한 정보를 보충해주기 위해서 별도의 코딩을 필요로 한다. (일반적으로 사용되는 방법은 활성화된 파일 데스크립션을 배열에 저장해서 사용하는 방법이다.)
select 는 기본적으로 fd_set 의 각 비트 값을 체크함으로써(0 인지 혹은 1인지) 파일지시자에 이벤트가 발생했는지 검사하는 방법을 쓴다고 앞에서 말했었다. 그런데 이 fd_set 은 각 비트가 가르키는 파일지시자 값이 하드 코딩 되어 있다. 예를 들어 첫번째 비트는 무조건 0번 파일지시자를, 두번째는 1번, 세번째는 2번 이런식이다. 그리고 fd_set 은 그 크기가 1024 로 고정되어 있다. (define 되어 있는 값이므로 고정되어 있다고 볼수는 없지만, 수정하는건 좋지 않으므로 변경할수 없는것이라고 못박았다. 이값은 FD_SETSIZE 로 define 되어 있다.)
그러므로 동시에 1025 명의 클라이언트가 서버에 접근했다고 하면, fd_set 은 1025 번째 파일지시자에 대해서 가르키질 못하므로, 1025에 대해서는 검사 자체가 불가능해지는 문제가 있다. 동시접속 1024 라면 상당히 큰 수이긴 하지만, 때때로 이게 문제가 될경우도 있다. 그러므로 소스 코드 차원에서 1024 보다 더큰 연결이 일어나지 않도록 연결접속 제한에 신경써야 한다.
1024 제한을 피하고 싶다면 poll 등 다른 방법을 생각해 봐야 할것이다.
그럼 fork 버젼의 서버를 select 버젼으로 재작성해보도록 하자. 클라이언트는 기존의 것을 수정하지 않고 쓰도록 한다.
예제 : zipcode_se.c
소스 프로그램은 주석과 같이 보면 이해하기에는 별로 어려움이 없을것이다.
별문제 없이 작동하긴 하지만 좀 세심하게 살펴보면 file I/O 쪽에서 문제가 발생할수도 있다는것을 알수 있을것이다. 여러개의 클라이언트가 접속할수 있는데, 열린파일은 하나이기 때문에, 파일위치를 잘못 참조하는 문제가 발생할수 있다.
이 문제는 각 클라이언트(fd)당 파일을 열수 있도록하면 문제 해결은 가능할 것이다.
소스코드는 다중연결서버 만들기 #1 의 fork 버젼 서버를 select 로 수정하는 방식으로 재 작성하도록 하겠다.
select 를 통해서 다중연결서버를 만들경우 fork 를 이용한 방법에 비해서 몇가지 장점이 있다.
fork 를 통해서 프로세스 생성시 발생하는 overload 를 줄일수 있으며 (접속응답을 빠르게 가져갈수 있다.), fork 를 사용할시 자식프로세스와 부모프로세스간의 통신을 위해서 IPC를 사용해야 하는데, select 로 구성할경우 단일프로세스로 모든 작업이 이루어지므로 굳이 IPC를 사용할 필요가 없다라는 점이다.
또한 fork 의 경우 프로세스를 생성시키는 것이므로, 아무래도 파일지시자 기반의 select 에 비해서 받아들일수 있는 클라이언트의 수에 제한이 있을수 밖에 없다. (생성시킬수 있는 프로세스가, 아무래도 생성시킬수 있는 파일지시자에 비해서 시스템의 제한을 많이 받게 된다).
이제 select 만의 단점에 대해서 알아보도록 하겠다. 가장 큰 단점은 select 에서 넘겨주는 정보가 너무 부실하단 점이다. 단지 이벤트가 발생한 파일지시자의 숫자만 돌려주기 때문이다.
그러므로 select의 부족한 정보를 보충해주기 위해서 별도의 코딩을 필요로 한다. (일반적으로 사용되는 방법은 활성화된 파일 데스크립션을 배열에 저장해서 사용하는 방법이다.)
select 는 기본적으로 fd_set 의 각 비트 값을 체크함으로써(0 인지 혹은 1인지) 파일지시자에 이벤트가 발생했는지 검사하는 방법을 쓴다고 앞에서 말했었다. 그런데 이 fd_set 은 각 비트가 가르키는 파일지시자 값이 하드 코딩 되어 있다. 예를 들어 첫번째 비트는 무조건 0번 파일지시자를, 두번째는 1번, 세번째는 2번 이런식이다. 그리고 fd_set 은 그 크기가 1024 로 고정되어 있다. (define 되어 있는 값이므로 고정되어 있다고 볼수는 없지만, 수정하는건 좋지 않으므로 변경할수 없는것이라고 못박았다. 이값은 FD_SETSIZE 로 define 되어 있다.)
그러므로 동시에 1025 명의 클라이언트가 서버에 접근했다고 하면, fd_set 은 1025 번째 파일지시자에 대해서 가르키질 못하므로, 1025에 대해서는 검사 자체가 불가능해지는 문제가 있다. 동시접속 1024 라면 상당히 큰 수이긴 하지만, 때때로 이게 문제가 될경우도 있다. 그러므로 소스 코드 차원에서 1024 보다 더큰 연결이 일어나지 않도록 연결접속 제한에 신경써야 한다.
1024 제한을 피하고 싶다면 poll 등 다른 방법을 생각해 봐야 할것이다.
그럼 fork 버젼의 서버를 select 버젼으로 재작성해보도록 하자. 클라이언트는 기존의 것을 수정하지 않고 쓰도록 한다.
예제 : zipcode_se.c
#include <sys/time.h> #include <sys/socket.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <netinet/in.h> #include <arpa/inet.h> //#define FD_SETSIZE 1024 int main(int argc, char **argv) { int server_sockfd, client_sockfd, sockfd; // select 를 통해서 넘어오는 fd 의 갯수 int fd_num; int state, client_len; int i, maxi, maxfd; // 클라이언트로부터의 입력문을 저장 char buf[255]; // zipcode.txt 에서 읽어온 라인을 저장 char line[255]; FILE *fp; // client 연결을 저장하기 위한 배열 int client[FD_SETSIZE]; struct sockaddr_in clientaddr, serveraddr; fd_set readfds, allfds; if (argc != 2) { printf("Usage : ./zipcode [port]\n"); printf("예 : ./zipcode 4444\n"); exit(0); } // 주소 파일을 읽어들인다. if ((fp = fopen("zipcode.txt", "r")) == NULL) { perror("file open error : "); exit(0); } fd_num = 0; // 주소 파일을 읽어들인다. if ((fp = fopen("zipcode.txt", "r")) == NULL) { perror("file open error : "); exit(0); } // INET 소켓연결을 작성한다. if ((server_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket error : "); exit(0); } bzero(&serveraddr, sizeof(serveraddr)); serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(atoi(argv[1])); state = bind (server_sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)); if (state == -1) { perror("bind error : "); exit(0); } state = listen(server_sockfd, 5); if (state == -1) { perror("listen error : "); exit(0); } // INET 소켓작성 끝 client_sockfd = server_sockfd; maxi = -1; // 현재 fd 의 최대값을 maxfd 에 입력한다. maxfd = server_sockfd; // fd 배열을 -1 로 초기화 한다. // 후에 accept 를 성공하면 해당 fd 로 배열을 채운다. for (i = 0; i < FD_SETSIZE; i++) { client[i] = -1; } // INET 소켓 fd 에 대하여, fd_set 에 등록한다. FD_ZERO(&readfds); FD_SET(server_sockfd, &readfds); memset(line, 0x00, 255); while(1) { allfds = readfds; fd_num = select(maxfd + 1 , &allfds, (fd_set *)0, (fd_set *)0, NULL); // 만약 소켓 파일지사자에 연결(읽기)이 들어온다면 // accept 한다. if (FD_ISSET(server_sockfd, &allfds)) { client_len = sizeof(clientaddr); // accept 한다. client_sockfd = accept(server_sockfd, (struct sockaddr *)&clientaddr, &client_len); // client 배열에 현재 연결된 client_sockfd 를 // 입력한다. for (i = 0; i < FD_SETSIZE; i++) { if (client[i] < 0) { client[i] = client_sockfd; printf("%d : %d\n", i, client_sockfd); break; } } // FD_SETSIZE 를 초과해서 client 가 접근할경우 // 연결을 종료한다. if (i == FD_SETSIZE) { close(sockfd); client[i] = -1; } // fd_set 에 등록한다. FD_SET(client_sockfd,&readfds); if (client_sockfd > maxfd) maxfd = client_sockfd; if (i > maxi) maxi = i; if (--fd_num <= 0) continue; } // client 배열에 입력된 fd 중 // 연결이된 (-1 이아닌) fd 에 대해서 // 읽을 데이타가 있는지 검사한후 // 읽을 데이타가 있으면 처리한다. for (i = 0; i <= maxi; i++) { if ((sockfd = client[i]) < 0) { continue; } printf("maxi %d\n", maxi); if (FD_ISSET(sockfd, &allfds)) { // 파일 stream pointer 을 처음으로 돌린다. rewind(fp); memset(buf, 0x00, 255); if (read(sockfd, buf, 255) <= 0) { close(sockfd); FD_CLR(sockfd, &readfds); client[i] = -1; } else { // client 가 quit 를 입력하면 // 연결을 종료시킨다. if (strncmp(buf, "quit", 4) ==0) { write(sockfd, "bye bye\n", 8); close(sockfd); FD_CLR(sockfd, &readfds); client[i] = -1; break; } while(fgets(line, 255, fp) != NULL) { if (strstr(line, buf) != NULL) write(sockfd, line, 255); memset(line, 0x00, 255); } write(sockfd, "end", 255); } if (--fd_num <= 0) break; } } } } |
별문제 없이 작동하긴 하지만 좀 세심하게 살펴보면 file I/O 쪽에서 문제가 발생할수도 있다는것을 알수 있을것이다. 여러개의 클라이언트가 접속할수 있는데, 열린파일은 하나이기 때문에, 파일위치를 잘못 참조하는 문제가 발생할수 있다.
이 문제는 각 클라이언트(fd)당 파일을 열수 있도록하면 문제 해결은 가능할 것이다.