[WinAPI] FTP 클라이언트 프로그램 만들기(CFtpConnection) – 3

FTP 관련 마지막으로 CFtpConnection를 사용하여 FTP 클라이언트 프로그램을 만들어 보겠습니다.

WinAPI에서는 CInternetSession과 CFtpConnection으로 FTP 프로그램을 정말 쉽게 만들 수 있습니다.
CFtpConnection의 멤버 함수 설명은 아래와 같습니다.

멤버 함수설명
CommandFTP 서버에 직접 명령을 보냄
SetCurrentDirectory현재 FTP 디렉터리 설정
GetCurrentDirectory현재 디렉터리를 가져옴
GetCurrentDirectoryAsURL현재 디렉터리를 가져옴(URL)
CreateDirectory서버에 디렉터리를 만듦
RemoveDirectory지정 된 디렉터리를 서버에서 제거
OpenFile연결 된 서버에서 파일 열기
GetFile연결 된 서버에서 파일 가져옴
PutFile서버에 파일 배치
Remove서버에서 파일 제거
Rename서버에서 파일 이름 변경

코드를 알아보겠습니다.

1. 선언

헤더파일에 아래와 같이 CInternetSession과 CFtpConnection을 선언합니다. 참고로 헤더는 afxinet.h입니다.

#include <afxinet.h>

CInternetSession* m_pSession;
CFtpConnection* m_pFtp;
2. 로그인

로그인 시 GetFtpConnection 함수를 사용하면 됩니다.

try
{
	m_pSession = new CInternetSession;
	m_pFtp = m_pSession->GetFtpConnection(strIP, strID, strPass, nPort, bPassive);

	ResetEvent(m_hEventClose);
	return TRUE;
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}
3. 로그아웃

로그아웃을 할 땐 close함수를 사용하면 됩니다.

if (NULL != m_pFtp)
{
	m_pFtp->Close();
	delete m_pFtp;
	m_pFtp = NULL;
}

if (NULL != m_pSession)
{
	m_pSession->Close();
	delete m_pSession;
	m_pSession = NULL;
}
4. 디렉터리 경로 읽기

현재 디렉터리의 경로는 GetCurrentDirectory함수로 읽어옵니다. 로그인 후 수행해야합니다.

try
{
	CString strDir;
	bRes = m_pFtp->GetCurrentDirectory(strDirectory);

	return bRes;
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}
5. 현재 디렉터리의 리스트 출력

현재 디렉터리의 파일 리스트는 CFtpFileFind 클래스를 사용하면 됩니다.

try
{
	CFtpFileFind finder(m_pFtp);
	BOOL bWorking = finder.FindFile(_T("*"));
	while (bWorking)
	{
		bWorking = finder.FindNextFile();

		CString strName = finder.GetFileName();
		CString strPath = finder.GetFilePath();
		CString strTitle = finder.GetFileTitle();
		CString strUrl = finder.GetFileURL();
		ULONGLONG ullSize = finder.GetLength();
		BOOL bIsDir = finder.IsDirectory();
	}
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}

FTP에서는 현재 경로에 있는 파일들만 파악할 수 있습니다.
예를 들어 아래와 같이 A의 폴더 리스트는 읽어올 수 있지만 B, C 폴더 하위의 리스트는 읽어올 수 없습니다.

6. 디렉터리 이동

SetCurrentDirectory 함수를 사용하여 현재 디렉터리를 이동합니다.

try
{
	bRes = m_pFtp->SetCurrentDirectory(strDirectory);
	return bRes;
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}
7. 폴더 생성/삭제

폴더를 생성하려면 CreateDirectory, 삭제하려면 RemoveDirectory 함수를 사용하면 됩니다.

// 생성
try
{
	bRes = m_pFtp->CreateDirectory(strPath);
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}

// 삭제
try
{
	bRes = m_pFtp->RemoveDirectory(strPath);
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}
8. 파일 삭제/이름 변경

파일을 삭제하려면 Remove, 이름을 변경하려면 Rename 함수를 사용하면 됩니다.

// 삭제
try
{
	bRes = m_pFtp->Remove(strFile);
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}

// 이름 변경
try
{
	bRes = m_pFtp->Rename(strFileOld, strFileNew);
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}

Rename 함수는 파일 뿐만 아니라, 폴더도 적용 됩니다.

9. 파일 업로드/다운로드 - 작은 사이즈

파일 업로드는 PutFile, 다운로드는 GetFile로 가능합니다. 주의할 점은 사이즈가 큰 파일을 하게 되면 함수 수행시간이 오래 걸려 프로그램이 멈춥니다.

// 업로드
try
{
	m_pFtp->PutFile(strLocalPath, strFtpFilePath);
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}

