// Session.h
class SessionImpl;

class Session
{
public:
    Session() = default;
    ~Session() = default;
    
private:
    std::unique_ptr<SessionImpl> m_pImpl;
};

위와 같이 전방선언한 클래스의 객체를 삭제할 때에는 생성자와 소멸자에 default를 붙여주면 안된다.

조금 더 정확하게는 스마트 포인터 사용 또는 헤더파일에서 원시 포인터를 delete를 하는 경우이다.

 

class Session
{
...
    ~Session() { m_pUserManager.reset(); }
};

왜냐하면 해당 클래스의 원형을 모른채로 소멸자를 호출하는 것과 동일하기 때문이다.

그래서 불완전한 타입을 삭제할 수 없다는 오류가 발생한다.

 

// Session.h
class SessionImpl;

class Session
{
public:
    Session();
    ~Session();
};


// Session.cpp
#include "SessionImpl.h"

Session::Session() {}
Session::~Session() {}

해결법은 간단하다. 기본 생성자와 소멸자를 사용하더라도 default 키워드 대신 cpp에 명시적으로 정의해주면 된다.

주의할 점은 "생성자와 소멸자 둘 다" default 키워드를 사용하면 안된다.

'C++ > 기타' 카테고리의 다른 글

라이브러리 업데이트 자동 적용  (0) 2023.05.16
string_view  (0) 2023.03.08
이니셜라이저 리스트 직접 초기화, 복제 리스트 초기화  (0) 2023.03.08
[algorithm] sort, stable_sort  (0) 2022.08.03
[algorithm] find, find_if  (0) 2022.08.03

서버 함수

bind

int bind(
    SOCKET s, // 클라이언트 접속 수용을 목저긍로 만든 소켓
    const struct sockaddr* name, // 지역 IP 주소와 지역 포트 번호가 담긴 구조체의 주소
    int namelen // 소켓 주소 구조체 변수의 길이
);
// 반환값 : 성공(0), 실패(SOCKET_ERROR)

서버의 지역 IP 주소와 지역 포트 번호를 결정하는 역할을 한다.

 

listen

int listen(
    SOCKET s, // bind 함수에 의해 지역 IP 주소와 지역 포트 번호가 설정된 상태
    int backlog // 접속 가능한 클라이언트의 개수. 프로토콜의 지원 가능한 최대값은 SOMAXCONN
);
// 반환값 : 성공(0), 실패(SOCKET_ERROR)

소켓과 결합된 TCP 포트 상태를 LISTENING으로 바꾸는 역할을 한다.

즉, 클라이언트 접속을 받아들일 수 있는 상태가 된것이다.

 

accept

SOCKET accept(
    SOCKET s, // 클라이언트 접속을 수용할 목적으로 만든 소켓
    struct sockaddr* addr, // 소켓 주소 구조체 변수의 주소
    int* addrlen
);
// 반환값 : 성공(new socket), 실패(INVALID_SOCKET)

서버에 접속한 클라이언트와 통신할 수 있도록 새로운 소켓을 생성하여 리턴하는 역할을 한다.

접속한 클라이언트의 IP 주소와 포트 번호를 알려준다.

접속한 클라이언트가 없을 경우 서버를 대기 상태로 만든다.

 

 

클라이언트 함수

connect

int connect(
    SOCKET s, // 서버와 통신하기 위해 만든 소켓
    const struct sockaddr* name,
    int namelen, // 구조체의 크기
);
// 반환값 : 성공(0), 실패(SOCKET_ERROR)

클라이언트가 서버에 접속하여 TCP 프로토콜 수준의 연결이 이루어지도록 한다.

 

 

데이터 전송 함수

send

int send(
    SOCKET s, // 통신할 대상과 연결된 소켓
    const char* buf, // 보낼 데이터를 담고 있는 어플리케이션 버퍼의 주소
    int len, // 보낼 데이터의 크기
    int flags // 대부분 0 사용. MSG_DONTROUTE, MSG_OOB를 사용하는 경우도 드물게 있다
);
// 반환값 : 성공(보낸 바이트 수), 실패(SOCKET_ERROR)

