Part1_멀티태스킹 기반의 서버 구현(2)

·2023년 11월 23일
0
post-custom-banner

[시그널을 통한 좀비 프로세스의 소멸]

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <stdlib.h>

void z_handler(int sig);

int main(int argc, char** argv)
{
	pid_t pid;
	int state, i;

	struct sigaction act;
	act.sa_handler = z_handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;

	// 시그널 핸들러 등록
	// SIGCHLD는 자식 프로세스가 종료됐을 때 발생하는 시그널
	state = sigaction(SIGCHLD, &act, 0); 

	if(state != 0)
	{
		puts("sigaction() error ");
		exit(1);
	}

	pid = fork();

	if(pid == 0)
	{
		// getpid()는 현재 실행되고 있는 프로세스의 ID를 리턴해줌
		// 자식 프로세스에 의해 실행되는 영역이므로 자식 프로세스의 ID를 출력하게 됨
		printf("자식 프로세스 생성 : %d \n", getpid());
		// 자식 프로세스가 종료하면서 3 반환
		exit(3);
	}
	else
		// 자식 프로세스가 부모 프로세스보다 먼저 종료될 수 있게 부모 프로세스의 종료는 3초 늦춤
		sleep(3);

	return 0;
}

void z_handler(int sig)
{
	pid_t pid;
	int rtn;
	
	// waitpid를 통해 자식 프로세스의 리턴값을 읽어들이고 있음
	while( (pid = waitpid(-1, &rtn, WNOHANG) ) > 0 )
	{
		// 자식 프로세스의 리턴 값으로 3을 넣어줬으니 이 안으로 들어옴
		// 여러 개의 자식 프로세스가 종료되는 상황을 염려해서 while 문 안에서 반복적으로 처리해 줌
		printf("소멸된 좀비의 프로세스 ID : %d \n", pid);
		printf("리턴된 데이터 : %d \n\n", WEXITSTATUS(rtn));
	}
}

[실행 결과]

→ 자식 프로세스의 ID과 소멸된 좀비 프로세스의 ID가 같은 것을 볼 수 있음

→ 자식 프로세스가 종료시 exit(3)을 실행하기 때문에 리턴된 데이터도 3인 것을 확인할 수 있음

z_handler 함수 안에서 SIGCHLD 시그널을 처리하는 데 있어서 while 루프를 돌면서 종료한 모든 프로세스를 찾아내려고 하고 있음

마치 여러 프로세스가 종료되더라도 SIGCHLD 시그널이 하나만 발생할 수도 있다는 의미처럼 보임

실제로 signal 함수를 통해서 시그널 핸들러를 설정하는 경우 이러한 일이 발생할 수도 있음

그러나 sigaction 함수를 사용해서 시그널 핸들러를 설정하는 경우에는 이런 문제점은 발생하지 않음

따라서 루프를 돌면서까지 waitpid 함수를 호출할 필요는 없지만 문제점을 제시하기 위해 포함시킴

[fork 함수를 이용한 다중 접속 서버의 구현]

(1) 프로세스 기반의 다중 접속 서버의 구현 모델

이전에 구현한 echo 서버의 경우 한번에 하나의 클라이언트에 대한 연결 요청만 수락했었음

즉, 동시에 둘 이상의 클라이언트에게 서비스를 해 주지는 못했음

여러 클라이언트들이 동시에 접속 가능하도록 echo 서버를 변경할 거임

그림에서 보면 클라이언트가 연결 요청을 할 때마다, echo 서버는 새로운 프로세스를 생성해서 클라이언트의 연결 요청을 수락하고 있음

→ 연결 요청이 들어오면 accept 함수 호출을 통해서 연결을 수락하고, 이 때 생성된 소켓의 파일 디스크립터를

새로운 프로세스에 전달해서 데이터 송/수신을 진행할 거임

(즉, 연결 요청을 한 클라이언트가 셋이라면 서버가 만들어내는 프로세스의 수도 셋임)

(2) 프로세스 기반의 다중 접속 에코 서버 구현하기

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

#define BUFSIZE 30

void error_handling(char* message);
void z_handler(int sig);

int main(int argc, char** argv)
{
	int serv_sock;
	int clnt_sock;
	struct sockaddr_in serv_addr;
	struct sockaddr_in clnt_addr;

	struct sigaction act;
	int addr_size, str_len, state;
	pid_t pid;
	char message[BUFSIZE];

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

	act.sa_handler = z_handler;
	sigemptyset(&act.sa_mask);
	act.sa_flags = 0;

	// 시그널 핸들러 등록
	state = sigaction(SIGCHLD, &act, 0);
	if(state != 0)
	{
		puts("sigaction() error");
		exit(1);
	}

	serv_sock = socket(PF_INET, SOCK_STREAM, 0);
	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
	serv_addr.sin_port = htons(atoi(argv[1]));

	if(bind(serv_sock, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1)
		error_handling("bind() error");

	if(listen(serv_sock, 5) == -1)
		error_handling("Listen() error");

	while(1)
	{
		addr_size = sizeof(clnt_addr);
		clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &addr_size);
		if(clnt_sock == -1)
			continue;

		// 클라이언트와의 연결을 독립적으로 생성
		if( (pid = fork() ) == -1)
		{
			close(clnt_sock);
			continue;
		}
		else if(pid > 0) // 부모프로세스인 경우
		{
			puts("연결 생성");
			close(clnt_sock);
			continue;
		}
		else // 자식 프로세스의 경우
		{
			close(serv_sock);
		
			// 자식 프로세스의 처리 영역 : 데이터 수신 및 전송
			while( (str_len = read(clnt_sock, message, BUFSIZE) ) != 0 )
			{
				write(clnt_sock, message, str_len);
				write(1, message, str_len);
			}

			puts("연결 종료");
			close(clnt_sock);
			exit(0);
		}
	}
	
	return 0;
}

