※ 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는 합쳐서 주소가 된다. 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라는 함수를 통해 포트를 세팅하는 것이다!
// 연결할 소켓, 주소, 주소 크기
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이라는 형으로 변환하여 사용한다.
// 소켓 리소스 반환
::closesocket(clientSocket);
// 윈속 종료
::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();
}
동일하다.
동일하다.
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를 사용하면 그 주소값이 안되더라도 다른 시도를 할 수 있다.
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;
}
backlog: 대기열 수
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;
}
동일하다.
동일하다.
#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();
}