Datagram Socket 통신

Stream Socket은 연결 지향형이고 Datagram은 비연결형의 Socket 통신 방법입니다. 신뢰성은 그만큼 떨어지지만 단순하고 빠른 전송을 위한 것으로 UDP 프로토콜을 사용합니다. Stream Socket과는 다르게 Datagram 소켓을 사용하여 간단히 Server - Client 통신 소스코드를 짜보도록 합시다. Stream Socket은 서버와 클라이언트 사이에 연결을 맺어주느라 소스 코드의 길이가 길지요. 지금 볼 datagram socket은 이러한 연결(Connection) 과정이 생략이 됩니다.

그러니까 Stream Socket과는 다르게 listen, accept를 datagram socket에서 사용할 필요가 없어졌습니다. 그래서 다행이라고 할까요? Stream socket보다는 코드의 길이가 짧습니다.

서버 코드

아래에서 구현한 소스코드는 client의 메시지를 받아서 그대로 client쪽으로 돌려주는 echo 서버의 소스코드입니다.

//dgram-server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

#define PORT 12346
#define BUF_SIZE 128

void err_exit(const char *err){
        perror(err);
        exit(1);
}

int main(void){
        int socket_fd, n;
        struct sockaddr_in host_addr, client_addr;
        socklen_t size;
        char buffer[BUF_SIZE] = {0,};

        //Datagram socket 생성
        socket_fd=socket(PF_INET,SOCK_DGRAM,0);
        if(socket_fd < 0) err_exit("socket error ");

        //server ip와 bind하기 위해 주소 설정
        memset(&host_addr, 0, sizeof(host_addr));
        host_addr.sin_family=AF_INET;
        host_addr.sin_port=htons(PORT); 
        host_addr.sin_addr.s_addr=0;    //자신의 주소로 자동으로 설정 

        //socket과 host_addr과 bind
        if(bind(socket_fd,(struct sockaddr *)&host_addr,sizeof(struct sockaddr)) < 0)
                err_exit("bind error ");

        //Client의 메시지를 그대로 다시 돌려준다. 
        memset(&client_addr, 0, sizeof(struct sockaddr));
        while(1){
                size = sizeof(struct sockaddr);

                //accept에서 client_addr을 얻어온 stream socket방식과 다르게
                //accept가 없는 datagram socket은 
                //recvfrom에서 client 정보를 얻어올 수 있다. 
                n = recvfrom(socket_fd, buffer, BUF_SIZE, 0,
                                (struct sockaddr*)&client_addr, &size);

                if(n < 0) err_exit("recvfrom error ");

                //Client의 접속 정보를 출력
                printf("Client Info : IP %s, Port %d\n", 
                        inet_ntoa(client_addr.sin_addr),
                        ntohs(client_addr.sin_port));


                //quit라는 메시지를 받으면 종료 
                if(!strcmp(buffer,"quit") || n == 0) break;

                //보낼 client_addr 객체까지 있어야 전달 가능
                n = sendto(socket_fd, buffer, n, 0,
                                (struct sockaddr*)&client_addr, sizeof(struct sockaddr));
                if(n < 0)err_exit("send error ");

        }

        shutdown(socket_fd, SHUT_RDWR);
        return 0;

}

 

이전 stream socket과 다른 점이 있다면, SOCK_STREAM이 아닌 SOCK_DGRAM을 사용한다는 점입니다. 

socket_fd=socket(PF_INET,SOCK_DGRAM,0);

stream socket은 send, recv를 사용하였는데, Datagram socket에서는 sendto, recvfrom을 사용하는 것을 알 수 있네요.

이 두 함수들의 끝 2개는 sockaddr의 구조체와 크기입니다. Server는 accept과정이 없기 때문에 client의 주소를 알 방법이 없습니다. 그래서 다시 echo할 주소가 없다는 거죠. recvfrom은 메시지를 받을때 송신자의 주소를 가져오기 위해서 사용합니다. sendto도 마찬가지로 보낼 곳을 지정할때 사용합니다. 이 주소 정보가 없다면 아래와 같은 관계가 성립됩니다.

send(sockfd, buf, len, flags) -> sendto(sockfd, buf, len, flags, NULL, 0)
recv(sockfd, buf, len, flags) ->  recvfrom(sockfd, buf, len, flags, NULL, NULL)

 

클라이언트 코드

이에 대응하는 client의 소스코드입니다. 

//dgram-client.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h> 

#define BUF_SIZE 128

void err_exit(const char *err){
        perror(err);
        exit(1);
}

