안녕하세요. 명월입니다.
이 글은 C++과 C#의 소켓 통신을 이용해 파일 전송하는 방법에 대한 글입니다.
이전에 C++과 C#간의 소켓 통신에서 문자열을 주고 받는 방법에 대해 설명한 적이 있습니다.
링크 - [C++] C++과 C#의 소켓 통신을 하는 방법(문자열 송수신)
위 소켓 통신에서 중요한 점은 unicode를 wchar_t로 변환하는 방법, wchar_t를 unicode 타입으로 변환하는게 키 포인트였습니다.
그리고 C#에서의 byte 타입이 c++에는 없습니다. 그런데 그 byte는 사실 unsigned char와 같은 타입입니다.
그걸 이용해서 이번에는 파일을 전송하는 서버 프로그램을 작성하겠습니다.
// 소켓을 사용하기 위해서 라이브러리 참조해야 한다.
#pragma comment(lib, "ws2_32")
// inet_ntoa가 deprecated가 되었는데.. 사용하려면 아래 설정을 해야 한다.
#pragma warning(disable:4996)
#include <stdio.h>
#include <iostream>
#include <vector>
#include <thread>
// 소켓을 사용하기 위한 라이브러리
#include <WinSock2.h>
// 수신 버퍼 사이즈
#define BUFFERSIZE 1024
using namespace std;
// 클라이언트로 부터 오는 데이터 수신 함수
// 수신 순서가 데이터 사이즈, 데이터 순으로 수신된다.
unsigned char* receive(SOCKET clientSock, int* size)
{
// 처음에는 데이터의 사이즈가 온다. C++과 C#은 기본적으로 빅 엔디안이기 때문에 엔디안 변환은 필요없다.
// char*를 4바이트로 받아버리면 int형이 된다.
if (recv(clientSock, (char*)size, 4, 0) == SOCKET_ERROR)
{
cout << "error" << endl;
return nullptr;
}
// 데이터를 unsigned char형식으로 받는다. =byte
unsigned char* buffer = new unsigned char[*size];
if (recv(clientSock, (char*)buffer, *size, 0) == SOCKET_ERROR)
{
cout << "error" << endl;
return nullptr;
}
// 받은 데이터를 리턴한다.
return buffer;
}
// 접속되는 client별 쓰레드
void client(SOCKET clientSock, SOCKADDR_IN clientAddr, vector<thread*>* clientlist)
{
// 접속 정보를 콘솔에 출력한다.
cout << "Client connected IP address = " << inet_ntoa(clientAddr.sin_addr) << ":" << ntohs(clientAddr.sin_port) << endl;
// 데이터의 사이즈 변수
int size;
// 저장할 디렉토리
wchar_t buffer[BUFFERSIZE] = L"d:\\work\\";
// 저장할 파일명 변수
wchar_t filename[BUFFERSIZE];
// 데이터를 받는다.
unsigned char* data = receive(clientSock, &size);
// 첫번째는 파일명이다. 이번에는 C#에서 unicode식이 아닌 utf8형식으로 파일명을 보냈다.
// c++에서는 unicode를 다루기 때문에 변환이 필요하다. (MB_ERR_INVALID_CHARS)
MultiByteToWideChar(CP_UTF8, 0, (const char*)data, size, filename, BUFFERSIZE);
// 수신 데이터를 메모리에서 삭제
delete data;
// 디렉토리 + 파일명
wcscat(buffer, filename);
// 데이터를 다시 받는다. 이번엔 업로드하는 파일 데이터이다.
data = receive(clientSock, &size);
// 저장할 파일 객체를 받는다.
FILE* fp = _wfopen(buffer, L"wb");
if (fp != NULL)
{
// 파일 저장
fwrite(data, 1, size, fp);
// 파일 닫기
fclose(fp);
}
else
{
// 파일 객체 취득에 실패할 경우 콘솔 에러 표시
cout << "File open failed" << endl;
}
// 수신 데이터를 메모리에서 삭제
delete data;
// 송신 데이터 선언 byte=1를 보내면 송신 완료.
char ret[1] = { 1 };
// 클라이언트로 완료 패킷을 보낸다.
send(clientSock, ret, 1, 0);
// 소켓을 닫는다.
closesocket(clientSock);
// 접속 종료 정보를 콘솔에 출력한다.
cout << "Client disconnected IP address = " << inet_ntoa(clientAddr.sin_addr) << ":" << ntohs(clientAddr.sin_port) << endl;
// 쓰레드에서 쓰레드를 제거한다.
for (auto ptr = clientlist->begin(); ptr < clientlist->end(); ptr++)
{
if ((*ptr)->get_id() == this_thread::get_id())
{
clientlist->erase(ptr);
break;
}
}
}
// 실행 함수
int main()
{
// 클라이언트 접속 중인 client list
vector<thread*> clientlist;
// 소켓 정보 데이터 설정
WSADATA wsaData;
// 소켓 실행.
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
return 1;
}
// Internet의 Stream 방식으로 소켓 생성
SOCKET serverSock = socket(PF_INET, SOCK_STREAM, 0);
// 소켓 주소 설정
SOCKADDR_IN addr;
// 구조체 초기화
memset(&addr, 0, sizeof(addr));
// 소켓은 Internet 타입
addr.sin_family = AF_INET;
// 서버이기 때문에 local 설정한다.
// Any인 경우는 호스트를 127.0.0.1로 잡아도 되고 localhost로 잡아도 되고 양쪽 다 허용하게 할 수 있따. 그것이 INADDR_ANY이다.
addr.sin_addr.s_addr = htonl(INADDR_ANY);
// 서버 포트 설정...저는 9090으로 설정함.
addr.sin_port = htons(9090);
// 설정된 소켓 정보를 소켓에 바인딩한다.
if (bind(serverSock, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
{
// 에러 콘솔 출력
cout << "error" << endl;
return 1;
}
// 소켓을 대기 상태로 기다린다.
if (listen(serverSock, SOMAXCONN) == SOCKET_ERROR)
{
// 에러 콘솔 출력
cout << "error" << endl;
return 1;
}
// 서버를 시작한다.
cout << "Server Start" << endl;
// 다중 접속을 위해 while로 소켓을 대기한다.
while(1)
{
// 접속 설정 구조체 사이즈
int len = sizeof(SOCKADDR_IN);
// 접속 설정 구조체
SOCKADDR_IN clientAddr;
// client가 접속을 하면 SOCKET을 받는다.
SOCKET clientSock = accept(serverSock, (SOCKADDR*)&clientAddr, &len);
// 쓰레드를 실행하고 쓰레드 리스트에 넣는다.
clientlist.push_back(new thread(client, clientSock, clientAddr, &clientlist));
}
// 종료가 되면 쓰레드 리스트에 남아 있는 쓰레드를 종료할 때까지 기다린다.
if (clientlist.size() > 0)
{
for (auto ptr = clientlist.begin(); ptr < clientlist.end(); ptr++)
{
(*ptr)->join();
}
}
// 서버 소켓 종료
closesocket(serverSock);
// 소켓 종료
WSACleanup();
return 0;
}
이번에는 C++로 된 파일 서버로 파일을 전송하기 위한 C#으로 된 클라이언트 소스입니다.
using System;
using System.IO;
using System.Text;
using System.Net;
using System.Net.Sockets;
namespace Example
{
class Program
{
// 실행 함수
static void Main(string[] args)
{
// 업로드할 파일 정보를 취득한다.
FileInfo file = new FileInfo("d:\\work\\nowonbuntistory.png");
// stream을 취득한다.
using (FileStream stream = new FileStream(file.FullName, FileMode.Open, FileAccess.Read))
{
// 파일 binary를 가져온다.
byte[] data = new byte[file.Length];
stream.Read(data, 0, data.Length);
// 소켓을 연다.
using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP))
{
// 파일 서버로 접속한다.
socket.Connect(IPAddress.Parse("127.0.0.1"), 9090);
// 전송 람다 함수 (C++과 약속한 규약대로 데이터 사이즈를 보내고 데이터를 보낸다.)
Action<byte[]> Send = (b) =>
{
// 먼저 데이터 사이즈를 보내고.
socket.Send(BitConverter.GetBytes(b.Length), 4, SocketFlags.None);
// 데이터를 보낸다.
socket.Send(b, b.Length, SocketFlags.None);
};
// 먼저 파일 명을 전송한다. (C++에서 \0를 보내지 않으면 메모리 구분이 되지 않으니 \0를 포함해서 보낸다.)
// 이번에는 unicode가 아닌 utf8형식으로 전송합니다.
Send(Encoding.UTF8.GetBytes("Download.png\0"));
// 파일 바이너리 데이터를 보낸다.
Send(data);
// 서버로 부터 byte=1 데이터가 오면 클라이언트 종료한다.
byte[] ret = new byte[1];
socket.Receive(ret, 1, SocketFlags.None);
if (ret[0] == 1)
{
Console.WriteLine("Completed");
}
}
}
Console.WriteLine("Press any key...");
Console.ReadKey();
}
}
}
먼저 d:\work 폴더에 전송할 파일을 준비했습니다.
그리고 서버를 기동하겠습니다.
이번에는 클라이언트를 실행하여 파일을 전송합니다.
Completed가 출력이 되었으니 아마 파일 전송이 되고 byte=1의 데이터를 수신이 제대로 되었습니다.
서버를 보니 클라이언트가 접속하고 접속이 종료된 것이 확인됩니다.
d:\work 폴더를 보면 파일이 업로드가 되어서 저장이 된 것이 확인 되었습니다.
이번에는 반대로 구현해 보겠습니다.
C#으로 작성된 파일 서버에 C++ 클라인트로 파일을 전송해 보겠습니다.
먼저 C#으로 된 파일 서버입니다.
using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Threading;
namespace Example
{
class Program
{
// 실행 함수
static void Main(string[] args)
{
// 포트 9090으로 서버를 설정한다.
IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 9090);
// 소켓을 얻는다.
using (Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp))
{
// bind하고 Listen 대기한다.
server.Bind(ipep);
server.Listen(20);
// 콘솔 출력.
Console.WriteLine("Server Start... Listen port 9090...");
// 다중 접속 환경
while (true)
{
// 접속을 대기힌다.
ThreadPool.QueueUserWorkItem((_) =>
{
Socket client = (Socket)_;
// 클라이언트 정보 취득
IPEndPoint ip = (IPEndPoint)client.RemoteEndPoint;
// 클라이언트 정보 콘솔 출력
Console.WriteLine("Client connected IP address = {0} : {1}", ip.Address, ip.Port);
// 취득 람다 함수식 - 데이터의 사이즈를 받고 데이터를 받는다.
Func<byte[]> Receive = () =>
{
// big endian타입으로 데이터 크기를 받는다.
var data = new byte[4];
client.Receive(data, SocketFlags.None);
// binary 객체 선언
data = new byte[BitConverter.ToInt32(data, 0)];
// 데이터 받기
client.Receive(data, SocketFlags.None);
return data;
};
// 파일 이름 받기
var filename = Encoding.UTF8.GetString(Receive());
// 파일 데이터를 받고 저장한다.
File.WriteAllBytes("d:\\work\\" + filename, Receive());
// byte=1의 데이터를 보낸다.
client.Send(new byte[1] { 1 }, SocketFlags.None);
// 접속 종료 정보 콘솔 출력
Console.WriteLine("Client disconnected IP address = {0} : {1}", ip.Address, ip.Port);
}, server.Accept());
}
}
}
}
}
서버와 클라이언트의 프로토콜은 위 C++서버와 C#클라이언트와 같습니다.
먼저 파일 이름 길이 수신 - 파일 이름 수신 - 데이터 길이 수신 - 데이터 수신 - byte=1 송신의 형태입니다.
C++로 된 클라이언트 소스입니다.
// 소켓을 사용하기 위해서 라이브러리 참조해야 한다.
#pragma comment(lib, "ws2_32")
// inet_ntoa가 deprecated가 되었는데.. 사용하려면 아래 설정을 해야 한다.
#pragma warning(disable:4996)
#include <stdio.h>
#include <iostream>
#include <vector>
#include <thread>
// 소켓을 사용하기 위한 라이브러리
#include <WinSock2.h>
// 수신 버퍼 사이즈
#define BUFFERSIZE 1024
// 실행 함수
int main()
{
// 파일 리소스 취득
FILE* fp = fopen("d:\\work\\nowonbuntistory.png", "rb");
// 취득 실패하면 종료한다.
if (fp == NULL)
{
cout << "File open failed" << endl;
return 1;
}
// 파일 길이 취득
fseek(fp, 0, SEEK_END);
int size = ftell(fp);
fseek(fp, 0, SEEK_SET);
// 파일 데이터를 읽어온다.
unsigned char* data = new unsigned char[size];
fread(data, 1, size, fp);
// 파일 리소스 닫기
fclose(fp);
// 소켓 정보 데이터 설정
WSADATA wsaData;
// 소켓 실행.
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
return 1;
}
// Internet의 Stream 방식으로 소켓 생성
SOCKET sock = socket(PF_INET, SOCK_STREAM, 0);
// 소켓 주소 설정
SOCKADDR_IN addr;
// 구조체 초기화
memset(&addr, 0, sizeof(addr));
// 소켓은 Internet 타입
addr.sin_family = AF_INET;
// 127.0.0.1(localhost)로 접속하기
addr.sin_addr.s_addr = inet_addr("127.0.0.1");
// 포트 9090으로 접속
addr.sin_port = htons(9090);
// 접속
if (connect(sock, (SOCKADDR*)&addr, sizeof(SOCKADDR_IN)) == SOCKET_ERROR)
{
// 에러 콘솔 출력
cout << "error" << endl;
return 1;
}
// 업로드 파일 이름 전송
const wchar_t* filename = L"Download2.png";
// unicode에서 utf8형식으로 변환
char buffer[BUFFERSIZE];
WideCharToMultiByte(CP_ACP, 0, filename, wcslen(filename) * 2, buffer, BUFFERSIZE, NULL, NULL);
// 파일 이름 길이
int len = strlen(buffer);
// 파일 이름 길이 전송
send(sock, (char*)&len, 4, 0);
// 파일 이름 전송
send(sock, (char*)buffer, len, 0);
// 파일 사이즈 전송
send(sock, (char*)&size, 4, 0);
// 파일 데이터 전송
send(sock, (char*)data, size, 0);
// 데이터 메모리 해제
delete data;
// 수신 대기 byte=1오면 종료한다.
unsigned char ret[1];
recv(sock, (char*)ret, 1, 0);
if (ret[0] == 1)
{
cout << "Completed" << endl;
}
// 서버 소켓 종료
closesocket(sock);
// 소켓 종료
WSACleanup();
return 0;
}
이번에는 C#이 서버를 기동하겠습니다.
클라이언트를 실행해서 파일을 전송하겠습니다.
Completed가 출력이 되었으니 아마 파일 전송이 되고 byte=1의 데이터를 수신이 제대로 되었습니다.
서버를 보니 Client가 접속하고 접속이 끊긴 게 확인이 되었습니다.
Download2.png파일명으로 업로드가 된 것을 확인했습니다.
확실히 C++과 C#과의 소켓 통신의 글을 두번 작성하니 byte와 unsigned char와 같은 것도 알겠고 unicode와 utf8, ascii code에 대한 string도 감이 오네요.
C++에서 string과 인코딩에 대해서도 조사해서 글을 작성 해야 겠습니다.
여기까지 C++과 C#의 소켓 통신을 이용해 파일 전송하는 방법에 대한 글이었습니다.
궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.
'Development note > C , C++ , MFC' 카테고리의 다른 글
[C++] 필수 라이브러리 Boost 설치하기(boost::asio::threadpool 예제) (0) | 2020.04.23 |
---|---|
[C++] 환경 설정 파일(ini)을 사용하는 방법 (0) | 2020.04.22 |
[C++] 라이브러리를 작성해서 사용하기 (Window 환경의 lib, dll) (0) | 2020.04.21 |
[C++] C++에서 사용되는 문자열 타입(LPSTR, LPCSTR, LPCTSTR, LPCWSTR), 메모리 누수(memory leak) 체크하는 방법과 UTF8 변환하는 방법 (0) | 2020.04.17 |
[C++] C++과 C#의 소켓 통신을 하는 방법(문자열 송수신) (0) | 2020.04.15 |
[MFC] 서비스 프로그램 (6) | 2012.12.02 |
[C++] ShowWindow 메크로 상수, Window 메시지 상수 (0) | 2012.10.20 |
[C++] afxsock 통신 - 클라이언트편 (0) | 2012.09.30 |