지난번까지 IOCP 로 패킷단위 통신을 protobuf와 패킷핸들러를 사용하는 형태로 채팅서버를 구현했다.
원래는 이제 구체적인 게임 기획과 proto파일 구성을 하려했는데, 해야 할 것 들을 정리해보니 아직 할 일이 남아있었다.
- DB커넥션, 풀
- TSL 적용하기
지금 TLS를 적용시켜놓는게 좋겠다 생각이 들어서 하려고 한다.
openssl , openssl 라이브러리 설치, 개인키와 인증서 생성, iocp모델에서 openssl 사용
이건 내용이 좀 많고, 여기에 다 정리하기에 적절하지 않은 것 같아 따로 글을 작성했다.
openssl BIO를 사용해 암호화 통신 구현하기 with.c++, IOCP
openssl BIO에 대한 지식보다는 왜 쓰게 됐는지, 어떻게 썼는지 경험을 정리한 글입니다. 이전에 만든 IOCP 에코서버를 베이스로 작성된 글입니다. I/O Multiplexing IOCP 에코서버 만들기 with.C/C++리눅스OS
dodontak.tistory.com
openssl과 라이브러리를 설치하고, 테스트도 해봤다!
목표
인증서버를 만들때는 어차피 모든 통신을 TLS보안통신을 해야 한다 생각했기 때문에 애초에 세션에 SSL 객체를 포함시켰고, 아예 평문통신에 대한 고려는 하지 않았다. 그런데 게임서버에서는 아직 평문통신을 해야할지 보안통신을 해야할지, 아니면 부분적으로만 보안통신을 해야할지 그렇게 하는게 가능은 할지 잘 모르는 상태다.
특히 TLS로 통신하면 암호화/복호화에 의한 오버헤드가 발생하기 때문에 안그래도 실시간으로 많은 양의 송 수신을 담당하는 게임서버 입장에서 성능적으로 부정적 영향이 클 수가 있다. 그래서 이를 직접 테스트 해보고싶어서 어떤 스위치를 만들어 보안통신과 평문통신을 갈아끼면서 할 수 있도록 만들고싶었다.
그래서 목표는 이렇다.
- 평문통신과 암호문통신을 선택적으로 할 수 있도록 TLS 계층 추가하기
구성

