다중접속서버 코드 분석

kenGwon·2024년 2월 20일
0

[Embedded Linux] BSP

목록 보기
21/36
post-thumbnail

다중접속서버

# 멀티 프로세스를 통한 다중접속서버

소켓을 통해서 서버-클라이언트가 통신을 하려면, 서버입장에서 통신을 구동하고 있는 프로세스는 반드시 하나의 클라이언트에 항상 연결되어있는 상태여야 한다.
하지만 다중접속서버를 구현하려면 서버는 accept()라는 블로킹 함수로 다른 클라이언트의 접속을 대기하고 있어야 한다. 그렇기 때문에 다중접속서버를 구현하기 위해 멀티프로세스 기법이 필요해지는 것이다.

서버는 클라이언트가 연결되면 fork()를 통해서 자기자신을 복제한 자식 프로세스를 만들어내고, 그 자시그포레스로 하여금 클라이언트와 read/write를 주고받도록 한다.
그러는 동안 부모 프로세스는 accept()라는 블로킹 함수를 호출하여 또 다른 클라이언트의 연결 요청을 대기하게 된다.

클라이언트가 많아지면 많아질수록 엄청나게 많은 컨텍스트 정보를 관리해야 하기 때문에 CPU입장에서는 부하가 늘어난다(소켓통신을 관리하는 프로세스의 컨텍스트 정보는 상당히 무겁다). 그래서 멀티프로세스 기반의 소켓을 통한 다중 접속 서버는 그다지 많이 사용하지는 않는다고 한다.

어찌되었든 이렇게 해서 만들어진 멀티프로세스 기반의 다중접속서버의 프로세스끼리 통신을 해야할 필요가 생긴다. 이 때는 보통 파이프를 사용한다.

# 멀티 스레드를 통한 다중접속서버

위에서처럼 멀티프로세스로 다중접속서버를 하려니까 IPC를 사용해야 해서 되게 귀찮아진다. 그래서 이걸 좀 편하게 할려고 멀티 스레드를 통한 다중접속서버를 사용하게 된다. 멀티 스레드로 여러개의 접속을 관리하려면 전역변수 하나만 딱 선언해놓고 그 변수에 대해서 여러개의 스레드가 접근하면서 통신을 하면된다.

다만 이러한 방식에서 주의해야 하는 것은 mutex나 semaphore와 같은 기법을 통해서 동기화를 잘 해줘야 한다는 것이다.

*나중에 Qt GUI할 때 '멀티 스레드를 통한 다중접속서버'를 구현해 볼 것이다.





우리의 인터넷 요금은 얼마나 큰 패킷 사이즈를 얼마나 많이 사용하느냐에 따라서 값이 책정된다. 그래서 요금제 데이터가 1.5GB라는 것은 데이터 패킷 1.5GB 분량을 송수신 했다는 것이다.


언제 어떤 타이밍에 context switching이 일어날지 모르기 때문에, 전역변수를 사용했다면 반드시 동기화 기법을 적용하여 race condition을 해결해주어야 한다.

모든 장비와 플랫폼은 소켓통신을 통해 네트워크 통신이 가능하도록 되어있다.

// iot_server.c
...
143 #if 1   //for MCU
144                         client_info[i].fd = -1;
145 #endif
...

MCU는 전원을 확 뽑아버리던가 reset 버튼을 눌러버리면, TCP/IP 흐름제어가 뚝 끊겨버려서 자기가 끊어졌다는 것을 알릴 방법이 없어진다. 그래서 다시 켜서 로그인을 시도해도 "already logined"가 뜨면서 접속이 안된다. 그래서 그러한 경우를 위해 "already logined"가 뜨면서 또 접속하면 아예 그 유저 관련 정보를 끊어버리겠다는 것이다.


...
233     pthread_mutex_lock(&mutx);
234     clnt_cnt--;
235     client_info->fd = -1;
236     pthread_mutex_unlock(&mutx);
...

뮤텍스 락/언락은 전역변수에 값을 "write"하는 경우에만 적용하면 된다. "read"하는 경우는 적용하지 않아도 된다.(당연한 얘기)
그리고 뮤텍스 락/언락 사이에는 최대한 코드량을 줄여야한다. mutex 사이에 코드량이 많아지면 프로그램의 응답속도가 떨어진다.(write가 발생하는 전역변수에 관한 부분만 추가해주면 되겠다.)


