C언어로 파일 입출력과 네트워크 프로그래밍 결합하기

도입 문구


C언어에서 파일 입출력과 네트워크 프로그래밍을 결합하여 다양한 응용 프로그램을 개발하는 방법에 대해 소개합니다. 파일 입출력 기능은 데이터를 저장하고 읽어오는 데 필수적인 기능이며, 네트워크 프로그래밍은 다른 시스템과 데이터를 주고받을 수 있는 중요한 기술입니다. 이 두 가지를 결합함으로써, 분산 시스템이나 서버-클라이언트 구조의 프로그램을 작성할 수 있게 됩니다. 본 기사에서는 C언어를 사용해 파일 입출력과 네트워크 프로그래밍을 함께 사용하는 방법을 단계별로 설명합니다.

C언어의 파일 입출력


C언어에서 파일 입출력은 프로그램이 파일에 데이터를 읽고 쓸 수 있도록 해주는 기능입니다. 이를 위해 stdio.h 라이브러리를 사용합니다. 기본적으로 파일을 열고, 데이터를 읽고 쓰며, 작업이 끝난 후 파일을 닫는 등의 작업을 수행할 수 있습니다.

파일 열기와 파일 포인터


파일을 열기 위해서는 fopen() 함수를 사용합니다. 이 함수는 파일의 경로와 모드를 지정하여 파일을 엽니다. 파일 포인터는 파일을 가리키는 변수로, 이 포인터를 통해 파일에 접근합니다. 예를 들어:

FILE *fp = fopen("example.txt", "r");

이 코드는 example.txt 파일을 읽기 모드로 엽니다. 파일을 열 때 사용하는 모드는 읽기(“r”), 쓰기(“w”), 추가(“a”) 등 다양한 옵션이 있습니다.

파일 읽기 및 쓰기


파일을 읽고 쓰는 방법은 fread()fwrite() 함수를 사용합니다. fread()는 파일에서 데이터를 읽어 변수에 저장하고, fwrite()는 변수의 데이터를 파일에 씁니다. 예시 코드로는 다음과 같습니다:

char buffer[100];
fread(buffer, sizeof(char), 100, fp);  // 파일에서 데이터를 읽기
fwrite(buffer, sizeof(char), 100, fp); // 파일에 데이터를 쓰기

파일 닫기


파일 작업이 끝난 후에는 fclose() 함수를 사용하여 파일을 닫습니다. 파일을 닫지 않으면 데이터가 제대로 저장되지 않거나 리소스가 낭비될 수 있습니다. 예를 들어:

fclose(fp);

이렇게 C언어에서는 파일을 읽고 쓰는 간단한 방법을 제공하며, 이를 통해 다양한 파일 기반 작업을 할 수 있습니다.

파일 포인터와 모드


파일을 열 때 사용하는 모드와 파일 포인터의 역할에 대해 설명하겠습니다. 파일 포인터는 파일에 접근할 수 있게 해주는 변수이며, 다양한 모드를 사용하여 파일을 다룰 수 있습니다.

파일 포인터


파일 포인터는 FILE 타입의 변수로, 파일을 여는 함수(fopen())가 반환하는 값입니다. 이 포인터를 통해 파일에 접근하고, 파일에서 데이터를 읽거나 쓸 수 있습니다. 파일 포인터가 유효하지 않으면 파일 작업을 진행할 수 없으므로, 파일을 열 때마다 반드시 파일 포인터의 상태를 확인해야 합니다. 예를 들어:

FILE *fp = fopen("example.txt", "r");
if (fp == NULL) {
    printf("파일을 열 수 없습니다.\n");
}

파일 모드