int main(int argc, char *argv[])
{
        int sockfd = 0, n = 0;
        uint16_t port;
        char buffer[BUF_SIZE] = {0,};
        struct sockaddr_in server_addr;
        int size; 

        if(argc != 3){
                printf("\n Usage: %s server_ip port \n",argv[0]);
                return 1;
        } 

        port = atoi(argv[2]);

        //Internet용 socket(Datagram)을 연다.
        if((sockfd = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
                err_exit("socket error ");

        //server_addr의 구조체를 깔끔히 0으로 도배 
        memset(&server_addr, 0, sizeof(server_addr)); 
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(port); 
        //127.0.0.1은 본인의 IP이다. 
        inet_aton(argv[1], (struct in_addr*)&server_addr.sin_addr);


        while(1){
                while(1){
                        printf("message:");
                        fgets(buffer, BUF_SIZE, stdin);
                        if(strlen(buffer) >= 100)
                                printf("message too large\n");
                        else break;

                };
                // \n을 \0로 처리 
                buffer[strlen(buffer)-1] = '\0';

                size = sizeof(struct sockaddr);
                //서버에게 메시지 전송
                //connect 콜이 없어서 server ip로 직접 보낸다.
                n = sendto(sockfd, buffer, strlen(buffer)+1, 0,
                                (struct sockaddr*)&server_addr, sizeof(struct sockaddr));
                
                if(n < 0) err_exit("send error ");
                if(!strcmp(buffer, "quit")) break;

                //서버로부터 에코된 메시지를 받음
                n = recvfrom(sockfd, buffer, BUF_SIZE, 0,
                                (struct sockaddr*)&server_addr, &size);

                if(n < 0) err_exit("recv error ");

                printf("%s\n", buffer);


        }

        shutdown(sockfd, SHUT_RDWR);
        return 0;
}

결과 화면

Server Client
# ./server
Client Info : IP 127.0.0.1, Port 60112
Client Info : IP 127.0.0.1, Port 60112
Client Info : IP 127.0.0.1, Port 60112
# ./client 127.0.0.1 12346
message:hello datagram server~~
hello datagram server~~
message:good good
good good
message:quit

 

여기까지 간단하게 UDP를 사용하는 server - client 통신 구현을 해보았습니다. 여러 클라이언트를 받기 위해서는 다중 프로세스, 다중 쓰레드, 다중입출력을 활용해야겠죠. 다중입출력 방식 중 하나인 epoll에 관한 설명도 저의 블로그에 있으니 확인해보세요.

https://reakwon.tistory.com/236

 

[LINUX] epoll의 개념과 이를 활용한 다중입출력 방식의 서버, 클라이언트

epoll 뿐만 아닌 다중 입출력의 설명과 코드를 아래의 note에서 확인하실 수 있습니다. https://reakwon.tistory.com/233 리눅스 프로그래밍 note 배포 티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포

reakwon.tistory.com

 

반응형
블로그 이미지

REAKWON

와나진짜

,

epoll 뿐만 아닌 다중 입출력의 설명과 코드를 아래의 note에서 확인하실 수 있습니다. 

https://reakwon.tistory.com/233

 

리눅스 프로그래밍 note 배포

티스토리에 리눅스에 관한 내용을 두서없이 여지껏 포스팅했었데요. 저도 제 포스팅을 찾기가 어렵기도 하고 티스토리에서 코드삽입을 하게 되면 이게 일자로 쭉 쓰여져있는 x같은 현상이 생겨

reakwon.tistory.com

 

epoll

epoll은 poll과 비슷한 방식으로 동작하지만 전체 파일 디스크립터가 아닌 이벤트가 발생한 객체만 되돌려줍니다. 그리고 두 가지 이벤트 트리거 방식을 선택하여 동작할 수 있는데, Level Trigger 방식과 Edge Trigger 방식입니다. 이 둘을 줄여서 LT, ET라고 하겠습니다. 

이번 글에서는 epoll에 대한 개념을 설명한 후에 Level Trigger 방식의 에코 서버, Edge Trigger 방식의 에코서버의 예제코드를 작성해보도록 하겠습니다. 서버의 소스코드나 클라이언트 소스코드가 길게 느껴질 수 있는데, 한줄 한줄 읽어보면 진짜 별거없습니다!

Level Trigger

LT방식은 어떤 일이 일어났을 때 상태에 따라서 트리거를 지속 시킵니다. 예를 들어서 입력 버퍼가 채워져있는 상태를 1, 아닌 상태를 0으로 놓고, 입력 버퍼가 채워져있는 상태는 계속 1인 상태이기 때문에 이벤트가 지속된다는 뜻입니다.

위의 그림에서도 볼 수 있듯이 1인 상태는 버퍼가 채워져있는 상태로 정의하고, 빨간색 부분에 대해서 지속적으로 이벤트 발생을 알려줍니다.

 

Edge Trigger

반면 ET방식은 사건이 발생한 시점에 딱 한번 트리거 됩니다. 그러니까 버퍼가 채워지는 그 시점에만 이벤트가 발생한다는 뜻입니다.

이러한 LT, ET 방식인지에 따라서 잠시 후에 나올 epoll_wait 함수가 다음 이벤트까지 기다리는 시점이 달라집니다. 이제 epoll을 다루기 위한 세가지 함수를 소개합니다.

 

1. epoll_create 

#include <sys/epoll.h>
int epoll_create(int size);

epol l객체를 생성합니다. 커널 2.6.8 버전부터 이 size라는 인자는 무시되며 그냥 0보다 큰 값으로 설정만 해주면 됩니다. 실패시에는 -1을 반환하고 정상 반환일때는 0보다 큰 값이 반환됩니다. 이 반환 값을 파일 디스크립터입니다. 그래서 close로 닫을 수 있습니다. 이렇게 나오는 epoll의 파일 디스크립터는 제어, 이벤트 대기시에 사용이 됩니다.

 

2. epoll_ctl

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll_ctl 함수는 epfd라는 epoll 파일 디스크립터에의해 참조되는 관심 리스트(interest list)에 fd를 더할지, 변경할지, 삭제할지를 도와주는 함수입니다. event는 그 fd에 대해서 관심있게 주시할 event를 설정해줍니다. poll과 유사합니다. event에는 아까 소개한 ET 방식도 설정할 수 있습니다. operation을 뜻하는 op는 아래의 표와 같이 세가지가 있습니다.

Op 설명
EPOLL_CTL_ADD epfd의 관심 리스트에 entry를 더해줍니다. entry는 관심있게 볼 파일 디스크립터 fd와 주시할 event를 포함한 개념이라고 보시면 됩니다.
EPOLL_CTL_MOD 관심 리스트의 fd와 연관된 세팅을 변경합니다. 여기서 fd는 전달받은 인자인 event로 새롭게 세팅 됩니다.
EPOLL_CTL_DEL 관심 리스트에 fd를 등록 해제합니다. 여기서 인자 event는 무시되니까 NULL을 전달하면 됩니다.

 

epoll_event 구조체는 아래와 같이 정의되어있습니다.

typedef union epoll_data {
        void        *ptr;
        int          fd;
        uint32_t     u32;
        uint64_t     u64;
} epoll_data_t;

struct epoll_event {
        uint32_t     events;      /* Epoll events */
        epoll_data_t data;        /* User data variable */
};

 

events의 설정할 수 있는 이벤트들은 poll과 비슷합니다.

Event 설명
EPOLLIN 데이터를 읽을 수 있는 event를 설정합니다. fd에 대해서 데이터를 읽을 수 있을 때 event가 발생됩니다.
EPOLLOUT 데이터를 쓸 수 있는 event를 설정합니다. 데이터를 쓸 수 있을때 event가 발생됩니다.
EPOLLRDHUP 커널 2.6.17부터 생긴 이벤트인데, 스트림 소켓이 커넥션이 닫혔거나 shutdown 됐을때 이벤트를 발생시킵니다.
EPOLLPRI POLLPRI와 같아 high priority 자료를 바로 읽을 수 있는 이벤트입니다.
EPOLLERR 파일 디스크립터에 에러가 발생했을때 이벤트가 발생됩니다. 예외적인 이벤트이므로 이러한 이벤트는 사용자가 설정할 필요없습니다.
EPOLLHUP 파일 디스크립터가 끊겼을 때 발생합니다. 역시 사용자가 일부러 이벤트를 지정할 필요없습니다.
EPOLLET Edge Trigger 방식으로 이벤트 트리거 방식을 변경시킵니다. epoll은 기본적으로 Level Trigger 방식을 사용하고 있습니다.
EPOLLONESHOT 파일 디스크립터에 대한 이벤트를 한번만 발생시키고자 할 때 이 이벤트를 지정하면 됩니다.

 

3. epoll_wait

#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);

epfd에 대해서 이벤트가 발생함을 기다리는 역할을 합니다. Event에는 발생한 event들이 있습니다. 최대 기다릴 events수를 maxevents에 넣을 수 있습니다. Timeout에 따라서 epoll_wait이 계속 기다릴지, 특정 시간 동안 기다릴지, 바로 반환할 지를 정해줄 수 있습니다.

  • timeout = -1 : 이벤트 발생시까지 무한히 대기합니다.
  • timeout = 0 : 곧 장 반환합니다.
  • timeout > 0 : ms만큼 대기하다가 반환됩니다.

 

예제 코드 - Level Trigger Epoll

아래는 Level Trigger 방식의 epoll 에코 서버의 예제입니다.

server 

//epoll_server-level.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>

#FD_MAX 1024
#define PORT 12346
#define BUF_SIZE 1

const char *welcome_message = "Welcome!\n";

void err_exit(const char *err){
        perror(err);
        exit(1);
}

void clear_fd(const int epoll_fd, const int fd){
        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
        close(fd);
}
int main(void){
        int socket_fd, accepted_fd;
        struct sockaddr_in host_addr, client_addr;
        socklen_t size;
        int epoll_fd, i, n, ret; 
        char buffer[BUF_SIZE] = {0,};
        struct epoll_event events[FD_MAX];
        int pos = 0;

        //STREAM socket 생성
        socket_fd=socket(PF_INET,SOCK_STREAM,0);
        if(socket_fd < 0) err_exit("socket error ");

        //server ip와 bind하기 위해 주소 설정
        memset(&host_addr, 0, sizeof(host_addr));
        host_addr.sin_family=AF_INET;
        host_addr.sin_port=htons(PORT); 
        host_addr.sin_addr.s_addr = 0;  //자신의 주소로 자동으로 설정 

        //socket과 host_addr과 bind
        if(bind(socket_fd,(struct sockaddr *)&host_addr,sizeof(struct sockaddr)) < 0)
                err_exit("bind error ");

        //접속 대기
        if(listen(socket_fd,3) < 0)
                err_exit("listen error ");

        for(i = 0; i < FD_MAX; i++)
                events[i].data.fd = -1;


        //그냥 0보다 크면 된다.
        epoll_fd = epoll_create(1024); 
        if(epoll_fd < 0) err_exit("epoll_create error ");

        struct epoll_event event;
        event.data.fd = socket_fd;
        event.events = EPOLLIN;

        if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event) < 0)
                err_exit("epoll_ctl error ");

        while(1){

                ret = epoll_wait(epoll_fd, events, FD_MAX, -1);
                //-1은 이벤트가 발생할때까지 무한정 대기
                if(ret == -1) err_exit("epoll_wait error ");
                //ret는 이벤트가 발생한 entry의 갯수, events는 발생한 events의 배열이 저장된다.
                for(i = 0; i < ret; i++){
                        //accept할 것이 있는가?
                        if(events[i].data.fd == socket_fd && events[i].events & EPOLLIN){
                                size = sizeof(struct sockaddr_in);

                                //Client가 connect할때까지 기디린다. 
                                accepted_fd = 
                                        accept(socket_fd,(struct sockaddr *)&client_addr,&size);

                                if(accepted_fd  < 0)
                                        err_exit("accept error ");

                                struct epoll_event client;
                                client.data.fd = accepted_fd;
client.events = EPOLLIN;


                                if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accepted_fd, &client) < 0)
                                        err_exit("epoll_ctl error ");
                                //Client의 접속 정보를 출력하고 접속 잘됐다고 메시지 전송
                                printf("Client Info : IP %s, Port %d\n", 
                                                inet_ntoa(client_addr.sin_addr),
                                                ntohs(client_addr.sin_port));

                                n = send(accepted_fd, welcome_message, strlen(welcome_message), 0);
                                if(n < 0) err_exit("send error ");
                                continue;
                        } 

                        if(events[i].events & EPOLLIN){

                                n = recv(events[i].data.fd, &buffer[pos], BUF_SIZE, 0);

                                if(n < 0) err_exit("recv error ");

                                //n == 0일 경우에 epoll_fd 관심 리스트에서 비워준다. 
                                if(n == 0) {
                                        clear_fd(epoll_fd, events[i].data.fd);
                                        pos = 0;
                                        continue;
                                }

                                if(buffer[pos] == '\0'){
                                        printf("rcv msg : %s\n", buffer);
                                        //quit라는 메시지를 받으면 종료
                                        if(!strcmp(buffer,"quit")){
                                                clear_fd(epoll_fd, events[i].data.fd);
                                                pos = 0;
                                                continue;
                                        }

                                        n = send(events[i].data.fd, buffer, pos, 0);
                                        if(n < 0) err_exit("send error ");
                                        pos = 0;
                                } else 
                                        pos++;
                        }

                }

        }
        printf("end\n");
        shutdown(socket_fd, SHUT_RDWR);

        return 0;

}

 

