다중 접속 서버

multiconnserver.png

위 그림에서 보이듯이 클라이언트의 서비스 요청(연결 요청)이 있을 때마다 에코 서버는 자식 프로세스를 생성해서 서비스를 제공한다. 즉, 서비스를 요청하는 클라이언트의 수가 다섯이라면 에코 서버는 추가로 다섯 개의 자식 프로세스를 생성해서 서비스를 제공한다. 이를 위해서 에코 서버는 다음의 과정을 거쳐야 한다. 이것이 기존 에코 서버와의 차이점이다.

  • 1단계 : 에코 서버(부모 프로세스)는 accept 함수호출을 통해서 연결요청을 수락한다.
  • 2단계 : 이때 얻게 되는 소켓의 파일 디스크립터를 자식 프로세스를 생성해서 넘겨준다.
  • 3단계 : 자식 프로세스는 전달받은 파일 디스크립터를 바탕으로 서비스를 제공한다.

다중접속 에코 서버의 구현

#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;
    struct sockaddr_in serv_adr, clnt_adr;

    pid_t pid;
    struct sigaction act;
    socklen_t adr_sz;
    int str_len, state;
    char buf[BUF_SIZE];
    if(argc != 2)
    {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    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);
    memset(&serv_sock, 0, sizeof(serv_sock));
    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)
    {
        adr_sz = sizeof(clnt_adr);
        clnt_sock = accept(serv_sock, (struct sockaddr*) &clnt_adr, &adr_sz);
        if(clnt_sock == -1)
            continue;
        else
            puts("new client connected...");
        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;
        }
        else
            close(clnt_sock);
    }
    close(serv_sock);
    return 0;
}

void read_childproc(int sig)
{
    pid_t pid;
    int status;
    pid = waitpid(-1, &status, WNOHANG);
    printf("removed proc id: %d \n", pid);
}

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

fork 함수로 인해 부모 프로세스의 모든 것이 복사된다. 그러나, 소켓은 복사되지 않는다! 소켓을 가리키는 파일 디스크립터만이 복사되었을 뿐이다. 소켓은 프로세스가 아닌 운영체제가 관리하기 때문이다.

하나의 소켓에 두 개의 파일 디스크립터가 존재하는 경우, 두 파일 디스크립터가 모두 종료되어야 소켓이 소멸한다.

socket.png

그래서 위 그림처럼 fork() 분리 이후 각자의 프로세스에서 필요 없는 디스크립터(소켓)를 닫아주어야 한다.

TCP의 입출력 루틴 분할

지금까지 구현한 에코 클라이언트의 데이터 에코 방식은 다음과 같았다.

“서버로 데이터를 전송한다! 그리고는 데이터가 에코되어 돌아올 때까지 기다린다. 무조건 기다린다. 그리고 에코되어 돌아온 데이터를 수신하고 나서야 비로소 데이터를 추가로 전송할 수 있다!”

프로그램 코드의 흐름이 read와 write를 반복하는 구조였기 때문이다.

데이터의 송신과 수신을 멀티 프로세스로 분리해 보자. 부모 프로세스는 데이터의 수신을 담당하고, 자식 프로세스는 데이터의 송신을 담당하도록 하면, 입력과 출력을 담당하는 프로세스가 각각 다르기 때문에 서버로부터의 데이터 수신여부에 상관없이 데이터를 전송할 수 있다. 이러면 각자의 역할이 명확해지고, 데이터 송수신이 잦은 프로그램의 성능향상에 효과가 있다.

에코 클라이언트의 입출력 루틴 분할

#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 buf[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);
    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");
    }

    pid = fork();
    if(pid == 0)
        write_routine(sock, buf);
    else
        read_routine(sock, buf);

    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);
            return;
        }
        write(sock, buf, strlen(buf));
    }
}

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