Computer Science 기본 지식/소켓 프로그래밍

[TCP/IP 소켓 프로그래밍] (7-2) 다중 접속 서버 - 멀티 프로세스 서버/클라이언트

로파이 2021. 3. 30. 15:21

출처 : 열혈 TCP/IP 소켓 프로그래밍 윤성우 저

 

둘 이상의 클라이언트에게 서비스를 제공하는 멀티 프로세스 기반 다중 접속 서버

멀티 프로세스 기반 다중 접속 서버/ 이미지 출처 : TCP/IP 소켓프로그래밍 (윤성우 저)

클라이언트가 보내는 메세지를 똑같은 내용으로 재전송해주는 에코 서버는 다음 과정을 거친다.

1. 에코 서버는 accept를 통해 클라이언트 연결 요청을 수락한다.

2. 이때 얻게되는 소켓의 핸들을 자식 프로세스에 복사하여 넘겨준다.

3. 자식 프로세스는 전달된 소켓 핸들을 가지고 서비스를 제공한다.

 

다중 접속 에코 서버 구현

 

- 좀비 프로세스 방지

좀비 프로세스가 될 수 있는 자식 프로세스는 프로세스 종료시 반환값을 반드시 부모 프로세스에게 전달해야한다. 그렇지 않는다면, 전달될때 까지 자식 프로세스는 소멸되지 않고 좀비 상태에 있게된다. 따라서 부모 프로세스는 자식 프로세스 종료시 반환되는 값을 전달받을 수 있도록 read_childproc 시그널 핸들링을 꼭 등록하도록한다.

// Register signal handling
struct sigaction act;
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
int state = sigaction(SIGCHLD, &act, 0);

- 부모 프로세스에서 클라이언트 소켓 종료

자신의 영역에서 필요한 소켓 핸들만 남김 / 이미지 출처 : TCP/IP 소켓프로그래밍 (윤성우 저)

부모 프로세스의 역할은 클라이언트 접속을 accept하는 것이고 연결된 클라이언트의 소켓 핸들을 복사하여 자식 프로세스에게 넘겨준다. 따라서 부모 프로세스에게 클라이언트 소켓 핸들은 필요 없으므로 꼭 close를 통해 소켓을 닫는다. 소켓을 닫지 않는다면, 자식 프로세스가 클라이언트에게 서비스를 제공하고 소켓을 닫는데 부모 프로세스에 소켓이 남아있기 때문에 소켓 소멸이 되지 않는다. 

 

- 자식 프로세스에서 서버 소켓 종료

자식 프로세스도 마찬가지로 자신은 클라이언트 소켓 핸들만 필요하고 서버 소켓은 필요가 없다. fork() 이후 자식 프로세스 영역에서 서비스 제공 시 서버 소켓은 닫아주도록한다.

 

echo_mpserv.cpp

#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include "error_handling.h"

#define BUF_SIZE 30
#define PORT 9190
void read_childproc(int sig)
{
    int status;
    pid_t id = waitpid(-1, &status, WNOHANG);
    if(WIFEXITED(status))
    {
        printf("Removed proc id : %d\n", id);
        printf("Child send: %d \n", WEXITSTATUS(status));
    }
}

