KoreanFoodie's Study
[C++ 게임 서버] 3-2. 소켓 프로그래밍 기초 #2 본문
[C++ 게임 서버] 3-2. 소켓 프로그래밍 기초 #2
핵심 :
1. 소켓 프로그래밍 셋업에는 여러 API 들을 통한 세팅이 필요하다. 하지만 너무 겁먹지 말고, 흐름만 이해하자.
2. 클라이언트 쪽에서는 핸드폰 구입(client소켓 세팅)과 가게의 위치와 번호(서버 IP 주소와 포트)를 알아낸 후, 연결(::connect)을 시도하기만 하면 된다.
3. 서버 쪽에서는 핸드폰 구입(listen 소켓 세팅) 후, 가게의 위치와 번호(서버 IP 주소와 포트)를 설정한 후, 안내원의 폰을 개통(소켓과 서버 주소 ::bind)하고 영업을 시작(::listen)하면 된다.
이제 저번에 이야기했던 개념을 바탕으로, 실제 코드를 보자.
아마 익숙치 않은 코드가 잔뜩 등장을 하게 될 텐데, 하나하나 전부 알아야 한다는 부담을 내려놓고 진행을 하면 된다!
그럼 소켓 프로그래밍 실습을 시작~~~하겠습니다!
아참, 프로젝트 구조는 다음과 같다 :
나중에 DummyClient 와 GameServer 를 동시에 시작하려면 아래와 같은 설정을 바꿔주면 된다(프로젝트 > 속성).
먼저, DummyClient 라는 클래스를 만들고, 클라이언트 쪽 코드부터 만들어 보자. 일단, 윈도우 소켓을 초기화 한다.
// 윈속 초기화 (ws2_32 라이브러리 초기화)
// 관련 정보가 wsaData에 채워짐
WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
일단 윈도우 소켓을 다루려면, WSAData 를 초기화 해 주어야 하는데, WSA 는 Window Socket API 의 약자이다. 😀
그리고 이제 클라이언트의 핸드폰을, 아니, 소켓을 만들어 주어야 한다.
// ad : Address Family (AF_INET = IPv4, AF_INET6 = IPv6)
// type : TCP(SOCK_STREAM) vs UDP(SOCK_DGRAM)
// protocol : 0
// return : descriptor
SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET)
{
int32 errCode = ::WSAGetLastError();
cout << "Socket ErrorCode : " << errCode << endl;
return 0;
}
위를 보면, ::socket 을 사용하여 clientSocket 을 만들어 주는데... 각각의 인자는 무엇을 의미할까?
일단, 첫 인자는 Address Family 로, IPv4 를 쓸 것인지 IPv6 를 쓸 것인지를 결정한다. 두 번째인 type 은 TCP 를 사용할 것인지, UDP 를 사용할 것인지를 의미하며, 마지막인 protocol 의 경우, 0을 넣어주면 알아서 결정해 준다.
참고로 해당 함수에 대한 문서를 참고하면 더 자세한 정보를 얻을 수 있다.
만약 소켓이 잘못 만들어졌다면, errorCode 를 출력해 준다.
그리고.. 이제 목적지를 연결해 주자. 연결을 제대로 하려면 IP 주소와 Port 값이 필요한데, IP주소를 아파트로, Port 를 호수로 생각하면 쉽다 🤣
// 연결할 목적지는? (IP주소 + Port) -> 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"); << deprecated
::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
serverAddr.sin_port = ::htons(7777); // 80 : HTTP
// host to network short
// Little-Endian vs Big-Endian
// ex) 0x12345678 4바이트 정수
// low [0x78][0x56][0x34][0x12] high < little
// low [0x12][0x34][0x56][0x78] high < big = network
일단, 서버 주소인 serverAddr 를 선언하고, memset 으로 초기화를 해 준다.
그리고 나서, sin_family 를 설정하는데... 아까 IPv4 를 쓴다고 했으니, AF_INET 으로 넣어준다.
그리고 이제 실제 주소에 연결을 해 주어야 하는데, IP 주소의 값은 "127.0.0.1" 로 넣었다. 이 주소는 자기 자신에게 피드백 루프를 계속 주는 주소다. inet_pton 이라는 함수가 생소해 보이기는 한데... 너무 신경 쓰지는 말자 😅
그리고 나서, 서버의 포트를 지정한다. 이때 htons 라는 함수를 이용해, 7777 번 포트를 할당할 것이다.
아, 참고로 htons 는 host to network short 의 약자인데, 자매품으로 htonl 이라는 함수도 있다. 눈치 빠른 사람은 알겠지만... host to network long 의 약자이다 😉
그 아래는 Little-Endian 과 Bin-Endian 설명이 적혀 있는데, 보통 Little-Endian 을 많이 이용한다. 궁금하면 디스어셈블리를 켜 보자.
디버그 > 창 > 메모리를 킨 다음, 주소 검색에서 &i 를 쳐 보면, i 의 값이 위처럼 반대로 잡혀 있는 것을 알 수 있다. 역시.. ㅡMSVC 도 Little-Endian 이었나? 아니, 보통 OS 별로 나뉘니 윈도우가 Little-Endian 일지도.
어쨋든, 서버 주소까지 지정을 했으니, 이제 진짜로 연결을 해 보자.
if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
int32 errCode = ::WSAGetLastError();
cout << "Connect ErrorCode : " << errCode << endl;
return 0;
}
크게 어려운 부분은 없을 것이다. 인자는 말했다시피, 정의된 API 를 잘 따라가 주도록 하자.
자, 그럼 이제 연결이 되었을 것이다!
// ---------------------------
// 연결 성공! 이제부터 데이터 송수신 가능!
cout << "Connected To Server!" << endl;
while (true)
{
// TODO
this_thread::sleep_for(1s);
}
// ---------------------------
// 소켓 리소스 반환
::closesocket(clientSocket);
// 윈속 종료
::WSACleanup();
그럼 while 아래에 우리가 수행할 것들을 넣어주기만 하면 된다.
할 일을 다 했을 경우에는, 소켓 리소스를 반환하고 윈속을 종료해 주면 된다.
이제 우리가 배웠던 클라이언트 쪽 코드를 종합하면, 다음과 같은 흐름을 가짊을 알 수 있다 😊
#include "pch.h"
#include <iostream>
#include <winsock2.h>
#include <mswsock.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int main()
{
int i = 0x12345678;
// 윈속 초기화 (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_STREAM) vs UDP(SOCK_DGRAM)
// protocol : 0
// return : descriptor
SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET)
{
int32 errCode = ::WSAGetLastError();
cout << "Socket ErrorCode : " << errCode << endl;
return 0;
}
// 연결할 목적지는? (IP주소 + Port) -> 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"); << deprecated
::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
serverAddr.sin_port = ::htons(7777); // 80 : HTTP
// host to network short
// Little-Endian vs Big-Endian
// ex) 0x12345678 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);
// 윈속 종료
::WSACleanup();
}
그럼 이제 서버 쪽을 보자. 서버쪽도 사실 클라이언트와 비슷하다.
핸드폰을(리슨 소켓) 먼저 만들고, 건물과 주소를 설정(IP 주소와 포트)해 할당하고, 안내원 핸드폰을 개통(소켓과 주소를 연동)하고, 영업 개시를 알리면(실제로 패킷을 listen) 된다.
우리는 이 흐름대로 코드를 살펴볼 것이다. 😆
먼저, WSA 를 초기화하고 소켓을 만든다. 프로토콜은 소켓을 만들 때 지정된다.
// 윈속 초기화 (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_STREAM) vs UDP(SOCK_DGRAM)
// protocol : 0
// return : descriptor
SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (listenSocket == INVALID_SOCKET)
{
int32 errCode = ::WSAGetLastError();
cout << "Socket ErrorCode : " << errCode << endl;
return 0;
}
그리고, 주소를 할당한다.
// 나의 주소는? (IP주소 + Port)->XX 아파트 YY 호
SOCKADDR_IN serverAddr; // IPv4
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY); //< 니가 알아서 해줘
serverAddr.sin_port = ::htons(7777); // 80 : HTTP
이때, ::htonl(INADDR_ANY) 부분이 조금 다른데, 우리가 아까 클라이언트에서는 127.0.0.1 을 지정했으나, 서버에서 하드코딩된 IP 주소만 받는다고 하면 조금 문제가 생길 것이다. 왜냐하면 서버는 다양한 주소와 통신을 해야 할 테니까.
그래서 위 구문을 쓰면, 어떤 주소가 와도 서버가 알아서 통신을 연결해 주게 된다.
주소도 할당했겠다, 이제 핸드폰을 개통하고, 영업을 시작하자!
// 안내원 폰 개통! 식당의 대표 번호
if (::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
int32 errCode = ::WSAGetLastError();
cout << "Bind ErrorCode : " << errCode << endl;
return 0;
}
// 영업 시작!
if (::listen(listenSocket, 10) == SOCKET_ERROR)
{
int32 errCode = ::WSAGetLastError();
cout << "Listen ErrorCode : " << errCode << endl;
return 0;
}
참고로, listen 의 두 번째 인자는, 최대 몇 개의 고객을 한 번에 응대할지에 대한 갯수라고 보면 된다.
그럼 while 문에서 서버가 다음과 같은 일을 처리할 것이다.
while (true)
{
SOCKADDR_IN clientAddr; // IPv4
::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();
음.. 간단하게 살펴보면, 클라이언트 소켓을 인식하고, 클라이언트의 주소를 ipAddress 에 넣고 있다.
사실 clientAddr.sin_addr 를 파 보면, 다음과 같이 정의되어 있다.
//
// IPv4 Internet address
// This is an 'on-wire' format structure.
//
typedef struct in_addr {
union {
struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { USHORT s_w1,s_w2; } S_un_w;
ULONG S_addr;
} S_un;
#define s_addr S_un.S_addr /* can be used for most tcp & ip code */
#define s_host S_un.S_un_b.s_b2 // host on imp
#define s_net S_un.S_un_b.s_b1 // network
#define s_imp S_un.S_un_w.s_w2 // imp
#define s_impno S_un.S_un_b.s_b4 // imp #
#define s_lh S_un.S_un_b.s_b3 // logical host
} IN_ADDR, *PIN_ADDR, FAR *LPIN_ADDR;
IPv4 주소라 그런지, 8바이트 크기의 union 으로 되어 있는데... IPv4 를 고려해서 ipAddress 를 16 바이트로 선언한 걸까 싶었지만, 사이즈를 char[8] 로 바꾸니까 출력이 깨지는 것으로 보아... 16바이트 Align 으로 메모리가 할당이 되어 뭔가 뻑이 간 것 같다. 주소값을 ipAddress 에 넣고 있으니까. 😄
이제 할 일이 다 마친 후, 윈속 종료만 해주면 끝이다. 😎
// 윈속 종료
::WSACleanup();
실제로 위 솔루션을 돌리게 되면, 다음과 같이 결과가 제대로 나오는 것을 확인할 수 있다! 😮
'Game Dev > Game Server' 카테고리의 다른 글
[C++ 게임 서버] 3-4. TCP vs UDP (0) | 2023.09.21 |
---|---|
[C++ 게임 서버] 3-3. TCP 서버 실습 (0) | 2023.09.21 |
[C++ 게임 서버] 3-1. 소켓 프로그래밍 기초 #1 (0) | 2023.09.19 |
[C++ 게임 서버] 2-10. TypeCast (0) | 2023.09.19 |
[C++ 게임 서버] 2-9. Object Pool (0) | 2023.09.15 |