webserv 허용함수 정리

정하나둘·2023년 8월 7일
0

webserv

목록 보기
4/4

webserve 과제에서 허용하고 있는 함수들은 다음과 같다.

execve, dup, dup2, pipe, strerror, gai_strerror,
errno, dup, dup2, fork, htons, htonl, ntohs, ntohl,
select, poll, epoll (epoll_create, epoll_ctl,
epoll_wait), kqueue (kqueue, kevent), socket,
accept, listen, send, recv, bind, connect,
getaddrinfo, freeaddrinfo, setsockopt, getsockname,
getprotobyname, fcntl, close, read, write, waitpid,
kill, signal, access, stat, opendir, readdir and
closedir.

아는 함수들도 있겠지만 복습한다는 개념으로 모두 다시 정리해보려 한다.

1. execve()함수

현재 수행되고 있는 프로세스를 대신하여 1번째 인자로 들어오는 새로운 프로세스를 수행시키는 함수 (자신은 종료됨)

  • 헤더: unistd.h
  • 형태: int execve(const char *pathname, char *const argv[], char *const envp[]);
  • 인수: const char *pathname 실행시킬 파일의 경로
    char *const argv[] 파일인자의 포인터
    char *const envp[] 환경변수의 포인터
  • 반환: 실패 시 -1, 현재 수행중인 프로세스 계속 진행

2. dup(), dup2()함수

File descriptor를 복제하는 함수.

  • 헤더: unistd.h
  • 형태: int dup(int fd);
    int dup2(int fd, int fd2);
  • 인수: (dup)int fd 복제할 file descriptor, dup이 돌려주는 파일 서술자는 가장 낮은 서술자를 반환합니다.
  • 반환: 실패 시 -1

3. pipe()함수

서로 독립된 프로세스들이 데이터를 주고 받게 해주는 함수
하나의 파이프 및 파이프에 대해 두 개의 파일 디스크립터 생성.
하나의 파이프를 프로세스들이 공유

  • 헤더: unistd.h
  • 형태: int pipe(int fd[2]);
  • 인수: int fd[2] 크기가 2인 int형 배열 요구
    fd[0]: 함수 호출 후 fd[0]에 데이터를 입력 받을 수 있는 파일 디스크립터가 담김(파이프 출구)
    fd[1]: 함수 호출 후 데이터를 출력할 수 있는 파일 디스크립터가 담김(파이프 입구)
  • 반환: 실패 시 -1

4. strerror()함수

오류 메세지 문자열을 가리키는 포인터(문자열)를 얻어온다

  • 헤더: string.h // c++ cstring
  • 형태: char *strerror(int errnum);
  • 인수: int errnum 에러 발생시 에러 넘버가 들어온다(아래에서 기술).
  • 반환: 실패 시 -1 성공 시 오류 번호에 해당하는 오류 문자열을 가리키는 포인터

5. errno

errno는 함수가 아니다. 광역변수로써 라이브러리 함수 수행 중 에러가 발생하면 에러 코드를 가지게 된다.

  • 헤더: errno.h
  • 반환: 에러없이 복귀되었다면 0, 수행 중 에러가 발생했다면 0 이외의 에러 값을 갖는다.

6. fork()함수

현재 실행되는 프로세스에 대해 독자적인 복사본 프로세스를 생성한다.

  • 헤더: unistd.h
  • 형태: pid_t fork(void);
  • 반환: 실패 시 -1, 자식 프로세스(새로 생성된 프로세스) 0, 부모프로세스에는 자식 프로세스의 pid가 반환된다.

7. hton+α(), ntoh+α()함수

  • 헤더: arpa/inet.h
    | 함수 구분 | 설명 |
    |:----------|:----------:|
    | uint32_t htonl(uint32_t hostlong) | long 형 호스트 바이트 순서 데이터를 네트워크 바이트 순서값 구함 |
    | uint16_t htons(uint16_t hostshort) | short 형 호스트 바이트 순서 데이터를 네트워크 바이트 순서값 구함 |
    | uint32_t ntohl(uint32_t netlong) | long 형 네트워크 바이트 순서 데이터를 호스트 바이트 순서 데이터 값 구함 |
    | uint16_t ntohs(uint16_t netshort) | short 형 네트워크 바이트 순서 데이터를 호스트 바이트 순서 데이터 값 구함 |

바이트 순서

바이트 순서는 해당하는 OS 시스템이 내부적으로 데이터를 표현하는 방법을 의미한다.
시스템이 내부적으로 데이터를 처리하는데 Big-Endian을 사용하느냐, Little-endian을 쓰느냐는 시스템의 CPU에 따라 달라진다.
이것을 호스트 바이트 순서라고 한다.

ex: 0x12345678의 32비트 데이터를 표현

Big Endian : 0x12 0x34 0x56 0x78

높은 주소 -> 낮은 주소

Little Endian : 0x78 0x56 0x34 012

낮은 주소 -> 높은 주소

네트워크는 Big Endian을 사용하고, 혹시나 Big Endian을 사용하는 CPU와 little Endian을 사용하는 CPU가 통신을 하게되면 문제가 발생할 수 있다.
따라서 통신할 때, 원활하게 사용하기 위해 변환해주는 함수가 hton+α 함수들이다.

8. select, poll, epoll, kqueue()함수

사전지식 :

멀티플렉싱, 멀티쓰레드, 멀티프로세스

하나의 서버에서 여러 개의 클라이언트 요청을 처리하기 위해 고안된 방법이다.

멀티 프로세스 :

여러개의 프로그램을 띄워놓고 각 프로그램에서 처리하게끔 하는 것.
프로그램을 짜다가 중간에 여러개의 요청을 처리해야 하는 로직을 마주하면 fork 함수를 수행하여 같은 내용을 복사된 별도의 프로세스(자식 프로세스)에서 처리하게 하는 것이다. 
주의해야 할 점은 여러 개의 프로세스가 동시에 돌고 있는 것이므로 어떤 것은 수행을 일찍 마칠 수도 있는데, 반드시 제대로 종료(할당받은 자원들 메모리 반납 등..)할 수 있도록 신경써야 한다는 것이다.(안 그러면 좀비 쓰레드가 된다) 
단점으로는 많은 양의 자원(메모리)를 소모하는 많은 양의 연산이 필요하다는 점이 있다.    

멀티 쓰레드 :

하나의 프로세스에서 여러 개의 쓰레드를 생성해 요청을 처리하는 방식.
멀티 프로세스는 기존의 프로세스가 완전히 새로운 별개의 프로세스로 복사되어 생성되기 때문에 자원 소모가 많다. 
하지만 멀티 쓰레드는 근본적으로 하나의 프로세스로 실행 흐름만 달라지고 함수 실행에 해당되는 스택영역은 독립적으로 가져가고 데이터와 힙 영역(메모리)를 공유한다.
따라서 프로세스 간 같은 데이터를 공유하기 어려웠던 멀티 프로세스와 달리 같은 메모리를 공유하므로 좀 더 편리할 수도 있으나 독이 될 수도 있다(동시에 같은 곳에서 접속하고 변경을 주려해 예상치 못한 문제가 발생 할 수 있다).
해당 문제를 방지하기 위해 뮤텍스나 세마포어 등을 이용한다(philosopher과제 참조)

멀티 플렉싱 :

하나의 통신채널(서버)을 통해 둘 이상의 데이터(클라이언트,시그널)를 전송하는데 사용되는 기술로, 
간단하게 두 개의 클라이언트를 위해 두 개의 프로세스, 두 개의 쓰레드를 두는 것이 아니라 하나의 하나의 프로세스 혹은 쓰레드에서 두 개 이상의 데이터의 입출력을 다룰 수 있는 기술이다.
커널에서는 하나의 쓰레드가 여러 개의 소켓(파일)을 핸들링 할 수 있는 select, poll, epoll, iocp(window), kqueue(Mac)을 제공한다.

서버의 리스팅 소켓을 bind, listen하고 클라이언트 쪽에서 connect가 들어오면 accept하여 클라이언트 소켓을 반환하고 그 소켓을 통해 클라이언트와 데이터 송수신
관리할 때 fd_set이라는 구조체를 사용

8-0. fd_set 구조체

  • fd를 관리하기 위해 디자인 된 구조체
  • 배열 형태로 0번 인덱스부터 fd 0을 매핑하고 있음

    fd_set 관련 함수
    -> FD_ZERO:인자로 전달된 fd_set의 모든 비트를 0으로 초기화
    -> FD_SET:인자로 전달된 fd_set의 인덱스를 1로 설정
    -> FD_CLR:인자로 전달된 fd_set의 인덱스를 0로 설정
    -> FD_ISSET:인자로 전달된 fd_set의 해당 인덱스가 1이면 양수를 반환

