[15장] 네트워킹과 파일 I/O: C++에서의 시스템 프로그래밍

이 블로그는 C++에서 네트워킹과 파일 I/O를 중점으로 다루며, 시스템 프로그래밍의 핵심 개념과 실용적인 기술을 탐구합니다. 네트워크 통신과 파일 시스템 작업을 통해 C++의 강력한 기능을 배워보세요.

클릭하면 큰 이미지로 볼 수 있습니다.
윤성우의 열혈 C++ 프로그래밍, 오렌지미디어

개요

이 장에서는 C++을 사용하여 네트워킹과 파일 입출력(I/O)을 다루는 시스템 프로그래밍의 기본을 살펴봅니다. 네트워킹은 데이터를 여러 컴퓨터 간에 전송하는 방법을, 파일 I/O는 데이터를 파일로 읽고 쓰는 방법을 의미합니다. 이러한 기술은 소프트웨어 개발에서 중요한 역할을 하며, C++의 강력한 기능을 통해 효율적으로 구현할 수 있습니다.

네트워킹

네트워킹은 서버와 클라이언트 간의 데이터 교환을 포함합니다. C++에서는 소켓 프로그래밍을 통해 네트워크 통신을 구현할 수 있습니다. 소켓은 네트워크 상에서 데이터를 교환하기 위한 엔드포인트로, IP 주소와 포트 번호를 사용합니다.

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

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int addrlen = sizeof(address);
    
    // 소켓 생성
    server_fd = socket(AF_INET, SOCK_STREAM, 0);

    // 주소 정보 설정
    address.sin_family = AF_INET;
    address.sin_addr.s_addr = INADDR_ANY;
    address.sin_port = htons(8080);

    // 소켓에 주소 바인드
    bind(server_fd, (struct sockaddr *)&address, sizeof(address));

    // 소켓을 수신 대기 상태로 설정
    listen(server_fd, 3);

    // 클라이언트 연결 수락
    new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);

    // 데이터 처리...
    
    // 소켓 닫기
    close(new_socket);
    close(server_fd);
    
    return 0;
}

파일 I/O

파일 I/O는 데이터를 파일 형식으로 읽고 쓰는 과정입니다. C++에서는 <fstream> 헤더를 통해 파일 I/O를 쉽게 처리할 수 있습니다. 파일 I/O를 사용하면 데이터를 영구적으로 저장하거나 나중에 사용하기 위해 불러올 수 있습니다.

#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::string data;

    // 파일 쓰기
    std::ofstream outfile("example.txt");
    outfile << "Hello, C++ File I/O!";
    outfile.close();

    // 파일 읽기
    std::ifstream infile("example.txt");
    getline(infile, data);
    std::cout << "File content: " << data << std::endl;
    infile.close();
    
    return 0;
}

C++에서 네트워킹과 파일 I/O를 사용하면 시스템 프로그래밍의 핵심 기능을 구현할 수 있습니다. 이 장에서는 이러한 기술의 기본적인 개념과 간단한 코드 예시를 제공하여 C++의 강력한 기능을 탐색하는 데 도움을 주고자 합니다.

이것이 C++이다:강의 현장을 그대로 옮긴 C++ 입문서, 한빛미디어

네트워킹

TCP/IP (Transmission Control Protocol/Internet Protocol)

TCP/IP는 인터넷에서 데이터를 안정적으로, 순서대로, 오류 없이 전송하는 데 사용되는 주요 프로토콜 스택입니다. TCP는 전송 제어 프로토콜(Transmission Control Protocol)의 약자로, 데이터가 목적지에 안전하고 정확하게 도착하도록 보장합니다. IP는 인터넷 프로토콜(Internet Protocol)의 약자로, 데이터 패킷이 올바른 목적지로 전송되도록 경로를 지정합니다.

TCP/IP의 특징

  • 신뢰성: 데이터의 정확한 전송을 보장합니다.
  • 연결 지향적: 통신을 시작하기 전에 연결을 설정합니다.
  • 순서 보장: 데이터가 전송된 순서대로 도착합니다.
  • 오류 검출 및 수정: 전송 중 발생한 오류를 검출하고 수정합니다.

