[C/C++] InternetReadFile Simple File Download.
복잡한 Socket 통신을 하지 않고 인터넷 URL 또는 Network에 공유된 파일을 다운로드하려면 어떻게 하면 좋을까요? MSDN에 보시면 아주 간단하게 사용할 수 있는 InternetReadFile API를 제공하고 있습니다. 해당 API는 파일을 다룰 때 사용하는 ReadFile과 매우 유사하게 동작하기 때문에 파일을 조금 다루어보셨다면 어렵지 않게 다룰 수 있을 거예요.
사용 전 가장 먼저 해주어야 할 행동은 역시 header를 include 해주는 것인데요, wininet을 사용하므로 lib로 링크해주어야 합니다. 이제 아주 간단한 방법으로 URL에 있는 파일을 로컬 파일로 저장하도록 해봅시다.
주의 : wininet을 사용하는 경우 서버 또는 service process에는 사용하면 안 됩니다. 서버 또는 service에서 구현하는 경우 Microsoft Windows HTTP Services (WinHTTP)를 이용해야 합니다.
file_donwloader.hpp
class header는 간단하게 initialize()를 이용하여 HINTERNET을 초기화시켜주도록 하고 download를 통해 url 정보를 save_path\save_file_name 형태로 저장하도록 구성했습니다. 물론 소멸자에선 HINTERNET Handle을 종료해주어야겠죠?
//
// header include
//
#include <wininet.h>
//
// lib link.
//
#pragma comment(lib, "Wininet.lib")
//
// file_donwloader.
//
class file_donwloader {
public:
file_donwloader();
~file_donwloader();
public:
bool download(
__in std::wstring url,
__in std::wstring save_path,
__in std::wstring save_file_name
);
private:
void initialize();
private:
HINTERNET _internet_handle;
};
file_downloader.cpp
간단한 샘플을 작성해보았습니다. 동기 형태로 동작하며, 지정된 URL 파일을 로컬 파일로 저장하는 코드입니다. 처음엔 조금 복잡해보일수도 있지만 예외처리가 들어가있어서 그래보이는것일 뿐 정말 간단한 코드 입니다.
#include "file_downloader.hpp"
file_donwloader::file_donwloader() {
_internet_handle = nullptr;
initialize();
}
file_donwloader::~file_donwloader() {
/*
생성한 Handle은 반드시 소멸해주어야하기때문에 잊지않도록 소멸자에 등록합니다.
*/
if (nullptr != _internet_handle) {
InternetCloseHandle(_internet_handle);
_internet_handle = nullptr;
}
}
void file_donwloader::initialize() {
/*
InternetOpen API를 이용하여 HINTERNET Handle을 초기화 합니다.
Agent값은 아무런 값이나 입력해도 되며, HTTP 프로토콜에서는 사용자 Agent로 사용되기도 합니다.
*/
_internet_handle = InternetOpen(
L"file_donwload",
INTERNET_OPEN_TYPE_DIRECT,
nullptr,
nullptr,
0
);
if (nullptr == _internet_handle) {
__debug_printf_trace("InternetOpen failed.");
}
}
bool file_donwloader::download(
__in std::wstring url,
__in std::wstring save_path,
__in std::wstring save_file_name
) {
bool result = false;
do {
/*
넘어온 parameter는 verify check를 해주어야겠죠? 필수 입력값이기때문에
셋중에 하나라도 미입력시 정상적으로 파일을 다운로드하지 못하거나 저장하지 못할 수 있습니다.
*/
if (nullptr == _internet_handle) {
__debug_printf_trace("_internet_handle nullptr.");
break;
}
if (url.empty()) {
__debug_printf_trace("empty url.");
break;
}
if (save_path.empty()) {
__debug_printf_trace("empty save_path.");
break;
}
if (save_file_name.empty()) {
__debug_printf_trace("save_file_name save_path.");
break;
}
/*
InternetOpenUrl API를 이용하여 실제 접속이 시작됩니다.
캐시가 아닌 원본파일을 항상 다운로드받기위해 Flag는 INTERNET_FLAG_RELOAD를 주었습니다.
다른 Flag를 설정하고싶은경우 MSDN을 참조하시면 됩니다.
*/
const HINTERNET open_url_handle = InternetOpenUrl(
_internet_handle,
url.c_str(),
nullptr,
0,
INTERNET_FLAG_RELOAD,
0
);
if (nullptr == open_url_handle) {
__debug_printf_trace("_open_url_handle nullptr.");
break;
}
/*
해당 URL에 404, 500 등 http error가 넘어오더라도 정상적인 URL정보이기때문에 InternetOpenUrl의 HINTERNET Handle이 넘어오게 됩니다.
그렇기때문에 NULL 또는 INVALID_HANDLE 정보로는 에러 여부를 확인할 수 없으므로
HttpQueryInfo API를 이용하여 HTTP_QUERY_STATUS_CODE를 확인합니다.
HTTP_STATUS_OK가 넘어왔다면 접속이 성공한것이니 이제 파일로 저장하기만 하면 됩니다.
*/
DWORD status_code = 0;
DWORD status_code_size = sizeof(status_code);
if (FALSE == HttpQueryInfo(
open_url_handle,
HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER,
&status_code,
&status_code_size,
nullptr
)) {
__debug_printf_trace("HttpQueryInfo failed (%d)", ::GetLastError());
break;
}
if (sizeof(status_code) != status_code_size) {
__debug_printf_trace("status_code != status_code_size failed.");
break;
}
if (HTTP_STATUS_OK != status_code) {
__debug_printf_trace("error status_code (%d).", status_code);
break;
}
//
// make save file name.
//
std::wstring file_name;
file_name.assign(save_path);
file_name.append(L"\\");
file_name.append(save_file_name);
const int max_buffer_size = 2014;
char buffer[max_buffer_size] = {};
CFile save_file;
if (save_file.Open(
file_name.c_str(),
CFile::modeCreate | CFile::modeReadWrite
)) {
//
// set result.
//
result = true;
DWORD number_of_bytes_read = 0;
do {
/*
여기가 가장 중요한 부분인데요 네트워크 통신이라는게 패킷단위로 왔다갔다 하다보니 한번의 통신으로 파일을 받을 수 없습니다.
용량이 아주 작은 통신 패킷이라면 한번에 받기도 하겠지만 지정된 버퍼보다 큰 사이즈 또는 네트워크 상태에 따라 정해지지 않은 크기를 주고받기때문에
InternetQueryDataAvailable API를 통해 현재 얼마만큼의 데이터를 읽을 수 있는지 확인하도록 합니다.
*/
DWORD read_available = 0;
if (FALSE == InternetQueryDataAvailable(
open_url_handle,
&read_available,
0,
0
)) {
result = false;
__debug_printf_trace("InternetQueryDataAvailable failed.");
break;
}
// resize buffer.
if (read_available > max_buffer_size) {
read_available = max_buffer_size;
}
//
// internet read file.
//
if (FALSE == InternetReadFile(
open_url_handle,
buffer,
read_available,
&number_of_bytes_read
)) {
result = false;
__debug_printf_trace("InternetReadFile failed.");
break;
}
if (0 < number_of_bytes_read) {
//
// write file.
//
save_file.Write(buffer, number_of_bytes_read);
memset(buffer, 0x00, number_of_bytes_read);
}
} while (0 != number_of_bytes_read);
save_file.Close();
}
InternetCloseHandle(open_url_handle);
} while (false);
return result;
}
코드가 복잡하지 않으니 하나하나 따라가 보겠습니다.
- initialize() - 생성자에서 호출.
- InternetOpen API를 이용하여 HINTERNET Handle을 초기화합니다. Agent값은 아무런 값이나 입력해도 되며, HTTP 프로토콜에서는 사용자 Agent로 사용되기도 합니다.
- InternetCloseHandle() - 소멸자에서 호출
- 생성한 Handle은 반드시 소멸해주어야 하기 때문에 잊지 않도록 소멸자에 등록합니다.
- download()
- 넘어온 parameter는 verify check를 해주어야겠죠? 필수 입력값이기 때문에 셋 중에 하나라도 미입력시 정상적으로 파일을 다운로드하지 못하거나 저장하지 못할 수 있습니다.
- InternetOpenUrl API를 이용하여 실제 접속이 시작됩니다. 캐시가 아닌 원본 파일을 항상 다운로드하기 위해 Flag는 INTERNET_FLAG_RELOAD를 주었습니다. 다른 Flag를 설정하고 싶은 경우 여기(MSDN)를 참조하시면 됩니다.
- 해당 URL에 404, 500 등 http error가 넘어오더라도 정상적인 URL 정보이기 때문에 InternetOpenUrl의 HINTERNET Handle이 넘어오게 됩니다. 그렇기 때문에 NULL 또는 INVALID_HANDLE 정보로는 에러 여부를 확인할 수 없으므로 HttpQueryInfo API를 이용하여 HTTP_QUERY_STATUS_CODE를 확인합니다. HTTP_STATUS_OK가 넘어왔다면 접속이 성공한 것이니 이제 파일로 저장하기만 하면 됩니다.
- 여기가 가장 중요한 부분인데요 네트워크 통신이라는 게 패킷단위로 왔다 갔다 하다 보니 한 번의 통신으로 파일을 받을 수 없습니다. 용량이 아주 적은 통신 패킷이라면 한 번에 받기도 하겠지만 지정된 버퍼보다 큰 사이즈 또는 네트워크 상태에 따라 정해지지 않은 크기를 주고받기 때문에 InternetQueryDataAvailable API를 통해 현재 얼마만큼의 데이터를 읽을 수 있는지 확인하도록 합니다.
- 마지막으로 다운로드 받은 파일을 로컬 파일로 저장하도록 합니다.
- 넘어온 parameter는 verify check를 해주어야겠죠? 필수 입력값이기 때문에 셋 중에 하나라도 미입력시 정상적으로 파일을 다운로드하지 못하거나 저장하지 못할 수 있습니다.
use sample
사용법도 간단합니다. 클래스 변수를 선언한 후 url, save path, file name을 지정하기만 하면 끝!
file_donwloader downloader;
if (false == downloader.download(
url.GetString(),
save_path.GetString(),
file_name.GetString()
)) {
CString message;
message.Format(L"파일 다운로드 실패!");
MessageBox(message.GetString());
break;
}
'⌨ DEVELOPMENT > C++' 카테고리의 다른 글
[C/C++] clock vs gettimeofday Linux와 Windows의 코드 실행 시간 측정 방법 (6) | 2020.05.03 |
---|---|
[Solved] Resolver error: The VS Code Server failed to start (3) | 2019.12.14 |
[C/C++] Get Process Name by Process Id 3가지 방법 (0) | 2019.12.12 |
[MFC] Dialog based - class name 지정하는 간단한 방법 (3) | 2019.09.08 |
[C++] Boost ASIO를 이용한 안전한 TCP/IP 비동기 소켓 서버 예제 (5) | 2019.08.07 |
[MFC] Dialog Load GIF image (5) | 2019.07.28 |
[C++] How to use GDIPlus Library in c++ (0) | 2019.07.28 |
[MFC] 다이얼로그의 윈도우 소멸자 호출 순서 (1) | 2019.07.28 |
댓글
이 글 공유하기
다른 글
-
[C/C++] clock vs gettimeofday Linux와 Windows의 코드 실행 시간 측정 방법
[C/C++] clock vs gettimeofday Linux와 Windows의 코드 실행 시간 측정 방법
2020.05.03 -
[Solved] Resolver error: The VS Code Server failed to start
[Solved] Resolver error: The VS Code Server failed to start
2019.12.14 -
[C/C++] Get Process Name by Process Id 3가지 방법
[C/C++] Get Process Name by Process Id 3가지 방법
2019.12.12 -
[MFC] Dialog based - class name 지정하는 간단한 방법
[MFC] Dialog based - class name 지정하는 간단한 방법
2019.09.08