8-1. select()함수

어느 소켓의 fd에 read, write, exception이 발생했는지 확인하는 함수

  • 헤더: sys/time.h // sys/types.h // unistd.h
  • 형태: int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

fd_set을 전달하여 호출하면 변화가 발생한(입력 받은 데이터가 존대하거나 출력이 가능한 상황 등) 소켓의 디스크립터만 1로 설정

fd_set에 대한 주소값을 전달하고 각 액션에 대한 결과를 적용하기 때문에 원본을 복사하여 복사본을 전달해야 함.

위와 같은 예시에서 readset에 fd_set을 전달했을 때 호출 후 아래와 같이 변했다면 fd1, fd3에 읽어들일 데이터가 있다(입력 버퍼에 데이터가 있다)라고 볼 수 있다.

  • 인수: int n 관리하는 파일의 개수, 파일의 개수는 최대 파일 지정 번호 + 1
    fd_set *readfds 읽을 데이터가 있는지 검사하기 위한 파일 목록
    fd_set *writefds 쓰여진 데이터가 있는지 검사하기 위한 파일 목록
    fd_set *exceptfds 파일에 예외 사항들이 있는지 검사하기 위한 파일 목록
    struct timeval *timeout fd_set에 등록된 파일들에 데이터 변경이 있는지를 timeout동안 기다린다. 만약 timeout시간동안 변경이 없다면 0을 반환 한다. timeout을 NULL로 하면, 데이터가 있을 때까지 무한정 기다리고, 멤버 값이 모두 0이면 즉시 반환한다.
  • 반환: 실패 시 -1, 타임 아웃 시 0, 이벤트 발생 파일 디스크립터 수 > 0

select를 이용한 멀티플렉싱 서버의 구현
-1단계 : 서버 소켓과 fd_set 생성

