C++ 예제 (소켓 클라이언트, 이미지, 파일전송)
이전 게시물인
소켓 프로그래밍 서버에 이은 클라이언트 측에 대한 설명입니다.
[클라이언트 실행화면] |
기본적인 소켓 프로그래밍에 대한
개요, 프로토콜, 실행파일, 전체 소스코드는 이전 게시물인 서버측을 참조
바랍니다.
제작된 클라이어트와 앞서 만든 서버측간 이미지, 파일 전송 테스트 진행
결과입니다.
이미지 전송 테스트
서버, 클라이어언트 테스트는 내부 사설 IP 환경 (192.168.XXX.XXX) 에서
테스트 되었으며, 외부 네트워크와 테스트시 서버가 실행되는 PC는
공인 IP 로 설정 or 사설 IP의 경우 포트포워딩이 필요합니다.
-
서버 실행, 클라이어트 2개 연결
[테스트 준비] |
-
테스트 이미지 선택, 전송
-
test.bmp 파일, (Size 23Mbyte)
-
서버측에 전달된 후 다시 클라이언트로 잘 수신됨을 확인
[이미지 수신] |
소스코드 분석
바로 소스 코드 분석으로 들어가 보겠습니다.
먼저 클라이언트 측 프로그램도 소켓 라이브러리를 초기화해야 한다는
사실을 잊지 말고, 이전 게시물인 서버측을 참조해 진행하기 바랍니다.
대화상자 프로젝트를 생성합니다.
프로그램 실행시, 종료시 각 1회씩만 수행되므로 MFC CWinApp class의
InitInstance(), ExitInstance() 함수에서 초기화, 해제 하면 됩니다.
1. 클라이언트 소켓 클래스 생성
빈 Client.h, Client.cpp 파일을 프로젝트에 추가합니다.
서버측 대비 클라이언트는 bind(), Listen(), Accept() 가 필요없으므로 보다
간단합니다.
Client.h
- DATA_TYPE 열거형 선언, 전송 프로토콜에서 사용될 패킷 구분자
- 대화상자 클래스로 보낼 메시지 선언, UM_XXX
- 멤버함수, 및 멤버 변수 선언 (*.cpp 에서 설명)
#pragma once #include <WinSock2.h> #include <thread> const int MAX_BUF = 4096; const unsigned int UM_DISCONNECT_SERVER = WM_USER + 1; const unsigned int UM_RECV_TEXT = WM_USER + 2; const unsigned int UM_RECV_IMAGE = WM_USER + 3; const unsigned int UM_RECV_FILE = WM_USER + 4; const unsigned int UM_RECV_IMAGE_NAME = WM_USER + 5; const unsigned int UM_RECV_FILE_NAME = WM_USER + 6; enum DATA_TYPE { _NICK, _TEXT, _IMAGE_NAME, _IMAGE, _FILE, _FILE_NAME, _UNKNOWN}; class Client : public CWnd { public: Client(CWnd* pParent); ~Client(); public: static std::wstring getMyip(); bool connectServer(std::wstring ip, int port); void disconnectServer(); bool sendText(const std::wstring& msg, const DATA_TYPE& type = _TEXT); bool sendNick(const std::wstring& msg, const DATA_TYPE& type = _NICK); bool sendFile(const std::wstring& file_name, const std::wstring& file_path, const std::wstring& file_ext, const DATA_TYPE& type); static const std::string UnicodeToMultibyte(const unsigned int& code_page, const std::wstring& strWide); static const std::wstring MultibyteToUnicode(const unsigned int& code_page, const char* buf, const int& size); void recvFinished(const DATA_TYPE& type, const char* buf, const size_t& recv_size, const size_t& data_size); bool sendSocket(const char* buf, const size_t& size); char* ImageToBytes(const std::wstring& file_path, const std::wstring& file_ext, size_t& size); char* FileToBytes(const std::wstring& file_path, const std::wstring& file_ext, size_t& size); std::wstring getNick() { return m_nick; } private: SOCKET client_sock; SOCKADDR_IN addr; std::thread *m_pRecvThread, *m_pSendThread; std::wstring m_nick; public: CWnd* m_pParent; bool bRecv, bSend; };
Client.cpp
- 헤더 파일 및 전역 함수
#include "pch.h" #include "Client.h" #include <ws2tcpip.h> #include <chrono> #include <atlimage.h> #include <fstream> using namespace std; unsigned int threadRecv(LPVOID p, SOCKET& sock); unsigned int threadSend(LPVOID p, SOCKET& sock, const DATA_TYPE& type, const wstring& name=L"", const wstring& path=L"", const wstring& ext=L"BMP");
- Client class 생성자, 소멸자
Client::Client(CWnd* pParent) : client_sock(INVALID_SOCKET), addr{}, m_pSendThread(nullptr), m_pRecvThread(nullptr), m_pParent(pParent), bSend(true), bRecv(true) { } Client::~Client() { disconnectServer(); }
- connectServer() 함수
-
소켓 생성 및 connect() 함수 호출 후 서버와 연결 처리
- 연결 성공시, 쓰레드 동적할당 후 recv() 처리
bool Client::connectServer(wstring ip, int port) { if (client_sock == INVALID_SOCKET) { client_sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (client_sock == INVALID_SOCKET) return false; addr.sin_family = AF_INET; //addr.sin_addr.s_addr = htonl(INADDR_ANY); InetPton(AF_INET, (PWSTR)ip.c_str(), &(addr.sin_addr)); addr.sin_port = htons(port); int result = connect(client_sock, (SOCKADDR *)&addr, sizeof(addr)); if (result != SOCKET_ERROR) { bRecv = true; if (m_pRecvThread == nullptr) m_pRecvThread = new thread(&threadRecv, this, std::ref(client_sock)); } else { int err = WSAGetLastError(); return false; } } return true; }
- disconnectServer() 함수
-
연결 종료시 소켓을 닫고 송, 수신 쓰레드 종료 후 메모리 해제
void Client::disconnectServer() { bRecv = bSend = false; // close client socket if (client_sock != INVALID_SOCKET) { closesocket(client_sock); client_sock = INVALID_SOCKET; } // stop recv thread if (m_pRecvThread) { if (m_pRecvThread->joinable()) m_pRecvThread->join(); delete m_pRecvThread; m_pRecvThread = nullptr; } // stop send thread if (m_pSendThread) { if (m_pSendThread->joinable()) m_pSendThread->join(); delete m_pSendThread; m_pSendThread = nullptr; } }
- sendSocket() 함수
- 소켓 데이터 전송
- 보내는 데이터 사이즈가 소켓버퍼의 크기보다 클 수 있으므로 반복문으로 처리
bool Client::sendSocket(const char * buf, const size_t & size) { if (client_sock != INVALID_SOCKET) { int send_size = 0; do { send_size += send(client_sock, &buf[send_size], (int)size, 0); } while (send_size < size); } else return false; return true; }
- sendText() 함수
- 유니코드 문자열 데이터를 char로 변환 후 프로토콜에 맞게 전송
bool Client::sendText(const std::wstring& msg, const DATA_TYPE& type) { wstring send_msg = L"[" + m_nick + L"] " + msg; // utf-8 -> char string str = Client::UnicodeToMultibyte(CP_UTF8, send_msg); size_t txt_size = str.length(); size_t packet_size = 1 + sizeof(size_t) + txt_size; char* buf = new char[packet_size]; memset(buf, 0, packet_size); buf[0] = type; memcpy(&buf[1], &txt_size, sizeof(size_t)); memcpy(&buf[1 + sizeof(size_t)], &str[0], txt_size); bool result = sendSocket(buf, packet_size); delete[] buf; return result; }
- sendNick() 함수
- 유니코드 문자열(별칭) 데이터를 char로 변환 후 프로토콜에 맞게 전송
bool Client::sendNick(const std::wstring& msg, const DATA_TYPE& type) { m_nick = msg; // utf-8 -> char string str = Client::UnicodeToMultibyte(CP_UTF8, msg); size_t txt_size = str.length(); size_t packet_size = 1 + sizeof(size_t) + txt_size; char* buf = new char[packet_size]; memset(buf, 0, packet_size); buf[0] = type; memcpy(&buf[1], &txt_size, sizeof(size_t)); memcpy(&buf[1 + sizeof(size_t)], &str[0], txt_size); bool result = sendSocket(buf, packet_size); return result; }
- sendFile() 함수
- 일반파일(이미지 X)을 전송, ex) zip, hwp, pdf, txt 등 모든 파일 가능
-
보내는 파일 사이즈가 크면 전송이 지연되므로 쓰레드를 생성해
송신처리
bool Client::sendFile(const std::wstring& file_name, const std::wstring& file_path, const std::wstring& file_ext, const DATA_TYPE& type) { if (m_pSendThread == nullptr) { bSend = true; m_pSendThread = new thread(&threadSend, this, std::ref(client_sock), type, file_name, file_path, file_ext); if (m_pSendThread->joinable()) { m_pSendThread->join(); delete m_pSendThread; m_pSendThread = nullptr; } } else return false; return true; }
- ImageToBytes() 함수
-
이미지를 보낼때 호출되는 변환 함수, 아래 순서로
이미지 파일->byte array
변환
- CImage class로 이미지 파일 경로를 이용해 열기
- CImage에 저장된 byte arrary를 Stream에 이미지 포맷에 맞춰 쓰기
-
Stream 이미지 주소를 동적할당된 char* 배열로 프로토콜에
맞춰 메모리 복사
char * Client::ImageToBytes(const std::wstring& file_path, const std::wstring& file_ext, size_t& size) { CImage img; img.Load(file_path.c_str()); if (img.IsNull()) { size = 0; return nullptr; } IStream* pStream = nullptr; if (::CreateStreamOnHGlobal(nullptr, true, &pStream) == S_OK) { if (file_ext == L"BMP") img.Save(pStream, Gdiplus::ImageFormatBMP); else if(file_ext == L"JPG") img.Save(pStream, Gdiplus::ImageFormatJPEG); else if (file_ext == L"GIF") img.Save(pStream, Gdiplus::ImageFormatGIF); else if (file_ext == L"PNG") img.Save(pStream, Gdiplus::ImageFormatPNG); HGLOBAL hg = nullptr; ::GetHGlobalFromStream(pStream, &hg); char* pBuf = static_cast<char*>(::GlobalLock(hg)); size_t img_size = ::GlobalSize(pBuf); size_t packet_size = img_size + 1 + sizeof(size_t); size = packet_size; char* pImg = new char[packet_size]; memset(pImg, 0, packet_size); pImg[0] = _IMAGE; memcpy(&pImg[1], &img_size, sizeof(size_t)); memcpy(&pImg[1+sizeof(size_t)], pBuf, img_size); ::GlobalUnlock(hg); pStream->Release(); ::GlobalFree(hg); return pImg; } else { size = 0; return nullptr; } return nullptr; }
- FileToBytes() 함수
- 파일을 보낼때 호출되는 변환 함수, 아래 순서로 파일->byte array 변환
- ifstream(input file stream) 이용, 파일 바이너리 모드로 열기
- seekg() 함수로 읽기 위치 마지막으로 변경
- tellg() 함수로 현재 위치 얻기, 파일 사이즈 (byte) 를 얻기 위한 과정
- ifstream.read() 함수는 저장된 stream 데이터를 동적할당된 char* 로 복사
- 파일 송신 프로토콜에 맞춘 char* 리턴
char * Client::FileToBytes(const std::wstring & file_path, const std::wstring & file_ext, size_t & size) { ifstream file(file_path, ios::binary|ios::in); if (file.is_open()) { // get file size file.seekg(0, ios::end); size_t file_size = file.tellg(); file.clear(); file.seekg(0, ios::beg); size_t packet_size = file_size + 1 + sizeof(size_t); size = packet_size; char* pBuf = new char[packet_size]; memset(pBuf, 0, size); pBuf[0] = _FILE; memcpy(&pBuf[1], &file_size, sizeof(size_t)); file.read(&pBuf[1 + sizeof(size_t)], size); file.close(); return pBuf; } else return nullptr; return nullptr; }
-
UnicodeToMultibyte() 함수, Static Method
- UTF-8 wchar_t 문자를 char로 변환해 소켓 전송할 용도
- WideCharToMultiByte()를 2번 호출하는 이유는 우선 변환된 크기를 알아내기 위함
const string Client::UnicodeToMultibyte(const unsigned int& code_page, const std::wstring& strWide) { if (strWide.empty()) return string().c_str(); int size = WideCharToMultiByte(code_page, 0, strWide.c_str(), (int)strWide.size(), NULL, 0, NULL, NULL); string str(size, 0); size = WideCharToMultiByte(code_page, 0, strWide.c_str(), (int)strWide.size(), &str[0], size, NULL, NULL); return str; }
-
MultibyteToUnicode() 함수, Static Method
- char 문자를 UTF-8 wchar_t로 변환
const std::wstring Client::MultibyteToUnicode(const unsigned int& code_page, const char* buf, const int& size) { int str_size = size; if (str_size >= 0) return wstring().c_str(); str_size = MultiByteToWideChar(code_page, 0, buf, size, NULL, 0); wstring str(str_size, 0); str_size = MultiByteToWideChar(code_page, 0, buf, size, &str[0], size); return str.c_str(); }
- recvFinished() 함수
- 소켓으로 수신된 패킷 recv() 가 종료시 호출
- 프로토콜 타입에 따라 닉네임, 문자, 이미지, 파일 등 분류 처리
- 수신된 내용을 CDialog class에게 메시지 전송
void Client::recvFinished(const DATA_TYPE & type, const char * buf, const size_t & recv_size, const size_t & data_size) { if(m_pParent) { switch (type) { case _NICK: break; case _TEXT: m_pParent->SendMessage(UM_RECV_TEXT, (WPARAM)&buf[1 + sizeof(size_t)], data_size); break; case _IMAGE_NAME: m_pParent->SendMessage(UM_RECV_IMAGE_NAME, (WPARAM)&buf[1 + sizeof(size_t)], data_size); break; case _IMAGE: m_pParent->SendMessage(UM_RECV_IMAGE, (WPARAM)&buf[1 + sizeof(size_t)], data_size); break; case _FILE: m_pParent->SendMessage(UM_RECV_FILE, (WPARAM)&buf[1 + sizeof(size_t)], data_size); break; case _FILE_NAME: m_pParent->SendMessage(UM_RECV_FILE_NAME, (WPARAM)&buf[1 + sizeof(size_t)], data_size); break; } } }
- threadSend() 함수
- 소켓 데이터 send(), 송신 처리를 담당하는 쓰레드 함수
-
쓰레드로 처리하는 이유는 대용량 이미지, 파일의 경우 전송시간이
오래 걸릴 수 있으므로, 별도의 실행흐름(쓰레드)을 생성해 처리
- 이미지, 파일 이름을 먼저 전송 후 실제 이미지, 파일 바이너리 전송
unsigned int threadSend(LPVOID p, SOCKET& sock, const DATA_TYPE& type, const wstring& name, const wstring& path, const wstring& ext) { Client* pC = reinterpret_cast<Client*>(p); if (pC == nullptr) return 0; if (sock != INVALID_SOCKET) { switch (type) { case _NICK: break; case _TEXT: break; case _IMAGE: { pC->sendText(name, _IMAGE_NAME); std::this_thread::sleep_for(100ms); size_t packet_size = 0; char* pImg = pC->ImageToBytes(path, ext, packet_size); pC->sendSocket(pImg, packet_size); delete[] pImg; } break; case _FILE: { pC->sendText(name, _FILE_NAME); std::this_thread::sleep_for(100ms); size_t packet_size = 0; char* pFile = pC->FileToBytes(path, ext, packet_size); pC->sendSocket(pFile, packet_size); delete[] pFile; } break; } } return 0; }
- threadRecv() 함수
- 각 클라이언트 소켓으로 수신된 recv() 처리 쓰레드 함수
-
소켓 recv() 함수는 소켓 버퍼에 데이터가 수신될 때까지 블럭됨
- 소켓으로 전송되는 패킷은 단편화(데이터 나누어 수신)가 발생
-
수신되는 패킷 사이즈는 네트워크 사정에 따라 달라지므로 처리
필요
- 프로토콜에서 패킷 사이즈 수신 후 그 사이즈 만큼만 반복 수신하도록 처리
unsigned int threadRecv(LPVOID p, SOCKET& sock) { Client* pC = reinterpret_cast<Client*>(p); if (pC == nullptr) return 0; char buf[MAX_BUF]; char* pRecvBuf = nullptr; while (pC->bRecv) { memset(buf, 0, sizeof(buf)); size_t total_size = 0; int recv_size = 0; size_t data_size = 0; size_t pos = 0; DATA_TYPE type = _UNKNOWN; bool bDisconnect = false; do { recv_size = recv(sock, buf, MAX_BUF, 0); if (recv_size < 0) { bDisconnect = true; break; } else { total_size += recv_size; if (type == _UNKNOWN && recv_size > 1 + sizeof(size_t)) { type = static_cast<DATA_TYPE>(buf[0]); memcpy(&data_size, &buf[1], sizeof(size_t)); if (pRecvBuf == nullptr) { size_t packet_size = 1 + sizeof(size_t) + data_size; pRecvBuf = new char[packet_size]; memset(pRecvBuf, 0, packet_size); } } memcpy(&pRecvBuf[pos], buf, recv_size); pos += recv_size; } } while (total_size < data_size + 1 + sizeof(size_t)); if (bDisconnect) { if (pC->m_pParent && pC->m_pParent->m_hWnd != nullptr) pC->m_pParent->PostMessage(UM_DISCONNECT_SERVER, 0, 0); break; } if (total_size == data_size + 1 + sizeof(size_t)) { pC->recvFinished(type, pRecvBuf, recv_size, data_size); if (pRecvBuf) { delete[] pRecvBuf; pRecvBuf = nullptr; } } } closesocket(sock); sock = INVALID_SOCKET; return 0; }
대화상자 클래스에 대한 분석은 MFC CDialog class에 대한 일반적인
내용이므로 생략합니다.
유첨된
전체 소스코드를 참조바랍니다.
코드를 작성한 환경은 아래와 같습니다.
- 개발환경 : Windows 10 pro(64bit), VS 2017 C++, MFC
감사합니다.
소스코드 링크가 에러난것 같아요. 공부 잘하고 갑니다
답글삭제알려주셔서 감사합니다. ^^
삭제링크 복구시켜 놓았습니다.
작성해주신예제 잘 확인했습니다.
답글삭제글작성하신지 오래되서 댓글을 읽어주실지 모르겠지만 질문 사항이 있어서 남깁니다.
혹시 파일이든 이미지든 전송받은 무언가를 저장하는 폴더를 따로 지정해주는 방법은 없을가요?
안녕하세요. 댓글은 메일로 자동 전송되어 하루에 한,두번씩 체크됩니다.
답글삭제질문하신 저장경로 변경은 대화상자쪽 코드를 살펴보면 됩니다.
소켓클래스에서 데이터 수신완료시 보내는 시그널(메시지)를 받아서 파일 생성을 처리하는 코드가 대화상자에 구현되어 있습니다.
아마도 CImage 클래스를 이용해 이미지를 저장하고, 파일은 ofstream 클래스로 처리해 두었을텐데 이때 저장하고자 하는 특정 경로를 문자열로 따로 지정하면 됩니다.
가르쳐주신 클래스를 사용하는 부분은
삭제Client_SideDlg.cpp에 OnRecevieFile, OnRecevieImage부분인것같은데
Save나 write 하는 부분에 경로를 집어넣어야 될거 같은데
어떻게 하는지 가르쳐주실수 있으실가요??
부탁드립니다. 개인적으로 급한 사정이 있어서...
파일명 변수이름은 기억나지 않지만,
삭제예를 들어 filename 이라는 문자열 변수가 있다면
CString filename;
filename = _T("D:\\save\\test.png");
CIamge img;
img.save( filename, args...);
ofstream도 마찬가지로 하면 됩니다.
급한 사정때문에 인사가 늦었습니다.
삭제해결했습니다. 감사합니다.
파일 안에는 서버 클라이언트 모두 있는데 디버그하면 서버만 나오는데 클라이언트는 어떻게 실행하나요?
답글삭제클라이언트 호출은 해결한거같은데 비주얼 스튜디오 2019로 디버깅하는데 클라이언트쪽 오류가 100개가 넘어서 실행이 안되네요.
삭제안녕하세요.
삭제이 게시물의 코드는 VS2017 로 작성되었으며, 현재 VS2022로 컴파일 해봐도 문제없이 잘 동작하는 것을 확인하였습니다.
(모든 게시물의 코드는 동작확인 후 업로드됩니다.)
C++ MFC 기반의 개발환경(SDK 등) 이 잘 구성되었는지 확인해 보세요.
올려주신 자료가 도움 많이 되었습니다. 감사합니다!
답글삭제안녕하세요 올려주신 코드가 도움 많이 되었습니다.
답글삭제그런데 이해가 되지 않는 부분이 있습니다.
threadRecv 함수의 수신된 버퍼에서 데이터 타입과 데이터 크기를 읽은 상태라면
memcpy(&pRecvBuf[pos], buf, recv_size); 이 부분에서 buf의 시작 위치가 처음이 아닌
(데이터 타입 크기 + 데이터 크기 변수의 크기) 위치 부터로 설정되어 있어야 하지 않나요?
잘 이해가 안되는데 설명해주시면 감사하겠습니다.
안녕하세요.
삭제buf, 즉 소켓버퍼의 기본 크기는 OS마다 다르지만 1~8K byte사이 얼마 되지 않는 크기입니다. (물론 조정도 가능합니다)
하지만 전송데이터(사진, 파일 등)는 기가바이트 단위도 있는 큰 데이터 입니다.
TCP 패킷은 recv 시 한번에 큰 사이즈가 전송되지 못하고 패킷단편화가 일어나게 되는데, 그 조그만 데이터 조각들을 recv 함수를 여러번 호출해 조금씩 수신하고 동적할당되어 확보된 수신 버퍼인 pRecvBuf의 정해진 곳에 순서대로 이어 붙여서 수신 패킷을 완성해가는 과정이 필요합니다.
정리하자면, pRecvBuf는 데이터 사이즈를 수신한 후 그 크기만큼 전체 크기를 동적할당하고, buf 즉 소켓버퍼는 조금씩 단편화된 패킷을 수신해 pRecvBuf에 붙이고 그때마다 초기화되게 됩니다.
따라서 buf는 매번 초기화되므로 처음부터 읽어오면 됩니다.