[C++/MFC] FTP 통신 프로그램 만들기

 

이번 프로젝트는 C++을 사용해서 FTP 프로그램을 만들어 보겠습니다!

이전 시간에는 C#을 사용해서 FTP 프로그램을 만들어 보았는데요, 그 때는 winform을 가지고 인터페이스를 만들었습니다. C#의 대표적인 윈도우 인터페이스를 만드는 환경입니다. 

 

하지만 이번에는 c++을 사용해서 만들거라서 MFC를 사용해서 인터페이스를 만들어 보도록 하겠습니다. 

이번에 만들 FTP 프로그램은 이전 winform을 가지고 만들었던 FTP 프로그램과 기능은 동일합니다. 단, 언어를 C#에서 C++로 바꿔서 만들었다고 보시면 될거 같아요.

 

기능은 이전과 동일하지만 좀 더 보완한 점도 있고 추가한 점도 있습니다.

 

🟪 기능 우선순위

1. FTP 연결 작업

2. 로컬과 FTP의 디렉토리 목록을 TreeControl로 보여주는 작업

3. Drag & Drop 기능을 통해 파일 업로드/다운로드 작업 (폴더 포함)

4. 프로그래스 바를 통한 업로드/다운로드 진행중인 모습 확인

5. 폴더 동기화 작업

6. 로컬과 FTP 서버의 현재 디렉토리 경로 확인

7. 성공/실패 메세지 표시란

 

우선순위를 두고 차례대로 기능을 소개하겠습니다.

 

🟩 FTP 연결 작업

 

FTP연결 관련해서 총 4가지 조건을 가지고 로직을 짰습니다.

1. FTP 연결에 성공 했을 때

2. FTP 연결을 해제 했을 때

3. FTP 인증에 실패 했을 때

4. FTP 미연결 시 해제 버튼을 클릭할 때

 

class CAboutDlg : public CDialogEx
{
private:
	CInternetSession* m_pInternetSession;
	CFtpConnection* m_pFtpConnection;
};


// FTP 연결 메서드
void CMFCApplication1Dlg::OnBnClickedButtonConnect()
{
	UpdateData(TRUE);

	CInternetSession session(_T("FTPExampleSession"));

	try
	{
		if (m_pFtpConnection != nullptr)
		{
			m_pFtpConnection->Close();
			delete m_pFtpConnection;
			m_pFtpConnection = nullptr;
		}

		m_pFtpConnection = session.GetFtpConnection(m_editIP, m_editID, m_editPW);
		AppendTextToEditControl(GetFormattedCurrentTime() +_T("FTP 연결 성공\n"));
		AfxMessageBox(_T("FTP 연결 성공"));

		// FTP 디렉토리 트리 로드
		LoadFTPDirectoryStructure(m_pFtpConnection, _T("/"), TVI_ROOT);
	}
	catch (CInternetException* pEx)
	{
		TCHAR szErr[1024];
		pEx->GetErrorMessage(szErr, 1024);
		AfxMessageBox(szErr);
		pEx->Delete();
	}
}

// FTP 해제 메서드
void CMFCApplication1Dlg::OnBnClickedButtonDisconnect()
{
	if (m_pFtpConnection != nullptr)
	{
		m_pFtpConnection->Close();
		delete m_pFtpConnection;
		m_pFtpConnection = nullptr;
		AppendTextToEditControl(GetFormattedCurrentTime() + _T("FTP 연결이 해제되었습니다."));
	}
	else
	{
		m_Logtext.SetWindowText(GetFormattedCurrentTime() + _T("현재 활성화된 FTP 연결이 없습니다.\n"));
		AfxMessageBox(_T("현재 활성화된 FTP 연결이 없습니다."));
	}
}

 

클래스 선언부에 미리 FTP와 관려된 클래스를 선언해줍니다. 이후 버튼 이벤트를 만들어서 FTP 연결/해제 로직을 작성해줍니다. 저는 fTP연결 할 때 이미 연결이 되어 있는 경우에는 이전 연결을 해제하고 재연결하는 방식으로 작성했습니다.

또한 연결 해제 할 때는 FTP연결이 되어 있다면 연결 해제하고, 연결된 FTP가 없다면 사용자에게 활성화된 FTP가 없다고 알려주는 메세지를 보여줬습니다.

 

🟨 로컬과 FTP의 디렉토리 목록을 TreeControl로 보여주는 작업

