| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 | 31 |
- redisReply
- Publish
- DPDK 초기화
- redisGetReply
- redis
- kni
- mbuf
- Descriptor Ring
- HugePage
- Kernel Bypass
- DPDK EAL
- SR-IOV
- DPDK Architecture
- polling
- pmd
- dpdk
- Pubsub
- rte_eth_dev
- lcore
- hiredis
- Mempool
- nic
- Subscribe
- Today
- Total
우당탕탕 개발일지
[C언어] libcurl 기반 FTP/SFTP 다운로드 구현 본문
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;
}
'Server > Linux, C' 카테고리의 다른 글
| [C언어] Redis를 활용한 Pub/Sub 기능 (0) | 2026.07.27 |
|---|---|
| [K8S] C 기반 HTTP 통신 구현 및 Kubernetes 배포 (0) | 2026.07.12 |
| [C언어] IPC(2) (feat. 메세지 큐) (0) | 2025.10.19 |
| [C언어] IPC(1) (feat. 공유 메모리) (0) | 2025.10.19 |
| [C언어] 시그널 (feat. Sigaction) (0) | 2025.09.30 |