본문 바로가기

BackEnd/Linux

Linux / network - 리눅스기초를 활용한 데이터 통신 10(Select, fd_set, FD테이블)

안녕하세요 인포돈 입니다.

본 내용은 우분투를 기본으로 작성되었습니다.
Cloud Computing을 활용하여 서버를 구축하였습니다.

Select를 활용한 데이터 통합 후 통신 (클라이언트 3, 서버 1)

 본 포스팅에서는 Select를 활용하여 3개의 클라이언트에서 들어온 단어를 " " 공백을 활용하여 구분하여 합치고, 정수의 경우 모두 합한 값을 한 줄로 표현하여 다시 클라이언트에 보내주는 프로그램을 목표로 합니다. 그러면 우리는 우선 select가 무엇인지에 대해서 알아야 합니다.

 

 - Select

int select(in nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)

ndfs : 감시할 파일의 개수를 의미합니다. 인덱스형 테이기 때문에 +1을 해주어야 합니다

readfds : 읽기 이벤트를 검사할 정보를 담은 비트

writefds : 쓰기 이벤트를 검사할 정보를 담은 비트

exceptfds : 예외 이벤트를 검사할 정보를 담은 비트

timeout : 이벤트를 기다릴 시간제한

 * 이때 timeval의 구조체는 아래와 같다.

struct timeval{
	long tv_sec;	//seconds
    long tv_usec;	//microseconds
}

 

 - Select 매크로 함수

FD_ZERO(fd_set *fds) : fd_set테이블을 0으로 초기화한다.

FD_SET(int fd, fd_set *fds) : fd_set테이블에 검사할 파일 목록을 추가한다.

FD_CLR(int fd, fd_set *fds) : fd_set테이블에서 파일을 삭제한다.

FD_ISSET(int fd, fd_set *fds) : fd_set테이블을 검사한다.

 

 - Select의 이해

앞선 기본 구조와 매크로 함수를 슬쩍 보시고 여기에서 이해하시면 됩니다. 기본적으로 우리가 알고 있는 비트의 박스들이라고 생각하면 쉽습니다.

0 0 0 0 0 0 0 0 0 0

요런 식으로 비트들이 되어있고 여기서 select함수를 사용하면, 이벤트가 발생할 때까지 대기합니다. 이때 만약 이벤트가 발생하면, 해당 소켓을 fd_set에 추가를 해주면 됩니다. 이때 우리가 앞서 살펴본 매크로 함수를 사용합니다. FD_SET이죠 이를 통해서 소켓의 정보를 넣어줍니다.

0 0 0 1 0 0 0 0 0 0

그렇다면 fd_set에는 데이터가 있음을 의미하게 됩니다.

그렇다면 이제 우리는 FD_ISSET을 활용해서 해당 데이터가 있는 소켓을 찾아내서 만약 데이터가 있다면 read를 통해서 값을 읽어오고 write를 통해서 보내주면 끝!

 

 이해가 어려우실 수 있는데 아주 쉽게 생각하면 됩니다. select는 fd_set이라는 비트 테이블에서 이던 특정 이벤트가 발생하면, fd_set테이블에 해당 정보를 넣어주고, 그 정보를 끝집어다가 사용하는 함수! 이 정도로 기억하셔도 됩니다.


서버 코드

#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#define MAXLINE 1024
#define PORTNUM 3600
#define SOCK_SETSIZE 1021
#define TIMEOUT 5

struct send_data{
        int num[3];
        char str[3][MAXLINE];
};
struct rev_data{
        int num;
        char str[MAXLINE];
};

