C++/C++

[C++] TCP socket 통신 구현

sihyeong 2022. 12. 19. 01:35

소켓

  • 소켓은 컴퓨터가 통신을 하기 위한 도구이다.
  • 컴퓨터 네트워크ㅡ를 경유히 종착점이다.
  • 오늘날 대부분의 통신은 인터넷 프로토콜을 기반으로 하고 있으며 대부분의 네트워크 소켓은 인터넷 소켓이다.
  • 한마디로 대부분이 소켓을 사용한다.
  • 인터넷의 웹이 동작하는 방식도 소켓을 기반으로 동작한다.

 

서버 - 클라이언트 환경은 아래의 그림의 작업을 통해 진행된다.

 

 

 

소켓 통신 사용 헤더

#include <WinSock2.h>
#include <WS2tcpip.h>

// 윈도우에서의 소켓을 사용하기 위해 초기화가 필요하고
// 초기화 함수가 ws2_32.lib의 WSCStartup()를 통해 진행된다.
#pragma comment(lib, "ws2_32.lib")

 

WSADATA 구조체

  • Windows 소켓 초기화 정보를 저장하기 위한 구조체

WSAStartup(소켓버전, WSADATA 구조체 주소)

  • Windows에 어떤 소켓을 사용할 것인지를 알려준다.
  • 버전을 알려줘야 하는데 WORD 타입으로 받기 때문에 2.2 버전을 사용할 경우, 실수를 정수값으로 변환해 넣어줘야 하고 MAKEWORD 매크로가 이를 도와준다.

WSACleanup()

  • 소켓 사용이 끝났음을 알려주는 부분이다.
  • 실제 소켓 코드는 WSAStartup()과 WSACleanup() 사이에 작성해야 한다.

 

 

socket(주소형식,  통신형식,  프로토콜)

  • socket은 핸들이다. 커널오브젝트를 접근을 위해 선언
  • AF_INET은 IPv4 사용을 지정
  • TCP통신을 위해선 SOCK_STREAM과 IPPROTO_TCP을 사용하고 둘은 쌍이다.
  • UDP통신은 SOCK_DGRAM과 IPPROTO_UDP 쌍이다.

https://learn.microsoft.com/ko-kr/windows/win32/api/winsock2/nf-winsock2-socket?f1url=%3FappId%3DDev16IDEF1%26l%3DKO-KR%26k%3Dk(WINSOCK2%252Fsocket)%3Bk(socket)%3Bk(DevLang-C%252B%252B)%3Bk(TargetOS-Windows)%26rd%3Dtrue 

 

socket 함수(winsock2.h) - Win32 apps

소켓 함수는 특정 전송 서비스 공급자에 바인딩된 소켓을 만듭니다.

learn.microsoft.com

 

SOCKADDR_IN

  • sockaddr_in을 통해 주소형식, ip, port 설정해준다.
  • 엔디안 문제가 있기 때문에 네트워크에서 사용하는 빅엔디안으로 변환하여 ip와 port를 지정한다.

 

connect(socket, sockaddr*, sockaddrSize)

  • servAddr을 통해 지정한 ip와 port를 이용한 서버에 연결을 원합니다 연결해주세요 라고 연결을 요청한다.
  • 연결이 될때까지 일부 대기하고, 서버가 닫혀있다면 리턴되며 종료된다.

 

recv(socket, cbuffer, PACKET_SIZE, flag); 

  • 연결이 된 후, recv를 호출하면 서버가 send해준 값을 recv로 받을 수 있다.
  • 데이터를 보내고 받을 때 byte단위로 작업되기 때문에 1byte인 char을 사용했다.
  • 보통 패킷의 형태로 보내고 받게되고, 패킷은 패킷크기, 메시지형식, 데이터1, 데이터2 ..... 이런식으로 보낸다.

 

클라이언트

