Operating System/UNIX

[UNIX] TCP/IP 소켓 (Socket) 프로그래밍

  • -

개념

물리적으로 연결된 네트워크상에서의 데이터 송수신에 사용할 수 있는 소프트웨어적 장치다.

(네트워크 망의 연결에 사용되는 도구, 더 나아가서 네트워크를 통한 두 컴퓨터의 연결)

 

커널에 내부 버퍼를 가지고 있고, 꽉 차면 더이상 전송과 수신을 하지 않으므로 데이터 손실이 없다.

리눅스는 소켓 조작을 파일 조작과 동일하게 간주한다. 즉, 소켓을 파일의 일종으로 구분한다.

 

네트워크의 연결에는 TCP와 UDP가 나뉘지만, 해당 포스팅에서는 TCP를 다룬다.

 

 

함수

서버 측에서의 소켓과 클라이언트 측에서의 소켓은 사용법이 다르다.

 

서버 측

#include <sys/socket.h>
int socket(int domain, int type, int protocol); // 소켓 생성
int bind(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // 소켓에 주소 정보 할당
int listen(int sockfd, int backlog); // 소켓을 연결요청이 가능한 상태로 전환
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // 연결 요청에 대한 수락

 

socket() 을 호출하면 소켓을 관리할 수 있는 파일 디스크립터 (이하 fd)를 반환한다.

 

fd에 대한 더 자세한 설명은 아래 링크 참고

 

[UNIX] 파일 디스크립터 (File Descriptor)

개념 유닉스 및 유닉스 계열 운영 체제에서 파일 또는 파이프나 네트워크 소켓과 같은 기타 입출력 리소스에 대한 프로세스 고유 식별자이자 핸들이다. 일반적으로 음수가 아닌 정수 값이며, 음

sikpang.tistory.com


 

소켓을 생성한 이후 bind()로 해당 소켓에 주소 정보를 할당하고,

listen()으로 연결 요청이 가능한 상태로 전환하는 작업을 해줘야 한다.

 

따라서 서버에서의 소켓 프로그래밍 시 호출 순서는 다음과 같다.

socket (소켓 생성) - bind (소켓 주소 할당) - listen (연결 요청 대기상태) - accept (연결 허용) - read/write (데이터 송수신) - close (연결 종료)

 

클라이언트 측

int connect(int sockfd, struct sockaddr *addr, socklen_t *addrlen); // 연결 요청을 진행

 

클라이언트 측에서는 서버 측보다 더 간단하게 소켓 관리가 가능하다.

connect() 로 서버 측 소켓에 접근을 시도만 하면 되기 때문이다.

 

클라이언트에서의 소켓 프로그래밍 시 호출 순서는 다음과 같다.

socket (소켓 생성) - connect (연결 요청) - read/write (데이터 송수신) - close (연결 종료)

 

소켓 커뮤니케이션

 

서버 측 소켓 코드 예시

int makeSocketAndListen(int portNumber)
{
    struct sockaddr_in server_addr;
    int socketFd;
    
    if ((socketFd = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP)) == -1)
        return -1;
    
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;	// IPv4 프로토콜 주소 체계
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);	// 컴퓨터의 IP주소가 자동으로 할당
    server_addr.sin_port = htons(portNumber);

    if (bind(socketFd, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)
        return -1;

    if (listen(socketFd, LISTEN_QUEUE_SIZE) == -1)
        return -1;
        
    return socketFd;
}

 

 

함수 인자 설명

소켓 프로토콜 체계

PF_INET   // IPv4 인터넷 프로토콜 체계
PF_INET6  // IPv6 인터넷 프로토콜 체계
PF_LOCAL  // 로컬 통신을 위한 UNIX 프로토콜 체계
PF_PACKET // Low level 소켓을 위한 프로토콜 체계
PF_IPX    // IPX 노벨 프로토콜 체계

 

: socket() 의 첫 번째 인자를 통해서 지정한 프로토콜 체계의 범위 내에서 프로토콜을 결정해야 한다.

 

소켓의 타입

: socket() 의 두 번째 인자를 통해서 소켓의 데이터 전송방식을 정해준다.

SOCK_STREAM // 연결지향형
SOCK_DGRAM // 비 연결지향형

 

