[C++] C++과 C#의 소켓 통신을 하는 방법(문자열 송수신)


Development note/C , C++ , MFC  2020. 4. 15. 20:21

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


이 글은 C++과 C#의 소켓 통신을 하는 방법(문자열 송수신)에 대한 글입니다.


이전에 C++의 소켓 통신과 C#의 소켓 통신에 대해 설명한 적이 있습니다.

링크 - [C#] 소켓 통신 - 1

링크 - [C++] 소켓(Socket) 통신을 하는 방법


제가 개발 일을 시작하기 시작한 10년 전쯤에는 C++의 프로그램도 많았고 프로젝트도 꽤 많았습니다. 최근에는 C++은 거의 없고, 보통은 Java나 C#이 대세이긴 합니다.

그 당시에 제가 기존의 C++서버가 있고 C# 클라이언트 프로그램을 만들어서 통신 할 때, 서로 통신이 제대로 되지 않아서 중간에 포기했던 적이 있습니다. 그 때는 선배들이 소켓 통신 할 때 같은 프로그램 언어가 아니면 통신이 제대로 되지 않는다고 선배들한테 혼났던 기억이 나네요.

그때도 생각하면 통신 소켓 자체가 통신 표준인데 프로그램 언어가 다르다는 이유로 왜 통신이 되지 않을까 생각한 적이 있었습니다만, 이런 저런 시간에 쫓겨 조사도 못하고 그냥 그렇구나 하고 넘어갔네요.. 근데 지금 생각하면 참 말도 안되는 이야기이고 우스운 이야기이지만 그랬던 적이 있습니다.


위 프로그램 언어가 다르면 소켓 통신이 되지 않는다라는 건 반은 맞고 반은 틀린 말입니다.

정확히는 프로그램 언어가 다른 이유가 아니고 소켓이 취급하는 자료형이 다른 것입니다.


C#은 기본적으로 소켓을 전송할 때, byte단위이고 문자열 자체가 Unicode로 되어 있습니다.

C++은 소켓이 char 단위이고 문자열이 const char*로 아스키 코드로 움직입니다. 이것을 맞추어야 합니다. 우리가 문자열을 한글을 사용하기 때문에 Unicode로 C++를 맞추어야 합니다.

C++에서 유니코드를 사용하려면 const char* 타입이 아닌 const wchar_t* 타입으로 사용해야 합니다.

그럼 먼저 이전에 작성했던 소켓 프로그램에서 타입을 바뀌겠습니다.

// 소켓을 사용하기 위해서 라이브러리 참조해야 한다.
#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;
// 수신 했을 때, 콘솔 출력 및 echo 데이터 만드는 함수
// 리턴 타입을 char*에서 wchar_t*로 변경하고 파라미터도 vector<char>에서 vector<wchar_t>로 변경했다.
wchar_t* print(vector<wchar_t>* str)
{
  // 포인트 위치
  int p = 0;
  // 버퍼 설정. +2은 \0를 넣기 위한 크기 (버퍼도 wchar_t타입으로 설정)
  wchar_t out[BUFFERSIZE + 2];
  // return을 하기 위해서는 힙에 데이터를 선언 해야 한다. wchar_t타입으로 변경
  wchar_t* ret = new wchar_t[str->size() + 10];
  // 메모리 복사 "echo - "를 붙힌다.
  // wchar_t는 2byte 단위이기 때문에 메모리 설정을 7이 아닌 14로 두배로 설정한다.
  memcpy(ret, L"echo - ", 14);
  // 콘솔 출력
  cout << "From Client message : ";
  // buffer사이지를 넘어서는 데이터일 경우 반복을 통해서 받는다.
  for (int n = 0; n < (str->size() / BUFFERSIZE) + 1; n++)
  {
    // 버퍼 사이즈 설정
    int size = str->size();
    // 수신 데이터가 버퍼 사이즈를 넘었을 경우.
    if (size > BUFFERSIZE) {
      if (str->size() < (n + 1) * BUFFERSIZE)
      {
        size = str->size() % BUFFERSIZE;
      }
      else
      {
        size = BUFFERSIZE;
      }
    }
    // echo 메시지와 콘솔 메시지를 작성한다.
    for (int i = 0; i < size; i++, p++)
    {
      out[i] = *(str->begin() + p);
      if (out[i] == '\0')
      {
        out[i] = ' ';
      }
      *(ret + p + 7) = out[i];
    }
    out[size] = '\0';
    // 콘솔 메시지 콘솔 출력.
    wprintf(L"%s", out);
  }
  cout << endl;
  // 에코 메시지는 끝에 개행 + ">"를 넣는다.
  memcpy(ret + p + 7, L"\n>\0", 6);
  return ret;
}
// 접속되는 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;
  // client로 메시지를 보낸다.(wchar_t 타입으로 설정해서 보낸다.)
  const wchar_t* message = L"Welcome server!\r\n>\0";
  // send함수가 char* 형식으로 보낼 수 있기 때문에 타입 캐스팅을 한다.
  // 사이즈는 문자열 길이 * 2 그리고 마지막 \0를 보내기 위한 +2를 추가한다.
  send(clientSock, (char*)message, wcslen(message) * 2 + 2, 0);
  // telent은 한글자씩 데이터가 오기 때문에 글자를 모을 buffer가 필요하다. (wchar_t 타입으로 선언)
  vector<wchar_t> buffer;
  // 수신 단위가 char가 아닌 wchar_t이다.
  wchar_t x;
  while (1)
  {
    // 수신을 받을 때도 char* 형식으로 받기 때문에 타입 캐스팅을 한다.
    if (recv(clientSock, (char*)&x, sizeof(x), 0) == SOCKET_ERROR)
    {
      cout << "error" << endl;
      break;
    }
    // 만약 buffer의 끝자리가 개행일 경우
    if (buffer.size() > 0 && *(buffer.end() - 1) == '\r' && x == '\n')
    {
      // 메시지가 exit일 경우는 수신대기를 멈춘다.
      if (*buffer.begin() == 'e' && *(buffer.begin() + 1) == 'x' && *(buffer.begin() + 2) == 'i' && *(buffer.begin() + 3) == 't') {
        break;
      }
      // 콘솔에 출력하고 에코 메시지를 받는다.
      wchar_t* echo = print(&buffer);
      // client로 에코 메시지 보낸다.
      send(clientSock, (char*)echo, buffer.size() * 2 + 20, 0);
      // 에코 메시지를 힙(new을 사용한 선언)에 선언했기 때문에 메모리 해지한다.
      delete echo;
      // 버퍼를 비운다.
      buffer.clear();
      // 다음 메시지 수신 대기
      continue;
    }
    // 버퍼에 글자를 하나 넣는다.
    buffer.push_back(x);
  }
  // 수신 대기가 끝나면 client와 소켓 통신을 끊는다.
  closesocket(clientSock);
  // 접속 정보를 콘솔에 출력한다.
  cout << "Client disconnected IP address = " << inet_ntoa(clientAddr.sin_addr) << ":" << ntohs(clientAddr.sin_port) << endl;
  // threadlist에서 현재 쓰레드를 제거한다.
  for (auto ptr = clientlist->begin(); ptr < clientlist->end(); ptr++)
  {
    // thread 아이디가 같은 것을 찾아서
    if ((*ptr)->get_id() == this_thread::get_id())
    {
      // 리스트에서 뺀다.
      clientlist->erase(ptr);
      break;
    }
  }
  // thread 메모리 해지는 thread가 종료 됨으로 자동으로 처리된다.
}
// 실행 함수
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;
}

