C++ 소켓 프로그래밍 입문하기

정은성·2023년 5월 24일
3
post-thumbnail

※ Rookiss님의 [C++과 언리얼로 만드는 MMORPG 게임 개발 시리즈] Part4: 게임 서버 강의를 보고 정리한 글입니다.

윈도우 소켓 사용

소켓 프로그래밍은 함수가 굉장히 많다. 네트워크 단 코드는 한 번 만들어 두고 오랫동안 쓰니 다 외우고 이해하려하지말자.

게임 서버에서 클라이언트와 서버의 소켓 프로그래밍 세팅은 이런식으로 진행된다.

클라이언트

WSAStaratup(): 윈도우 소켓 시작

socket(): 소켓 생성

Ip,Port Setting: ip,port 세팅

connect(): 소켓(서버와) 연결

closeSocket(): 소켓닫기

WSACleanup() : 윈도우 소켓 끝내기

서버

WSAStaratup(): 윈도우 소켓 시작

socket(): 소켓 생성

Ip,Port Setting : ip,port 세팅

bind() : 바인드→ 정보를 묶어줌

listen(): 대기상태→ 클라이언트 요청을 받음

accept(): 클라이언트 요청 받기

closeSocket(): 소켓 닫기

WSACleanup(): 윈도우 소켓 끝내기

이것을 코드로 옮겨보자.

클라이언트 코드

윈도우 소켓 시작

WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
	return 0;

소켓 생성

SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
// 인자
// ad:Address Family (AF_INET = IPv4, AF_INET6 = IPv6)
// type: TCP(SOCK_STEAM) vs UDP(SOCK_DGRAM)
// protocol : 0 -> 알아서 골라줌

if (clientSocket == INVALID_SOCKET) {
	int32 errorCode = ::WSAGetLastError();
	cout << "Socket ErrorCode; " << errorCode << endl;
	return 0;
}

return 되는 SOCKET의 자료형을 보면 uint형이다 왜일까?

SOCKET엔 id같은 값이 들어가 있다. 나중에 요청하게 되면 운영체제가 그에 맞게 처리해준다!

Ip,Port 세팅

ip와 port는 합쳐서 주소가 된다. ex) XX동 YY호

SOCKADDR_IN serverAddr; // IPv4
::memset(&serverAddr, 0, sizeof(serverAddr)); // 메모리 0으로 밀어주기
serverAddr.sin_family = AF_INET; // IPv4

//경고가 뜨는 예전방법
	//serverAddr.sin_addr.s_addr = ::inet_addr("127.0.0.1"); // 문자열로 이루어진것을 변환하는 것
::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); // 127.0.0.1 = 루프백 ip -> 내 컴퓨터
serverAddr.sin_port = ::htons(7777); // port 세팅

hton: host to network short

왜 포트 세팅으로 htons라는 함수를 거쳐 세팅해줄까?

바로 숫자를 저장하는 방법 때문이다. 숫자를 저장하는 방법엔 두 가지가 있다.

Little-endian vs BigEndian

0x12345678

[0x78][0x56] [0x34][0x12] ← Little

[0x12][0x34] [0x56][0x78] ← Big == network 표준

이렇게 저장하는 법이 여러가지 이기 때문에 두 가지 기기의 저장방법이 일치하지 않으면 같은 값을 보고도 다르게 해석 할 수 있다.

그래서 그것을 맞춰주기 위해 htons라는 함수를 통해 포트를 세팅하는 것이다!

Connect

// 연결할 소켓, 주소, 주소 크기
if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
		int32 errCode = ::WSAGetLastError();
		cout << "Connect ErrorCode: " << errCode << endl;
		return 0;
	}

왜 주소를 SOCKADDR*로 캐스팅해줄까?

현재 우리는 AF_INET, SOCK_STEAM을 통해 소켓을 생성해서 SOCKADDR_IN형으로 만들어줬지만 소켓은 다양한 타입으로 생성할 수 있기 때문에 SOCKADDR이라는 형으로 변환하여 사용한다.

ClsoseSocket

// 소켓 리소스 반환
::closesocket(clientSocket);

윈도우 소켓 Cleanup

// 윈속 종료
::WSACleanup();

WSACleanup은 Startup해준 횟수만큼 진행해 주어야한다.

전체 코드