위와 같이 Session을 상속받는 TLSSession을 만들어서 PacketSession이 무엇을 상속받느냐에 따라 평문통신을 하거나, 암호문 통신을 하도록 만들었다.
핸드셰이크, 암/복호화 간단 요약
핸드셰이크 과정
- 클라이언트가 SSL_connect 호출. ssl의 wbio에 hello 데이터 쌓임.
- 클라의 wbio에 쌓인 데이터를 sendBuffer에 옮겨서 서버로 전송.
- 서버에서 recv받은 데이터를 rbio에 넣음.
- SSL_accept 호출. wbio에 response 데이터 쌓임.
- 서버의 wbio에 쌓인 데이터를 sendBuffer로 옮겨서 클라에게 전송
- 클라에서 recv받은 데이터를 rbio에 넣음.
- SSL_connect호출. wbio에 response 데이터 쌓임.
- ...
위와 같은 과정을 반복하여 SSL_connect와 SSL_accept가 1(OK)를 리턴할때까지 반복하면 된다. 단 OK를 리턴한 이후에도 끝이 아니고 보내야할 문자가 또 wbio에 쌓이니까 그것까지 보내줘야한다. (클라이언트 측에서 그랬었다.)
암호화 과정
send할 때 내가 가진 데이터를 암호화해서 보내야 한다. 그 과정은 이렇다.
- 평문 데이터를 SSL_write로 wbio에 넣음 - 이 때 평문이 암호문으로 변경된다.
- BIO_read로 wbio에 적힌 암호문을 sendBuffer에 옮겨 적음.
- Send함수로 sendBuffer를 상대에게 보냄.
복호화 과정
- recvBuffer1에 받은 암호문을 BIO_write로 rbio에 그대로 옮겨 적음.
- SSL_read로 rbio에 담긴 암호문을 평문으로 해독하여 recvBuffer2로 옮겨 적음.
- 평문으로 된 데이터를 사용함.
SslObject 클래스 추가
ssl, rbio, wbio를 관리하는 클래스.
SSL Accept, Connect, Read, Write와 BIO read, write를 해준다.
#pragma once
#include <openssl/ssl.h>
enum SslStatus
{
Ok,
WantRead,
WantWrite,
Shutdown,
Fail
};
class SslObject
{
public:
SslObject() = default;
~SslObject();
void Init(SSL_CTX* ctx);
SslStatus Accept();
SslStatus Connect();
SslStatus Read(BYTE* buffer, size_t readSize, size_t* readLen);
SslStatus Write(BYTE* buffer, size_t dataLen, size_t* writtenLen);
uint32 HasSslPending();
uint32 GetRBioPendingSize();
uint32 GetWBioPendingSize();
uint32 ReadRBio(BYTE* buffer, int32 readSize);
uint32 WriteRBio(BYTE* buffer, int32 readSize);
uint32 WriteWBio(BYTE* buffer, int32 dataLen);
uint32 ReadWBio(BYTE* buffer, int32 readSize);
private:
SSL* _ssl = nullptr;
BIO* _rbio = nullptr;
BIO* _wbio = nullptr;
};
#include "pch.h"
#include "SslObject.h"
SslObject::~SslObject()
{
SSL_free(_ssl);
}
void SslObject::Init(SSL_CTX* ctx)
{
ASSERT_CRASH(_ssl = SSL_new(ctx));
ASSERT_CRASH(_rbio = BIO_new(BIO_s_mem()));
ASSERT_CRASH(_wbio = BIO_new(BIO_s_mem()));
SSL_set_bio(_ssl, _rbio, _wbio);
}
// rbio에 적힌 데이터를 읽어서 핸드쉐이크 하는 함수. (서버 -> 클라)
// 보낼 데이터가 있으면 wbio에 적히고, 데이터는 꺼내서 직접 보내야함.
SslStatus SslObject::Accept()
{
int32 ret = SSL_accept(_ssl);
if (ret == 1)
return SslStatus::Ok;
int32 err = SSL_get_error(_ssl, ret);
if (err == SSL_ERROR_WANT_READ)
return SslStatus::WantRead;
else if (err == SSL_ERROR_WANT_WRITE)
return SslStatus::WantWrite;
else
return SslStatus::Fail;
}
// rbio에 적힌 데이터를 읽어서 핸드쉐이크 하는 함수. (클라 -> 서버)
// 보낼 데이터가 있으면 wbio에 적히고, 데이터는 꺼내서 직접 보내야함.
SslStatus SslObject::Connect()
{
int32 ret = SSL_connect(_ssl);
if (ret == 1)
return SslStatus::Ok;
int32 err = SSL_get_error(_ssl, ret);
if (err == SSL_ERROR_WANT_READ)
return SslStatus::WantRead;
else if (err == SSL_ERROR_WANT_WRITE)
return SslStatus::WantWrite;
else
return SslStatus::Fail;
}
// rbio에 적힌 데이터를 복호화 하는 함수
SslStatus SslObject::Read(BYTE* buffer, size_t readSize, size_t* readLen)
{
int32 ret = SSL_read_ex(_ssl, buffer, readSize, readLen);
if (ret == 0) // 실패
{
int32 err = SSL_get_error(_ssl, ret);
if (err == SSL_ERROR_WANT_READ)//복호화 하기에 데이터 부족함
return SslStatus::WantRead;
else if (err == SSL_ERROR_ZERO_RETURN)
return SslStatus::Shutdown;
else
return SslStatus::Fail;
}
return SslStatus::Ok;
}
// wbio에 데이터를 암호화 해서 쓰는 함수
SslStatus SslObject::Write(BYTE* buffer, size_t dataLen, size_t* writtenLen)
{
int32 ret = SSL_write_ex(_ssl, buffer, dataLen, writtenLen);
if (ret == 0) // 실패 사실상 발생하지 않음.
return SslStatus::Fail;
return SslStatus::Ok;
}
// rbio에 복호화 할 수 있는 데이터가 남아있는지 확인하는 함수.
// 1 데이터 있음. 0 데이터 없음.
uint32 SslObject::HasSslPending()
{
return SSL_has_pending(_ssl);
}
uint32 SslObject::GetRBioPendingSize()
{
return BIO_pending(_rbio);
}
uint32 SslObject::GetWBioPendingSize()
{
return BIO_pending(_wbio);
}
// bio에 암/복호화 없이 직접 읽거나 쓰는 함수들.
// 리턴값 > 0 읽거나 쓴 바이트 수, 0 -1 실패, -2 BIO 오류
uint32 SslObject::ReadRBio(BYTE* buffer, int32 readSize)
{
return BIO_read(_rbio, buffer, readSize);
}
uint32 SslObject::WriteRBio(BYTE* buffer, int32 writeSize)
{
return BIO_write(_rbio, buffer, writeSize);
}
uint32 SslObject::ReadWBio(BYTE* buffer, int32 readSize)
{
return BIO_read(_wbio, buffer, readSize);
}
uint32 SslObject::WriteWBio(BYTE* buffer, int32 dataLen)
{
return BIO_write(_wbio, buffer, dataLen);
}
TLSSession 클래스 추가
class TLSSession : public Session
{
public:
TLSSession(ServiceRef service);
virtual void TLSAccept() final;
virtual void TLSConnect() final;
virtual void ProcessTLSHandshakeAcceptRecv(int32 numOfBytes) final;
virtual void ProcessTLSHandshakeConnectRecv(int32 numOfBytes) final;
virtual uint8 Decrypt(RecvBuffer& encBuffer, RecvBuffer& decBuffer) final;
virtual bool Encrypt(SendBufferRef& decBuffer, SendBufferRef& encBuffer) final;
virtual bool HasSslPendingData() final { return _ssl.HasSslPending(); }
void HandshakeSend();
protected:
SslObject _ssl;
};
TLSSession::TLSSession(ServiceRef service) : Session(service)
{
if (ServiceRef service = _service.lock())
_ssl.Init(service->GetSSLContext());
}
void TLSSession::TLSAccept()
{
SslStatus status = _ssl.Accept();
uint32 pendingDataSize;
switch (status)
{
case SslStatus::Ok:
cout << "OK" << endl;
HandshakeSend();
_recvEvent.SetEventType(EventType::Recv);
RegisterRecv();
break;
case SslStatus::WantRead:
//wbio에 보낼 데이터가 생겼으면 보내고 recv 등록
_recvEvent.SetEventType(EventType::TLSHandshakeAcceptRecv);
HandshakeSend();
RegisterRecv();
break;
case SslStatus::WantWrite:
//wbio가 꽉 차서 Accept가 진행되지 못한 경우. wbio에 있는 데이터를 Send한다.
HandshakeSend();
break;
default:
// 에러 발생함. 연결 종료.
RegisterDisconnect();
break;
}
}
void TLSSession::TLSConnect()
{
SslStatus status = _ssl.Connect();
uint32 pendingDataSize;
switch (status)
{
case SslStatus::Ok:
cout << "OK" << endl;
HandshakeSend();
_recvEvent.SetEventType(EventType::Recv);
RegisterRecv();
break;
case SslStatus::WantRead:
//wbio에 보낼 데이터가 생겼으면 보내고 recv 등록
_recvEvent.SetEventType(EventType::TLSHandshakeConnectRecv);
HandshakeSend();
RegisterRecv();
break;
case SslStatus::WantWrite:
//wbio가 꽉 차서 Accept가 진행되지 못한 경우. wbio에 있는 데이터를 Send한다.
HandshakeSend();
break;
default:
// 에러 발생함. 연결 종료.
RegisterDisconnect();
break;
}
}
//
void TLSSession::ProcessTLSHandshakeAcceptRecv(int32 numOfBytes)
{
_recvEvent.Clear();
if (numOfBytes == 0) // 클라이언트가 정상적으로 연결을 종료한 경우
{
RegisterDisconnect();
return;
}
RecvBuffer& encBuffer = GetEncRecvBuffer();
encBuffer.OnWrite(numOfBytes);
uint32 wlen = _ssl.WriteRBio(encBuffer.ReadPos(), numOfBytes);
encBuffer.OnRead(wlen);
encBuffer.Clean();
TLSAccept();
}
void TLSSession::ProcessTLSHandshakeConnectRecv(int32 numOfBytes)
{
_recvEvent.Clear();
if (numOfBytes == 0) // 서버가 정상적으로 연결을 종료한 경우
{
RegisterDisconnect();
return;
}
RecvBuffer& encBuffer = GetEncRecvBuffer();
encBuffer.OnWrite(numOfBytes);
uint32 wlen = _ssl.WriteRBio(encBuffer.ReadPos(), numOfBytes);
encBuffer.OnRead(wlen);
encBuffer.Clean();
TLSConnect();
}
// enc버퍼의 암호문을 복호화 해서 dec버퍼에 넣는 함수.
// 리턴 0 성공. 1 shutdown, 2 에러
uint8 TLSSession::Decrypt(RecvBuffer& encBuffer, RecvBuffer& decBuffer)
{
uint32 wlen = _ssl.WriteRBio(encBuffer.ReadPos(), encBuffer.DataSize());
encBuffer.OnRead(wlen);
size_t recvSize;
SslStatus status = _ssl.Read(decBuffer.WritePos(), decBuffer.FreeSize(), &recvSize);
switch (status)
{
case SslStatus::Ok:
decBuffer.OnWrite(recvSize);
return 0;
case SslStatus::WantRead://복호화 하기에 데이터 부족함.
return 0;
case SslStatus::Shutdown://상대가 shutdown함. shutdown 호출 가능.
return 1;
default:// 에러 발생. shutdown 호출 불가.
return 2;
}
}
// dec버퍼의 평문을 암호화 해서 enc버퍼에 넣는 함수.
bool TLSSession::Encrypt(SendBufferRef& decBuffer, SendBufferRef& encBuffer)
{
size_t writtenLen;
SslStatus status = _ssl.Write(decBuffer->GetBuffer(), decBuffer->GetDataLen(), &writtenLen);
if (status == SslStatus::Fail)
return false;
uint32 pendingSize = _ssl.GetWBioPendingSize();
if (pendingSize == 0)
return false;
SendBufferRef sendBuffer = make_shared<SendBuffer>(pendingSize);
uint32 rlen = _ssl.ReadWBio(sendBuffer->GetBuffer(), pendingSize);
sendBuffer->OnWrite(rlen);
encBuffer = sendBuffer;
return true;
}
void TLSSession::HandshakeSend()
{
uint32 pendingDataSize = _ssl.GetWBioPendingSize();
if (pendingDataSize > 0)
{
SendBufferRef sendBuffer = make_shared<SendBuffer>(pendingDataSize);
uint32 readLen = _ssl.ReadWBio(sendBuffer->GetBuffer(), pendingDataSize);
sendBuffer->OnWrite(readLen);
{
lock_guard<mutex> lock(_m);
_sendBuffers.push(sendBuffer);
}
bool expected = false;
if (_sendRegistered.compare_exchange_strong(expected, true))
{
RegisterSend();
}
}
}
핸드셰이크는 서버는 ProcessAccept 이후에, 클라이언트는 ProcessConnect 이후에 즉시 시작하도록 하기 위해 아래와 같이 넣어줬다.
void Listener::ProcessAccept(SessionRef session, AcceptEvent* acceptEvent)
{
/*...*/
session->_isConnected.store(true);
_service->AddSession(session);
session->TLSAccept();
}
void Session::ProcessConnect()
{
_connectEvent.Clear();
_isConnected.store(true);
if (ServiceRef service = _service.lock())
service->AddSession(static_pointer_cast<Session>(shared_from_this()));
TLSConnect();
}
기존에는 ProcessConnect를 호출해서 isConnected = true, AddSession, RegisterRecv를 했는데, 이제는 즉시 RegisterRecv를 하면 안되고 TLS핸드셰이크를 해야하니까 이렇게 바꿨다.
평문통신용으로 Session::TLSAccept, Connect 를 아래와 같이 만들었기 때문에 만약 패킷세션이 Session을 상속받는다면 TLS 핸드셰이크 없이 바로 평문통신을 할 수 있도록 만들어줬다.
void Session::TLSAccept()
{
RegisterRecv();
}
void Session::TLSConnect()
{
RegisterRecv();
}
마찬가지로 TLSSession의 거의 모든 virtual 함수들은 위와같은 이유로 virtual로 만들어졌으며 기존 Session에 구현된 virtual함수는 아무것도 안하는 함수들이고 TLSSession을 상속받았을때만 암호화통신 관련 작업을 하도록 설계했다. (굉장히 오랫동안 고민해서 만들어진 구성이다. 거의 3~4일동안 만들었다 지웠다 한듯...)
자세한 코드는 github를 참조하자. 여기 다 적기엔 너무 양이 많다.
테스트


