[리눅스 프로그래밍] (IPC) Socket 통신 다루기

Yoon Yeoung-jin·2022년 7월 6일
0

Linux

목록 보기
10/13

01. Socket

소켓이란 무엇인가? 소켓의 정의는 다음과 같다.

Socket
네트워크 소켓은 컴퓨터 네트워크를 경유하는 프로세스 간 통신의 종착점이다.

즉, 프로그램이 네트워크에서 데이터를 통신할 수 있도록 연결해주는 연결부이다. 이러한 소켓을 이용하여 통신하는 것을 소켓 통신 한다고 한다.

그러면 소켓통신 하는 과정은 어떻게 되는가? 이에 대한 답변은 다음 그림과 같다.

그림 1. 클라이언트, 서버간이 소켓통신 과정 (SOCK_STREAM 일 경우)

그림 1의 과정을 서버와 클라이언트로 나누어서 순서를 보면 다음과 같다.

[Server]
1. socket() 함수를 사용하여 서버의 소켓을 연다
2. 열어놓은 소켓에 서버의 IP, PORT 를 바인딩 한다. 
3. listen() 함수를 사용하여 메시지를 받을 수 있는 상태로 전환한다.
4. 클라이언트가 보낸 connect request를 받고 클라이언트와 통신하기 위한 파일 디스크립터를 생성한다.
5. send() 를 통해서 메시지를 주거나 recv()를 통해서 메시지를 받는다.
6. 열어놓은 소켓을 닫는다. 
[Client]
1. socket() 함수를 사용하여 클라이언트의 소켓을 연다. 
2. 서버에 connect request 를 전송한다.
3. 메시지를 보내거나 받는다.
4. 소켓을 닫는다. 

위와 같은 과정으로 socket 통신을 하는데 우리는 socket을 구별해야 한다. 이때 socket을 구별하는 방법은 세가지가 존재한다. 이에 대한 표는 다음과 같다.

Domaindefinitionaddress 정의 방법
Unix domain socketAF_UNIXfilepath
IPv4 Internet domain socketAF_INETIPv4 주소 + Port 번호
IPv6 Internet domain socketAF_INET6IPv6 주소 + Port 번호

socket 통신 하는데 데이터를 주고받는 방식은 다음과 같다.

Socket typedefinition특징
StreamSOCK_STREAMConnect-orient, byte stream, reliable, 양방향
DatagramSOCK_DGRAMConnectionless, unreliable, 양방향

위 표를 보면 socket 통신 하는데 데이터를 주고받는 방식이 두개이다. 그리고 [그림 1]의 캡션을 보면 (SOCK_STREAM 일 경우) 라는 문구를 명시해 놓았다. 그러면 Datagram 통신일때는 통신 과정이 다른가? 결론은 [그림 1] 과정에서 몇가지 생략을 한것이 SOCK_DGRAM 일 경우의 통신 과정이다. 이에 대한 그림은 다음과 같다.

[Server]
1. socket() 함수를 사용하여 서버의 소켓을 연다
2. 열어놓은 소켓에 서버의 IP, PORT 를 바인딩 한다. 
3. send() 를 통해서 메시지를 주거나 recv()를 통해서 메시지를 받는다.
4. 열어놓은 소켓을 닫는다. 
[Client]
1. socket() 함수를 사용하여 클라이언트의 소켓을 연다. 
2. 메시지를 보내거나 받는다.
3. 소켓을 닫는다. 

02. 사용 API

  • 주소 체계 종류
    • AF_INET
    • AF_INET6
    • AF_UNIX
    • AF_LOCAL
    • AF_LINK(Low level socket 인터페이스를 이용)
    • AF_PACKET(IPX 노벨 프로토콜을 사용한다.)