int main(int argc, char *argv[])
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	struct timeval timeout;
	fd_set reads, cpy_reads;

	socklen_t	adr_sz;
	int fd_max, str_len, fd_num, i;
	...;

	serv_sock=socket(PF_INET, SOCK_STREAM, 0);
	...;

	if (bind(serv_sock, ( struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
	{
		printf("bind() error");
	}
	if (listen(serv_sock, 5) == -1)
	{
		printf("listen() error");
	}

	FD_ZERO(&reads);	//fd_set 초기화
	FD_SET(serv_sock, &reads);	//서버 소켓을 관리 대상으로 지정
	fd_max=serv_sock;	//최대 파일 디스크립터 값

-2단계 : select 함수 호출

	while(1)
	{
		cpy_reads = reads;	//원본 fd_set 복사
		timeout.tv_sec = 5;
		timeout.tv_usec = 5000;	//타임아웃 설정

		if ((fd_num = select(fd_max + 1, &cpy_reads, 0, 0, &timeout)) == -1)
			break;	//아직 서버 소켓만 있으므로 connect 연결 요청 시 서버소켓에 데이터가 들어오게 됨
		if (fd_num == 0)
			continue ;	//타임아웃 시 continue

-3단계 : 소켓에 따른 구분
->for문으로 fd_set의 인덱스르 하나씩 순회하면서 변화가 있는 인덱스를 찾아냄
->만약 그 fd가 서버 소켓이면 connect요청이므로 새로운 소켓을 생성하여 fd_set에 등록

		for (i = 0; i < fd_max + 1; i++)
		{
			if (FD_ISSET(i, &cpy_reads))
			{
				if (i == serv_sock)
				{
					adr_sz = sizeof(clnt_adr);
					clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);
					FD_SET(clnt_sock, &reads);
					if (fd_max < clnt_sock);
						fd_max = clnt_sock;
					printf("connected client: %d\n", clnt_sock);
				}	// 변화가 일어난 소켓이 서버 소켓이면 connect 요청인 경우
				else
				{
					str_len = read(i, buf, BUF_SIZE);
					if (str_len == 0)	//close request!
					{
						FD_CLR(i, &reads);
						close(i);
						printf("closed client: %d\n", i);
					}
					else
						write(i, buf, str_len);	//echo!
				} 	// 다른 소켓인 경우에는 데이터 read
			}
		}
	}
	return (0);
}

단점 : 이벤트별로 감시할 파일들을 fd_set이라는 fd비트 배열에 등록하고 등록된 fd에 어떠한 이벤트가 발생했을 경우 fd_set을 확인하는 방식으로 동작하므로 한 번에 다수의 fd를 조회하여 I/O 상태를 관찰하기 때문에 지속적인 polling이 필요하다(CPU 낭비).
또한 한 번에 조회할 수 있는 fd의 수가 1024개로 제한되어있으며
커널에 의해 완성되는 기능이 아니라 함수에 의해 완성되는 기능이므로 select 함수의 호출을 통해 전달된 정보는 커널에 등록되지 않은 것이며 함수를 호출할 때마다 매번 관련 정보를 전달해야 한다.
최대 1024개의 file descriptor를 하나하나 체크하기 때문에 O(n)의 계산량을 가지고 있으며 당연하게도 file descriptor 수가 증가하면 성능이 떨어진다.

8-2 poll()함수

  • 헤더: #include <poll.h>
  • 형태: int poll(struct poolfd *ufds, unsigned int nfds, int timeout);
    select와 비슷한 함수로 여러 file descriptor에 대해서 I/O를 수행하기 위한 준비가 될 때까지 기다리는 함수이다.

select의 경우 입출력 이벤트가 발생했을 때 넘겨주는 정보가 너무 적어서 poll함수는 어떤 이벤트를 기다리는지 설정할 수 있고 해당 이벤트가 발생하면 revents를 채워서 돌려준다.

우선 poll 함수의 첫 번째 인자인 pollfd구조체에 대해 알아보자. 중요

struct pollfd
{
	int fd;         // 관심있어하는 파일지시자
	short events;   // 발생된 이벤트
	short revents;  // 돌려받은 이벤트
};

해당 구조체에는 3가지 멤버변수가 있는데, 이 구조체에 우리가 관심있어하는 파일 지시자를 세팅하고(fd), 관심있어하는 파일 지시자가 어떤 이벤트가 발생하는걸 기다릴 것인지(events)를 지정하게 된다. 그럼 poll은 해당 fd에 해당 events가 발생하는지 검사하고 해당 events가 발생하면 revents를 채워서 돌려주게 된다.

revents는 events가 발생했을때 커널에서 이 events에 어떻게 반응했는지에 대한 반응값이다. 후에 revents 값을 조사함으로써, 해당 fd에 어떤 event가 일어났고 그 event를 어떻게 처리했는지(입/출력이 제대로 이뤄졌는지, 아님 에러가 발생했는지)를 알아내서 적절한 조치를 취할 수 있게 된다. 커널에서 설정해주는 값이다.

short events에 세팅할 수 있는 내용들에 대해 알아보자
이 이벤트들은 OR(|)로 여러가지를 줄 수 있으며 사용자가 설정한다.
아래의 표를 보면 revents에는 모든 event가 설정되어질 수 있지만, events에는 아래 3개는 설정할 수 없다.

2번째 인자인 nfds는 pollfd의 배열의 크기이다.

3번째 인자인 timeout은 select의 time과 같은 역할을 한다.
-1일 경우, 이벤트가 발생하기 전까지 영원히 기다린다.
0일 경우, 기다리지 않고 곧바로 다음 루틴을 진행한다.
0보다 큰 양의 정수일 경우, 해당 시간만큼 기다리게 된다. 해당 시간내에 어떤 이벤트가 발생하면 즉시 되돌려주며, 시간을 초과하게 될 경우 0을 return한다.

  • 반환: 에러 시 -1, 성공 시 revent가 발생한 pollfd 구조체의 숫자를 돌려준다.

poll 함수를 이용한 멀티플랙싱 echo 서버 프로그램

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <poll.h>

#define PORT 20162
#define BUFFER_SIZE 100
#define LISTEN_QUEUE_SIZE 5
#define theNumberOfFDs 100

int main(int argc, char* argv[])
{
    int listenFD, connectFD;
    struct sockaddr_in listenSocket, connectSocket;

    socklen_t addrSz; // address size를 구할 변수

    int i;
    ssize_t strLen;
    char buf[BUFFER_SIZE];

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

    listenFD = socket(PF_INET, SOCK_STREAM, 0);
    memset(&listenSocket, 0, sizeof(listenSocket));

    listenSocket.sin_family = AF_INET;
    listenSocket.sin_addr.s_addr = htonl(INADDR_ANY);
    listenSocket.sin_port = htons(PORT);


    if (bind(listenFD, (struct sockaddr *) &listenSocket, sizeof(listenSocket)) == -1) {
        printf("Can not bind.\n");
        return -1;
    }

    if (listen(listenFD, LISTEN_QUEUE_SIZE) == -1) {
        printf("Listen fail.\n");
        return -1;
    }

    // pollfd 배열 구조체 생성
    struct pollfd pollFDs[theNumberOfFDs];

    pollFDs[0].fd = listenFD; // 0번째 배열에는 listen을 지정
    pollFDs[0].events = POLLIN; // 읽도록 만든다.
    pollFDs[0].revents = 0; // 처음에는 0으로 초기화 한다(아직 아무 일도 일어나지 않았으니)

    for (i = 1; i < theNumberOfFDs; i++)
        pollFDs[i].fd = -1; // 0번째 배열은 listen을 위한것이니 1번째부터 모두 -1로 초기화

    while (1)
    {
        int result = poll(pollFDs, theNumberOfFDs, -1); // -1 :: 무한 대기

        if (result > 0)
        {
            if (pollFDs[0].revents == POLLIN)
            {
                // 새로운 커넥션 요청이 들어왔을 때
                connectFD = accept(listenFD, (struct sockaddr*)&connectSocket, &addrSz);

                for (i = 1; i < theNumberOfFDs; i++)
                {
                    if (pollFDs[i].fd == -1) // 비어있는 fd슬롯을 찾아서 넣어준다.
                    {
                        pollFDs[i].fd = connectFD;
                        pollFDs[i].events = POLLIN;
                        pollFDs[i].revents = 0;
                        break; // 모두 다 넣고 break를 통해 한번만 실행
                    }
                }
            }

            for (i = 1; i < theNumberOfFDs; i++)
            {
                switch (pollFDs[i].revents)
                {
                    // no events
                    case 0:
                        break;

                    // data is ready
                    case POLLIN:
                        strLen = read(pollFDs[i].fd, buf, BUFFER_SIZE);
                        printf("%lu bytes read\n", strLen);

                        buf[strLen] = '\0';
                        fputs(buf, stdout);
                        fflush(stdout);

                        write(pollFDs[i].fd, buf, strlen(buf));


                    // 슬롯 초기화
                    // default:
                        // close(pollFDs[i].fd);
                        // pollFDs[i].fd = -1;
                        // pollFDs[i].revents = 0;


                }
            }
        }


    }

    close(listenFD);

    return 0;
}

echo 클라이언트 프로그램

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/select.h>
#include <poll.h>

#define PORT 20162
#define BUFFER_SIZE 100
#define LISTEN_QUEUE_SIZE 5
#define theNumberOfFDs 100

int main(int argc, char** argv)
{
    if (argc != 2)
    {
        printf("Usage: %s IPv4-address\n", argv[0]);
        return -1;
    }

    struct sockaddr_in connectSocket;

    memset(&connectSocket, 0, sizeof(connectSocket));

    connectSocket.sin_family = AF_INET;
    inet_aton(argv[1], (struct in_addr*) &connectSocket.sin_addr.s_addr);
    connectSocket.sin_port = htons(PORT);

    int connectFD = socket(AF_INET, SOCK_STREAM, 0);

    if (connect(connectFD, (struct sockaddr*) &connectSocket, sizeof(connectSocket)) == -1)
    {
        printf("Can not connect.\n");
        return -1;
    }

    else
    {
        int readBytes, writtenBytes;
        char sendBuffer[BUFFER_SIZE];
        char receiveBuffer[BUFFER_SIZE];

    while(1)
    {
            //서버에 문자열을 보낸 뒤 서버가 보낸 echo를 받아 출력.
            printf("input :: ");

            fgets(sendBuffer,BUFFER_SIZE,stdin);

            write(connectFD, sendBuffer, strlen(sendBuffer));


            readBytes = read(connectFD, receiveBuffer, BUFFER_SIZE);
            printf("%d bytes read\n", readBytes);
            receiveBuffer[readBytes] = '\0';

            fputs(receiveBuffer, stdout);
            fflush(stdout);
     }



    }

    close(connectFD);
    return 0;


}

poll 함수 특징
우선 file descriptor가 무제한적이고 select에 비해 해당 fd에 대해 보다 많은 정보를 되돌려줌으로, 보통 select보다 선호된다.

8-3 epoll API(epoll_create, epoll_ctl, epoll_wait)함수

epoll API는 select와 poll함수와 비교해서 fd의 준비 상태를 알리는 방식에서 더 유연성을 갖는다.(또한 리눅스에서만 호환성을 갖는다. 윈도우는 IOCP)
앞의 select 함수와 poll 함수는 모든 fd에 대해 이벤트 발생을 찾아야해 횟수가 큰 반복문이 필요했으며 함수 호출 시 매번 전달해야하는 관찰 대상 정보들 때문에 상대적으로 속도가 느렸다.

epoll API의 장점은 우선 상태변화의 확인을 위한, 전체 파일 디스크립터를 대상으로 하는 반복문이 필요가 없다는 점이고 select 함수에 대응하는 epoll_wait 함수 호출 시, 관찰 대상의 정보를 매번 전달할 필요가 없다는 점이다.

epoll 기반 프로그램 구현에 필요한 함수와 구조체들을 정리해보자

epoll_create()함수

epoll 파일 디스크립터 저장소(epoll 인스턴스) 생성

  • 헤더: sys/epoll.h
  • 형태: int epoll_create(int size);
  • 인수: int size epoll 감시할 fd의 수
  • 반환: 성공 시 epoll의 다른 함수에서 사용할 새로운 fd 반환, 실패 시 -1 반환
    소멸 시 close 함수 사용함.

epoll_ctl()함수

저장소(epoll 인스턴스)에 fd 등록 및 삭제

  • 헤더: sys/epoll.h
  • 형태: int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  • 인수: int epfd : 관찰대상을 등록할 epoll인스턴스의 fd
    int op : 관찰대상의 추가, 삭제 또는 변경여부 지정
    int fd : 등록할 관찰대상의 fd
    struct epoll_event *ev : 관찰대상의 관찰 이벤트 유형
  • 반환: 성공 시 epoll의 다른 함수에서 사용할 새로운 fd 반환, 실패 시 -1 반환
    소멸 시 close 함수 사용함.

2번째 인자인 op에 전달 가능한 상수와 그 의미

1) EPOLL_CTL_ADD : 관심있는 fd를 epoll 인스턴스에 등록
2) EPOLL_CTL_MOD : 기존 fd의 이벤트 발생상황을 변경
3) EPOLL_CTL_DEL : 기존 fd를 epoll 인스턴스 관심 목록에서 삭제 이때, 4번째 인자인 event에 NULL 전달.

epoll_event 구조체

이벤트가 발생한 fd를 묶는 용도로 사용되나, fd를 epoll 인스턴스에 등록할 때, 이벤트 유형을 등록하는 용도로도 사용된다.

struct epoll_event{

uint32_t    events;	/* epoll 이벤트 (비트 마스트) */	//수신할 데이터가 존재하는 이벤트 발생 시

epoll_data_t    data;	/* 사용자 데이터 */			//epoll 인스턴스에 sockfd 파일 디스크립터 등록을 위함임
};

epoll_event 구조체 멤버인 events에 저장 가능한 상수와 이벤트 유형

물론 해당 상수들 또한 OR(|)연산자를 이용해서 둘 이상을 함께 등록할 수 있다.

event 구조체 속 epoll_data_t 구조체

typedef union epoll_data {

void    *ptr;	/* 사용자 정의 데이터 포인터 */

int    fd;	/* 파일 디스크립터 */

uint32_t    u32;	/* 32비트 정수 */

uint64_t    u64;	 /* 64비트 정수 */

} epoll_data_t;

epoll_ctl 예시:

  • epoll_ctl(A, EPOLL_CTL_ADD, B, C); : epoll 인스턴스 A에 파일 디스크립터 B를 등록한다. 이 때 C 이벤트 관찰을 목적으로 한다.
  • epoll_ctl(A, EPOLL_CTL_DEL, B, NULL); : epoll 인스턴스 A에서 파일 디스크립터 B 삭제

epoll_wait() 함수