iot_server

/* 코드 학습: kenGwon */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <pthread.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <dirent.h>
#include <sys/time.h>
#include <time.h>
#include <errno.h>

#define BUF_SIZE 100
#define MAX_CLNT 32
#define ID_SIZE 10
#define ARR_CNT 5

#define DEBUG

typedef struct {
        int fd;
        char *from;
        char *to;
        char *msg;
        int len;
} MSG_INFO;

typedef struct {
        int index;
        int fd;
        char ip[20];
        char id[ID_SIZE];
        char pw[ID_SIZE];
} CLIENT_INFO;

// 함수 선언
void* clnt_connection(void* arg);
void send_msg(MSG_INFO* msg_info, CLIENT_INFO* first_client_info);
void error_handling(char* msg);
void log_file(char* msgstr);

// 전역변수 선언
int clnt_cnt = 0;
pthread_mutex_t mutx;


int main(int argc, char *argv[])
{
        int serv_sock, clnt_sock; // 소켓 fd 변수
        struct sockaddr_in serv_adr, clnt_adr; // 소켓 주소와 포트를 관리하기 위한  구조체
        int clnt_adr_sz;
        int sock_option = 1;
        pthread_t t_id[MAX_CLNT] = {0};
        int str_len = 0;
        int i;
        char idpasswd[(ID_SIZE*2)+3];
        char *pToken;
        char *pArray[ARR_CNT] = {0};
        char msg[BUF_SIZE];

        CLIENT_INFO client_info[MAX_CLNT] = {{0,-1,"","1","PASSWD"}, \
                {0,-1,"","2","PASSWD"},  {0,-1,"","3","PASSWD"}, \
                {0,-1,"","4","PASSWD"},  {0,-1,"","5","PASSWD"}, \
                {0,-1,"","6","PASSWD"},  {0,-1,"","7","PASSWD"}, \
                {0,-1,"","8","PASSWD"},  {0,-1,"","9","PASSWD"}, \
                {0,-1,"","10","PASSWD"},  {0,-1,"","11","PASSWD"}, \
                {0,-1,"","12","PASSWD"},  {0,-1,"","13","PASSWD"}, \
                {0,-1,"","14","PASSWD"},  {0,-1,"","15","PASSWD"}, \
                {0,-1,"","16","PASSWD"},  {0,-1,"","17","PASSWD"}, \
                {0,-1,"","18","PASSWD"},  {0,-1,"","19","PASSWD"}, \
                {0,-1,"","20","PASSWD"},  {0,-1,"","21","PASSWD"}, \
                {0,-1,"","22","PASSWD"},  {0,-1,"","23","PASSWD"}, \
                {0,-1,"","24","PASSWD"},  {0,-1,"","25","PASSWD"}, \
                {0,-1,"","26","PASSWD"},  {0,-1,"","27","PASSWD"}, \
                {0,-1,"","28","PASSWD"},  {0,-1,"","29","PASSWD"}, \
                {0,-1,"","30","PASSWD"},  {0,-1,"","31","PASSWD"}, \
                {0,-1,"","HM_CON","PASSWD"}};

        // 명령행 인수 체크
        if (argc != 2)
        {
                printf("Usage: %s <port>\n", argv[0]);
                exit(1);
        }

        fputs("IoT Server Start!!\n", stdout); // 프로그램 시작을 알려줌

        // 뮤텍스 생성
        if (pthread_mutex_init(&mutx, NULL))
                error_handling("mutex init error");

        // 서버 소켓 생성
        serv_sock = socket(PF_INET, SOCK_STREAM, 0); // Protocol Family for InterNET

        // 주소와 포트를 할당하기 위한 sockaddr_in 구조체 server_adr에 메모리 초기화
        memset(&server_adr, 0, sizeof(server_adr));

        // 초기화된 server_adr 구조체에 값 할당
        serv_adr.sin_family = AF_INET; // 패밀리 지정
        serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); // 리틀 앤디안&빅 앤디안 고려하여 바이트 오더 적용
        serv_adr.sin_port = htons(atoi(argv[1])); // 바이트 오더 적용하여 포트번호 할당

        // 소켓 옵션 설정
        setsocketopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, (void*)&sock_option, sizeof(sock_option));

        // 소켓 바인딩 시도 (serv_adr는 원래 sockaddr_in 구조체여서 sockaddr로 타입캐스팅해서 포인터를 넘겼다)
        if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
                error_handling("bind() error");

        // 바인딩 된 소켓으로 (연결요청 수락대기를 할 수 있는 큐의 크기는 5로 만들면서) listen을 시작
        if (listen(serv_sock, 5) == -1)
                error_handling("listen() error");

        while (1)
        {
                clnt_adr_sz = sizeof(clnt_adr); // 클라이언트 주소의 크기를 담는 변수를 생성(이 공간이 아래서 필요함)
                clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &clnt_adr_sz);
                // accept 를 이용해서 연결이 완성되면 accept()는 소켓과 연결되는 "파일 지시자"를 돌려주고
                // 이 "파일 지시자" 를 통해서 클라이언트와 서버간의 메시지를 주고 받게 된다.

                if (clnt_cnt >= MAX_CLNT) // 클라이언트 accept 이후 예외처리1
                {
                        printf("socket full\n");
                        shutdown(clnt_sock, SHUT_WR);
                        continue;
                }
                else if (clnt_cnt < 0) // 클라이언트 accept 이후 예외처리2
                {
                        perror("accept");
                        continue;
                }

                // 시스템 콜 함수로 clnt_sock라는 file descriptor에 담겨있는 내용을 읽어서 idpasswd에 저장함
                str_len = read(clnt_sock, idpasswd, sizeof(idpasswd));
                idpasswd[str_len] = '\0';

                if (str_len > 0) // 0이라면 파일의 끝을 의미, 0 보다 큰 양수라면 읽어들인 buf의 크기
                {
                        i = 0;
                        pToken = strtok(idpasswd, "[:]"); // 버퍼에 담은 값을 토크나이징하여 토큰 포인터 변수에 담음

                        while (pToken != NULL) // 이어서 계속 토크나이징 하여 pArray를 완성한다.
                        {
                                pArray[i] = pToken;
                                if (i++ > ARR_CNT)
                                        break;
                                pToken = strtok(NULL, "[:]");
                        }

                        for (i = 0; i < MAX_CLNT; i++)
                        {
                                if (!strcmp(client_info[i].id, pArray[0])) // pArray의 값과 일치하는 client_info를 찾았다면
                                {
                                        if (client_info[i].fd != -1) // 처음 로그인한 상태가 아니라면...
                                        {
                                                sprintf(msg, "[%s] Already logged!\n", pArray[0]);
                                                write(clnt_sock, msg, strlen(msg)); // 서버에서 클라이언트로 안내메시지 전송
                                                log_file(msg);
                                                shutdown(clnt_sock, SHUT_WR); // 이미 로그인해있는 계정이므로 셧다운

#if 1 // for MCU ... MCU는 전원을 뽑아서 끈다던지 하는 예기치 못한 상황으로 비정상 종료되었는데 서버가 모르고 있을수도 있다. 그러므로 그런 경우를 고려하여, 클라이언트가 mcu라면 fd를 -1로 초기화해주는 로직을 추가한다.
                                                client_info[i].fd = -1;
#endif
                                                break; // 사용자 검색 for루프 탈출
                                        }

                                        if (!strcmp(client_info[i].pw, pArray[1])) // 패스워드가 일치한다면...
                                        {
                                                // 네트워크 바이트오더가 적용된 클라이언트의 주소를 inet_ntoa로 변환하여 복사
                                                strcpy(client_info[i].ip, inet_ntoa(clnt_adr.sin_addr));

                                                // 전역변수를 건드릴 것이므로 mutex로 임계구역 설정
                                                pthread_mutex_lock(&mutx);
                                                client_info.index = i;
                                                client_info.fd = clnt_sock;
                                                clnt_cnt++; // 전역변수 증가
                                                pthread_mutex_unlock(&mutx);

                                                sprintf(msg, "[%s] New connected! (ip:%s, fd:%d, sockcnt: %d)\n", \
                                                                pArray[0], inet_ntoa(clnt_adr.sin_addr), clnt_sock, clnt_cnt);
                                                log_file(msg);
                                                write(clnt_sock, msg, strlen(msg)); // 클라이언트 소켓으로 메세지 전송

                                                // 생성된 클라이언트 소켓으로 스레드 생성
                                                // 함수포인터로 clnt_conection()를 start_routine함수로 지정했고,
                                                // CLIENT_INFO라는 구조체를 void포인터로 타입캐스팅하여 argument로 전달했다.
                                                pthread_create(t_id+i, NULL, clnt_connection, (void*)(client_info + i));
                                                pthread_detach(t_id[i]); // 생성된 스레드를 메인스레드에서 분리시킨다.

                                                break; // 사용자 검색 for루프 탈출
                                        }
                                }
                        }

                        // 모든 client_info를 순회했는데 못찾았다면...
                        if (i == MAX_CLNT)
                        {
                                sprintf(msg, "[%s] Authentication Error!\n", pArray[0]);
                                write(clnt_sock, msg, strlen(msg));
                                log_file(msg);
                                shutdown(clnt_sock, SHUT_WR);
                        }
                }
                else // 클라이언트 소켓에 적힌 값을 read했는데, 아예 클라이언트로부터 잘못된 값이 들어온 경우
                        shutdown(clnt_sock, SHUT_WR);

        } // 무한반복문 brace

        return 0;
}