Echo Server와 Echo Client

Echo 서버는 클라이언트로부터 받은 데이터를 그대로 돌려보내는 간단한 서버입니다. 이를 통해 TCP/IP 통신의 기본을 이해할 수 있습니다.

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

#define PORT 8080

int main() {
    int server_fd, new_socket;
    struct sockaddr_in address;
    int opt = 1;
    int addrlen = sizeof(address);
    char buffer[1024] = {0};
    char *hello = "Hello from server";

    // 소켓 생성
    server_fd = socket(AF_INET, SOCK_STREAM, 0);

    // 소켓 옵션 설정
    setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

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

    // 소켓에 주소 바인드
    bind(server_fd, (struct sockaddr *)&address, sizeof(address));

    // 수신 대기 상태로 설정
    listen(server_fd, 3);

    // 클라이언트 연결 수락
    new_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen);

    // 데이터 읽기
    read(new_socket, buffer, 1024);
    std::cout << "Message from client: " << buffer << std::endl;

    // 데이터 전송
    send(new_socket, hello, strlen(hello), 0);
    std::cout << "Hello message sent\n";

    // 소켓 닫기
    close(new_socket);
    close(server_fd);

    return 0;
}
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#define PORT 8080

int main() {
    int sock = 0;
    struct sockaddr_in serv_addr;
    char *hello = "Hello from client";
    char buffer[1024] = {0};

    // 소켓 생성
    sock = socket(AF_INET, SOCK_STREAM, 0);

    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(PORT);

    // 서버 주소 설정
    inet_pton(AF_INET, "127.0.0.1", &serv_addr.sin_addr);

    // 서버에 연결
    connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr));

    // 데이터 전송
    send(sock, hello, strlen(hello), 0);
    std::cout << "Hello message sent\n";

    // 데이터 수신
    read(sock, buffer, 1024);
    std::cout << "Message from server: " << buffer << std::endl;

    // 소켓 닫기
    close(sock);

    return 0;
}

사용된 소켓 함수들의 기능 설명

이것이 C++이다:강의 현장을 그대로 옮긴 C++ 입문서, 한빛미디어
  • socket(): 소켓을 생성합니다. AF_INET는 IPv4 인터넷 프로토콜을, SOCK_STREAM은 TCP 연결 지향형 소켓을 의미합니다.
  • setsockopt(): 소켓의 옵션을 설정합니다. 이 경우, 소켓을 재사용할 수 있도록 설정합니다.
  • bind(): 소켓에 주소(포트 번호와 IP 주소)를 할당합니다.
  • listen(): 서버 소켓이 클라이언트의 연결을 기다리도록 설정합니다. 여기서 3은 대기할 수 있는 최대 클라이언트 수를 의미합니다.
  • accept(): 클라이언트의 연결 요청을 수락합니다.
  • connect(): 클라이언트가 서버에 연결을 시도합니다.
  • read() / recv(): 네트워크를 통해 데이터를 읽습니다.
  • send() / write(): 네트워크를 통해 데이터를 전송합니다.
  • close(): 소켓 연결을 종료합니다.

이 함수들을 조합하여 TCP/IP 기반의 서버와 클라이언트 통신을 구현할 수 있습니다.

UDP/IP (User Datagram Protocol/Internet Protocol)

UDP/IP는 인터넷 프로토콜 스택의 일부로, TCP/IP와 함께 널리 사용됩니다. UDP는 연결이 없는 프로토콜로, 데이터가 목적지에 도착하는 것을 보장하지 않고, 데이터가 도착한 순서대로 처리되는 것도 보장하지 않습니다. 이러한 특성 때문에 UDP는 빠른 전송 속도가 필요한 응용 프로그램(예: 실시간 비디오 스트리밍, 온라인 게임, 음성 통화 등)에 적합합니다.

UDP의 특징

  • 비연결성: 연결 설정 없이 데이터를 전송합니다.
  • 효율성: 오버헤드가 적어 속도가 빠릅니다.
  • 신뢰성 부족: 데이터의 도착 순서나 도착 자체를 보장하지 않습니다.
  • 경량 프로토콜: 간단한 구조로 되어 있어 구현이 용이합니다.