int main(int argc, char*argv[])
{
    // Register signal handling
    struct sigaction act;
    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    int state = sigaction(SIGCHLD, &act, 0);

    int serv_sock = socket(PF_INET, SOCK_STREAM, 0);
    struct sockaddr_in serv_adr = {};
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_adr.sin_port = htons(PORT);

    if(bind(serv_sock, (sockaddr*)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error");
    
    puts("Server is On...");
    struct sockaddr_in clnt_adr = {};
    while(1)
    {
        socklen_t adr_sz = sizeof(clnt_adr);
        int clnt_sock = accept(serv_sock, (sockaddr*)&clnt_adr, &adr_sz);
        if(clnt_sock == -1)
            continue;
        else
            puts("new client connected...");
        
        pid_t pid = fork();
        if(pid == -1)
        {
            close(clnt_sock);
            continue;
        }
        
        // child process
        if(pid == 0)
        {
            char buf[30] = {};
            int str_len = 0;
            close(serv_sock);
            while((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
                write(clnt_sock, buf, str_len);

            // close client socket
            close(clnt_sock);
            puts("client disconnected...");
            return 0;
        }
        // parent process
        else
        {
            close(clnt_sock);
        }
    }
    close(serv_sock);
    return 0;
}

 

TCP 입출력을 분할한 클라이언트

입출력 분리 / 이미지 출처 : TCP/IP 소켓프로그래밍 (윤성우 저)

기존 에코 클라이언트의 문제점은 메세지 전송 후 추가 전송을 위해 수신될 때까지 기다려야 하는 것이다. 전송 역할과 수신 역할을 독립적으로 분리하여 수신여부와 상관없이 전송이 되도록한다.

 

이를 위해 기존 프로세스를 복사하여 자식 프로세스에서는 전송을 맡고 부모 프레서스는 수신을 맡을 수 있다. 두 프로세스는 한 소켓을 통해 입출력을 수행한다.

 

- 자식 프로세스의 출력 루틴

 // child process - write routine
 while(1)
 {
   fgets(buf, BUF_SIZE, stdin);
   if(!strcmp(buf,"q\n") || !strcmp(buf,"Q\n"))
   {	
   	shutdown(sock, SHUT_WR);
   	break;
   }
   write(sock, buf, strlen(buf));
 }

출력에서는 더 이상 메세지를 보내고 싶지 않은 경우 출력 스트림을 종료시키고 EOF(0)를 전달하기 위해 Half-Close 방식으로 소켓을 종료한다. 출력 스트림을 종료시키지 않고 최종 return에서 자식 프로세스 종료 시 전달한다면, 부모 프로세스의 소켓이 남아있기 때문에 EOF가 전달되지 않는다. 

 

- 부모 프로세스의 입력 루틴

// parent process - read routine
while(1)
{
  int str_len=read(sock, buf, BUF_SIZE);
  if(str_len==0)
  	break;

  buf[str_len]=0;
  printf("Message from server: %s", buf);
}

 

echo_mpclient.cpp

+ 좀비 프로세스를 막기 위한 코드를 기존 책의 코드에서 추가하였다. 

더보기
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/socket.h>
#include "error_handling.h"
#define IP_ADDRESS "127.0.0.1"
#define PORT 9190
#define BUF_SIZE 30

void read_childproc(int sig)
{
    int status;
    pid_t id = waitpid(-1, &status, WNOHANG);
    if (WIFEXITED(status))
    {
        printf("Removed proc id : %d\n", id);
        printf("Child send: %d \n", WEXITSTATUS(status));
    }
}

int main(int argc, char *argv[])
{
    // Register signal handling
    struct sigaction act;
    act.sa_handler = read_childproc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    int state = sigaction(SIGCHLD, &act, 0);

    int sock;
    pid_t pid;
    char buf[BUF_SIZE];
    struct sockaddr_in serv_adr;

    sock = socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0, sizeof(serv_adr));
    serv_adr.sin_family = AF_INET;
    serv_adr.sin_addr.s_addr = inet_addr(IP_ADDRESS);
    serv_adr.sin_port = htons(PORT);

    if (connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("connect() error!");

    pid = fork();
    if (pid == 0)
    {
        // child process - write routine
        while (1)
        {
            fgets(buf, BUF_SIZE, stdin);
            if (!strcmp(buf, "q\n") || !strcmp(buf, "Q\n"))
            {
                shutdown(sock, SHUT_WR);
                break;
            }
            write(sock, buf, strlen(buf));
        }
    }

    else
    {
        // parent process - read routine
        while (1)
        {
            int str_len = read(sock, buf, BUF_SIZE);
            if (str_len == 0)
                break;

            buf[str_len] = 0;
            printf("Message from server: %s", buf);
        }
    }

    close(sock);
    return 0;
}