void* clnt_connection(void* arg)
{
        CLIENT_INFO* client_info = (CLIENT_INFO*)arg;
        int str_len = 0;
        int index = client_info->index;
        char msg[BUF_SIZE]; // 클라이언트로부터 수신한 메시지가 담길 버퍼
        char to_msg[MAX_CLNT * ID_SIZE + 1]; // 클라이언트로 다시 전송할 메시지가 담긴 버퍼
        int i = 0;
        char *pToken;
        char *pArray[ARR_CNT] = {0}
        char strBuff[130] = {0}; // 실제 채팅내용이 담길 버퍼

        MSG_INFO msg_info; // 메시지가 담길 구조체 선언
        CLIENT_INFO* first_client_info;

        // 첫번째 클라이언트의 정보를 잡고 있는 이유는 클라이언트를 순회하기 편하게 하기 위함이다.
        first_client_info = (CLIENT_INFO*)((void*)client_info - (void*)(sizeof(CLIENT_INFO) * index));

        while (1)
        {
                memset(msg, 0x00, sizeof(msg)); // 메시지 버퍼 초기화
                str_len = read(client_info->fd, msg, sizeof(msg) - 1);
                if (str_len <= 0) // read()의 리턴값이 0 혹은 -1이라는 오류코드라면...
                        break; // 바깥 while문 탈출

                msg[str_len] = '\0'; // 전송받은 문자열의 맨 끝에 널문자 추가
                // 아래는 일반적인 스트링 토크나이징
                pToken = strtok(msg, "[:]");
                i = 0;
                while (pToken != NULL)
                {
                        pArray[i] = pToken;
                        if (i++ > ARR_CNT) // 토큰 배열이 담을 수 있는 최대 갯수를 넘어섰다면
                                break; // 토크나이징 while문 탈출
                        pToken = strtok(NULL, "[:]");
                }

                msg_info.fd = client_info->fd; // 타입이 다른데 괜찮은건가?
                msg_info.from = client_info->id;
                msg_info.to = pArray[0]; // 클라이언트에서 보낼때 이렇게 보내기로 약속했음
                sprintf(to_msg, "[%s]%s", msg_info.from, pArray[1]); // 이게 클라이언트 쪽에서 다시 찍힐 메시지임
                msg_info.msg = to_msg;
                msg_info.len = strlen(to_msg);

                sprintf(strBuff, "msg : [%s->%s] %s",\
                                msg_info.from, msg-info.to, pArray[1]);
                log_file(strBuff);
                send_msg(%msg_info, first_client_info);
        }

        close(client_info->fd); // 클라이언트가 쪽에서 read를 햇는데 0 이하의 값이 나왔다는 것은 연결종료의사임

        sprintf(strBuff, "Disconnect ID: %s (ip: %s, fd: %d, sockcnt:%d)\n",\
                        client_info->id, client_info->ip, client_info->fd, clnt_cnt - 1);
        log_file(strBuff);

        // clnt_cnt 전역변수 조작하기 위해 뮤텍스 사용
        pthread_mutex_lock(&mutx);
        clnt_cnt--;
        client_info->fd = -1; // 해당 아이디로 소켓 연결이 없음을 나타내기 위해서 fd를 -1로 설정해야함
        pthread_mutex_unlock(&mutx);

        return 0;
}

