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

[TCP/IP 소켓 프로그래밍] (7-3) 다중 접속 서버 - 프로세스 간 통신 IPC

로파이 2021. 3. 31. 21:55

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

 

fork로 복제된 자식 프로세스는 부모 프로세스와 완전히 독립적인 메모리 공간을 가지고 있기 때문에 자료를 공유할 수 있는 방법이 쉽지 않다.

 

PIPE

따라서 프로세스 간 자원을 주고 받기 위해서는 파이프라는 것을 생성해야한다. 파이프는 운영체제 자원으로 통신을 위한 메모리 공간을 가진다. 두 프로세스는 파이프의 메모리를 통해 자원을 주고 받을 수 있다.

 

- 운영체제 지원이 필요한 이유

두 프로세스는 메모리를 공유하지 않으므로, 즉 공유 공간이 없으므로 두 프로세스가 동시에 접근 가능한 메모리 영역을 할당하기 위해 운영체제의 도움이 필요하다.

int pipe(int filedes[2]);
  • 성공 시 0, 실패 시 -1 반환
  • filedes[0] 파이프로부터 데이터를 수신하는데 사용되는 파일 디스크립터가 저장된다.
  • filedes[1] 파이프로 데이터를 전송하는데 사용되는 파일 디스크립터가 저장된다.

위 함수를 통해 파이프 메모리에 접근할 수있는 두 개의 디스크립터를 운영체제로 부터 할당 받게 된다. 파이프를 이용하여 데이터 송수신을 하고 싶다면, 입구 또는 출구의 디스크립터 중 하나를 자식 프로세스에 전달해야 한다.

#include <cstdio>
#include <unistd.h>
#define BUF_SIZE 30

int main(int argc, char* argv[])
{
    int fds[2];
    char str[] = "Who are you?";
    char buf[BUF_SIZE];
    pid_t pid;


    pipe(fds);
    pid = fork();
    // 자식 프로세스일 경우
    if(pid == 0)
    {
        write(fds[1], str, sizeof(str));
    }
    // 부모 프로세스일 경우
    else
    {
        read(fds[0], buf, BUF_SIZE);
        puts(buf);
    }
    return 0;
}

위 예제에서 자식 프로세스는 파이프 입구로 데이터를 write하고 부모 프로세스는 출구로 부터 데이터를 읽는다.

위의 경우 자식 프로세스에서 부모 프로세스에게만 데이터를 전송하고 있다.

 

- 양방향 Pipe 통신 예제

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

한 파이프를 통해 양쪽에서 데이터를 주고 받을 수 있으며, 각 파일 디스크립터를 입구와 출구로 사용하기만 하면된다.

int main(int argc, char* argv[])
{
    int fds[2];

    char str1[]="Who are you";
    char str2[]="Thank you for your message";
    char buf[BUF_SIZE];
    pid_t pid;

    pipe(fds);
    pid = fork();
    // 자식 프로세스
    if(pid == 0)
    {
        write(fds[1], str1, sizeof(str1));
        sleep(2);
        read(fds[0], buf, BUF_SIZE);
        printf("Child proc ouput: %s \n", buf);
    }
    // 부모 프로세스
    else
    {
        read(fds[0], buf, BUF_SIZE);
        printf("Parent proc output : %s \n", buf);
        write(fds[1], str2, sizeof(str2));
        sleep(3);
    }
    return 0;
}
  1. 자식 프로세스가 데이터를 전송하고 잠시 대기한다.
  2. 부모 프로세스는 데이터를 수신받고 데이터를 전송한다.
  3. 자식 프로세스는 부모 프로세스로부터 전송된 데이터를 수신받는다.

자식 프로세스가 잠시 대기하는 이유는 파이프에 쓰여진 데이터를 먼저 읽는 프로세스가 데이터를 전달받기 때문이다. 따라서 보낸 쪽의 프로세스임에도 수신쪽보다 먼저 읽는다면 데이터가 전해지지 않는다. 파이프의 입출구를 통해 메모리 공간을 어느 프로세스이든 데이터를 읽을 수 있기 때문에 데이터 흐름에 유의해야한다.

 

따라서 다음과 같이 파이프를 두 개를 사용하여 부모 프로세스용 자식 프로세스용으로 분리하여 사용가능하다.

int main(int argc, char* argv[])
{
    int fds1[2], fds2[2];
    char str1[] = "who are you?";
    char str2[] = "thank you for your message";
    char buf[BUF_SIZE];
    pid_t pid;

    pipe(fds1), pipe(fds2);
    pid = fork();
    if(pid == 0)
    {
        write(fds1[1], str1, sizeof(str1));
        read(fds2[0],buf,BUF_SIZE);
        printf("Child proc output %s\n", buf);
    }
    else{
        read(fds1[0], buf , BUF_SIZE);
        printf("Parent proc output: %s \n", buf);
        write(fds2[1], str2, sizeof(str2));
        sleep(3);
    }
    return 0;
}

 

메세지를 저장하는 에코 서버

클라이언트로 부터 수신한 메세지를 저장하는 에코 서버를 구현한다.

 

에코 서버에서 가지는 자식 프로세스의 종류는 다음과 같다.

1. 프로세스 1: 저장하는 프로세스는 fork()로 복사하여 생성한다.

2. 프로세스 2,...n: 클라이언트와 연결이 될 때마다 새로운 프로세스를 생성하여 클라이언트에게 에코 메세지를 전송하는 역할과 파이프를 통해 프로세스 1에게 에코 메세지를 전달한다.

 

- 파이프 기반 에코 메세지 수신

// pipe
int fds[2];
pipe(fds);
pid_t pid = fork();

// child process 1
if(pid == 0)
{
  FILE* fp = fopen("echomsg.txt", "wb");
  char msgbuf[BUF_SIZE];
  int i, len;

  // write message from client into files 
  for(i = 0; i < 10; ++i)
  {
    len = read(fds[0], msgbuf, BUF_SIZE);
    fwrite((void*)msgbuf, 1, len, fp);
  }
  fclose(fp);
  return 0;
}

 먼저 파이프를 생성하여 에코 메세지를 저장하는 자식 프로세스 부분이다.  파이프로부터 데이터를 최대 10번 수신하여 메세지를 텍스트 파일에 기록한다.

 

- 멀티 프로세스 기반 다중 접속 서버

    while(1)
    {
        adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (sockaddr*)&clnt_adr, &adr_sz);
        if(clnt_sock == -1)
            continue;
        else
            puts("new client connected...");

        pid = fork();
        // child process 2
        if(pid == 0)
        {
            close(serv_sock);

            char buf[BUF_SIZE];
            int str_len;

            while((str_len = read(clnt_sock, buf, BUF_SIZE)) != 0)
            {
                // echo
                write(clnt_sock, buf, str_len);
                // receive message from client and pass it to child1 process through pipe
                write(fds[1], buf, str_len);
            }

            close(clnt_sock);
            puts("client disconnected...");
            return 0;
        }
        else
            close(clnt_sock);
    }

클라이언트와 접속될 때마다 새로운 프로세스를 생성하고 기존 서버 소켓을 닫는다. 클라이언트 프로세스는 소켓으로 부터 데이터를 수신받아 클라이언트 소켓으로 에코 메세지를 전송하고 파이프의 입구를 통해 프로세스 1에게 메세지를 전달한다.