client

//client.c
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <arpa/inet.h> 

#define BUF_SIZE 128

void err_exit(const char *err){
        perror(err);
        exit(1);
}

int main(int argc, char *argv[])
{
        int sockfd = 0, n = 0;
        uint16_t port;
        char buffer[BUF_SIZE] = {0,};
        struct sockaddr_in server_addr; 

        if(argc != 3){
                printf("\n Usage: %s server_ip port \n",argv[0]);
                return 1;
        } 

        port = atoi(argv[2]);

        //Internet용 socket을 연다.
        if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
                err_exit("socket error ");

        //server_addr의 구조체를 깔끔히 0으로 도배 
        memset(&server_addr, 0, sizeof(server_addr)); 
        server_addr.sin_family = AF_INET;
        server_addr.sin_port = htons(port); 
        //127.0.0.1은 본인의 IP이다. 
        inet_aton(argv[1], (struct in_addr*)&server_addr.sin_addr);

        //client는 서버의 ip, port, protocol 설정후 connect로 서버에게
        //바로 연결한다.
        if(connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0)
                err_exit("connect error ");


        while(1){
                //서버로부터 에코된 메시지를 받음
                n = recv(sockfd, buffer, BUF_SIZE, 0);

                if(n < 0) err_exit("recv error ");

                printf("%s\n", buffer);

                while(1){
                        printf("message:");
                        fgets(buffer, BUF_SIZE, stdin);
                        if(strlen(buffer) >= 100)
                                printf("message too large\n");
                        else break;

                };
                // \n을 \0로 처리 
                buffer[strlen(buffer)-1] = '\0';

                //서버에게 메시지 전송
                n = send(sockfd, buffer, strlen(buffer)+1, 0);
                if(n < 0) err_exit("send error ");

        }

        shutdown(sockfd, SHUT_RDWR);
        return 0;
}

 