int main()
{
	WSADATA wsaData;
	SOCKET hSocket;
	SOCKADDR_IN servAddr;

	// 1. 윈속 초기화
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		cout << "Failed WSAStartup()" << endl;

	// 2. socket 생성
	// 물리적인 모양이 다르다, sw는 함수의 인자로 모양을 결정
	// ip 형식, tcp, udp 등의 통신 환경 세팅
	// SOCK_STREAM : tcp
	// SOCK_DGRAM : udp
	hSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

	// 3. 서버 연결
	//		3-1. 연결할 서버 주소 셋팅
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;	// IPv4 사용 세팅
	inet_pton(AF_INET, "127.0.0.1", &servAddr.sin_addr);
	servAddr.sin_port = htons(30002);

	//		3-2 서버에 연결 시도
	//		blocking 함수, 완료까지 대기
	if (connect(hSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		cout << "Failed connect()" << endl;

	// 4. 서버로 부터 통신 대기
	int recvSize;
	char recvData[255];
	// 값이 들어올 때 까지 대기
	recvSize = recv(hSocket, recvData, sizeof(recvData), 0);
	if (recvSize == -1)
		cout << "recv() Error" << endl;
        
	cout << "recv " << recvSize << " message : " << recvData << endl;

	// 5. 소켓 종료 -> 윈속 종료
	closesocket(hSocket);
	WSACleanup();
	
	system("pause");
	return 0;
}

 

 

bind(listenSocket, &sockaddr, sockaddrSize)

  • 서버의 부분에서는 몇가지 추가 작업이 필요로 한다. 그 중 하나가 bind이다.
  • 소켓에 servAddr에서 초기화 한 ip, port 등을 묶어준다.
  • 한마디로 이러한 ip와 port를 socket에서 사용할거에요 알려주는 형식

 

listen(listenSocket, 최대 접속승인 수)

  • listenSocket을 이용해 접속을 승인할 수 있는 접속 대기 상태로 만들어주는 역할을 한다.
  • 한마디로 상대방에서 연결을 주면 해당 연결을 받을 수 있는 서버 상태로 만들어 주는 역할

 

accept(listenSocket, sockaddr&, sockaddrSize)

  • 클라이언트에서 connect()를 호출했다면 서버에서 accept()를 통해 연결 요청을 승인하는 부분이다.
  • 연결 요청이 들어올 때 까지 대기한다.
  • 연결이 완료된다면 연결이 된 소켓을 반환하게 된다.
  • 이 반환된 소켓을 통해 클라이언트와 상호작용을 하게 된다.

 

send()

  • 수신과 같은 맥락으로 데이터를 보내는 부분이다.

 

  • clientsocket과 연결을 끊고(종료)하고, 연결을 받는 소켓 listensocket도 종료하고
  • WSACleanup()을 통해 소켓 통신을 종료한다.

 

서버

int main()
{
	WSADATA wsaData;
	SOCKET listenSocket;	// listen 전용 소켓
	SOCKADDR_IN servAddr;

	// 1. 윈속 초기화
	if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		cout << "Failed WSAStartup()" << endl;

	// 2. socket 생성
	listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (listenSocket == INVALID_SOCKET)
		cout << "Failed socket()" << endl;

	// 3. 서버 자신의 주소를 셋팅
	//		3-1. 서버 주소 셋팅
	memset(&servAddr, 0, sizeof(servAddr));
	servAddr.sin_family = AF_INET;
	inet_pton(AF_INET, "127.0.0.1", &servAddr.sin_addr);
	servAddr.sin_port = htons(30002);
	// socket과 local address를 연결시켜주는 함수
	// socket을 만들고, listen 할 주소를 socket에 연결시켜주는 함수
	if (bind(listenSocket, (SOCKADDR*)&servAddr, sizeof(servAddr)) == SOCKET_ERROR)
		cout << "Binding Error" << endl;
	// 연결을 수신하는 상태로 소켓의 상태 변경
	// 소켓을 접속 대기 상태로 만들어준다.
	// 두번째 인자는 한꺼번에 요청 가능한 최대 접속승인 수를 의미
	if (listen(listenSocket, 5) == SOCKET_ERROR)
		cout << "listen Error" << endl;

	//		3-2. 클라이언트 연결 기다리기
	SOCKADDR_IN clientAddr;
	SOCKET clientSocket;
	int sizeClientAddr = sizeof(clientAddr);
	clientSocket = accept(listenSocket, (SOCKADDR*)&clientAddr, &sizeClientAddr);
	if (clientSocket == SOCKET_ERROR)
		cout << "Failed accept()" << endl;

	// 4. 클라이언트에게 send
	char sendData[255] = "안녕";

	send(clientSocket, sendData, sizeof(sendData)+1, 0);

	// 5. 소켓 종료 -> 윈속 종료
	closesocket(clientSocket);
	closesocket(listenSocket);
	WSACleanup();

	return 0;
}