모든 내용은 [윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어] 를 기반으로 제 나름대로 이해하여 정리한 것입니다. 다소 부정확한 내용이 있을수도 있으니 이를 유념하고 봐주세요!
# 클라이언트를 위한 서버라면, 모든 클라이언트의 만족도를 평균 이상으로 끌어올려야 한다.
- 전체적인 서비스 제공시간이 조금 늦어지더라도, 연결요청을 해오는 모든 클라이언트에게 "동시에" 서비스를 제공해서 평균적인 만족도를 높일 필요가 있음
- 네트워크 프로그램은 CPU의 연산을 필요로 하지 않는 데이터의 송수신(입출력) 시간이 큰 비중을 차지하므로,
둘 이상의 클라이언트에게 동시에 서비스를 제공하는 것이 CPU를 보다 효율적으로 사용하는 방법이 됨
# 다중접속 서버의 구현방법들
1. 멀티프로세스 기반 서버 : 다수의 프로세스를 생성하는 방식으로 서비스 제공 - 윈도우에서는 지원하지 않음
2. 멀티플렉싱 기반 서버 : 입출력 대상을 묶어서 관리하는 방식으로 서비스 제공
3. 멀티쓰레딩 기반 서버 : 클라이언트의 수만큼 쓰레드를 생성하는 방식으로 서비스 제공
# 프로세스 : 메모리 공간을 차지한 상태에서 실행중인 프로그램
# CPU의 코어 수만큼 프로세스는 동시 실행이 가능.
코어의 수를 넘어서는 개수의 프로세스가 생성되면,
프로세스 별로 코어에 할당되는 시간이 나뉘게 됨.
# 프로세스 ID(PID)
- 모든 프로세스는 생성 형태에 관계없이 운영체제로부터 ID를 부여받음
- 2 이상의 정수형태를 띔
(숫자 1은 운영체제가 시작되자마자 실행되는(운영체제의 실행을 돕는) 프로세스에게 할당됨)
- ps au(a와 u는 옵션)를 통해 현재 실행중인 프로세스 확인 가능
# fork 함수호출을 통한 프로세스의 생성
#include <unistd.h>
pid_t fork(void);
// -> 성공시 프로세스 ID, 실패시 -1 반환
- fork 함수는 호출한 프로세스의 "복사본을 생성"함
- 이미 실행중인(fork 함수를 호출한) 프로세스를 복사함
- 두 프로세스 모두, "fork 함수반환 이후" 문장을 실행하게 됨.
- 이 둘은 메모리 영역까지 동일하게 복사하기 때문에 "복사 시점에는" 완전히 동일함
=> 복사 이후의 프로그램 흐름은 fork 함수의 반환값을 기준으로 나뉘도록 프로그래밍을 해야함
- fork 함수호출(반환) 이후에는 서로 완전히 분리된 메모리 구조를 지님
(동일한 코드를 실행하나, 완전히 다른 프로세스)
- 부모(원본, fork를 호출한) 프로세스 : fork 함수의 반환값 = 자식프로세스의 ID
- 자식(복사본) 프로세스 : fork 함수의 반환값 = 0
# fork.c
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int gval=10;
int main(int argc, char *argv[])
{
pid_t pid;
int lval=20;
gval++,lval+=5;
pid=fork();
/*
지금까지 gval=11, lval=25 이다.
이 함수호출 이후에 부모, 자식프로세스는
동일한 코드를 실행하나, 엄연히 다른 프로세스임!
*/
if(pid==0) // if child process
gval+=2, lval+=2;
else // if parent process
gval-=2, lval-=2;
if(pid==0) // if child process
printf("Child Proc: [%d, %d]\n",gval,lval);
else // if parent process
printf("Parent Proc: [%d, %d]\n",gval,lval);
}
# 좀비 프로세스
- 프로세스는 생성 못지않게 소멸도 중요
- 프로세스가 생성되고 나서 할 일을 다하면(main 함수의 실행을 완료하면) 소멸되어야 하는데,
소멸되지 않고 시스템의 중요한 리소스를 차지하는 프로세스가 바로 "좀비 프로세스"
- 좀비 프로세스는 시스템에 부담을 주는 원인이 됨
# 좀비 프로세스 생성 이유
- fork 함수의 호출로 생성된 자식 프로세스가 종료되는 두가지 상황
1) 인자를 전달하면서 exit 함수를 호출
2) main함수에서 return문을 실행하면서 값을 반환
=> 두 값 모두 운영체제로 전달되고, 운영체제는 이 값이 자식 프로세스를 생성한 부모 프로세스에게 전달될 때까지 자식 프로세스를 소멸시키지 않음 => 이 때 좀비 프로세스 발생 가능성이 있음
=> 따라서, 해당 자식 프로세스를 생성한 부모 프로세스에게 자식 프로세스로부터의 exit 함수의 인자값이나,
return문의 반환값을 전달해야 좀비프로세스를 소멸할 수 있음
- 부모 프로세스가 가만히 있는다고 운영체제가 알아서 이 값을 전달해주는 것이 아니므로, 부모 프로세스의 적극적인 요청이 있어야 한다.
# 부모 프로세스가 종료되면 "좀비 상태에 있던" 자식 프로세스도 종료된다.
# 좀비 프로세스의 소멸1
1) wait 함수의 사용
#include <sys/wait.h>
pid_t wait(int * statloc);
// -> 성공시 종료된 자식 프로세스의 ID, 실패시 -1 반환
- 이 함수 호출 시기에 이미 종료된 자식 프로세스가 있다면, 자식 프로세스가 종료되면서 전달한 값(exit 함수의 인자값 or return문 반환값)이 매개변수로 전달된 주소의 변수에 저장됨.
- 이때, 이 변수에 저장된 값은 전달된 값 뿐만 아니라 다른 정보가 함께 포함되어 있음.
1) WIFEXITED : 자식 프로세스가 정상종료한 경우 "참(true)"를 반환
2) WEXITSTATUS : 자식 프로세스의 전달값을 반환
- 즉, wait 함수호출 이후에는 다음과 같은 유형의 코드를 구성해야함
int status;
wait(&statue);
if(WIFEXITED(status)) // 자식프로세스가 정상종료했을 때,
{
puts("Normeal termination!");
printf("Child pass num: %d", WEXITSTATUS(status)); // 자식프로세스가 전달한값
}
# 정리
1) wait의 반환값 : 자식 프로세스의 ID or -1
2) wait의 인자(status) : 자식 프로세스 정상종료여부(WIFEXITED) + 자식 프로세스 전달값(WEXITSTATUS)
- 주의 : wait 함수는 호출된 시점에서 종료된 자식프로세스가 없다면,
임의의 자식 프로세스가 종료될 때까지 "블로킹" 상태에 빠짐(wait 함수는 반환을 하지 않음)
# wait.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char * argv[])
{
int status;
pid_t pid=fork(); // 자식 프로세스 생성
if(pid==0)
return 3; // 생성된 자식 프로세스는 3을 반환하며 종료
else // 부모 프로세스의 경우
{
printf("Child PID: %d \n", pid); // 생성한 자식 프로세스 ID 출력
pid=fork(); // 새로운 자식 프로세스 생성
if(pid==0)
exit(7); // 새로운 생성된 자식 프로세스가 7을 반환하며 종료
else
{
printf("Child PID: %d \n",pid); // 생성한 자식 프로세스 ID 출력
wait(&status); // 종료된 자식 프로세스를 기다림
if(WIFEXITED(status))
printf("Child send one: %d \n",WEXITSTATUS(status));
wait(&status); // 종료된 자식 프로세스를 기다림
if(WIFEXITED(status))
printf("Child send two: %d \n",WEXITSTATUS(status));
sleep(30); // 자식 프로세스보다 먼저 종료되는 것을 방지하기 위함임
}
}
return 0;
}
# 좀비 프로세스의 소멸2
- wait 함수의 블로킹이 문제가 될 수 있을 때 waitpid 함수의 호출을 고려하자.
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int * statloc, int options);
// 성공시 종료된 자식 프로세스의 ID(또는 0), 실패시 -1 반환
- pid : 종료를 확인하고자 하는 자식 프로세스 ID, "-1"을 전달하면 wait 함수와 마찬가지로 "임의의 자식프로세스" 종료를 기다림
- statloc : wait의 statloc과 동일
- options : sys/wait.h에 선언된 상수 "WNOHANG"을 인자로 전달하면, 종료된 자식 프로세스가 종료하지 않아도
블로킹 상태에 있지않고 0을 반환하면서 함수를 빠져나옴
# waitpid.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc, char * argv[])
{
int status;
pid_t pid=fork(); // 자식 프로세스 생성
if(pid==0) // 자식 프로세스의 경우
{
sleep(15); // 15초 간 블로킹 후
return 24; // 24를 반환하며 종료
}
else // 부모 프로세스의 경우
{
/*
임의의 자식 프로세스의 종료를 기다리되,
현재 종료된 프로세스가 없다면 0을 반환하며 빠져나옴
*/
while(!waitpid(-1,&status,WNOHANG))
{
sleep(3); // 3초간 부모 프로세스 블로킹
puts("Sleep 3sec.");
}
if(WIFEXITED(status))
printf("Child send %d \n",WEXITSTATUS(status));
}
return 0;
}
# 시그널 핸들링
- 일반적으로 부모 프로세스도 자식 프로세스 못지 않게 바쁘므로, 언제까지 자식 프로세스의 종료를 기다리면서 waitpid 함수 호출만 할 수는 없다.
- 자식 프로세스 종료의 인식주체는 운영체제임
- 따라서, 부모 프로세스는 자신이 하던 일을 하고 있다가, 자식 프로세스 종료시 운영체제가 이를 부모 프로세스에게 알려주게 하면 효율적인 프로그램의 구현이 가능
- 특정 시그널 발생시(e.g. 자식 프로세스의 종료라는 상황),
특정 함수의 호출을 운영체제에게 요구하게 하는 것 : "시그널 등록"
- 이 요구(시그널 등록)는 함수 호출을 통해 이루어짐 : 시그널 등록함수
# 시그널 등록함수
#include <signal.h>
void (*signal(int signo, void (*func)(int)))(int);
// -> 시그널 발생시 호출되도록 이전에 등록된 함수의 포인터 반환
- 함수이름 : signal
- 매개변수 : int signo, void (*func)(int)
1) signo : 특정 상황에 대한 정보
- SIGALRM : alarm 함수호출을 통해서 등록된 시간이 된 상황
- SIGINT : CTRL + C 가 입력된 상황
- SIGCHLD : 자식 프로세스가 종료된 상황
2) func : 특정 상황에서 호출한 함수의 주소값(포인터) - 반드시 형식을 지켜야함
- 반환형 : 매개변수가 int이고, 반환형이 void인 함수포인터
- 시그널이 등록되면, 등록된 시그널 발생시(등록된 상황 발생시),
운영체제는 해당 시그널에 등록된 함수를 호출해줌
# alarm 함수
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// -> 0 또는 SIGALRM 시그널이 발생하기까지 남아있는 시간을 초단위로 반환
- seconds초 가 지나면 SIGALRM 시그널이 발생함.
- 0을 전달하면 이전에 설정된 SIGALRM 시그널 발생의 예약이 취소됨.
- 이 함수를 호출해놓고(시그널 발생을 예약만 해놓고), (signal 함수호출을 통해서) 이 시그널 발생시 호출되어야 할 함수를 지정하지 않으면 프로세스가 그냥 종료되어 버리므로 주의
# 시그널이 발생하면 sleep 함수의 호출로 블로킹 상태에 있던 프로세스가 깨어남
- 시그널 등록된 함수(시그널 핸들러)의 호출을 유도하는 것은 운영체제이지만,
그래도 프로세스가 잠들어 있는 상태에서 함수가 호출될 수는 없음
- 시그널이 발생하면, 시그널에 해당하는 시그널 핸들러의 호출을 위해서
sleep 함수의 호출로 블로킹 상태에 있던 프로세스는 깨어나게 됨
# signal.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void timeout(int sig)
{
if(sig==SIGALRM)
puts("Time out!");
alarm(2); // SIGALRM 시그널 반복 발생을 위함임
}
void keycontrol(int sig)
{
if(sig==SIGINT)
puts("CTRL+C pressed");
}
int main(int argc, char * argv[])
{
int i;
signal(SIGALRM, timeout); // 시그널 등록
signal(SIGINT, keycontrol); // 시그널 등록
alarm(2); // 2초 뒤 SIGALRM 시그널 발생
for(i=0;i<3;++i)
{
puts("wait...");
sleep(100);
/*
여기서 블로킹 상태에 빠지나, 시그널 등록된 함수를 호출하기 위해서는
해당 프로세스가 다시 깨어나야 함. 따라서 실제로는 300초만큼 블로킹 상태에 빠지지 않음
*/
}
return 0;
}
# sigaction 함수를 이용한 시그널 핸들링
- signal 함수를 대체할 수 있고, 훨씬 안정적으로 동작하는 함수
- signal 함수는 유닉스 계열의 운영체제 별로 동작방식에 있어서 약간의 차이를 보일 수 있지만,
sigaction 함수는 차이를 보이지 않는다.
- signal 함수는 과거 프로그램과의 호환성을 위해서 유지만 되고 있을 뿐임 => 실제로는 sigaction 함수를 사용
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oldact);
-> 성공시 0, 실패시 -1 반환
- signo : 시그널의 정보를 인자로 전달
- act : 첫번째 인자에 해당하는 시그널 발생시 호출될 함수(시그널 핸들러)의 정보 전달
- oldact : 이전에 등록되었던 시그널 핸들러의 함수포인터를 얻는데 사용되는 인자, 필요 없으면 0을 전달
- sigaction 함수 사용을 위해서는 sigaction 구조체 변수를 선언 및 초기화 해야함
struct sigaction
{
void(*sa_handler)(int); // 시그널 핸들러의 주소값(함수포인터) 저장
sigset_t sa_mask; // 모든 비트를 0으로 초기화
int sa_flags; // 0으로 초기화
// 2, 3번째 멤버는 시그널 관련 옵션 및 특성지정에 사용되나,
// 좀비프로세스 생성 방지를 위한 목적이라면 둘다 0으로 두는 것으로 충분
}
act.sa_handler=(시그널 핸들러의 함수포인터);
sigemptyset(&act.sa_mask); // 0으로 초기화
act.sa_flags=0; // 0으로 초기화
sigaction(SIGALRM,&act,0); // 시그널 등록
# sigaction.c
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void timeout(int sig)
{
if(sig==SIGALRM)
puts("Time out!");
alarm(2);
}
int main(int argc, char * argv[])
{
int i;
struct sigaction act;
/* sigaction 구조체 초기화 */
act.sa_handler=timeout; // 시그널 핸들러는 timeout 함수포인터
sigemptyset(&act.sa_mask); // 0으로 초기화
act.sa_flags=0; // 0으로 초기화
sigaction(SIGALRM,&act,0); // 시그널 등록
alarm(2); // 2초 후에 SIGALRM 시그널 발생예약
for(i=0;i<3;++i) // 시그널 핸들러를 호출하기 위해서는 해당 프로세스는 깨어나야만 함!
{
puts("wait...");
sleep(100);
}
return 0;
}
# *시그널 핸들링을 통한 좀비 프로세스의 소멸
- 자식 프로세스가 종료된 상황에 대한 시그널 : SIGCHLD
# remove_zombie.c : 좀비 프로세스의 생성을 막는 예제
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
void read_childproc(int sig)
{
int status;
pid_t pid=waitpid(-1,&status,WNOHANG);
/*
임의의 자식 프로세스 종료를 기다림
현재 종료된 자식 프로세스가 없다면 0을 반환
*/
if(WIFEXITED(status))
{
printf("Removed proc id: %d \n",pid);
printf("Child send: %d \n",WEXITSTATUS(status));
}
}
int main(int argc, char * argv[])
{
pid_t pid;
struct sigaction act;
/* sigaction 구조체 초기화 */
act.sa_handler=read_childproc; // 시그널 핸들러는 read_childproc 함수포인터
sigemptyset(&act.sa_mask); // 0으로 초기화
act.sa_flags=0; // 0으로 초기화
sigaction(SIGCHLD,&act,0); // 시그널 등록
pid=fork(); // 자식 프로세스 생성
if(pid==0) // 자식 프로세스의 경우
{
puts("Hi, I'm child process");
sleep(10); // 10초 블로킹 후
return 12; // 12를 반환하며 종료
}
else // 부모 프로세스의 경우
{
printf("Child proc id: %d \n",pid); // 생성한 자식 프로세스의 ID 출력후,
pid=fork(); // 새로운 자식 프로세스 생성
if(pid==0) // 새로운 자식 프로세스의 경우
{
puts("Hi, I'm child process");
sleep(10); // 10초 블로킹 후
exit(24); // 24를 전달하며 종료
}
else // 부모 프로세스의 경우
{
int i;
printf("Child proc id: %d \n",pid); // 두번째로 생성한 자식 프로세스의 ID를 출력후,
for(i=0;i<5;++i) // 자식 프로세스보다 빨리 종료되는 것을 방지하기 위해 25초 블로킹
{
puts("wait...");
sleep(5);
}
}
}
return 0;
}
# 멀티태스킹 기반의 다중접속 서버
- 프로세스 기반의 다중접속 서버의 구현 모델 :
각 에코 클라이언트들이 연결요청을 할때마다,
서버는 자식 프로세스를 생성해서 각 에코 클라이언트에 연결완료하고 서비스 제공
1단계 : 에코 서버(부모 프로세스)는 accept 함수호출을 통해서 연결요청을 수락한다.
2단계 : 이때 얻게 되는 소켓의 파일 디스크립터를 자식 프로세스를 생성해서 넘겨준다.
- fork 함수는 부모 프로세스가 소유하고 있는 것을 전부 복사하기에 따로 파일 디스크립터 값을
자식 프로세스에게 명시적 전달을 하는 과정이 필요 없음
3단계 : 자식 프로세스는 전달받은 파일 디스크립터를 바탕으로 에코 클라이언트에게 서비스를 제공한다.
# fork 함수호출을 통한 파일 디스크립터의 복사
- 소켓은 프로세스의 소유가 아니라 "운영체제의 소유"이다.
- 따라서 fork 함수 호출시 "파일 디스크립터만" 복사된다고 할 수 있다.
- 또한, 소켓이 복사된다는 것은 말이 안된다.
(동일한 PORT번호 할당된 소켓이 두 개 이상이 될 수는 없다는 관점)
# 하나의 소켓에 두개의 파일 디스크립터가 존재하는 경우,
- 두 개의 파일 디스크립터가 모두 종료(소멸)되어야 소켓은 소멸된다.
- 서버소켓의 파일 디스크립터는 서버의 부모 프로세스만 가지게,
클라이언트 소켓과의 연결을 위해 생성된(accept 함수 호출 과정에서 생성된)
소켓의 파일 디스크립터는 서버의 자식 프로세스만 가지게,
close 함수로 파일 디스크립터를 미리 닫아주어야 한다.
# 다중접속 에코 서버의 구현
- Ch 04의 echo_client.c와 함께 실행하시면 됩니다.
echo_mpserv.c
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.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) // 실행파일의 경로/PORT번호 를 인자로 받아야함
{
printf("Usage : %s <port> \n",argv[0]);
exit(EXIT_FAILURE);
}
/* sigaction 구조체 초기화 */
act.sa_handler=read_childproc; // 시그널 핸들러는 read_childproc 함수포인터
sigemptyset(&act.sa_mask); // 0으로 초기화
act.sa_flags=0; // 0으로 초기화
state=sigaction(SIGCHLD,&act,0); // 시그널 등록(자식 프로세스 종료시 read_childproc 함수호출)
serv_sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
/* 서버 주소정보 초기화 */
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(true)
{
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 error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE); // -1
}
void read_childproc(int sig)
{
pid_t pid;
int status;
pid=waitpid(-1,&status,WNOHANG);
if(WIFEXITED(status))
{
printf("removed proc id: %d \n",pid);
// printf("removed proc send: %d \n",WEXITSTATUS(status));
}
}
# TCP의 입출력 루틴 분할
- 지금까지 구현한 에코 클라이언트의 데이터 에코방식은 하나의 프로세스를 기반으로 프로그램이 동작했기에, 한번 데이터를 전송하면 에코되어 돌아오는 데이터를 수신할 때까지 마냥 기다려야 함
- 두 개의 프로세스를 기반으로 하여 하나는 입력 루틴, 다른 하나는 출력 루틴으로 사용하면 데이터의 송수신을 분리할 수 있다.
- 여기서 입출력 루틴 분할을 위한 모델은 다음과 같다.
1) 클라이언트의 부모 프로세스는 데이터 수신을 담당
2) 클라이언트의 자식 프로세스는 데이터 송신을 담당
=> 서버로부터의 데이터 수신여부에 상관없디 데이터 전송이 가능
- 이와 같은 구현의 장점으로는 부모 프로세스는 데이터 수신과 관련된 코드만 작성하면되고, 자식 프로세스는 데이터 송신과 관련된 코드만 작성하면 되기 때문
- 또 다른 장점은 데이터 송수신이 잦은 프로그램의 성능향상이 있음(데이터 수신 여부와 상관없이 데이터 전송이 가능하기 때문)
# 에코 클라이언트의 입출력 루틴 분할
- 위에서 구현한 echo_mpserv.c와 같이 실행하시면 됩니다.
echo_mpclient.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
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) // 실행파일의 경로/IP/PORT번호 를 인자로 입력받아야 함
{
printf("Usage : %s <IP> <port>\n",argv[0]);
exit(EXIT_FAILURE);
}
sock=socket(PF_INET,SOCK_STREAM,0); // TCP 소켓 생성
if(sock==-1)
error_handling("socket() error");
/* 서버 주소정보 초기화 */
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!");
else
puts("Connected................");
pid=fork(); // 자식 프로세스 생성
if(pid==0) // 자식 프로세스의 경우
write_routine(sock,buf); // 출력 루틴 실행
else // 부모 프로세스의 경우
read_routine(sock,buf); // 입력 루틴 실행
close(sock); //
return 0;
}
void error_handling(char *message)
{
fputs(message,stderr);
fputc('\n',stderr);
exit(EXIT_FAILURE);
}
void read_routine(int sock, char *buf)
{
while(true)
{
int str_len=read(sock,buf,BUF_SIZE-1); // 서버로부터 데이터 수신
if(str_len==0)
return;
buf[str_len]='\0';
printf("Message from server: %s",buf);
}
}
void write_routine(int sock, char *buf)
{
while(true)
{
fgets(buf,BUF_SIZE,stdin); // 표준입력으로부터 입력받음
if(!strcmp(buf,"q\n")||!strcmp(buf,"Q\n"))
{
shutdown(sock,SHUT_WR); // 출력 스트림 종료
return;
}
write(sock,buf,strlen(buf)); // 널문자를 제외한 문자열을 서버로 송신
}
}
[출처] : 윤성우 저, "열혈강의 TCP/IP 소켓 프로그래밍", 오렌지미디어
'Programming > 열혈 TCP, IP 소켓 프로그래밍(저자 윤성우)' 카테고리의 다른 글
Ch 11. 프로세스간 통신(Inter Process Communication) (0) | 2020.08.09 |
---|---|
Ch 10. 내용 확인문제 (0) | 2020.08.08 |
Ch 09. 내용 확인문제 (0) | 2020.08.08 |
Ch 09. 소켓의 다양한 옵션 (0) | 2020.08.08 |
Ch 08. 내용 확인문제 (0) | 2020.08.08 |