목표들
RedisConnectionPool회원가입, 로그인 로직 구체적으로 정리 및 구현- 쿼리바인더 만들고 업데이트 필요SQL쿼리 바인더 - 현재는 sql인젝션에 취약한 상태임비밀번호 해싱과 관리엑세스 토큰 발급- 타이머 관리 - 현재 사용이 불편함. 근데 사용처는 거의없어서 그냥둬도 될지도?
- 이메일 API 사용
- 너무 느린 빌드 문제 해결하기
Client/Server PacketHandler.h 코드 자동생성 기능 만들기게임서버강의에서 배운 Protocol.proto 파일을 파싱해서 패킷핸들러 헤더를 자동으로 만들어주는 프로그램을 만들자.
이번엔 이메일 보내는걸 구현해보자.
c++ 로 어떻게 email을 보낼지 여러가지 방법을 알아봤는데, 가장 가벼우면서 접근성이 좋은 SMTP라는 것을 발견했다. 원래는 이메일 API사용을 고려했는데 smtp는 api라고 하긴 좀 그렇고 그냥 구글에서 제공하는 서비스? 느낌이다.
c/c++ 메일 전송(smtp프로토콜) | 이론과 실습
🎄 개요 안녕하세요! 오랜만에 c++ 네트워크글이 돌아왔습니다... 이번에는 저가 엄청나게 유익한 내용을 들고왔는데요.. 흔히 여러분들중에서 키로거사용경험이 있으신분들은 smtp를 이용한 메
mawile.tistory.com
smtp에 대한 공부는 위 블로그 글을 보면서 했고, 내가 실습해본 내용은 아래 게시글에 정리해봤다.
SMTP로 이메일 보내기 with.C/C++
SMTP 란?간이 전자우편 전송 프로토콜 (Simple Mail Transfer Protocol, SMTP).인터넷에서 이메일을 보낼때 쓰는프로토콜. TCP포트번호는 25번이다. 텍스트 기반 프로토콜로서 모든 문자가 7bit ASCII로 되어있
dodontak.tistory.com
smtp는 결국 smtp.gmail.com에 TCP TLS연결을 해서 핸드쉐이크를 하고, 프로토콜에 맞춰서 메시지를 보내면 구글 smtp서버에서 요청한대로 이메일을 보내주는 서버다.
그런데 이게 블로킹으로 하면 한번 이메일을 보내는데 시간이 꽤나 오래 걸리는 문제가 있었다. 만약 ClientPacketHandler에서 패킷을 처리하다가 이메일을 보내야하면 거의 1~2초는 해당 스레드가 멈춰버리는 것. 극단적인 경우를 생각해서 만약 10스레드를 돌리는데 100명이 인증코드 이메일을 요청하면 20초동안 서버가 멈추는것이다. 이건 영 좋지 않으니 절대 패킷핸들러 안에서 블로킹으로 처리해서는 안됐다.
이런 이유로 어떻게 구현할지 고민을 많이 했다. 아예 epoll을 사용해 멀티스레드 비동기 email전송 전담 컨테이너를 만들어야하나? 까지 생각해봤는데, 솔직히 프로젝트에서 이메일이 핵심은 아니기때문에 더 단순한 방법을 생각해봤다.
다른 방법은 session이나 listener처럼 smtpmail epollObject를 추가해서 epoll에 등록시켜놓고 비동기로 처리하는 방법이었다. 그런데 더쉽고 나쁘지 않은 방법이 떠올라서 (게다가 이렇게하면 전체적인 구조를 많이 뜯어고쳐야 해서 귀찮다.) 이것도 폐기했다.
아무튼 최종적으로 선택한 방법은 worker 스레드처럼 담당 mail 스레드를 몇개 설정해서 mail queue에 수신자, 제목, 내용이 담긴 Mail객체를 넣으면 mail스레드가 깨어나서 send처리 하는 것. 기존의 스레드 사용방식과 잡큐 사용방식을 그대로 카피해서 메일용으로 만들어주기만 하면 돼서 굉장히 쉽게 구현할 수 있다.
email은 엄청 빨리 보내져야할 필요도 딱히 없고, 메일을 보내는 과정에서 다른 스레드와 데이터 레이스를 한다거나 하는 경우는 없기때문에 괜찮은 방법이라 생각한다. 다만 100명이 메일을 보내면 마지막에 메일을 보낸 사람은 꽤 늦게 메일을 받을 것 같다.
이것도 개선할 방법이 있긴 한데, smtp연결을 커넥션풀로 관리하는 것. ehlo, TLS연결, AUTH LOGIN까지 마쳐놓은 커넥션들을 미리 준비해놓는다면 메일을 보내는 속도가 훨씬 빨라질 것이다. 하지만 gmail은 내가 관리하는 서버가 아니기때문에 서버측에서 커넥션을 끊는 경우도 처리해야한다. mail담당 epollObject를 만들어 epoll로 감지하도록 만들어야 하는 것. email에 이렇게까지 공을 들이고싶지 않고, 빨리 인증서버를 마무리 짓고싶어서 스레드풀까지만 활용했다.
SMTPConnection.h
TLS연결을 클라이언트 입장에서 다른서버로 해야 하기때문에 기존 서버 서비스와 별도의 CTX를 준비해야한다.
매번 메일 하나 보낼때마다 새로 커넥션을 만들고 핸드셰이크하고 인증하고 보내고 닫고 반복하는게 매우 매우 불편한데, 인증서버 만들면 게임서버도 빨리 만들어야 하는 상황이라 참았다.
#pragma once
#include "Types.h"
#include "SslCtx.h"
#include "SslObject.h"
#include <string>
#include <mutex>
#include <queue>
#include <memory>
typedef struct Mail
{
Mail(std::string _emailTo, std::string _subject, std::string _message) :
emailTo(_emailTo), subject(_subject), message(_message) {}
std::string emailTo;
std::string subject;
std::string message;
} Mail;
class SMTPManager
{
public:
SMTPManager();
~SMTPManager();
void Init(std::string emailFrom);
bool Empty();
void PushMail(std::shared_ptr<Mail> mail);
std::shared_ptr<Mail> PopMail();
SMTPConnectionRef GetConnection();
private:
SslCtx _ctx;
std::string _DNSAddress;
std::string _emailFrom;
std::string _STMPServer;
int _serverPort;
std::mutex _m;
std::queue<std::shared_ptr<Mail>> _mailQueue;
};
class SMTPConnection
{
enum { READ_SIZE = 0x800 };
public:
SMTPConnection(SSL_CTX* ctx, int fd, std::string DNSAddress,
std::string emailFrom, std::string STMPServer);
~SMTPConnection();
void SendMail(std::shared_ptr<Mail> mail);
private:
void Ehlo();
void AuthLogin();
void SendMail(const std::string& emailTo, const std::string& subject, const std::string& message);
void Quit();
private:
BYTE _readBuffer[READ_SIZE + 1];
std::string _writeBuffer;
std::string _DNSAddress;
std::string _emailFrom;
std::string _STMPServer;
int _socket;
SslObject _ssl;
};
SMTPConnection.cpp
중간중간 주석처리된 cout은 테스트의 흔적이다.
앱 비밀번호는 민감한 사항 같으니 getenv를 사용해서 github 에 비밀번호가 올라가는 불상사는 막도록 하자. 참고로 앱 비밀번호를 줄때는 띄어쓰기가 포함되어있는데 사용할때는 띄어쓰기를 빼고 넣어줘야한다.
#include "SMTPConnection.h"
#include "Utils.h"
#include "unistd.h"
#include <netinet/in.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/buffer.h>
#include <netdb.h>
#include <arpa/inet.h>
using namespace std;
string Base64Encode(const string& input)
{
BIO* bio = BIO_new(BIO_f_base64());
BIO* bmem = BIO_new(BIO_s_mem());
bio = BIO_push(bio, bmem);
BIO_set_flags(bio, BIO_FLAGS_BASE64_NO_NL); // 줄바꿈 없이
BIO_write(bio, input.c_str(), (int)input.size());
BIO_flush(bio);
BUF_MEM* bptr;
BIO_get_mem_ptr(bio, &bptr);
string result(bptr->data, bptr->length);
BIO_free_all(bio);
return result;
}
/*===================
SMTPManager
===================*/
SMTPManager::SMTPManager() : _ctx(false), _DNSAddress("google.co.kr"),
_STMPServer("smtp.gmail.com"), _serverPort(587)
{
}
SMTPManager::~SMTPManager() {}
void SMTPManager::Init(string emailFrom)
{
_emailFrom = emailFrom;
}
SMTPConnectionRef SMTPManager::GetConnection()
{
int serverSock = SocketUtil::CreateSocket();
if (serverSock < 0)
{
cerr << "SMTPManager GerConnection CeateSocket\n";
return nullptr;
}
struct sockaddr_in mailServerAddr;
memset(&mailServerAddr, 0, sizeof(struct sockaddr_in));
hostent* host = gethostbyname(_STMPServer.c_str());
if (host == nullptr)
{
cerr << "SMTPManager GerConnection gethostbyname error\n";
SocketUtil::CloseSocket(serverSock);
return nullptr;
}
memcpy(&(mailServerAddr.sin_addr), host->h_addr_list[0], host->h_length);
mailServerAddr.sin_family = host->h_addrtype;
mailServerAddr.sin_port = htons(_serverPort);
int result = connect(serverSock, (sockaddr*)&mailServerAddr, sizeof(mailServerAddr));
if (result == -1)
{
cerr << "SMTPManager GerConnection connect error\n";
SocketUtil::CloseSocket(serverSock);
return nullptr;
}
SMTPConnectionRef conn = make_shared<SMTPConnection>(_ctx.GetCtx(), serverSock, _DNSAddress,
_emailFrom, _STMPServer);
if (conn == nullptr)
SocketUtil::CloseSocket(serverSock);
return conn;
}
bool SMTPManager::Empty()
{
return _mailQueue.empty();
}
void SMTPManager::PushMail(shared_ptr<Mail> mail)
{
lock_guard<mutex> lock(_m);
_mailQueue.push(mail);
}
shared_ptr<Mail> SMTPManager::PopMail()
{
lock_guard<mutex> lock(_m);
if (_mailQueue.empty())
return nullptr;
shared_ptr<Mail> ret = _mailQueue.front();
_mailQueue.pop();
return ret;
}
/*===================
SMTPConnection
===================*/
SMTPConnection::SMTPConnection(SSL_CTX* ctx, int fd, string DNSAddress, string emailFrom, string STMPServer)
: _ssl(ctx, fd), _socket(fd), _DNSAddress(DNSAddress), _emailFrom(emailFrom), _STMPServer(STMPServer)
{
_writeBuffer.reserve(READ_SIZE);
}
SMTPConnection::~SMTPConnection()
{
SocketUtil::CloseSocket(_socket);
}
void SMTPConnection::SendMail(shared_ptr<Mail> mail)
{
Ehlo();
AuthLogin();
SendMail(mail->emailTo, mail->subject, mail->message);
Quit();
}
void SMTPConnection::Ehlo()
{
size_t readLen = 0;
size_t writeLen = 0;
// cout << _socket << endl;
/*========== ehlo ==========*/
readLen = read(_socket, _readBuffer, READ_SIZE);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
_writeBuffer = "ehlo " + _DNSAddress + "\r\n";
// cout << _writeBuffer;
write(_socket, (BYTE*)_writeBuffer.data(), _writeBuffer.length());
_writeBuffer.clear();
/*========== START TLS ==========*/
readLen = read(_socket, _readBuffer, READ_SIZE);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
_writeBuffer = "STARTTLS\r\n";
// cout << _writeBuffer;
write(_socket, (BYTE*)_writeBuffer.data(), _writeBuffer.length());
_writeBuffer.clear();
/*========== TLS ehlo ==========*/
readLen = read(_socket, _readBuffer, READ_SIZE);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
_ssl.Connect();
_writeBuffer = "ehlo " + _DNSAddress + "\r\n";
// cout << _writeBuffer;
_ssl.Write((BYTE*)_writeBuffer.data(), _writeBuffer.length(), &writeLen);
_writeBuffer.clear();
}
void SMTPConnection::AuthLogin()
{
size_t readLen = 0;
size_t writeLen = 0;
/*========== AUTH LOGIN ==========*/
_ssl.Read(_readBuffer, READ_SIZE, &readLen);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
_writeBuffer = "AUTH LOGIN\r\n";
// cout << _writeBuffer;
_ssl.Write((BYTE*)_writeBuffer.data(), _writeBuffer.length(), &writeLen);
_writeBuffer.clear();
/*========== encodedEmail ==========*/
string encodedEmail = Base64Encode(_emailFrom);
_ssl.Read(_readBuffer, READ_SIZE, &readLen);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
_writeBuffer = encodedEmail + "\r\n";
// cout << _writeBuffer;
_ssl.Write((BYTE*)_writeBuffer.data(), _writeBuffer.length(), &writeLen);
_writeBuffer.clear();
/*========== encodedPw ==========*/
string encodedPw = Base64Encode(getenv("SMTP_APP_PASSWORD"));
_ssl.Read(_readBuffer, READ_SIZE, &readLen);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
_writeBuffer = encodedPw + "\r\n";
// cout << _writeBuffer;
_ssl.Write((BYTE*)_writeBuffer.data(), _writeBuffer.length(), &writeLen);
_writeBuffer.clear();
}
void SMTPConnection::SendMail(const string& emailTo, const string& subject, const string& message)
{
size_t readLen;
size_t writeLen;
/*========== mail ==========*/
_ssl.Read(_readBuffer, READ_SIZE, &readLen);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
_writeBuffer = "mail from:<" + _emailFrom + ">\r\n";
// cout << _writeBuffer;
_ssl.Write((BYTE*)_writeBuffer.data(), _writeBuffer.length(), &writeLen);
_writeBuffer.clear();
/*========== rcpt ==========*/
_ssl.Read(_readBuffer, READ_SIZE, &readLen);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
_writeBuffer = "rcpt to:<" + emailTo + ">\r\n";
// cout << _writeBuffer;
_ssl.Write((BYTE*)_writeBuffer.data(), _writeBuffer.length(), &writeLen);
_writeBuffer.clear();
/*========== data ==========*/
_ssl.Read(_readBuffer, READ_SIZE, &readLen);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
_writeBuffer = "data\r\n";
// cout << _writeBuffer;
_ssl.Write((BYTE*)_writeBuffer.data(), _writeBuffer.length(), &writeLen);
_writeBuffer.clear();
/*========== subject ==========*/
std::string msgId = std::to_string(time(nullptr)) + "-dodontak@gmail.com";
_ssl.Read(_readBuffer, READ_SIZE, &readLen);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
_writeBuffer = "To: " + emailTo + "\r\n" +
"From: " + _emailFrom + "\r\n" +
"Subject: " + subject + "\r\n" +
"Message-ID: " + msgId + "\r\n\r\n" +
message + "\r\n.\r\n";
// cout << _writeBuffer;
_ssl.Write((BYTE*)_writeBuffer.data(), _writeBuffer.length(), &writeLen);
_writeBuffer.clear();
}
void SMTPConnection::Quit()
{
size_t readLen = 0;
size_t writeLen = 0;
/*========== quit ==========*/
_ssl.Read(_readBuffer, READ_SIZE, &readLen);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
_writeBuffer = "quit\r\n";
// cout << _writeBuffer;
_ssl.Write((BYTE*)_writeBuffer.data(), _writeBuffer.length(), &writeLen);
_writeBuffer.clear();
_ssl.Read(_readBuffer, READ_SIZE, &readLen);
_readBuffer[readLen] = 0;
// cout << _readBuffer << endl;
}
사용
ClientPackerHandler에서 인증코드를 보내는 부분이 있다. (메인스레드)
GSMTPManager->PushMail(make_shared<Mail>(a1_email, "Verify Code From AuthServer", verfiy_code));
GThreadManager->_mailCv.notify_one();
notify_one으로 아래 코드에서 자고있던 스레드를 깨워서 SendMail 함수를 실행시킨다. (mail스레드)
void MailThread()
{
while (true)
{
shared_ptr<Mail> mail = nullptr;
{
std::unique_lock<std::mutex> lock(GThreadManager->_mailMutex);
GThreadManager->_mailCv.wait(lock, []() { return !GSMTPManager->Empty(); });
mail = GSMTPManager->PopMail();
}
if (mail)
{
GSMTPManager->GetConnection()->SendMail(mail);
}
}
}
mail스레드 3개, 이메일 요청 10개 일 때 처리가 잘 되는지 확인해봤다.

예상대로 이메일 담당 스레드가 3개밖에 안되고, 매번 인증을 하고 보내려니 좀 느리긴 했다.
나중에 시간이 남으면 개선해보자.
'프로젝트 > Project_Island' 카테고리의 다른 글
| 나중에 인증서버에서 할 일 (0) | 2026.03.17 |
|---|---|
| 22. 인증서버 마무리 (0) | 2026.03.15 |
| 20. DBConnection 리팩토링 (0) | 2026.03.11 |
| 19. 로그인 (0) | 2026.03.11 |
| 18. 회원가입 (0) | 2026.03.10 |