우당탕탕 개발일지

[C언어] libcurl 기반 FTP/SFTP 다운로드 구현 본문

Server/Linux, C

[C언어] libcurl 기반 FTP/SFTP 다운로드 구현

YUDENG 2026. 7. 7. 22:20
FTP (File Transfer Protocol) 란?

 

 

일반적으로 특정 서버에 접속해서 파일을 가져오거나, 반대로 내 로컬 장비의 파일을 서버로 전송할 때 FTP를 많이 사용한다. FTP란, 서버와 클라이언트 간 파일을 업로드하거나 다운로드할 때 사용하는 파일 전송 프로토콜이다.

 

예를 들어, 10.10.1.1 서버에서 10.10.3.101 서버에 접속하여 해당 서버 안에 있는 파일을 가져온다고 가정하면, 보통 다음과 같은 정보가 필요하다.

  • 접속 대상 서버 IP
  • 사용자 계정
  • 비밀번호
  • 접근 가능한 디렉토리 경로
  • 파일명
ftp 10.10.3.101
Name: user
Password: qwer

 

위 명령어를 입력하면 FTP 서버에 접속을 시도하며, 서버에서 사용자 인증을 요구하면 계정 정보를 입력한다.

인증이 정상적으로 완료되면 FTP 명령어를 사용할 수 있는 상태가 된다. 리눅스 쉘처럼 디렉토리 이동, 파일 목록 조회, 파일 업로드, 파일 다운로드와 같은 기본적인 작업을 수행할 수 있다.

 

로컬 서버에 있는 파일을 FTP 서버로 업로드하려면 put 명령어를 사용한다.

put 00_260701.tar

 

반대로 FTP 서버에 있는 파일을 로컬 서버로 다운로드하려면 get 명령어를 사용한다.

get template.yaml

 

 

FTP를 사용할 때 주의할 점은 기본적으로 계정 정보와 데이터가 암호화되지 않은 상태로 전송될 수 있다. 그래서 보안이 중요한 환경에서는 일반 FTP 보다 SFTP나 FTPS를 사용하는 경우가 많다.

 

 

SFTP (SSH File Transfer Protocol) 란?

 

SFTP 는 SSH를 기반으로 파일을 전송하는 프로토콜이다. 이름에 FTP가 들어가지만, 일반 FTP에 보안 기능을 추가한 방식이라기 보다는 SSH 연결 위에서 동작하는 별도의 파일 전송 방식으로 봐야한다.

 

SFTP는 SSH를 사용하여 서버와 클라이언트 간 통신을 암호화한다. 따라서 사용자 계정, 비밀번호, 전송되는 파일 내용이 암호화된 상태로 전달된다. 또한, SSH 프로토콜을 사용하므로 22번 포트를 사용한다. 접속 방식은 다음과 같다.

sftp user@10.10.3.101

 

 

curl

 

curl은 URL을 이용해서 서버와 데이터를 주고받는 클라이언트 도구이다.

  • FTP / SFTP → 파일 전송 프로토콜
  • curl → 프로토콜을 사용해서 요청을 보내는 도구

 

이전 FTP, SFTP 명령어들은 대화형 방식으로 사용자가 서버에 접속한 뒤 명령어를 하나씩 입력한다. 반면, curl은 보통 한 줄 명령으로 하나의 작업을 처리한다.

 

예를 들어, 다음과 같이 HTTP 요청을 보낼 수 있으며,

curl http://example.com

 

다음과 같이 FTP 파일 다운로드나 SFTP 파일 다운로드도 수행할 수 있다. 여기서 -O 옵션은 원격 서버에 있는 파일명을 그대로 사용해서 로컬에 저장하는 옵션이다.

curl -u user:pswd -O ftp://10.10.3.101/test.zip
curl -u user:pawd -O sftp://10.10.3.101/test.zip

 

즉, curl은 http://, ftp://, sftip:// 같은 URL 스킴을 보고 어떤 프로토콜을 쓸지 결정한다.

 

 

libcurl 라이브러리

 

C 프로그램에서는 FTP / SFTP 를 직접 구현하려면 복잡하기 때문에 보통 libcurl 이라는 라이브러리를 사용한다.

  • curl → 터미널에서 사용하는 명령어
  • libcurl → C 프로그램에서 사용하는 라이브러리

 

C에서 FTP / SFTP 를 직접 구현하려면, 다음과 같은 작업을 모두 직접 처리해야 한다.