서버를 만들었습니다.

전체적으로 기존의 char의 타입을 전부 wchar_t로 변경했습니다. wchar_t는 2byte이고 char는 1byte입니다. 그것만 주의하면 바꾸는데 크게 어려움은 없습니다.


클라이언트를 C#으로 작성하겠습니다.

using System;
using System.Threading;
using System.Linq;
using System.Text;
using System.Net;
using System.Net.Sockets;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 개행 코드
      char CR = (char)0x0D;
      char LF = (char)0x0A;
      // 수신 버퍼
      StringBuilder sb = new StringBuilder();
      // 소켓을 연다.
      using (Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP))
      {
        // 로컬의 9090포트로 접속한다.
        socket.Connect(IPAddress.Parse("127.0.0.1"), 9090);
        // 수신을 위한 쓰레드
        ThreadPool.QueueUserWorkItem((_) =>
        {
          while (true)
          {
            // 서버로 오는 메시지를 받는다.
            byte[] ret = new byte[2];
            socket.Receive(ret, 2, SocketFlags.None);
            // 메시지를 unicode로 변환해서 버퍼에 넣는다.
            sb.Append(Encoding.Unicode.GetString(ret, 0, 2));
            // 개행 + \n이면 콘솔 출력한다.
            if (sb.Length >= 4 && sb[sb.Length - 4] == CR && sb[sb.Length - 3] == LF && sb[sb.Length - 2] == '>' && sb[sb.Length - 1] == '\0')
            {
              // 버퍼의 메시지를 콘솔에 출력
              string msg = sb.ToString();
              Console.Write(msg);
              // 버퍼를 비운다.
              sb.Clear();
            }
          }
        });
        // 송신을 위한 입력대기
        while (true)
        {
          // 콘솔 입력 대기
          string k = Console.ReadLine();
          byte[] data = Encoding.Unicode.GetBytes(k + "\r\n");
          // 송신.
          socket.Send(data, data.Length, SocketFlags.None);
          // exit 명령어가 오면 종료
          if ("exit".Equals(k, StringComparison.OrdinalIgnoreCase))
          {
            break;
          }
        }
      }
    }
  }
}

