[ep11-3, ep11-4의 학습목표]
1. TCP 기반으로 반복 실행 서버 형태의 프로그램을 작성할 수 있다.
2. TCP 기반으로 동시 동작 서버 형태의 프로그램을 작성할 수 있다.
3. UDP 기반의 서버/클라이언트 프로그램을 작성할 수 있다.
포그라운드(foreground) 프로세스: 사용자가 볼 수 있는 앞 공간에서 실행되는 프로세스
vs
백그라운드(background) 프로세스: 사용자가 보지 못하는 뒷 공간에서 실행되는 프로세스
이 백그라운드 프로세스 중에서 사용자와 상호작용하지 않고 혼자 묵묵히 일을 수행하는 프로세스들을 Windows에서는 서비스(service)라 하고 UNIX체계에서는 데몬(daemon)이라고 한다
여담) 데몬이 '눈에 보이지 않는' 악마이지 않는가? 우리말로 직역하면 귀신프로세스 느낌으로 받아들이면 될 것 같다
https://claremont.tistory.com/entry/ep2-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4process
ㅁ데몬 프로세스 = 서버
네트워크를 통해 데이터를 주고받으며 동작하는 네트워크 프로그램은 일반적으로 서버와 클라이언트로 역할을 구분
서버는 클라이언트의 요청에 따라 다양한 서비스를 제공하는 프로그램이며 데몬 프로세스라고도 함
e.g. HTTP 데몬: 웹사이트에 접속할 때 웹 서비스를 제공하는 HTTP 데몬이 동작하고 있어 사용자들이 웹 페이지를 볼 수 있음
ㅁ데몬 프로세스 종류
1. 반복 실행 서버: 데몬 프로세스가 직접 모든 클라이언트의 요청을 차례대로 처리하는 형태
네트워크 프로그램을 처음 도입한 당시에는 반복 서버 형태로 서비스를 제공, 그러나 다양한 네트워크 서비스가 등장하면서 데몬 프로세스의 개수가 너무 많아지는 문제 발생
- 서버 프로그램이 클라이언트의 요청을 직접 처리하여 한 번에 한 클라이언트의 요청만 처리할 수 있음
- 여러 클라이언트가 서비스를 요청할 경우 클라이언트가 자기 순서를 기다리는데 이를 방지하기 위해 반복 실행 서버 프로세스를 여러 개 동작시킬 수 있음
- 반복 실행 서버는 인터넷 소켓과 TCP(SOCK_STREAM)를 이용해 통신
- 서버는 계속 동작하면서 클라이언트가 요청하는 서비스를 받아 처리
"서버 측"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORTNUM 9001 // 9001번 포트를 사용
int main() {
char buf[256];
struct sockaddr_in sin, cli;
int sd, ns, clientlen = sizeof(cli);
// 서버 주소 구조체에 소켓의 종류를 AF_INET 으로 지정하고 포트 번호와 서버의 IP 주소를 설정
memset((char*)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(PORTNUM);
sin.sin_addr.s_addr = inet_addr("192.168.147.129");
// 소켓 생성 시 SOCK_STREAM 으로 지정해 TCP를 사용
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
// 생성한 소켓을 bind() 함수를 사용해 htons와 inet_addr로 설정한 IP주소/포트 번호와 연결
if (bind(sd, (struct sockaddr*)&sin, sizeof(sin))) {
perror("bind");
exit(1);
}
// listen() 함수를 호출해 클라이언트의 요청을 받을 준비가 끝났음을 운영체제에 알림
// 한 번에 5개의 클라이언트가 접속할 수 있도록 설정
if (listen(sd, 5)) {
perror("listen");
exit(1);
}
// while 문으로 무한 반복하게 함 (이 서버는 무한 반복하면서 서비스를 제공해야 함)
while (1) {
// accept() 함수를 사용해 클라이언트의 요청이 올 때까지 기다림
// 클라이언트의 접속 요청이 오면 새로운 소켓 기술자 ns가 생성되고 이를 통해 클라이언트와 통신
if ((ns = accept(sd, (struct sockaddr*)&cli, &clientlen)) == -1) {
perror("accept");
exit(1);
}
// accept() 함수를 통해 알아낸 클라이언트의 IP 주소를 문자열로 변환해 버퍼에 저장
sprintf(buf, "%s", inet_ntoa(cli.sin_addr));
printf("*** Send a Message to Client(%s)\n", buf); // 어떤 클라이언트가 서버로 접속했는지를 출력
strcpy(buf, "Welcome to Network Server!!!"); // 서버에서 클라이언트로 보낼 간단한 환영 메시지 작성
// send() 함수를 사용해 클라이언트로 메시지를 전송
if (send(ns, buf, strlen(buf) + 1, 0) == -1) {
perror("send");
exit(1);
}
// 클라이언트가 보낸 메시지를 recv() 함수로 받아서 출력
if (recv(ns, buf, sizeof(buf), 0) == -1) {
perror("recv");
exit(1);
}
printf("** From Client : %s\n", buf);
close(ns);
// 작업이 끝나면 클라이언트와 접속할 때 사용한 소켓 기술자를 닫고
// 다시 accept() 함수를 수행해 클라이언트의 접속 요청을 기다림
}
close(sd);
return 0;
}
①
③
④
⑤
다른 시스템에서 접속을 요청해도 같은 결과를 출력
"클라이언트 측"
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORTNUM 9001 // 클라이언트도 서버와 같은 포트 번호를 사용
int main() {
int sd;
char buf[256];
struct sockaddr_in sin;
memset((char*)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(PORTNUM);
sin.sin_addr.s_addr = inet_addr("192.168.147.129"); // 접속할 서버의 주소를 지정
// 소켓을 생성
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
// 서버와 접속
if (connect(sd, (struct sockaddr*)&sin, sizeof(sin))) {
perror("connect");
exit(1);
}
// 서버에서 보낸 메시지를 받음
if (recv(sd, buf, sizeof(buf), 0) == -1) {
perror("recv");
exit(1);
}
// 서버로부터 받은 메시지를 출력
printf("** From Server : %s\n", buf);
// 서버로 보낼 메시지를 복사한 후 메시지를 서버로 전송
strcpy(buf, "I want a HTTP Service.");
if (send(sd, buf, sizeof(buf) + 1, 0) == -1) {
perror("send");
exit(1);
}
close(sd);
return 0;
}
②
2. 동시 동작 서버
• 데몬 프로세스가 직접 서비스를 제공하지 않음
• 서비스와 관련 있는 다른 프로세스를 fork() 함수로 생성한 후 이 프로세스를 클라이언트와 연결해 서비스를 제공
• 데몬 프로세스의 개수가 너무 많아지는 문제를 해결하기 위해 도입됨
(Linux 시스템에 따라 이 슈퍼 데몬을 기본으로 설치하지 않는 경우도 있음)
[예제 13-2] 동시 동작 서버
"서버 측"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORTNUM 9002
int main() {
char buf[256];
struct sockaddr_in sin, cli;
int sd, ns, clientlen = sizeof(cli);
// 소켓을 생성
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
memset((char*)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(PORTNUM);
sin.sin_addr.s_addr = inet_addr("192.168.147.129");
// 특정 IP 주소와 연결
if (bind(sd, (struct sockaddr*)&sin, sizeof(sin))) {
perror("bind");
exit(1);
}
// 클라이언트의 접속을 5개까지 처리하도록 설정
if (listen(sd, 5)) {
perror("listen");
exit(1);
}
while (1) {
// 클라이언트 접속 요청을 받음
if ((ns = accept(sd, (struct sockaddr*)&cli, &clientlen)) == -1) {
perror("accept");
exit(1);
}
switch (fork()) { // 자식 프로세스를 생성하고, 자식 프로세스가 클라이언트의 응답을 처리하게 함
case 0:
strcpy(buf, "Welcome to Server");
if (send(ns, buf, strlen(buf) + 1, 0) == -1) {
perror("send");
exit(1);
}
if (recv(ns, buf, sizeof(buf), 0) == -1) {
perror("recv");
exit(1);
}
printf("** From Client: %s\n", buf);
close(ns);
sleep(5);
exit(0);
}
}
close(sd);
return 0;
}
실행 결과는 위와 같지만, 클라이언트가 접속했을 때 서버의 실행 상태를 보면 다음과 같이 서버 프로그램이 하나 더 동작
[예제 13-3] 동시 동작 서버 - exec() 함수 사용하기
"서버 측"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORTNUM 9003
int main() {
struct sockaddr_in sin, cli;
int sd, ns, clientlen = sizeof(cli);
// 소켓을 생성
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
printf("** Create Socket\n");
memset((char*)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(PORTNUM);
sin.sin_addr.s_addr = inet_addr("192.168.147.129");
// 위에서 설정한 특정 IP 주소와 연결
if (bind(sd, (struct sockaddr*)&sin, sizeof(sin))) {
perror("bind");
exit(1);
}
printf("** Bind Socket\n");
// 클라이언트의 접속을 5개까지 처리하도록 설정
if (listen(sd, 5)) {
perror("listen");
exit(1);
}
printf("** Listen Socket\n");
while (1) {
// 클라이언트의 접속 요청을 받음
if ((ns = accept(sd, (struct sockaddr*)&cli, &clientlen)) == -1) {
perror("accept");
exit(1);
}
printf("** Accept Client\n");
switch (fork()) { // 자식 프로세스를 생성해 클라이언트의 응답을 처리하게 함
// 클라이언트는 반복 실행 서버와 달리 클라이언트와 통신할 수 있는 소켓 기술자(ns)를 표준 입력과 표준 출력으로 복사한 후
// execl() 함수로 han 프로세스를 실행, han은 표준 출력으로 간단한 메시지를 출력하는 프로세스
case 0:
printf("** Fork Client\n");
close(sd);
dup2(ns, STDIN_FILENO);
dup2(ns, STDOUT_FILENO);
close(ns);
execl("./han", "han", (char*)0);
}
close(ns);
}
return 0;
}
"테스트 프로그램"
#include <unistd.h>
#include <stdio.h>
int main() {
// han 프로세스에서 표준 출력으로 메시지를 출력하면 기본 표준 출력인 모니터가 아닌 클라이언트 프로세스로 전달
// 이는 부모 프로세스인 서버 프로그램에서 exec() 함수를 호출하기 전에 소켓 기술자를 표준 출력으로 복제했기 때문
printf("Welcome to Server, from Han!");
// 프로세스 목록을 확인할 수 있도록 시간을 지연시킴
sleep(5);
return 0;
}
"클라이언트 측"
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORTNUM 9003
int main() {
int sd, len;
char buf[256];
struct sockaddr_in sin;
memset((char*)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(PORTNUM);
sin.sin_addr.s_addr = inet_addr("192.168.147.129");
// 서버와 연결하기 위해 서버 주소를 지정하고 소켓을 생성
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
// connect() 함수를 호출해 서버와 연결을 요청
printf("==> Create Socket\n");
if (connect(sd, (struct sockaddr*)&sin, sizeof(sin))) {
perror("bind");
exit(1);
}
// 서버에서 메시지를 받아 출력
printf("==> Connect Server\n");
if ((len = recv(sd, buf, sizeof(buf), 0)) == -1) {
perror("recv");
exit(1);
}
buf[len] = '\0';
printf("==> From Server : %s\n", buf);
// 소켓을 닫음
close(sd);
return 0;
}
서버를 실행한 다음 클라이언트를 실행하면 다음과 같은 메시지가 출력
프로세스 목록을 확인해 보면 서버 프로세스 외에 han 프로세스가 동작하고 있음을 알 수 있음
[예제 13-4] 명령행 인자로 소켓 기술자 전달
"서버 측"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORTNUM 9004
int main() {
char buf[256];
struct sockaddr_in sin, cli;
int sd, ns, clientlen = sizeof(cli);
// 소켓을 생성
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
printf("** Create Socket\n");
memset((char*)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(PORTNUM);
sin.sin_addr.s_addr = inet_addr("192.168.147.129");
// 위에서 설정한 특정 IP 주소와 연결 (IP 주소는 실습 환경에 맞춰 지정)
if (bind(sd, (struct sockaddr*)&sin, sizeof(sin))) {
perror("bind");
exit(1);
}
printf("** Bind Socket\n");
// 클라이언트의 접속을 5개까지 처리하도록 설정
if (listen(sd, 5)) {
perror("listen");
exit(1);
}
printf("** Listen Socket\n");
while (1) {
// 클라이언트의 접속 요청을 받음
if ((ns = accept(sd, (struct sockaddr*)&cli, &clientlen)) == -1) {
perror("accept");
exit(1);
}
printf("** Accept Client\n");
switch (fork()) { // 자식 프로세스를 생성하고 해당 자식 프로세스가 클라이언트의 응답을 처리하게 함
case 0:
// execlp() 함수를 사용해 bit 프로세스를 호출할 때 51행에서 저장한 소켓 정보를 인자로 함께 전송
// bit 프로세스가 클라이언트와 연결되어 서비스를 제공할 것
printf("** Fork Client\n");
close(sd);
sprintf(buf, "%d", ns); // 클라이언트와 통신할 수 있는 소켓을 버퍼에 저장
execlp("./bit", "bit", buf, (char*)0);
close(ns);
}
close(ns);
}
return 0;
}
// [예제 13-3]의 서버 프로그램과 다른 점은 execlp() 함수를 호출하면서 소켓 기술자를 인자로 지정한다는 점
// execlp() 함수를 호출했으므로 자식 프로세스는 bit 프로세스 이미지로 바뀌고
// buf로 지정한 인자는 bit 프로세스의 명령행 인자로 전달
"bit 프로그램"
#include <sys/socket.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[]) {
char buf[256];
int len, ns;
ns = atoi(argv[1]); // 명령행 인자(문자열)로 전달받은 소켓 기술자를 정수형으로 변환
// 간단한 환영 메시지를 클라이언트로 전송
strcpy(buf, "Welcome to Server, from Bit");
if ((send(ns, buf, strlen(buf) + 1, 0)) == -1) {
perror("send");
exit(1);
}
// 클라이언트에서 보낸 메시지를 받아서 출력
if ((len = recv(ns, buf, sizeof(buf), 0)) == -1) {
perror("recv");
exit(1);
}
printf("@@ [Bit] From Client: %s\n", buf);
close(ns);
return 0;
}
"클라이언트 측"
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define PORTNUM 9004
int main() {
int sd, len;
char buf[256];
struct sockaddr_in sin;
memset((char*)&sin, '\0', sizeof(sin));
sin.sin_family = AF_INET;
sin.sin_port = htons(PORTNUM);
sin.sin_addr.s_addr = inet_addr("192.168.147.129");
// 소켓을 생성
if ((sd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
printf("==> Create Socket\n");
// 생성한 소켓을 통해 서버와 연결
if (connect(sd, (struct sockaddr*)&sin, sizeof(sin))) {
perror("connect");
exit(1);
}
printf("==> Connect Server\n");
// 서버에서 메시지를 전달받음
if ((len = recv(sd, buf, sizeof(buf), 0)) == -1) {
perror("recv");
exit(1);
}
buf[len] = '\0';
printf("==> From Server : %s\n", buf);
// 답장 메시지를 작성해 send() 함수를 통해 서버로 전송
strcpy(buf, "I want a SSH Service.");
if (send(sd, buf, sizeof(buf) + 1, 0) == -1) {
perror("send");
exit(1);
}
close(sd);
return 0;
}
// 실행 결과: 클라이언트는 bit 프로세스가 보낸 메시지를 받고
// bit 프로세스도 클라이언트가 보낸 메시지를 받아 출력함을 확인
참고 및 출처: 시스템 프로그래밍 리눅스&유닉스(이종원)
'Computer Science > UNIX & Linux' 카테고리의 다른 글
[UNIX/Linux] ep11-4) UDP 소켓 프로그래밍 (0) | 2024.12.11 |
---|---|
[UNIX/Linux] ep11-3+) TCP 소켓 프로그래밍 실습 (0) | 2024.12.10 |
[UNIX/Linux] ep11-2+) 소켓 프로그래밍 함수 실습 (0) | 2024.12.05 |
[UNIX/Linux] ep11-2) 소켓 프로그래밍 함수 (0) | 2024.12.04 |
[UNIX/Linux] ep11-1+) 소켓 프로그래밍 기초 함수 실습 (3) | 2024.12.03 |