C++로 IRC 구현하기 (2)

yeshyungseok·2024년 3월 14일
post-thumbnail

소켓 프로그래밍

여기서 내가 구현할 것은 IRC 서버이므로 차트의 오른쪽 부분을 위주로 살펴보려고 한다(물론, 전체 동작 흐름을 이해하기 위해서는 클라이언트와 서버 모두에 대한 이해가 필요하다).

먼저, 소켓 프로그래밍을 위해 사용되는 함수들을 소켓의 생성부터, 클라이언트와의 연결까지 순서대로 살펴보려고 한다.


int socket(int domain, int type, int protocol)

역할: 소켓을 생성하고 파일 디스크립터 값을 반환한다.
domain: 프로토콜 체계를 지정한다. 주로 AF_INET(IPv4)나 AF_INET6(IPv6)를 사용한다.
type: 소켓의 타입을 지정한다. SOCK_STREAM(TCP)이나 SOCK_DGRAM(UDP) 중 하나를 사용한다.
protocol: 소켓을 사용할 프로토콜을 지정한다. 보통 0으로 설정하여 시스템이 자동으로 선택하도록 한다.

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen)

역할: 소켓에 주소를 할당한다.
sockfd: 소켓 디스크립터 == socket() 함수로 생성한 소켓의 반환 값.
addr: 할당할 주소를 지정한다. 일반적으로 struct sockaddr 구조체를 사용하며, IP 주소와 포트 번호를 지정한다.
addrlen: 주소의 길이를 나타낸다.

int listen(int sockfd, int backlog)

역할: 연결을 받을 수 있도록 소켓을 대기 상태로 설정한다.
sockfd: 소켓 디스크립터 == bind() 함수로 주소를 할당한 소켓의 반환 값.
backlog: 대기 큐의 크기를 지정한다. 이 큐에는 연결 요청이 대기된다(대기 큐를 넘어선 연결 요청이 발생할 시 연결 요청은 실패).

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen)

역할: 클라이언트의 연결 요청을 수락하고 통신을 위한 새로운 소켓을 생성한다.
sockfd: 소켓 디스크립터 == listen() 함수로 대기 상태로 설정한 소켓의 반환 값.
addr: 클라이언트의 주소 정보가 담길 구조체의 포인터.
addrlen: 주소의 길이를 나타낸다.

위 함수들의 매개변수에 struct sockaddr 구조체가 자주 등장하는 것을 볼 수 있다.

sockaddr 구조체는 소켓 주소 정보를 담기 위한 구조체이다. 네트워크 프로그래밍에서 주소 정보를 지정하기 위해 사용되며 IPv4나 IPv6와 같은 주소 체계에 따라 다르게 사용된다.

일반적으로 sockaddr 구조체는 다음과 같이 정의된다:

struct sockaddr {
    unsigned short sa_family;   // 주소 체계(AF_INET, AF_INET6 등)
    char sa_data[14];           // 주소 정보를 담는 부분
};

하지만 이 구조체는 주로 일반적인 구조체보다는 다양한 상황에 맞게 파생된 구조체들을 사용한다. 예를 들어, IPv4를 사용하는 경우에는 sockaddr_in 구조체를 사용한다.

IPv4에서 sockaddr_in 구조체는 다음과 같이 정의된다:

struct sockaddr_in {
    short int sin_family;            // 주소 체계 (AF_INET)
    unsigned short int sin_port;     // 포트 번호
    struct in_addr sin_addr;         // IP 주소
    unsigned char sin_zero[8];       // 필요한 경우 0으로 채워질 여분의 공간
};

sockaddr_in 구조체는 IPv4 주소를 저장하고, 포트 번호 및 기타 정보를 포함한다. 이와 유사하게 IPv6의 경우에는 sockaddr_in6 구조체를 사용한다.

sockaddr 구조체와 그 파생된 구조체들은 소켓 함수에서 주소 정보를 지정하거나 받아오는데 사용된다. 예를 들어, bind() 함수에서 서버 소켓에 주소를 할당하거나, accept() 함수에서 클라이언트의 주소 정보를 받아오는 등의 경우에 사용된다.

간단 구현

int main(int argc, char **argv) {
    if (argc == 3) { // 인자의 개수가 정확히 3개인지 확인 (프로그램 이름, 포트, 패스워드)
        int port = std::stoi(argv[1]); // 첫 번째 인자(포트 번호)를 정수로 변환
        std::string password = argv[2]; // 두 번째 인자(패스워드)를 문자열로 저장
        
 		struct sockaddr_in serv_addr; // 서버 주소 정보를 담을 구조체 선언
        std::memset(&serv_addr, 0, sizeof(serv_addr)); // 구조체를 0으로 초기화
        serv_addr.sin_family = AF_INET; // 주소 체계를 IPv4로 설정
        serv_addr.sin_addr.s_addr = INADDR_ANY; // 모든 인터페이스의 주소를 사용
        serv_addr.sin_port = htons(port); // 호스트 바이트 순서를 네트워크 바이트 순서로 변환

        int sockfd = socket(AF_INET, SOCK_STREAM, 0); // 소켓 생성
        
        if (sockfd == ERROR) {
            std::cerr << "Error opening socket" << std::endl;
            return EXIT_FAILURE; // 소켓 생성 실패 시 오류 메시지 출력 후 프로그램 종료
        }

        if (bind(sockfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) == ERROR) { // 소켓에 주소 할당
            std::cerr << "Error on binding" << std::endl;
            return EXIT_FAILURE; // 바인딩 실패 시 오류 메시지 출력 후 종료
        }

        if (listen(sockfd, 5) == ERROR) { // 연결 요청 대기열 설정
            std::cerr << "Error on listening" << std::endl;
            return EXIT_FAILURE; // 리스닝 실패 시 오류 메시지 출력 후 종료
        }
    } else {
        std::cout << "Correct usage is ./ircserv [port] [password]" << std::endl; // 올바른 사용법 안내
        return EXIT_FAILURE; // 인자가 부족할 때 프로그램 종료
    }
}

위 코드는 소켓의 생성부터 연결 요청 대기까지의 과정을 간단하게 구현한 코드이다.

소켓 프로그래밍의 개념을 익히기 위해 빠르게 작성하여 아직 OOP스럽지 못한 하드 코딩을 해 놓은 상태이다.

실제 클라이언트와의 연결을 수립하기 위해서는 연결 요청을 accept하는 과정이 추가되어야 하는데, 아직 멀티플렉싱(비동기 처리)에 대한 공부가 부족해 남겨 놓은 상태이다.

멀티플렉싱을 구현하여 다중 클라이언트에 대한 연결 및 이벤트 처리를 수행하기 위해서는 무한루프와 select 함수에 대한 이해가 필요한데, 이 부분은 다음 포스팅에서 알아보도록 하겠다.

profile
FE 개발자

0개의 댓글