int main(int argc, char **argv)
{
        int listen_fd, client_fd;
        socklen_t addrlen;
        int fd_num;
        int maxfd = 0;
        int sockfd;
        int i= 0;
        char buf[MAXLINE];
        fd_set readfds, allfds;

        struct rev_data rev_msg;
        //char send_msg[MAXLINE];
        struct send_data send_msg;
        struct timeval tv;

        bool flag = false;

        struct sockaddr_in server_addr, client_addr;

        if((listen_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
        {
                perror("socket error");
                return 1;
        }
        memset((void *)&server_addr, 0x00, sizeof(server_addr));
        server_addr.sin_family = AF_INET;
        server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
        server_addr.sin_port = htons(PORTNUM);

        if(bind(listen_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1)
        {
                perror("bind error");
                return 1;
        }
        if(listen(listen_fd, 5) == -1)
        {
                perror("listen error");
                return 1;
        }

        FD_ZERO(&readfds);
        FD_SET(listen_fd, &readfds);

        maxfd = listen_fd;
        while(1)
        {
                allfds = readfds;
                if(flag){
                        for(i = 4 ; i <= maxfd ; i++){
                                if(FD_ISSET(i, &allfds)){
                                        write(i,&send_msg,sizeof(send_msg));
                                }
                        }
                }
                tv.tv_sec = 3;
                tv.tv_usec = 0;

                fd_num = select(maxfd + 1 , &allfds, (fd_set *)0,
                                          (fd_set *)0, &tv);

                if (FD_ISSET(listen_fd, &allfds))
                {
                        addrlen = sizeof(client_addr);
                        client_fd = accept(listen_fd,
                                        (struct sockaddr *)&client_addr, &addrlen);

                        FD_SET(client_fd,&readfds);
                        if (client_fd > maxfd)
                                maxfd = client_fd;
                        printf("Accept OK : %s (%d) \n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
                        continue;
                }

                for (i = 0; i <= maxfd; i++)
                {
                        sockfd = i;
                        if (FD_ISSET(sockfd, &allfds))
                        {       
                                memset(&rev_msg, 0x00, sizeof(struct rev_data));
                                if (read(sockfd, &rev_msg, sizeof(rev_msg)) <= 0)
                                {       
                                        close(sockfd);
                                        FD_CLR(sockfd, &readfds);
                                }
                                else
                                {      
                                        if (strncmp(rev_msg.str, "quit\n", 5) ==0)
                                        {       
                                                close(sockfd);
                                                FD_CLR(sockfd, &readfds);
                                        }else if(strlen(rev_msg.str)==0){
                                                continue;
                                        }
                                        else
                                        {       flag = true;
                                                printf("from client %d : Read : %s and %d \n",sockfd, rev_msg.str, rev_msg.num);
                                                strcpy(send_msg.str[sockfd-4], rev_msg.str);
                                                send_msg.num[sockfd-4] = rev_msg.num;
                                        }
                                }
                                if (--fd_num <= 0)
                                        break;
                        }
                }//for i end
        }
}

 코드가 길어 보이지만 정말 크게 어려울 것이 없는 코드입니다. 하나하나 묶어서 설명해 드리겠습니다.

 

 - 구조체

struct send_data{
        int num[3];
        char str[3][MAXLINE];
};
struct rev_data{
        int num;
        char str[MAXLINE];
};

많이 설명할 것도 없습니다. rev_data는 클라이언트로부터 받을 구조체이고, send_data는 앞서 말한 클라이언트에게서부터 데이터를 합쳐서 다시 클라이언트에게 보내줄 구조체입니다.

 

 -  listen 초기 세팅

fd_set readfds, allfds;
struct timeval tv;

FD_ZERO(&readfds);
FD_SET(listen_fd, &readfds);

maxfd = listen_fd;

기본 변수들을 선언해 주고 FD_ZERO를 통해서 초기화해줍니다. (이때 앞서 말한 fd_set테이블이 readfds가 되는 겁니다.) 그리고 FD_SET을 통해서 우리가 소켓에서 받아올 listen_fd를 넣어줍니다. 즉, readfds테이블에 listen_fd를 넣어주는 겁니다. 넣어주어야 이벤트를 감지할 수 있기 때문이죠.

allfds = readfds;
if(flag){
    for(i = 4 ; i <= maxfd ; i++){
        if(FD_ISSET(i, &allfds)){
            write(i,&send_msg,sizeof(send_msg));
        }
    }
}
tv.tv_sec = 3;
tv.tv_usec = 0;

fd_num = select(maxfd + 1 , &allfds, (fd_set *)0, (fd_set *)0, &tv);

여기서 헷갈리실 수 있는데 if(flag) 부분은 클라이언트에게 send_msg를 보내주는 역할인데 앞쪽에 서술해야만 했던 이유는 아래쪽에 배치할 경우 데이터가 꼬여버리는 현상이 일어나서 앞쪽에 비치하였습니다. (우선은 무시!)

앞서 세팅한 fd_set을 allfds에 옮겨 줍니다. (데이터의 손실을 방지하기 위해서!) 그런 다음 저희는 이벤트가 들어오지 않으면 얼마나 기다렸다가 다음 코드를 실행할지 설정해줍니다.

 

마지막으로 우리는 select함수를 통해서 소켓이 들어오기 전까지 대기를 해줍니다.

 

 - 클라이언트 접속

if (FD_ISSET(listen_fd, &allfds)){
    addrlen = sizeof(client_addr);
    client_fd = accept(listen_fd,(struct sockaddr *)&client_addr, &addrlen);

    FD_SET(client_fd,&readfds);
    if (client_fd > maxfd)
    maxfd = client_fd;
    printf("Accept OK : %s (%d) \n",inet_ntoa(client_addr.sin_addr),ntohs(client_addr.sin_port));
    continue;
}

이곳은 기본 소켓과 똑같습니다. 클라이언트가 접속을 하면 앞서 만들어 놓은 select가 발생하여 데이터의 개수를 반환하게 됩니다. 만약 해당 listen_fd에 클라이언트가 접속을 했다면, 이벤트가 발생했을 것이고 우리는 accept을 통해서 클라이언트와 수신할 준비를 완료합니다. 물론 이때 우리는 fd_set테이블도 바꿔줘야 합니다. 접속이 들어온 클라이언트의 정보를 담기 위함이죠.

 

 - 데이터 읽어오기

for (i = 0; i <= maxfd; i++){
    sockfd = i;
    if (FD_ISSET(sockfd, &allfds)){       
        memset(&rev_msg, 0x00, sizeof(struct rev_data));
        if (read(sockfd, &rev_msg, sizeof(rev_msg)) <= 0){	//클라이언트에게 들어온 데이터가 없음       
            close(sockfd);
            FD_CLR(sockfd, &readfds);
        }else	=//클라이언트에게 들어온 데이터가 있음
        {      
            if (strncmp(rev_msg.str, "quit\n", 5) ==0) {//quit이라는 단어가 있는 경우
                close(sockfd);
                FD_CLR(sockfd, &readfds);
            }else if(strlen(rev_msg.str)==0){	//str이 없는경우 (timeval로 넘어온경우)
                continue;
            }
            else{		=//정상적으로 들어온 경우
                flag = true;
                printf("from client %d : Read : %s and %d \n",sockfd, rev_msg.str, rev_msg.num);
                strcpy(send_msg.str[sockfd-4], rev_msg.str);
                send_msg.num[sockfd-4] = rev_msg.num;
            }
    }
    if (--fd_num <= 0)
    break;
}

여기가 어려울 수도 있지만 천천히 살펴보시면 됩니다. 우리가 검사한 maxfd를 순회하면서 이벤트가 발생한 데이터가 있는지 확인합니다. 주석을 하나씩 살펴보면 됩니다. 이때 우리는 quit이라는 단어가 있으면 그 클라이언트를 종료해주고 만약 str이 안 들어온 경우 즉 timeval로 인해서 이벤트를 받지 않고 넘어온 경우도 처리해 줍니다. 만약 정상적으로 클라이언트에게 정보가 들어오면 우리는 해당 str를 send_msg에 넣어줍니다. 이때 배열을 sockfd-4로 한경우는 해당 위치에 정보가 들어있는데.... 사실 이경우는 저도 정확하게 알지는 못하지만 정확한 데이터가 출력되지 않아서 해당 데이터가 담긴 위치를 console에 찍어보니 -4를 해줘야 정확한 데이터가 넘어옴을 확인해서 변경해 줬습니다... ㅜㅜ


클라이언트 코드

#include <sys/socket.h>  /* 소켓 관련 함수 */
#include <arpa/inet.h>   /* 소켓 지원을 위한 각종 함수 */
#include <sys/stat.h>
#include <stdio.h>      /* 표준 입출력 관련 */
#include <string.h>     /* 문자열 관련 */
#include <unistd.h>     /* 각종 시스템 함수 */
#include <stdlib.h>

#define MAXLINE    1024

struct send_data{
        int num[3];
        char str[3][MAXLINE];
};
struct rev_data{
        int num;
        char str[MAXLINE];
};

int main(int argc, char **argv)
{
    struct sockaddr_in serveraddr;
    int server_sockfd;
    int client_len;
    char buf[MAXLINE];
    struct rev_data rev_msg;    //서버에 보내는 데이터
    struct send_data send_msg;  //서버에서 받는 데이터
    int maxfd = 0;
    char temp_buf[MAXLINE];

    fd_set temps, reads;
    int result;
    struct timeval tv;

    if ((server_sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
    {
        perror("error :");
        return 1;
    }

    /* 연결요청할 서버의 주소와 포트번호 프로토콜등을 지정한다. */
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    serveraddr.sin_port = htons(3600);

    client_len = sizeof(serveraddr);

    /* 서버에 연결을 시도한다. */
    if (connect(server_sockfd, (struct sockaddr *)&serveraddr, client_len)  == -1)
    {
        perror("connect error :");
        return 1;
    }

    FD_ZERO(&reads);
    FD_SET(server_sockfd, &reads);
    FD_SET(0,&reads);
    maxfd = server_sockfd;

while(1)
{
    temps = reads;
    tv.tv_sec = 4;
    tv.tv_usec = 0;

    result = select(maxfd + 1, &temps, 0, 0, &tv);

    if(result == -1){
        printf("error \n");
        return 1;
    }else if(result ==0){
        continue;
    }else{

    if(FD_ISSET(0,&temps)){

    memset(buf, 0x00, MAXLINE);
    read(0, buf, MAXLINE);    /* 키보드 입력을 기다린다. */
    if(strncmp(buf, "quit\n",5) == 0)
        break;
    memset(rev_msg.str, 0x00, MAXLINE);
    memset(&rev_msg, 0x00, sizeof(struct rev_data));
    char *ptr = strtok(buf," ");
    strcpy(rev_msg.str,ptr);

    ptr = strtok(NULL," ");
    rev_msg.num = atoi(ptr);

    if (write(server_sockfd, &rev_msg, sizeof(rev_msg)) <= 0) /* 입력 받은 데이터를 서버로 전송한다. */
    {
        perror("write error : ");
        return 1;
    }
    memset(buf, 0x00, MAXLINE);
    FD_CLR(0,&temps);
    }

    if(FD_ISSET(server_sockfd,&temps)){
    /* 서버로 부터 데이터를 읽는다. */
    if (read(server_sockfd, &send_msg, sizeof(send_msg)) <= 0)
    {
        perror("read error : ");
        return 1;
    }
    printf("read : %s %s %s  and %d \n", send_msg.str[0],send_msg.str[1],send_msg.str[2], send_msg.num[0]+send_msg.num[1]+send_msg.num[2]);
    }
    }
}
    close(server_sockfd);
    return 0;
}

클라이언트 코드는 역시 뭐 크게 해 줄 게 없습니다. send_msg로 서버에게서 받은 데이터를 출력하면 끝! 기본적인 코드들은 이전 포스트를 참고해 주시면 됩니다.


실행 화면

서버 실행 화면
클라이언트 실행 화면

본 포스팅을 마무리로 리눅스에서의 데이터 통신에 대해서 다루어 봤습니다. 물론 코드들이 모두 지저분하고 억지로 끼어 맞춘 부분이 많이 있다는 것을 느끼고 있습니다. 그러나 이번 리눅스 시리즈를 포스팅하면서 리눅스에 대해서 처음으로 다뤄보고 코딩해보면서 이론을 배우면서 느꼈던 데이터 통신과는 또 다른 문제들이 많이 존재함을 알 수 있었습니다. 보다 서버를 잘 다루려면 이러한 데이터를 어떻게 효율적으로 보내고 받을지에 대한 고찰이 계속되어야 될 거 같습니다