select 함수와 마찬가지로 fd의 변화를 대기하는 함수이다.
구조체 epoll_event 기반의 배열을 넉넉히 선언 후, epoll_wait 함수 호출 시 인자로 전달하면, 상태변화(이벤트)가 발생한 파일 디스크립터의 정보가 이 배열에 별도로 묶인다.-> select 함수 사용시처럼 전체 관심대상 파일 디스크립터를 대상으로한 반복문이 필요없다.

  • 헤더: sys/epoll.h
  • 형태: int epoll_wait(int epfd, struct epoll_event *events, int maxevents, timeout);
  • 인수: int epfd : 이벤트 발생의 관찰 영역인 epoll 인스턴스의 파일 디스크립터
    struct. epoll_event *events : 이벤트가 발생한 fd가 채워질 버퍼의 주소값
    int maxevents : 두 번째 인자로 전달된 주소값의 버퍼에 등록 가능한 최대 이벤트 수 - 버퍼를 동적할당 해야함.
    int timeout : 1/1000초 단위의 대기시간, -1 전달 시 이벤트 발생까지 무한대기
  • 반환: 실패 시 -1 반환, 성공 시 이벤트가 발생한 fd 수

epoll_wait 예시:

int event_cnt;
struct epoll_event *ep_events;
...
ep_events=malloc(sizeof(struct epoll_event) * EPOLL_SIZE);	//EPOLL_SIZE는 매크로 상수값
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
...

epoll 함수를 이용한 멀티플렉싱 echo 서버 프로그램

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

#define BUFFER_SIZE 100
#define EPOLL_SIZE 50
void	error_handling(char *message)
{
	fputs(message, stderr);
	fputc('\n', stderr);
	exit(EXIT_FAILURE);
}

int main(int argc, char** argv)
{
	int serv_sock, clnt_sock;
	struct sockaddr_in serv_adr, clnt_adr;
	socklen_t adr_sz;
	int	str_len, i;
	char	buf[BUF_SIZE];

	struct epoll_event *ep_events;
	struct epoll_event event;
	int epfd, event_cnt;

	if (argc != 2);	//실행파일 경로/PORT번호를 입력으로 받아야 함
	{
		printf("Usage : %s <port> \n", argv[0]);
		exit(EXIT_FAILURE);
	}

	/*서버 주소정보 초기화 */
	serv_sock = socket(PF_INET, SOCK_STREAM, 0);	//TCP 소켓 생성
    memset(&serv_sock, 0, sizeof(serv_sock));
    serv_soc.sin_family = AF_INET;
    serv_soc.sin_addr.s_addr = htonl(INADDR_ANY);
    serv_soc.sin_port = htons(PORT);

	/*서버 주소정보를 토대로 주소 할당*/
	if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
		error_handling("bind() error");

	/*클라이언트로부터 연결요청을 수락할 준비 완료(진짜 서버소켓이 됌)*/
	if (listen(serv_sock, 5) == -1)
		error_handling("listen() error");

	epfd=epoll_creat(EPOLL_SIZE);	//epoll 인스턴스 생성(관심 대상 파일 디스크립터 저장소)
	ep_events = malloc(sizeof (struct epoll_event) * EPOLL_SIZE);
	/*epoll 인스턴스에 있는 fd 중 실제로 이벤트가 발생한 fd를 따로 모아놓는 동적배열
	최대 EPOLL_SIZE 만큼 이벤트가 발생할 수 있음*/

	event.events=EPOLLIN;	//수신한 데이터가 있는 이벤트
	event.data.fd = serv_sock;	//서버소켓이 대상
	epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event);	//epoll 인스턴스에 이벤트 등록

	while(1)
	{
		event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
		/*epoll 인스턴스에 있는 관심대상에서 이벤트가 발생할 때까지 무한 대기*/
		if (event_cnt == -1)
		{
			puts("epoll_wait() error");
			break;
		}
		for (i = 0; i < event_cnt; ++i)	//이벤트 발생한 파일 디스크립터에 대해서만 반복문(select와 가장 큰 차이)
		{
			if (ep_events[i].data.fd == serv_sock)
			/*클라이언트의 연결요청도 데이터 전송을 통해 이루어지므로 서버소켓에 수신된 데이터가 존재한다는 것은 클라이언트의 연결요청이 있었다는 의미*/
			{
				adr_sz = sizeof(clnt_adr);
				clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz);	//클라이언트의 연결요청을 수락

				/*클라이언트와의 송수신을 위해 새로 생성된 소켓에 대해 이벤트 등록*/
				event.events=EPOLLIN;
				event.data.fd = clnt_sock;
				epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);

				printf("connected client: %d \n", clnt_sock);
			}
			else	//클라이언트의 메세지를 실제로 수신하는 소켓에 대해 (accept 함수호출로 생성된 소켓)
			{
				str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
				if (str_len == 0)
				{
					epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL);
					//클라이언트가 종료했으므로 이 소켓 또한 관심대상에서 제외

					close(ep_events[i].data.fd);	//이 소켓의 연결도 종료
					printf("close client: %d \n", ep_events[i].data.fd);
				}
				else
					write(ep_events[i].data.fd, buf. str_len);	//수신한 문자열을 다시 클라이언트로 에코
			}
		}
	}
	close(serv_sock);	//서버소켓 소멸
	close(epfd);	//epoll 인스턴스 소멸
	return (0);
}

8-4 kqueue() API함수

kqueue는 FreeBSD환경에서 주로 사용된다(epoll은 linux).
커널에 event를 저장할 queue를 생성하고 I/O event가 queue에 쌓이면(event가 발생하면) 해당부분만 처리하는 방식이기 때문에 select, poll처럼 event가 발생한 FD를 찾기위해 전체를 탐색할 필요가 없다. epoll과 환경만 다르고 같다.

kqueue() 함수

  • 헤더: sys/event.h
  • 형태: int kqueue(void);
  • 반환: 커널에 새로운 event queue를 만들고, fd를 return한다.
    return된 fd는 아래 kevent()함수에서 이벤트를 등록, 모니터링 하는데 사용된다.
    이 queue는 fork(2)로 자식 프로세스 분기 시 상속되지 않는다. error 발생시 -1 반환

kevent()함수

  • 헤더: sys/event.h

  • 형태: int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);

  • 인수: int kq : kqueue()로부터 반환받은 event queue의 fd.
    const struct kevent *changelist : <sys/event.h>에 정의된 kevent 구조체 배열의 인자에 대한 pointer. changelist 배열에 저장된 kevent 구조체(이벤트)들은 kqueue에 등록된다.
    int nchanges : 등록할 이벤트의 갯수
    struct kevent *eventlist : 발생한 event가 return 될 배열
    int nevents : eventlist 배열의 크기
    const struct timespec *timeout : timespec구조체 포인터 전달, NULL일 경우 이벤트 발생까지 무한대기한다.

  • 반환: kevent()는 이 배열에서 생성한 kevent를 최대 nevents만큼 정리하여 담아주고 그 갯수를 return한다.

kqueue() 함수의 모든 작업은 kevent라는 구조체에 의해서 이루어진다.
kevent는 ident, filter라는 인자를 하나의 key로 삼아 식별되며, kqueue 내에는 독립적인 kevent만 존재하게 된다. 식별자인 ident 외 filter까지 하나의 key로 보는 이유는 filter가 기존 저장된 event가 이미 존재하는지 판단하기 때문이다.
filter는 kevent의 초기 등록시 실행되며, I/O event가 발생할 때마다 filter가 확인을 하게되고, 신규 event로 판단하면 해당 kevent는 kqueue에 배치된다. 사용자가 kqueue에서 kevent를 검색하려고 할 때도 실행되는데 만약 event 발생 조건에 부합되지 않는다면 해당 kevent는 kqueue에서 제거되고 return되지 않는다. 이렇게 선분류를 해주는 filter덕분에 kqueue에는 최소한의 kevent가 배치될 수 있다.

struct kevent 구조체

#include <sys/event.h>