Level Trigger 방식의 결과화면입니다.

Server
# ./server
Client Info : IP 127.0.0.1, Port 42872
rcv msg : hello epoll server
Client Info : IP 127.0.0.1, Port 46286
rcv msg : This is level trigger
rcv msg : good bye~~~~~
rcv msg : quit
rcv msg : GOOD BYE ^C
Client1 Client2
# ./client 127.0.0.1 12346
Welcome!
message:hello epoll server
hello epoll server
message: GOOD BYE
GOOD BYE
message:^C
# ./client 127.0.0.1 12346
Welcome!
message:This is level trigger
This is level trigger
message: good bye~~~~~
good bye~~~~~
message:quit
quit
message:
message:

 

예제 코드 - Edge Trigger Epoll

위 LT방식은 ET방식으로 바꿔볼까요? event에다가 EPOLLET를 추가하시면 ET방식으로 동작하게 됩니다. accepted_fd를 EPOLLET event를 같이 추가해서 실행해보세요.

struct epoll_event client;
client.data.fd = accepted_fd;
client.events = EPOLLIN | EPOLLET;

그 후 실행하게 되면 에코가 되지 않습니다.

Server
# ./server
Client Info : IP 127.0.0.1, Port 43596
Client Info : IP 127.0.0.1, Port 43600
Client1 Client2
# ./client 127.0.0.1 12346
Welcome!
message:Hello! epoll ET server
# ./client 127.0.0.1 12346
Welcome! message:Hell;;

 