파일을 열 때 사용할 수 있는 모드는 파일을 어떻게 읽고 쓸지를 결정합니다. 주로 사용하는 모드는 다음과 같습니다:

  • “r” : 읽기 모드. 파일을 읽을 수만 있으며, 파일이 존재하지 않으면 오류가 발생합니다.
  • “w” : 쓰기 모드. 파일이 존재하지 않으면 새로 생성하며, 기존 파일은 덮어씁니다.
  • “a” : 추가 모드. 파일 끝에 데이터를 추가합니다. 파일이 없으면 새로 생성합니다.
  • “r+” : 읽기 및 쓰기 모드. 파일을 읽고 쓸 수 있으며, 파일이 존재하지 않으면 오류가 발생합니다.
  • “w+” : 읽기 및 쓰기 모드. 파일을 읽고 쓰며, 파일이 존재하지 않으면 새로 생성하고 기존 파일은 덮어씁니다.
  • “a+” : 읽기 및 추가 모드. 파일을 읽고 끝에 데이터를 추가하며, 파일이 없으면 새로 생성합니다.

파일을 열 때 지정하는 모드는 파일을 어떤 방식으로 다룰지를 결정하므로, 요구사항에 맞는 모드를 선택해야 합니다. 예를 들어:

FILE *fp = fopen("example.txt", "w");

이 코드는 example.txt 파일을 쓰기 모드로 엽니다. 파일이 없으면 새로 생성되며, 기존 내용은 덮어씁니다.

모드에 따른 파일 작업 예시


파일을 열 때 사용한 모드에 따라 파일 작업이 달라집니다. 예를 들어, 읽기 모드에서 파일에 데이터를 쓰면 오류가 발생합니다. 따라서 파일을 다룰 때는 적절한 모드를 선택하는 것이 중요합니다.

네트워크 프로그래밍 기본 개념


네트워크 프로그래밍은 네트워크 상에서 데이터를 주고받을 수 있는 프로그램을 만드는 기술입니다. 이를 통해 다른 컴퓨터와 통신하는 클라이언트-서버 구조의 응용 프로그램을 개발할 수 있습니다. C언어에서 네트워크 프로그래밍은 주로 소켓을 사용하여 구현됩니다.

소켓(Socket)이란?


소켓은 네트워크 연결의 끝점을 나타내는 추상화된 개념입니다. 클라이언트와 서버 간의 데이터 전송을 위해 소켓을 생성하고 이를 통해 연결을 설정합니다. 소켓을 사용하여 IP 주소와 포트 번호를 지정하여 특정 서버에 연결하거나 데이터를 송수신합니다. C언어에서는 sys/socket.h 라이브러리를 사용하여 소켓을 다룹니다.

소켓 프로그래밍의 주요 단계


네트워크 프로그래밍은 다음과 같은 주요 단계로 나눌 수 있습니다:

  1. 소켓 생성: socket() 함수를 사용하여 소켓을 생성합니다. 이 함수는 프로토콜 타입과 소켓 종류를 지정하여 소켓을 반환합니다.
  2. 서버에 연결: 클라이언트는 서버와의 연결을 위해 connect() 함수를 사용합니다.
  3. 데이터 송수신: send()recv() 함수 또는 write()read() 함수를 사용하여 데이터를 송수신합니다.
  4. 연결 종료: close() 함수를 사용하여 소켓을 닫고, 연결을 종료합니다.

서버와 클라이언트 간의 통신


서버-클라이언트 모델에서, 서버는 클라이언트의 요청을 받아 처리하는 역할을 하며, 클라이언트는 서버에 요청을 보내고 응답을 받습니다. 클라이언트는 서버의 IP 주소와 포트 번호를 알고 있어야 하며, 이를 통해 서버와 연결을 설정하고 데이터를 주고받습니다.

네트워크 프로그래밍을 통해 클라이언트-서버 간의 다양한 통신을 구현할 수 있으며, 파일 전송, 채팅 프로그램, 웹 서버 등 다양한 응용 프로그램을 개발할 수 있습니다.

소켓 라이브러리 사용법


C언어에서 소켓을 사용하려면 sys/socket.h 라이브러리를 포함해야 합니다. 이 라이브러리에는 소켓을 생성하고 연결하는 데 필요한 함수들이 정의되어 있습니다. 또한, netinet/in.harpa/inet.h 라이브러리도 IP 주소와 포트 번호를 다룰 때 사용됩니다.

소켓 생성하기


소켓을 생성하려면 socket() 함수를 사용합니다. 이 함수는 소켓을 생성하고, 네트워크 통신에 사용할 프로토콜과 타입을 설정합니다. 기본적인 소켓 생성 코드는 다음과 같습니다:

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

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("소켓 생성 오류");
    return -1;
}

