본문 바로가기

[단계별로 IOCP 실습] 5단계 효율적인 Send 구현 (1-Send 구현하기)

@iamrain2025. 8. 6. 15:52
  • 버퍼에 데이터를 쌓아두고 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()
	{
		...
	}
iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차