본문 바로가기

컴퓨터 과학/리눅스 프로그래밍

[이론]리눅스 프로그래밍 - 네트워크(TCP/IP, UDP)

반응형

이 책을 보고 작성하였습니다.

 

네트워크에 대한 개론

이론적인 부분은 다룬 적이 있어서 다음 글을 참고하기를 바란다.

https://messy-developing-diary.tistory.com/168

 

IT 엔지니어를 위한 네트워크 입문

CH 1 : 네트워크 시작하기프로토콜 : 네트워크 상의 통신에 필요한 규약 대표적으로 이더넷-TCP/IP 기반 프로토콜이 있음 물리적 측면 : 데이터 전송 매체, 신호 규약, 회선 규격 등 이더넷에서 널리

messy-developing-diary.tistory.com

 

TCP/IP와 UDP 통신에 대해서 보다 실전적인 부분을 다루고자 한다.

 

주요 주소 체계

TCP/IP : MAC 주소(48 bit), IP 주소(IPv4 - 32bit, IPv6 - 128bit), 도메인 네임(서버나 단말을 구분하기 위한 문자열)

MAC 주소
IP 주소(IPv4)

 

포트 번호 : 서비스들을 구분하기 위한 식별자(하나의 IP 주소를 chrome과 firefox가 같이 사용한다고 생각하면 편하다!)

    - etc/service : 유닉스에서 잘 알려진 포트 번호를 찾아볼 수 있다.

service : 부팅 시 자동으로 켜지고, 백그라운드에 계속 실행되는 프로세스, 리눅스에서는 daemon이라 부르기도 함(윈도우 운영체제에서는 service, 리눅스에서는 daemon이라 불렀는데 요즘에는 혼용해서 사용)

 

BSD 소켓

소켓 : 프로세스 간의 상호 양방향 통신 방식 - 소프트웨어로 작성된 통신 접속점

    조작을 위한 파일/소켓 디스크립터를 제공한다.

함수 이름 헤더 파일 설명 주요 인자

socket() <sys/socket.h> 소켓 생성 domain, type, protocol
bind() <sys/socket.h> 소켓에 로컬 주소 지정 sockfd, addr, addrlen
listen() <sys/socket.h> 소켓을 수신 대기 상태로 전환 (서버용) sockfd, backlog
accept() <sys/socket.h> 수신 대기 중인 연결 수락 (클라이언트 소켓 생성) sockfd, addr, addrlen
connect() <sys/socket.h> 원격 호스트에 연결 요청 (클라이언트용) sockfd, addr, addrlen
send(), write() <sys/socket.h> 연결된 소켓에 데이터 전송 sockfd, buf, len, flags
recv(), read() <sys/socket.h> 연결된 소켓으로부터 데이터 수신 sockfd, buf, len, flags
sendto() <sys/socket.h> 연결 없는 소켓에 데이터 전송 (UDP 등) sockfd, buf, len, flags, dest_addr, addrlen
recvfrom() <sys/socket.h> 연결 없는 소켓으로부터 데이터 수신 sockfd, buf, len, flags, src_addr, addrlen
close() <unistd.h> 소켓 종료 sockfd
shutdown() <sys/socket.h> 소켓의 송수신 기능 종료 sockfd, how
getsockname() <sys/socket.h> 로컬 주소 확인 sockfd, addr, addrlen
getpeername() <sys/socket.h> 연결된 원격 주소 확인 sockfd, addr, addrlen
setsockopt() <sys/socket.h> 소켓 옵션 설정 sockfd, level, optname, optval, optlen
getsockopt() <sys/socket.h> 소켓 옵션 확인 sockfd, level, optname, optval, optlen
select() <sys/select.h> 다중 소켓의 상태 검사 (입출력 가능 여부) nfds, readfds, writefds, exceptfds, timeout
poll() <poll.h> 다중 소켓 상태 검사 (보다 유연함) fds, nfds, timeout
inet_pton() <arpa/inet.h> IP 문자열을 이진 형태로 변환 af, src, dst
inet_ntop() <arpa/inet.h> IP 이진 데이터를 문자열로 변환 af, src, dst, size

 

소켓을 활용한 IPC : named pipe와 유사한 모습을 보인다.(실습 추가할 것)

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <wait.h>
#include <sys/socket.h>


