- 버퍼에 데이터를 쌓아두고 Send 스레드에서 처리하는 방식 구현
N-Send와 1-Send 방식
N-Send 방식
비동기 I/O에서 통신하는 방법
1. windows 소켓 프로그래밍에서 Send를 할 때 비동기 함수인 WSASend를 이용
2. 비동기 I/O는 함수가 완전히 실행될 때까지 기다려주지 않음. 즉, 함수를 호출했다고 즉시 데이터가 전송되는 것이 아님.
3. 나중에 IOCP 오브젝트에서 완료정보를 받아 완료되었다는 것을 확인해야 함.
비동기 I/O는 예약하는 것이라고 이해할 수 있다. 이 때 이전의 Send가 완료되었는지 확인하지 않고 전송하는 것이 N-Send 방식!
1-Send 방식
1-Send 방식은 완료되는 것을 기다렸다가 완료되면 다음 Send를 진행하는 방식이다.
즉, 순서대로 I/O 작업을 진행한다.
비교
Send의 완료처리 기준을 먼저 알아보자.
- Server -> Client 전송이 되었을 때 : 완료 X
- Server의 소켓 버퍼에 복사가 되었을 때 : 완료 처리
그럼 언제 전송하는가? 커널의 I/O가 Client에게 전송한다.
완료처리의 시점이 전송이 되었을 때가 아니기 때문에 N-Send 방식이 문제가 되는 상황이 발생할 수 있다.
어떤 문제가 발생할 수 있는가 하면
- 네트워크 상황이 안좋아서 혹은 이슈가 생겨서 Client가 데이터를 받지 않으면 Server 소켓 버퍼에 데이터가 계속 쌓인다.
- 만약 버퍼가 꽉차서 100 byte를 보내야하는데 10 byte만 보내준 상황이라고 가정
- Client에게 90 byte를 더 보내주어야 하는데 90 byte 전송 전에 50 byte를 전송
- 이 때 데이터의 순서가 중요하다면 N-Send 방식은 [10, 50, 90]으로 순서가 꼬여서 보장되지 않는다.
와 같은 경우가 생길 수 있다.
사실 게임 서버에서는 로그인 패킷을 제외하면 대부분 크기가 작다. 작은 패킷을 여러 번 보내기 때문에 버퍼가 가득 차는 일은 거의 없다고 볼 수 있다.
만약 버퍼가 찬다면 N-Send의 문제라기 보다는 Client 측의 문제일 확률이 높다.
아무튼 중요도를 떠나서 학습할 때는 뭐든 해보는 것이 좋다! 그러니 1-Send를 구현해보자!
구현
이번 단계에서는 버퍼에 담아놓고 순차적으로 보내는 방법을 구현할 것이다.
어떻게 구현할지 설계해보자.
1. `client` class에 `SendBuffer`를 큰 사이즈로 생성
2. `SendBuffer`의 시작 위치를 지정할 변수를 선언
3. `Send`를 호출할 때 `SendBuffer`의 시작 위치부터 데이터를 덮어씀
4. 시작 위치를 데이터의 크기만큼 늘림. 이 때 늘린 값이 데이터의 길이!
5. `Send`를 진행하고 있는지 확인할 수 있는 변수를 선언
6. `Send`를 진행하고 있는지 체크하고 아니라면 `Send`를 보냄
7. 완료 신호를 받으면 완료처리를 하고 `Send` 진행 여부를 `false`로 변경
이렇게하면 단일 스레드에서 동작하는 1-Send를 구현할 수 있다.
단일 스레드를 완성한 후 멀티스레드 환경에서 동작하도록 구현한다.
1. Sender 스레드에서 진행할 함수를 선언
2. 함수 내부에서 모든 클라이언트 전송 가능 여부 체크
3. 전송이 가능하면 전송
4. 전송 시 `Lock` 처리 (한 스레드만 전송 가능하도록)
`Send`를 진행할 때 경합이 발생할 수 있기 때문에 락을 걸고 진행 여부를 체크하면서 동작하도록 해야 한다.
`stClientInfo`
...
#include <mutex>
class stClientInfo
{
public:
stClientInfo()
{
...
ZeroMemory(&mSendOverlappedEx, sizeof(stOverlappedEx));
...
}
~stClientInfo() = default;
...
void Clear()
{
mSendPos = 0;
mIsSending = false;
}
...
bool SendMsg(const UINT32 dataSize_, char* pMsg_)
{
std::lock_guard<std::mutex> guard(mSendLock);
if ((mSendPos + dataSize_) > MAX_SOCKBUF)
{
mSendPos = 0;
}
auto pSendBuf = &mSendBuf[mSendPos];
CopyMemory(pSendBuf, pMsg_, dataSize_);
mSendPos += dataSize_;
return true;
}
bool SendIO()
{
...
}
...
private:
...
};
`IOCPServer.h`
class IOCPServer
{
public:
...
private:
...
void CreateSendThread()
{
...
}
void CloseSocket(stClientInfo* pClientInfo, bool bIsForce = false)
{
...
}
void SendThread()
{
...
}
'C++' 카테고리의 다른 글
Class와 구조체의 차이 (1) | 2025.08.18 |
---|---|
[단계별로 IOCP 실습] 6단계 효율적인 Send 구현 (1-Send 구현하기) (3) | 2025.08.07 |
[단계별로 IOCP 실습] 4단계 네트워크와 로직 처리 스레드 분리 (4) | 2025.08.05 |
[단계별로 IOCP 실습] 3단계 애플리케이션과 네트워크 코드 분리 (2) | 2025.08.04 |
[단계별로 IOCP 실습] 2단계 메모리 최적화 (0) | 2025.08.01 |