웬디의 기묘한 이야기

글 작성자: WENDYS
반응형

 

복잡한 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를 통해 현재 얼마만큼의 데이터를 읽을 수 있는지 확인하도록 합니다.

    • 마지막으로 다운로드 받은 파일을 로컬 파일로 저장하도록 합니다.

 

 

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;
            
}

 

 

반응형