struct kevent {
    uintptr_t ident;        /* identifier for this event */
    int16_t   filter;       /* filter for event */
    uint16_t  flags;        /* action flags for kqueue */
    uint32_t  fflags;       /* filter flag value */
    intptr_t  data;         /* filter data value */
    void      *udata;       /* opaque user data identifier */
};
  • ident : event 식별자, fd번호가
  • filter : event 선처리할 때 사용되는 filter
    -
    • EVFILT_READ : FD를 ident로 지정(모니터링) -> 읽을 data가 있을 때마다 event return. fd의 종류에 따라 조금씩 다른 동작을 한다.(socket, vnodes, fifo, pipe 등)
    • EVFILT_WRITE : FD를 ident로 지정(모니터링) -> 쓸 data가 있을 때마다 event return.
    • EVFILT_EMPTY : FD를 ident로 지정(모니터링) -> 쓸 data가 없을 때마다 event return.
    • EVFILT_VNODE : FD를 fflags에서 지정한 event를 ident로 지정 -> event 발생 시 반환
    • EVFILT_PROC : 감시할 pid 또는 fflags에서 지정한 event를 ident로 지정 -> event 발생 시 반환
    • EVFILT_SIGNAL : signal number를 ident로 지정 -> signal 발생 시 반환
    • EVFILT_TIMER : 임의의 timer를 ident로 지정 -> 주기마다 반환
  • flags : event에 수행할 작업
    -
    • EV_ADD : kqueue에 이벤트를 추가
      있는 event를 또 추가하면 인자가 update(덮어쓰기)되어 중복 방지
      추가된 event는 EV_DISABLE 플래그로 재정의되지 않는 한 자동으로 활성화
    • EV_ENABLE : kevent() 호출 시 event 반환을 허용
    • EV_DISABLE : 이벤트를 비활성화 하여 kevent()가 반환하지 않도록 함. 필터 자체는 비활성화 되지 않음
    • EV_DISPATCH : 이벤트 전달 직후 EV_DISABLE 설정
    • EV_DELETE : kqueue에서 이벤트를 제거
    • EV_RECEIPT : kequeue 대량 변경 시 유용 (보류중인 event는 배제)
    • EV_ONESHOT : event 감지로 인한 첫 번째 filter 실행만 반환
    • EV_CLEAR : 사용자가 event 검색 후 상태 재설정
      재 상태 대신 상태 변화를 보고하는 필터에 유용
    • EV_ERROR : 각종 error
  • fflags : filter별 flag
  • data : filter별 data 값. filter가 EVFILT_READ인 경우 data에는 read가 가능한 바이트 수가 기록된다.
  • udata : event와 함께 등록하여 event return시 사용할 수 있는 user-data이다. udata 또한 event의 식별자로 사용될 수 있다(optional - kevent64() 및 kevent_qos()는 인자 flags로 udata를 식별자로 사용할지 말지 결정할 수 있다).

kevent 구조체를 쉽게 초기화하기 위한 EV_SET() 함수도 제공된다.

EV_SET(&kev, ident, filter, flags, fflags, data, udata); // kevent 구조체의 주소를 전달

kqueue를 활용한 멀티플렉싱 서버 구현

#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>

#include <iostream>
#include <map>
#include <vector>

using namespace std;

void exit_with_perror(const string& msg)
{
    cerr << msg << endl;
    exit(EXIT_FAILURE);
}

void change_events(vector<struct kevent>& change_list, uintptr_t ident, int16_t filter,
        uint16_t flags, uint32_t fflags, intptr_t data, void *udata)
{
    struct kevent temp_event;

    EV_SET(&temp_event, ident, filter, flags, fflags, data, udata);
    change_list.push_back(temp_event);
}

void disconnect_client(int client_fd, map<int, string>& clients)
{
    cout << "client disconnected: " << client_fd << endl;
    close(client_fd);
    clients.erase(client_fd);
}

int main()
{
    /* 서버 소켓 초기화와 listen 함수로 대기 */
    int server_socket;
    struct sockaddr_in server_addr;

    if ((server_socket = socket(PF_INET, SOCK_STREAM, 0)) == -1)
        exit_with_perror("socket() error\n" + string(strerror(errno)));

    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(8080);
    if (bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)
        exit_with_perror("bind() error\n" + string(strerror(errno)));

    if (listen(server_socket, 5) == -1)
        exit_with_perror("listen() error\n" + string(strerror(errno)));
    fcntl(server_socket, F_SETFL, O_NONBLOCK);
    
 	/* kqueue 초기화 */
    int kq;
    if ((kq = kqueue()) == -1)
        exit_with_perror("kqueue() error\n" + string(strerror(errno)));


    map<int, string> clients; // map for client socket:data
    vector<struct kevent> change_list; // kevent vector for changelist
    struct kevent event_list[8]; // kevent array for eventlist

    /* 서버소켓에 탐지할 이벤트 추가 */
    change_events(change_list, server_socket, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL);
    cout << "echo server started" << endl;

    /* 이벤트 감지 후 처리할 반복문 */
    int new_events;
    struct kevent* curr_event;
    while (1)
    {
        /* 새로운 이벤트 감지 및 갯수 반환 */
        new_events = kevent(kq, &change_list[0], change_list.size(), event_list, 8, NULL);
        if (new_events == -1)
            exit_with_perror("kevent() error\n" + string(strerror(errno)));

        change_list.clear(); // clear change_list for new changes

        for (int i = 0; i < new_events; ++i)
        {
            curr_event = &event_list[i];

            /* 에러 처리 */
            if (curr_event->flags & EV_ERROR)
            {
                if (curr_event->ident == server_socket)
                    exit_with_perror("server socket error");
                else
                {
                    cerr << "client socket error" << endl;
                    disconnect_client(curr_event->ident, clients);
                }
            }
            else if (curr_event->filter == EVFILT_READ)
            {
                if (curr_event->ident == server_socket)
                {
                    /* 새 클라이언트 accept */
                    int client_socket;
                    if ((client_socket = accept(server_socket, NULL, NULL)) == -1)
                        exit_with_perror("accept() error\n" + string(strerror(errno)));
                    cout << "accept new client: " << client_socket << endl;
                    fcntl(client_socket, F_SETFL, O_NONBLOCK);

                    /* 클라이언트 소켓 감지 이벤트 추가 - add read, write */
                    change_events(change_list, client_socket, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, NULL);
                    change_events(change_list, client_socket, EVFILT_WRITE, EV_ADD | EV_ENABLE, 0, 0, NULL);
                    clients[client_socket] = "";
                }
                else if (clients.find(curr_event->ident)!= clients.end())
                {
                    /* 클라이언트로부터 data read */
                    char buf[1024];
                    int n = read(curr_event->ident, buf, sizeof(buf));

                    if (n <= 0)
                    {
                        if (n < 0)
                            cerr << "client read error!" << endl;
                        disconnect_client(curr_event->ident, clients);
                    }
                    else
                    {
                        buf[n] = '\0';
                        clients[curr_event->ident] += buf;
                        cout << "received data from " << curr_event->ident << ": " << clients[curr_event->ident] << endl;
                    }
                }
            }
            else if (curr_event->filter == EVFILT_WRITE)
            {
                /* data를 클라이언트에게 send */
                map<int, string>::iterator it = clients.find(curr_event->ident);
                if (it != clients.end())
                {
                    if (clients[curr_event->ident] != "")
                    {
                        int n;
                        if ((n = write(curr_event->ident, clients[curr_event->ident].c_str(),
                                        clients[curr_event->ident].size()) == -1))
                        {
                            cerr << "client write error!" << endl;
                            disconnect_client(curr_event->ident, clients);  
                        }
                        else
                            clients[curr_event->ident].clear();
                    }
                }
            }
        }

    }
    return (0);
}

vector를 통해 탐지할 이벤트를 관리, return되는 event는 크기 8의 event_list로 받음. (만약 반환할 이벤트가 설정 배열 길이인 8보다 많다면, nevents만큼만 배열에 담고 나머지는 다음 kevent 호출 때 반환.)
(nevents 크기는 최대 60000개까지 가능)

client 연결 해제 시에는 DELETE 이벤트를 등록하지 않고 close()만 하도록 함. close()시 닫힌 fd를 참조하는 모든 이벤트는 비활성화되고 삭제되기 때문이다.

kqueue는 select나 poll에 비해 이벤트 처리에서 효율적인데, 그 이유는 다음과 같다.

  • 이벤트 발생 시, 해당 이벤트에 접근하는 시간복잡도가 O(1)이다.
    select와 poll의 경우 이벤트 발생 시 해당 이벤트에 접근하는 시간복잡도가 O(N)이나, kqueue는 발생한 이벤트를 정리하여 return해주기 때문에 O(1)로 접근 가능하다.
  • 등록된 이벤트를 따로 관리할 필요가 없다.
    select는 fd_set, poll은 poll_fd 구조체의 배열로 모니터링할 이벤트들을 사용자가 관리하고, 이를 select()나 poll()에 전달해야 하지만, kqueue의 경우 새로 등록할 이벤트, 발생한 이벤트만 관리해주면 된다. 모니터링되는 이벤트는 kqueue, 즉 커널에서 관리된다.

