본문 바로가기
CS/컴퓨터네트워크

[열혈] 멀티 프로세스 기반 서버구현

by sun__ 2020. 10. 25.

목적 : 멀티 프로세스 기반 서버 구현에 대해 이해

 

프로그램 :

1. 멀티 프로세스를 이용해서 한 번에 복수의 클라이언트와 통신 가능

2. 서버-클라이언트가 연결된 직후 서버 프로그램은 클라이언트의 ip, port번호를 출력

3. 클라이언트가 서버에 메세지를 보내면 서버는 그 메세지를 다시 클라이언트에게 전송함. 클라이언트 프로그램은 해당 메시지 출력.

4. 클라이언트는 q를 입력하여 연결을 끝낼 수 있음. 이 때 half-close 방식 사용

5. 서버는 어떤 클라이언트와 통신이 끝나면 그 클라이언트와 통신을 담당한 프로세스를 종료하고 sigaction을 이용해서 거둬들임. 해당 프로세스 아이디 출력

 

에코 서버-클라이언트, 좀비 프로세스, 고아 프로세스, fork. wait. waitpid, signal, sigaction 에 대한 설명

더보기

에코 서버-클라이언트 : 클라이언트가 메세지를 보내면 서버가 똑같은 메세지를 클라이언트에게 보내서 클라이언트 프로그램에서 다시 출력하는 모델

 

좀비 프로세스 : 자식 프로세스가 종료하여 반환됐을 때 부모 프로세스가 wait, waitpid로 거두지 않는다면 그 자식 프로세스는 좀비 프로세스가 된다.

 

고아 프로세스 : 어떤 프로세스가 종료되기 전 부모 프로세스가 먼저 종료되면 이 프로세스는 고아 프로세스가 된다.

 

fork

#include <unistd.h>

pid_t fork(void);
/*
성공시 자식 프로세스 id, 실패 시 -1 반환
*/

 

wait, waitpid

waitpid를 많이 쓴다

#include <sys/wait.h>

pid_t wait(int * status);
/* 
성공 시 반환된 자식 프로세스의 pid, 실패시 -1 반환.
status엔 자식 프로세스의 정보가 등록된다. 매크로 함수와 같이 사용한다.
WIFEXITED(status) : 자식 프로세스가 정상 종료된 경우 true 반환
WEXITSTATUS(status) : 자식 프로세스의 전달 값 반환
*/

pid_t waitpid(pid_t pid, int * status, int options);
/*
성공 시 반환된 자식 프로세스의 pid, 실패시 -1 반환

pid : 
특정 자식 프로세스의 반환을 기다릴 때 사용 가능
pid에 -1 주면 아무 프로세스의 반환 기다릴 수도 있음.

status : wait와 동일

options : WNOHANG을 인자로 전달하면 블로킹 상태에 빠지지 않음. 함수 종료시키고 0 반환함
*/

 

signal, sigaction

signal은 이제 쓰이지 않는다. 

#include <signal.h>

void (*signal(int signo, void (*func)(int)))(int);
/*
이전에 설정된 시그널 핸들러를 반환
signo : 핸들링할 시그널 번호
SIGALRM : alarm함수호출의 시간이 다 되면 받는 시그널
SIGINT : CTRL+C 입력 시 받는 시그널
SIGCHLD : 자식프로세스가 종료되면 받는 시그널

func : 새로 지정할 시그널 핸들러
*/

int sigaction(int signo, const struct sigaction * act, struct sigaction * oldact);
/*
성공시 0, 실패시 -1 반환
signo : 핸들링할 시그널 번호
act : 새로 지정할 시그널 핸들러를 포함한 sigaction 구조체
oldact : 이전에 설정된 시그널 핸들러를 포함한 정보를 deep copy할 변수
*/

struct sigaction{
    void (*sa_handler)(int); //시그널 핸들러
    sigset_t sa_mask;	//시그널 핸들러를 적용할 다른 시그널 셋 64비트 정수형
    int sa_flags;	//옵션
}
    

 


<fork 시 파일 디스크립터 복제 문제>

위 그림은 멀티프로세스를 사용해서 다중 접속을 허용하는 서버를 그림으로 나타낸 것이다.

 

부모 프로세스가 accept하여 자식 프로세스를 fork로 만들고 그 프로세스에게 연결된 클라이언트와 통신을 맡긴다.

 

자식 프로세스는 부모 프로세스의 서버 소켓과 클라이언트담당 소켓 두 개의 디스크립터를 갖는다. 마찬가지로 부모 프로세스는 서버 소켓과 클라이언트담당 소켓 두 개의 디스크립터를 갖는다.

 

소켓의 디스크립터를 여러 프로세스에서 소지할 시 모든 프로세스에서 해당 디스크립터를 close해야 완전히 종료가 된다.

 

자식 프로세스는 서버 소켓이 필요 없으므로 서버 소켓을 미리 닫아두고, 부모 프로세스는 클라이언트담당 소켓이 필요 없으므로 클라이언트 담당 소켓을 미리 닫아둬야 한다.

 

