Coding Memo

Socket Programming Basic 본문

Game Server (C++)

Socket Programming Basic

minttea25 2022. 10. 18. 14:53

본 포스팅은 인프런에 등록되어 있는 Rockiss 님의 강의를 보고 간단하게 정리한 글입니다.


소켓을 이용해서 간단하게 서버와 클라이언트 접속을 해보도록 하자.

소켓 프로그래밍을 위해서는 다음과 같은 라이브러리와 헤더가 추가되어야 한다. (아래 문서에도 나와있다.)

(https://learn.microsoft.com/ko-kr/windows/win32/winsock/creating-a-basic-winsock-application)

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

MS에서 제공하는 윈도우 소켓을 이용한다. 자세한 문서는 아래에 있다.

https://learn.microsoft.com/ko-kr/windows/win32/winsock/windows-sockets-start-page-2

 

Windows 소켓 2 - Win32 apps

Windows Sockets 2(Winsock)를 사용하면 프로그래머가 사용되는 네트워크 프로토콜과 관계없이 유선으로 애플리케이션 데이터를 전송하는 고급 인터넷, 인트라넷 및 기타 네트워크 지원 애플리케이션

learn.microsoft.com

 

테스트를 위해서 기본적으로 ip는 루프백 주소인 127.0.0.1을 사용하고 포트는 8888을 사용하겠다.

 

아래 내용의 위 문서에 대부분 나오는 내용이다.


클라이언트

 

클라이언트에서는 서버에 접속할 소켓 하나만 만들면 된다.

 

 

1. 윈속 초기화 (= ws2_32 DLL초기화)

 

WSAStartup과 MakeWord로 초기화하면 된다. 이때 반환 값이 0이 아니면 에러가 발생했다는 의미이다.

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

 

2. IPv4 포맷으로 소켓 생성

 

socket 생성 시 인자로 3개가 들어간다.

 

socket(int af, int type, int protocol)

af: Address Family의 약자로 IPv4일 경우 AF_INET으로, IPv6일 경우 AF_INET6로 지정

type: 스트림 타입으로 TCP일 경우 SOCK_STREAM으로, UDP일 경우 SOCK_DGRAM으로 지정

protocol: 특정 프로토콜을 지정하지 않을 것이므로 0

 

위에 언급된 타입 말고도 더 많은 타입이 있으니 아래를 참고하자.

IPv4와 TCP로 소켓 생성

SOCKET clientSocket =  ::socket(AF_INET, SOCK_STREAM, 0);
if (clientSocket == INVALID_SOCKET)
{
    int errCode = ::WSAGetLastError();
    cout << "Socket ErrorCode : " << errCode << endl;
    return 1;
}

 

3.  서버에 연결할 목적지 설정

SOCKADDR_IN을 생성 및 memset으로 초기화를 해주고,  연결할 ip주소와 포트를 할당 해준다.

한 가지 주의할 점은, 네트워크에서는 BIG ENDIAN을 사용하기 때문에 요청할 포트를 (보통 컴퓨터들을 little-endian을 사용한다.) htons(host to network short)를 이용해 big-endian으로 바꿔주어 요청해야한다.

SOCKADDR_IN serverAddr; // IPv4 사용시 이용
::memset(&serverAddr, 0, sizeof(serverAddr)); // clear serverAddr
serverAddr.sin_family = AF_INET;
::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
serverAddr.sin_port = ::htons(8888);

 

4. 연결

connect로 연결을 시도하고 연결에 실패하거나 에러가 발생하면 SOCKET_ERROR를 반환한다.

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

 

5. TODO

연결에 성공했으니 이제 할일을 작성하면 된다.

while(true) 
{
	// TODO
}

 

6. 할 작업이 끝났으면 소켓을 닫아주고 윈속을 종료

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

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

 

[전체코드]

더보기
int main()
{
    WSAData wsaData;
    if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        // fail
        return 0;
    }

    SOCKET clientSocket = ::socket(AF_INET, SOCK_STREAM, 0);

    if (clientSocket == INVALID_SOCKET)
    {
        // fail
        int errCode = ::WSAGetLastError();
        cout << "Socket ErrorCode : " << errCode << endl;
        return 0;
    }

    SOCKADDR_IN serverAddr; // IPv4 사용시 이용
    ::memset(&serverAddr, 0, sizeof(serverAddr)); // clear serverAddr
    serverAddr.sin_family = AF_INET;
    ::inet_pton(AF_INET, "127.0.0.1", &serverAddr.sin_addr);
    serverAddr.sin_port = ::htons(8888); // 80: HTTP, 22: ~~

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

    // -------------------------------------------------------------
    // connect 성공! 이제부터 데이터 송수신 가능
    cout << "Connected To Server !" << endl;

    while (true)
    {
        // TODO

        this_thread::sleep_for(1s);
    }

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

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

서버

서버는 listening할 소켓과 실제로 데이터를 주고 받을 소켓이 따로 존재한다.

 

1. 윈속 초기화

클라이언트와 내용은 같다.

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

 

2. 소켓 생성

소켓 생성 자체는 클라이언트와 동일하지만 이번 소켓은 연결 요청을 대기 하고 있을 소켓이다.

SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);

