멀티 프로세스 기반 서버는 다음과 같은 과정을 통해 non-blocking한(즉, 다중 접속 서버를) 구현한다.
- 서버(부모 프로세스)는 accept 함수 호출을 통해서 연결 요청을 수락한다.
- 이때 얻게 되는 소켓의 파일 디스크립터를 자식 프로세스를 생성해서 넘겨준다.
- 자식 프로세스는 전달받은 파일 디스크립터를 바탕으로 서비스를 제공한다.
- 부모 프로세스는 1의 과정을 되풀이하며 지속적으로 클라이언트의 연결 요청을 수락한다. 이후 반복.
// echo_mpserv.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[])
{
if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
}
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];
// SIGCHLD가 발생하면 좀비를 회수하도록 처리한다.
act.sa_handler = read_childproc;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGCHLD, &act, 0);
// tcp 서버 소켓을 만든다.
serv_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 = 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...");
// fork()하여 자식 프로세스에게 클라이언트에게 서비스 제공하는 작업을 전담한다.
// 이때, 자식 프로세스는 부모 프로세스의 메모리 공간을 복사하여 생성되므로 별도의 전달 작업
// 없이 클라이언트와 연결된 소켓을 사용할 수 있다.
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)
{
sig = 0;
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);
}
TCP의 입출력 루틴(Routine) 분할
멀티 프로세스 기반으로 서버를 작성하면 기능 별로 코드를 분할하기 용이하다. 특히 데이터를 송신하는 부분과 데이터를 수신하는 부분을 분할하여 작성하면 코드의 가독성이 크게 증대된다.
입출력 루틴 분할의 또 다른 장점은, 데이터 송수신이 잦은 프로그램의 성능향상을 들 수 있다.

위 그림의 왼쪽은 이전 에코 클라이언트의 데이터 송수신 방식을, 그리고 오른쪽은 입출력 루틴을 분리시킨 에코 클라이언트의 데이터 송수신 방식을 보여준다. 일단 서버에서의 차이는 없다. 차이가 나는 부분은 클라이언트 영역이다. 입출력 루틴이 분리된 클라이언트는 데이터의 수신 여부에 상관없이 데이터 전송이 가능하기 떄문에 연속해서 데이터의 전송이 가능하다. 따라서 동일한 시간 내에서의 데이터 송수신 분량이 상대적으로 많을 수밖에 없다. 그리고 이러한 성능적 차이는 데이터의 전송 속도가 느린 환경에서 더 확실히 드러난다.
// 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[])
{
// 인자 개수 체크
if (argc != 3)
{
printf("Usage : %s <IP> <port>\n", argv[0]);
exit(1);
}
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(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);
}
}
// 출력 루틴 : 표준 입력으로부터 입력받아 서버에 보낸다.
// q 또는 Q라는 입력을 받으면 shutdown 시스템콜을 사용하여
// write 버퍼를 닫는 동시에 서버에 EOF를 보낸다.
// 서버 측은 EOF를 전달받으면 close()를 통하여 클라이언트와의 연결을 끊는다고 기대할 수 있다.
// 그러면 부모에서 동작하고 있는 read_routine에서도 read()가 0을 반환함을 통해
// 연결이 끊겼음을 인지한다.
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 하므로 별도로 좀비를 회수할 필요가 없다.
return ;
}
write(sock, buf, strlen(buf));
}
}
void error_handling(char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
참고자료