소켓 생성 → 서버 연결 → 로그인 처리 → FTP 명령 전송 → 데이터 포트 처리 → 파일 수신 → 에러 처리 → 타임아웃 처리

 

libcurl 기본 사용 흐름

 

libcurl을 사용할 때 기본 흐름은 다음과 같다.

1. curl_global_init() 
2. curl_easy_init() 
3. curl_easy_setopt() 
4. curl_easy_perform() 
5. curl_easy_cleanup() 
6. curl_global_cleanup()

 

curl_global_init()

 

#include <curl/curl.h>
 
CURL *curl_easy_init();

 

curl_global_init()은 libcurl 전체 초기화 함수이다. 프로그램에서 libcurl을 사용하기 전 최소 한 번 호출하는 용도이다. 일반적으로 다음과 같이 사용한다.

CURLcode rc = CURLE_OK;

rc = curl_global_init(CURL_GLOBAL_DEFAULT);
if ( rc != CURLE_OK ) 
{
    printf("curl_global_init failed (%s)\n", curl_easy_strerror(rc));
    return -1;
}

 

 

libcurl에서는 CURL 자료형을 사용한다. CURL은 하나의 요청 작업을 관리하는 핸들 타입이다. 사용자는 CURL * 포인터 변수를 만들고, 이 핸들에 URL이나 계정과 같은 옵션을 설정한다.

CURL *curl;

 

 

curl_easy_setopt()
#include <curl/curl.h>
 
CURLcode curl_easy_setopt(CURL *handle, CURLoption option, parameter);


curl_easy_setopt()는 libcurl 요청에 필요한 옵션을 설정하는 함수이다.

curl_easy_setopt() 옵션은 300개 이상이며, 가장 자주 쓰는 기본 옵션들은 다음과 같다.

 

  • CURLOPT_URL : 요청할 URL 지정
  • CURLOPT_USERNAME: 인증에 사용할 사용자명 지정
  • CURLOPT_PASSWORD: 인증에 사용할 비밀번호 지정
  • CURLOPT_USERPWD: 사용자명과 비밀번호를 한 번에 넣는 방식

즉, 아래 두 방식은 비슷한 의미이다.

curl_easy_setopt(curl, CURLOPT_USERNAME, "user");
curl_easy_setopt(curl, CURLOPT_PASSWORD, "pswd");
curl_easy_setopt(curl, CURLOPT_USERPWD, "user:pswd");

 

 

  • CURLOPT_TIMEOUT: 전체 요청에 대한 제한 시간 설정

예를 들어, 30초 안에 전체 요청이 성공이든 실패처리든 끝나게 하려면 다음과 같이 설정할 수 있다.

curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L);
  • CURLOPT_CONNECTTIOMOUT: 서버 연결 단계에 대한 제한 시간 설정

예를 들어, 서버 연결 시간을 10초로 제한하려면 다음과 같이 설정한다.

curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 10L);

 

  • CURLOPT_WRITEDATA: 서버에서 받은 데이터를 어디에 저장할지 지정

파일로 바로 저장할 때 사용하는 옵션으로, libcurl의 기본 write 함수가 사용된다.

  • CURLOPT_WRITEFUNCTION

받은 데이터를 직접 처리하고 싶을 때 사용하는 옵션이다.

 

#define CURL_SET_OPT(handle, opt, param) \
    do { \
        if((rc = curl_easy_setopt(handle, opt, param)) != CURLE_OK) { \
            printf("curl_easy_setopt fail.(%d, %s)\n", rc, curl_easy_strerror(rc)); \
            goto free_return; \
        } \
    } while (0)

CURL_SET_OPT(curl, CURLOPT_NOPROGRESS, 1L);
CURL_SET_OPT(curl, CURLOPT_URL, "ftp://10.10.3.101/test.zip");
CURL_SET_OPT(curl, CURLOPT_USERPWD, "user:pswd");
CURL_SET_OPT(curl, CURLOPT_CONNECTTIMEOUT, 10L);
CURL_SET_OPT(curl, CURLOPT_TIMEOUT, 60L);
CURL_SET_OPT(curl, CURLOPT_WRITEDATA, fp);

 

HTTP 관련 설정 옵션

 

