나만의 작은 도서관
[C++] 게임서버 탐구 일지 #1. Winsock TCP 서버 기초- 클라이언트-서버 연결하기 본문
목차
- Winsock이란
- Winsock을 이용한 서버 열기
- 클라이언트를 서버에 연결하기
- 깃헙 코드
Winsock이란?
Winsock은 윈도우 환경에서 통신하기 위해 MS에서 제작한 소켓 라이브러리이다. Winsock을 사용하기 위해 다음과 같은 헤더 및 라이브러리를 추가하면 된다. 다음 목차부터 작성하는 클라이언트, 서버 코드엔 해당 및 라이브러리가 추가된 상태로 작성하였다.
// Winsock 통신을 위한 헤더들
#include <WinSock2.h>
#include <mswsock.h>
#include <WS2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
+) 참고로, 여기저기 WSA로 시작하는 함수나 타입들이 잔뜩 나올텐데 WSA는 WinSock API의 약자이다.
Winsock을 이용한 서버 열기
winsock 초기화
Winsock을 사용해 통신하기 위해선 우선적으로 winsock을 초기화 해줘야한다. 초기화는 WSAStartup() 함수로 실행한다. (winsock은 다양한 버전이 있지만 가장 많이 쓰이는 2.2버전을 사용한다.)
// WSAStartup(): winsock 초기화 (ws2_32 라이브러리로 초기화). 초기화 정보가 wsaData에 채워진다.
WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
socket() 과정 실행
TCP서버는 반드시 socket() -> bind() -> listen() 과정이 존재한다. socket()을 실행하기 위해 Listen 소켓을 하나 생성한다. 소켓은 라이브러리에 정의된 SOCKET 타입을 사용한다.SOCKET 타입을 생성하는 socket()함수는 3가지 인자가 필요한데, 각각 다음과 같다.
SOCKET WSAAPI socket(
[in] int af,
[in] int type,
[in] int protocol
);
- af: address family의 약자. 네트워크 주소를 넣으면 된다. ex. AF_INET = ipv4 사용하도록 설정
- type: 새 소켓의 타입. TCP나 UDP와 같은 타입을 선택하는 옵션이다.
- protocol: 프로토콜 종류. 0은 알아서 배정해달라는 뜻이다.
아래 예제에서 af는 ipv4를 의미하는 AF_INET를, type은 TCP를 의미하는 SOCK_STREAM을, 프로토콜은 따로 지정하지 않을 것이기 때문에 0을 넣어주었다.
// socket(): 소켓 하나를 생성하고, 소켓의 descriptor를 가리키는 uint 포인터를 반환한다.
SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
// 정상적으로 소켓이 만들어졌는지 확인.
if (listenSocket == INVALID_SOCKET)
{
int32 errCode = ::WSAGetLastError();
cout << "Socket ErrorCode : " << errCode << endl;
return 0;
}
bind() 과정 실행
Winsock에서 소켓과 주소를 엮어주기 위해선 SOCKADDR 구조체에 주소를 따로 설정한 다음, 소켓과 SOCKADDR를 bind() 함수의 인자로 넣어야한다.
SOCKADDR 타입 또한 여러가지 있는데, IPv4버전을 사용하는 소켓이었므로 IPv4버전인 SOCKADDR_IN을 사용한다.
// SOCKADDR_IN: AF_INET(IPv4) 버전의 주소 및 포트가 설정된 구조체이다.
SOCKADDR_IN serverAddr;
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET; // IPv4
serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY); // localhost 지정. INADDR_ANY: 니가 알아서 해줘
serverAddr.sin_port = ::htons(7777); // port : 7777
+) 참고로 htons는 host to network short의 약자로, host(현재 컴퓨터를 의미)의 엔디안 방식 -> 빅엔디안(네트워크 표준)으로 변경해주는 htoX 시리즈 중 short 타입으로 반환하는 함수이다.
+) INADDR_ANY는 주소가 여러 개 있을 때, 그 중 이론상 말이 되는 주소들로 연결해주는 기능이다. (INADDR_ANY가 아닌 하드코딩된 주소를 사용했다면, 그 주소로만 접근할 수 있다.)
SOCKADDR 타입을 만들었다면 bind() 함수를 통해 소켓과 주소를 엮어준다.
// bind(): 소켓과 서버 주소를 엮어준다(바인드한다).
if(::bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
int32 errCode = ::WSAGetLastError();
cout << "Bind ErrorCode : " << errCode << endl;
return 0;
}
listen() 과정 실행
소켓을 온전히 만들었으므로, 해당 소켓을 listen소켓으로 만들기 위해 listen()을 실행해야한다. listen()의 인자로 만들어준 소켓을 인자로 넣어주면 된다. (listen()의 두번째 인자는 연결 대기 큐의 길이를 설정한다.)
listen까지 완료되었다면 서버는 열린 상태가 된다.
// listen(): 해당 소켓으로 conncet 요청을 듣기 시작한다.
if (::listen(listenSocket, 10) == SOCKET_ERROR)
{
int32 errCode = ::WSAGetLastError();
cout << "Listen ErrorCode : " << errCode << endl;
return 0;
}
cout << "Server open" << '\n';
accept() 과정 실행
클라이언트로부터 connect() 요청을 받았을 때 요청을 처리해야하므로 accept()를 실행해야한다. accept()는 호출 시 무한정 대기하다가 conncet 요청을 감지하면 수락하고 리턴한다.
이로써 서버는 클라이언트 연결 작업까지 준비를 끝냈다.
while (true)
{
SOCKADDR_IN clientAddr;
::memset(&clientAddr, 0, sizeof(clientAddr));
int32 addrLen = sizeof(clientAddr);
// accept(): 무한정 대기하다가 conncet 요청을 감지하면 수락하고 리턴한다.
SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&clientAddr, &addrLen);
if (clientSocket == INVALID_SOCKET)
{
int32 errCode = ::WSAGetLastError();
cout << "Accept ErrorCode : " << errCode << endl;
return 0;
}
// 접속한 포트의 ip주소를 출력해서 확인한다.
char ipAddress[16];
::inet_ntop(AF_INET, &clientAddr.sin_addr, ipAddress, sizeof(ipAddress));
cout << "Client Connected! IP = " << ipAddress << endl;
}
WSACleanup(); // winsock을 종료한다. (안해도 문제가 생기진 않음)
클라이언트를 서버에 연결하기
winsock 초기화
클라이언트라고 다를건 없다. 우선 Winsock을 초기화 해준다.
// WSAStartup(): winsock 초기화 (ws2_32 라이브러리로 초기화). 초기화 정보가 wsaData에 채워진다.
WSAData wsaData;
if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
return 0;
소켓 생성
통신에 사용할 소켓을 하나 생성한다. 서버와 동일하게 IPv4주소와, TCP타입을 선택한 소켓을 생성하였다.
// socket(): 소켓 하나를 생성해 리턴한다.
SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET)
{
int32 errCode = ::WSAGetLastError();
cout << "Socket ErrorCode : " << errCode << endl;
return 0;
}
connect() 함수 실행
연결 요청을 할 서버 주소와 만들어둔 소켓을 인자로 넣어 해당 서버에 연결을 요청한다. 연결에 성공했다면 이제부터 데이터 송수신이 가능하다.
// SOCKADDR_IN: AF_INET(IPv4) 버전의 주소 및 포트가 설정된 구조체이다.
SOCKADDR_IN serverAddr;
::memset(&serverAddr, 0, sizeof(serverAddr));
serverAddr.sin_family = AF_INET; // IPv4
// serverAddr.sin_addr.s_addr = ::inet_addr("127.0.0.1"); // localhost -deprecated-
::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr); // localhost(사람이 읽을 수 있는 텍스트 표현의 IP주소를 이진 표현으로 변환해주는 함수)
serverAddr.sin_port = ::htons(7777); // port : 7777
// connect(): 해당 소켓으로 서버 주소에 연결을 요청한다.
if (::connect(clientSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
{
int32 errCode = ::WSAGetLastError();
cout << "Connect ErrorCode : " << errCode << endl;
return 0;
}
// 연결 성공! 데이터 송수신 가능!
cout << "Connection is sucess" << endl;
실행 결과
정상적으로 연결된 것을 확인할 수 있다.
깃헙 코드
https://github.com/pledge24/WinGameServerPractice/tree/14898e5a27657754551f2a2d3dffa4331ceec959
'C++ > Windows 게임서버' 카테고리의 다른 글
[C++] 게임서버 탐구 일지 #5. Winsock TCP 서버 - 논블로킹 (0) | 2024.12.28 |
---|---|
[C++] 게임서버 탐구 일지 #4. 다양한 소켓 옵션 (1) | 2024.12.28 |
[C++] 게임서버 탐구 일지 #3. Winsock UDP echo 서버 (0) | 2024.12.27 |
[C++] 게임서버 탐구 일지 #2. Winsock TCP 서버 기초- send(), recv() (0) | 2024.12.27 |
[C++] 게임서버 탐구 일지 #0. TCP 통신 기초 이론 (1) | 2024.12.25 |