목적 : 멀티 프로세스 기반 서버 구현에 대해 이해
프로그램 :
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);
}
실행 결과 캡쳐
'CS > 컴퓨터네트워크' 카테고리의 다른 글
[열혈] 멀티쓰레드 기반 서버구현 (0) | 2020.12.22 |
---|---|
[열혈] 멀티플렉싱 기반 서버구현 (0) | 2020.11.24 |
[열혈] dns (0) | 2020.10.24 |
[열혈] half close (0) | 2020.10.24 |
[열혈] 단순한 서버-클라이언트 코드 구조의 이해 (1) | 2020.10.19 |