언리얼 엔진 소켓 프로그래밍 (2) Send, Recv / 쓰레드 생성

Kclient·2022년 10월 19일
0

UnrealEngine

목록 보기
2/5

지난 시간에는 FSocket과 Winsock을 사용하여 서버와 연결을 해보았다.
이제 패킷을 전송해보고 받는 것을 알아보자.
또한 동기식 소켓으로 구현을 하여 데이터를 받으면 Recv 가 대기를 하기 때문에 게임 루프에 영향을 준다.
스레드를 생성하여 게임 루프에 영향이 가지 않도록 하는 방법도 알아보자.

1. Send / Recv

사실 이것들은 별 특별한게 없다.

Send부터 알아보자.

// send 함수 사용 예시
int nSendLen = send(Socket, buff, buff[0], 0);

순서대로 지난시간에 만들었던 SOCKET 변수, 보낼 데이터, 보낼 데이터의 길이, 마지막은 flags 값인데 보통 0을 넣으면 된다. (참고 MSDN 설명)

반환값으로는 보낸 패킷의 길이를 받아올 수 있다.

Recv 도 간단하다.

// Recv 함수 사용 예시
char RecvBuff[BUFSIZE] // 패킷을 저장할 변수

int RecvLen = recv(Socket, RecvBuff, BUFSIZE, 0);

순서대로 SOCKET 변수, 패킷을 저장할 변수의 시작 위치와 저장할 수 있는 최대의 크기, 마지막은 flags 값이다. 이것도 보통 0을 넣으면 된다. (참고 MSDN 설명)

Send와 Recv 모두 주의해야 할 것은 두 번째 파라미터로 변수의 시작 위치를 넘겨준다는 것이다.

그리고 Send와 Recv의 세번째 파라미터에 보내주는 길이가 의미하는 것도 잘 생각하고 제대로 넣어주자.


2. FRunnableThread

우리는 동기 소켓으로 생성하여 연결을 하였다.

동기 소켓의 특징으로는 데이터를 받을 때까지 대기를 하며 이 때문에 클라이언트가 더 이상 진행을 하지 못하고 멈춘다.

위에 적인 코드를 사용하여 액터의 Tick 함수에 Recv를 넣고 하고 게임을 실행하면 아마 작동이 멈출 것이다.

언리얼 엔진에서 Tick 함수는 MainThread 가 액터 하나씩 돌아다니면서 순차적으로 호출을 하는데, 동기 소켓의 Recv 함수에서 데이터를 받을 때까지 대기를 하니 게임도 덩달아 같이 정지가 되어버린다.

이를 해결하기 위해 Recv 함수를 스레드를 생성하여 게임 루프와는 별개로 작동시키게 할 것이다.

일단 해보자.

언리얼 엔진에서는 FRunnable 이라는 class를 이용하여 스레드를 생성할 수 있다.

// HOUSE_OF_TREE_API 는 예제로 사용한 프로젝트의 이름이다.
class HOUSE_OF_TREE_API ClientSocket : public FRunnable
{
public:
	// 생성자와 소멸자
	ClientSocket();
	~ClientSocket() override;

	// 상속받은 세개의 함수
	bool Init() override;
	uint32 Run() override;
	void Exit() override;
    
	// 소켓 변수
	SOCKET Socket;
    
private:
	// 쓰레드
	FRunnableThread* Thread;
};

먼저 비어있는 C++ 클래스를 생성하고 FRunnable 를 상속받자.

그리고 스레드를 시작하고 (Init), 작동시키고 (Run), 종료할 수 있는 (Exit) 함수 세 개를 오버 라이딩하자.

그다음 소켓통신에 사용할 소켓 변수와 스레드 변수를 선언했다.

코드를 찬찬히 보면 FRunnableThread 은 뭘까 싶을 수 있다.

간단히 설명하면 FRunnableThread는 스레드 '객체' 이고 FRunnable 은 이 쓰레드 객체가 실행할 '대상'이다.

잘 이해가 안 되신다면 스레드를 생성하는 다음 코드를 봐보자.

ClientSocket::ClientSocket()
{
	Thread = FRunnableThread::Create(this, TEXT("Network Thread"));
}

ClientSocket::~ClientSocket()
{
	if (Thread)
	{
		// 스레드 종료
		Thread->WaitForCompletion();
		Thread->Kill();
		delete Thread;
	}
}

#pragma endregion

bool ClientSocket::Init()
{
	UE_LOG(LogNet, Warning, TEXT("Thread has been initialized"));
    
	// Socket 연결을 하는 코드를 넣으면 된다. (너무 길어서 뺐다)
}

uint32 ClientSocket::Run()
{
	while (true)
	{
		// Recv 작업을 여기서 진행 하면된다.
	}

	return 0;
}

void ClientSocket::Exit()
{
	if (Socket)
	{
		// Socket 연결을 끊고 Winsock 사용을 종료
		closesocket(Socket);
		WSACleanup();
	}
}

생성자에서 FRunnableThread::Create(this, TEXT("Network Thread")); 이 코드를 통해서 쓰레드를 생성하고 작동할 '대상'을 지정했다.

이 FRunnable class에서 작동할 것이니 this를 넣어줬다.

뒤에 문자열은 Thread의 이름이다. 알기 쉽게 지어주자. (참고 언리얼 C++ 레퍼런스 FRunnableThread)

소멸자에는 Thread가 작동 중이면 종료하고 객체를 삭제하도록 해놨다.

스레드가 생성이 되면 Init 함수가 실행이 되고 그다음에 Run 함수를 실행한다.

Recv 작업을 계속할 것이므로 무한 루프를 하고 그 안에서 recv 함수를 호출하면 된다.

스레드를 종료시키면 Exit 함수가 호출되고 종료된다.

Init 함수에서는 소켓을 연결하고, Exit 함수에서는 소켓 연결을 끊도록 작성했다.


이제 실행을 해보면 게임 루프 대기 없이 recv 작업을 수행할 수 있게 되었다!

사실 여기서 다른 문제가 하나 더 있는데, 네트워크를 통해 정보를 받아 게임상의 액터를 조작할 경우 MaingameThread가 아니라며 엔진에서 작업을 막아버린다.

결국 새로운 스레드를 생성해 정보를 받아왔지만 이것을 게임에 반영하려면 다시 메인 게임 루프로 정보를 가져와야 한다는 것이다...

나는 메시지 큐를 사용하여 정보를 저장해놓고 그것을 메인 게임 루프에서 처리하는 방식으로 작업을 진행하였는데 이것은 다음에 다시 작성해보겠다.

다른 좋은 방법이 있으면 공유해주시길 바란다.

링크는 실제 내가 사용한 코드다. 참고용으로 올린다. (참고 ClientSocket.h / ClientSocket.cpp)


어쩌다 보니 글이 매우 장황해졌는데 전달이 잘 되었는지는 모르겠다.

제작을 하면서 제일 고생을 한 부분이었고, 다른 사람들은 나보다 덜 고생하길 바라는 마음으로 작성하다 보니 그렇게 되었다.

그럼 이만...

profile
뭐든 손에 잡히는 대로 해보자

0개의 댓글