이제 패킷 송수신과 패킷핸들러의 사용까지 만들었으니 서버와 클라의 통신은 준비가 끝났다고 볼 수 있다.

근데 생각해보니 TLS로 통신하는걸 안만들었다. 지금은 평문통신을 해서 문제가 없지만, TLS를 붙여서 돌려보는것도 중요한 목표중에 하나였으니 클라이언트에서도 반드시 구현해야한다.

 

서버와 마찬가지로 뗏다 붙였다 할 수 있게 만들어서 평문통신일때와 암호화통신일때 각각 테스트 할 수 있게 만들자.


매우 찾기 힘든 문제를 겪으면서 상당히 오래 걸렸는데 어찌됐든 잘 만들어봤다.

 

TLSSession 추가

PacketSession을 상속받는 TLSSession을 만들어줬다.

그리고 원래는 GameInstance에서 Socket을 연결한 뒤에 Session을 만들었는데, Session을 만들어놓고, Session에서 멤버함수를 사용해 서버와 연결하는 방식으로 수정했다.

 

그리고 서버와 더미클라이언트에서 쓰던 SslObject를 그대로 가져와서 사용했다.

단 #include <openssl/ssl.h> 을 인클루드 할때 define UI 라는 부분이 충돌하는데 아래와 같이 수정해서 해결했다. (오래걸리게 한 범인 중 하나)

#define UI UI_ST
#include <openssl/ssl.h>
#undef UI

 

class CLIENT_API PacketSession : public TSharedFromThis<PacketSession>
{
	friend class UClientGameInstance;

public:
	PacketSession(FString IpAddress, uint32 Port);
	virtual ~PacketSession();

	bool ConnectToGameServer();
	virtual void TLSConnect();
	virtual void Run();

	void HandleRecvPackets();
	void SendPacket(SendBufferRef SendBuffer);

	TAtomic<bool> IsRunning;

	TSharedPtr<class RecvWorker> RecvWorkerThread;
	TSharedPtr<class SendWorker> SendWorkerThread;
	TQueue<TArray<uint8>> RecvPacketQueue;
	TDeque<SendBufferRef> SendDeque;

	FSocket* Socket;
	FString IpAddress;
	int16 Port;
};

class CLIENT_API TLSSession : public PacketSession
{
public:
	TLSSession(FString IpAddress, uint32 Port, SSL_CTX* CTX);
	virtual ~TLSSession() override;
	virtual void Run() override;

	virtual void TLSConnect() override;
	void HandshakeSend();
	void HandshakeRecv();

public:
	TSharedPtr<SslObject> SslRef;
};

 

bool PacketSession::ConnectToGameServer()
{
    FIPv4Address Ip;
    FIPv4Address::Parse(IpAddress, Ip);
    TSharedRef<FInternetAddr> InternetAddr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr();
    InternetAddr->SetIp(Ip.Value);
    InternetAddr->SetPort(Port);

    GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Yellow, TEXT("Connecting To Server"));

    if (Socket->Connect(*InternetAddr))
    {
       GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("Connection Successed"));
    }
    else
    {
       GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Red, TEXT("Connection Failed"));
       return false;
    }
    TLSConnect();
    return true;
}

ConnectToGameServer는 원래랑 똑같은데 connect에 성공하면 TLSConnect도 호출하도록 만들었다.

TLSConnect는 가상함수로 만들어서 평문통신이라면 바로 RecvWorker, SendWorker 스레드를 돌리고, 아니라면 TLSConnect를 성공한 다음 돌리도록 만들었다. 

void PacketSession::TLSConnect()
{
    Run();
}
void TLSSession::TLSConnect()
{
    switch (SslRef->Connect())
    {
    case SslStatus::Ok:
       GEngine->AddOnScreenDebugMessage(-1, 5.f, FColor::Green, TEXT("TLS Successed"));
       HandshakeSend();
       Run();
       break;
    case SslStatus::WantRead:
       //wbio에 보낼 데이터가 생겼으면 보내고 recv
       HandshakeSend();
       HandshakeRecv();
       break;
    case SslStatus::WantWrite:
       //wbio가 꽉 차서 Accept가 진행되지 못한 경우. wbio에 있는 데이터를 Send한다.
       HandshakeSend();
       break;
    default:
       //TODO 에러 발생함. 연결 종료.
       break;
    }
}

 


NetworkWorker 수정

기존에는 RecvWorker와 SendWorker만 있었는데, 이것들을 상속받은 TLSWorker들을 만들었다.

그리고 원래는 Worker 클래스의 객체를 생성할 때 생성자에서 Thread Create를 했는데, 이를 분리하여 StartThread 함수를 따로 만들어서 처리했다. (오래 걸리게 한 주범)

 

평문 세션은 일반버전, TLSSession은 TLS버전의 네트워크 워커를 받도록 만들었다.