9. getaddrinfo

getaddrinfo는 domain address를 받아서 네트워크 주소 정보(IP address)를 가져오는 함수이다.

예를들면, http://www.google.co.kr라는 domain address가 있는데, 이 주소는 사람이 알아보기 쉬운 주고이긴 하지만, 컴퓨터는 이 주소를 가지고 해당되는 구글의 서버를 찾아가지 못한다. 그래서 이 domain address와 대응되는 IP주소가 무엇인지를 알아 낸 뒤에 그 IP주소로 연결을 해야한다.
즉, Domain address -> IP address 변환을 하고 싶을 때 사용하는 함수라는 뜻이다.
이걸 전문용어로 DNS (Domain Name System/Service) resolving 이라고 한다.

getaddrinfo 함수는 총 4개의 매개변수를 가진다.
그 중, 1~3번째는 입력 매개변수이고, 4번째 매개변수는 결과를 사용자에게 돌려주는 출력 매개변수이다.
결과는 addrinfo 구조체 (strcut addrinfo) 의 linked list로 돌려준다.

이 결과는 사용을 끝낸 뒤엔 freeaddrinfo 함수로 메모리 해제를 해주어야 한다. 그렇지 않으면 메모리 누수가 발생한다.

헤더

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>
  • 형태 : int getaddrinfo(const char *hostname, const char *service, const struct addrinfo *hints, struct addrinfo **result);
  • 인수 : const char *hostname : 호스트 이름 혹은 주소 문자열(주소 문자열은 => IPv4의 점으로 구분하는 10진 주소 문자열이거나 IPv6의 16진 문자열)
    const char *service : 서비스 이름 혹은 10진수로 표현한 포트 번호 문자열
    const struct addrinfo *hints : getaddrinfo 함수에게 말그대로 힌트를 준다. 희망하는 유형을 알려주는 힌트를 제공한다. addrinfo 구조체에 hint로 줄 정보를 채운 뒤, 그것의 주소값을 넘기면 된다. 이 힌트는 반환받을 result를 제한하는 기준을 지정하는 것이다. 예를들면, IPv4주소만 받고 싶거나, IPv6주소만 받고 싶을 수도 있고, 둘다 받고 싶을수도 있다. 이럴땐hints의 ai_family의 값을 조작하면 된다. 별도의 hint를 제공하지 않을 경우, NULL을 넣는다.
    struct addrinfo **result : DNS서버로부터 받은 네트워크 주소 정보(IP 주소)를 돌려주는 output 매개변수이다. addrinfo 구조체를 사용하며, 링크드 리스트이다. 이addrinfo 구조체에 대해서는 아래 그림을 참고하면 이해하기 쉽다. 이 result의 내용중 필요한것들은 적절히 copy하여 사용자의 변수로 옮겨두어야 하며, result는 사용이 끝나는 즉시 freeaddrinfo 함수로 메모리 해제를 해주어야 한다.
  • 반환 : 성공 시 0 반환, 실패 시 아래 에러 중 하나 반환

EAI_ADDRFAMILY
지정된 네트워크 호스트에 요청했던 address family 주소가 없는 경우이다. 예를들어 IPv4주소만 갖는 네트워크 호스트에게 'IPv6주소를 내놔라' 하고 요청한다면, 이런 에러가 반환된다.

EAI_AGAIN
nameserver가 일시적인 오류 표시를 반환했다. 일정 시간 이후에 다시 시도하라는 의미이다.

EAI_BADFLAGS
hints.ai_flags에 잘못된 플래그가 포함되어 있다. 또는 hints.ai_flags에는 AI_CANONNAME이 포함되었고 이름이 NULL인 경우이다.

EAI_FAIL
nameserver가 지속되는 오류 표시를 반환했다. EAI_AGAIN과는 달리 다시 시도해도 실패할 것이라는 의미다.

EAI_FAMILY
요청한 address family는 지원되지 않는다.

EAI_MEMORY
getaddrinfo를 수행하기에 메모리가 부족한 경우다. 거의 막장인 상황이다. kernel의 OOM killer가 제대로 작동하길 기대하면서 기다렸다가 일정 시간 지난 후 재시도 해보고 그래도 같은 에러를 받는다면 속편하게 재부팅 하라.

EAI_NODATA
지정한 네트워크 호스트가 있긴 한데... 네트워크 주소가 정의되어 있지 않은 경우다.

EAI_NONAME
노드 또는 서비스를 알 수 없다.
또는 노드와 서비스 모두가 NULL이다.
또는 AI_NUMERICSERV가 hints.ai_flags에 지정되었는데, 정작 service는 포트 번호를 나타내는 숫자형태의 문자열이 아니었다.
어떤 경우이건, 입력 매개변수를 잘못 넣었다는 의미다. 자신이 매개변수를 잘 넣었는지? 스스로의 코드를 다시 점검 해볼 것.

EAI_SERVICE
요청한 서비스를 요청한 소켓 유형에 사용할 수 없다. 다른 소켓 유형을 통해 사용할 수 있다. 예를 들어 서비스가 "쉘"(스트림 소켓에서만 사용할 수있는 서비스)이고 hints.ai_protocol이 IPPROTO_UDP이거나 hints.ai_socktype이 SOCK_DGRAM 인 경우 이 오류가 발생할 수 있다.
또는 서비스가 NULL이 아니고 hints.ai_socktype이 SOCK_RAW (서비스 개념을 지원하지 않는 소켓 유형) 인 경우 오류가 발생할 수 있습니다.

EAI_SOCKTYPE
요청한 소켓 유형이 지원되지 않는다.
예를 들어, hints.ai_socktype 및 hints.ai_protocol이 일치하지 않는 경우다. ai_socktype에는 SOCK_DGRAM 를 넣고, ai_protocol에는 IPPROTO_TCP를 넣은 hint를 입력 매개변수로 넘기면 발생할 수 있는 에러이다.

EAI_SYSTEM
위 모든 경우를 제외한 뭔가 다른 시스템 오류다, 골치아픈 경우다. 자세한 내용은 errno를 확인해야 하는 경우다.

const struct addrinfo *hints의 addrinfo 구조체 원형

struct addrinfo {
   int          ai_flags;           // 추가적인 옵션을 정의 할 때 사용함. 여러 flag를 bitwise OR-ing 하여 넣는다 
   int          ai_family;          // address family를 나타냄. AF_INET, AF_INET6, AF_UNSPEC 
   int          ai_socktype;        // socket type을 나타냄. SOCK_SREAM, SOCK_DGRAM 
   int          ai_protocol;        // IPv4와 IPv6에 대한 IPPROTO_xxx와 같은 값을 가짐. 
   socklen_t    ai_addrlen;         // socket 주소인 ai_addr의 길이를 나타냄 
   char        *ai_canonname;       // 호스트의 canonical name을 나타냄 
   struct sockaddr    *ai_addr;     // socket 주소를 나타내는 구조체 포인터 
   struct addrinfo    *ai_next;     // 주소정보 구조체 addrinfo는 linked list이다. 다음 데이터의 포인터 
};

getaddrinfo()에서 hints 인자를 넣을 때, 위의 int형 4개 필드에만(최대 4개) 값을 지정해준다. 나머지는 0(또는 NULL)이어야 한다. 실제 우리는 memset을 활용해서 전체 구조체를 0으로 설정하고, 4개 이하의 필드에만 값을 지정해준다.

ai_flags : 이 필드는 기본 동작을 수정해준다. 여러 예시들을 알아보자.

  • AI_PASSIVE : 이 경우, host 인자는 NULL이어야 한다. 클라이언트 connect()함수를 호출할 때 활성화된 소켓으로 이용할 수 있는 소켓 주소를 가져온다.
  • AI_CANONNAME : 기본적으로 ai_canonname필드는 NULL이다. 이 플래그가 설정되면 addrinfor 구조체 첫 번째의 ai_canonname필드에 가서 host의 규범적(canonical) 이름을 나타낸다.
  • AI_NUMERICHOST : 호스트 이름으로 "12.23.12.23"과 같이 숫자로 된 IP주소를 사용함을 의미한다.
  • AI_NUMERICSERV : 기본적으로 service 인자는 서비스이름이나 포트번호인데, 이 플래그가 사용되면 service 인자가 포트번호여야 한다.

