개요
accept() 함수의 경우 한번에 하나의 클라이언트와 연결을 맺을 수 있었다. 그렇다면 for문을 돌려 여러번의 accept() 함수를 호출할 수 있지 않을까 생각이 들 수 있다. 하지만 한 클라이언트와 연결을 설정한 후에는 해당 클라이언트가 연결을 종료할 때까지 또 다른 클라이언트와 통신할 수 없다. 이때, 사용해야 하는 것이 멀티 프로세스이며, 이에 사용되는 시스템 콜이 바로 fork()이다.
fork() 시스템 콜
특정 프로세스에서 fork() 시스템 콜을 호출하면 호출한 프로세스와 똑같은 메모리 자원이 메모리에 그대로 복사된다. 이때, 복사된 프로세스를 자식 프로세스라고 하고, 복사한 프로세스를 부모 프로세스라고 한다. 즉, 부모는 자식에게 본인의 프로세스를 복사해서 전달하며, 자식은 해당 프로세스 자원을 받아 자신만의 작업을 실행할 수 있다.
#include <unistd.h>
pid_t fork(void);
이러한 fork() 함수는 다음과 같이 부모 프로세스인지 혹은 자식 프로세스인지에 따라서 그 리턴값이 구분된다.
- 부모 프로세스 : 생성한 자식 프로세스의 pid(process id)
- 자식 프로세스 : 0
- 만약에 부모 프로세스에서 오류가 발생하면, 자식 프로세스는 생성되지 않고, -1을 반환한다.
따라서 fork 함수를 호출한 후, 그 리턴값으로 부모 프로세스와 자식 프로세스를 구분하고, 그에 맞는 작업을 수행하도록 할 수 있다.
echo server 구현
그럼 fork를 이용해 다시 echo server에 대해 생각해보면, 우리가 fork 함수를 이용해서 멀티 프로세스를 구현하면, 여러개의 자식 프로세스를 생성해서 각각 하나의 클라이언트와 통신하도록 할 수 있으며, 부모는 기존에 하던 작업을 계속해서 수행할 수 있다. 다음 코드에서 server에서 socket, bind, listen 과정이 구현되었다고 가정하고, 여러 개의 자식 프로세스가 각각의 클라이언트와 통신할 수 있음을 보이는 부분에 집중해보자.
for(;;) {
clientlen = sizeof(clntaddr);
connfd = accept(listenfd, (struct sockaddr*) &clntaddr, &clientlen);
if(connfd == -1)
error_handling("accept error");
// 자식 프로세스이면, echo 기능 실행
if((childpid = fork()) == 0 {
close(listenfd);
str_echo(connfd);
close(connfd);
prinft("클라이언트 연결이 해제되었습니다(pid=%d)\n", getpid());
exit(0);
}
// 부모 프로세스이면, 해당 소켓 close 이후에 또 다른 클라이언트를 accept
printf("fork(), child pid = %d\n", childpid);
close(connfd);
}
close(listenfd);
우선 childpid = fork()의 값을 통해 부모 프로세스인지 자식 프로세스인지 구분할 수 있다.
이때, 반환값이 0인 경우에는 자식 프로세스에 해당하므로 연결 요청을 수락한 클라이언트(connfd)에 대해 echo 서비스를 제공하도록 한다.
그렇지 않은 경우에는 부모 프로세스에 해당하므로 기존의 connfd를 닫고 다시 connfd = accept(); 부분으로 돌아가서 또 다른 클라이언트의 연결을 수락한다.
여기서 echo 기능은 다음과 같이 구현이 가능하다.
void str_echo(int connfd) {
ssize_t n;
char buf[100];
again:
while((n = read(connfd, buf, BUF_SIZE)) > 0) {
// 받은 문자열을 그대로 echo한다.
write(connfd, buf, n);
}
if (n < 0 && errno == EINTR)
goto again;
else if (n < 0)
error_handling("read() error");
}
echo client 구현
클라이언트의 경우에는 서버와 달리 기존의 코드 그대로 connect 요청을 하면 된다. 그리고 연결 요청이 완료되면 str_cli 함수를 호출해서 서버와 데이터를 송수신한다. 아래의 경우에는 소켓 생성 및 주소 설정은 모두 이루어졌다고 가정하고 구현을 이어간다. str_cli 내에서 서버와 에코 통신을 계속 진행하고, input == NULL 인 경우에 종료하도록 한다. 여기서 fgets 함수는 표준 입력으로 ctrl+D를 입력받은 경우에 그 값을 NULL로 저장한다.
// 서버에 연결을 요청한다.
if (connect(sockfd, (struct sockaddr*) &serv_addr, sizeof(serv_addr)) == -1) {
error_handling("connect() error");
} else {
printf("connect() return!\n");
str_cli(stdin, sockfd);
}
void str_cli(FILE *fp, int sockfd) {
int n;
char sendline[BUF_SIZE];
char recvline[BUF_SIZE];
while(1) {
fputs("메시지를 입력하세요: ", stdout);
char *input = fgets(sendline, BUF_SIZE, fp);
if (input == NULL)
break;
write(sockfd, sendline, strlen(sendline));
if ((n = read(sockfd, recvline, BUF_SIZE)) == 0)
error_handling("str_cli error 발생!");
recvline[n] = 0;
fputs(recvline, stdout);
}
}
'컴퓨터 사이언스 > Network' 카테고리의 다른 글
클라이언트의 시스템 콜(connect) (1) | 2023.11.21 |
---|---|
stream socket vs datagram socket (0) | 2023.11.21 |
서버의 시스템 콜(bind, listen, accept) (0) | 2023.11.21 |
HTTP version별 특징 (0) | 2023.11.21 |
proxy 서버 만들기 (0) | 2023.11.21 |