여기서 AF_INET은 IPv4 인터넷 주소 체계를 의미하고, SOCK_STREAM은 TCP 소켓을 사용한다는 뜻입니다. 0은 기본 프로토콜을 의미합니다.

서버 주소 설정


서버와 연결하려면 서버의 IP 주소와 포트 번호를 지정해야 합니다. 이를 위해 struct sockaddr_in 구조체를 사용합니다. 아래는 서버 주소를 설정하는 예제입니다:

struct sockaddr_in server_addr;
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(12345);  // 포트 번호
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");  // 서버 IP 주소

htons()는 네트워크 바이트 순서로 포트 번호를 변환하는 함수이며, inet_addr()는 IP 주소 문자열을 네트워크 주소로 변환합니다.

서버 연결


서버에 연결하려면 클라이언트 소켓에서 connect() 함수를 호출해야 합니다. 이 함수는 서버 주소와 소켓을 연결합니다. 예를 들어:

int status = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (status < 0) {
    perror("서버 연결 실패");
    return -1;
}

이렇게 소켓을 생성하고 서버에 연결할 수 있습니다. 이후 send()recv() 함수를 사용하여 데이터를 송수신합니다.

소켓 종료


네트워크 통신이 끝난 후, close() 함수를 사용하여 소켓을 종료합니다. 예:

close(sockfd);

소켓을 종료하여 리소스를 해제하는 것은 중요하며, 이를 통해 연결이 정상적으로 종료됩니다.

서버와 클라이언트 프로그램 구현


C언어에서 서버와 클라이언트 프로그램을 구현하는 것은 네트워크 프로그래밍의 핵심입니다. 클라이언트는 서버에 연결하여 데이터를 요청하고, 서버는 이를 처리하여 응답하는 구조로 동작합니다. 여기에서는 간단한 서버와 클라이언트 프로그램을 구현하는 방법을 소개합니다.

서버 프로그램 구현


서버는 클라이언트의 요청을 기다리고, 요청이 오면 이를 처리하는 프로그램입니다. 서버는 다음과 같은 순서로 동작합니다:

  1. 소켓 생성: 서버는 socket() 함수를 사용하여 소켓을 생성합니다.
  2. 주소 바인딩: bind() 함수를 사용하여 소켓에 IP 주소와 포트 번호를 바인딩합니다.
  3. 클라이언트 연결 대기: listen() 함수를 사용하여 클라이언트의 연결을 대기합니다.
  4. 연결 수락: accept() 함수로 클라이언트와의 연결을 수락합니다.
  5. 데이터 처리: 클라이언트가 보낸 데이터를 읽고 응답을 보냅니다.
  6. 소켓 종료: 통신이 끝나면 close()로 소켓을 종료합니다.

다음은 간단한 서버 코드 예제입니다:

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

int main() {
    int server_sock, client_sock;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[1024];

    // 소켓 생성
    server_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock < 0) {
        perror("소켓 생성 실패");
        exit(1);
    }

    // 주소 설정
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(12345);

    // 주소 바인딩
    if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("바인딩 실패");
        exit(1);
    }

    // 클라이언트 연결 대기
    listen(server_sock, 5);

    // 클라이언트 연결 수락
    client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len);
    if (client_sock < 0) {
        perror("클라이언트 연결 수락 실패");
        exit(1);
    }

    // 데이터 수신
    recv(client_sock, buffer, sizeof(buffer), 0);
    printf("클라이언트: %s\n", buffer);

    // 데이터 송신
    strcpy(buffer, "Hello from server!");
    send(client_sock, buffer, strlen(buffer), 0);

    // 소켓 종료
    close(client_sock);
    close(server_sock);
    return 0;
}

클라이언트 프로그램 구현


클라이언트는 서버에 요청을 보내고, 서버로부터 응답을 받는 역할을 합니다. 클라이언트 프로그램은 다음과 같은 순서로 동작합니다:

  1. 소켓 생성: socket() 함수를 사용하여 소켓을 생성합니다.
  2. 서버 연결: connect() 함수를 사용하여 서버에 연결합니다.
  3. 데이터 송신: 서버에 데이터를 보내기 위해 send() 함수를 사용합니다.
  4. 데이터 수신: 서버로부터 데이터를 받기 위해 recv() 함수를 사용합니다.
  5. 소켓 종료: 통신이 끝나면 close()로 소켓을 종료합니다.