02-01. 사용 구조체

  • sockaddr: 소켓의 주소를 담는 기본 구조체
    struct sockaddr {
        u_short sa_family;      /* 주소체계를 구분하기 위한 변수, 2 bytes, u_short는 unsigned short를 의미 */
        char sa_data[14];       /* 실제 주소를 저장하기 위한 변수. 14 bytes*/
    }
  • sockaddr_in: sockaddr 구조체에서 sa_family가 AF_INET인 경우 (즉. 주소 체계를 IPv4를 사용하는 경우)
    struct sockaddr_in{
        short sin_family;           /* 주소 체계: AF_INET */
        u_short sin_port;           /* 포트 번호를 의미, 범위는 0 ~ 65535, 2 bytes */
        struct in_addr sin_addr;    /* 호스트 IP 주소 */
        char sin_zero[8];           /* 8 bytes dummy data. 반드시 모두 0으로 채워져 있어야 한다. 이유는 sockaddr 구조체와 크기를 일치시키기 위함이다. */
    }
    
    struct in_addr{
        u_long s_addr;              /* 32비트 IP 주소를 저장할 변수 */
    }
  • sockaddr_in6: AF_INET6인 경우 (IPv6 주소체계를 사용하는 경우)
    struct sockaddr_in6{
        sa_family_t     sin6_family;        /* AF_INET6 */
        in_port_t       sin6_port;          /* IPv6 포트를 저장, ntohs() 또는 htons()로 조작하는 것이 좋음 */
        uint32_t        sin6_flowinfo;      /* IPv6 헤더와 연관된 트래픽 클래스와 플로루 레이블을 포함 */
        struct          in6_addr sin6_addr; /* 16 bytes IPv6 주소를 저장하는 변수 */
        uint32_t        sin6_scope_id;      /* sin6_addr의 주소 범위에 따라 달라지는 식별자를 포함할 수 있다. */
    }
    
    struct in6_addr{
        unsigned char s6_addr[16];          /* IPv6 address store */
    }
  • sockaddr_un: 하나의 시스템에서 서로 다른 프로세스 사이의 통신에 사용되는 소켓의 주소를 지정하는데 사용되는 구조체 (AF_UNIX, AF_LOCAL 인 경우)
    struct sockaddr_un{
        sa_family_t     sun_family;             /* AF_UNIX */
        char            sun_path[UNIX_PATH_MAX] /* 파일 시스템 경로 지정. NULL로 끝나는 문자열이여야 한다. 경로의 최대 길이는 NULL terminator를 포함해서 108 bytes 이다. */
    }