// 내 디렉토리 목록을 가져와서 트리뷰에 뿌려주는 메서드
void CMFCApplication1Dlg::LoadDirectoryStructure(const CString& strPath, HTREEITEM hParentItem)
{
	CFileFind finder;
	CString strWildcard(strPath);
	strWildcard += _T("\\*.*");

	BOOL bWorking = finder.FindFile(strWildcard);
	int nIndex = 0;
	while (bWorking)
	{
		bWorking = finder.FindNextFile();

		// '.' 및 '..'을 건너뜁니다.
		if (finder.IsDots())
			continue;

		CString strFilePath = finder.GetFilePath();
		CString strFileName = finder.GetFileName();

		// 트리 컨트롤에 아이템 추가
		HTREEITEM hItem = m_TreeCtrl.InsertItem(strFileName, hParentItem);

		// 디렉토리인 경우, 재귀적으로 하위 디렉토리를 추가합니다.
		if (finder.IsDirectory())
		{
			LoadDirectoryStructure(strFilePath, hItem);
		}
	}
}



// 클릭한 TreeView의 디레토리 목록을 ListView에 보여주는 메서드
void CMFCApplication1Dlg::OnTvnSelchangedTree1(NMHDR* pNMHDR, LRESULT* pResult)
{
	LPNMTREEVIEW pNMTreeView = reinterpret_cast<LPNMTREEVIEW>(pNMHDR);

	HTREEITEM hSelectedItem = m_TreeCtrl.GetSelectedItem();
	if (hSelectedItem == NULL) {
		return;
	}
	// 선택된 항목 경로
	CString strSelectedPath = GetFullPathFromTreeItem(hSelectedItem);

	// List Control 초기화
	m_ListCtrl.DeleteAllItems();

	CFileFind finder;
	CString strWildcard(strSelectedPath);
	strWildcard += _T("\\*.*");

	BOOL bWorking = finder.FindFile(strWildcard);
	int nIndex = 0;
	while (bWorking) {
		bWorking = finder.FindNextFileW();

		if (finder.IsDots())
			continue;

		CString strFilePath = finder.GetFilePath();
		CString strFileName = finder.GetFileName();
		// 파일인지 디렉토리인지 확인
		BOOL bIsDirectory = finder.IsDirectory();

		// 파일 크기
		ULONGLONG fileSize = finder.GetLength();

		// 마지막 수정 날짜와 시간
		CTime lastModified;
		finder.GetLastWriteTime(lastModified);
		CString strLastModified = lastModified.Format(_T("%Y-%m-%d %H:%M:%S"));

		// List Control에 정보 추가
		int nItem = m_ListCtrl.InsertItem(nIndex++, strFileName);
		m_ListCtrl.SetItemText(nItem, 1, bIsDirectory ? _T("Folder") : _T("File"));
		m_ListCtrl.SetItemText(nItem, 2, strLastModified);
		if (!bIsDirectory)
		{
			CString strFileSize;
			strFileSize.Format(_T("%llu bytes"), fileSize);
			m_ListCtrl.SetItemText(nItem, 3, strFileSize);
		}
	}
	*pResult = 0;
}


// 클릭한 노드의 경로를 보여줌.
CString CMFCApplication1Dlg::GetFullPathFromTreeItem(HTREEITEM hItem)
{
	CString strPath;
	CString strItemText;

	// 아이템이 존재할 때까지 부모 아이템을 따라가며 경로를 구성
	while (hItem != NULL)
	{
		// 현재 아이템의 텍스트를 가져옴
		strItemText = m_TreeCtrl.GetItemText(hItem);

		// 현재 경로 앞에 추가 (역순으로 구성됨)
		strPath = _T("\\") + strItemText + strPath;

		// 부모 아이템으로 이동
		hItem = m_TreeCtrl.GetParentItem(hItem);
	}

	// 루트 경로를 추가 (C 드라이브 가정)
	strPath = _T("C:\\Test") + strPath;

	m_localPath.SetWindowTextW(strPath);

	// 최종적으로 전체 경로 반환
	return strPath;
}

 

로컬의 디렉토리를 보여줄 때는 C++에서 제공해주는 CFileFinde 클래스를 사용했습니다. 해당 클래스는 로컬의 파일 관련 메서드를 다양하게 가지고 있어서 매우 쉽게 기능을 제작했습니다. (앞으로 로컬 파일과 관련된 기능을 제작할 때는 필수로 사용해야 할 거 같습니다!!)

 

CFileFind 클래스를 사용해서 파일들을 찾고 디렉토리인 경우, 재귀적으로 하위 디렉토리를 추가해서 Tree View에 노드를 추가하는 방식으로 구현했습니다.

 