다음은 간단한 클라이언트 코드 예제입니다:

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

int main() {
    int sock;
    struct sockaddr_in server_addr;
    char buffer[1024];

    // 소켓 생성
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("소켓 생성 실패");
        exit(1);
    }

    // 서버 주소 설정
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(12345);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 서버 연결
    if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("서버 연결 실패");
        exit(1);
    }

    // 데이터 송신
    strcpy(buffer, "Hello from client!");
    send(sock, buffer, strlen(buffer), 0);

    // 데이터 수신
    recv(sock, buffer, sizeof(buffer), 0);
    printf("서버: %s\n", buffer);

    // 소켓 종료
    close(sock);
    return 0;
}

서버-클라이언트 간 데이터 흐름


서버와 클라이언트는 send()recv() 함수를 사용하여 데이터를 주고받습니다. 클라이언트는 서버에 요청을 보내고, 서버는 이를 처리하여 응답을 보냅니다. 데이터 송수신 후, 클라이언트는 응답을 출력하고 소켓을 종료합니다.

서버와 클라이언트 프로그램을 함께 실행하면, 클라이언트는 서버에 “Hello from client!”를 보내고, 서버는 이를 받아 “Hello from server!”로 응답합니다. 이와 같이 간단한 서버-클라이언트 통신을 구현할 수 있습니다.

파일 전송을 위한 소켓 결합


C언어에서 파일을 네트워크를 통해 전송하려면 소켓을 사용하여 서버와 클라이언트 간에 파일을 송수신하는 방법을 결합해야 합니다. 이 방법을 사용하면 서버와 클라이언트 간에 데이터를 전송하는 것뿐만 아니라, 파일을 읽고 쓰는 작업도 동시에 처리할 수 있습니다. 여기서는 파일을 서버에서 클라이언트로 전송하는 예제를 다룹니다.

서버 프로그램: 파일 전송


서버는 클라이언트가 연결된 후, 파일을 읽고 이를 네트워크를 통해 클라이언트로 전송합니다. 이때 fopen()을 사용해 파일을 열고, send()를 사용하여 데이터를 전송합니다. 파일을 한 번에 전송하는 것이 아니라, 일정 크기만큼 버퍼를 사용해 나누어 전송하는 방식으로 구현할 수 있습니다.

다음은 파일을 클라이언트로 전송하는 서버 코드입니다:

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

#define CHUNK_SIZE 1024  // 파일을 한 번에 전송할 크기

int main() {
    int server_sock, client_sock;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[CHUNK_SIZE];
    FILE *file;
    size_t bytes_read;

    // 소켓 생성
    server_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock < 0) {
        perror("소켓 생성 실패");
        exit(1);
    }

    // 주소 설정
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(12345);

    // 주소 바인딩
    if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("바인딩 실패");
        exit(1);
    }

    // 클라이언트 연결 대기
    listen(server_sock, 5);

    // 클라이언트 연결 수락
    client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len);
    if (client_sock < 0) {
        perror("클라이언트 연결 수락 실패");
        exit(1);
    }

    // 전송할 파일 열기
    file = fopen("file_to_send.txt", "rb");
    if (file == NULL) {
        perror("파일 열기 실패");
        exit(1);
    }

    // 파일을 버퍼 단위로 읽어 클라이언트로 전송
    while ((bytes_read = fread(buffer, 1, CHUNK_SIZE, file)) > 0) {
        send(client_sock, buffer, bytes_read, 0);
    }

    // 파일 전송 후 파일 닫기
    fclose(file);
    printf("파일 전송 완료\n");

    // 소켓 종료
    close(client_sock);
    close(server_sock);
    return 0;
}

클라이언트 프로그램: 파일 수신


클라이언트는 서버로부터 파일을 수신하여 로컬 파일로 저장합니다. recv()를 사용하여 서버로부터 받은 데이터를 버퍼에 저장하고, 이를 로컬 파일에 기록합니다.