int main(){

    int ret, fd[2];
    int status;
    char buf[] = "Hello World", line[BUFSIZ];
    pid_t pid;

    ret = socketpair(AF_LOCAL, SOCK_STREAM, 0, fd);
    if(ret == -1){
        return -1;
    }

    printf("socket 1 : %d\n", fd[0]); //하나가 read면 다른 하나가 write인 방식, rw가 정해져 있지는 않다.
    printf("socket 2 : %d\n", fd[1]);

    if((pid = fork()) < 0){
        return -1;
    }
    else if(pid == 0){
        write(fd[1], buf, strlen(buf)+1);
        printf("Data send : %s\n", buf);
        close(fd[1]); 
    }
    else{
        wait(&status); 					/* 자식 프로세스의 종료 대기 */
        read(fd[0], line, BUFSIZ); 		/* 자식 프로세스에서 온 데이터 읽기 */
        printf("Received data : %s\n", line);
        close(fd[0]); 
    }

    return 0;
}

 

UDP 네트워크 프로그래밍

UDP : 데이터 전송 시 신뢰성은 없지만 속도가 빨라 일반적인 LAN에서 많이 사용된다.(패킷의 분실 여부를 확인하는 절차가 부재 -     TCP와 대조되는 사항)

 

소켓 함수

int socket(int domain, int type, int protocol);
//성공 시 파일 디스크립터, 실패 시 -1

 

소켓 도메인

AF_INET IPv4 인터넷 프로토콜 사용 대부분의 인터넷 애플리케이션
AF_INET6 IPv6 인터넷 프로토콜 사용 IPv6 기반 네트워크
AF_UNIX 또는 AF_LOCAL 로컬(유닉스) 프로세스 간 통신 동일 시스템 내의 IPC
AF_PACKET 로우 레벨 패킷 접근 (Linux 전용) 네트워크 모니터링, 패킷 스니핑
AF_NETLINK 커널과 사용자 공간 간 통신 (Linux 전용) 라우팅 테이블 변경 등 시스템 관리
AF_BLUETOOTH 블루투스 통신 Bluetooth 장치 제어

 

소켓 타입

SOCK_STREAM 스트림 기반 (연결 지향) 신뢰성 보장, 순차적 전송, 연결 필요 (TCP 기반) 웹 서버, FTP 등
SOCK_DGRAM 데이터그램 기반 (비연결형) 비신뢰성, 순서 미보장, 빠름 (UDP 기반) DNS, VoIP 등
SOCK_RAW 원시 소켓 헤더까지 직접 제어 가능 ping, traceroute
SOCK_SEQPACKET 순서화된 고정 패킷 스트림 신뢰성 있음, 메시지 경계 보존 일부 특수 IPC
SOCK_RDM 신뢰성 있는 메시지 전송 (순서 없음) 거의 사용되지 않음 실험적 용도

 

프로토콜

IPPROTO_TCP TCP 프로토콜 사용 (SOCK_STREAM과 함께)
IPPROTO_UDP UDP 프로토콜 사용 (SOCK_DGRAM과 함께)
IPPROTO_ICMP ICMP 프로토콜 사용 (SOCK_RAW과 함께)

 

바인드 함수

소켓에 로컬 주소를 할당하는 데 사용되는 함수, 서버 측에 주로 사용된다. 명시적으로 bind()를 호출해야 클라이언트가 접속할 수 있는 고정 주소를 가질 수 있다.

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
// return 0(success), -1(error)

 

sockfd socket()으로 생성한 소켓의 파일 디스크립터
addr 소켓에 할당할 로컬 주소 정보가 들어 있는 구조체 포인터 (struct sockaddr *)
addrlen addr 구조체의 크기 (바이트 단위)

 

sockaddr_in(IPv4), sockaddr_in6 (IPv6) , sockaddr_un (유닉스 도메인) => sockaddr과 호환 가능하다.

struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr));           // 구조체 초기화
addr.sin_family = AF_INET;                // IPv4 주소 체계 지정
addr.sin_port = htons(8080);              // 포트 번호 8080 (네트워크 바이트 순서)
addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 루프백 주소 (localhost)

 

#include <sys/socket.h>

