본문으로 건너뛰기
Previous
Next
MFC 소켓 | CAsyncSocket·CSocket 네트워크 완벽 가이드

MFC 소켓 | CAsyncSocket·CSocket 네트워크 완벽 가이드

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 비교를 다루겠습니다. ⚖️