다음은 파일을 서버에서 받아서 로컬에 저장하는 클라이언트 코드입니다:

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

#define CHUNK_SIZE 1024  // 파일을 한 번에 받을 크기

int main() {
    int sock;
    struct sockaddr_in server_addr;
    char buffer[CHUNK_SIZE];
    FILE *file;
    size_t bytes_received;

    // 소켓 생성
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("소켓 생성 실패");
        exit(1);
    }

    // 서버 주소 설정
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(12345);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 서버 연결
    if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("서버 연결 실패");
        exit(1);
    }

    // 파일을 저장할 파일 열기
    file = fopen("received_file.txt", "wb");
    if (file == NULL) {
        perror("파일 열기 실패");
        exit(1);
    }

    // 서버로부터 데이터를 받아 파일로 저장
    while ((bytes_received = recv(sock, buffer, CHUNK_SIZE, 0)) > 0) {
        fwrite(buffer, 1, bytes_received, file);
    }

    // 파일 수신 후 파일 닫기
    fclose(file);
    printf("파일 수신 완료\n");

    // 소켓 종료
    close(sock);
    return 0;
}

서버와 클라이언트 간 파일 전송의 흐름


서버와 클라이언트 간의 파일 전송은 크게 다음과 같은 흐름으로 진행됩니다:

  1. 서버는 파일을 읽고 버퍼에 담은 후, send() 함수를 사용하여 클라이언트로 전송합니다.
  2. 클라이언트recv()를 사용하여 서버로부터 파일 데이터를 수신하고, 이를 로컬 파일로 저장합니다.
  3. 파일 전송이 완료되면 서버와 클라이언트는 각각 소켓을 종료하고, 리소스를 해제합니다.

이 방식은 네트워크를 통해 파일을 전송하는 기본적인 방법이며, 크기가 큰 파일을 전송할 때는 전송 속도나 오류 처리 등을 고려하여 더 효율적인 방법을 추가할 수 있습니다.

오류 처리와 예외 관리


파일 입출력 및 네트워크 프로그래밍에서는 여러 가지 오류가 발생할 수 있습니다. 예를 들어, 파일을 열 수 없거나, 네트워크 연결에 실패하는 경우 등이 있을 수 있습니다. 이러한 오류를 적절히 처리하고 예외를 관리하는 것이 중요합니다. 오류가 발생하면 프로그램이 예기치 않게 종료되거나, 데이터가 손실될 수 있기 때문에, 오류 처리는 안정적인 프로그램을 만드는 데 필수적입니다.

파일 입출력 오류 처리


파일을 다룰 때 발생할 수 있는 주요 오류에는 파일을 열 수 없거나, 읽기/쓰기 중 문제가 발생하는 경우가 있습니다. C언어에서 파일 관련 오류는 대부분 fopen(), fread(), fwrite(), fclose() 등의 함수 호출 후 반환 값을 체크함으로써 처리할 수 있습니다. 예를 들어:

FILE *file = fopen("example.txt", "r");
if (file == NULL) {
    perror("파일 열기 오류");
    exit(1);  // 파일을 열 수 없으면 프로그램 종료
}

파일을 열 때 NULL을 반환하면 파일이 존재하지 않거나 접근할 수 없음을 의미하며, perror() 함수를 사용하여 오류 메시지를 출력할 수 있습니다.

또한, fread()fwrite() 함수를 사용할 때에도 읽거나 쓸 수 있는 데이터의 양을 확인하고, 파일 작업이 성공적으로 수행되었는지 검사해야 합니다.

size_t bytes_read = fread(buffer, 1, 1024, file);
if (bytes_read == 0 && !feof(file)) {
    perror("파일 읽기 오류");
    exit(1);  // 파일을 읽을 수 없으면 오류 처리
}

네트워크 오류 처리


네트워크 프로그래밍에서는 연결 실패, 데이터 송수신 오류 등 여러 가지 오류가 발생할 수 있습니다. 예를 들어, 클라이언트가 서버에 연결하지 못하거나, 데이터를 전송할 때 문제가 발생하는 경우가 있습니다. 네트워크 관련 오류는 socket(), connect(), send(), recv() 등 함수 호출 후 반환 값을 체크하여 처리할 수 있습니다.

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("소켓 생성 오류");
    exit(1);  // 소켓 생성 실패 시 오류 처리
}