왜 이런 결과가 나올까요?

우리가 구현한 방식은 한 글자씩 버퍼에서 읽어옵니다. 이는 epoll_wait이 반환된 후에 읽을 수가 있죠. LT방식일때는 버퍼에 데이터가 남아있으면 epoll_wait에 의해서 반환되게 됩니다. 하지만 ET방식은 버퍼가 채워지는 이벤트 순간 한번만 epoll_wait이 반환되기 때문에 버퍼에서 한글자만 읽고 대기하게 되는 겁니다.

그럼 여기서 질문, select와 poll은 어떤 방식을 쓰고 있는 걸까요?

우리가 현상을 보게 되면 버퍼가 남이있다면 select와 poll은 return됩니다. 이런 결과를 봐서 우리는 select와 poll은 LT방식인 것을 알 수 있습니다. 이러한 문제를 해결하기 위해서는 non-blocking 방식의 read를 해야합니다.즉, 더 이상 버퍼에 남아있는 데이터가 없어서 오류를 발생할때까지 읽어야합니다. 이때 남아있는 데이터가 없으면 EAGAIN 에러를 발생하게 됩니다. 그래서 이런 방식으로 수정해야합니다.

  • 비차단 모드로 설정 

 

//NON-BLOCKING 모드로 전환
int flags = fcntl(accepted_fd, F_GETFL);
flags |= O_NONBLOCK;
if(fcntl(accepted_fd, F_SETFL, flags) < 0) 
    err_exit("fcntl error ");
  • read에서 EAGAIN 오류를 만날때까지 읽기
while(1){
    n = recv(events[i].data.fd, &buffer[pos], BUF_SIZE, 0);

    if(n < 0) 
        if(errno == EAGAIN) break;
    if(n == 0) {
        clear_fd(epoll_fd, events[i].data.fd);
        pos = 0;
        break;
    }

    pos++;
}

 