또한 저는 TreeControl에서 클릭한 디렉토리의 하위 목록들을 ListControl에 보여주고 싶어서 클릭한 노드의 경로를 가져와서 해당 디렉토리의 하위 목록을 ListControl에 추가했습니다.

 

 

🟥 Drag & Drop 기능을 통해 파일 업로드/다운로드 작업 (폴더 포함)

 

void CMFCApplication1Dlg::OnLButtonDown(UINT nFlags, CPoint point)
{

	CWnd* pWnd = GetFocus();

	CString msg;
	msg.Format(_T("Focused Control: %p"), pWnd);
	AfxMessageBox(msg); // 현재 포커스된 컨트롤이 무엇인지 확인
	// List Control에서의 클릭인지 확인
	if (pWnd == &m_ListCtrl || pWnd == &m_ListCtrl2)
	{
		LVHITTESTINFO hitTestInfo;
		hitTestInfo.pt = point;
		pWnd->ScreenToClient(&hitTestInfo.pt);
		int nItem = m_ListCtrl.HitTest(&hitTestInfo);
		
		if (nItem != -1) // -1은 항목이 없음을 의미
		{
			m_nDragIndex = nItem; // 드래그 시작한 항목의 인덱스 저장
			m_bDragging = TRUE;

			// 드래그 이미지를 생성하고 드래그를 시작합니다.
			m_pDragImage = m_ListCtrl.CreateDragImage(nItem, &point);
			m_pDragImage->BeginDrag(0, CPoint(0, 0));
			m_pDragImage->DragEnter(GetDesktopWindow(), point);
			::SetCapture(m_hWnd);
		}
	}

	CDialogEx::OnLButtonDown(nFlags, point);
}



/// <summary>
/// Drop 했을 때 처리 메서드
/// </summary>
/// <param name="nFlags"></param>
/// <param name="point"></param>
void CMFCApplication1Dlg::OnLButtonUp(UINT nFlags, CPoint point)
{
	if (m_bDragging)
	{
		m_bDragging = FALSE;
		::ReleaseCapture();
		m_pDragImage->EndDrag();
		delete m_pDragImage;
		m_pDragImage = nullptr;

		// 드래그된 항목의 텍스트 얻기
		CString draggedItemText = m_ListCtrl.GetItemText(m_nDragIndex, 1);
		CString draggedItemText2 = m_ListCtrl2.GetItemText(m_nDragIndex, 1);

		// 드롭할 윈도우 탐색
		ClientToScreen(&point);
		CWnd* pDropWnd = WindowFromPoint(point);

		// pDropWnd가 m_ListCtrl2를 가리키는지 확인
		if (pDropWnd->GetSafeHwnd() == m_ListCtrl2.GetSafeHwnd()) {
			if (draggedItemText == "File") {
				// List1에서 List2로 드래그 -> 파일 업로드
				UploadFileToFtp(m_nDragIndex);
			}
			else if (draggedItemText == "Folder") {
				// 폴더 업로드
				UploadFolderFromFtp(m_nDragIndex);
			}
		}
		// pDropWnd가 m_ListCtrl을 가리키는지 확인
		else if (pDropWnd->GetSafeHwnd() == m_ListCtrl.GetSafeHwnd()) {
			if (draggedItemText2 == "File") {
				// List2에서 List1로 드래그 -> 파일 다운로드
				DownloadFileFromFtp(m_nDragIndex);
			}
			else if (draggedItemText2 == "Folder") {
				// 폴더 다운로드
				DownloadFolderFromFtp(m_nDragIndex);
			}
		}
	}

	CDialogEx::OnLButtonUp(nFlags, point);
}

 

마우스 클릭을 했을 때(OnLButtonDown) 와 클릭을 땟을 때(OnLButtonUp)의 경우를 가지고 Drag & Drop 기능을 구현했습니다. Drag & Drop 기능은 ListControl에서 구현되도록 만들었는데요, 클릭을 했을 때에 어떤 ListControl인지 구분 가능하게 했으며, 클릭된 ListControl의 목록을 가져올 수 있게 구현했습니다. 

 

클릭된 요소의 속성에 따라 File 또는 Folder를 업로드/다운로드 할 수 있게 구현했습니다.

 

 

🟧 파일 업로드/다운로드 기능