위 설명을 그림으로 나타내면 다음과 같다.

 


 

<tcp의 입출력 루틴 분할>

 

규모가 큰 프로그램에선 보통 입력버퍼를 담당하는 프로세스와 출력버퍼를 담당하는 프로세스를 분할한다.

 


<코드 설명>

 

1. 멀티 프로세스를 이용해서 한 번에 복수의 클라이언트와 통신 가능

2. 서버-클라이언트가 연결된 직후 서버 프로그램은 클라이언트의 ip, port번호를 출력

3. 클라이언트가 서버에 메세지를 보내면 서버는 그 메세지를 다시 클라이언트에게 전송함. 클라이언트 프로그램은 해당 메시지 출력.

4. 클라이언트는 q를 입력하여 연결을 끝낼 수 있음. 이 때 half-close 방식 사용

5. 서버는 어떤 클라이언트와 통신이 끝나면 그 클라이언트와 통신을 담당한 프로세스를 종료하고 sigaction을 이용해서 거둬들임. 해당 프로세스 아이디 출력

 

 

echo_mpserver.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>

#define BUF_SIZE 30
void error_handling(char* message);
void read_childproc(int sig);

int main(int argc, char** argv) {
	int serv_sock, clnt_sock;
	char buf[BUF_SIZE];
	int str_len, state;

	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t clnt_adr_sz;

	pid_t pid;
	struct sigaction act;

	if (argc != 2) {
		printf("usage : %s <port>\n", argv[0]);
		exit(1);
	}

	//자식 프로세스의 종료 시 SIGCHLD 시그널 발생. 시그널 핸들러(해당 프로세스 아이디 출력) 설정
	act.sa_handler = read_childproc;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;
	state = sigaction(SIGCHLD, &act, 0);

	//주소 설정
	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	if (serv_sock == -1)
		error_handling("socket() error");
	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");


	while (1) {
		clnt_adr_sz = sizeof(clnt_adr);
		clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
		if (clnt_sock == -1) continue;	//accept 실패 시
		else {	//accept 성공 시 개통 성공을 알리는 메세지와 클라이언트의 ip, port번호 출력
			puts("new client connected....");
			printf("connected client IP: %s \t port: %d  \n", inet_ntoa(clnt_adr.sin_addr), ntohs(clnt_adr.sin_port));
		}

		pid = fork(); //연결된 클라이언트와 통신을 담당할 자식 프로세스 생성
		if (pid == -1) {
			close(clnt_sock);
			continue;
		}
		if (pid == 0) {
			close(serv_sock);	//필요없는 소켓 종료
			while ((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
				write(clnt_sock, buf, str_len);

			close(clnt_sock);
			puts("client disconnected....");
			return 0;		//SIGCHLD 발생
		}
		else
			close(clnt_sock);	//서버 소켓에 해당하는 프로세스는 필요없는 소켓 종료 후 다른 클라이언트의 연결 기다리도록

	}
	close(serv_sock);
	return 0;
}

void error_handling(char* message) {
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

void read_childproc(int sig) {
	pid_t pid;
	int status;
    //종료되어 거둬지길 기다리는 자식 프로세스 중 아무거나 받음. WNOHANG이므로 블록되진 않음
	pid = waitpid(-1, &status, WNOHANG);	
	printf("removed proc id: %d \n", pid);
}

 

echo_mpclient.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>


#define BUF_SIZE 30
void error_handling(char* message);
void read_routine(int sock, char* buf);
void write_routine(int sock, char* buf);

int main(int argc, char** argv) {
	int sock;
	pid_t pid;
	char message[BUF_SIZE];
	struct sockaddr_in serv_adr;

	if (argc != 3) {
		printf("usage : %s <IP> <port>\n", argv[0]);
		exit(1);
	}

	//주소 설정
	sock = socket(PF_INET, SOCK_STREAM, 0);
	if (sock == -1)
		error_handling("socket() error");
	memset(&serv_adr, 0, sizeof(serv_adr));
	serv_adr.sin_family = AF_INET;
	serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
	serv_adr.sin_port = htons(atoi(argv[2]));

	if (connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
		error_handling("connect() error");	//연결 실패
	else	//연결 성공
		puts("connected...............");

	//입력 버퍼, 출력 버퍼를 담당할 루틴을 나눔
	pid = fork();
	if (pid == 0) write_routine(sock, message);
	else read_routine(sock, message);
	close(sock);
	return 0;
}

void read_routine(int sock, char* buf) {
	while (1) {
		int str_len = read(sock, buf, BUF_SIZE);
		if (str_len == 0) return;

		buf[str_len] = 0;
		printf("message from server: %s", buf);
	}
}
void write_routine(int sock, char* buf) {
	while (1) {
		fgets(buf, BUF_SIZE, stdin);
		if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n")) {
			shutdown(sock, SHUT_WR); 		//half close
			return;
		}
		write(sock, buf, strlen(buf));
	}
}

void error_handling(char* message) {
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(1);
}

 

실행 결과 캡쳐