// 범용 소켓 주소 구조체 (모든 주소 구조체와 호환되도록 설계됨)
struct sockaddr {
    sa_family_t sa_family;   // 주소 체계 (AF_INET, AF_INET6, AF_UNIX 등)
    char        sa_data[14]; // 주소 정보를 담는 데이터 (각 주소 체계별로 다르게 해석됨)
};

 

little endian(작은 값 바이트 먼저 표기), big endian(큰 값 바이트 먼저 표기)의 차이를 고려해야 함(기기의 차이에 따라 메모리 저장 방식, 네트워크 데이터 순서 등이 달라질 수 있음) => 유닉스는 호스트 바이트 순서와의 변환을 위한 함수들을 제공하고 있다.

htons() Host → Network 16비트 (short) 호스트 바이트 순서의 16비트 값을 네트워크 바이트 순서로 변환
htonl() Host → Network 32비트 (long) 호스트 바이트 순서의 32비트 값을 네트워크 바이트 순서로 변환
ntohs() Network → Host 16비트 (short) 네트워크 바이트 순서의 16비트 값을 호스트 바이트 순서로 변환
ntohl() Network → Host 32비트 (long) 네트워크 바이트 순서의 32비트 값을 호스트 바이트 순서로 변환

 

네트워크 주소를 변환해주는 함수 역시 제공하고 있다.

inet_addr() 문자열 → 이진 IPv4 (in_addr) 192.168.0.1 → uint32_t (네트워크 바이트 순서)
inet_aton() 문자열 → 이진 IPv4 (in_addr) 192.168.0.1 → struct in_addr로 저장 (성공 여부 반환)
inet_ntoa() 이진 → 문자열 IPv4 (in_addr) struct in_addr → dotted string (192.168.0.1)
inet_pton() 문자열 → 이진 IPv4/IPv6 AF_INET/AF_INET6에 따라 in_addr 또는 in6_addr로 변환
inet_ntop() 이진 → 문자열 IPv4/IPv6 in_addr/in6_addr → char * 형태로 주소 반환
gethostbyname() 도메인 → IP IPv4 호스트 이름을 IP 주소로 변환 (구식, thread-safe 아님)
gethostbyaddr() IP → 도메인 IPv4 IP 주소를 도메인 이름으로 변환 (구식)
getaddrinfo() 도메인 → IP/포트 IPv4/IPv6 최신 API, 도메인과 서비스 이름을 소켓 주소로 변환 (다중 프로토콜 지원)
getnameinfo() 소켓주소 → 문자열 주소 IPv4/IPv6 소켓 주소에서 도메인 이름 및 서비스명 추출 (역변환)

 

바이트 조작 함수 : memset, memcpy, memcmp 등 함수=> C에서 다루는 내용

 

Listen 함수

TCP 서버 소켓 프로그래밍에서 클라이언트로부터의 연결 요청을 대기하기 위해 사용되는 함수

#include <sys/socket.h>

int listen(int sockfd, int backlog);

 

파라미터  설명
sockfd socket()으로 생성하고, bind()까지 완료된 소켓 디스크립터
backlog 커넥션 큐의 길이 (대기 큐 크기): 클라이언트가 동시에 연결 요청할 수 있는 수

 

  • 완료되지 않은 연결 큐 (SYN_RCVD 상태)
    클라이언트가 연결 요청을 보냈지만 3-way handshake가 완료되지 않음
  • 완료된 연결 큐 (ESTABLISHED 대기 상태)
    3-way handshake가 완료된 연결이 대기 중이며 accept()를 기다리는 상태

 

UDP 송수신 실습

/*server*/

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>

#define UDP_PORT 5100

int main(int argc, char **argv)
{
    int sockfd,n;
    struct sockaddr_in servaddr, cliaddr;
    socklen_t len;
    char mesg[1000];

    sockfd = socket(AF_INET, SOCK_DGRAM, 0); 	/* UDP를 위한 소켓 생성 */

    /* 접속되는 클라이언트를 위한 주소 설정 후 운영체제에 서비스 등록 */
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(UDP_PORT);
    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    /* 클라이언트로부터 메시지를 받아서 다시 클라이언트로 전송 */
    do {
        len = sizeof(cliaddr);
        n = recvfrom(sockfd, mesg, 1000, 0, (struct sockaddr *)&cliaddr, &len);
        sendto(sockfd, mesg, n, 0, (struct sockaddr *)&cliaddr, sizeof(cliaddr));
        mesg[n] = '\0';
        printf("Received data : %s\n", mesg);
    } while(strncmp(mesg, "q", 1));

    close(sockfd); 				/* 사용이 끝난 후 소켓 닫기 */

    return 0;
}
/*client*/

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