이제 서버를 먼저 실행해 보겠습니다.

이제 클라이언트를 접속하고 메시지를 보내보겠습니다.

메시지가 잘 전달되고 echo 메시지도 제대로 나옵니다. 한글도 해보고 싶은데.. 제 PC가 한글 OS가 아니라서...

접속 종료도 됩니다.


이제는 반대로 C#서버에 C++ 클라이언트를 접속하겠습니다.


먼저 서버로 된 C# 소스입니다.

using System;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;

namespace Example
{
  class Program
  {
    // 실행 함수
    static void Main(string[] args)
    {
      // 포트 9090으로 서버 설정
      IPEndPoint ipep = new IPEndPoint(IPAddress.Any, 9090);
      Socket server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
      // bind하고 서버 Listen 대기
      server.Bind(ipep);
      server.Listen(20);
      // 콘솔 출력
      Console.WriteLine("Server Start");
      // 다중 접속 시작
      while (true)
      {
        // ThreadPool로 접속이 되면
        ThreadPool.QueueUserWorkItem((_) =>
        {
          // 메시지 버퍼
          StringBuilder sb = new StringBuilder();
          // 클라이언트 받기
          Socket client = (Socket)_;
          // 클라이언트 정보
          IPEndPoint ip = (IPEndPoint)client.RemoteEndPoint;
          // 콘솔 출력
          Console.WriteLine("Client connected IP address = {0} : {1}", ip.Address, ip.Port);
          // 메시지 전송
          client.Send(Encoding.Unicode.GetBytes("Welcome server!\r\n>\0"), SocketFlags.None);
          try
          {
            // 수신 대기
            while (true)
            {
              // 서버로 오는 메시지를 받는다.
              byte[] ret = new byte[2];
              client.Receive(ret, 2, SocketFlags.None);
              // 메시지를 unicode로 변환해서 버퍼에 넣는다.
              sb.Append(Encoding.Unicode.GetString(ret, 0, 2));
              // 개행 + \n이면 콘솔 출력한다.
              if (sb.Length >= 2 && sb[sb.Length - 2] == '\r' && sb[sb.Length - 1] == '\n')
              {
                // exit면 접속을 끊는다.
                if (sb.Length >= 4 && sb[sb.Length - 4] == 'e' && sb[sb.Length - 3] == 'x' && sb[sb.Length - 2] == 'i' && sb[sb.Length - 1] == 't')
                {
                  break;
                }
                // 버퍼의 메시지를 콘솔에 출력
                string msg = sb.ToString();
                // echo 메시지 전송
                client.Send(Encoding.Unicode.GetBytes("echo - " + msg + ">\0"), SocketFlags.None);
                // 콘솔 출력
                Console.Write(msg);
                // 버퍼를 비운다.
                sb.Clear();
              }
            }
          }
          catch
          {
          // 에러 발생하면 종료
          }
          // 접속 종료 메시지 콘솔 출력
          Console.WriteLine("Client disconnected IP address = {0} : {1}", ip.Address, ip.Port);
        // 클라이언트와 접속이 되면 Thread 생성
        }, server.Accept());
      }
      server.Close();
    }
  }
}

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
using namespace std;
// 콘솔에 메시지를 출력하는 함수
void print(vector<wchar_t>* str)
{
  // 포인트 위치
  int p = 0;
  // 버퍼 설정. +1은 \0를 넣기 위한 크기
  wchar_t out[BUFFERSIZE + 2];
  // 콘솔 출력
  cout << "From server message : ";

  for (int n = 0; n < (str->size() / BUFFERSIZE) + 1; n++)
  {
    // 버퍼 사이즈 설정
    int size = str->size();
    // 수신 데이터가 버퍼 사이즈를 넘었을 경우.
    if (size > BUFFERSIZE) {
      if (str->size() < (n + 1) * BUFFERSIZE)
      {
        size = str->size() % BUFFERSIZE;
      }
      else
      {
        size = BUFFERSIZE;
      }
    }
    // echo 메시지와 콘솔 메시지를 작성한다.
    for (int i = 0; i < size; i++, p++)
    {
      out[i] = *(str->begin() + p);
    }
    // 콘솔 메시지 콘솔 출력.
    wprintf(L"%s", out);
    //cout << out;
  }
}
// 실행 함수
int main()
{
  // 소켓 정보 데이터 설정
  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;
  }
  // telent은 한글자씩 데이터가 오기 때문에 글자를 모을 buffer가 필요하다.(wchar_t 타입으로 선언)
  vector<wchar_t> buffer;
  // 수신 데이터
  wchar_t x;
  while (1)
  {
    // 데이터를 받는다. 에러가 발생하면 멈춘다.
    // char* 형식으로 받기 때문에 타입 캐스팅을 한다.
    if (recv(sock, (char*)&x, sizeof(x), 0) == SOCKET_ERROR)
    {
      // 에러 콘솔 출력
      cout << "error" << endl;
      break;
    }
    // 버퍼에 글자를 하나 넣는다.
    buffer.push_back(x);
    // \r\n>\0가 나오면 콘솔에 출력하고 콘솔로 부터 메시지를 기다린다.
    if (buffer.size() > 4 && *(buffer.end() - 4) == '\r' && *(buffer.end() - 3) == '\n' && *(buffer.end() - 2) == '>' && *(buffer.end() - 1) == '\0')
    {
      // 메시지 출력
      print(&buffer);
      // 버퍼 초기화
      buffer.clear();
      // 콘솔로 부터 입력을 받는다.
      char input[BUFFERSIZE];
      // 유저로 부터 입력 받기
      cin >> input;
      // 입력받은 길이를 받는다.
      int size = strlen(input);
      // 개행을 넣는다.
      wchar_t buffer[BUFFERSIZE * 2];
      // char*에서 wchar_t*으로 변환하는 함수
      mbstowcs(&buffer[0], input, BUFFERSIZE * 2);
      *(buffer + size) = '\r';
      *(buffer + size + 1) = '\n';
      *(buffer + size + 2) = '\0';
      // 서버로 보내기
      // send함수가 char* 형식으로 보낼 수 있기 때문에 타입 캐스팅을 한다.
      send(sock, (char*)buffer, wcslen(buffer) * 2, 0);
      // 메시지가 exit라면 종료
      if (*input == 'e' && *(input + 1) == 'x' && *(input + 2) == 'i' && *(input + 3) == 't')
      {
        break;
      }
      continue;
    }
  }
  // 서버 소켓 종료
  closesocket(sock);
  // 소켓 종료
  WSACleanup();
  return 0;
}

이제 반대로 서버를 기동하겠습니다.

그리고 클라이언트를 기동하고 메시지를 보냅니다.

제대로 전송이 됩니다.

종료까지 깔끔하게 되네요.


C++에서 C#과 소켓 통신할 때는 이 wchar_t* 타입으로 변환이 중요합니다. 이게 물론 글자만 해당되는 것이고 파일등을 전송할 때는 unicode가 아닌 바이너리이기 때문에 그대로 받아도 됩니다. C++에서는 byte타입이 없는데 이것은 unsigned char와 같은 형식이기 때문에 char 대신 unsigned char*로 받아서 파일을 작성하면 됩니다.

설명이 어렵네요... 그냥 다음 글에서 한번 구현해 봐야겠습니다.


여기까지 C++과 C#의 소켓 통신을 하는 방법(문자열 송수신)에 대한 글이었습니다.


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