흔히 많이 사용하는 HTTP 프로토콜 또한 설정을 통해 curl로 요청이 가능하다. 기본적으로 libcurl 요청 메소드는 GET으로 설정되어 있다. 

  • CURLOPT_POST: POST 요청 사용
  • CURLOPT_CUSTOMREQUEST: 요청 메소드를 직접 지정

 

  • CURLOPT_HTTPHEADER: HTTP 헤더 추가
char str_content_type_hdr[] = "Content-Type: application/json";

pst_hdr_list = curl_slist_append(pst_hdr_list, str_content_type_hdr);
if (pst_hdr_list == NULL)
{
	printf("curl_slist_append fail (Content-Type)\n");
	goto curl_end;
}

CURL_SET_OPT(curl, CURLOPT_HTTPHEADER, pst_hdr_list);

curl_easy_perform(curl);

if (pst_hdr_list != NULL)
	curl_slist_free_all(pst_hdr_list);

 

요청이 끝난 후에는 추가한 헤더 리스트를 해제해야 한다.

 

  • CURLOPT_POSTFIELDS: HTTP body 데이터 지정 
  • CURLOPT_POSTFIELDSIZE: body 데이터의 길이

JSON 데이터를 body에 담아서 전송하려면 다음과 같이 설정할 수 있다.

char str_body[] = "{\"id\":1,\"name\":\"test\"}";

CURL_SET_OPT(curl, CURLOPT_POSTFIELDS, str_body);
CURL_SET_OPT(curl, CURLOPT_POSTFIELDSIZE, strlen(str_body));

 

 

curl_easy_perform()
#include <curl/curl.h>
 
CURLcode curl_easy_perform(CURL *easy_handle);

 

curl_easy_perform() 은 앞서 설정된 옵션을 바탕으로 실제 요청을 실행하는 함수이다. curl_easy_perform() 을 호출해야 실제 FTP GET 과 같은 명령어가 수행되는 것이다.

CURLcode rc = CURLE_OK;

rc = curl_easy_perform(curl);
if ( rc != CURLE_OK ) 
{
    printf("curl_easy_perform failed (%s)\n", curl_easy_strerror(rc));
    return -1;
}

 

실패한 경우, CURLE_OK가 아닌 값이 반환되며, curl_easy_strerror() 를 사용하면 에러 내용을 문자열로 확인할 수 있다.

 

파일 저장 관련 옵션

 

바이너리 파일을 가져올 때, 데이터를 저장하는 방식은 크게 2가지 이다.

방식 메모리 사용
FTP/SFTP → malloc(file_size)에 직접 저장
FTP/SFTP → 임시 파일 → 마지막에 malloc(file_size) 중간
FTP/SFTP → chunk 단위 처리 작음
임시 파일 → fread() chunk 단위 처리 작음

 

 

1. 파일로 저장

 

가장 단순한 방법이지만, 디스크 I/O가 발생하게 된다.

int main(void)
{
    char                        str_remote_file_path[MAX_FILEPATH_LEN] = "";
    char                        str_local_file_path[MAX_FILEPATH_LEN] = "";
    FILE                        *fp = NULL;
    size_t                      sz_read_len = 0;
    
    /* . . . set file path ... */
    
    /* SFTP download to local temp file */
    fp = fopen(str_local_file_path, "wb");
    if( !fp )
    {
        printf("fopen fail (local:%s, errno:%d)\n", str_local_file_path, errno);
        goto send_resp;
    }

    rc = get_ftp_file(str_remote_file_path, fp, &sz_read_len);
    fclose(fp);
    fp = NULL;

    if( rc < 0 )
    {
        printf("get_ftp_file fail (remote:%s)\n", str_remote_file_path);
        goto send_resp;
    }
}
int get_ftp_file(const char *pstr_remote_file_path, FILE *fp, size_t *psz_read_len)
{
    FILE *fp = NULL;
    fp = fopen(pstr_remote_file_path, "wb");
  
    // ... set option ...
  
    CURL_SET_OPT(curl, CURLOPT_WRITEFUNCTION, write_callback);
    CURL_SET_OPT(curl, CURLOPT_WRITEDATA, fp);
    
    curl_easy_perform(curl);
    
    fflush(fp);
    
    if( fseek(fp, 0, SEEK_END) < 0 )
    {
    	printf("fseek fail (remote:%s, errno:%d)\n", pstr_remote_file_path, errno);
        goto free_return;
    }
    
    i_file_size = ftell(fp);
    if( l_file_size <= 0 )
    {
    	printf("downloaded file size is zero. filepath[%s]\n", pstr_remote_file_path);
        goto free_return;
    }
    
    if( fseek(fp, 0, SEEK_SET) < 0 )
    {
    	printf("fseek fail (remote:%s, errno:%d)\n", pstr_remote_file_path, errno);
        goto free_return;
    }
    
    *psz_read_len = (size_t)l_file_size;
  
free_return:
    if( curl )
        curl_easy_cleanup(curl);
        
    return 0;
}
size_t write_callback(void *ptr, size_t size, size_t nmemb, void *userdata)
{
    size_t  i_real_size = size * nmemb;
    FILE    *fp = (FILE *)userdata;

    if( !ptr || !fp || i_real_size == 0 )
        return 0;

    return fwrite(ptr, size, nmemb, fp);
}

 