어플리케이션 데이터를 송신 버퍼에 복사함으로써 하부 프로토콜(TCP/IP)에 의해 데이터가 전송되도록 한다.

데이터 복사가 성공적으로 이루어지면 값을 곧바로 리턴하지만 실제 데이터 전송이 완료된 것은 아니다.

 

첫번째 인자로 사용한 소켓의 특성(블로킹 소켓, 넌블로킹 소켓)에 따라 성공시 반환값이 조금 달라진다.

블로킹 소켓이라면 len과 같은 값, 넌블로킹 소켓이라면 송신 버퍼의 여유 공간만큼 데이터를 복사하기 때문에 복사한 데이터 바이트 수를 리턴한다. (최소1, 최대 len)

 

recv

int recv(
    SOCKET s, // 통신할 대상과 연결된 소켓
    char* buf, // 받은 데이터를 저장할 어플리케이션 버퍼의 주소
    int len, // 수신 버퍼로부터 복사할 최대 데이터 크기
    int flags // 대부분 0 사용. MSG_PEEK, MSG_OOB를 드물게 사용한다
              // 복사하고 나면 해당 데이터를 수신 버퍼에서 삭제하지만 MSG_PEEK 사용 시 삭제하지 않는다
);
// 반환값 : 성공(받은 바이트 수 또는 0), 실패(SOCKET_ERROR)

수신 버퍼에 도착한 데이터를 어플리케이션 버퍼로 복사하는 역할을 한다.

수신 버퍼에 데이터가 도달한 경우 복사한 바이트 수를 리턴하고 접속 종료 요청이라면 0을 리턴한다.

 

recv 사용 시 주의할점은 len보다 작은 데이터가 복사될 수 있기 때문에 받을 데이터의 크기를 미리 알고 있다면 전부 받을 때까지 여러 번 호출해야 한다.

IOCP(Input/Output Completion Port)

 

IOCP는 다수의 클라이언트의 요청을 효율적으로 처리하기 위한 방법들 중 하나이다.

사용할 스레드를 미리 만들어두고 소켓에서의 IO가 완료되었을 때 잠들어있는 스레드를 깨워서 작업을 처리한다.

최적화 할 수 있는 스레드의 수는 프로세서의 2배 정도가 적당하다고 한다.

 

IOCP는 스레드를 관리하기 위해 1개의 스레드 큐와 2개의 스레드 리스트, 1개의 작업 큐를 사용하고 각 스레드는 3개의 큐중 하나의 큐에만 존재할 수 있다.

 

IOCompletionQueue

FIFO Queue. 입출력이 완료된 작업들이 들어간다. 스레드들은 이 큐에서 작업을 꺼내서 수행한다.

 

WatingThreadQueue

LIFO Queue. 작업 대기중인 스레드들이 들어있다. 만약 입출력이 완료되었다면 이 큐에서 스레드를 하나 꺼내서 사용한다.

 

ReleaseThreadList

현재 작업을 수행하고 있는 스레드들이 들어간다. 만약 실행중인 스레드가 일시정지해야 한다면 해당 스레드를 PausedThreadList로 보내고 WatingThreadQueue에서 새로운 스레드를 하나 꺼내온다. 이것 때문에 프로세서의 갯수보다 많은 스레드를 미리 만들어 놓는 것이다.

 

PausedThreadList

어떠한 원인으로 인해 일시정지된 스레드들이 들어간다. 일시정지가 풀리더라도 ReleaseThreadList가 꽉 차있다면 바로 ReleaseThreadList로 보내지 않고 대기한다.

 

 

IOCP를 사용하려면 먼저 IOCP를 만들어야 한다.

 

HANDLE WINAPI CreateIoCompletionPort(
    _In_        HANDLE fileHandle, // IOCP와 연결할 핸들. 최초 생성시 INVALID_HANDLE_VALUE를 넘긴다
    _In_opt_    HANDLE ExistingCompletionPort, // IOCP 핸들. 최초 생성시 NULL
    _In_        ULONG_PTR CompletionKey, // IO 완료 시 넘어갈 값. 사용자가 넘기고 싶은 값을 넘김
    _In_        DWORD NumberOfConcurrentThreads) // 한 번에 동작할 수 있는 최대 스레드 개수. 0을 넘기면 프로세서 숫자로 자동 지정됨
    
