멀티 프로세스는 부모 프로세스가 자신의 프로세스를 복제하여 자식 프로세스를 생성해서 처리하는 방식으로 여러개의 프로세스로 다중 처리를 하는 방식입니다.
이것을 서버 운용 방식에 응용한 멀티 프로세스 서버는 클라이언트 세션을 프로세스 단위로 나누어서 처리하여 다중 접속, 다중 요청 처리를 가능하게 합니다.
멀티 프로세스는 프로세스들마다 격리된 환경이기때문에 부모 프로세스와 자식 프로세스간에 변수 공유가 불가능합니다.
그래서 데이터를 공유하기 위해서는 프로세스간 통신 방식인 IPC나 공유메모리를 이용해야합니다.
리눅스 C에서 멀티 프로세스 서버는 fork를 이용해 개발할 수 있습니다.
다음 예제는 멀티 프로세스 방식의 에코 서버 애플리케이션 소스 예제입니다.
서버 프로세스가 클라이언트 세션을 생성할 때 마다 fork를 통해 새로운 프로세스를 생성합니다.
fork의 리턴값은 해당 프로세스 pid 값인데 자식 프로세스일 경우 0을 리턴합니다.
따라서 fork 리턴값이 0일 분기를 자식프로세스가 생성된 시점으로 처리해주면 됩니다.
fork는 프로세스 단위이기때문에 변수 공유가 불가능하다 했습니다.
하지만 프로세스 복제가 일어나면서 변수도 복제가 일어나기 때문에 예제에서는 자식 프로세스에서 클라이언트 소켓 fd를 이용해 세션을 사용할 수 있습니다.
소스 중간에 "signal(SIGCHLD, SIG_IGN);" 부분이 있는데 해당 부분은 좀비 프로세스 발생을 방지 하기 위한 것입니다.
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#define MAX_CLIENT 5
#define BUF_SIZE 1024
int client_index = 0;
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Usage : %s [port]\n", argv[0]);
exit(0);
}
int pid;
int server_sock, client_sock;
if ((server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0)
{
perror("socket error : ");
exit(0);
}
int on = 1;
if(setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0) {
perror("socket option set error : ");
exit(0);
}
struct sockaddr_in server_addr, client_addr;
int client_addr_size = sizeof(client_addr);
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(atoi(argv[1]));
if(bind(server_sock , (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0 )
{
perror("bind error : ");
exit(0);
}
if(listen(server_sock, 5) < 0 )
{
perror("listen error : ");
exit(0);
}
signal(SIGCHLD, SIG_IGN);
char buf[BUF_SIZE];
while(1)
{
printf("accept...\n");
client_sock = accept(server_sock, (struct sockaddr *)&client_addr, (socklen_t *)&client_addr_size);
if (client_sock < 0)
{
perror("Accept error : ");
continue;
}
if(client_index == MAX_CLIENT) {
printf("client accept full(max client count : %d)\n", MAX_CLIENT);
close(client_sock);
continue;
}
client_index++;
printf("client accepted(Addr: %s, Port: %d)\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
pid = fork();
if (pid == 0)
{
printf("pid:%d\n", pid);
while(1)
{
memset(buf, 0x00, BUF_SIZE);
if (read(client_sock, buf, sizeof(buf)) <= 0)
{
printf("Client %d close\n", client_sock, buf);
client_index--;
close(client_sock);
break;
}
printf("read : %s\n", buf);
if(write(client_sock, buf, sizeof(buf)) <= 0) {
printf("Client %d close\n", client_sock, buf);
client_index--;
close(client_sock);
break;
}
printf("write : %s\n", buf);
}
}
if (pid == -1)
{
close(client_sock);
perror("fork error : ");
continue;
}
}
close(server_sock);
return 0;
}
멀티 프로세스의 장점은 프로세스마다 격리된 환경이기때문에 프로세스간에 서로 미치는 영향 요소가 적으며 디버깅이 수월합니다.
단점은 자신의 프로세스를 복제하기때문에 프로세스를 생성하는 데에 소모되는 자원이 크기때문에 멀티쓰레드 방식보다 속도가 느립니다.
따라서 웹 서버같이 세션 확립 요청이 잦은 환경에서는 적합하지 않고 자원 이슈에 대한 비중이 크지 않고 요청이 잦지 않은 환경에서는 멀티 프로세스 방식을 채택하는것이 효율적일 것입니다.
멀티 쓰레드는 하나의 프로세스에서 여러개의 쓰레드 단위로 나누어서 다중 처리를 하는 방식입니다.
멀티 쓰레드 서버는 클라이언트 세션을 쓰레드 단위로 나누어서 처리합니다.
멀티 쓰레드는 같은 프로세스 내에 존재하기때문에 멀티 프로세스와는 달리 격리된 환경이 아닙니다.
그래서 쓰레드간에 변수 공유가 가능합니다.
리눅스 C에서 멀티 쓰레드 서버는 pthread를 이용해 개발할 수 있습니다.
다음 예제는 리눅스 C 기반의 멀티 쓰레드 방식의 에코 서버 애플리케이션 소스 예제입니다.
#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#define MAX_CLIENT 5
#define BUF_SIZE 1024
void *t_function(void *data);
int client_index = 0;
int main(int argc, char **argv)
{
if (argc != 2)
{
printf("Usage : %s [port]\n", argv[0]);
return 1;
}
int server_sock, client_sock;
pthread_t thread_client[MAX_CLIENT];
if((server_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) < 0 )
{
printf("socket create error\n");
return -1;
}
int on = 1;
if(setsockopt(server_sock, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) < 0)
{
printf("socket option set error\n");
return -1;
}
struct sockaddr_in server_addr, client_addr;
int client_addr_size = sizeof(client_addr);
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(atoi(argv[1]));
if(bind(server_sock, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0 )
{
printf("bind error\n");
return -1;
}
if(listen(server_sock, 5) < 0)
{
printf("listen error\n");
return -1;
}
while(1)
{
printf("accept...\n");
client_sock = accept(server_sock, (struct sockaddr *)&client_addr, (socklen_t *)&client_addr_size);
if(client_sock < 0)
{
printf("accept error\n");
}
if(client_index == MAX_CLIENT)
{
printf("client accept full(max client count : %d)\n", MAX_CLIENT);
close(client_sock);
continue;
}
if(pthread_create(&thread_client[client_index], NULL, t_function, (void *)&client_sock) != 0 )
{
printf("Thread create error\n");
close(client_sock);
continue;
}
client_index++;
printf("client accepted(Addr: %s, Port: %d)\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
}
return 0;
}
void *t_function(void *arg)
{
int client_sock = *((int *)arg);
pid_t pid = getpid(); // process id
pthread_t tid = pthread_self(); // thread id
printf("pid:%u, tid:%x\n", (unsigned int)pid, (unsigned int)tid);
char buf[BUF_SIZE];
while(1)
{
memset(buf, 0x00, sizeof(buf));
if (read(client_sock, buf, sizeof(buf)) <= 0)
{
printf("Client %d close\n", client_sock, buf);
client_index--;
close(client_sock);
break;
}
printf("read : %s\n", buf);
if(write(client_sock, buf, sizeof(buf)) <= 0)
{
printf("Client %d close\n", client_sock, buf);
client_index--;
close(client_sock);
break;
}
printf("write : %s\n", buf);
}
}
멀티 쓰레드의 장점은 프로세스에 비해 경량된 요소이기 때문에 비교적 자원소모가 적고 속도가 빠릅니다.
단점은 디버깅이 어렵고 격리되지 않은 환경으로 인해 특정 쓰레드에 문제가 생겼을 때 다른 쓰레드에 영향을 미칠 요소가 큽니다.
따라서 신중하게 다루어야하며 요청이 잦은 서버나 빠른 속도가 필요한 환경에서는 멀티 쓰레드 방식을 선택하는것이 효율적일 수 있습니다.
다음은 리눅스 C 기반의 에코 클라이언트 소스 예제입니다.
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#define BUF_SIZE 1024
int main(int argc, char **argv)
{
if(argc != 3)
{
printf("usage : %s ip_Address port\n", argv[0]);
exit(0);
}
int client_sock;
if((client_sock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
{
perror("socket error : ");
exit(1);
}
struct sockaddr_in client_addr;
int client_addr_size = sizeof(client_addr);
client_addr.sin_family = AF_INET;
client_addr.sin_addr.s_addr = inet_addr(argv[1]);
client_addr.sin_port = htons(atoi(argv[2]));
if(connect(client_sock, (struct sockaddr*)&client_addr, client_addr_size) < 0)
{
perror("connect error : ");
exit(1);
}
char buf[BUF_SIZE];
while(1)
{
memset(buf, 0x00, sizeof(buf));
printf("write : ");
fgets(buf, sizeof(buf), stdin);
buf[strlen(buf)-1] = '\0';
if(write(client_sock, buf, sizeof(buf)) <= 0)
{
close(client_sock);
break;
}
memset(buf, 0x00, sizeof(buf));
if (read(client_sock, buf, sizeof(buf)) <= 0)
{
close(client_sock);
break;
}
printf("read : %s\n", buf);
}
return 0;
}