void z_handler(int sig)
{
	pid_t pid;
	int rtn;

	pid = waitpid(-1, &rtn, WNOHANG);
	printf("소멸된 좀비의 프로세스 ID : %d \n", pid);
	printf("리턴된 데이터 : %d \n\n", WEXITSTATUS(rtn));
}

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

[실행 결과]

(1) client 1

(2) client 2

(3) server

둘 이상의 에코 클라이언트가 동시에 에코 서버에 접속해서 서비스를 받고 있음

[fork 함수 호출에 의한 파일 디스크립터의 복사]

fork 함수 호출 이후에 serv_sock과 clnt_sock가 복사됨

그러나 서버의 부모 프로세스에서는 clnt_sock이 필요 없고,

서버의 자식 프로세스에서는 serv_sock이 필요 없으므로 바로 종료해 줌

파일 디스크립터가 복사되는 경우 하나의 소켓에 두 개의 파일 디스크립터가 존재하게 됨

이 경우 하나의 파일 디스크립터가 종료되어도 소켓은 종료되지 않음

(두 개의 파일 디스크립터가 모두 종료되어야 소켓이 종료됨)

따라서 10-16과 같이 프로그램을 유지하면 클라이언트가 종료되어도 serv_sock이 남아있으므로 종료되지 않음

그림 10-17과 같은 모습이 되도록 서버를 구현해야 함

[TCP 입출력 루틴 분할하기]

지금까지 만든 에코 클라이언트 데이터 교환 방식은 데이터를 서버로 전송하면 에코 서버가 데이터를 에코 해 줄 때 까지 기다려야 했음

수신 이후에 다시 데이터를 서버로 전송할 수 있는 형식이었음

→ 하나의 프로세스만을 사용했기 때문

이번엔 데이터를 전송하는 프로세스와 데이터를 수신하는 프로세스를 각각 분리 해 볼거임

그림 10-18을 보면 클라이언트는 서버와의 연결 이후에

부모 프로세스는 서버로부터 데이터를 수신하기 위한 목적으로 사용되고,

자식 프로세스는 서버로 데이터를 전송하기 위한 목적으로 사용되고 있음

→ 데이터를 수신하는 입력 프로세스와 데이터를 전송하는 출력 프로세스를 분리시켜 주기 때문에

클라이언트 입장에서는 서버로부터 데이터 수신 유무와 상관 없이 데이터 전송이 가능하게 됨

이런식으로 구현 했을 때의 장점은 무엇일까?

(1) 하나의 프로세스가 하나의 역할만을 담당하기 때문에 신경 쓸 부분이 적어짐

(2) 데이터의 송/수신이 잦은 프로그램의 성능을 향상시킬 수 있음

왼쪽 클라이언트는 반드시 서버로부터 에코된 데이터를 수신하고 난 다음에 전송함

그러나 오른쪽 클라이언트는 서버로부터 데이터를 수신하기도 전에 또 다른 데이터를 에코 서버로 전송할 수 있음

→ 네트워크 트래픽이 많이 발생해서 호스트간 데이터 전송 속도가 현저하게 느릴 경우의 효율성은 차이를 보이게 됨

[입/출력을 분리시킨 에코 클라이언트]

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

#define BUFSIZE 30

void error_handling(char* message);

int main(int argc, char** argv)
{
	int sock;
	pid_t pid;
	char message[BUFSIZE];
	int str_len, recv_len, recv_num;
	struct sockaddr_in serv_addr;

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

	sock = socket(PF_INET, SOCK_STREAM, 0);

	memset(&serv_addr, 0, sizeof(serv_addr));
	serv_addr.sin_family = AF_INET;
	serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
	serv_addr.sin_port = htons(atoi(argv[2]));

	if(connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
		error_handling("connect() error!");
	
	pid = fork();
	if(pid == 0) // 자식 프로세스의 실행 영역
	{
		while(1)
		{
			fputs("전송할 메시지를 입력하세요 (q to quit) : ", stdout);
			fgets(message, BUFSIZE, stdin);
			if(!strcmp(message, "q\n"))
			{
				shutdown(sock, SHUT_WR);
				close(sock);
				exit(0);
			}
			write(sock, message, strlen(message));
		} // while(1) end
	}
	else // 부모 프로세스의 실행 영역
	{
		while(1)
		{
			int str_len = read(sock, message, BUFSIZE);
			if(str_len == 0)
				exit(0);
		}
		message[str_len] = 0;
		printf("서버로부터 전송된 메시지 : %s\n", message);
	}

	close(sock);
	return 0;
}

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

[실행 결과]

post-custom-banner

0개의 댓글