전체 풀 소스코드는 아래와 같습니다. 

//epoll_server-edge.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <errno.h>
#include <fcntl.h>

#define PORT 12346
#define BUF_SIZE 1

const char *welcome_message = "Welcome!\n";

void err_exit(const char *err){
        perror(err);
        exit(1);
}

void clear_fd(const int epoll_fd, const int fd){
        epoll_ctl(epoll_fd, EPOLL_CTL_DEL, fd, NULL);
        close(fd);
}
int main(void){
        int socket_fd, accepted_fd;
        struct sockaddr_in host_addr, client_addr;
        socklen_t size;
        int epoll_fd, i, n, ret; 
        char buffer[BUF_SIZE] = {0,};
        struct epoll_event events[FD_MAX];
        int pos = 0;

        //STREAM socket 생성
        socket_fd=socket(PF_INET,SOCK_STREAM,0);
        if(socket_fd < 0) err_exit("socket error ");

        //server ip와 bind하기 위해 주소 설정
        memset(&host_addr, 0, sizeof(host_addr));
        host_addr.sin_family=AF_INET;
        host_addr.sin_port=htons(PORT); 
        host_addr.sin_addr.s_addr = 0;  //자신의 주소로 자동으로 설정 

        //socket과 host_addr과 bind
        if(bind(socket_fd,(struct sockaddr *)&host_addr,sizeof(struct sockaddr)) < 0)
                err_exit("bind error ");

        //접속 대기
        if(listen(socket_fd,3) < 0)
                err_exit("listen error ");

        for(i = 0; i < FD_MAX; i++)
                events[i].data.fd = -1;


        //그냥 0보다 크면 된다.
        epoll_fd = epoll_create(1024); 
        if(epoll_fd < 0) err_exit("epoll_create error ");

        struct epoll_event event;
        event.data.fd = socket_fd;
        event.events = EPOLLIN;
        //event.events = EPOLLIN | EPOLLET;

        if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &event) < 0)
                err_exit("epoll_ctl error ");

        while(1){

                ret = epoll_wait(epoll_fd, events, FD_MAX, -1);
                //-1은 이벤트가 발생할때까지 무한정 대기
                if(ret == -1) err_exit("epoll_wait error ");
                //ret는 이벤트가 발생한 entry의 갯수, events는 발생한 events의 배열이 저장된다.
                for(i = 0; i < ret; i++){
                        //accept할 것이 있는가?
                        if(events[i].data.fd == socket_fd && events[i].events & EPOLLIN){
                                size = sizeof(struct sockaddr_in);

                                //Client가 connect할때까지 기디린다. 
                                accepted_fd = 
                                        accept(socket_fd,(struct sockaddr *)&client_addr,&size);

                                if(accepted_fd  < 0)
                                        err_exit("accept error ");

                                struct epoll_event client;
                                client.data.fd = accepted_fd;
                                client.events = EPOLLIN | EPOLLET;

                                //NON-BLOCKING 모드로 전환
                                int flags = fcntl(accepted_fd, F_GETFL);
                                flags |= O_NONBLOCK;
                                if(fcntl(accepted_fd, F_SETFL, flags) < 0) 
                                        err_exit("fcntl error ");


                                if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, accepted_fd, &client) < 0)
                                        err_exit("epoll_ctl error ");
                                //Client의 접속 정보를 출력하고 접속 잘됐다고 메시지 전송
                                printf("Client Info : IP %s, Port %d\n", 
                                                inet_ntoa(client_addr.sin_addr),
                                                ntohs(client_addr.sin_port));

                                n = send(accepted_fd, welcome_message, strlen(welcome_message), 0);
                                if(n < 0) err_exit("send error ");
                                continue;
                        } 

                        if(events[i].events & EPOLLIN){
                                while(1){
                                        n = recv(events[i].data.fd, &buffer[pos], BUF_SIZE, 0);

                                        if(n < 0) 
                                                if(errno == EAGAIN) break;
                                        if(n == 0) {
                                                clear_fd(epoll_fd, events[i].data.fd);
                                                pos = 0;
                                                break;
                                        }

                                        pos++;
                                }

                                //quit라는 메시지를 받으면 종료
                                if(!strcmp(buffer,"quit") || n == 0){
                                        clear_fd(epoll_fd, events[i].data.fd);
                                        pos = 0;
                                        continue;
                                }
                                printf("rcv msg : %s\n", buffer);

                                n = send(events[i].data.fd, buffer, pos, 0);
                                if(n < 0) err_exit("send error ");
                                pos = 0;
                        }

                }

        }
        printf("end\n");
        shutdown(socket_fd, SHUT_RDWR);

        return 0;

}