void send_msg(MSG_INFO* msg_info, CLIENT_INFO* first_client_info)
{
        int i = 0;

        if (!strcmp(msg_info->to, "ALLMSG")) // 클라이언트쪽에서 모두에게 보내는 메세지라고 지정해서 보냈다면
        {
                for (i = 0; i < MAX_CLNT; i++)
                        if ((first_client_info + i)->fd != -1) // 활성화 되어있는 유저들에게만 메시지가 가야함
                                write((first_client_info + i)->fd, msg_info->msg, msg_info->len);
        }
        else if (!strcmp(msg_info->to, "IDLIST")) // 접속중인 유저 리스트를 알려달라는 메세지를 보냈다면
        {
                char* idlist = (char*)malloc(ID_SIZE * MAX_CLNT); // 동적할당으로 아이디리스트 생성
                msg_info->msg[strlen(msg_info->msg) - 1] = '\0'; // 문자열 마지막에 널문자 추가
                strcpy(idlist, msg_info->msg);

                for (i = 0; i < MAX_CLNT; i++)
                {
                        if ((first_client_info + i)->fd != -1) // 활성화 되어있는 유저라면
                        {
                                strcat(idlist, (first_client_info + i)->id); // 해당 유저의 아이디를 최종 문자열 버퍼에 추가
                                strcat(idlist, " ");
                        }
                }
                strcat(idlist, "\n");
                write(msg_info->fd, idlist, strlen(idlist));
                free(idlist);
        }
        else // 귓속말 메시지라면
                for (i = 0; i < MAX_CLNT; i++) // 모든 클라이언트를 순회하면서
                        if ((first_client_info + i)->fd != -1) // 로그인 되어있고
                                if (!strcmp(msg_info->to, (first_client_info + i)->id)) // 귓속말 상대와 일치한다면
                                        write((first_client_info + i)->fd, msg_info->msg, msg_info->len);
}

