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


Study/C , C++ , MFC  2020. 4. 14. 17:14

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


이 글은 C++에서 소켓(Socket) 통신을 하는 방법에 대한 글입니다.


소켓 통신이란 PC간에 인터넷을 이용해서 통신하는 것을 소켓 통신입니다. 사실 통신을 하기 위해서는 여러가지 설정을 해야 하는데 그것을 현재는 OS(운영체제)에서 거의 다 구현을 해 놓고, 그 프로세스간의 종착점인 소켓에 개발자는 패킷만 보내면 된다라는 뜻으로 소켓 통신이라고 합니다.

소켓 통신에는 하나의 프로토콜(규약)이 있는데 먼저 서버 소켓이 대기(Listen)를 하면 클라이언트 소켓이 접속(Connect)합니다. 서버 소켓에서는 승인하여(accept) 데이터 송수신(send, receive)를 합니다. 종료할 때는 어느 종단점(Server나 client)에서 종료를 해도 상관없습니다.

출처 - http://jkkang.net/unix/netprg/chap2/net2_1.html

그럼 먼저 Server 소켓을 만들어서 window의 텔넷으로 접속되는 것을 확인한 후에 그 사야에 맞추어서 클라이언트를 만들겠습니다.

// 소켓을 사용하기 위해서 라이브러리 참조해야 한다.
#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* print(vector<char>* str)
{
  // 포인트 위치
  int p = 0;
  // 버퍼 설정. +1은 \0를 넣기 위한 크기
  char out[BUFFERSIZE + 1];
  // return을 하기 위해서는 힙에 데이터를 선언 해야 한다.
  char* ret = new char[str->size() + 10];
  // 메모리 복사 "echo - "를 붙힌다.
  memcpy(ret, "echo - ", 7);
  // 콘솔 출력
  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';
    // 콘솔 메시지 콘솔 출력.
    cout << out;
  }
  cout << endl;
  // 에코 메시지는 끝에 개행 + ">"를 넣는다.
  memcpy(ret + p + 7, "\n>\0", 3);
  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로 메시지를 보낸다.
  const char* message = "Welcome server!\r\n>\0";
  send(clientSock, message, strlen(message) + 1, 0);
  // telent은 한글자씩 데이터가 오기 때문에 글자를 모을 buffer가 필요하다.
  vector<char> buffer;
  // 수신 데이터
  char x;
  while (1)
  {
    // 데이터를 받는다. 에러가 발생하면 멈춘다.
    if (recv(clientSock, &x, sizeof(char), 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;
      }
      // 콘솔에 출력하고 에코 메시지를 받는다.
      const char* echo = print(&buffer);
      // client로 에코 메시지 보낸다.
      send(clientSock, echo, buffer.size() + 10, 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));
  }
  // 종료가 되면 쓰레드 리스트에 남아 있는 쓰레드를 종료할 때까지 기다린다.
  for (auto ptr = clientlist.begin(); ptr < clientlist.end(); ptr++)
  {
    (*ptr)->join();
  }
  // 서버 소켓 종료
  closesocket(serverSock);
  // 소켓 종료
  WSACleanup();
  return 0;
}

그럼 위 소스를 실행하겠습니다.

Server Start가 콘솔에 출력이 되고 accept 함수에서 client가 접속할 때까지 대기합니다.

그럼 텔 넷으로 접속하겠습니다.

그리고 Hello world를 전송합니다.

서버에서도 클라이언트에서 온 메시지를 콘솔로 출력을 해서 echo를 붙여서 클라이언트(telnet)로 보내진 것을 확인되었습니다.

이제 서버는 만들어 졌습니다.


서버를 기반으로 클라이언트를 만들겠습니다.

// 소켓을 사용하기 위해서 라이브러리 참조해야 한다.
#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<char>* str)
{
  // 포인트 위치
  int p = 0;
  // 버퍼 설정. +1은 \0를 넣기 위한 크기
  char out[BUFFERSIZE + 1];
  // 콘솔 출력
  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);
    }
    // 콘솔 메시지 콘솔 출력.
    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가 필요하다.
  vector<char> buffer;
  // 수신 데이터
  char x;
  while (1)
  {
    // 데이터를 받는다. 에러가 발생하면 멈춘다.
    if (recv(sock, &x, sizeof(char), 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);
      // 개행을 넣는다.
      *(input + size + 1) = '\r';
      *(input + size + 2) = '\n';
      // 서버로 보내기
      send(sock, input, size + 3, 0);
      // 메시지가 exit라면 종료
      if (*input == 'e' && *(input + 1) == 'x' && *(input + 2) == 'i' && *(input + 3) == 't')
      {
        break;
      }
      continue;
    }
  }
  // 서버 소켓 종료
  closesocket(sock);
  // 소켓 종료
  WSACleanup();
  return 0;
}

이제 클라이언트를 실행해서 서버로 접속합니다.

접속해서 test / test를 작성해서 보냈습니다.

서버에서는 클라이언트에서 받은 메시지가 표시되고 다시 클라이언트에 echo 메시지가 표시됩니다. 접속이 된 것이 확인이 됩니다.


현재 제가 위에서 telnet으로 접속한 것과 client로 접속한 것 두 client가 접속되어 있는 다중 접속 상태가 되었습니다.

이제 종료를 해보겠습니다.

제가 중간에 다시 접속해서 임시 포트가 바뀌었습니다.;;;

다중 접속과 종료도 무난하게 됩니다.


참조 - https://docs.microsoft.com/en-us/windows/win32/api/winsock2/

참조 - http://www.cplusplus.com/forum/general/58677/


여기까지 C++에서 소켓(Socket) 통신을 하는 방법에 대한 글이었습니다.


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