막바지에 세웠던 목표들
RedisConnectionPool회원가입, 로그인 로직 구체적으로 정리 및 구현- 쿼리바인더 만들고 업데이트 필요SQL쿼리 바인더 - 현재는 sql인젝션에 취약한 상태임비밀번호 해싱과 관리엑세스 토큰 발급- 타이머 관리 - 현재 사용이 불편함. 근데 사용처는 거의없어서 그냥둬도 될지도?
이메일 API 사용- 너무 느린 빌드 문제 해결하기
Client/Server PacketHandler.h 코드 자동생성 기능 만들기게임서버강의에서 배운 Protocol.proto 파일을 파싱해서 패킷핸들러 헤더를 자동으로 만들어주는 프로그램을 만들자.
이제 인증서버 구동을 위해 필수적으로 해야할것들은 다했다.
목표로 정했던 것 중에 타이머와 느린 빌드 해결이 남았다.
타이머
결론부터 말하면 그대로 둘 것. 딱히 지금 상태에서 바꿀 필요가 없어보인다.
마음에 안들었던 부분은 두가지였는데, 하나는 사용부분이 좀 복잡해보인다는 이유. 다른 하나는 상대방으로부터 클로즈 노티가 와도 타이머를 중단시키지 않는다는 점이다. 즉 정상종료가 되어도 타이머가 사라지지 않는 것.
그런데 이것도 타이머가 엄청 쌓이면 문제가 되겠지만 타이머를 2초로 설정해놨고, 시간이되면 알아서 없어지게 만들어서 쌓일 일이 없어서 그냥 놔둬도 될 것 같다.
vscode, 도커 관련
프로젝트 도중에 vscode 를 컨테이너에서 연결해서 쓰는데에 문제가 좀 있었다.
당시에는 일단 ai한테 해결법 물어보고 막 시도해보다가 원하는대로 동작하니 일단 넘어갔는데 제대로된 해결법을 알아보고 정리해봤다.
컨테이너 접속
VScode에서 도커 컨테이너 접속하기 (Dev Containers)
macOS에서 작성된 글입니다. vscode에서 Remote Development 확장 설치WSL, Dev Containers, Remote - SSH, Remote - Tunnels가 포함된 세트다. 이제 두가지 경우가 있다.컨테이너가 이미 올라가 있는 경우쉬프트 + 커맨
dodontak.tistory.com
자동완성 안됨 문제 해결
VScode 컨테이너 환경에서 c++ 자동완성 쓰기 (Clangd)
macOS, 컨테이너는 debian:bookworm 환경에서 작성된 글입니다. VScode에서 도커 컨테이너 접속하기 (Dev Containers)macOS에서 작성된 글입니다. vscode에서 Remote Development 확장 설치WSL, Dev Containers, Remote - SSH, Re
dodontak.tistory.com
수정할 것 정리
이제 진짜 핵심적인건 다 구현했으니 전체적인 코드의 흐름을 리뷰하면서 수정해야할 점들을 짚어보자.
확인하면서 즉석으로 수정한것들
- EpollEvent 생성할때 자동으로 owner의 _epollevent에 자기자신을 등록하게 바꿔서 외부에서 session->setEpollEvent호출할 일이 없는데 Timer만들때 호출하길래 제거함.
- Session의 Dispatch의 switch case에서 Timer와 Accept를 처리하지 않는 문제. 사실 세션에는 이 스위치 케이스가 들어갈리 없지만 만에하나 들어간다면 disconnect 하는걸로 변경.
- Listener 초기화 할 때 serviceRef 복사생성자 호출하는것을 move로 오버헤드 줄임.
- Listener 초기화 할 때 sock_addr을 service에서 쓸데없이 가져오는 부분 제거.
- Session ProcessRead함수에서 _readBuffer.OnRead 를 실패할 경우 처리 안했어서 수정함.
- handle_c_signup에서 email_skip 추가. email_skip == true이면 pg에서 where email 제외함. redis에서도 set email 제외함. 그리고 클라이언트에게는 계정 생성 성공 패킷 보냄. 더미 클라이언트에서는 success에 email_skip이면 더이상 패킷 보내지 않고 disconnect.
- handle_c 함수들에서 어떤 실패나 오류에도 실패 패킷 보내도록 수정.
- handle_c 함수들에서 is_null 잘못 쓰던부분 수정.
- handle_c 함수들에서 redis->Get~~을 쓰기 전에 null체크를 해야하는데, 안하고 쓰던 부분 수정. 추가로 proto파일에 GetRedis를 하는 모든 message에 expired를 추가해 만료로 인한 실패인지 알려주도록 설정함. 디테일은 클라이언트 만들때 다시 한번 깎아야할 듯 함.
- socketutil의 MakeSocketnonblock 함수 리턴값 void에서 bool로 변경. + 사용처에서 실패처리 추가.
수정할 것들
- Utils.h, cpp 파일 정리하기. 지금은 클래스없이 쓰는 함수들이 있는데 수정하자.
- 로그인 10분간 5회 실패 시 차단기능 만들기
- 이메일, nickname 이상한거 못넣게 유효성 검사 추가하기.
- 스레드 종료방법 구현하기.
Utils.h, cpp 파일 정리하기
생으로 나와있던 함수들을 Utils 클래스에 넣었다.
class Utils
{
public:
static void ErrorExit(const char* err_str);
static std::string GetRandomStr(int len);
static std::string CreateAccessToken(const std::string& user_id, const std::string& nickname);
static bool VerifyAccessToken(const std::string& token, std::string& out_user_id);
};
같이 있는 소켓유틸에도 Bind, SetReuseAddress를 추가했다.
Bind는 없어도 되는데 NetAddress를 만들어놓고 아예 안쓰길래 바인드 함수를 만들어서 사용하게 했다.
SetReuseAddress는 테스트하느라 서버 껐다 켰다 하는데 가끔 같은 포트에 바인드했다고 바인드 에러가 발생해서 그걸 안나게하려고 추가했다.
class SocketUtils
{
public:
static int CreateSocket();
static void CloseSocket(int socket);
static bool MakeSocketNonblock(int sock);
static bool Bind(int socket, NetAddress addr);
static bool SetReuseAddress(int socket, bool flag);
};
로그인 10분간 5회 실패 시 차단기능 만들기
지금은 실패횟수를 redis에 기록하긴 하지만, 구체적인 구현은 안되어있다. 구현해보자.
다른부분은 전부 생략시켰고, fail_count와 블록 처리 부분만 표시했다.
void Handle_C_LOGIN(const PacketSessionRef& session, const Protocol::C_LOGIN& pkt)
{
Protocol::S_LOGIN response;
/*...*/
string pgGetUserData = "SELECT user_id, password, is_block FROM auth.users WHERE nickname = $1";
/*...*/
string a_is_block = pg->GetValue(0, 2);
/*...*/
if (a_is_block == "t")
{//블록된 유저라면
response.set_success(fail);
response.set_is_block(true);
session->Send(ClientPacketHandler::MakeWriteBuffer(response));
return;
}
if (BCrypt::validatePassword(password, a_password))
{//로그인 성공
/*...*/
}
else
{//비밀번호 틀림 레디스에 실패횟수 기록
string redisIncrFailCount = "INCR %s:fail_count EX 600";
RedisConnection* redis = GDBConnectionPool->PopRedis();
if(false == redis->Execute(redisIncrFailCount, nickname.c_str()))
{
redis->Clear();
GDBConnectionPool->Push(&redis);
session->Send(ClientPacketHandler::MakeWriteBuffer(response));
return;
}
int fail_count = redis->GetInt();
response.set_fail_count(fail_count);
redis->Clear();
if (fail_count >= 5)
{
response.set_is_block(true);
string pgBlockSQL = "UPDATE auth.users SET is_block = true WHERE nickname = $1";
PGConnection* pg = GDBConnectionPool->PopPG();
pg->AddValue(nickname);
if(pg->ExecuteSQL(pgBlockSQL) == false)
{
cout << "Fail To pgBlockSQL in Handle_C_LOGIN" << endl;
pg->Clear();
GDBConnectionPool->Push(&pg);
session->Send(ClientPacketHandler::MakeWriteBuffer(response));
return;
}
pg->Clear();
GDBConnectionPool->Push(&pg);
}
}
session->Send(ClientPacketHandler::MakeWriteBuffer(response));
}
이메일, nickname 유효성 검사
이것때문에 정규표현식라는걸 처음 배웠다.
이메일은 [영문숫자]@[영문숫자].[영문] 같은 포멧을 지키도록 만들었다. 총 길이제한은 100자.
닉네임은 영문숫자만 가능하게 만들었다. 길이제한은 2~20자.
#include <regex>
bool Utils::VerifyEmail(const string& email)
{
if (email.length() > 100)
return false;
const regex pattern(R"([a-zA-Z0-9]+@[a-zA-Z0-9]+(\.[a-zA-Z]{2,}){1,2})");
return regex_match(email, pattern);
}
bool Utils::VerifyNickname(const string& nickname)
{
const regex pattern(R"([a-zA-Z0-9]{2,20})");
return regex_match(nickname, pattern);
}
Handle_C_SIGNUP 함수
회원가입 시 패킷으로 온 이메일이나 닉네임이 올바르지 않으면 실패패킷을 보낸다.
if (Utils::VerifyEmail(email) == false || Utils::VerifyNickname(nickname) == false)
{
response.set_reason("invalid email or nickname");
session->Send(ClientPacketHandler::MakeWriteBuffer(response));
return;
}
그리고 실패에 대한 이유가 적혀있으면 좋을 것 같아서 protocol.proto에서 reason을 추가해줬다.
스레드 종료방법 구현하기
void WorkerThread() {
while (true) {
JobRef job = nullptr;
{
unique_lock<mutex> lock(GThreadManager->_workerMutex);
GThreadManager->_workerCv.wait(
lock, []() { return !GJobQueue->Empty(); });
job = GJobQueue->PopJob();
}
if (job) job->Execute();
}
}
void MailThread() {
while (true) {
shared_ptr<Mail> mail = nullptr;
{
unique_lock<mutex> lock(GThreadManager->_mailMutex);
GThreadManager->_mailCv.wait(
lock, []() { return !GSMTPManager->Empty(); });
mail = GSMTPManager->PopMail();
}
if (mail) {
SMTPConnectionRef conn = GSMTPManager->GetConnection();
if (conn) conn->SendMail(mail);
}
}
}
워커스레드와 메일스레드를 보면 지금은 단순히 무한반복을 한다.
사실 지금은 스레드를 중단시키는 경우의 코드가 전혀 없어서 만들어도 쓸모는 없겠지만, 찝찝하니 만들어두자.
스레드 관리이니 ThreadManager에 추가하자.
void WorkerThread() {
while (GThreadManager->IsRunning()) {
/*...*/
}
void MailThread() {
while (GThreadManager->IsRunning()) {
/*...*/
}
class ThreadManager
{
public:
/*...*/
bool IsRunning() { return _running; }
/*...*/
private:
bool _running;
/*...*/
};
ThreadManager::ThreadManager() : _running(true) {/*...*/}
이제 인증서버는 90퍼센트 완성했다.
'프로젝트 > Project_Island' 카테고리의 다른 글
| 23. 게임서버 만들기의 시작 (0) | 2026.03.24 |
|---|---|
| 나중에 인증서버에서 할 일 (0) | 2026.03.17 |
| 21. EmailAPI - SMTP (0) | 2026.03.13 |
| 20. DBConnection 리팩토링 (0) | 2026.03.11 |
| 19. 로그인 (0) | 2026.03.11 |