// 에러메세지를 stderr를 통해서 터미널에 찍음
void error_handling(char * msg)
{
        fputs(msg, stderr);
        fputc('\n', stderr);
        exit(1);
}

// 일반적인 로그를 터미널에 찍음
void log_file(char * msgstr)
{
        fputs(msgstr, stdout);
}


iot_client

/* 코드 학습: kenGwon */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <pthread.h>
#include <signal.h>

#define BUF_SIZE 100
#define NAME_SIZE 20
#define ARR_CNT 5

void* send_msg(void* arg);
void* recv_msg(void* arg);
void error_handling(char* msg);

char name[NAME_SIZE] = "[Default]";
char msg[BUF_SIZE];

int main(int argc, char *argv[])
{
        int sock;
        struct sockaddr_in serv_addr;
        pthread_t snd_thread, rcv_thread;
        void * thread_return;

        // 명령행 인수 예외처리
        if (argc != 4)
        {
                printf("Usage : %s <IP> <port> <name>\n", argv[0]);
                exit(1);
        }

        // 사용자 이름 넣기
        sprintf(name, "%s", argv[3]);

        // 소켓 fd 생성 후 예외처리
        sock = socket(PF_INET, SOCK_STREAM, 0);
        if (sock == -1)
                error_handling("socket() error");

        // 소켓 주소&포트 정보가 담길 구조체의 메모리를 초기화하고 주소&포트 정보를 쓴다.
        memset(&serv_addr, 0, sizeof(serv_addr));
        serv_addr.sin_family = AF_INET;
        serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
        serv_addr.sin_port = htons(atoi(argv[2]));

        // 서버에 연결 후 예외처리
        if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == -1)
                error_handling("connect() error");

        // 메시지 담기
        sprintf(msg, "[%s:PASSWD]", name);
        write(sock, msg, strlen(msg)); // 서버로 메시지 전송

        // 서버와 메시지를 주고 받는 스레드 두개를 생성함
        pthread_create(&rcv_thread, NULL, recv_msg, (void *)&sock);
        pthread_create(&snd_thread, NULL, send_msg, (void *)&sock);

        pthread_join(snd_thread, &thread_return);
        //pthread_join(rcv_thread, &thread_return);

        close(sock); // 소켓을 닫는다.:
        return 0;
}

