개요
해당 글은 C++(CPP) 언어 및 C++ 라이브러리인 Boost/Asio를 활용한 파일 전송 시스템을 공부하는 글이다.
대부분의 Server와 Client는 데스크탑과 노트북으로 고정되어 있다. 연결 구조도는 아래와 같다.
목차
1. WinSock2.h를 활용한 윈도우 파일전송 시스템 실슴
1. WinSock2.h를 활용한 윈도우 파일전송 시스템 실습
먼저 간단하게 mp3 파일이 담긴 압축파일을 통으로 전송하는 Sender의 소스 코드다.
#include "stdafx.h"
#include <winsock2.h>
#pragma comment(lib, "ws2_32")
void ErrorHandler(const char *pszMessage)
{
printf("ERROR: %s\n", pszMessage);
::WSACleanup();
exit(1);
}
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsa = { 0 };
if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
ErrorHandler("윈속을 초기화 할 수 없습니다.");
//전송할 파일 open
FILE *fp = NULL;
errno_t nResult = fopen_s(&fp, "music.zip", "rb");
if (nResult != 0)
ErrorHandler("전송할 파일을 개방할 수 없습니다.");
SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHandler("접속 대기 소켓을 생성할 수 없습니다.");
SOCKADDR_IN svraddr = { 0 };
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(/*포트입력*/);
svraddr.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
if (::bind(hSocket,
(SOCKADDR*)&svraddr, sizeof(svraddr)) == SOCKET_ERROR)
ErrorHandler("소켓에 IP주소와 포트를 바인드 할 수 없습니다.");
if (::listen(hSocket, SOMAXCONN) == SOCKET_ERROR)
ErrorHandler("리슨 상태로 전환할 수 없습니다.");
puts("파일송신서버를 시작합니다.");
SOCKADDR_IN clientaddr = { 0 };
int nAddrLen = sizeof(clientaddr);
SOCKET hClient = ::accept(hSocket,
(SOCKADDR*)&clientaddr, &nAddrLen);
if (hClient == INVALID_SOCKET)
ErrorHandler("클라이언트 통신 소켓을 생성할 수 없습니다.");
puts("클라이언트가 연결되었습니다.");
char byBuffer[65536]; //64KB
int nRead, nSent, i = 0;
while ((nRead = fread(byBuffer, sizeof(char), 65536, fp)) > 0)
{
//파일에서 읽고 소켓으로 전송한다.
//전송에 성공하더라도 nRead와 nSent 값은 다를 수 있다.
// 이 때문에 파일 전송시 파일에 대한 정보, 패킷당 보내지는 용량에 대해 Receiver에게 정보를 알려줘야한다.
nSent = send(hClient, byBuffer, nRead, 0);
printf("[%04d] 전송된 데이터 크기: %d\n", ++i, nSent);
fflush(stdout);
}
// 클라이언트의 수신을 위한 임의의 송신 대기 시간
::Sleep(100);
//서버가 먼저 클라이언트 연결을 종료함.
::closesocket(hSocket);
::closesocket(hClient);
puts("클라이언트 연결이 끊겼습니다.");
fclose(fp);
::WSACleanup();
return 0;
}
다음은 파일을 전송받을 Receiver의 코드다.
#include "stdafx.h"
#include <winsock2.h>
#pragma comment(lib, "ws2_32")
void ErrorHandler(const char *pszMessage)
{
printf("ERROR: %s\n", pszMessage);
::WSACleanup();
exit(1);
}
int _tmain(int argc, _TCHAR* argv[])
{
WSADATA wsa = { 0 };
if (::WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
ErrorHandler("윈속을 초기화 할 수 없습니다.");
SOCKET hSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (hSocket == INVALID_SOCKET)
ErrorHandler("소켓을 생성할 수 없습니다.");
SOCKADDR_IN svraddr = { 0 };
svraddr.sin_family = AF_INET;
svraddr.sin_port = htons(/*서버프로그램할당포트*/);
svraddr.sin_addr.S_un.S_addr = inet_addr(/*IP*/);
if (::connect(hSocket,
(SOCKADDR*)&svraddr, sizeof(svraddr)) == SOCKET_ERROR)
ErrorHandler("서버에 연결할 수 없습니다.");
puts("파일 수신 시작");
FILE *fp = NULL;
errno_t nResult = fopen_s(&fp, "Sleep away.zip", "wb");
if (nResult != 0)
ErrorHandler("파일을 생성 할 수 없습니다.");
char byBuffer[65536]; //64KB
int nRecv;
while ((nRecv = ::recv(hSocket, byBuffer, 65536, 0)) > 0)
{
fwrite(byBuffer, nRecv, 1, fp);
putchar('#'); // 한번 서버로부터 패킷을 받아 FILE* 변수
// fp에 저장될 때 마다 #를 출력
}
fclose(fp);
printf("파일수신 끝\n");
//소켓을 닫고 프로그램 종료.
::closesocket(hSocket);
::WSACleanup();
return 0;
}
- Sender가 전송될 파일의 64kb 만큼씩 읽어 버퍼에 저장한 뒤 Send() 함수를 사용해 Receiver로 전송한다.
실행 결과와 WireShark를 통한 송수신 패킷 관찰
- Receiver의 실행 결과창에는 #가 128개 즉 수신받아 파일 변수에 넣은 횟수가 128이라는데 이는 전송측 실행결과 창에 나오는 71과 차이가 있다.(해당 결과는 루프백 IP에서 실행하면 달라짐)
- 전송받을 파일이 제대로 전송 되었는지 검증하는 무결성, 보안 검사 등이 없고 미리 받을 파일의 정보를 받아서 기입하는 등 사전에 송수신측 간에 파일의 정보를 교환하는 좋은 방식이 아니다.
- 파일에서 읽어낸 양(nRead)와 실제 Receiver로 전송된 양(nSent)은 반드시 같지 않다. 파일에서 읽은 양보다 적은 양을 전송하기도 하는데 이는 수신 측의 버퍼에 여유가 없어 다 전송되지 못할 때 발생한다.
이를 개선하기 위해 Win32 API 파일 기반 파일 송수신 코드를 작성 해볼 것이다.
주요 코드 개선
a. TransmitFile() 사용
b. MD5 해시 알고리즘을 사용
a. TransmitFile() 사용
TransmitFile() 함수를 사용하면 우리가 보낼 파일 그 자체와 해당 파일에 대한 요약 정보같은 추가 데이터를 함께 보낼 수 있다. 파일 이름, 파일 용량 같은 데이터를 먼저 보낼 수 있기 때문에 이를 활용하면 파일에 대한 보안과 무결성을 체크 가능하다. 함께 보낼 추가 데이터로 사용할 구조체를 구현한 다음 해당 구조체에 담을 정보들을 활용할 것이다.
* LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers 매개 변수가 추가로 보내게 될 파일 정보 구조체가 들어갈 위치이다. 이때 수신측(Receiver)도 해당 구조체의 정보를 알아야 수신 버퍼에서 읽어 활용할 수 있는데 이는 후술하겠다.
b. MD5 해시 알고리즘 사용
파일 정보를 담는 구조체를 활용해서 파일 정보를 MD5 해시 알고리즘으로 계산한 값을 함께 담아 보낸 뒤, 수신측에서 해당 값을 통해 파일의 변조, 손실 여부를 구현해본다.
사용할 라이브러리는 bcrypt.h이다.
구현 순서
1. BCrypt 알고리즘 제공자(Provider) 열기
2. 해시 객체 및 크기 설정
3. 해시 객체 생성
4. 파일 읽기 및 해시 데이터 업데이트(읽기(버퍼) 크기에 따라 다 읽을 때까지 반복하며 업데이트)
5. 해시 계산 완료 및 결과 출력
6. 리소스 정리
따라서 구현하게될 Sender와 Receiver의 기능을 대략적으로 말하면
Sender: 파일을 읽은 다음 해당 파일 정보로 MD5 문자열을 계산, 파일 정보 구조체에 함께 담아 파일 정보와 함께 TransmitFile() 함수로 이를 Receiver에게 전송한다.
Receiver: 파일을 전송받는다면 해당 파일 정보로 MD5 문자열을 계산, 이 문자열을 넘겨받은 파일 정보 구조체에 담겨있는 문자열과 비교해 일치하는지 확인한다.
다음은 구현간 있었던 오류를 나열한 부분이다.
1. MD5 문자열을 계산하는 함수는 CreateFile함수를 통해 반환된 HANDLE 변수를 인자로 받아 해당 파일을 읽어내 문자열을 계산해 저장한다. 이때 파일 포인터의 위치를 생각하지 않고 파일을 읽는 상황이 Sender와 Receiver에 각각 발생했다다. 해결하기 위해 많은 시도를 했었다...
1-1: Sender는 CreateFile로 열람한 파일을 MD5로 계산한 다음 TransmitFile() 함수를 실행하는데, 이때 MD5 를 계산하기 위해 HANDLE 파일 변수를 먼저 ReadFile() 함수를 통해 읽어버리기 때문에 파일포인터가 파일 변수의 뒤로 간다. 이때 파일포인터를 다시 앞으로 돌려 주지 않아 TransmitFile()로 전송할 때 파일 전송에 앞서 먼저 전송할 파일 정보 구조체만 전송되고 본 전송 파일이 가지 않는 현상이 발생했다. 해당 현상은 MSVC의 디버거로 중단점을 검토하면서 파일 정보 구조체만큼은 정상적으로 전송된 부분과 WireShark로 정상 전송될 때와 비전송 될 때의 패킷 차이를 인지하면서 알게 되었는데, 정상 전송될 때는 파일 정보 구조체와 함께 내가 전송하기로 했던 크기인 65536바이트 만큼의 데이터가 전송된다. 하지만 파일포인터의 위치로 인해 TransmitFile()이 전성되지 않을 때는 정확히 파일 정보 구조체의 용량 만큼만 전송되는 것을 패킷정보로 확인했다. 해당 오류는 SetFilePointer() 함수를 MD5 계산 함수의 마무리 부분에 위치시키면서 해결했다.
1-2: Receiver에서는 반대로 도착한 파일을 저장하면서 이미 파일 포인터의 위치가 맨 뒤로 가 있었다. 해결을 위해 MD5 계산 함수로 HANDLE 파일 변수를 넘기기 전, SetFilePointer() 함수를 실행한 다음 넘겨줬다.
2. MD5 계산중 오류가 생길 때마다 CloseHandle(HANDLE 파일변수) 함수를 실행시키는 함수를 정상 계산 후에도 넣어서, TransmitFile() 함수가 파일을 못읽는 문제가 발생했었다....
3. 정말 중요했던건데 Receiver에서 파일을 수신 받기위해 저장할 파일을 CreateFile() 함수로 HANDLE 파일 변수를 생성할 때, 파일 핸들의 상태를 쓰기 전용으로 설정해버렸다. CreateFile() 함수의 두번째 인자로 GENERIC_WRITE 만 넘겨버렸기 때문에, 파일을 읽어서 MD5를 계산 할 수 없었고 이는 사전에 ReadFile() 함수를 실행 후 오류 발생시 null을 리턴하는 기능을 통해 확인했다. 이후 CreateFile() 함수의 두번째 인자로 GENERIC_READ 변수도 함께 넘겨주는 것으로 해결했다.
HANDLE hFile = ::CreateFileA(
fData.szName,
GENERIC_WRITE | GENERIC_READ, // 문제 3, 동시에 넘겨 줄 수 있다.
0,
NULL,
CREATE_ALWAYS, //파일을 생성한다.
0,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
ErrorHandler("전송할 파일을 개방할 수 없습니다.");
출처
- MSDN의 CreateFileA() 함수 문서
https://learn.microsoft.com/ko-kr/windows/win32/api/fileapi/nf-fileapi-createfilea
CreateFileA 함수(fileapi.h) - Win32 apps
파일 또는 I/O 디바이스를 만들거나 엽니다. 가장 일반적으로 사용되는 I/O 디바이스는 다음과 같습니다. \_file, 파일 스트림, 디렉터리, 실제 디스크, 볼륨, 콘솔 버퍼, 테이프 드라이브, 통신 리소
learn.microsoft.com
- MSDN의 TransmitFile() 함수 문서
https://learn.microsoft.com/ko-kr/windows/win32/api/mswsock/nf-mswsock-transmitfile
TransmitFile 함수(mswsock.h) - Win32 apps
TransmitFile 함수(mswsock.h)는 연결된 소켓 핸들을 통해 파일 데이터를 전송합니다.
learn.microsoft.com
- 인프런 강의 "Windows 소켓 프로그래밍 입문에서 고성능 서버까지!"-널널한 개발자 저
'C++ 게임 서버 개발 > 네트워크 프로그래밍 실습' 카테고리의 다른 글
IOCP 와 IOCP 에코 서버 (0) | 2025.01.24 |
---|