2011. 1. 27. 10:15

비동기(asynchronous) Http File Downloader 공유

(r1 : 2012/10/04)


비동기 Http File download를 지원하는 CHttpFileDownloader class를 공유합니다.
물론 MFC를 사용합니다.
비동기로 진행하기 때문에, Download 함수는 바로 리턴되며, 다운로드가 완료되면 지정된 callback 함수를 호출하게 됩니다. 물론, 동기 함수 즉 Download 완료때까지 block되는 함수도 함께 제공됩니다. 내부적으론, CInternetSession MFC class와 CHttpFile MFC class를 사용합니다.

비동기 함수 특징상, 다운로드 완료되기 전에 또 해당 class instance로 다운로드 함수를 호출할 수 있습니다. 내부적으로 semaphore를 사용하여, 하나의 instance에서 하나의 download만 진행하도록 하였습니다. 즉, 이전 작업 완료 전에 다른 작업이 시도되면, 앞 작업이 완료될 때 까지 Download 함수는 block됩니다. 만일 내부 코드를 수정하면, 동시에 몇개의 다운로드를 진행할 수도 있을 겁니다. 하지만 여러개의 Download를 동시에 해야 한다면, 되도록 그 개수만큼 class intance를 생성하시기 바랍니다.

지원 API는 다음과 같습니다.

DWORD DownloadToFile
IN LPCTSTR lpszUrl
IN LPCTSTR lpszFilePath
IN DWORD dwInternalBufCbSize
IN OPTIONAL HANDLE hEventCompletion
IN OPTIONAL pfnCbHttpFileDownloader pfnCb
IN OPTIONAL LPVOID pContext
IN OPTIONAL CWnd* pcWnd
IN OPTIONAL UINT uMsgIdProgress
IN OPTIONAL UINT uMsgIdComplete
IN OPTIONAL WPARAM wParam

리턴값 : Win32 Error Code (다운로드의 성공/실패를 의미하지 않음)
lpszUrl : 다운로드할 URL. .html/.php 등도 지원되며, https://도 지원됨
lpszFilePath : 저장할 파일 경로
dwInternalBufCbSize : 내부에서 사용할 버퍼 크기. 즉 다운로드를 받을 때 사용할 버퍼 크기. 보통 1024.
hEventCompletion : 다운로드가 종료될 때 SetEvent를 수신받을 Event Handle. NULL 가능.
pfnCb : 다운로드 진행 상황과 종료 결과를 수신할 callback 함수. NULL 가능
pContext : callback context. NULL 가능
pcWnd : 메시지를 수신받을 윈도우. NULL 가능
uMsgIdProgress : 다운로드 진행 상황을 받을 Window User Message Id. 0 가능. lParam은 %
uMsgIdComplete : 다운로드 완료 상황을 받을 Window User Message Id. 0 가능. lParam은 0(실패)/1(성공)
wParam : 다운로드 진행 상황/완료 수신받을 때 전달 받을 wParam. 0 가능

위 함수는 바로 리턴되는 함수로, 다운로드 완료 시점을 알 기 위해서는 pfnCb나 hEventCompletion을 이용할 수 있습니다. hEventCompletion을 이용하는 방법은 다음과 같습니다.

HANDLE hEvent = ::CreateEvent(NULL, TRUE, FALSE, NULL);
cDownload.DownloadToFile(..., hEvent, ...);
::MsgWaitForMultipleObjects(1, &hEvent, ...);

와 같습니다. 즉, DownloadToFile(...)이 non-blocking 함수인데, 위와 같이 completion event를 이용하면, blocking 효과를 줄 수 있다는 겁니다. 그리고 Message Pumping 하면서 Wait 하기 때문에, UI가 Pending되지 않습니다.

그리고 비동기 처리를 위해 내부에서 Main Thread와 Download Thread 두개가 생성되니 유의 하시기 바랍니다.

callback 함수의 prototype은 다음과 같습니다.
typedef BOOL (CALLBACK *pfnCbHttpFileDownloader)
IN CHttpFileDownloader::TYPE_CALLBACK_DATA nType
IN DWORD dwData
IN LPVOID pContext

즉, callback type (진행 정도 혹은 다운로드 결과)과 data (% 혹은 리턴 결과값)이 들어옵니다.
BOOL CALLBACK CallbackFunction(CHttpFileDownloader::TYPE_CALLBACK_DATA, DWORD, LPVOID)
{
   ...
   return TRUE;
}

와 같이 선언된 함수의 pointer를 전달하실 수 있습니다. 만일 callback 함수에서 FALSE가 리턴되면, 다운로드가 중단됩니다.

DWORD StopCurrentJob
IN DWORD dwTimeout

는 현재 진행중인 download를 중단하는 함수입니다. 만일 현재 작업으로 인해 대기중인 다운로드 작업이 있다면, 다음으로 진행됩니다. 물론, 본 함수는 callback context에서 호출해서는 안됩니다. Win32 Error Code가 리턴됩니다.

VOID Close
IN DWORD dwTimeout

위 함수는 현재 진행중인 download와 대기중인 작업 모두 중단시킵니다. 그리고, 더이상 class instance는 다운로드 작업을 진행할 수 없습니다. 본 class의 파괴자에서 자동으로 호출됩니다.

static DWORD DownloadToFileStatic
IN LPCTSTR lpszUrl
IN LPCTSTR lpszFilePath
IN DWORD dwInternalBufCbSize
IN OPTIONAL pfnCbHttpFileDownloader pfnCb
IN OPTIONAL LPVOID pContext
IN OPTIONAL CWnd* pcWnd
IN OPTIONAL UINT uMsgIdProgress
IN OPTIONAL UINT uMsgIdComplete
IN OPTIONAL WPARAM wParam

위 함수는 동기화 함수로 다운로드가 완료될 때 까지 블럭됩니다. static 함수이므로, class intance 선언이 필요없이 다음과 같이 그냥 사용 가능합니다.
CHttpFileDownloader::DownloadToFileStatic(TEXT(http://www....), TEXT("C:\tmp.html"), ...);

argument는 DownloadToFile(...)을 참고하시기 바랍니다.

눈치 빠르신 분은 확인하실 수 있겠지만, 실제 DownloadToFile(...) 내부에서는 DownloadToFileStatic을 사용합니다. 혹시 수정 사항이 있다면, DownloadToFileStatic 함수를 수정하면 됩니다.

* 2012/10/04

새로운 r1을 공유합니다.
 
기존 uMsgId에서 uMsgIdProgress와 uMsgIdComplete로 구별하였습니다. 즉, UI 개발시 Message로 완료/실패 여부를 알 수 있도록 하였습니다. 기존에는 Progress만으로 완료를 구별할 수 없더군요... (100%가 여러번 들어올 수 있음)

그리고, lpszFilePath에서 경로가 존재하지 않는 경우, 경로를 만들어주도록 하였습니다. 즉, caller는 경로의 존재 여부를 신경쓸 필요 없이 호출하면 알아서 필요할 때 마다 경로를 만들도록 하였습니다.