#define UDP_PORT 5100

int main(int argc, char **argv){

    int sockfd, n;
    socklen_t clisize;
    struct sockaddr_in servaddr, cliaddr;
    char mesg[BUFSIZ];

    if(argc != 2) {
        printf("usage : %s <IP address>\n", argv[0]);
        return -1;
    }

    sockfd = socket(AF_INET, SOCK_DGRAM, 0); 	/* UDP를 위한 소켓 생성 */

    /* 서버의 주소와 포트 번호를 이용해서 주소 설정 */
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;

    /* 문자열을 네트워크 주소로 변경 */
    inet_pton(AF_INET, argv[1], &(servaddr.sin_addr.s_addr));
    servaddr.sin_port = htons(UDP_PORT);

    /* 키보드로부터 문자열을 입력받아 서버로 전송 */
    do {
        fgets(mesg, BUFSIZ, stdin);
        sendto(sockfd, mesg, strlen(mesg), 0, (struct sockaddr *)&servaddr,
        sizeof(servaddr));
        clisize = sizeof(cliaddr);

        /* 서버로부터 데이터를 받아서 화면에 출력 */
        n = recvfrom(sockfd, mesg, BUFSIZ, 0, (struct sockaddr*) &cliaddr, &clisize);
        mesg[n] = '\0';
        fputs(mesg, stdout);
    } while(strncmp(mesg, "q", 1));

    close(sockfd);

    return 0;
}

 

TCP 서버와 클라이언트 프로그래밍

앞선 예제들과 유사한 메커니즘을 보인다!

/*server*/

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define TCP_PORT 5100