Server
# ./server
Client Info : IP 127.0.0.1, Port 34820
Client Info : IP 127.0.0.1, Port 34832
rcv msg : hello! epoll ET server
rcv msg : HI HI ~~~~
rcv msg : BYE!!!
rcv msg : edge trigger~~~~
rcv msg : GOOD
Client Info : IP 127.0.0.1, Port 60190
rcv msg : I'm back
rcv msg : Bye
Client1 Client2
# ./client 127.0.0.1 12346
Welcome!
message:hello! epoll ET server
hello! epoll ET server
message:edge trigger~~~~
edge trigger~~~~
message:GOOD
GOOD
message:^C
# ./client 127.0.0.1 12346
Welcome!
message:I'm back
I'm back
message:Bye Bye
message:^C
# ./client 127.0.0.1 12346
Welcome!
message:HI HI ~~~~
HI HI ~~~~
message:BYE!!! BYE!!!
message:quit
quit
message:
message:

 

이상으로 epoll에 관한 설명을 마칩니다.

반응형
블로그 이미지

REAKWON

와나진짜

,

시큐어부트(Secure Boot)

시큐어부트(Secure Boot)는 말 그대로 안전한 부팅을 의미합니다. 시큐어 부팅은 ROM으로부터 시작되는 부팅의 시작부터 파일 시스템이 얹어지는 부팅 완료까지 안전하게 부팅을 하는 절차입니다. 주로 세 단계의 인증을 거치게 됩니다. 맨 처음 부팅의 시작인 부트로더들의 인증, 그리고 부트로더에 의해 커널 이미지가 올라오게 되면 커널 이미지를 인증, 이후 커널에 의해 파일 시스템이 마운트되기 전에 파일 시스템의 인증을 거치게 됩니다. 

결론은 부트 로더 인증 -> 커널 인증 -> 파일 시스템 인증 순으로 이어지게 된다는 것이죠. 

부트 로더 인증

임베디드 시스템에서 Boot Loader는 한 가지만 존재하지 않습니다. ROM에서 시작되는 부트 로더1부터 시작해 부트로더2, 부트로더 3 등이 있으며, 여기서도 부트로더 순서대로 인증하는 CoT(Chain Of Trust)라는 기술이 존재하지만 이 포스팅에는 부트로더와 다른 Firmware 이미지들을 한 꺼번에 인증하는 FIP 인증을 다룹니다.

FIP(Firmware Image Package)라고하는 펌웨어 이미지 바이너리를 인증합니다. FIP는 단순히 부트로더들의 모임이라고 생각하시면 되고, FIP를 서명한 서명값이 FIP끝에 달리게 됩니다. 이때 인증은 각 SoC 업체의 Firmware를 사용하여 인증을 하게 되는데요. NXP사의 s32g 칩의 경우에는 HSE(Hardware Security Engine)이라는 펌웨어가 그 역할을 하게 되지요. 

 

인증을 하는 경우에는 공개키가 있어야겠죠? 이 공개키는 어딘가에 저장이 되어야하는데 nxp s32g의 경우 FAT 파일 시스템을 통해서 특정 파티션에 저장하고 있습니다.

커널 인증 

부트로더 인증이 되었다면 그 이후 커널 인증을 거쳐야합니다. 커널 인증은 커널이 변조되었는지 아닌지를 판별하게 됩니다. 커널 인증을 하는 대표적인 방법은 Verified Boot라는 건데요. U-boot에서 사용하는 커널 인증 방법으로 FIT(Flattened Image Tree)를 이용합니다. 이 FIT는 아래와 같은 형식의 .its라는 파일을 가지고 생성되어 집니다. its는 FIT에 대한 정보를 갖고 있는 소스 파일입니다. 

/dts-v1/;
/ {
    description = "Verified boot FIT Image";
    #address-cells = <2>;
    images {
        kernel-1 {
            description = "FIT kernel Image";
            data = /incbin/("Image");
            type = "kernel";
            arch = "arm64";
            os = "linux";
            compression = "none";
            load =  <0x00080000>;
            entry = <0x00080000>;
            hash@1 {
                algo = "sha256";
            };
        };
        tee-1 {
            description = "Arm Trusted Firmware";
            data = /incbin/("optee.bin");
            type = "standalone";
            arch = "arm64";
            compression = "none";
            load =  <0x08400000>;
            entry = <0x08400000>;
            hash@1 {
                algo = "sha256";
            };
        };
        fdt-1 {
            description = "FIT device tree";
            data = /incbin/("fit-linux-kernel.dtb");
            type = "flat_dt";
            arch = "arm64";
            compression = "none";
            hash@1 {
                algo = "sha256";
            };
        };
    };
    configurations {
        default = "config-1";
        config-1 {
            description = "verified boot FIT configuration";
            kernel = "kernel-1";
            loadables = "tee-1";
            fdt = "fdt-1";
            signature-1 {
                algo = "sha256,rsa2048";
                key-name-hint = "dev";
                sign-images = "fdt", "kernel", "loadables";
            };
        };
    };
};