void PacketSession::Run()
{
    RecvWorkerThread = MakeShared<RecvWorker>(Socket, AsShared());
    SendWorkerThread = MakeShared<SendWorker>(Socket, AsShared());
    RecvWorkerThread->StartThread();
    SendWorkerThread->StartThread();
}
void TLSSession::Run()
{
    TSharedPtr<TLSRecvWorker> TLSRW = MakeShared<TLSRecvWorker>(Socket, AsShared(), SslRef);
    RecvWorkerThread = TLSRW;
    TSharedPtr<TLSSendWorker> TLSSW = MakeShared<TLSSendWorker>(Socket, AsShared(), SslRef);
    SendWorkerThread = TLSSW;
    
    RecvWorkerThread->StartThread();
    SendWorkerThread->StartThread();
}

생성한걸 바로 대입연산하지 않은 이유는 언리얼이 그렇게 하면 안되고 저렇게 해야해서 그렇다.

 

그리고 생성자에서 쓰레드를 만들지 않고 따로 함수를 만든 이유는 아래와 같이 만들면 TLSSession을 만들 때 EncBuffer나 virtual 함수 오버라이딩이 초기화 되기 전에 RecvWorker의 생성자가 불러와지기 때문에 그 부분에서 문제가 발생한다.

RecvWorker::RecvWorker(FSocket* Socket, TSharedPtr<PacketSession> Session)
    : SslRef(nullptr), DecBuffer_(BUFFER_SIZE), Socket(Socket), sessionRef(Session)
{
	Thread = FRunnableThread::Create(this, TEXT("RecvWorkerThread"));
}

TLSRecvWorker::TLSRecvWorker(FSocket* Socket, TSharedPtr<PacketSession> Session, SslObjectRef Ssl)
	: RecvWorker(Socket, Session, Ssl), EncBuffer_(BUFFER_SIZE)
{
}

 

구체적으로 겪은것은 핸드쉐이크 직후에 핸드쉐이크 더미 메시지(?)를 SSL_read함수가 처리하는 과정에서 원래는 복호화 되는 내용이 없어야 하는데 왜인지 3바이트가 복호화 될 때도 있고 아닐때도 있었다. 이게 TLSSession 을 안쓰고 평문통신을 하면 생기지 않는 문제라서 내가 Ssl을 적용시키면서 뭔가 실수를 했나? 하고 SSL 관련된 코드를 이리저리 뜯어보고 확인해봤는데 도저히 해결이 안됐다.

그래서 진짜 하나하나 코드의 흐름을 따라가면서 확인해봤는데 TLSSession에서 EncBuffer와 DecBuffer가 분명히 Getter에 의해 서로 다른 버퍼를 지목해야하는데(포인터로 봤을 때) 둘이 처음 호출 될 때는 똑같고 두번째 호출될때부터는 다른 이해하기 힘든 문제가 있었다.

그래서 이 부분을 콕찝어서 AI에게 물어보니 이전에는 헛소리만 하던 AI가 이번엔 원인을 정확하게 짚어줬다. TLSRecvWorker 생성자를 호출할 때 RecvWorker 생성자가 먼저 호출되고 이 때 쓰레드가 생성및 실행되는데, 문제는 TLSRecvWorker가 아직 초기화 되지 않은 상태에서 쓰레드에서 RecvWorker를 사용하기 때문에 이 때는 아직 virtual 테이블이 수정되지 않은 상태이고, 그래서 Getter를 오버라이딩 되기 전의 것으로 사용해서 생긴 문제. 마찬가지로 Decrypt함수도 아마 오버라이딩 되지 않은 상태로 쓸 수 있는 문제도 있고 아무튼 의도하지 동작으로 함수들이 실행되어서 생긴 문제였다.

 

이 문제는 스레드의 실행 시점을 생성자 안이 아니라 생성을 완전히 마친 뒤에 따로 StartThread 함수로 호출해주는것으로 해결했다. (위에 적은 Run 함수에서처럼)

 

게임서버쪽과 비슷하게 암호화, 복호화를 거쳐서 그 후로는 평문통신과 똑같이 동작하도록 만들었다.

 

 

 

추가로 기왕 TLS연결이 성공했으니 맥북 도커에서 돌아가는 인증서버에도 접속이 잘 되는지 확인해봤다.

언리얼쪽 클라이언트와 인증서버는 아직 protobuf protocol을 맞춰놓지 않아서 패킷 송 수신은 안해봤지만 적어도 TLS 연결이 정상적으로 되는걸 확인했다.


현재까지의 git 버전

클라이언트 (인증서버 연결 부분은 commit 하지 않았음.)

 

Merge pull request #1 from Dodontak/test · Dodontak/Project_Island_Client@e9a9eb0

Feat: TLSSession, TLSNetworkWorker 및 Refactor

github.com

서버

 

Feat: protocol 변경 · Dodontak/Project_Island_GameServer@02cd9b0

protocol 수정에 맞춰서 코드 수정됨

github.com

 

+ Recent posts