[C++] C++과 C#의 소켓 통신을 이용해 파일 전송하는 방법


Development note/C , C++ , MFC  2020. 4. 16. 19:52

안녕하세요. 명월입니다.


이 글은 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#의 소켓 통신을 이용해 파일 전송하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.