마지막 configurations에서 signature-1을 보시면 FIT 인증을 위한 정보들이 저장이 됩니다. 이 its를 가지고 mkimage라는 u-boot의 툴을 통해서 FIT를 아래와 같은 방법으로 만들어냅니다.

./u-boot/tools/mkimage -f fit-image.its -K u-boot.dtb -k keys -r image.fit
  • -f : its의 파일 이름
  • -K : 그렇다면 공개키는 어디 있을까요? 공개키는 앞서 인증한 FIP의 커널을 부팅하는 부터로더인 U-boot의 dtb에 존재합니다. 그 dtb 파일의 이름을 -K 옵션으로 정해주면 이 dtb 파일에 공개키가 삽입이 됩니다. 
  • -k : 서명할 키와 인증서가 있는 디렉토리를 정해줍니다. keys라는 디렉토리에 위치해있으며 openssl이든 다른 키 생성 툴이든 사용하여 키와 인증서를 만들어야합니다. 위의 예시에는 키에 대한 정보(key-name-hint)가 dev입니다.
  • -r : 인증이 필수라는 뜻으로 인증을 강제합니다. 이 옵션을 정해줌으로써 인증이 실패한 커널은 부팅하지 않습니다. 
  • image.fit : 최종적으로 나오는 fit 이미지의 이름을 정해줍니다. input이 아닙니다.

 

이 FIT 이미지는 U-boot의 bootm 부팅 커맨드로만 동작이 가능하며 u-boot에서 iminfo 라는 명령을 통해서 이미지의 정보를 확인할 수도 있습니다.

자세한 사용 방법은 아래를 참고하시면 될 것 같네요.

https://blog.crysys.hu/2018/06/verified-boot-on-the-raspberry-pi/

 

Verified boot on the Raspberry Pi – CrySyS Blog

This blog post, written by István Telek, is the third post in a series of blog posts on transforming the Raspberry Pi into a security enhanced IoT platform. It describes how you can implement a verified boot process on the Raspberry pi. Introduction Secur

blog.crysys.hu

 

파일 시스템 인증

자, 이제 마지막 인증 과정입니다. 바로 실제 데이터들이 존재하는 파일 시스템을 마운트하기전 파일 시스템을 인증하는 과정이 필요하게 되지요. 파일 시스템을 인증하는 대표적인 기술로 사용되는 것이 dm-verity라는 기술입니다. dm-verity는 파일 시스템 이미지의 원본 블록 을 1차로 나온 Hash 데이터를 다시 2차 Hash를 가하고, 이후 n 번의 Hash를 가하게 되면 결국 마지막 나오는 hash값을 갖고 파일 시스템의 무결성을 검증합니다. 하지만 인증을 위해서는 이 hash값을 서명해야됩니다. 결국 우리는 파일 시스템 원본 이미지 + 해쉬 블록을 미리 생성하고, root hash까지 미리 생성하여 안전하게 보관해야합니다. 이때 hash가 계산되는 모습을 보면 트리 형태로 보여지는데 이를 merkle Tree 라고합니다. 결과적으로 나온 root hash는 노출되어서는 안돼요. 부팅이 되고 마운트될때 파일 시스템의 이미지를 root hash로 만들어서 갖고 있는 root hash와 같다면 인증에 성공합니다.

https://www.timesys.com/security/dm-verity-without-an-initramfs/

한가지 중요한 것은 파일 시스템의 쓰기가 발생하면 안된다는 점입니다. 쓰기가 일어나면 인증은 다음 부팅때 당연히 실패합니다. 그래서 파일 시스템은 read-only 파일 시스템에서만 dm-verity가 사용이 가능합니다. 이 dm-verity에 관한 내용과 파일 시스템 인증에 관한 방법은 아래의 페이지에서 세세하게 다루고 있으니 이 페이지를 참고하시기 바랍니다.

https://www.timesys.com/security/dm-verity-without-an-initramfs/

 

DM-Verity Without an Initramfs - Timesys

Learn how you can implement file system verification on your embedded system without the use of an initramfs. This can significantly save boot time and storage requirements in many situations.

www.timesys.com

 

이상으로 시큐어부팅에 관한 포스팅을 마치도록 하겠습니다.

반응형

'컴퓨터 > 보안 기술' 카테고리의 다른 글

[사이버 보안] dm-verity 개념과 실습(Manual)  (1) 2023.05.18
블로그 이미지

REAKWON

와나진짜

,