HANDLE hPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0); // CP 최초 생성
HANDLE port = CreateIoCompletionPort(socket, hPort, (ULONG_PTR)session, 0); // 디바이스-CP 연결

 

CreateIoCompletionPort 함수는 2가지 일을 한다.

1. CP 생성

2. 디바이스와 CP 연결

 

하나의 함수로 서로 다른일을 하기 때문에 명확하게 다른 이름으로 래핑하여 사용하면 좋을 것 같다.

 

 

IOCompletionQueue

입출력 완료시 말고도 유저가 직접 PostQueuedCompletionStatus 함수를 호출해서 정보를 직접 큐에 집어넣을 수도 있다.

 

BOOL WINAPI PostQueuedCompletionStatus(
    _In_        HANDLE CompletionPort,
    _In_        DWORD dwNumberofByteTransferred,
    _In_        ULONG_PTR dwCompletionKey,
    _in_opt_    LPOVERLAPPED lpOverlapped)

 

전달한 정보들은 입출력이 완료되면 CompletionQueue에 저장되어 스레드가 자신을 처리해주기를 기다리게 된다.

 

 

WatingThreadQueue

GetQueuedCompletionStatus 함수로 스레드를 대기 상태로 만든다.

ReleaseThreadList에 공간이 있고, 완료된 입출력이 있는 경우에만 대기 상태가 해제된다.

 

 

ReleaseThreadList

입출력 장치 또는 PostQueuedCompletionStatus로 보낸 정보를 GetQueuedCompletionStatus 함수를 통해서 받아온다.

 

BOOL WINAPI GetQueuedCompletionStatus(
    _In_    HANDLE CompletionPort,
    _Out_   LPDWORD lpNumberofBytes,
    _Out_   PULONG_PTR lpCompletionKey,
    _Out_   LPOVERLAPPED *lpOverlapped,
    _In_    DWORD dwMilliseconds)

 

입출력 처리를 하고 난 뒤에 재호출해야만 다시 대기 상태로 전환된다. 재호출하지 않으면 대기 상태로 전환되지 않기 때문에 주의해야 한다.

 

 

PausedThreadList

GetQueuedCompletionStatus를 호출하지 않더라도 스레드가 Blocking 상태에 빠지면 IOCP가 알아서 PauseThreadList에 집어넣는다.

 

 

 

 

 

 

 

 

엔진과 클라이언트 프로젝트를 분리하고 엔진에서 라이브러리를 뽑아서 사용할 때, 업데이트를 배치파일로 만들었다 하더라도 매번 실행하는 번거로움이 발생했다.

 

그래서 엔진 프로젝트 빌드 후 이벤트로 배치파일 자동 실행으로 해결.

 

C++17 이후부터 등장한 타입이다.

 

c_str 메소드가 없는것을 빼면 std::string과 인터페이스가 동일하다. 오히려 문자열을 축소시킬 수 있도록 remove_prefix, remove_suffix 메소드가 추가로 제공된다.

 

string_view는 매개변수로 전달 시 const string&과 달리 오버헤드가 없다. 정확히는 없다기 보다 값으로 전달하지만 스트링에 대한 포인터와 길이만 갖고 있기 때문에 오버헤드가 매우 적다.

그러니 읽기 전용 스트링을 함수의 매개변수로 받는 경우에는 const string&, const char* 대신 string_view를 사용하도록 하자.

 

string str = "Hello";
string_view sv = " world";
auto result = str + sv; // compile error

 

둘을 결합시키고 싶다면 data 메소드를 사용하면 된다.

 

string 리터럴을 string_literals::s를 통해 만들 수 있는 것처럼 string_view 리터럴도 string_view_literals::sv를 통해 만들 수 있다.

+ Recent posts