1. 연결지향형 소켓 (SOCK_STREAM)
    - 중간에 데이터가 소멸되지 않고 목적지로 전송된다.
    - 전송 순서대로 데이터가 수신된다.
    - 전송되는 데이터의 경계가 존재하지 않는다.
    - 다른 연결지향형 소켓 하나와만 연결이 가능하다.

 

2. 비 연결지향형 소켓 (SOCK_DGRAM)
    - 전송된 순서에 상관없이 가장 빠른 전송을 지향한다.
    - 전송된 데이터는 손실과 파손의 우려가 있다.
    - 전송되는 데이터의 경계가 존재한다. (두 번을 보내면 두 번을 받아야 한다.)
    - 한 번에 전송할 수 있는 데이터의 크기가 제한된다.

 

프로토콜 정보 추가 전달

: socket() 의 세 번째 인자를 통해서 프로토콜의 정보를 추가 전달해준다.

IPPROTO_TCP // IPv4, 연결지향형
IPPROTO_UDP // IPv4, 비 연결지향형

     

    sockaddr 구조체

    #include <sys/socket.h>
    
    struct sockaddr {
    	__uint8_t       sa_len;      // (unsigned char)
    	sa_family_t     sa_family;   // (unsigned char)
    	char            sa_data[14];
    };

     

    1. sa_family

    AF_INET // IPv4 인터넷 프로토콜에 적용하는 주소 체계
    AF_INET6 // IPv6 인터넷 프로토콜에 적용하는 주소 체계
    AF_LOCAL // 로컬 통신을 위한 유닉스 프로토콜의 주소 체계

     

    2. sa_data: 32비트 IP주소정보를 저장, 네트워크 바이트 순서
    3. sa_len: sa_data의 길이

     

    sockaddr_in 구조체

    #include <netinet/in.h>
    
    struct sockaddr_in {
    	__uint8_t       sin_len;      // (unsigned char)
    	sa_family_t     sin_family;   // (unsigned char)
    	in_port_t       sin_port;     // (unsigned short)
    	struct  in_addr sin_addr;     // (unsigned int)
    	char            sin_zero[8];
    };

     

    sockaddr의 sa_data에 저장되는 주소정보는 ip주소와 port 번호가 포함되어야 하고,
    남은 부분은 0으로 채울 것을 bind()가 요구하는데 이는 매우 불편해서 sockaddr_in 구조체가 나왔다.

     

    sockaddr_in의 바이트 열이 bind()가 요구하는 바이트 열이 되어 형변환해서 전달 시
    sockaddr 구조체 변수에 bind()가 요구하는 대로 데이터를 채워 넣은 효과를 볼 수 있다.


    1. sin_port: 16비트 port 번호를 저장, 네트워크 바이트 순서
    2. sin_addr: 32비트 IP주소정보를 저장, 네트워크 바이트 순서
    3. sin_zero: sockaddr_in의 크기를 sockaddr과 일치시키기 위한 멤버, 반드시 0으로 채워야 한다.

     

    Port

    IP만 있다면 목적지 컴퓨터로 데이터를 전송할 순 있지만 최종 목적지인 응용프로그램에까지 데이터를 전송할 순 없다.

    운영체제는 PORT 번호를 활용해 해당 소켓에 데이터를 전달한다.

    따라서, 하나의 운영체제 내에서 동일한 PORT 번호는 하나의 소켓에만 할당 가능하다.

     

    - PORT는 16비트로 표현하기 때문에 0 ~ 65535 까지다.
    - 0 ~ 1023은 예약된 포트라 사용할 수 없다.
    - TCP PORT와 UDP PORT는 중복되어도 상관없다.

     

    네트워크 바이트 순서

    1. 빅 엔디안
       - 상위 바이트의 값을 작은 번지수에 저장 (1 저장시, 0000 0000 0000 0001)
       - 네트워크상으로 데이터를 전송할 때 사용함


    2. 리틀 엔디안
       - 상위 바이트의 값을 큰 번지수에 저장 (1 저장시, 0001 0000 0000 0000)
       - 인텔계열 CPU에서 사용함

    3. 바이트 순서의 변환 
       - short 버전은 port 번호의 변환에 사용함
       - long 버전은 ip 주소의 변환에 사용함

    unsigned short htons(unsigned short); // host to network (short) 
    unsigned short ntohs(unsigned short); // network to host (short) 
    unsigned long htonl(unsigned long); // host to network (long) 
    unsigned long ntohl(unsigned long); // network to host (long)

     

    4. sockaddr 구조체 변수에 데이터를 채울 때 이외에는 신경쓰지 않아도 된다. 변환이 자동으로 이루어지기 때문이다.

     

    문자열과 IP 주소 변환

    • 문자열 to IP주소
    #include <arpa/inet.h>
    
    in_addr_t inet_addr(const char* str);  // (unsigned int)

    성공 시 빅 엔디안으로 변환된 32비트 정수 값, 실패 시 INADDR_NONE 반환

    “211.214.107.99”와 같이 점이 찍힌 문자열을 전달한다.

     

    #include <arpa/inet.h>
    
    int inet_aton(const char* str, struct in_addr* addr);

    성공 시 1, 실패 시 0

    inet_addr()과 같으나 구조체 sockaddr_in의 멤버인 in_addr에 대입해준다.

     

    • IP주소 to 문자열
    #include <arpa/inet.h>
    
    char* inet_ntoa(struct in_addr addr);

    변환된 문자열을 반환한다.

     

    • INADDR_ANY
    addr.sin_addr.s_addr = inet_addr(INADDR_ANY);

    : 컴퓨터의 IP주소가 자동으로 할당된다.

    편리하고 컴퓨터 내에 두개 이상의 IP를 할당 받아서 사용하는 경우(라우터 등),

    할당 받은 IP중 어떤 주소를 통해서 데이터가 들어오더라도 PORT 번호만 일치하면 수신할 수 있다.

    서버 프로그램의 구현에 많이 선호된다.

     

    sockaddr_in 초기화 예시

    struct sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));   // sin_zero 초기화를 위함
    
    char* ip = "211.217.168.13";
    char* port = "9190";
    
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(ip);
    addr.sin_port = htons(atoi(port));

       

      listen()

      연결 대기 상태로 전환한다.
      서버 소켓은 대기 큐를 관리하는 문지기이고, 데이터를 주고받으려면 소켓이 따로 필요하지만 직접 만들 필요는 없다.
      accept()가 성공 시 생성된 소켓의 파일 디스크립터를 반환하기 때문이다.

      두번째 인자 backlog : 연결요청 대기 큐 크기 (실험적 결과에 의존, 웹의 경우 최소 15)

       

      connect()

      서버 소켓의 대기 큐에 등록한다.

      첫번째 인자 servaddr : 연결 요청할 서버의 주소 정보

      두번째 인자 addrlen : 두 번째 매개변수에 전달된 주소의 변수 크기 (바이트)

      세번째 인자 sock : 클라이언트 소켓의 파일 디스크립터

       

       

      소켓 프로그래밍 시 주의점

      1. 서버는 계속 켜져있으며 연결요청을 하는 모든 클라이언트에게 서비스를 제공해야 한다.
          * 소켓을 close()하면 상대방 소켓에게 EOF가 전달됨 (예로 상대 read의 반환값이 0)

       

      2. 서버는 한 번의 write 호출로 데이터를 전송했지만, 전송할 데이터의 크기가 크다면 운영체제가 내부적으로 이를 여러 번으로 나눠 전송할 수도 있다. 이 과정에서 모두 전송되지 않았음에도 불구하고 클라이언트는 read함수를 한 번 호출하여 원하는 결과를 얻지 못하게 된다.

       

          - echo 서버의 경우는 클라이언트에게 받은 문자열 길이 만큼 다시 보내야하기 때문에

              보낼 문자열의 길이를 기억하고 있다가 write로 반환된 문자열의 길이를 누적하여 같아질 때까지 반복하여 해결
          - 에코가 아닌 경우는 그들만의 프로토콜로 정의함

       

          * write()는 전송할 데이터가 내부 출력버퍼로 이동이 완료되는 시점에 반환된다.

              TCP의 경우 전송을 보장하기 때문에 데이터가 전송이 완료되어야 반환된다.

       

      3. TCP의 Half-close
          : 한 쪽에서 close()를 하면 연결이 종료되기 때문에 꼭 받아야 할 데이터도 전송받지 못한다.


          소켓을 생성하면 A의 출력 버퍼에서 B의 입력 버퍼로 흘러가는 흐름이 형성되고,

          B의 출력 버퍼에서 A의 입력 버퍼로 흘러가는 스트림이 형성된다.
          이 중 하나만 끊는 게 Half-close이나 close()는 두 개를 모두 끊어버린다.


          이 문제를 해결하기 위해 스트림의 일부만 종료하는 방법이 있다.

        #include <sys/socket.h>
        
        int shutdown(int sock, int howto);


        - howto : 종료 방법에 대한 정보 전달

        SHUT_RD // 입력 스트림 종료
        SHUT_WR // 출력 스트림 종료
        SHUT_RDWR // 입출력 스트림 종료

         

        출력 스트림을 종료하면 상대 호스트로 EOF 전달, 따라서 서버는 Half-close로 출력 스트림을 닫는다.

         

         

            send(), recv()

            #include <sys/socket.h>
            
            ssize_t send(int sockfd, const void* buf, size_t nbytes, int flags);
            ssize_t recv(int sockfd, void* buf, size_t nbytes, int flags);

             

            1. sockfd : 데이터 수신 대상과의 연결을 의미하는 소켓의 fd

            2. buf : 수신된 데이터를 저장할 버퍼의 주소 값

            3. nbytes : 수신할 수 있는 최대 바이트 수

            4. flags : 데이터 수신 시 적용할 다양한 옵션 정보 (비트 OR 연산 사용 가능)

             

            Flag Description
            MSG_OOB 긴급 데이터의 전송을 위함 (out-of-band) TCP 헤더에 긴급 데이터 존재 여부와 offset을 저장하여 1바이트만 알려줌하지만 그렇게 긴급으로 전송되진 않음, 전혀 다른 통신 경로로 전송되는 데이터를 뜻함
            MSG_PEEK 입력 버퍼에 수신된 데이터의 존재 유무를 확인입력 버퍼에 존재하는 데이터가 읽혀지더라도 입력버퍼에서 데이터가 지워지지 않음. 데이터가 존재하지 않으면 blocking 됨
            MSG_DONTROUTE 데이터 전송과정에서 라우틴 테이블을 참조하지 않고 로컬 네트워크상에서 목적지를 찾음
            MSG_DONTWAIT 입출력 함수 호출과정에서 blocking 되지 않을 것을 요구함
            MSG_WAITALL 요청한 바이트 수에 해당하는 데이터가 전부 수신될 때까지 호출된 함수가 반환되는 것을 막음

             

            read(), write()와 대체로 비슷하지만 마지막 인자인 flags의 유무가 다르다.

            socket을 통한 I/O를 할 때는 send(), recv()가 더 가볍다.

             

              fcntl() 

              #include <fcntl.h>
              
              int fcntl(int fd, int cmd, ...); // 성공 시 매개변수 cmd에 따른 값

               

               

              : 소켓을 Non Blocking 특성으로 변경한다.

               

              // 두 번째 인자
              F_GETFL // fd에 설정되어 있는 특성정보를 int형으로 얻을 수 있음
              F_SETFL // fd의 특성정보를 변경
              int flag = fcntl(fd, F_GETFL, 0);
              fcntl(fd, F_SETFL, flag | O_NONBLOCK);

               

              : 기존에 설정되어 있던 특성정보를 얻어오고, Non Blocking 입출력을 의미하는 특성을 더해준다.

              read & write 함수 호출시에도 데이터의 유무에 상관없이 블로킹이 되지 않는 소켓이 만들어진다.

               

              소켓을 Non Blocking으로 변경해줌으로써 데이터의 수신 시점과 데이터가 처리되는 시점을 분리할 수 있다.

               

              I/O Blocking, Non-Blocking 및  I/O 멀티플렉싱에 대한 더 자세한 설명한 아래 링크 참고

               

              [UNIX] I/O 멀티플렉싱 (Multiplexing) 및 select

              I/O가 성능에 미치는 영향 I/O란 데이터 입출력을 일컫는 말이다. 네트워크 전송, 콘솔 출력, 파일 입출력 등을 모두 포함한다. 함수 실행과 같이 CPU를 사용하는 작업으로 인한 blocking은 개발자가

              sikpang.tistory.com

               

              『윤성우의 열혈 TCP/IP 소켓 프로그래밍』 책을 읽고 정리한 글입니다.
              검색을 허용하지 않고, 수익을 창출하지 않습니다.
                Contents

                포스팅 주소를 복사했습니다

                이 글이 도움이 되었다면 공감 부탁드립니다.