암호화 통신(좌)
class PacketSession : public TLSSession
{
public:
PacketSession(ServiceRef service) : TLSSession(service) {}
virtual ~PacketSession() {}
virtual uint32 OnRecv(BYTE* buffer, uint32 len) final;
virtual void OnRecvPacket(BYTE* buffer, uint32 size) abstract;
};
평문 통신 (우)
class PacketSession : public Session
{
public:
PacketSession(ServiceRef service) : Session(service) {}
virtual ~PacketSession() {}
virtual uint32 OnRecv(BYTE* buffer, uint32 len) final;
virtual void OnRecvPacket(BYTE* buffer, uint32 size) abstract;
};
PacketSession이 TLSSession을 상속받으면 암호화통신, Session을 상속받으면 평문통신을 하도록 만들었는데 잘 된다!
현재까지의 git 버전
Fix: Send에서 Encrypt 실패처리 · Dodontak/Project_Island_GameServer@f19bb4f
Session::Send - Encrypt 실패 시 return 하도록 처리함. (발생할 일은 거의 없을듯) TLSSession:Decrypt - _ssl.Read를 할 때 bio에 적힌걸 버퍼에 복사하는것이기 때문에 WritePos 를 가져와야하는데 ReadPos를 가져오
github.com
'프로젝트 > Project_Island' 카테고리의 다른 글
| 40. 외부 라이브러리들 dll -> lib 변경 (0) | 2026.04.13 |
|---|---|
| 39. DBConnection, Pool 만들기 (0) | 2026.04.09 |
| 37. PacketHandler 테스트 (0) | 2026.04.04 |
| 36. PacketHandler추가, 코드 자동화 (0) | 2026.04.03 |
| 35. Protobuf 추가하기 (0) | 2026.04.02 |