ai_family : 기본적으로 getaddrinfo()는 IPv4와 IPv6 소켓 주소를 리턴한다.

  • AF_INET : IPv4 주소로 리턴한다.
  • AF_INET6 : IPv6 주소로 리턴한다.
  • AF_UNSPEC : 프로토콜에 관계없이, 사용할 수 있다.

ai_socktype : 소켓의 종류 3가지에 따라 적어준다.

  • SOCK_STREAM : TCP
  • SOCK_DGRAM : UDP
  • SOCKRAW : RAW

ai_protocol : 0 또는 IPPROTO

getaddrinfo 함수 예제

www.google.com의 80포트 네트워크 주소를 얻는 예제다.

#include <sys/types.h>
#include <sys/socket.h>
#include <netdb.h>

int main(int argc,char *argv[])
{
	int status;
	struct addrinfo hints;
	struct addrinfo *servinfo;  // 결과를 저장할 변수

	memset(&hints, 0, sizeof(hints)); // hints 구조체의 모든 값을 0으로 초기화
	hints.ai_family = AF_UNSPEC;     // IPv4와 IPv6 상관하지 않고 결과를 모두 받겠다
	hints.ai_socktype = SOCK_STREAM; // TCP stream sockets

	status = getaddrinfo("www.google.com", "80", &hints, &servinfo);
}

10. gai_strerror 함수

  • 형태 : const char *gai_strerror(int errcode);
  • 인수 : int errcode : getaddrinfo 함수에서 반환된 EAI 코드
    해당 errcode를 매개변수로 받아 다음의 오류 메세지를 출력한다.

11. freeaddrinfo 함수

  • 형태 : void freeaddrinfo(strcut addrinfo *result);
  • 인수 : strcut addrinfo *result : getaddrinfo함수에서 마지막에 넣어줬던 result addrinfo 구조체의 주소
    getaddrinfo()함수에서 동적 할당한 구조체 메모리 해제

12. setsockopt 함수

  • 헤더 : <sys/socket.h>
  • 형태 : int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);
  • 인수 : int sockfd : socket(2), accept(2) 등으로 생성된 socket descriptor
    int level : optname 값이 socket level인지 특정 protocol에 대한 설정인지를 지정하는 값
    . SOL_SOCKET : optname이 socket level에서 설정하는 option명임을 지정함
    . IPPROTO_IP : optname이 IP protocol level에서 설정하는 option명임을 지정함
    . IPPROTO_TCP : optname이 TCP protocol level에서 설정하는 option명임을 지정함
    int optname : level의 종류에 따라 다른 설정이름을 갖는다.

SOL_SOCKET level의 상수
SO_ACCEPTCONN : accept된 connection 여부 조회(get only) : 1이면 accept(2)된 connection.
SO_BROADCAST : datagram socket에 boradcast flag값을 (set/get)
SO_DOMAIN : socket에 설정된 domain값 (ex. AF_INET, AF_UNIX 등...)을 얻는다. (get only)
SO_ERROR : socket error를 읽고 지움. (get only)
SO_DONTROUTE : gateway를 통해서 전송을 금지하고 직접 연결된 host끼리만 전달하게 함. (set/get)
SO_KEEPALIVE : cconnection-oriented socket에서 keep alive message를 전송할 수 있도록 함. (set/get)
SO_LINGER : linger option 설정 (set/get)

struct linger {
        int l_onoff;    /* linger active */
        int l_linger;   /* how many seconds to linger for */
};
l_onoff를 1로 설정하면, close(2), shutdown(2) 함수를 실행하면 미전송된 데이터를 정상적으로 전송하거나
    linger timeout이 도래되면 함수를 return.  그렇지 않으면 바로 return되고 background로 작업하게 됨.