// 다운로드
try
{
	m_pFtp->GetFile(strFtpFilePath, strLocalPath);
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}
10. 파일 업로드/다운로드 - 큰 사이즈

큰 사이즈의 파일 업로드/다운로드 일 경우 CInternetFile클래스를 사용해야 합니다.
업로드일 경우입니다. 해당 코드를 쓰레드에서 돌리면 됩니다.

try
{
	// 로컬 파일 읽기
	CStdioFile file(strLocalFilePath, CStdioFile::modeRead | CStdioFile::shareExclusive | CStdioFile::typeBinary);

	CString strFileName = file.GetFileName();
	CString strFtpFileName;
	strFtpFileName.Format(_T("%s/%s"), strFtpPath, strFileName);
	CheckString(strFtpFileName);

	// FTP 파일 생성
	CInternetFile* pInternet = m_pFtp->OpenFile(strFtpFileName, GENERIC_WRITE);
	if (NULL == pInternet)
	{
		file.Close();
		return FALSE;
	}

	ULONGLONG ull = file.GetLength();
	ULONGLONG ullRead = 0;
	do 
	{
		// 로컬 파일 Read
		BYTE byBuff[MAX_BUFFER_SIZE] = { 0, };
		UINT unRead = file.Read(byBuff, MAX_BUFFER_SIZE);

		// FTP 파일 Write
		pInternet->Write(byBuff, unRead);

		ullRead += unRead;
	} while (ullRead <ull);

	pInternet->Close();
	file.Close();
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}

다운로드 할 경우는 아래 방식으로 하면 됩니다.

try
{
	// ftp 파일 open
	CInternetFile* pInternet = m_pFtp->OpenFile(strFtpFilePath, GENERIC_READ);
	if (NULL == pInternet)
		return FALSE;

	// 로컬 파일 생성
	CString strFileName = pInternet->GetFileName();
	CString strLocalFileName;
	strLocalFileName.Format(_T("%s/%s"), strLocalPath, strFileName);
	CheckString(strLocalFileName);
	CStdioFile file(strLocalFileName, CStdioFile::modeCreate | CStdioFile::modeWrite | CStdioFile::shareExclusive | CStdioFile::typeBinary);

	do
	{
		// ftp 파일 읽기
		BYTE byBuff[MAX_BUFFER_SIZE] = { 0, };
		UINT unRead = pInternet->Read(byBuff, MAX_BUFFER_SIZE);

		if (0 == unRead)
			break;
		// 로컬에 쓰기
		file.Write(byBuff, unRead);

	} while (true);

	pInternet->Close();
	file.Close();
}
catch (CException* e)
{
	SetLastErrorMsg(e);
	return FALSE;
}
11. 에러 코드

작업 도중 에러가 발생해 exception 처리를 할 땐 아래와 같이 사용하시면 됩니다. SetLastErrorMsg를 선언하여 아래와 같이 정의해 줍니다.

CString SetLastErrorMsg(CException* e)
{
	DWORD dwErrorCode = GetLastError();
    
	TCHAR tzErrorMsg[LEN_ERR_MSG] = { 0, };
	e->GetErrorMessage(tzErrorMsg, LEN_ERR_MSG);

	CString strErrorMsg;	
	strErrorMsg.Format(_T("Error Code : 0x%X, Error Msg : %s"), dwErrorCode, tzErrorMsg);
	
	return strErrorMsg;
}
*유의 사항

개발할 때 유의사항이 있습니다.

1. FTP는 현재 경로의 파일만 조회가능합니다. (5. 현재 디렉터리 파일 리스트 조회)

2. FTP 서버에서 클라이언트를 강제로 종료하면, 인터넷 세션 종료가 체크되지 않을 수가 있습니다.
정확한 이유는 모르겠지만 서버에서 세션 종료 메시지를 날리지 않을수도 있다고하는데, 정확한 원인은 모르겠네요.
그래서 클라이언트에서는, 서버에 오랫동안 작업을 하지 않을 때 접속이 된건지 안된건지 확인하기 위해 현재 디렉터리 경로 조회를 하여 확인한다고 합니다.(GetCurrentDirectory 사용)

3. FTP 동작 중에는 다른 동작을 동시에 할 수 없습니다.
간단히 위 코드로 치면, FTP 서버에서 다운로드 한창 하고 있는데 현재 디렉터리 경로를 읽어올 수가 없습니다.
이 경우 CRITICAL_SECTION이나 임계영역을 설정하여 해결할 수 있습니다.

4. 재귀함수는 사용 시 주의해야합니다.
파일 리스트 조회 시 재귀함수로 만들어보려했는데, CFileFind 재귀 호출에 대한 이슈가 있습니다.
자세한 내용은 MSDN 링크을 참고하세요