UDP Echo Server와 Client 코드 예시

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

#define PORT 8080

int main() {
    int sockfd;
    struct sockaddr_in servaddr, cliaddr;

    // 소켓 생성
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

    memset(&servaddr, 0, sizeof(servaddr));
    memset(&cliaddr, 0, sizeof(cliaddr));

    servaddr.sin_family = AF_INET; // IPv4
    servaddr.sin_addr.s_addr = INADDR_ANY;
    servaddr.sin_port = htons(PORT);

    // 소켓에 주소 바인드
    bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr));

    int len, n;
    char buffer[1024];
    len = sizeof(cliaddr);

    while(true) {
        n = recvfrom(sockfd, (char *)buffer, 1024, MSG_WAITALL, ( struct sockaddr *) &cliaddr, (socklen_t *)&len);
        buffer[n] = '\0';
        std::cout << "Client : " << buffer << std::endl;
        sendto(sockfd, (const char *)buffer, strlen(buffer), MSG_CONFIRM, (const struct sockaddr *) &cliaddr, len);
    }

    return 0;
}
#include <iostream>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>

#define PORT 8080

int main() {
    int sockfd;
    struct sockaddr_in servaddr;

    // 소켓 생성
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);

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

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

    int n, len;
    char buffer[1024] = "Hello from client";
    sendto(sockfd, (const char *)buffer, strlen(buffer), MSG_CONFIRM, (const struct sockaddr *) &servaddr, sizeof(servaddr));
    n = recvfrom(sockfd, (char *)buffer, 1024, MSG_WAITALL, (struct sockaddr *) &servaddr, (socklen_t *)&len);
    buffer[n] = '\0';
    std::cout << "Server : " << buffer << std::endl;

    // 소켓 닫기
    close(sockfd);

    return 0;
}

사용된 소켓 함수들의 기능 설명

뇌를 자극하는 C++ STL, 한빛미디어
  • socket(): UDP 소켓을 생성합니다. 여기서 SOCK_DGRAM은 데이터그램(UDP) 소켓을 의미합니다.
  • bind(): 서버 측에서 소켓에 IP 주소와 포트 번호를 할당합니다.
  • sendto(): 데이터그램을 지정된 목적지 주소로 보냅니다. 이 함수는 데이터를 전송하고 해당 주소로 직접 데이터를 라우팅합니다.
  • recvfrom(): 데이터그램을 수신하고, 데이터를 보낸 소스의 주소 정보도 함께 받습니다. 이 함수는 수신 대기 상태에서 데이터가 도착할 때까지 블록됩니다.
  • close(): 열려 있는 소켓을 닫고, 해당 소켓과 관련된 리소스를 해제합니다.

UDP 프로그래밍은 TCP에 비해 상대적으로 간단하지만, 신뢰성이 낮은 통신을 수용해야 하는 경우에 적합합니다. 데이터의 순서, 무결성, 신뢰성을 애플리케이션 레벨에서 관리해야 할 수도 있습니다.

기타 네트워킹

HTTP/HTTPS

C++에서 HTTP 요청을 처리하기 위해 libcurl 라이브러리를 자주 사용합니다. 다음은 간단한 HTTP GET 요청을 보내는 예시입니다.

#include <iostream>
#include <curl/curl.h>

int main() {
    CURL *curl;
    CURLcode res;

    curl_global_init(CURL_GLOBAL_DEFAULT);
    curl = curl_easy_init();
    if(curl) {
        curl_easy_setopt(curl, CURLOPT_URL, "http://example.com");
        // HTTPS 요청 시 SSL 인증서 검사 생략
        // curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
        // curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);

        res = curl_easy_perform(curl);
        if(res != CURLE_OK)
            fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res));

        curl_easy_cleanup(curl);
    }
    curl_global_cleanup();
    return 0;
}
  • libcurl은 다양한 프로토콜을 지원하는 라이브러리로, 여기서는 HTTP 요청을 보내는 데 사용됩니다.
  • curl_easy_setopt() 함수를 사용하여 다양한 옵션을 설정할 수 있습니다.
  • HTTPS 요청을 위해 SSL 인증서 검사를 생략하는 옵션도 설정할 수 있습니다.