02-02. 사용 함수

  • socket
    • 헤더 파일: <sys/types.h><sys/socket.h>
    • 함수 원형: int socket(int domain, int type, int protocol);
    • 설명: 소켓을 생성하여 반환한다.
    • 입력 변수:
      • int domain: 프로토콜 family를 지정해주는 변수
        • AF_UNIX(프로토콜 내부), AF_INET(IPv4), AF_INET6(IPv6)
      • int type: 어떤 타입의 프로토콜을 사용할 것인지 설정
        • SOCK_STREAM(TCP), SOCK_DGRAM(UDP), SOCK_RAW(사용자 정의)
      • int protocal: 어떤 프로토콜의 값을 결정하는 것
        • 0IPPROTO_TCP(TCP), IPPROTO_UDP(UDP)
    • 반환값:
      • 에러: 1
      • 성공: 소켓에 대한 파일디스크립터
  • bind
    • 헤더 파일: <sys/types.h><sys/socket.h>
    • 함수 원형: int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);
    • 설명: 소켓을 바인딩 하는 함수 (쉽게 말하자면 소켓과 해당 프로세스를 묶음으로서 해당 프로세스가 소켓을 통해 다른 컴퓨터로부터 연결을 받아들일 수 있게 한다.)
    • 입력 변수:
      • int sockfd: 소켓 식별자 또는 소켓 디스크립터
      • struct sockaddr *myaddrsockaddr 포인터
      • socklen_t addrlenmyaddr 구조체 크기
    • 반환값:
      • 에러: 1
      • 성공: 0
  • listen
    • 헤더 파일: <sys/socket.h>
    • 함수 원형: int listen(int sock, int backlog);
    • 설명: 클라이언트가 연결요청 할 수 있는 상태로 만들어주는 함수.
    • 입력 변수:
      • int sock: 연결요청 대기상태에 두고자 하는 소켓의 파일 디스크립터 전달. 이 함수의 인자로 전달된 디스크립터의 소켓이 서버 소켓이 된다.
      • int backlog: 연결요청 대시 큐(queue)의 크기정보 전달.
        • Ex. 5가 전달되면 큐의 크기가 5가 되어서 클라이언트의 연결 요청을 5개까지 대기시킬 수 있다.
    • 반환값
      • 에러: 1
      • 성공: 0
  • accept
    • 헤더 파일: <sys/types.h><sys/socket.h>
    • 함수 원형: int accept(int sockfd, struct sockaddr* addr, socklent_t *addrlen);
    • 설명: 해당 소켓에 연결 요청이 왔을 때 연결을 받아들이는 함수이다.
    • 입력 변수:
      • int sockfd: 연결을 기다리는 소켓 디스크립터.
      • struct sockaddr* addr: 받아들인 클라이언트 주소 및 포트 정보가 저장될 구조체 주소값.
      • socklen_t *addrlensockaddr 구조체의 길이가 저장된 변수의 주소값
    • 반환값
      • 에러: 1
      • 성공: 새로운 소켓 디스크립터
  • connect
    • 헤더 파일: <sys/types.h><sys/socket.h>
    • 함수 원형: int connect(int socket, const struct sockaddr *address, socklen_t address_len);
    • 설명: 클라이언트 소켓을 생성하고, 서버로 연결을 요청
    • 입력 변수:
      • int socket: 클라이언트 소켓의 파일 디스크립터.
      • const struct sockaddr *address: 연결 요청을 보낼 서버의 주소 정보를 지닌 구조체 변수의 포인터.
      • socklen_t address_len: 포인터가 가리키는 주소 정보 구조체 변수의 크기.
    • 반환값
      • 에러: 1
      • 성공: 0
  • sendto
    • 헤더 파일: <sys/socket.h>
    • 함수 원형: ssize_t sendto(int socket, const void *buffer, size_t length, int flags, const struct sockaddr *dest_addr, socklen_t dest_len);
    • 설명: UDP/IP 통신에서 소켓으로 데이터를 전송하는 함수
    • 입력 변수:
      • int socket: 소켓 디스크립터
      • const void *buffer: 전송할 데이터
      • size_t length: 데이터의 바이트 단위 길이
      • int flags: 전송을 위한 옵션
      • const struct sockaddr *dest_addr: 목적지 주소 정보
      • socklen_t dest_len: 목적지 주소 정보 크기
    • 반환값
      • 에러: 1, 아래의 내용은 상세 errno의 대한 내용이다.
        [EACCES]           The SO_BROADCAST option is not set on the socket and a broadcast address is given as the destination.
        [EAGAIN]           The socket is marked non-blocking and the requested operation would block.
        [EBADF]            An invalid descriptor is specified.
        [ECONNRESET]       A connection is forcibly closed by a peer.
        [EFAULT]           An invalid user space address is specified for a parameter.
        [EHOSTUNREACH]     The destination address specifies an unreachable host.
        [EINTR]            A signal interrupts the system call before any data is transmitted.
        [EMSGSIZE]         The socket requires that message be sent atomically, and the size of the message to be sent makes this impossible. IOV_MAX.
        [ENETDOWN]         The local network interface used to reach the destination is down.
        [ENETUNREACH]      No route to the network is present.
        [ENOBUFS]          The system is unable to allocate an internal buffer.  The operation may succeed when buffers become available.
        [ENOBUFS]          The output queue for a network interface is full.  This generally indicates that the interface has stopped sending, but may be caused by transient congestion.
        [ENOTSOCK]         The argument socket is not a socket.
        [EOPNOTSUPP]       socket does not support (some of) the option(s) specified in flags.
        [EPIPE]            The socket is shut down for writing or the socket is connection-mode and is no longer connected.  In the latter case, and if the socket is of type SOCK_STREAM, the SIGPIPE signal is generated to the calling thread.
        [EADDRNOTAVAIL]    The specified address is not available or no longer available on this machine.
    • 성공: 실제 전송한 바이트 수
    • int flags의 옵션들
      • MSG_OOB: SOCK_STRAM에서만 사용되며 out-of-band 데이터로 전송될 수 있음을 의미
      • MSG_DONTROUTE: 데이터는 라우팅 될수 없음으로 지정
      • MSG_DONTWAIT: NONE BLOCKING 통신이 가능하도록 설정
      • MSG_NOSIGNAL: 상대방과 연결이 끊겼을 때, SIGPIPE 시스널을 받지 않도록 한다.
  • recvfrom
    • 헤더 파일: <sys/socket.h>
    • 함수 원형: ssize_t recvfrom(int socket, void *restrict buffer, size_t length, int flags, struct sockaddr *restrict address, socklen_t *restrict address_len);
    • 설명: UDP 통신 프로그램으로부터 데이터를 수신한다.
    • 입력 변수:
      • int socket: 소켓 디스크립터
      • void *restrict buffer: 수신한 데이터를 저장할 버퍼
      • size_t length: 읽을 데이터 크기
      • int flags: 읽을 데이터 유형 또는 읽는 방법에 대한 옵션
      • struct sockaddr *restrict address: 접속한 상대 시스템의 socket 주소 정보를 저장할 버퍼
      • socklen_t *restrict address_len
        • INPUT: address의 크기를 설정한 후에 호출한다.
        • OUTPUT: 호출 후에는 실제 할당된 address의 크기가 저장된다.
    • 반환값
      • 에러: 1, 아래 내용은 errno에 대한 내용이다.
        * EAGAIN or EWOULDBLOCK : time out이 발생하였거나 socket에 non-blocking이 설정된 경우 
        * EBADF : sockfd가 유효하지 않는 descriptor 
        * ECONNREFUSED : network상에서 접속 거부된 경우 
        * EFAULT : 읽을 데이터를 저장할 buf가 유효하지 않은 메모리인 경우 
        * EINTR : signal이 발생하여 interrupted 된 경우 
        * EINVAL : 파라미터의 값이 유효하지 않은 경우 
        * ENOMEM : 데이터 수신을 위한 메모리 할당이 되지 않은 경우 (recvmsg(2)호출시) 
        * ENOTCONN : connect(2), accept(2)가 호출되지 않은 상태인 경우 
        * ENOTSOCK : sockfd가 socket descriptor가 아닌 일반 파일인 경우
    • 성공: 실제로 수신한 데이터 길이를 리턴한다.

