본문 바로가기
Computer Science/UNIX & Linux

[UNIX/Linux] ep11-3) TCP 소켓 프로그래밍

by 클레어몬트 2024. 12. 8.

https://claremont.tistory.com/entry/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-IPCInter-Process-Communication

 

[운영체제] IPC(Inter-Process Communication)

https://claremont.tistory.com/entry/ep2-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4process [운영체제] ep2) 프로세스(process)ㅁ프로세스(process): 실행 중인 프로그램 (실행/스케줄링의 단위 및 자료구조)보조기억장치에 저장

claremont.tistory.com

https://claremont.tistory.com/entry/UNIXLinux-ep11-1-%EC%86%8C%EC%BC%93-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%EA%B8%B0%EC%B4%88

 

[UNIX/Linux] ep11-1) 소켓 프로그래밍 기초

https://claremont.tistory.com/entry/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-IPCInter-Process-Communication [운영체제] IPC(Inter-Process Communication)https://claremont.tistory.com/entry/ep2-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4process [운영체제] ep2)

claremont.tistory.com

https://claremont.tistory.com/entry/UNIXLinux-ep11-2-%EC%86%8C%EC%BC%93-%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%B0%8D-%ED%95%A8%EC%88%98

 

[UNIX/Linux] ep11-2) 소켓 프로그래밍 함수

https://claremont.tistory.com/entry/%EC%9A%B4%EC%98%81%EC%B2%B4%EC%A0%9C-IPCInter-Process-Communication [운영체제] IPC(Inter-Process Communication)https://claremont.tistory.com/entry/ep2-%ED%94%84%EB%A1%9C%EC%84%B8%EC%8A%A4process [운영체제] ep2)

claremont.tistory.com

 

[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

 

[운영체제] ep2) 프로세스(process)

ㅁ프로세스(process): 실행 중인 프로그램 (실행/스케줄링의 단위 및 자료구조)보조기억장치에 저장된 프로그램을 메모리에 적재하고 실행하는 순간, 그 프로그램은 프로세스가 된다그리고 이 과

claremont.tistory.com

 

데몬 프로세스 = 서버

네트워크를 통해 데이터를 주고받으며 동작하는 네트워크 프로그램은 일반적으로 서버클라이언트로 역할을 구분

서버는 클라이언트의 요청에 따라 다양한 서비스를 제공하는 프로그램이며 데몬 프로세스라고도

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 프로그램에서 보낸 것임을 알 수 있음

 

프로세스 목록을 확인해 보면 서버 프로세스 외에 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 프로세스도 클라이언트가 보낸 메시지를 받아 출력함을 확인

위에가 서버, 아래가 클라이언트

 

 

 

 

 

 

참고 및 출처: 시스템 프로그래밍 리눅스&유닉스(이종원)