void * send_msg(void * arg)
{
        int* sock = (int*)arg; // 소켓 fd
        int str_len;
        int ret;
        fd_set initset, newset;
        struct timeval tv;
        char name_msg[NAME_SIZE + BUF_SIZE + 2];

        FD_ZERO(&initset); // fd_set 구조체 타입의 initset을 0으로 초기화한다.
        FD_SET(STDIN_FILENO, &initset); // initset의 fd를 stdin으로 설정한다.

        fputs("Input a message! [ID]msg (Defaulkt ID:ALLMSG)\n", stdout);

        while(1)
        {
                memset(msg, 0, sizeof(msg)); // 전역변수 msg 버퍼 초기화
                name_msg[0] = '\0';
                tv.tv_sec = 1; // timeval의 단위를 '초 단위'로 설정하겠다는 뜻
                tv.tv_usec = 0; // timeval의 단위를 '마이크로 초 단위'로 설정하겠다는 뜻

                newset = initset;
                ret = select(STDIN_FILENO + 1, &newset, NULL, NULL, &tv);

                if(FD_ISSET(STDIN_FILENO, &newset))
                {
                        fgets(msg, BUF_SIZE, stdin);

                        if(!strcmp(msg, "quit\n", 5)) // 사용자가 quit을 입력했다면...
                        {
                                *sock = -1; // 소켓 fd를 -1로 변경하고 스레드 종료
                                return NULL;
                        }
                        else if (msg[0] != '[') // 귓속말 모드가 아니라면...
                        {
                                strcat(name_msg, "[ALLMSG]");
                                strcat(name_msg, msg);
                        }
                        else // 귓속말 모드인 경우
                                strcpy(name_msg, msg);

                        // 완성된 문자열을 서버로 전송했는데 실패했다면 소켓을 닫고 스레드 종료
                        if (write(*sock, name_msg, strlen(name_msg)) <= 0)
                        {
                                *sock = -1;
                                return NULL;
                        }
                }

                if(ret == 0) // stdin에서 문자열을 읽었는데 아무것도 없는데 소켓도 닫혀있다면 스레드 종료
                {
                        if(*sock == -1)
                                return NULL;
                }
        }
}

void * recv(void * arg)
{
        int * sock = (int *) arg;
        int i;
        char *pToken;
        char *pArray[ARR_CNT] = {0};
        char name_msg[NAME_SIZE + BUF_SIZE + 1];
        int strlen;

        while(1)
        {
                memset(name_msg, 0, sizeof(name_msg));
                str_len = read(*sock, name_msg, NAME_SIZE + BUF_SIZE);

                if (str_len <= 0) // 서버로부터 값을 읽었는데 실패했다면 소켓을 닫고 스레드 종료
                {
                        *sock =-1;
                        return NULL;
                }

                name_msg[str_len] = '\0';
                fputs(name_msg, stdout);
        }
}

void error_handling(char *msg)
{
        fputs(msg, stderr);
        fputc('\n', stderr);
        exit(1);
}

Makefile

  • 매번 컴파일을 할 때마다 gcc iot_client.c -o iot_client -D_REENTRANT -pthread 이렇게 쳐주는게 귀찮다. 그래서 Make 유틸리티를 쓰는 것이다.
  • 그리고 메이크 유틸리티는 소스파일의 touch 시간을 기준으로 다시 컴파일 할지 말지를 결정한다.
#CC=arm-linux-gnueabihf-gcc
CC=gcc

TARGET_SRV=iot_server
OBJECT_SRV=$(TARGET_SRV).o

TARGET_CLN=iot_client
OBJECT_CLN=$(TARGET_CLN).o

#LDFLAGS=-D_REENTRANT -pthread -lmysqlclient
LDFLAGS=-D_REENTRANT -pthread

all : $(TARGET_SRV) $(TARGET_CLN)
#all : iot_server iot_client

$(TARGET_SRV):$(OBJECT_SRV)
#iot_server: iot_server.o
    $(CC) -o $@ $(OBJECT_SRV) $(LDFLAGS)
	#gcc -o iot_server iot_server.o -D_REENTRANT -pthread
    