int main(int argc, char **argv)
{

    int ssock;
    socklen_t clen;
    int n;
    struct sockaddr_in servaddr, cliaddr;
    char mesg[BUFSIZ];

    if ((ssock = socket(AF_INET, SOCK_STREAM, 0)) < 0)
    {
        perror("socket()");
        return -1;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(TCP_PORT);

    if (bind(ssock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        return -1;

    if (listen(ssock, 8) < 0) // 동시 접속하는 대기 큐 설정
        return -1;

    clen = sizeof(cliaddr);
    do
    {
        /* 클라이언트가 접속하면 접속을 허용하고 클라이언트 소켓 생성 */
        int n, csock = accept(ssock, (struct sockaddr *)&cliaddr, &clen);

        /* 네트워크 주소를 문자열로 변경 */
        inet_ntop(AF_INET, &cliaddr.sin_addr, mesg, BUFSIZ);
        printf("Client is connected : %s\n", mesg);
        if ((n = read(csock, mesg, BUFSIZ)) <= 0)
            perror("read()");
        printf("Received data : %s", mesg);

        /* 클라이언트로 buf에 있는 문자열 전송 */
        if (write(csock, mesg, n) <= 0)
            perror("write()");
        close(csock);
    } while (strncmp(mesg, "q", 1));

    return 0;
}
/*client*/

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define TCP_PORT 5100

int main(int argc, char **argv)
{
    int ssock;
    struct sockaddr_in servaddr;
    char mesg[BUFSIZ];

    if(argc < 2) {
        printf("Usage : %s IP_ADRESS\n", argv[0]);
        return -1;
    }

    /* 소켓을 생성 */
    if((ssock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket()");
        return -1;
    }

    /* 소켓이 접속할 주소 지정 */
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;

    /* 문자열을 네트워크 주소로 변경 */
    inet_pton(AF_INET, argv[1], &(servaddr.sin_addr.s_addr));
    servaddr.sin_port = htons(TCP_PORT);

    /* 지정한 주소로 접속 */
    if(connect(ssock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("connect()");
        return -1;
    }

    fgets(mesg, BUFSIZ, stdin);
    if(send(ssock, mesg, BUFSIZ, MSG_DONTWAIT) <= 0) { 	/* 데이터를 소켓에 쓴다. */
        perror("send()");
        return -1;
    }

    memset(mesg, 0, BUFSIZ);
    if(recv(ssock, mesg, BUFSIZ, 0) <= 0) { 		/* 데이터를 소켓으로부터 읽는다. */
        perror("recv()");
        return -1;
    }

    printf("Received data : %s", mesg); 		/* 받아온 문자열을 화면에 출력 */ 

    close(ssock); 					/* 소켓을 닫는다. */ 

    return 0;
}

 

shutdown() : 서버에서 읽기용 소켓 또는 쓰기용 소켓 둘 중 하나만을 닫을 수 있다. 우아한 close() 함수

 

멀티 쓰레드 서버-클라이언트 예제

 앞선 예제의 경우, 다른 클라이언트가 접속할 경우, 통신이 완료될 때까지 대기 상태가 된다. 쓰레드를 이용하여 병렬 처리를 해줌으로써 이 문제를 해결할 수 있다. 멀티 스레드 방식 외에도 I/O 멀티플랙싱 방식도 사용하는데, 이 경우 클라이언트의 요구 사항이 많지 않다면 하나의 소켓 디스크립터를 효율적으로 사용할 수 있게 해준다.

      

방식명  지원 플랫폼  FD 수 제한 내부  구조 성능 (FD 수 증가 시) 이벤트 등록 방식 경량성 / 무거움 재사용성 스레드 적합성 사용 난이도 대표 사용 예
select UNIX 계열 (POSIX) 1024개 (FD_SETSIZE 제한) 선형 검색 낮음 (O(n)) 매 호출 시 전체 FD 전달 경량 낮음 낮음 낮음 초기 네트워크 서버
poll UNIX 계열 (POSIX) 제한 없음 (OS 자원 한도) 선형 검색 낮음 (O(n)) 매 호출 시 전체 FD 전달 중간 낮음 낮음 낮음 중기형 네트워크 서버
epoll Linux 전용 제한 없음 이벤트 기반 비동기 큐 높음 (O(1)) 사전 등록 후 대기 경량 높음 높음 중간 리눅스 고성능 서버
kqueue BSD 계열, macOS 제한 없음 이벤트 기반 비동기 큐 높음 (O(1)~O(log n)) 사전 등록 후 대기 경량 높음 높음 중간~높음 macOS, FreeBSD 서버
IOCP Windows 전용 제한 없음 완료 기반 비동기 모델 매우 높음 비동기 작업 등록 무거움 높음 매우 높음 높음 Windows 고성능 서버

 

select() 함수

파일 디스크리터 변화를 확인하고, 데이터 송수신이 가능한 상태인지 확인

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

 

nfds 감시할 FD 중 가장 큰 값 + 1 (예: FD가 4, 7, 10이면 → nfds = 11) : 전체 개수가 아닌 열린 파일 디스크립터의 제일 큰 수
readfds 읽기 감시 대상 FD 집합 (데이터가 들어올 수 있는지 확인)
writefds 쓰기 감시 대상 FD 집합 (쓰기 가능 여부 확인)
exceptfds 예외 감시 대상 FD 집합 (일반적으로 OOB(out-of-band) 데이터 감지)
timeout 대기 시간 설정 - NULL: 무한 대기 - 0: 즉시 반환 - 값 지정: 해당 시간만큼 대기

 

리턴 값

> 0 이벤트가 발생한 FD의 개수
0 타임아웃 발생 (이벤트 없음)
-1 오류 발생 (예: 잘못된 FD, 시그널 인터럽트 등)

 

FD 조작 : fd_set에 fd를 추가하거나 삭제한다.

void FD_ZERO(fd_set *set);     // FD 집합 초기화
void FD_SET(int fd, fd_set *set);   // FD 추가
void FD_CLR(int fd, fd_set *set);   // FD 제거
int  FD_ISSET(int fd, fd_set *set); // FD가 이벤트 발생했는지 확인

 

select 사용 예제 - TCP_SELECT_SERVER, TCP_SELECT_CLIENT

폴링 방식을 이용하였다.

/*select server*/

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define SERVER_PORT 5100

int main(int argc, char **argv){

    int ssock;
    socklen_t clen;
    int n;
    struct sockaddr_in servaddr, cliaddr;
    char mesg[BUFSIZ];
    fd_set readfd;
    int maxfd, client_index, start_index;
    int client_fd[5] = {0, }; 

     /* 서버 소켓 디스크립터 연다. */
    if((ssock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket()");
        return -1;
    }

    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERVER_PORT);

    if(bind(ssock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind()");
        return -1;
    }

    if(listen(ssock, 8) < 0) { //클라이언트 접속 대기 최대 8개
        perror("listen()");
        return -1;
    }

    FD_ZERO(&readfd);
    maxfd = ssock;
    client_index = 0;

    do{
        FD_SET(ssock, &readfd);
         for(start_index = 0; start_index < client_index; start_index++) {
            FD_SET(client_fd[start_index], &readfd);
            if(client_fd[start_index] > maxfd)
                maxfd = client_fd[start_index]; 	/* 가장 큰 소켓의 번호를 저장 */
        }
        maxfd++; //순회 하면서 setup 해주기(폴링을 위하여)

        /* select( ) 함수에서 읽기가 가능한 부분만 조사 */
        select(maxfd, &readfd, NULL, NULL, NULL); 	/* 읽기가 가능해질 때까지 블로킹 */
        if(FD_ISSET(ssock, &readfd)) { 			/* 읽기가 가능한 소켓이 서버 소켓인 경우 */
            clen = sizeof(struct sockaddr_in);		/* 클라이언트의 요청 받아들이기 */
            int csock = accept(ssock, (struct sockaddr*)&cliaddr, &clen); //연결 가능한 클라이언트에 대한 정보 제공
            if(csock < 0) {
                perror("accept()");
                return -1;
            } else {
                /* 네트워크 주소를 문자열로 변경 */
                inet_ntop(AF_INET, &cliaddr.sin_addr, mesg, BUFSIZ);
                printf("Client is connected : %s\n", mesg);

                /* 새로 접속한 클라이언트의 소켓 번호를 fd_set에 추가 */
                FD_SET(csock, &readfd);
                client_fd[client_index] = csock;
                client_index++;
                continue;
            }
            if (client_index == 5) break;
        }

        /* 읽기 가능했던 소켓이 클라이언트였던 경우 */
        for(start_index = 0; start_index < client_index; start_index++) {
            /* for 루프를 이용해서 클라이언트들을 모두 조사 */
            if(FD_ISSET(client_fd[start_index], &readfd)) {
                memset(mesg, 0, sizeof(mesg));

                /* 해당 클라이언트에서 메시지를 읽고 다시 전송(Echo) */
                if((n = read(client_fd[start_index], mesg, sizeof(mesg))) > 0) {
                    printf("Received data : %s", mesg);
                    write(client_fd[start_index], mesg, n);
                    close(client_fd[start_index]); 	/* 클라이언트 소켓을 닫는다. */

                    /* 클라이언트 소켓을 지운다. */
                    FD_CLR(client_fd[start_index], &readfd);
                    client_index--;
                }
            }
        }
    }while(strncmp(mesg, "q", 1));

    close(ssock);

    return 0;
}

 

client는 위의 tcp 클라이언트 프로그램을 그대로 사용하면 된다.

 

epoll 방식(비동기 처리 방식)

select는 모든 fd에 대해서 송수신 여부를 확인하기 때문에 속도가 느리다. 이러한 단점을 해결하기 위해 만든 방법에 epoll이다. 

기존의 방식이 사용자가 fd를 관리하였다면, epoll은 커널이 fd를 관리한다. 순회를 해야했던 select와 다르게 커널이 이벤트가 발생한 파일 디스크립터들만을 구조체 배열로 넘겨 주므로 메모리 카피에 대한 비용이 줄어든다.

함수명  설명
epoll_create() 또는 epoll_create1() epoll 인스턴스 생성
epoll_ctl() 감시할 FD를 epoll 인스턴스에 등록/수정/삭제
epoll_wait() 이벤트 발생을 대기하고 반환

 

epoll_ctl() 파라미터

epfd epoll_create()로 얻은 epoll 인스턴스의 FD
op 동작 종류: EPOLL_CTL_ADD, EPOLL_CTL_MOD, EPOLL_CTL_DEL
fd 감시할 파일 디스크립터
event 감시할 이벤트 정보 (struct epoll_event)

 

이벤트 구조체

struct epoll_event {
    uint32_t events;   // 감시할 이벤트 (예: EPOLLIN, EPOLLOUT, EPOLLET)
    void *data;        // 사용자 정의 데이터 포인터 (예: FD 값 저장 등)
};

EPOLLIN = 읽기 이벤트
EPOLLOUT = 쓰기 가능
EPOLLET = 엣지 트리거 (기본은 레벨 트리거)

 

epoll_wait() 파라미터

epfd epoll 인스턴스의 FD
events 이벤트 정보가 저장될 배열 포인터
maxevents events 배열의 크기
timeout 대기 시간 (ms 단위)-1: 무한 대기, 0: 즉시 반환

 

epoll_wait event 비트 마스크

비트 마스크 상수 16진수 값 설명
EPOLLIN 0x00000001 읽기 이벤트: 읽을 수 있는 데이터가 있음 (예: 수신 버퍼)
EPOLLPRI 0x00000002 우선순위 높은 데이터 수신 가능 (Out-of-band data 등)
EPOLLOUT 0x00000004 쓰기 이벤트: 데이터를 쓸 수 있음 (송신 버퍼에 여유 공간 있음)
EPOLLRDNORM 0x00000040 일반 데이터 읽기 가능 (EPOLLIN과 유사)
EPOLLRDBAND 0x00000080 우선순위 밴드 데이터 읽기 가능
EPOLLWRNORM 0x00000100 일반 데이터 쓰기 가능 (EPOLLOUT과 유사)
EPOLLWRBAND 0x00000200 우선순위 밴드 데이터 쓰기 가능
EPOLLMSG 0x00000400 사용되지 않음 (무시됨, 호환성 목적)
EPOLLERR 0x00000008 에러 상태 발생 (자동 감지됨, 명시하지 않아도 감지됨)
EPOLLHUP 0x00000010 연결 끊김(Hang-up) 발생 (자동 감지됨)
EPOLLRDHUP 0x00002000 연결의 읽기 종료 감지 (peer가 shutdown(SHUT_WR) 또는 close 호출한 경우)
EPOLLET 0x80000000 Edge Triggered 모드 설정 (기본은 Level Triggered)
EPOLLONESHOT 0x40000000 이벤트 한 번만 감지 후 자동 해제 (재등록 필요)
EPOLLEXCLUSIVE 0x10000000 다중 쓰레드 epoll 대기에서 이벤트 독점 감시 (epoll 인스턴스 경쟁 방지)

 

epoll 서버 예제

클라이언트는 전에 구현했던 tcp_client를 사용하면 된다.

#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/epoll.h>

#define SERVER_PORT 5100
#define MAX_EVENT 32

void setnonblocking(int fd){
    int opts = fcntl(fd, F_GETFL);
    opts |= O_NONBLOCK;
    fcntl(fd, F_SETFL, opts);
}

int main(){

    int ssock, csock;
    socklen_t clen;
    int n, epfd, nfds = 1; 			/* 기본 서버 추가 */
    struct sockaddr_in servaddr, cliaddr;

    struct epoll_event ev;
    struct epoll_event events[MAX_EVENT];
    char mesg[BUFSIZ];

    /* 서버 소켓 디스크립터를 연다. */
    if((ssock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
        perror("socket()");
        return -1;
    }

    setnonblocking(ssock); 			/* 서버를 넌블로킹 모드로 설정 */

    memset(&servaddr, 0, sizeof(servaddr)); 	/* 운영체제에 서비스 등록 */
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons(SERVER_PORT);
    if(bind(ssock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind()");
        return -1;
    }

    if(listen(ssock, 8) < 0) { 			/* 클라이언트의 소켓들을 위한 큐 생성 */
        perror("listen()");
        return -1;
    }

    /* epoll_create() 함수를 이용해서 커널에 등록 */
    epfd = epoll_create(MAX_EVENT);
    if(epfd == -1) {
        perror("epoll_create()");
        return 1;
    }

    /* epoll_ctl() 함수를 통해 감시하고 싶은 서버 소켓을 등록 */
    ev.events = EPOLLIN;
    ev.data.fd = ssock;
    if(epoll_ctl(epfd, EPOLL_CTL_ADD, ssock, &ev) == -1) {
        perror("epoll_ctl()");
        return 1;
    }

    do {
        epoll_wait(epfd, events, MAX_EVENT, 500);
        for(int i = 0; i < nfds; i++) {
            if(events[i].data.fd == ssock) { 	/* 읽기가 가능한 소켓이 서버 소켓인 경우 */
                clen = sizeof(struct sockaddr_in);

                /* 클라이언트의 요청 받아들이기 */
                csock = accept(ssock, (struct sockaddr*)&cliaddr, &clen);
                if(csock > 0) {
                    //새로운 event 추가
                    /* 네트워크 주소를 문자열로 변경 */
                    inet_ntop(AF_INET, &cliaddr.sin_addr, mesg, BUFSIZ);
                    printf("Client is connected : %s\n", mesg);
                    
                    setnonblocking(csock); 	/* 클라이언트를 넌블로킹 모드로 설정 */

                    /* 새로 접속한 클라이언트의 소켓 번호를 fd_set에 추가 */
                    ev.events = EPOLLIN | EPOLLET;
                    ev.data.fd = csock;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, csock, &ev);
                    nfds++;
                    continue;
                }
            } else if(events[i].events & EPOLLIN) { 	/* 클라이언트의 입력 */
                if(events[i].data.fd < 0) continue; 	/* 소켓이 아닌 경우의 처리 */
                memset(mesg, 0, sizeof(mesg));

                /* 해당 클라이언트에서 메시지를 읽고 다시 전송(Echo) */
                if((n = read(events[i].data.fd, mesg, sizeof(mesg))) > 0) {
                    printf("Received data : %s", mesg);
                    write(events[i].data.fd, mesg, n);
                    

                    /* 클라이언트 소켓을 닫고 지운다. */
                    close(events[i].data.fd); 	
                    epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                    nfds--;
                }
            }
        }
    } while(strncmp(mesg, "q", 1));

    close(ssock); 				/* 서버 소켓을 닫는다. */

    return 0;
}

 

책에 HTTP 웹 서버를 구현하는 코드가 나오는데 별도의 글로 다룰려고 한다. 글이 너무 길어질 것 같다.

 

서버를 실행시키는 방법(daemon)

전원을 껐다 켜도 프로그램을 자동으로 실행시키고 싶을 수 있다. 백그라운드에서 자동으로 실행되는 프로그램들을 리눅스에서는 데몬(daemon)이라 부른다.

 

방법은 .service 파일을 작성하고 systemctl enable daemon.service 방식으로 등록 후 start해주면 실행된다.

 

서비스 파일

[Unit]
Description=My Custom Daemon
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/mydaemon
Restart=on-failure
User=nobody
Group=nogroup

[Install]
WantedBy=multi-user.target

 

 

섹션 주요 항목 설명
[Unit] Description 서비스 설명
  After 서비스가 시작되기 전 의존 대상 (예: network.target 이후 실행)
[Service] Type 프로세스 유형: simple, forking, oneshot, notify 등
  ExecStart 실행할 명령 또는 바이너리 경로
  Restart 실패 시 재시작 정책 (always, on-failure 등)
  User, Group 해당 유저/그룹으로 서비스 실행
[Install] WantedBy enable 시 연결될 대상 runlevel (multi-user.target 등)

 

/etc/systemd/system 폴더에 저장

#systemd에 서비스 파일 리로드
sudo systemctl daemon-reexec     # 또는
sudo systemctl daemon-reload

#서비스 등록 및 시작
sudo systemctl enable mydaemon.service    # 부팅 시 자동 실행
sudo systemctl start mydaemon.service     # 즉시 실행

#서비스 상태 확인
sudo systemctl status mydaemon.service

#서비스 수동 정지 및 재시작
sudo systemctl stop mydaemon.service
sudo systemctl restart mydaemon.service

 

정리하자면 통신은 결국

setting -> open -> read/write -> close 이거나,

setting -> open -> bind -> listen -> read/write -> close (for server)

setting -> open -> connect -> read/write -> close (for client)

 

모두 파일 입출력 때 했던 내용과 매우 유사한 모습을 보이는 것을 알 수 있다.(리눅스의 특징이기도 하다!!)

 

출처 : https://github.com/valentis/LinuxProgrammingWithRaspberryPi/tree/master

 

GitHub - valentis/LinuxProgrammingWithRaspberryPi: Linux Programming With Raspberry Pi

Linux Programming With Raspberry Pi. Contribute to valentis/LinuxProgrammingWithRaspberryPi development by creating an account on GitHub.

github.com

 

반응형