안녕하세요 인포돈 입니다.
본 내용은 우분투를 기본으로 작성되었습니다.
Cloud Computing을 활용하여 서버를 구축하였습니다.
3개의 클라이언트 fork를 활용한 데이터 통합
이전 포스팅까지 1개의 서버와 3개의 클라이언트가 연동되어 데이터를 통합하는 형식이었습니다. 그러나 이런 클라이언트가 만약 1억 개라면? 서버는 모든 클라이언트와 순서대로 데이터를 주고받아야 하기 때문에 처리 속도가 떨어질 수밖에 없습니다. 이러한 문제점을 해결하기 위해서 우리는 fork라는 기법을 사용하려 합니다.
fork란?
프로세스를 복사하는 함수이다. fork는 기본적으로 복제가 되면 부모와 자식 프로세스가 생기게 된다. 이때 자식과 부모를 구분하기 위해서 pid를 할당해 주게 되는데 부모의 경우 무작위 양수를 할당하고 자식의 경우 0의 값을 할당받게 된다. 이를 통해서 자식과 부모를 구분할 수 있다.
해당 그림처럼 fork함수를 실행하면 곧바로 부모 프로세스와 자식 프로세스로 나뉘고 나눠진 시점은 부모와 자식 프로세스가 모두 같은 상태이다. 이렇게 프로세스가 2개가 되면 멀티 태스킹이 가능해지고 앞서 말한 처리속도를 훨씬 빠르게 할 수 있다.
서버 코드
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>#include <unistd.h>
#define MAXBUF 1024
int main(int argc, char **argv)
{
int server_sockfd, client_sockfd;
int client_temp[3];
int client_len, n, status, cnt = 0;
char buf[MAXBUF];
char msg[MAXBUF];
struct sockaddr_in clientaddr, serveraddr;
client_len = sizeof(clientaddr);
int pid;
int p[2];
pipe(p);
if ((server_sockfd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP )) == -1){
perror("socket error : ");
exit(0);
}
memset(&serveraddr, 0x00, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(atoi(argv[1]));
bind (server_sockfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
listen(server_sockfd, 5);
memset(msg, 0x00, MAXBUF);
while(1)
{
client_temp[cnt] = accept(server_sockfd, (struct sockaddr *)&clientaddr, &client_len);
printf("New Client[%d] Connect: %s port : %d\n",cnt,
inet_ntoa(clientaddr.sin_addr),ntohs(clientaddr.sin_port));
pid = fork();
if(pid == 0){ //자식프로세스
close(server_sockfd);
memset(buf, 0x00, MAXBUF);
n = read(client_temp[cnt], buf, MAXBUF); //클라이언트값 수신 buf[n]=0;
write(p[1], buf, sizeof(buf)); //부모 프로세스에 buf 전송
close(client_sockfd);
return 0;
} else if(pid > 0){ //부모프로세스
n = read(p[0], buf, sizeof(buf)); //자식 프로세스에서 보낸 값 수신
for(int i = 0 ; buf[i]!=0 ; i ++){ //필요한 값 외의 쓰레기값 제거
if(buf[i]=='\n'){
buf[i] = 0;
break;
}
}
strcat(msg," ");
cnt++;
strcat(msg, buf);
if(cnt == 3){ //프로세스 3개가 모두 돌았다면
char t[sizeof(msg)]; //보기 편리하게 msg출력
strcpy(t,msg);
char *ptr = strtok(t, " ");
for(int i = 0 ; i < 3 ; i++){
printf("from Client[%d] : %s \n",i,ptr);
ptr = strtok(NULL," ");
}
strcat(msg, "\n");
printf("msg : %s \n", msg);
for(int i = 0 ; i < 3 ; i ++){ //각 클라이언트에게 전송
write(client_temp[i], msg, sizeof(msg));
close(client_temp[i]);
}}
}
}
close(server_sockfd);
return 0;
}
- fork
pid = fork();
if(pid == 0){ //자식프로세스
close(server_sockfd);
memset(buf, 0x00, MAXBUF);
n = read(client_temp[cnt], buf, MAXBUF); //클라이언트값 수신 buf[n]=0;
write(p[1], buf, sizeof(buf)); //부모 프로세스에 buf 전송
close(client_sockfd);
return 0;
} else if(pid > 0){ //부모프로세스
n = read(p[0], buf, sizeof(buf)); //자식 프로세스에서 보낸 값 수신
for(int i = 0 ; buf[i]!=0 ; i ++){ //필요한 값 외의 쓰레기값 제거
if(buf[i]=='\n'){
buf[i] = 0;
break;
}
}
fork를 한 부분을 살펴보면 pid == 0 일 때 즉, 자식 프로세스일 때는 앞서 부모 프로세스에서 응답한 클라이언트와 연결을 이어갑니다. 해당 자식 프로세스에서는 더 이상 서버 소켓이 필요하지 않음으로 닫아줍니다. 그 후 클라이언트에 입력한 값을 읽어온 후 부모 프로세스 buf에 전송하고 자식 프로세스를 끝냅니다.
(여기서 자식, 부모 프로세스 간에 데이터를 주고받는 write부분은 바로 pipe라는 기법입니다. 아래에서 따로 설명하겠습니다.)
부모 프로세스의 경우 자식 프로세스에서 보낸 값이 들어오면, 해당 값을 buf에 저장하고 종료합니다.
사실 여기서 의문이 드시는 분이 있을 겁니다. 굳이 3개로 나눈 자식 프로세스를 왜? 굳이? 바로 클라이언트에 보내지 않고 다시 부모 프로세스에 보내는지.
이는 앞선 주제와 동일하게 3개의 클라이언트에서 받아온 3개의 문장을 하나로 합쳐 모든 클라이언트에 뿌려 하기에 해당 값들이 다 들어올 때까지 기다려야 합니다. 따라서 자식 프로세스에서 바로 wrtie를 부모 클래스에 데이터를 보내줍니다.
- pipe란
프로세스 간의 통신을 할 때 사용하는 커뮤니케이션의 한 방법이다. 기존의 FORK의 경우 부모의 프로세스를 자식이 그대로 복사한 것이지만, 변수나 함수를 공유하지는 않는다. 이러한 다른 2가지의 프로세스가 통신하기 위해서는 PIPE를 활용해 데이터를 주고받는다. (물론 PIPE 외에도 프로세스 간의 통신을 하기 위한 방법은 여러 개가 있다. 대표적으놀 MESSAGE QUEUE도 있다.)
int p[2];
pipe(p);
... 중략
while(1)
{
... 중략
if(pid == 0){ //자식프로세스
close(server_sockfd);
memset(buf, 0x00, MAXBUF);
n = read(client_temp[cnt], buf, MAXBUF); //클라이언트값 수신 buf[n]=0;
write(p[1], buf, sizeof(buf)); //부모 프로세스에 buf 전송
close(client_sockfd);
return 0;
} else if(pid > 0){ //부모프로세스
n = read(p[0], buf, sizeof(buf)); //자식 프로세스에서 보낸 값 수신
for(int i = 0 ; buf[i]!=0 ; i ++){ //필요한 값 외의 쓰레기값 제거
if(buf[i]=='\n'){
buf[i] = 0;
break;
}
}
... 중략
해당 코드를 보면 파이프를 선언하는 방법은 간단하다 int p [2]와 pipe(p)를 통해서 선언을 한다. 이때 왜 int 배열 2개의 크기를 쓰느냐? 의문이 드실 겁니다. 여기서 p [1]은 쓰기용 파이프 p [0]은 읽기용 파이프라고 보시면 됩니다. 부모에서든 자식에서든 pipe에 넣어주기 위해서는 p [1] pipe의 값을 읽기 위해서는 p [0]을 사용하면 됩니다.
그렇다면 자식 프로세스의 코드를 살펴봅시다.
write(p[1], buf, sizeof(buf)); //부모 프로세스에 buf 전송
이를 해석하면 pipe에 buf를 sizeof(buf)만큼 쓰겠다. 자식 프로세스는 클라이언트에서 받은 값은 buf에 넣어주었고 이를 pipe에 다시 넣어줍니다.
n = read(p[0], buf, sizeof(buf)); //자식 프로세스에서 보낸 값 수신
그럼 이제 부모 프로세스를 살펴보면 pipe에 있는 값을 buf로 끄내 오는 작업을 합니다.
이러한 형식으로 사용되는 게 바로 pipe라고 합니다.
클라이언트 코드
클라이언트 코드는 이전 포스팅의 코드와 동일합니다.
결과 화면
이번 코드를 처음 작성할 때 2가지에 대한 고민이 가장 컸었다. 1) 어떻게 받은 값을 통합할 것인가? 2) 어떻게 받은 값들을 클라이언트에게 보낼 것인가?
이러한 고민은 1) 번의 경우 pipe를 활용해 자식 프로세스의 값들을 모두 부모 프로세스에 전달하여 내용을 통합하였다. 2)의 경우 서버에서 모든 클라이언트에게 통합한 값을 전달하는 방법으로 해결하였다.
해당 프로그램을 전체적인 흐름은 다음과 같이 설계하였다.
- 각 클라이언트들이 서버에 접속한다.
- 각 클라이언트마다 자식 프로세스를 할당한다.
- 자식 프로세스에서 각 클라이언트의 입력값을 저장해 부모 프로세스에 보낸다.
- 부모 프로세스에서 해당 값을 msg문자열에 저장한다.
- 3개의 값이 입력되었다면, msg문자열을 각 클라이언트에게 전송한다.
그러나 이런 코드 역시 효율적이 못함을 인지하고 있다. 프로세스가 3개가 아니라면 오류 동작을 보이게 된다. 좀 더 동적이게 프로그램을 설계해야함을 느끼고 있지만, 리눅스를 처음 다루다 보니, 일반적으로 window에서 작업하는 것보다 자잘한 오류가 너무 많이 발생하여, 하드코딩식으로 코딩하였다. 좀더 리눅스에 익숙해졌을 때 다시 한번 리팩터링을 통해 클린 코드를 작성함을 고려하면서 이본 포스팅을 마친다.