void CMFCApplication1Dlg::UploadFileToFtp(int nIndex)
{
	// 인터넷 세션 객체 생성
	CInternetSession internetSession;

	// FTP 서버에 연결
	CFtpConnection* ftpConnection = nullptr;

	HTREEITEM hSelectedItem = m_TreeCtrl.GetSelectedItem();
	if (hSelectedItem == NULL) {
		return;
	}

	ftpConnection = internetSession.GetFtpConnection(m_editIP, m_editID, m_editPW);
	CString strLocalFilePath = GetFullPathFromTreeItem(hSelectedItem) + _T("\\") + m_ListCtrl.GetItemText(nIndex, 0);
	CString strRemoteFilePath = GetSelectedFtpPath() + _T("/") + m_ListCtrl.GetItemText(nIndex, 0);

	// 프로그래스 바 초기화
	CProgressCtrl* pProgressCtrl = (CProgressCtrl*)GetDlgItem(IDC_PROGRESS1);
	pProgressCtrl->SetRange(0, 100);
	pProgressCtrl->SetPos(0);

	// 로컬 파일 열기
	CFile localFile;
	if (!localFile.Open(strLocalFilePath, CFile::modeRead | CFile::typeBinary)) {
		AfxMessageBox(_T("로컬 파일을 열 수 없습니다."));
		return;
	}

	// 파일 크기 가져오기
	DWORD dwFileSize = (DWORD)localFile.GetLength();

	// 파일 크기가 0인 경우
	if (dwFileSize == 0) {
		// 빈 파일이라도 원격 파일을 열어야 함
		CInternetFile* remoteFile = ftpConnection->OpenFile(strRemoteFilePath, GENERIC_WRITE, FTP_TRANSFER_TYPE_BINARY | INTERNET_FLAG_TRANSFER_BINARY);

		if (!remoteFile) {
			AfxMessageBox(_T("원격 파일을 열 수 없습니다."));
			localFile.Close();
			return;
		}

		// 프로그래스 바를 100%로 설정
		pProgressCtrl->SetPos(100);

		// 파일 닫기
		remoteFile->Close();
		delete remoteFile;
	}
	else {
		// 파일이 0바이트가 아닌 경우
		DWORD dwBytesRead = 0;
		DWORD dwTotalBytesRead = 0;
		const int bufferSize = 4096;  // 4KB 버퍼
		byte buffer[bufferSize];

		// 원격 파일 열기 (쓰기 모드)
		CInternetFile* remoteFile = ftpConnection->OpenFile(strRemoteFilePath, GENERIC_WRITE, FTP_TRANSFER_TYPE_BINARY | INTERNET_FLAG_TRANSFER_BINARY);

		if (!remoteFile) {
			AfxMessageBox(_T("원격 파일을 열 수 없습니다."));
			localFile.Close();
			return;
		}

		// 파일을 청크 단위로 읽어 전송하면서 프로그래스 바 업데이트
		while ((dwBytesRead = localFile.Read(buffer, bufferSize)) > 0)
		{
			remoteFile->Write(buffer, dwBytesRead);
			dwTotalBytesRead += dwBytesRead;

			// 프로그래스 바 업데이트
			int nProgress = (int)(((double)dwTotalBytesRead / dwFileSize) * 100.0);
			pProgressCtrl->SetPos(nProgress);
		}

		// 파일 닫기
		localFile.Close();
		remoteFile->Close();
		delete remoteFile;
		m_Logtext.SetWindowText(GetFormattedCurrentTime() + m_ListCtrl.GetItemText(nIndex, 0) + _T(" 파일 업로드 완료!\n"));
	}
}

 

해당 파일을 읽어 올 때 파일 크기를 가져와서 업로드/다운로드 되는 과정을 나타낼 수 있게 했습니다. 왜냐하면 프로그래스 바에 보여줄 예정이라서요. ㅎㅎ

 

업로드/다운로드 될 파일을 가져와서 상대 경로에 읽을 수 있게 만들었습니다.

 

 

🟧 폴더 업로드/다운로드 기능

/// <summary>
/// 폴더 업로드 메서드
/// </summary>
/// <param name="nIndex"></param>
void CMFCApplication1Dlg::UploadFolderFromFtp(int nIndex)
{
	// 인터넷 세션 객체 생성
	CInternetSession internetSession;

	// FTP 서버에 연결
	CFtpConnection* ftpConnection = nullptr;

	CString strLocalPath;
	m_localPath.GetWindowText(strLocalPath);

	ftpConnection = internetSession.GetFtpConnection(m_editIP, m_editID, m_editPW);
	CString strRemoteFilePath = GetSelectedFtpPath() + _T("/") + m_ListCtrl.GetItemText(nIndex, 0);
	CString strLocalFilePath = strLocalPath + _T("\\") + m_ListCtrl.GetItemText(nIndex, 0);

	// 폴더 생성
	BOOL result = ftpConnection->CreateDirectory(strRemoteFilePath);
	if (!result)
	{
		AfxMessageBox(_T("폴더가 생성되지 않았습니다."));
		return;
	}

	// 총 파일 개수를 계산
	g_totalFiles = CountFilesInFolder(strLocalFilePath);
	g_uploadedFiles = 0; // 초기화

	// 프로그래스 바 초기화
	CProgressCtrl* pProgressCtrl = (CProgressCtrl*)GetDlgItem(IDC_PROGRESS1);
	pProgressCtrl->SetRange(0, 100);
	pProgressCtrl->SetPos(0);

	// 로컬 폴더 내의 파일 및 폴더를 재귀적으로 FTP 서버로 업로드
	UploadFolderContents(strLocalFilePath, strRemoteFilePath, ftpConnection, pProgressCtrl);
	AppendTextToEditControl(GetFormattedCurrentTime() + m_ListCtrl.GetItemText(nIndex, 0) + _T(" 폴더 업로드 완료!\n"));

}

