[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
댓글을 사용할 수 없습니다.