$(TARGET_CLN):$(OBJECT_CLN)
#iot_client:iot_client.o
    $(CC) -o $@ $(OBJECT_CLN) $(LDFLAGS)

%.o:%.c
#iot_server.o:iot_server.c
    $(CC) -c -o $@ $<
    #$(CC) -c -o iot_server.o iot_server.c

clean:
    rm -f *.o $(TARGET_SRV) $(TARGET_CLN)

make 동작 플로우

  • 라벨명은 계층적으로 실행할 수 있다. 코드를 차근차근 봐보자.
      1. 위 코드를 보면 all : $(TARGET_SRV) $(TARGET_CLN) 이 바로 그러한 계층적 라벨 호출에 해당하는 것이다. 이 라인을 만나면 첫번째 $(TARGET_SRV)라벨이 Makefile에 있는지 아래로 내려가면서 찾는다.
      1. 아래에 $(TARGET_SRV)라벨이 있기 때문에 $(TARGET_SRV):$(OBJECT_SRV)로 이동한다. 그러면 또 '필요파일'(콜론':' 뒤에 오는 파일)을 찾도록 하고 있다.
      1. 그러면 또 $(OBJECT_SRV)를 찾으러 내려간다. 그런데 저런 이름의 라벨은 없다.
      1. 그러면 현재 디렉토리에서 해당 파일이 있는지 찾아본다. 그러면 $(OBJECT_SRV)에 해당하는 iot_server.o를 찾아낸다.
      1. 만약 현재 디렉토리에 iot_server.o가 없다면, 바로 18번째 라인을 실행한다.
      1. 만약 현재 디레곹리에 iot_server.o가 있다면, 그것에 해당하는 라벨이 아래에 또 있어서 그쪽으로 이동하게 된다.
      1. %.o:%.c 라벨이 바로 그것에 해당한다. 여기서 같은 이름의 c파일을 찾도록 하는데, 이 라벨을 통해 iot_server.c를 찾게 된다.
      1. 이제 최종 명령인 $(CC) -c -o $@ $<를 통해서 컴파일을 하려는데, 여기서 make 유틸리티의 고유기능이 발휘된다.
      1. make 유틸리티는 소스파일의 최종 수정시간과 목적파일의 최종 수정시간을 비교하여 컴파일 여부를 결정한다.
      1. 최종명령이 실행되었으므로, 다시 라벨 호출 스택을 거슬러 올라가서 13번째 라인의 두번째 라벨이었던 $(TARGET_CLN)에 대한 처리를 시작한다.
      1. 동작 로직은 위와 비슷하게 한번 더 반복하게 된다.

결국 핵심은 make 유틸리티는 Makefile을 위에서부터 차근차근 읽어가면서 라벨이 라벨을 호출하면서 계층적으로 호출스택이 깊어지면서 최종명령어가 실행되고, 다시 위로 올라와서 다음 계층의 명령을 실행하는 식의 구조로 작동한다는 것이다.

각종 옵션 설명

  • 가장 중요한 것은 라벨 아래에 작업 내용을 명시할 때는 반드시 탭(tab)으로 들여쓰기를 해주어야 한다는 것이다.(space 4칸으로 하면 오류난다!! 반드시 tab으로 들여쓰기를 해주어야 한다.)

  • all:, clean 같은것들은 라벨(레이블)이라고 부른다. 그냥 make를 치면 Makefile에서 첫번째로 등장하는 라벨을 실행한다. 그래서 그냥 make를 치든 make all를 치든 동일한 동작이 발생한다.

  • LDFLAGS: 컴파일을 할 때 링크해야하는 라이브러리가 있을 수 있다.

  • D_REENTRANT(Define_REENTRANT) 옵션을 쓰면 스레드를 쓰는데 똑같은 기능을 지원하는 pthread 함수 중에서 가장 안전한 함수를 쓰겠다는 것을 의미하는 옵션이다. 이건 -pthread를 링크하여 컴파일 하려면 반드시 넣어줘야 하는 옵션이다.

  • 27번째 라인에서 @:앞에 있는 목적파일(여기서는 '라벨 이름')이고, <:뒤에 있는 필요파일(여기서는 파일이름) 을 의미한다.

profile
스펀지맨

0개의 댓글