int status = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
if (status < 0) {
    perror("서버 연결 실패");
    exit(1);  // 서버에 연결할 수 없으면 오류 처리
}

socket()connect() 함수는 오류 발생 시 -1을 반환하며, perror()를 통해 오류 메시지를 출력할 수 있습니다.

파일과 네트워크 결합 시 오류 처리


파일 입출력과 네트워크 프로그래밍을 결합할 때는 두 가지의 오류를 동시에 처리해야 합니다. 예를 들어, 파일을 읽고 이를 네트워크를 통해 전송하는 경우, 파일을 열 수 없거나 소켓을 통해 데이터를 전송할 수 없는 상황이 발생할 수 있습니다. 이러한 경우에는 파일 오류와 네트워크 오류를 각각 처리하여 프로그램이 중단되지 않도록 해야 합니다.

FILE *file = fopen("file_to_send.txt", "rb");
if (file == NULL) {
    perror("파일 열기 오류");
    exit(1);
}

int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
    perror("소켓 생성 오류");
    fclose(file);  // 소켓 오류 발생 시 파일을 닫고 종료
    exit(1);
}

예외 관리


예외 처리는 프로그램이 비정상적으로 종료되지 않고, 오류가 발생했을 때 적절한 조치를 취하는 과정입니다. 예를 들어, 네트워크 연결이 끊어졌을 때 재연결을 시도하거나, 파일이 손상되었을 때 다른 파일을 사용하도록 유도할 수 있습니다. 예외 관리가 잘 되어 있으면 프로그램의 신뢰성과 안정성이 향상됩니다.

int retries = 3;
while (retries > 0) {
    int status = connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));
    if (status == 0) {
        break;  // 연결 성공
    } else {
        retries--;
        if (retries == 0) {
            perror("서버 연결 실패");
            exit(1);  // 재시도 횟수 초과 시 종료
        }
        sleep(1);  // 재시도 전 대기
    }
}

디버깅 및 로깅


디버깅과 로깅은 오류를 추적하고 문제를 해결하는 데 중요한 도구입니다. 오류 발생 시 디버깅 정보를 출력하거나 로그 파일에 기록하여, 나중에 문제를 분석할 수 있습니다. C언어에서는 fprintf(stderr, "오류 메시지\n"); 등을 사용하여 오류 메시지를 출력하거나, 로깅 라이브러리를 사용하여 파일에 기록할 수 있습니다.

fprintf(stderr, "소켓 연결 실패: %s\n", strerror(errno));

결론


파일 입출력과 네트워크 프로그래밍에서 오류 처리는 안정적인 프로그램을 구현하는 데 필수적인 요소입니다. 파일을 열 수 없거나 네트워크 연결에 실패하는 경우, 적절한 오류 메시지와 예외 처리를 통해 프로그램의 비정상 종료를 방지하고, 사용자에게 유용한 정보를 제공할 수 있습니다.

실습 예제: 서버-클라이언트 파일 전송


이 실습 예제에서는 서버와 클라이언트 간에 파일을 전송하는 기본적인 네트워크 프로그래밍 방법을 구현합니다. 이 예제를 통해 파일을 네트워크로 전송하는 데 필요한 소켓 프로그래밍의 기본 개념을 이해할 수 있습니다. 서버는 파일을 읽어서 클라이언트로 전송하고, 클라이언트는 서버로부터 받은 데이터를 로컬 파일에 저장합니다.

서버 코드: 파일 전송


서버는 클라이언트의 연결을 기다리고, 연결이 되면 파일을 읽고 데이터를 클라이언트로 전송합니다. 파일을 전송할 때는 데이터를 작은 블록으로 나누어 전송하는 방식으로 구현합니다.

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

#define CHUNK_SIZE 1024  // 한 번에 전송할 파일 크기