WebSocket

C++에서 WebSocket 통신을 위해 Boost.Beast 라이브러리를 사용할 수 있습니다. 아래 예시는 WebSocket 클라이언트의 기본적인 구조를 보여줍니다.

WebSocket 프로토콜을 이용한 간단한 클라이언트 구현을 위해 C++에서 Boost.Beast 라이브러리를 사용할 수 있습니다. Boost.Beast는 Boost.Asio를 기반으로 한 라이브러리로, HTTP 및 WebSocket 프로토콜을 처리할 수 있습니다. 아래 예제는 WebSocket 클라이언트가 서버에 연결하고 메시지를 보내는 기본적인 과정을 보여줍니다.

#include <boost/beast/core.hpp>
#include <boost/beast/websocket.hpp>
#include <boost/asio/connect.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <cstdlib>
#include <iostream>
#include <string>

 ...

using boost::asio::ip::tcp;
namespace websocket = boost::beast::websocket;

int main(int argc, char** argv) {
    try {
        // 필요한 인자가 모두 있는지 확인
        if(argc != 3) {
            std::cerr << "Usage: websocket-client <host> <port>\n";
            return EXIT_FAILURE;
        }
        auto const host = argv[1];
        auto const port = argv[2];

        // I/O 컨텍스트 객체
        boost::asio::io_context ioc;

        // DNS를 해결하기 위한 리졸버
        tcp::resolver resolver{ioc};
        auto const results = resolver.resolve(host, port);

        // WebSocket 스트림
        websocket::stream<tcp::socket> ws{ioc};

        // 연결
        auto ep = boost::asio::connect(ws.next_layer(), results);

        // 핸드셰이크
        ws.handshake(host, "/");

        // 메시지 보내기
        ws.write(boost::asio::buffer(std::string("Hello, WebSocket!")));

        // 버퍼와 읽기 상태 코드
        boost::beast::multi_buffer buffer;
        ws.read(buffer);

        // 에코된 메시지 출력
        std::cout << boost::beast::buffers(buffer.data()) << std::endl;

        // 연결 종료
        ws.close(websocket::close_code::normal);
    }
    catch(std::exception const& e) {
        std::cerr << "Error: " << e.what() << std::endl;
        return EXIT_FAILURE;
    }
    return EXIT_SUCCESS;
}
  • 이 코드는 Boost.Beast 및 Boost.Asio 라이브러리를 사용하여 WebSocket 클라이언트를 구현합니다.
  • 클라이언트는 주어진 호스트와 포트로 연결을 시도합니다.
  • 연결 후, 클라이언트는 서버에 대해 WebSocket 핸드셰이크를 수행합니다.
  • 핸드셰이크 성공 후, 클라이언트는 메시지를 서버로 보내고, 서버로부터 에코된 메시지를 받습니다.
  • 마지막으로 클라이언트는 연결을 정상적으로 종료합니다.
전문가를 위한 C++ : C++20 병렬 알고리즘 파일시스템 제네릭 람다 디자인 패턴 객체지향의 원리를 익히는 확실한 방법 개정판, 한빛미디어

이 예제는 간단한 WebSocket 클라이언트의 기본적인 작동 원리를 보여줍니다. 실제 응용 프로그램에서는 보다 복잡한 로직과 예외 처리가 필요할 수 있습니다.

파일 I/O (Input/Output)

파일 I/O는 데이터를 파일로부터 읽고 (Input) 파일에 쓰는 (Output) 작업을 말합니다. C++에서 파일 I/O는 파일스트림을 사용하여 수행됩니다. 파일스트림은 데이터의 흐름을 파일과 연결하고, 이를 통해 데이터를 읽거나 쓸 수 있습니다.