if (listenSocket == INVALID_SOCKET)
{
    int errCode = ::WSAGetLastError();
    cout << "Socket ErrorCode : " << errCode << endl;
    return 1;
}

 

3. listen하고 있을 주소와 포트 설정

INADDR_ANY는 서버의 IP주소를 자동으로 찾고 정하도록 한다.

따라서 실행 환경(위치, ip주소)가 바뀌더라도 수정할 필요가 없다.

SOCKADDR_IN serverAddr; // IPv4 사용시 이용
::memset(&serverAddr, 0, sizeof(serverAddr)); // clear serverAddr
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY); // 알아서 정하도록 하기
serverAddr.sin_port = ::htons(8888);

 

4. 설정한 소켓으로 bind 하기

소켓에 ip 주소와 포트를 결합시켜 사용가능한 소켓으로 만든다.

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

 

5. listen으로 연결 요청을 대기 시킨다.

두번째 인자로 들어가는 backlog는 연결 대기줄의 최대길이를 뜻한다.

 

참고로 연결을 하는 것은 아니고 요청을 대기만 한다.

따라서 이 소켓은 listen 만하고 있고 실제로 데이터를 송수신하는데 사용되는 소켓이 아니라는 뜻이다.

if (::listen(listenSocket, 10) == SOCKET_ERROR)
{
    int errCode = ::WSAGetLastError();
    cout << "Listen ErrorCode : " << errCode << endl;
    return 1;
}

 

6. (연결이 성공했다면) 실제로 데이터 패킷을 주소받을 소켓 생성 및 패킷 송수신 대기

소켓 생성과 초기화를 해주고, accept로 연결을 완료한다.

 

accept는 연결 큐가 비어있으면 block이 되고 연결이 들어오면 하나를 꺼내서 사용하게 된다.

listening 상태에 있는 소켓을 accept 함수에 의해 반환되는 소켓으로 연결한다.

즉, listening상태의 소켓을 accept를 이용해 실제 데이터를 주고 받을 소켓과 연결을 한다.

 

마지막으로 연결된 ip주소를 확인한다.

while (true)
{
    SOCKADDR_IN clientAddr; // IPv4
    ::memset(&clientAddr, 0, sizeof(clientAddr));

    int addrLen = sizeof(clientAddr);
    // 실시간으로 연결하여 패킷을 주고받을 소켓
    SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&serverAddr, &addrLen);
    if (clientSocket == INVALID_SOCKET)
    {
        int errCode = ::WSAGetLastError();
        cout << "Accept ErrorCode : " << errCode << endl;
        return 1;
    }

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

    // TODO
}

 

7. 윈속 종료

::WSACleanup();

 

[전체 코드]

더보기
int main()
{
    WSAData wsaData;
    if (::WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
    {
        return 0;
    }

    SOCKET listenSocket = ::socket(AF_INET, SOCK_STREAM, 0);
    if (listenSocket == INVALID_SOCKET)
    {
        int errCode = ::WSAGetLastError();
        cout << "Socket ErrorCode : " << errCode << endl;
        return 0;
    }

    SOCKADDR_IN serverAddr;
    ::memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = ::htonl(INADDR_ANY);
    serverAddr.sin_port = ::htons(8888);

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

    if (::listen(listenSocket, 10) == SOCKET_ERROR)
    {
        int errCode = ::WSAGetLastError();
        cout << "Listen ErrorCode : " << errCode << endl;
        return 0;
    }

    // ---------------------------------------------

    while (true)
    {
        SOCKADDR_IN clientAddr; // IPv4
        ::memset(&clientAddr, 0, sizeof(clientAddr));

        int addrLen = sizeof(clientAddr);
        // 실시간으로 연결하여 패킷을 주고받을 소켓
        SOCKET clientSocket = ::accept(listenSocket, (SOCKADDR*)&serverAddr, &addrLen);
        if (clientSocket == INVALID_SOCKET)
        {
            // fail
            int 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();
}

Java에서 socket 프로그래밍을 간단하게 했었지만 C++에서 하는 것은 처음이다.

문법 차이가 있어서 그런지 몰라도 뭔가 신선하다.

'Game Server (C++)' 카테고리의 다른 글

Socket Programming - UDP  (0) 2022.11.03
Socket Programming - TCP  (0) 2022.10.31
Type Cast  (1) 2022.10.11
Object Pool  (0) 2022.10.11
Memory Pool  (0) 2022.10.09