03. 예제 코드

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>

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

#define QUEUE_DEFAULT_NUM 5
#define BUF_MAX_LEN 4096

static void print_help(const char *progname)
{
    printf("Usage: %s (s|c Message) (stream|datagram) (filepath)\n", progname);
}

static int setting_sock_type(const char *TYPE, int *out)
{   
    if(!strcmp(TYPE, "stream")){
        /* stream type */
        *out = SOCK_STREAM;
    } else if(!strcmp(TYPE, "datagram")){
        /* datagram type */
        *out = SOCK_DGRAM;
    } else {
        printf("[ERROR] setting_sock_type - input wrong socket type\n ");
        return -1;
    }
    return 0;
}

static void setting_sock_filepath(const char *PATH, char *out)
{
    memset((void *)out, 0, sizeof(out));
    strcpy(out, PATH);
}

static int partial_message_recv(int fd, void *buf, size_t size, int flags)
{
    int written = 0;
    int ret = 1;

    while(ret){
        ret = recv(fd, (char *)buf + written , size - written, flags);
        if(ret == -1){
            perror("recv()");
            return ret;
        }
        written += ret;
    }
    return 0;
}

static int partial_message_send(int fd, void *buf, size_t size, int flags)
{
    int written = 0;
    int ret;

    while(written < size){
        ret = send(fd, (char *)buf + written , size - written, flags);
        if(ret == -1){
            return ret;
        }
        written += ret;
    }
    printf("send data: %s\n", (char *)buf);
    return 0;
}