파일스트림의 종류:

  1. ofstream: 출력 파일 스트림 (쓰기 작업에 사용)
  2. ifstream: 입력 파일 스트림 (읽기 작업에 사용)
  3. fstream: 입출력 파일 스트림 (읽기 및 쓰기 모두에 사용)

파일 I/O의 기본 단계:

  1. 파일 스트림 객체 생성
  2. 파일 열기
  3. 데이터 읽기/쓰기
  4. 파일 닫기

파일 I/O 코드 예시

1. 파일 쓰기 예시 (ofstream 사용)

#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ofstream outFile("example.txt");  // 파일 스트림 객체 생성 및 파일 열기
    if (outFile.is_open()) {
        outFile << "Hello, File I/O!";  // 데이터 쓰기
        outFile.close();  // 파일 닫기
    } else {
        std::cerr << "Unable to open file";
    }
    return 0;
}

2. 파일 읽기 예시 (ifstream 사용)

#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ifstream inFile("example.txt");  // 파일 스트림 객체 생성 및 파일 열기
    std::string line;
    if (inFile.is_open()) {
        while (getline(inFile, line)) {  // 파일로부터 한 줄씩 읽기
            std::cout << line << '\n';
        }
        inFile.close();  // 파일 닫기
    } else {
        std::cerr << "Unable to open file";
    }
    return 0;
}

3. 파일 읽기 및 쓰기 예시 (fstream 사용)

#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::fstream fileStream("example.txt", std::fstream::in | std::fstream::out);  // 파일 열기 (읽기 및 쓰기 모드)

    if (fileStream.is_open()) {
        fileStream << "Hello, File I/O!";  // 파일에 쓰기
        fileStream.seekg(0);  // 파일 읽기 위치를 시작점으로 이동

        std::string line;
        while (getline(fileStream, line)) {  // 파일에서 읽기
            std::cout << line << '\n';
        }
        fileStream.close();  // 파일 닫기
    } else {
        std::cerr << "Unable to open file";
    }
    return 0;
}

파일 I/O 주의사항:

전문가를 위한 C++ : C++20 병렬 알고리즘 파일시스템 제네릭 람다 디자인 패턴 객체지향의 원리를 익히는 확실한 방법 개정판, 한빛미디어
  • 파일을 사용하기 전에 항상 파일이 제대로 열렸는지 확인해야 합니다 (is_open() 메소드 사용).
  • 파일 작업을 완료한 후에는 반드시 파일을 닫아야 합니다 (close() 메소드 사용).
  • 파일 읽기/쓰기 위치를 조절하기 위해서는 seekg() (읽기 위치 조절) 및 seekp() (쓰기 위치 조절) 메소드를 사용할 수 있습니다.
  • 파일을 여러 모드 (예: 읽기/쓰기)로 열 때는 fstream을 사용하고, 모드를 지정하기 위해 std::fstream::in, std::fstream::out 등의 플래그를 사용합니다.

파일 I/O는 프로그램이 데이터를 영구적으로 저장하고, 필요할 때 다시 불러와야 할 때 매우 중요합니다. C++의 파일스트림을 이용하면 이러한 작업을 쉽게 수행할 수 있습니다.

결론

파일 I/O는 프로그램이 파일에서 데이터를 읽고 쓰는 중요한 과정입니다. C++에서는 파일스트림을 사용하여 이러한 작업을 수행합니다. ofstream, ifstream, fstream 세 가지 기본적인 파일스트림 클래스를 통해 각각 쓰기, 읽기, 그리고 읽기/쓰기 작업을 할 수 있습니다. 파일을 열 때는 항상 파일이 제대로 열렸는지 확인해야 하며, 작업 완료 후에는 파일을 닫아야 합니다. 파일 읽기/쓰기 위치를 조절하고자 할 때는 seekg, seekp 함수를 활용할 수 있습니다. 이러한 C++의 파일 I/O 기능을 통해 데이터를 영구적으로 저장하고 필요할 때 쉽게 불러올 수 있어, 프로그래밍에서 매우 유용하게 활용됩니다. 이를 통해 프로그램의 데이터 관리 및 저장 방식을 효율적이고 강력하게 구현할 수 있습니다.