안녕하세요. 명월입니다.
이 글은 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#의 소켓 통신을 하는 방법(문자열 송수신)에 대한 글이었습니다.
궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.
'Development note > C , C++ , MFC' 카테고리의 다른 글
[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.16 |
[MFC] 서비스 프로그램 (6) | 2012.12.02 |
[C++] ShowWindow 메크로 상수, Window 메시지 상수 (0) | 2012.10.20 |
[C++] afxsock 통신 - 클라이언트편 (0) | 2012.09.30 |
[C++] afxsock 통신 - 서버편 (0) | 2012.09.30 |