#include<WinSock2.h>
#include <MSWSock.h>
#include<WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
	// 원속 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

	// 인자
	// ad:Address Family (AF_INET = IPv4, AF_INET6 = IPv6)
	// type: TCP(SOCK_STEAM) vs UDP(SOCK_DGRAM)
	// protocol : 0 -> 알아서 골라줌
	// return descriptor
	SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
	// uint형인데 왜 일까?
	// socket id이다. 나중에 요청하게 되면 id값을 넘겨줘 운영체제가 그에 맞게 처리하는 것

	if (clientSocket == INVALID_SOCKET) {
		int32 errorCode = ::WSAGetLastError();
		cout << "Socket ErrorCode; " << errorCode << endl;
		return 0;
	}

	// 연결할 목적지 -> IP + Port ex) XX아파트 YY 호
	SOCKADDR_IN serverAddr; // IPv4
	::memset(&serverAddr, 0, sizeof(serverAddr));
	serverAddr.sin_family = AF_INET;
	//serverAddr.sin_addr.s_addr = ::inet_addr("127.0.0.1"); // 문자열로 이루어진것을 변환하는 것
	::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); // 127.0.0.1 루프백 ip -> 내 컴퓨터
	serverAddr.sin_port = ::htons(7777); 
	// htons: host to network short
	// 왜 쓸까? 숫자를 저장하는 방법 때문이다.
	// Little-Endian vs Big-Endian
	// ex) 0x012345678 4바이트 정수
	// low [0x78][0x56][0x34][0x12] high < little
	// low [0x12][0x34][0x56][0x78] high < big = network 표준

	if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
		int32 errCode = ::WSAGetLastError();
		cout << "Connect ErrorCode: " << errCode << endl;
		return 0;
	}

	cout << "Connected To Server!" << endl;

	while (true) {
		// TODO

		this_thread::sleep_for(1s);
	}

	// 소켓 리소스 반환
	::closesocket(clientSocket);

	// 윈속 종료 -> Start 해준 만큼
	::WSACleanup();
}

서버 코드

윈도우 소켓 시작

동일하다.

소켓 생성

동일하다.

Ip,Port 세팅

SOCKADDR_IN serverAddr; // IPv4
::memset(&serverAddr, 0, sizeof(serverAddr)); // 값을 0으로 밀어주기
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
serverAddr.sin_port = ::htons(7777);

INADDR_ANY: 알아서 주소 값을 할당해줌.

→ 만약 주소 값을 직접 할당해주면 그 곳으로 밖에 접속 되지 않음.

하지만 INADDR_ANY를 사용하면 그 주소값이 안되더라도 다른 시도를 할 수 있다.

Bind

if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
	int32 errCode = ::WSAGetLastError();
	cout << "Bind ErrorCode: " << errCode << endl;
	return 0;
}

소켓에 주소를 할당 해준다.

Listen

							//소켓, 백로그
if (::listen(listenSocket, 10) == SOCKET_ERROR) {
	int32 errCode = ::WSAGetLastError();
	cout << "Listen ErrorCode: " << errCode << endl;
	return 0;
}

backlog: 대기열 수

Accept

SOCKADDR_IN clientAddr; // 클라이언트와 연결된 소켓을 만들어줌
::memset(&clientAddr, 0, sizeof(clientAddr)); // 메모리 오염 대비 0으로 모두 밀어줌
int32 addrLen = sizeof(clientAddr); // 길이

// 클라이언트에 접속 시도가 오면 대입
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket == INVALID_SOCKET) {
	int32 errCode = ::WSAGetLastError();
	cout << "Accept ErrorCode: " << errCode << endl;
	return 0;
}

ClsoseSocket

동일하다.

윈도우 소켓 Cleanup

동일하다.

전체 코드

#include<WinSock2.h>
#include <MSWSock.h>
#include<WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")

int main()
{
	// 원속 초기화 (ws2_32 라이브러리 초기화)
	// 관련 정보가 wsaData에 채워짐
	WSAData wsaData;
	if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
		return 0;

	// 인자
	// ad:Address Family (AF_INET = IPv4, AF_INET6 = IPv6)
	// type: TCP(SOCK_STEAM) vs UDP(SOCK_DGRAM)
	// protocol : 0 -> 알아서 골라줌
	// return descriptor
	SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
	// uint형인데 왜 일까?
	// socket id이다. 나중에 요청하게 되면 id값을 넘겨줘 운영체제가 그에 맞게 처리하는 것

	if (listenSocket == INVALID_SOCKET) {
		int32 errorCode = ::WSAGetLastError();
		cout << "Socket ErrorCode; " << errorCode << endl;
		return 0;
	}

	
	// 나의 주소 -> IP + Port ex) XX아파트 YY 호
	SOCKADDR_IN serverAddr; // IPv4
	::memset(&serverAddr, 0, sizeof(serverAddr)); // 값을 0으로 밀어주기
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY); // < 알아서 해줌 주소를 직접 적으면 그 곳으로 밖에안됨
													  // 하지만 알아서 해주게되면 모든 것으로 시도를 할 수 있다. 
	serverAddr.sin_port = ::htons(7777);
	
	// 안내원 폰 세팅
	if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
		int32 errCode = ::WSAGetLastError();
		cout << "Bind ErrorCode: " << errCode << endl;
		return 0;
	}

	// 영업 시작
	// backlog: 대기열 수
	if (::listen(listenSocket, 10) == SOCKET_ERROR) {
		int32 errCode = ::WSAGetLastError();
		cout << "Listen ErrorCode: " << errCode << endl;
		return 0;
	}

	while (true) {
		SOCKADDR_IN clientAddr;
		::memset(&clientAddr, 0, sizeof(clientAddr));
		int32 addrLen = sizeof(clientAddr);

		SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
		if (clientSocket == INVALID_SOCKET) {
			int32 errCode = ::WSAGetLastError();
			cout << "Accept ErrorCode: " << errCode << endl;
			return 0;
		}

		// 손님 
		char ipAddress[16];
		::inet_ntop(AF_INET, &clientAddr.sin_addr, ipAddress, sizeof(ipAddress));
		cout << "Client Connected! IP = " << ipAddress << endl;

		// TODO
	}

	::WSACleanup();

}

0개의 댓글