MFC 소켓 | CAsyncSocket·CSocket 네트워크 완벽 가이드
이 글의 핵심
MFC 소켓은 CAsyncSocket과 CSocket 클래스로 구현합니다. CAsyncSocket은 비동기 이벤트 기반(OnReceive, OnAccept), CSocket은 동기식 블로킹 모드입니다. Create로 소켓 생성, Bind-Listen-Accept로 서버, Connect로 클라이언트를 만듭니다.
들어가며
MFC 소켓은 Winsock을 C++ 클래스로 감싼 것입니다. CAsyncSocket은 비동기 이벤트 기반, CSocket은 동기식 블로킹 방식입니다. 2000년대 Windows 채팅 프로그램, 파일 전송 도구 등에 많이 사용되었습니다.
// 가장 간단한 MFC 소켓 클라이언트
CSocket sock;
sock.Create();
sock.Connect(_T("127.0.0.1"), 9000);
CString msg = _T("Hello, Server!");
sock.Send(msg, msg.GetLength());
char buf[1024];
int received = sock.Receive(buf, sizeof(buf));
sock.Close();
1. CAsyncSocket
1.1 기본 사용법
// 클라이언트 소켓 클래스
class CMyClientSocket : public CAsyncSocket
{
protected:
CWnd* m_pWnd; // 알림받을 윈도우
public:
CMyClientSocket(CWnd* pWnd) : m_pWnd(pWnd) {}
virtual void OnConnect(int nErrorCode)
{
if (nErrorCode == 0) {
AfxMessageBox(_T("연결 성공!"));
} else {
CString msg;
msg.Format(_T("연결 실패: %d"), nErrorCode);
AfxMessageBox(msg);
}
}
virtual void OnReceive(int nErrorCode)
{
char buf[1024];
int received = Receive(buf, sizeof(buf) - 1);
if (received > 0) {
buf[received] = '\0';
CString msg;
msg.Format(_T("받음: %s"), CString(buf));
AfxMessageBox(msg);
}
}
virtual void OnClose(int nErrorCode)
{
AfxMessageBox(_T("연결 종료"));
Close();
}
};
// 다이얼로그에서 사용
class CMyDialog : public CDialog
{
private:
CMyClientSocket* m_pSocket;
protected:
virtual BOOL OnInitDialog()
{
CDialog::OnInitDialog();
AfxSocketInit(); // 소켓 초기화 (한 번만)
return TRUE;
}
afx_msg void OnBnClickedConnect()
{
m_pSocket = new CMyClientSocket(this);
if (!m_pSocket->Create()) {
AfxMessageBox(_T("소켓 생성 실패!"));
delete m_pSocket;
m_pSocket = NULL;
return;
}
if (!m_pSocket->Connect(_T("127.0.0.1"), 9000)) {
int err = GetLastError();
if (err != WSAEWOULDBLOCK) {
AfxMessageBox(_T("연결 실패!"));
}
}
}
afx_msg void OnBnClickedSend()
{
if (m_pSocket == NULL) {
return;
}
CString msg;
GetDlgItemText(IDC_EDIT_MESSAGE, msg);
m_pSocket->Send((LPCTSTR)msg, msg.GetLength());
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_BN_CLICKED(IDC_BTN_CONNECT, &CMyDialog::OnBnClickedConnect)
ON_BN_CLICKED(IDC_BTN_SEND, &CMyDialog::OnBnClickedSend)
END_MESSAGE_MAP()
2. CAsyncSocket 서버
2.1 Listen Socket
class CMyListenSocket : public CAsyncSocket
{
private:
CMyDialog* m_pDialog;
public:
CMyListenSocket(CMyDialog* pDialog) : m_pDialog(pDialog) {}
virtual void OnAccept(int nErrorCode)
{
if (nErrorCode == 0) {
m_pDialog->OnAcceptConnection();
}
}
};
2.2 Client Socket (서버측)
class CMyServerSocket : public CAsyncSocket
{
private:
CMyDialog* m_pDialog;
int m_nID;
public:
CMyServerSocket(CMyDialog* pDialog, int id)
: m_pDialog(pDialog), m_nID(id) {}
int GetID() const { return m_nID; }
virtual void OnReceive(int nErrorCode)
{
char buf[1024];
int received = Receive(buf, sizeof(buf) - 1);
if (received > 0) {
buf[received] = '\0';
m_pDialog->OnDataReceived(m_nID, CString(buf));
}
}
virtual void OnClose(int nErrorCode)
{
m_pDialog->OnClientDisconnected(m_nID);
Close();
}
};
2.3 서버 다이얼로그
class CMyDialog : public CDialog
{
private:
CMyListenSocket* m_pListenSocket;
CList<CMyServerSocket*> m_clients;
int m_nNextID;
protected:
virtual BOOL OnInitDialog()
{
CDialog::OnInitDialog();
AfxSocketInit();
m_pListenSocket = NULL;
m_nNextID = 0;
return TRUE;
}
afx_msg void OnBnClickedStart()
{
m_pListenSocket = new CMyListenSocket(this);
if (!m_pListenSocket->Create(9000)) {
AfxMessageBox(_T("소켓 생성 실패!"));
delete m_pListenSocket;
m_pListenSocket = NULL;
return;
}
if (!m_pListenSocket->Listen()) {
AfxMessageBox(_T("Listen 실패!"));
return;
}
SetDlgItemText(IDC_STATIC_STATUS, _T("서버 시작: 포트 9000"));
GetDlgItem(IDC_BTN_START)->EnableWindow(FALSE);
GetDlgItem(IDC_BTN_STOP)->EnableWindow(TRUE);
}
afx_msg void OnBnClickedStop()
{
if (m_pListenSocket != NULL) {
m_pListenSocket->Close();
delete m_pListenSocket;
m_pListenSocket = NULL;
}
POSITION pos = m_clients.GetHeadPosition();
while (pos) {
CMyServerSocket* pSocket = m_clients.GetNext(pos);
pSocket->Close();
delete pSocket;
}
m_clients.RemoveAll();
SetDlgItemText(IDC_STATIC_STATUS, _T("서버 중지"));
GetDlgItem(IDC_BTN_START)->EnableWindow(TRUE);
GetDlgItem(IDC_BTN_STOP)->EnableWindow(FALSE);
}
public:
void OnAcceptConnection()
{
CMyServerSocket* pClientSocket = new CMyServerSocket(this, m_nNextID++);
if (m_pListenSocket->Accept(*pClientSocket)) {
m_clients.AddTail(pClientSocket);
CString msg;
msg.Format(_T("클라이언트 연결: ID %d (총 %d개)"),
pClientSocket->GetID(), m_clients.GetCount());
CListBox* pList = (CListBox*)GetDlgItem(IDC_LIST_LOG);
pList->AddString(msg);
} else {
delete pClientSocket;
}
}
void OnDataReceived(int clientID, const CString& data)
{
CString msg;
msg.Format(_T("[클라이언트 %d] %s"), clientID, data);
CListBox* pList = (CListBox*)GetDlgItem(IDC_LIST_LOG);
pList->AddString(msg);
// 에코백
BroadcastMessage(msg);
}
void OnClientDisconnected(int clientID)
{
POSITION pos = m_clients.GetHeadPosition();
while (pos) {
POSITION prevPos = pos;
CMyServerSocket* pSocket = m_clients.GetNext(pos);
if (pSocket->GetID() == clientID) {
m_clients.RemoveAt(prevPos);
delete pSocket;
break;
}
}
CString msg;
msg.Format(_T("클라이언트 연결 종료: ID %d (남은: %d개)"),
clientID, m_clients.GetCount());
CListBox* pList = (CListBox*)GetDlgItem(IDC_LIST_LOG);
pList->AddString(msg);
}
void BroadcastMessage(const CString& msg)
{
POSITION pos = m_clients.GetHeadPosition();
while (pos) {
CMyServerSocket* pSocket = m_clients.GetNext(pos);
pSocket->Send((LPCTSTR)msg, msg.GetLength() * sizeof(TCHAR));
}
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_BN_CLICKED(IDC_BTN_START, &CMyDialog::OnBnClickedStart)
ON_BN_CLICKED(IDC_BTN_STOP, &CMyDialog::OnBnClickedStop)
END_MESSAGE_MAP()
3. CSocket (동기식)
3.1 간단한 클라이언트
void CMyDialog::OnBnClickedConnect()
{
CSocket sock;
if (!sock.Create()) {
AfxMessageBox(_T("소켓 생성 실패!"));
return;
}
if (!sock.Connect(_T("127.0.0.1"), 9000)) {
AfxMessageBox(_T("연결 실패!"));
return;
}
// 메시지 전송
CString msg = _T("Hello, Server!");
sock.Send((LPCTSTR)msg, msg.GetLength() * sizeof(TCHAR));
// 응답 수신 (블로킹)
TCHAR buf[1024];
int received = sock.Receive(buf, sizeof(buf));
if (received > 0) {
buf[received / sizeof(TCHAR)] = _T('\0');
AfxMessageBox(CString(_T("받음: ")) + buf);
}
sock.Close();
}
4. 실전 예제: 채팅 프로그램
4.1 프로토콜 정의
// Protocol.h
#pragma pack(push, 1)
enum MessageType {
MSG_LOGIN = 1,
MSG_LOGOUT = 2,
MSG_CHAT = 3,
MSG_USER_LIST = 4
};
struct MessageHeader {
BYTE type;
WORD length;
};
struct LoginMessage {
MessageHeader header;
TCHAR username[32];
};
struct ChatMessage {
MessageHeader header;
TCHAR username[32];
TCHAR message[256];
};
#pragma pack(pop)
4.2 채팅 클라이언트
class CChatClientSocket : public CAsyncSocket
{
private:
CChatClientDlg* m_pDialog;
CString m_strUsername;
public:
CChatClientSocket(CChatClientDlg* pDialog)
: m_pDialog(pDialog) {}
void SetUsername(const CString& username) { m_strUsername = username; }
virtual void OnConnect(int nErrorCode)
{
if (nErrorCode == 0) {
// 로그인 메시지 전송
LoginMessage msg;
msg.header.type = MSG_LOGIN;
msg.header.length = sizeof(LoginMessage);
_tcscpy_s(msg.username, m_strUsername);
Send(&msg, sizeof(msg));
m_pDialog->OnConnected();
} else {
AfxMessageBox(_T("연결 실패!"));
}
}
virtual void OnReceive(int nErrorCode)
{
MessageHeader header;
int received = Receive(&header, sizeof(header), MSG_PEEK);
if (received < sizeof(header)) {
return;
}
if (header.type == MSG_CHAT) {
ChatMessage msg;
Receive(&msg, sizeof(msg));
m_pDialog->OnChatMessage(msg.username, msg.message);
}
}
void SendChatMessage(const CString& message)
{
ChatMessage msg;
msg.header.type = MSG_CHAT;
msg.header.length = sizeof(ChatMessage);
_tcscpy_s(msg.username, m_strUsername);
_tcscpy_s(msg.message, message);
Send(&msg, sizeof(msg));
}
};
class CChatClientDlg : public CDialog
{
private:
CChatClientSocket* m_pSocket;
CString m_strUsername;
CListBox m_listChat;
protected:
afx_msg void OnBnClickedConnect()
{
CString server, username;
GetDlgItemText(IDC_EDIT_SERVER, server);
GetDlgItemText(IDC_EDIT_USERNAME, username);
if (server.IsEmpty() || username.IsEmpty()) {
AfxMessageBox(_T("서버와 이름을 입력하세요!"));
return;
}
m_strUsername = username;
m_pSocket = new CChatClientSocket(this);
m_pSocket->SetUsername(username);
if (!m_pSocket->Create()) {
AfxMessageBox(_T("소켓 생성 실패!"));
return;
}
if (!m_pSocket->Connect(server, 9000)) {
int err = GetLastError();
if (err != WSAEWOULDBLOCK) {
AfxMessageBox(_T("연결 실패!"));
}
}
}
afx_msg void OnBnClickedSend()
{
if (m_pSocket == NULL) {
return;
}
CString msg;
GetDlgItemText(IDC_EDIT_MESSAGE, msg);
if (!msg.IsEmpty()) {
m_pSocket->SendChatMessage(msg);
SetDlgItemText(IDC_EDIT_MESSAGE, _T(""));
}
}
public:
void OnConnected()
{
m_listChat.AddString(_T("*** 서버에 연결되었습니다 ***"));
GetDlgItem(IDC_BTN_CONNECT)->EnableWindow(FALSE);
GetDlgItem(IDC_BTN_SEND)->EnableWindow(TRUE);
}
void OnChatMessage(const CString& username, const CString& message)
{
CString text;
text.Format(_T("[%s] %s"), username, message);
m_listChat.AddString(text);
// 스크롤 맨 아래로
m_listChat.SetTopIndex(m_listChat.GetCount() - 1);
}
DECLARE_MESSAGE_MAP()
};
BEGIN_MESSAGE_MAP(CChatClientDlg, CDialog)
ON_BN_CLICKED(IDC_BTN_CONNECT, &CChatClientDlg::OnBnClickedConnect)
ON_BN_CLICKED(IDC_BTN_SEND, &CChatClientDlg::OnBnClickedSend)
END_MESSAGE_MAP()
4.3 채팅 서버
class CChatServerSocket : public CAsyncSocket
{
private:
CChatServerDlg* m_pDialog;
int m_nID;
CString m_strUsername;
public:
CChatServerSocket(CChatServerDlg* pDialog, int id)
: m_pDialog(pDialog), m_nID(id) {}
int GetID() const { return m_nID; }
CString GetUsername() const { return m_strUsername; }
virtual void OnReceive(int nErrorCode)
{
MessageHeader header;
int received = Receive(&header, sizeof(header), MSG_PEEK);
if (received < sizeof(header)) {
return;
}
if (header.type == MSG_LOGIN) {
LoginMessage msg;
Receive(&msg, sizeof(msg));
m_strUsername = msg.username;
m_pDialog->OnUserLogin(m_nID, m_strUsername);
} else if (header.type == MSG_CHAT) {
ChatMessage msg;
Receive(&msg, sizeof(msg));
m_pDialog->OnChatMessage(m_nID, msg.username, msg.message);
}
}
virtual void OnClose(int nErrorCode)
{
m_pDialog->OnUserLogout(m_nID);
Close();
}
void SendChatMessage(const CString& username, const CString& message)
{
ChatMessage msg;
msg.header.type = MSG_CHAT;
msg.header.length = sizeof(ChatMessage);
_tcscpy_s(msg.username, username);
_tcscpy_s(msg.message, message);
Send(&msg, sizeof(msg));
}
};
class CChatServerDlg : public CDialog
{
private:
CAsyncSocket* m_pListenSocket;
CList<CChatServerSocket*> m_clients;
int m_nNextID;
public:
void OnUserLogin(int clientID, const CString& username)
{
CString msg;
msg.Format(_T("*** %s님이 입장했습니다 ***"), username);
CListBox* pList = (CListBox*)GetDlgItem(IDC_LIST_LOG);
pList->AddString(msg);
// 모든 클라이언트에게 알림
BroadcastMessage(_T("SERVER"), msg);
}
void OnUserLogout(int clientID)
{
CString username;
POSITION pos = m_clients.GetHeadPosition();
while (pos) {
POSITION prevPos = pos;
CChatServerSocket* pSocket = m_clients.GetNext(pos);
if (pSocket->GetID() == clientID) {
username = pSocket->GetUsername();
m_clients.RemoveAt(prevPos);
delete pSocket;
break;
}
}
CString msg;
msg.Format(_T("*** %s님이 퇴장했습니다 ***"), username);
CListBox* pList = (CListBox*)GetDlgItem(IDC_LIST_LOG);
pList->AddString(msg);
BroadcastMessage(_T("SERVER"), msg);
}
void OnChatMessage(int clientID, const CString& username, const CString& message)
{
CString log;
log.Format(_T("[%s] %s"), username, message);
CListBox* pList = (CListBox*)GetDlgItem(IDC_LIST_LOG);
pList->AddString(log);
// 브로드캐스트
BroadcastMessage(username, message);
}
void BroadcastMessage(const CString& username, const CString& message)
{
POSITION pos = m_clients.GetHeadPosition();
while (pos) {
CChatServerSocket* pSocket = m_clients.GetNext(pos);
pSocket->SendChatMessage(username, message);
}
}
};
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Windows API Winsock | 소켓 네트워크 프로그래밍 완벽 가이드
- MFC 멀티스레딩 | CWinThread·동기화 완벽 가이드
- MFC 기초 | Microsoft Foundation Class 시작 가이드
이 글이 도움이 되셨나요? MFC 소켓 네트워크 프로그래밍을 마스터하는 데 도움이 되었기를 바랍니다!
다음 글에서는 MFC와 Win32 API 비교를 다루겠습니다. ⚖️