지금까지 서버코드를 짜면서 클라이언트를 딱히 고려하지 않았기때문에 뜯어고쳐야할 부분이 많다.
하나하나 고쳐가면서 클라이언트가 서버로 패킷을 보내고, 받을 수 있도록 만들어보자. 그리고 이번에 만든 클라이언트로 나중에 성능테스트도 할 수 있게 최대한 잘 만들어보자.
서버측 accept 과정 수정.
클라이언트가 connect를 하는 부분을 만들기 전에 서버의 accept부분을 살펴봤는데 문제가 있었다.
기존 accept과정 - (블로킹, 서버측에서 먼저 핸드셰이크 요청)
- listener에서 accept로 tcp/ip 연결 성공
- 서버가 SSL_accept 호출 (SslObject생성자에서 블로킹으로 실행됨)
- 핸드셰이크 완료시 소켓 논블로킹으로 변경 후 epoll에 fd등록
기존의 방법은 블로킹 상태로 서버가 먼저 ssl_accept를 하기때문에 만약 클라이언트가 connect만 하고 ssl_accept를 안해주면 영원히 멈춰버리는것. 그래서 실제ssl핸드셰이크 순서를 참고하여 클라이언트가 먼저 ssl 핸드셰이크를 요청하는 방식으로 수정했다. 또 기존에는 논블로킹으로 만드는게 어려워서 일단 블로킹으로 ssl연결만 하고 논블로킹으로 변경했는데, connect이후 즉시 논블로킹으로 변경하도록 수정했다.
수정된 accept과정 - (논블로킹, 클라측에서 먼저 핸드셰이크 요청)
- listenser에서 accept로 tcp/ip 연결 성공.
- SslObjectRef 생성, session에 저장만 해둠.
- 소켓 논블로킹으로 변경 후 epoll에 fd 등록. EventType::HandShaking (이벤트 발생시 핸드셰이크 함)
- read 이벤트 발생하면 SslObject->SslHandShake 호출. ssl 없으면 만들고, SSL_accept 호출함.
성공시 EventType::Read로 변경. 실패 시 리턴값에 따라 재시도 or disconnect
ThreadManager Launch 수정
기존에는 main문에서
for (int i = 0; i < 5; i++)
GThreadManager->Launch();
이렇게 실행하면 ThreadManager의 멤버함수 WorkerThread를 호출하는 방식이었다.
이 방법은 서버만 구현할때는 상관없었지만, 클라이언트와 서버가 스레드에서 할 일이 다르기때문에 수정해야한다.
Launch함수의 인자로 콜백 함수를 넣어서, 스레드를 실행하면 콜백함수를 호출하도록 만들고, 콜백함수는 클라나 서버에서 커스텀해서 넣자.
void ThreadManager::Launch(std::function<void()> callback)
{
_workerThreads.push_back(std::thread([=](){
InitTLS();
callback();
DestroyTLS();
}));
}
기존의 ThraedManager::WorkerThread()
void ThreadManager::WorkerThread()
{
while (true)
{
JobRef job = nullptr;
{
std::unique_lock<std::mutex> lock(_m);
_cv.wait(lock, [this]() { return !GJobQueue->Empty(); });
job = GJobQueue->PopJob();
}
std::cout << "thread No." << LThreadId << " Awaken!" << std::endl;
job->Execute();
}
}
그런데 문제가 있다. 단순히 멤버함수를 지우고, 외부로 꺼내는걸로는 해결이 안된다.
내부에서 멤버 mutex도 사용하고, 멤버 condition_variable도 사용하기 때문. 일단은 _m과 _cv를 public으로 바꿔서 해결했다.
덤으로 왜 있었는지 모르는 캡쳐 [this]도 제거해줬다.
AuthServer main.cpp
void WorkerThread()
{
while (true)
{
JobRef job = nullptr;
{
std::unique_lock<std::mutex> lock(GThreadManager->_m);
GThreadManager->_cv.wait(lock, []() { return !GJobQueue->Empty(); });
job = GJobQueue->PopJob();
}
std::cout << "thread No." << LThreadId << " Awaken!" << std::endl;
job->Execute();
}
}
for (int i = 0; i < 5; i++)
GThreadManager->Launch(WorkerThread);
이제 이렇게 해주면 커스텀 함수를 스레드에서 실행되게 만들 수 있다.
Service.h 수정하기
service에서 모든 로직을 실행시키는데, 지금은 service가 오직 인증서버만을 위해 만들어져있다.
인증서버용, 더미클라이언트용으로 분리시킬 수 있게 수정하자.
Service.h
AuthService와 ClientService를 추가하고, Service를 상속받는다.
Auth는 이전에 쓰던 생성자 파라메터를 그대로 가져오고, Client는 이중 인증서와 키는 필요없으니 빼자.
class AuthService : public Service
{
public:
AuthService(const char* ip, int port, const char* certFile, const char* keyFile, SessionFactory factory);
virtual ~AuthService();
virtual int Start();
};
class ClientService : public Service
{
public:
ClientService(const char* ip, int port, SessionFactory factory);
virtual ~ClientService();
virtual int Start();
};
base Service 생성자도 client와 auth가 겹치는것만 필요하도록 수정하자.
Service::Service(const char* ip, int port, SessionFactory factory)
: _addr(ip, port), _sessionFactory(factory)
{
_ctx = std::make_shared<SslCtx>();
}
Service의 Start함수를 추상함수로 바꾸고, 기존의것은 AuthService::Start로 가져오자.
클라이언트의 Start는 일단 비워두고 나중에 손보자. 일단은 클라이언트용와 인증서버용 서비스를 분리하는데에 의의를 두자.
int AuthService::Start()
{
signal(SIGPIPE, SIG_IGN);
ListenerRef listener = make_shared<Listener>(shared_from_this(), _addr.GetPort());
EpollEvent* listenEvent = new EpollEvent(listener, EventType::Accept);
_epollCore = make_shared<EpollCore>();
_epollCore->Register(listenEvent);
_epollCore->StartEpollWait();
return 1;
}
int ClientService::Start()
{
signal(SIGPIPE, SIG_IGN);
return 1;
}
그리고 이미 바꿔놨지만 기록하지 않은게 있는데 세션 생성자를 바꿨다.
기존엔 Listener에서 accept를 한 뒤에 Session을 만들어서 클라이언트를 관리했다. 그런데 지금은 PacketSession, AuthSession까지 생겼고, 클라이언트에서 사용할 세션은 또 다를것이기 때문에 각각의 세션에 맞는 생성자로 세션을 생성해줘야 한다.
Service.h
using SessionFactory = function<SessionRef(int, struct sockaddr_in, ServiceRef)>;
class Service : public enable_shared_from_this<Service>
{
public:
Service(const char* ip, int port, SessionFactory factory);
virtual ~Service() {}
SessionRef MakeSession(int clinetSocket, struct sockaddr_in addr, ServiceRef service);
/*...*/
SessionFactory _sessionFactory;
};
SessionRef Service::MakeSession(int clinetSocket, struct sockaddr_in addr, ServiceRef service)
{
return _sessionFactory(clinetSocket, addr, service);
}
/* AuthService 생성자 */
AuthService(const char* ip, int port, const char* certFile, const char* keyFile,
SessionFactory factory);
/*----------------------- main.cpp --------------------------------*/
main()
{
ServiceRef service = make_shared<AuthService>(
"127.0.0.1",
stoi(av[1]),
"TLS/server.crt",
"TLS/server.key",
[](int clientSocket, sockaddr_in addr, ServiceRef service) {
return std::make_shared<CAuthSession>(clientSocket, addr, service);
} // CAuthSessionRef를 생성해서 리턴해주는 함수 (생성자 역할로 넣음)
);
}
이렇게 Service에 세션 생성자를 _sessionFactory에 저장해서 Listener에서 SessionRef를 만들때 사용한다.
SessionRef session = _service->MakeSession(clientSocket, client_addr, _service);
일단 이렇게 DummyClient를 만들기 위해서 몇가지 작업을 마쳤다.
- SSL_accept 클라이언트가 먼저 요청하는 형식으로 변경
- ThreadManager가 커스텀 함수를 처리할 수 있도록 수정
- 인증서버용 Service와 클라이언트용 Service의 분리


일단 이것저것 변경한 서버쪽은 잘 돌아간다.
근데 막상 여기까지 했는데 epoll로 클라이언트를 어떻게 여럿 접속시킬지 잘 안떠오른다. 좀 고민이 필요한 문제같다. 원래는 게임서버 강의를 참고해서 만들면 되겠지 싶었는데 IOCP랑 epoll이 좀 많이 달라서 생각보다 어려울 것 같다.
'프로젝트 > Project_Island' 카테고리의 다른 글
| 13. DummyClient <-> AuthServer 통신 (0) | 2026.03.07 |
|---|---|
| 12. DummyClient (2) (0) | 2026.03.06 |
| 10. 프로젝트 리펙토링 + 모듈화 (0) | 2026.03.06 |
| 9. DBConnectionPool 기초다지기 (0) | 2026.03.05 |
| 8. PacketHandler (0) | 2026.03.04 |