static int do_recv_data(const int socket_type, const char *filepath)
{
    int server_fd;
    int client_fd;
    char buf[BUF_MAX_LEN];

    memset((void *)buf, 0, sizeof(buf));

    struct sockaddr_un addr;

    server_fd = socket(AF_UNIX, (int)socket_type, 0);
    if(server_fd == -1){
        perror("socket()");
        return -1;
    }
    memset((void *)&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, filepath, sizeof(addr.sun_path)-1);
    if(bind(server_fd, (const struct sockaddr *)&addr, sizeof(addr)) == -1){
        perror("bind()");
        close(server_fd);
        return -1;
    }
    if(socket_type == SOCK_STREAM){
        listen(server_fd, QUEUE_DEFAULT_NUM);
        client_fd = accept(server_fd, NULL, NULL);
        if(partial_message_recv(client_fd, (void *)buf, sizeof(buf), 0) == -1){
            close(server_fd);
            close(client_fd);
            return -1;
        }
        printf("Server : Client said %s\n", buf);
        close(server_fd);
        close(client_fd);
    }
    else if(socket_type == SOCK_DGRAM){
        if(recvfrom(server_fd, (void *)buf, sizeof(buf), 0, NULL, NULL) == -1){
            perror("recvfrom()");
            close(server_fd);
            return -1;
        }
        printf("Server : Client said %s\n", buf);
        close(server_fd);
    } else {
        printf("[ERROR] do_recv_data - wrong socket_type\n");
        return -1;
    }
    
    return 0;
}

static int do_send_data(const int socket_type, const char *filepath, const char *message)
{
    int client_fd;

    struct sockaddr_un addr;

    client_fd = socket(AF_UNIX, (int)socket_type, 0);
    if(client_fd == -1){
        perror("socket()");
        return -1;
    }
    memset((void *)&addr, 0, sizeof(addr));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, filepath, sizeof(addr.sun_path)-1);
    if(socket_type == SOCK_STREAM){
        if(connect(client_fd, (const struct sockaddr *)&addr, sizeof(addr)) == -1){
            perror("connect()");
            close(client_fd);
            return -1;
        }
        if(partial_message_send(client_fd, (void *)message, sizeof(message), 0) == -1){
            close(client_fd);
            return -1;
        }
        close(client_fd);
    }
    else if(socket_type == SOCK_DGRAM){
        if(sendto(client_fd, (void *)message, sizeof(message), 0, (struct sockaddr *)&addr, sizeof(addr)) == -1){
            perror("sendto()");
            close(client_fd);
            return -1;
        }
        close(client_fd);
    } else {
        printf("[ERROR] do_recv_data - wrong socket_type\n");
        return -1;
    }
    
    return 0;
}

int main(int argc, char **argv)
{
    int SOCK_TYPE;
    char filepath[1024];
    
    if(argc < 4){
        print_help(argv[0]);
        return -1;
    }

    if(!strcmp(argv[1], "s")){
        /* Server */
        if(setting_sock_type(argv[2], &SOCK_TYPE) == -1){
            goto main_err;
        }
        setting_sock_filepath(argv[3], filepath);
        if(do_recv_data((const int)SOCK_TYPE, (const char *)filepath) == -1){
            goto main_err;
        }

    } else if(!strcmp(argv[1], "c")){
        /* Client */
        if(argc < 5){
            print_help(argv[0]);
            return -1;
        }
        // char message[1024];
        // memset((void *)message, 0, sizeof(message));
        // strncpy(message, (const char *)argv[2], sizeof(argv[2]));
        if(setting_sock_type(argv[3], &SOCK_TYPE) == -1){
            goto main_err;
        }
        setting_sock_filepath(argv[4], filepath);
        if(do_send_data((const int)SOCK_TYPE, (const char *)filepath, (const char *)argv[2]) == -1){
            goto main_err;
        }
    } else {
        goto main_err;
    }
    return 0;
main_err:
    print_help(argv[0]);
    return -1;
}

03-01. 예제 코드 사용 방법

  • 해당 예제 코드는 AF_UNIX 만을 지원한다.
  • 실행 커맨드
    • s: server 역할
    • c Message: client 역할 그리고 전달할 Message입력
    • stream: 전달 메세지를 stream 모드로 전달 (TCP)
    • Datagram: 전달 메세지를 datagram 모드로 전달 (UDP)
    • file path: 파일 경로 입력
./main (s|c Message) (stream|datagram) (file path)
profile
신기한건 다 해보는 사람

1개의 댓글

comment-user-thumbnail
2024년 5월 6일

안녕하세요.
리눅스 프로그래밍에 대한 깊이 있는 내용 잘 보았습니다.
메세지큐, 통신, IPC 에 대해서 글 작성하실 때 참고하신 자료가 있을까요?
저자님처럼 깊게 배워보고 싶습니다.

답글 달기