/// <summary>
/// 업로드 폴더 갯수 반환 메서드
/// </summary>
/// <param name="strLocalFolderPath"></param>
/// <returns></returns>
int CMFCApplication1Dlg::CountFilesInFolder(const CString& strLocalFolderPath)
{
	CFileFind finder;
	CString strSearchPath = strLocalFolderPath + _T("\\*.*");
	BOOL bWorking = finder.FindFile(strSearchPath);
	int fileCount = 0;

	while (bWorking)
	{
		bWorking = finder.FindNextFile();
		if (finder.IsDots()) // . and .. directories
			continue;

		if (finder.IsDirectory())
		{
			// 서브 폴더의 파일 개수도 포함
			fileCount += CountFilesInFolder(strLocalFolderPath + _T("\\") + finder.GetFileName());
		}
		else
		{
			fileCount++;
		}
	}

	return fileCount;
}

/// <summary>
/// 하위 디렉토리 업로드 메서드
/// </summary>
/// <param name="strLocalFolderPath"></param>
/// <param name="strRemoteFolderPath"></param>
/// <param name="pFtpConnection"></param>
/// <param name="pProgressCtrl"></param>
void CMFCApplication1Dlg::UploadFolderContents(const CString& strLocalFolderPath, const CString& strRemoteFolderPath, CFtpConnection* pFtpConnection, CProgressCtrl* pProgressCtrl)
{
	CFileFind finder;
	BOOL bWorking = finder.FindFile(strLocalFolderPath + _T("\\*.*"));
	while (bWorking)
	{
		bWorking = finder.FindNextFile();
		if (finder.IsDots()) // . and .. directories
			continue;

		CString strFileName = finder.GetFileName();
		CString strLocalFilePath = strLocalFolderPath + _T("\\") + strFileName;
		CString strRemoteFilePath = strRemoteFolderPath + _T("/") + strFileName;

		if (finder.IsDirectory())
		{
			// 서브 폴더가 발견되면, 서버에 폴더를 생성하고 재귀 호출
			if (!pFtpConnection->CreateDirectory(strRemoteFilePath))
			{
				AfxMessageBox(_T("Failed to create directory on FTP server: ") + strRemoteFilePath);
				continue;
			}

			// 재귀 호출
			UploadFolderContents(strLocalFilePath, strRemoteFilePath, pFtpConnection, pProgressCtrl);
		}
		else
		{
			// 파일을 발견하면 FTP 서버로 업로드
			if (!pFtpConnection->PutFile(strLocalFilePath, strRemoteFilePath))
			{
				AfxMessageBox(_T("Failed to upload file: ") + strLocalFilePath);
			}

			// 업로드된 파일 수를 증가시키고 프로그래스 바 업데이트
			g_uploadedFiles++;
			int nProgress = (int)(((double)g_uploadedFiles / g_totalFiles) * 100.0);
			pProgressCtrl->SetPos(nProgress);
		}
	}
}

 

폴더 업로드/다운로드 기능 또한 보내지는 과정을 프로그래스 바에 보여줘야 하기 때문에 어떤 걸 기준으로 보여줄까 하다가 폴더는 하위 디렉토리 갯수에 따라 프로그래스 진행률을 보여주기로 했습니다. 

따라서 업로드/다운로드 할 폴더의 하위 디렉토리 갯수를 구하는 메서드를 구현하고 재귀함수를 통해 타겟 폴더의 하위 디렉토리를 돌면서 파일 또는 폴더를 만날 때마다 진행률을 증가 시켜줬습니다.

 

 

전체 코드는 제 깃허브에서 확인할 수 있습니다!😊

 

GitHub - 7jjin/MFC_Project

Contribute to 7jjin/MFC_Project development by creating an account on GitHub.

github.com