int main() {
    int server_sock, client_sock;
    struct sockaddr_in server_addr, client_addr;
    socklen_t client_len = sizeof(client_addr);
    char buffer[CHUNK_SIZE];
    FILE *file;
    size_t bytes_read;

    // 소켓 생성
    server_sock = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sock < 0) {
        perror("소켓 생성 실패");
        exit(1);
    }

    // 서버 주소 설정
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(12345);

    // 주소 바인딩
    if (bind(server_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("바인딩 실패");
        exit(1);
    }

    // 클라이언트 연결 대기
    listen(server_sock, 5);

    // 클라이언트 연결 수락
    client_sock = accept(server_sock, (struct sockaddr*)&client_addr, &client_len);
    if (client_sock < 0) {
        perror("클라이언트 연결 수락 실패");
        exit(1);
    }

    // 전송할 파일 열기
    file = fopen("file_to_send.txt", "rb");
    if (file == NULL) {
        perror("파일 열기 실패");
        exit(1);
    }

    // 파일을 버퍼 단위로 읽어 클라이언트로 전송
    while ((bytes_read = fread(buffer, 1, CHUNK_SIZE, file)) > 0) {
        send(client_sock, buffer, bytes_read, 0);
    }

    // 파일 전송 후 파일 닫기
    fclose(file);
    printf("파일 전송 완료\n");

    // 소켓 종료
    close(client_sock);
    close(server_sock);
    return 0;
}

클라이언트 코드: 파일 수신


클라이언트는 서버에 연결하고, 서버로부터 데이터를 수신하여 로컬 파일에 저장합니다. 클라이언트는 데이터를 수신하는 동안 파일을 열어 그 데이터를 기록합니다.

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

#define CHUNK_SIZE 1024  // 한 번에 받을 파일 크기

int main() {
    int sock;
    struct sockaddr_in server_addr;
    char buffer[CHUNK_SIZE];
    FILE *file;
    size_t bytes_received;

    // 소켓 생성
    sock = socket(AF_INET, SOCK_STREAM, 0);
    if (sock < 0) {
        perror("소켓 생성 실패");
        exit(1);
    }

    // 서버 주소 설정
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(12345);
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");

    // 서버 연결
    if (connect(sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0) {
        perror("서버 연결 실패");
        exit(1);
    }

    // 파일을 저장할 파일 열기
    file = fopen("received_file.txt", "wb");
    if (file == NULL) {
        perror("파일 열기 실패");
        exit(1);
    }

    // 서버로부터 데이터를 받아 파일로 저장
    while ((bytes_received = recv(sock, buffer, CHUNK_SIZE, 0)) > 0) {
        fwrite(buffer, 1, bytes_received, file);
    }

    // 파일 수신 후 파일 닫기
    fclose(file);
    printf("파일 수신 완료\n");

    // 소켓 종료
    close(sock);
    return 0;
}

서버-클라이언트 간 데이터 흐름

  1. 서버는 파일을 읽어서 send() 함수를 통해 클라이언트로 전송합니다.
  2. 클라이언트recv() 함수를 사용하여 서버로부터 데이터를 수신하고, 이를 fwrite() 함수를 통해 로컬 파일에 저장합니다.
  3. 파일 전송이 완료되면 서버와 클라이언트는 각각 소켓을 종료하고, 리소스를 해제합니다.

이 예제에서는 CHUNK_SIZE를 1024로 설정하여 파일을 작은 블록으로 나누어 전송합니다. 이렇게 함으로써, 파일의 크기와 관계없이 안정적으로 데이터를 전송할 수 있습니다.

요약


본 기사에서는 C언어에서 파일 입출력과 네트워크 프로그래밍을 결합하여 서버와 클라이언트 간에 파일을 전송하는 방법에 대해 설명했습니다. 서버는 파일을 읽고 이를 네트워크를 통해 클라이언트로 전송하며, 클라이언트는 서버로부터 파일을 받아 로컬에 저장합니다. 또한, 파일 전송을 위한 소켓 프로그래밍의 기초, 오류 처리, 예외 관리, 그리고 네트워크 통신에서 발생할 수 있는 다양한 문제를 해결하는 방법도 다루었습니다. 이러한 방법들을 통해, C언어를 이용한 파일 전송 프로그램을 효율적이고 안전하게 구현할 수 있습니다.