SO_OOBINLINE : out of bound data를 직접 읽을 수 있게 set/get (주로 X.25에서 사용)
SO_PROTOCOL : socket에 설정된 protocol을 읽음.
SO_RCVBUF : socket에서 읽을 수 있는 최대 buffer의 크기를 set/get함
SO_REUSEADDR : bind(2) 시에 local 주소를 재사용할 것인지 여부를 set/get함
SO_SNDBUF : socket에서 write할 수 있는 최대 buffer의 크기를 set/get함
SO_TYPE : 설정된 socket의 type(ex. SOCK_STREAM, SOCK_DGRAM0을 get함

const void *optval : 설정값을 저장하기 위한 버퍼의 포인터
socklen_t optlen : optval의 크기
설정값(optval)을 void * 로 넘기는 이유는 설정하고자 하는 소켓옵션에 따라서, boolean, interger, 구조체등 다양한 크기를 가지는 데이터형이 사용되기 때문이다. 만약 변경하고자 하는 소켓옵션이 boolean값을 사용한다면, 0혹은 1값을 사용하면 된다.

  • 반환: 정상 작동 시 0 반환, 에러 시 -1 반환(상세 오류 내용은 errno에 설정됨)

    EBADF : sockfd 유효한 descriptor가 아님.
    EFAULT : optval가 이 프로세스의 유효한 메모리 번지가 아님.
    EINVAL : optval이나 optlen이 유효하지 않음.
    ENOPROTOOPT : level이 알려지지 않은 값.
    ENOTSOCK : sockfd가 file descriptor이지 socket descriptor가 아님.

근데 사실 이건 다 의미없다.
https://cjwoov.tistory.com/30
해당 블로그를 읽어보자.^^

13. getsockname 함수

지정된 소켓에 대한 로컬 이름(IP와 port 번호)을 얻어내는 함수이다.

  • 헤더 : <sys/socket.h>
  • 형태 : int getsockname(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
  • 인자 : int sockfd : 정상적으로 생성된 socket descriptor. 주로 connect(2), accept(2), bind(2)된 socket descriptor.
    struct sockaddr *addr : 자신의 주소를 저장할 buffer.
    - 정상적으로 실행되면, sockfd에 설정된 자신의 address주소값이 채워진다. (OUTPUT용)
    - socket에 자신의 주소는 client socket에서는 bind(2)를 하지 않는 이상은 connect(2)시에 자동으로 할당되며,
    server socket도 자신의 주소는 port번호만 설정하는 것이 일반적이라 accept(2)시에 할당된다.
    따라서 대부분은 connect(2), accept(2) 후의 socket descriptor에 사용한다.
    구조체 종류는 Address Family (AF_* 상수)의 종류에 따라 다른 구조체를 parameter로 전달해야 한다.
    socklen_t *addrlen :
    - 두번째 파라미터인 addr 구조체의 크기를 설정하고 함수를 호출해야 합니다. (INPUT)
    - getsockname( ) 호출이 끝나면 실제로 addr에 채워진 사이즈가 저장됩니다. (OUTPUT)
  • 반환 : 정상 실행 시 0 반환, 오류 발생 시 -1 반환 (상세 오류정보 errno 전역변수에 저장됨)

    EBADF : sockfd가 유효하지 않은 descriptor입니다.
    EFAULT : addr 변수가 유효한 메모리 영역이 아닙니다.
    EINVAL : addrlen 변수가 유효한 메모리 영역이 아닙니다.
    ENOBUFS : 시스템에서 처리를 위한 resource 부족합니다.
    ENOTCONN : socket이 연결된 상태가 아닙니다. (connect()를 호출하지 않았거나 accept()되지않은 socket입니다.
    ENOTSOCK : sockfd가 socket이 아닙니다.

  • 예제 :
#include <sys/socket.h>

......

int       connected_fd;
struct    sockaddr_in my_addr;
socklen_t addr_len   = sizeof(struct sockaddr_in);
char      my_ip_addr[16];

......

if(getsockname(connected_fd, &my_addr, &addr_len) == -1) {
    fprintf(stderr, "getsockname() 호출 error : %s\n", strerror(errno));
    return -1;   
}

strncpy(my_ip_addr, inet_ntoa(my_addr.sin_addr.s_addr), 16);
printf("나의 IP address는 %s입니다.\n", my_ip_addr);

......

14. fcntl 함수

  • 헤더 : <fcntl.h> <unistd.h>
  • 형태 :
    int fcntl(int fd, int cmd);
    int fcntl(int fd, int cmd, long arg);
    int fcntl(int fd, int cmd, struct flock *lock);
  • 인자 :
    int fd : open(2), socket(2) 등의 시스템 호출을 통해서 만들어진 파일 지정자
    int cmd : fd 에 대한 특성을 제어하기위한 값
    int arg : cmd 값에 따라 추가로 필요한 인자값
  1. F_DUPFD : 기존 fd 를 복제한다. 언뜻보면 dup2(2) 함수와 매우 비슷한데, dup2 는 복사될 파일지정자를 사용자가 지시하는 반면, F_DUPFD 를 사용할경우 arg 와 같은 크기의 파일지정자를 되돌려주거나, 이미 사용되어지고 있다면, arg 보다 큰 할당가능한 파일지정번호중 가장 작은 번호를 되돌려준다.
    이 복사된 파일지정번호는 잠금, 파일위치 포인터, 플레그 등을 공유한다. 즉 파일지정자들중 하나에서 파일의 위치가 변경된다면(lseek등을 이용), 다른 파일지정자도 변경된다.
    그렇지만 close-on-exec 는 공유하지 않는다. (close-on-exec)아래에 기술 예정
  2. F_GETFD : fd 플래그들을 조회한다.(리턴값으로 fd에 대한 플래그를 넘겨준다) 현재는 FD_CLOEXEC 플래그 하나만 반환해준다. (close-on-exec와 관련)
  3. F_SETFD : fd 플래그들을 설정한다(FD_CLOEXEC(close-on-exec) 의 값을 지정된 비트값으로 설정한다.). 설정할 새로운 플래그 값은 세 번째 인수 arg에 지정한다.
  4. F_GETFL : 파일지정자에 대한 플래그값 - open(2) 호출시 지정한 플래그를 되돌려준다.
  5. F_SETFL : arg 에 지정된 값으로 파일지정자 fd 의 플래그를 재 설정한다. 현재는 단지 O_APPEND, O_NONBLOCK, O_SYNC, O_DSYNC, O_RSYNC, O_FSYNC, O_ASYNC 만을 설정할수 있다. 다른 플래그들 (O_WRONLY 와 같은) 은 영향을 받지 않는다.
  6. F_GETOWN : 이것은 비동기 입출력과 관련되어서 사용되며, SIGIO와 SIGURG 신호를 받는 프로세스 아이디를 얻기 위해서 사용된다.
  7. F_SETOWN : 비동기 입출력과 관련되어서 사용되며, SIGIO, SIGURG 시그널을 수신하는 프로세스 아이디(혹은 그룹)을 설정하기 위해서 사용된다.

struct flock *lock : 전체나 파일의 일부를 다른 프로세스에서 사용하지 못하게 제한 해주기 위한 잠금 옵션
파일을 열 때 open() 함수나 fopen() 함수의 mode를 이용하여 다른 프로세스가 읽기나 쓰기를 제한할 수 있다. 그러나 이것은 파일 전체에 대해 적용되며 제한을 변경하기 위해서는 파일을 닫았다가 다시 열어야 한다.
fcntl()은 오픈된 파일에 대해서 필요에 따라 제한을 여러 번 변경하실 수 있으며, 파일 전체 뿐만 아니라 일부만 제한할 수 있다. 즉, 파일의 특정 부분을 "잠금" 상태를 만들 수 있어서 fcntl() 함수를 "파일 잠금 함수"라기 보다는 "레코드 잠금 함수"로 부른다.

주의할 점

주의 하실 점은 파일에 대한 잠금은 프로세스별로 잠금 정보를 지정하는 것 뿐이지 실제로 다른 프로세스가 읽고 쓰는 것을 못하게 하는 것은 아니다. 즉, 쓰기를 제한했다고 해서 다른 프로세스에서 write() 가 실행이 안 되거나 에러가 발생하지 않는다.

잠금 정보는 하나의 파일을 여러 프로세스가 동시에 사용하는 경우, 같은 시간에 쓰기를 하거나 아직 한쪽에서 변경 중이라면 다른 프로세스가 읽지를 못하게 하기 위한 정보를 주고 받기 위한 방법으로 이해해야 한다.

쓰기를 하기 전에 쓰기 잠금이 가능한지를 확인한 후에 write() 를 실행하는게 보통이다. 그러나 확인없이 쓰기를 한다면 쓰기가 가능하다. 그러므로 잠금 정보는 프로세스가 쓰기를 못하게 한다가 아니라 지금은 읽어 서는 안 된다 또는 쓰기를 해서는 안 된다라는 정보로 이용해야 한다.

잠금옵션인 lock을 사용하기 위해서는 cmd에 다음과 같은 명령어를 써줘야 한다.
F_GETLK : 레코드의 잠금 상태를 돌려주며, 정보는 세번째 인수인 lock에 담겨져 온다.
F_SETLK : 레코드 잠금을 요청하며, 다른 프로세스가 먼저 선점해서 실패했다면 즉시 -1 로 복귀한다.
F_SETLKW : 끝의 W는 wait의 약자로 레코드 잠금을 요청했는데, 다른 프로세스가 먼저 선점해서 실패했다면 그 프로세스가 해제할 때까지 대기한다.

struct flock *lock의 구조체 struct flock

struct flock {
        short   l_type;
        short   l_whence;
        off_t   l_start;
        off_t   l_len;
        pid_t   l_pid;
        __ARCH_FLOCK_PAD
};

l_type 은 어떻게 잠금을 할지, 해제할지를 정한다. 즉, 아래와 같은 상수가 정의되어 있다.
F_RDLCK : 다른 프로세스가 읽기 잠금만 가능하게하고 쓰기 잠금은 못하게 한다.
F_WRLCK : 다른 프로세스는 읽기 잠금과 쓰기 잠금 모두 불가능하도록 한다.
F_UNLCK : 잠금을 해제한다.

l_whence 는 블록할 영역을 지정하는 기준 위치를 지정한다. 즉, 파일 첫 부분부터 따질지, 아니면 현재 읽기/쓰기 포인터를 기준으로 따질지를 정한다.
SEEK_SET : 파일의 시작 위치
SEEK_CUR : 현재 읽기/쓰기 포인터를 기준
SEEK_END : 파일의 끝을 기준
즉, l_whence가 SEEK_CUR이고 l_start가 0이면서 l_len이 0 이면 파일 전체를 가르키게 된다.

l_pid 는 F_GETLK 를 실행하여 레코드에 대한 잠금 상태 정보를 구할 때, 이미 잠금을 실행하고 있는 프로세스의 ID 이다.

14.1 CLOSE-ON-EXEC

보통 프로세스에서 exec를 시켜서 새로운 프로세스를 실행시키면 이 새로운 프로세스는 기존의 프로세스의 이미지를 덮어쓰게 된다. 그러면서 특별한 설정이 없을경우 열린파일지정자를 그대로 넘겨주게 된다.
그러나 때때로 exec 를 이용해서 프로세스를 만들기전에 기존에 열렸던 파일지정자들을 깨끗하게 정리하고 싶을때가 있을것이다. 이러한 경우를 close-on-exec 시킨다라고 말하며, fcntl 을 이용해서 열린 파일지정자에 대해서 close-on-exec 작동을 하도록 할수 있다.
예제 :

#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>

int main()
{
    int fd;
    int val;
    fd = open("exec_copy.txt", O_CREAT);

    // FD_CLOEXEC 값을 fcntl 을 이용해서 
    // 가져온 다음 세팅되어 있는지 검사한다.  
    val = fcntl(fd, F_GETFD, 0);
    if (val & FD_CLOEXEC)
        printf("close-on-exec bit on\n");
    else
        printf("close-on-exec bit off\n");


    // FD_CLOEXEC 를 세팅한다. 
    val |= FD_CLOEXEC;
    if (val & FD_CLOEXEC)
        printf("close-on-exec bit on\n");
    else
        printf("close-on-exec bit off\n");
    fcntl(fd, F_SETFD, val);

    // loop 프로그램을 exec 시킨다.  
    execl("/home/my_cvs/test/c_source/loop", "./loop", 0);
}

다양한 예제는 아래 블로그에서 확인할 수 있다.
https://www.joinc.co.kr/w/Site/system_programing/File/Fcntl#CLOSEONEXEC

끝!

참고 :
https://hyeonski.tistory.com/9
https://sncap.tistory.com/226
https://www.it-note.kr/120
https://learn.microsoft.com/ko-kr/windows/win32/api/winsock2/nf-winsock2-setsockopt
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=sojuchoigo&logNo=120021310463
https://www.joinc.co.kr/w/man/2/getsockname
https://badayak.com/entry/C%EC%96%B8%EC%96%B4-%EB%A0%88%EC%BD%94%EB%93%9C-%EC%9E%A0%EA%B8%88-%ED%95%A8%EC%88%98-fcntl
https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=neakoo35&logNo=30131475424

profile
내가 다시 보려고 만드는 42서울 본과정 블로그

0개의 댓글