[잠깐만~]
본 포스팅을 공부하기 전에 TCP/IP 기반 Server/Client 구조를 모르면 Line by Line으로 코드가 설명된 아래의 포스팅을 먼저 공부하고 오세요.
본 포스팅의 아래 포스팅의 upgrad 버전이기 때문에 Line by Line으로 코드 설명이 없습니다.
서버/클라이언트 데이터 반복적으로 주고받기
TCP/IP 통신은 서버와 클라이언트 간의 데이터 전송에 널리 사용되는 방법입니다. 이 블로그 게시물에서는 TCP/IP 프로토콜을 사용하여 서버에서 클라이언트로 또는 그 반대로 데이터를 보내는 간단한 C++ 프로그램의 구현에 대해 논의할 것입니다. 데이터 전송은 while 문을 사용하여 반복적으로 수행되므로 서버와 클라이언트가 여러 메시지를 주고받을 수 있습니다. 이때, 구조체(Struct)를 선언해서 서버/클라이언트 각각에 통신 소켓을 전송 및 수신 할 것입니다. 서버 및 클라이언트 프로그램을 구현하기 위해 C++의 소켓 프로그래밍 라이브러리를 사용합니다. 여기서, 소켓은 두 프로세스 간의 통신을 위한 endpoint 이며 소켓 프로그래밍 라이브러리는 소켓을 만들고 관리할 수 있는 기능 집합을 제공합니다.
Socket이란?
소켓은 네트워크에서 실행되는 두 프로그램 간의 양방향 통신 링크의 끝점입니다. IP 주소와 포트 번호의 조합으로 네트워크로 연결된 시스템에서 특정 프로세스를 식별하는 데 사용됩니다.
컴퓨터 네트워킹에서 소켓은 서로 다른 장치에서 실행 중인 프로세스 간에 연결을 설정하는 데 사용됩니다. 소켓은 통신 끝점 역할을 하며 데이터를 보내고 받는 송신 및 수신 프로세스 모두에서 사용됩니다. 소켓은 네트워크를 통해 연결된 다른 장치에서 실행되는 프로세스와 동일한 장치에서 실행되는 프로세스 사이의 연결을 설정하는 데 사용할 수 있습니다.
소켓은 이메일 송·수신, 인스턴트 메시징, 파일 전송, 오디오 및 비디오 스트리밍과 같은 다양한 유형의 네트워크 통신에 널리 사용됩니다. 소켓 프로그래밍 인터페이스는 애플리케이션이 TCP 및 UDP와 같은 다양한 프로토콜을 사용하여 네트워크를 통해 데이터를 송·수신하는 표준 방법을 제공합니다.
1단계: 서버 생성 + 예시 코드 첨부
서버를 생성하려면 먼저 socket() 함수를 사용하여 소켓을 생성해야 합니다. 이 함수는 주소 패밀리, 소켓 유형 및 프로토콜의 세 가지 매개변수를 사용합니다. 이 경우 주소 패밀리 AF_INET, 소켓 유형 SOCK_STREAM 및 프로토콜 IPPROTO_TCP를 사용합니다.
소켓을 생성한 후 bind() 함수를 사용하여 소켓을 특정 주소와 포트 번호에 Blinding 해야 합니다. 이 함수는 소켓 파일 설명자, 바인딩할 주소 및 주소 길이의 세 가지 매개 변수를 사용합니다.
다음으로 listen() 함수를 사용하여 클라이언트에서 들어오는 연결을 수신해야 합니다. 이 함수는 소켓 파일 설명자와 주어진 시간에 대기할 수 있는 최대 연결 수의 두 가지 매개 변수를 사용합니다.
클라이언트와 연결이 설정되면 서버는 accept() 함수를 사용하여 들어오는 연결을 수락해야 합니다. 이 함수는 소켓 파일 설명자, 클라이언트 주소 및 주소 길이의 세 가지 매개 변수를 사용합니다.
[참고]
Define 된 PORT와 SERVER_IP는 본인의 환경설정에 맞게 입력해야 됩니다.
▶ PORT : 프로그램의 고유 입력 번호라고 생각하면 쉽습니다. 그렇기 때문에 임의의 숫자로 입력하면 됩니다. (6000번대 이상의 값을 입력하는 것을 추천합니다.)
▶ SERVER_IP : 각 WiFi 또는 랜선에 따른 IP가 부여됩니다. CMD 창에서 ipconfig를 통해 현재 사용하고 있는 IP를 확인할 수 있습니다. https://easycode.tistory.com/19 포스팅 [목차] TCP/IP 네트워크 구성 클라이언트(client) 기초 지식 → It's like taking a restaurant order에서 자세히 확인할 수 있습니다.
[Server Part 예시 코드]
※ 코드 Test를 위해선 Server 프로젝트와 Client 프로젝트 각각의 만들어 실행해야 됩니다.
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<winsock2.h>
#include<iostream>
#include<string>
#include<sstream>
#include<fstream>
#pragma comment (lib, "Ws2_32.lib")
#define PORT 8000
#define PACKET_SIZE 1000
#define SERVER_IP "xxx.xxx.xxx.xxx" // 서버 아이피 입력하세요.
#define CAMERA_BUFFER_Value2 100
using namespace std;
typedef struct _EXAMPLE_SEND_PACKET
{
int32_t Counter;
char Value1[16];
int32_t Value2;
int32_t Value3;
int32_t Value4;
}_EXAMPLE_SEND_PACKET;
typedef struct _EXAMPLE_RECV_PACKET
{
int32_t Counter;
bool Value5;
bool Value6;
bool Value7;
bool Value8;
}_EXAMPLE_RECV_PACKET;
struct _EXAMPLE_SEND_PACKET clientRecv;
struct _EXAMPLE_RECV_PACKET ServerRequest;
int main(int argc, char* argv[], char* envp[])
{
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET hServerSocket, hClientSocket;
hServerSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
SOCKADDR_IN tServerAddr = {};
tServerAddr.sin_family = AF_INET;
tServerAddr.sin_port = htons(PORT);
tServerAddr.sin_addr.s_addr = htonl(INADDR_ANY);
bind(hServerSocket, (SOCKADDR*)&tServerAddr, sizeof(tServerAddr));
listen(hServerSocket, 5);
SOCKADDR_IN tClientAddr = {};
int iClientAddrLen = sizeof(tClientAddr);
hClientSocket = accept(hServerSocket, (SOCKADDR*)&tClientAddr, &iClientAddrLen);
////////////////////////////// 무한 루프 //////////////////////////////
while (true)
{
recv(hClientSocket, (char*)&ServerRequest, sizeof(struct _EXAMPLE_RECV_PACKET), 0);
clientRecv.Counter = ServerRequest.Counter + 1;
strncpy(clientRecv.Value1, "TEST GOOD", 16);
clientRecv.Value2 = 123456;
clientRecv.Value3 = 654321;
clientRecv.Value4 = 1;
send(hClientSocket, (char*)&clientRecv, sizeof(struct _EXAMPLE_SEND_PACKET), 0);
Sleep(1000); //1000ms 일시 정지 → 초당 1번
}
closesocket(hClientSocket);
closesocket(hServerSocket);
WSACleanup();
return 0;
}
[코드 설명]
이 코드는 Winsock 라이브러리를 사용하여 C++에서 TCP/IP 서버 및 클라이언트를 구현한 것입니다. 프로그램의 목적은 서버에서 클라이언트로 데이터를 보내고 클라이언트로부터 데이터를 반복적으로 받는 것입니다.
코드는 필요한 헤더 파일을 포함하고 WSAStartup 함수를 사용하여 Winsock 라이브러리를 초기화하는 것으로 시작합니다. 그런 다음 socket() 함수는 서버용 소켓을 만드는 데 사용되고 bind() 및 listen() 함수는 소켓을 특정 주소 및 포트 번호에 바인딩하고 클라이언트에서 들어오는 연결을 각각 수신하는 데 사용됩니다. accept() 함수는 클라이언트에서 들어오는 연결을 수락하는 데 사용됩니다.
다음으로 코드는 프로그램이 종료될 때까지 계속되는 while 루프에 들어갑니다. while 루프 내에서 recv() 함수는 ServerRequest라는 구조체 _EXAMPLE_RECV_PACKET에 저장되는 클라이언트로부터 데이터를 받는 데 사용됩니다. 수신된 데이터를 처리한 후 clientRecv라는 구조체 _EXAMPLE_SEND_PACKET의 값을 업데이트하고 업데이트된 데이터를 send() 함수를 사용하여 클라이언트로 보냅니다.
구조체 _EXAMPLE_SEND_PACKET 및 _EXAMPLE_RECV_PACKET은 서버와 클라이언트 간에 전송될 데이터를 저장하는 데 사용됩니다. 구조체에는 Counter, Value1, Value2 등과 같은 변수 세트가 있습니다. 이러한 변수는 서버와 클라이언트 간에 전송될 데이터를 저장하는 데 사용됩니다.
마지막으로 코드는 클라이언트 및 서버 소켓을 닫고 각각 closesocket() 및 WSACleanup() 함수를 사용하여 Winsock 라이브러리를 정리합니다.
2단계: 클라이언트 생성 + 예시 코드 첨부
클라이언트를 생성하려면 먼저 서버에서 했던 것처럼 socket() 함수를 사용하여 소켓을 생성해야 합니다. 그러나 이번에는 connect() 함수를 사용하여 소켓을 서버에 연결해야 합니다. 이 함수는 소켓 파일 설명자, 서버 주소 및 주소 길이의 세 가지 매개 변수를 사용합니다.
[참고]
Define 된 PORT와 SERVER_IP는 본인의 환경설정에 맞게 입력해야 됩니다.
▶ PORT : 프로그램의 고유 입력 번호라고 생각하면 쉽습니다. 그렇기 때문에 임의의 숫자로 입력하면 됩니다. (6000번대 이상의 값을 입력하는 것을 추천합니다.)
▶ SERVER_IP : 각 WiFi 또는 랜선에 따른 IP가 부여됩니다. CMD 창에서 ipconfig를 통해 현재 사용하고 있는 IP를 확인할 수 있습니다. https://easycode.tistory.com/19 포스팅 [목차] TCP/IP 네트워크 구성 클라이언트(client) 기초 지식 → It's like taking a restaurant order에서 자세히 확인할 수 있습니다.
[Client Part 예시 코드]
※ 코드 Test를 위해선 Server 프로젝트와 Client 프로젝트 각각의 만들어 실행해야 됩니다.
#include<stdio.h>
#include<stdlib.h>
#include <thread>
#include<winsock2.h>
#include<iostream>
#include<string>
#include<sstream>
#include<fstream>
#pragma comment (lib, "Ws2_32.lib")
#define PORT 8000
#define PACKET_SIZE 1000
#define SERVER_IP "xxx.xxx.xxx.xxx" // 서버 아이피를 입력하세요
#define CAMERA_BUFFER_Value2 100
using namespace std;
typedef struct _EXAMPLE_SEND_PACKET
{
int32_t Counter;
char Value1[16];
int32_t Value2;
int32_t Value3;
int32_t Value4;
}_EXAMPLE_SEND_PACKET;
typedef struct _EXAMPLE_RECV_PACKET
{
int32_t Counter;
bool Value5;
bool Value6;
bool Value7;
bool Value8;
}_EXAMPLE_RECV_PACKET;
////////////// Socket 통신을 위한 struct //////////////////
struct _EXAMPLE_SEND_PACKET clientRecv;
struct _EXAMPLE_RECV_PACKET ServerRequest;
int main(int argc, char* argv[], char* envp[])
{
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//// 1. TCP/IP 소켓 통신 시작
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////////////
ServerRequest.Counter = 0; //통신을 위한 Counter 값 0으로 초기화
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
SOCKET hSocket; //소켓 생성
hSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
SOCKADDR_IN tAddr = {};
tAddr.sin_family = AF_INET;
tAddr.sin_port = htons(PORT);
tAddr.sin_addr.s_addr = inet_addr(SERVER_IP);
///////////////////////// 무한 루프 ////////////////////////////
while (true)
{
/////////////// 서버쪽으로 Counter 증가 신호 전송 /////////////////
ServerRequest.Counter = ServerRequest.Counter + 1; // 일정 값 이상일 때 초기화 필요 없음.
ServerRequest.Value5 = TRUE;
ServerRequest.Value6 = TRUE;
ServerRequest.Value7 = TRUE;
ServerRequest.Value8 = TRUE;
/////////////////// 서버쪽으로 연결 요청 /////////////////////
connect(hSocket, (SOCKADDR*)&tAddr, sizeof(tAddr));
/////////////////// 서버쪽으로 전송 //////////////////////////
send(hSocket, (char*)&ServerRequest, sizeof(struct _EXAMPLE_RECV_PACKET), 0); //서버에 메세지를 전달
/////////////////// 서버로부터 데이터 수신 ///////////////////
recv(hSocket, (char*)&clientRecv, PACKET_SIZE, 0);
printf("----------------------------------------------------------------\n");
printf("-------------------------- Client Part -------------------------\n");
printf("----------------------------------------------------------------\n");
printf("Client_RecvData Counter : %d\n", clientRecv.Counter);
printf("Client_RecvData Value1 : %s\n", clientRecv.Value1);
printf("Client_RecvData Value2 : %d\n", clientRecv.Value2);
printf("Client_RecvData Value3 : %d\n", clientRecv.Value3);
printf("Client_RecvData Value4 : %d\n", clientRecv.Value4);
printf("----------------------------------------------------------------\n");
Sleep(1000); //1000ms 일시 정지 → 초당 1번
}
closesocket(hSocket);
WSACleanup(); // WSAStartup을 하면서 지정한 내용을 제거
return 0;
}
[코드 설명]
이 코드는 Winsock 라이브러리를 사용하여 C++에서 TCP/IP 클라이언트를 구현한 것입니다. 프로그램의 목적은 데이터를 서버로 보내고 서버로부터 데이터를 반복적으로 받는 것입니다.
코드는 필요한 헤더 파일을 포함하고 WSAStartup 함수를 사용하여 Winsock 라이브러리를 초기화하는 것으로 시작합니다. 그런 다음 socket() 함수를 사용하여 클라이언트용 소켓을 만듭니다.
다음으로 코드는 프로그램이 종료될 때까지 계속되는 while 루프에 들어갑니다. while 루프 내에서 ServerRequest라는 구조체 _EXAMPLE_RECV_PACKET이 새로운 카운터 값으로 업데이트되고 connect() 함수가 서버에 연결하는 데 사용됩니다. 업데이트된 데이터는 send() 함수를 사용하여 서버로 전송됩니다. recv() 함수는 clientRecv라는 구조체 _EXAMPLE_SEND_PACKET에 저장된 서버로부터 데이터를 받는 데 사용됩니다. 수신된 데이터는 콘솔에 인쇄됩니다.
구조체 _EXAMPLE_SEND_PACKET 및 _EXAMPLE_RECV_PACKET은 서버와 클라이언트 간에 전송될 데이터를 저장하는 데 사용됩니다. 구조체에는 Counter, Value1, Value2 등과 같은 변수 세트가 있습니다. 이러한 변수는 서버와 클라이언트 간에 전송될 데이터를 저장하는 데 사용됩니다.
마지막으로 이 코드는 각각 closesocket() 및 WSACleanup() 함수를 사용하여 클라이언트 소켓을 닫고 Winsock 라이브러리를 정리합니다.
3단계: 데이터 송수신
서버와 클라이언트 간의 연결이 설정되면 데이터 송수신을 시작할 수 있습니다. 이 예에서는 while 문을 사용하여 데이터를 보내고 받는 프로세스를 여러 번 반복합니다.
서버에서 클라이언트로 데이터를 보내려면 send() 함수를 사용합니다. 이 함수는 소켓 파일 설명자, 보낼 데이터 및 데이터 길이의 세 가지 매개 변수를 사용합니다.
클라이언트로부터 데이터를 수신하기 위해 서버는 recv() 함수를 사용합니다. 이 함수는 소켓 파일 설명자, 데이터를 저장할 버퍼 및 데이터의 최대 길이의 세 가지 매개 변수를 사용합니다.
클라이언트는 send() 함수를 사용하여 서버로 데이터를 전송하고 recv() 함수를 사용하여 서버에서 데이터를 수신하여 동일한 작업을 역순으로 수행합니다.
아래의 실행 결과를 보면 Server에 저장된 Value1~4까지의 값이 반복적으로 Client Part에 출력되는 것을 볼 수 있습니다.
이러한 과정을 통해 소켓 프로그래밍 라이브러리를 사용하여 while 문을 사용하여 반복적으로 데이터를 주고받을 수 있는 서버와 클라이언트를 만들 수 있었습니다. 이 지식을 통해 이제 서버와 클라이언트 간의 데이터 통신이 필요한 자체 애플리케이션을 만들 수 있습니다.
댓글