파일을 사용하는 경우, 불가피하게 메모리를 절약하는 방법은 다음과 같이 마지막에 메모리를 할당해주는 것이다.

int main(void)
{
    unsigned char	*puc_file_data = NULL;

    /* Local file -> send buf */
    rc = _read_local_file(str_local_file_path, &puc_file_data, &sz_read_len);
    if( rc < 0 || !puc_file_data || sz_read_len == 0 )
    {
    	printf("_read_local_file fail (local:%s)\n", str_local_file_path);
        goto send_resp;
    }

    /* SEND */
    rc = send_resp((int)sz_read_len, (char *)puc_file_data);
    
    free(puc_file_data);
    puc_file_data = NULL;
    unlink(str_local_file_path);
}
static int _read_local_file(const char *pstr_file_path,
        unsigned char **ppuc_data, size_t *psz_len)
{
    FILE            *fp = NULL;
    struct stat     st;
    unsigned char   *puc_buf = NULL;
    size_t          sz_read = 0;

    if( !pstr_file_path || !ppuc_data || !psz_len )
        return -1;

    *ppuc_data = NULL;
    *psz_len = 0;

    if( stat(pstr_file_path, &st) < 0 || st.st_size <= 0 )
        return -1;

    puc_buf = (unsigned char *)malloc((size_t)st.st_size);
    if( !puc_buf )
        return -1;

    fp = fopen(pstr_file_path, "rb");
    if( !fp )
    {
        free(puc_buf);
        return -1;
    }

    sz_read = fread(puc_buf, 1, (size_t)st.st_size, fp);
    fclose(fp);

    if( sz_read != (size_t)st.st_size )
    {
        free(puc_buf);
        return -1;
    }

    *ppuc_data = puc_buf;
    *psz_len = sz_read;

    return 0;
}

 

 

2. 메모리에 저장

 

메모리 크기가 작아서 별 영향이 없다면, 바로 메모리에 저장하는 방식이 가장 간편하고 효율적이다.

CURLOPT_WRITEFUNCTION 옵션을 사용하면 서버로부터 데이터를 받을 때 호출할 콜백 함수를 지정할 수 있다.

typedef struct 
{
    unsigned char   *puc_data;
    size_t          i_size;
} st_sftp_mem_t;

int get_ftp_file(const char *pstr_remote_file_path, st_sftp_mem_t *pst_mem)
{
  // ... set option ...
  
  CURL_SET_OPT(curl, CURLOPT_WRITEFUNCTION, write_callback);
  CURL_SET_OPT(curl, CURLOPT_WRITEDATA, pst_mem);
  
  curl_easy_perform(curl);

free_return:
    if( curl )
        curl_easy_cleanup(curl);
        
    return 0;
}
size_t write_callback(void *ptr, size_t size, size_t nmemb, void *userdata)
{
    /*
        ptr       : 받은 데이터가 들어있는 버퍼
        size      : 데이터 단위 크기, 보통 1
        nmemb     : 데이터 개수
        userdata  : CURLOPT_WRITEDATA로 넘긴 사용자 데이터
    */
    
    size_t real_size = size * nmemb;
    st_sftp_mem_t *pst_mem = (st_sftp_mem_t *)userdata;
    unsigned char *puc_new_data = NULL;

    if( !ptr || !pst_mem || i_real_size == 0 )
        return 0;

    puc_new_data = realloc(pst_mem->puc_data, pst_mem->i_size + i_real_size);
    if( !puc_new_data )
        return 0;

    pst_mem->puc_data = puc_new_data;
    memcpy(pst_mem->puc_data + pst_mem->i_size, ptr, i_real_size);
    pst_mem->i_size += i_real_size;

    return real_size;
}

 

 

728x90