본문으로 건너뛰기
Previous
Next
MFC 채팅 프로그램 만들기 | Winsock 실전 프로젝트

MFC 채팅 프로그램 만들기 | Winsock 실전 프로젝트

MFC 채팅 프로그램 만들기 | Winsock 실전 프로젝트

이 글의 핵심

MFC와 CAsyncSocket으로 멀티 클라이언트 채팅 프로그램을 만듭니다. 서버는 Accept로 다중 연결 관리, 프로토콜로 메시지 타입 구분, 귓속말·파일 전송·유저 목록 기능을 구현합니다.

들어가며

이 글에서는 MFC와 Winsock으로 완전한 채팅 프로그램을 만듭니다. 멀티 클라이언트 지원, 파일 전송, 귓속말 등 실전 기능을 구현하며, 네트워크 프로그래밍 기술을 종합합니다.

구현 기능

  • 멀티 클라이언트 서버 (동시 접속 지원)
  • 로그인/로그아웃
  • 전체 채팅
  • 귓속말 (1:1 메시지)
  • 유저 목록 표시
  • 파일 전송
  • 채팅 로그 저장
  • 서버 통계 (접속자 수, 트래픽)

1. 프로젝트 구조

ChatApp/
├─ Protocol.h             // 프로토콜 정의
├─ Server/
│  ├─ ChatServer.h/cpp    // 서버 메인
│  ├─ ServerSocket.h/cpp  // Listen 소켓
│  └─ ClientSocket.h/cpp  // 클라이언트 소켓 (서버측)
└─ Client/
   ├─ ChatClient.h/cpp    // 클라이언트 메인
   └─ ClientSocket.h/cpp  // 서버 연결 소켓

2. 프로토콜 정의 (Protocol.h)

#pragma once
#include <windows.h>

#pragma pack(push, 1)

enum MessageType {
    MSG_LOGIN           = 1,
    MSG_LOGOUT          = 2,
    MSG_CHAT            = 3,
    MSG_WHISPER         = 4,
    MSG_FILE_REQUEST    = 5,
    MSG_FILE_ACCEPT     = 6,
    MSG_FILE_DATA       = 7,
    MSG_FILE_COMPLETE   = 8,
    MSG_USER_LIST       = 9,
    MSG_SERVER_MESSAGE  = 10
};

struct MessageHeader {
    BYTE type;
    WORD length;
};

struct LoginMessage {
    MessageHeader header;
    TCHAR username[32];
};

struct LogoutMessage {
    MessageHeader header;
    TCHAR username[32];
};

struct ChatMessage {
    MessageHeader header;
    TCHAR username[32];
    TCHAR message[512];
};

struct WhisperMessage {
    MessageHeader header;
    TCHAR fromUser[32];
    TCHAR toUser[32];
    TCHAR message[512];
};

struct FileRequestMessage {
    MessageHeader header;
    TCHAR fromUser[32];
    TCHAR toUser[32];
    TCHAR filename[256];
    DWORD fileSize;
};

struct FileDataMessage {
    MessageHeader header;
    DWORD sessionId;
    DWORD chunkIndex;
    WORD dataSize;
    BYTE data[4096];
};

struct FileCompleteMessage {
    MessageHeader header;
    DWORD sessionId;
};

struct UserListMessage {
    MessageHeader header;
    WORD userCount;
    TCHAR users[100][32];
};

struct ServerMessage {
    MessageHeader header;
    TCHAR message[512];
};

#pragma pack(pop)

3. 서버 - ClientSocket 클래스

// Server/ClientSocket.h
class CChatServerDlg;

class CClientSocket : public CAsyncSocket
{
private:
    CChatServerDlg* m_pDialog;
    int m_nID;
    CString m_strUsername;
    
public:
    CClientSocket(CChatServerDlg* pDialog, int id);
    
    int GetID() const { return m_nID; }
    CString GetUsername() const { return m_strUsername; }
    void SetUsername(const CString& username) { m_strUsername = username; }
    
    virtual void OnReceive(int nErrorCode);
    virtual void OnClose(int nErrorCode);
    
    void SendMessage(const void* pData, int size);
};

// Server/ClientSocket.cpp
#include "ClientSocket.h"
#include "ChatServerDlg.h"

CClientSocket::CClientSocket(CChatServerDlg* pDialog, int id)
    : m_pDialog(pDialog), m_nID(id)
{
}

void CClientSocket::OnReceive(int nErrorCode)
{
    MessageHeader header;
    int received = Receive(&header, sizeof(header), MSG_PEEK);
    
    if (received < sizeof(header)) {
        return;
    }
    
    switch (header.type) {
        case MSG_LOGIN: {
            LoginMessage msg;
            Receive(&msg, sizeof(msg));
            
            m_strUsername = msg.username;
            m_pDialog->OnUserLogin(m_nID, m_strUsername);
            break;
        }
        
        case MSG_CHAT: {
            ChatMessage msg;
            Receive(&msg, sizeof(msg));
            
            m_pDialog->OnChatMessage(m_nID, msg.username, msg.message);
            break;
        }
        
        case MSG_WHISPER: {
            WhisperMessage msg;
            Receive(&msg, sizeof(msg));
            
            m_pDialog->OnWhisperMessage(msg.fromUser, msg.toUser, msg.message);
            break;
        }
        
        case MSG_FILE_REQUEST: {
            FileRequestMessage msg;
            Receive(&msg, sizeof(msg));
            
            m_pDialog->OnFileRequest(msg.fromUser, msg.toUser, msg.filename, msg.fileSize);
            break;
        }
        
        default:
            // 알 수 없는 메시지 무시
            char buffer[1024];
            Receive(buffer, sizeof(buffer));
            break;
    }
    
    CAsyncSocket::OnReceive(nErrorCode);
}

void CClientSocket::OnClose(int nErrorCode)
{
    m_pDialog->OnUserLogout(m_nID);
    Close();
    
    CAsyncSocket::OnClose(nErrorCode);
}

void CClientSocket::SendMessage(const void* pData, int size)
{
    Send(pData, size);
}

4. 서버 - 메인 다이얼로그

// Server/ChatServerDlg.h
class CChatServerDlg : public CDialog
{
private:
    CAsyncSocket* m_pListenSocket;
    CList<CClientSocket*> m_clients;
    int m_nNextID;
    
    CListBox m_listLog;
    CListCtrl m_listUsers;
    
public:
    CChatServerDlg(CWnd* pParent = NULL);
    
    void OnAcceptConnection();
    void OnUserLogin(int clientID, const CString& username);
    void OnUserLogout(int clientID);
    void OnChatMessage(int clientID, const CString& username, const CString& message);
    void OnWhisperMessage(const CString& fromUser, const CString& toUser, const CString& message);
    void OnFileRequest(const CString& fromUser, const CString& toUser, 
        const CString& filename, DWORD fileSize);
    
    void BroadcastMessage(const void* pData, int size, int exceptID = -1);
    void SendToUser(const CString& username, const void* pData, int size);
    void SendUserList();
    
protected:
    virtual BOOL OnInitDialog();
    
    DECLARE_MESSAGE_MAP()
    
    afx_msg void OnBnClickedStart();
    afx_msg void OnBnClickedStop();
};

// Server/ChatServerDlg.cpp
#include "ChatServerDlg.h"
#include "ServerSocket.h"

CChatServerDlg::CChatServerDlg(CWnd* pParent)
    : CDialog(IDD_SERVER_DIALOG, pParent)
    , m_pListenSocket(NULL)
    , m_nNextID(0)
{
}

BOOL CChatServerDlg::OnInitDialog()
{
    CDialog::OnInitDialog();
    
    AfxSocketInit();
    
    // 유저 목록 컬럼
    m_listUsers.InsertColumn(0, _T("ID"), LVCFMT_LEFT, 50);
    m_listUsers.InsertColumn(1, _T("Username"), LVCFMT_LEFT, 150);
    m_listUsers.InsertColumn(2, _T("IP"), LVCFMT_LEFT, 120);
    m_listUsers.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
    
    return TRUE;
}

void CChatServerDlg::OnBnClickedStart()
{
    m_pListenSocket = new CServerSocket(this);
    
    if (!m_pListenSocket->Create(9000)) {
        AfxMessageBox(_T("포트 9000에서 소켓 생성 실패!"));
        delete m_pListenSocket;
        m_pListenSocket = NULL;
        return;
    }
    
    if (!m_pListenSocket->Listen()) {
        AfxMessageBox(_T("Listen 실패!"));
        return;
    }
    
    m_listLog.AddString(_T("[서버] 시작됨 - 포트 9000"));
    
    GetDlgItem(IDC_BTN_START)->EnableWindow(FALSE);
    GetDlgItem(IDC_BTN_STOP)->EnableWindow(TRUE);
}

void CChatServerDlg::OnBnClickedStop()
{
    if (m_pListenSocket != NULL) {
        m_pListenSocket->Close();
        delete m_pListenSocket;
        m_pListenSocket = NULL;
    }
    
    POSITION pos = m_clients.GetHeadPosition();
    while (pos) {
        CClientSocket* pSocket = m_clients.GetNext(pos);
        pSocket->Close();
        delete pSocket;
    }
    m_clients.RemoveAll();
    
    m_listUsers.DeleteAllItems();
    m_listLog.AddString(_T("[서버] 중지됨"));
    
    GetDlgItem(IDC_BTN_START)->EnableWindow(TRUE);
    GetDlgItem(IDC_BTN_STOP)->EnableWindow(FALSE);
}

void CChatServerDlg::OnAcceptConnection()
{
    CClientSocket* pClientSocket = new CClientSocket(this, m_nNextID++);
    
    if (m_pListenSocket->Accept(*pClientSocket)) {
        m_clients.AddTail(pClientSocket);
        
        CString log;
        log.Format(_T("[서버] 새 연결 - ID %d (총 %d개)"), 
            pClientSocket->GetID(), m_clients.GetCount());
        m_listLog.AddString(log);
    } else {
        delete pClientSocket;
    }
}

void CChatServerDlg::OnUserLogin(int clientID, const CString& username)
{
    // 유저 목록에 추가
    int index = m_listUsers.InsertItem(m_listUsers.GetItemCount(), _T(""));
    
    CString strID;
    strID.Format(_T("%d"), clientID);
    m_listUsers.SetItemText(index, 0, strID);
    m_listUsers.SetItemText(index, 1, username);
    m_listUsers.SetItemText(index, 2, _T("127.0.0.1"));
    
    CString log;
    log.Format(_T("[입장] %s"), username);
    m_listLog.AddString(log);
    
    // 입장 메시지 브로드캐스트
    ServerMessage msg;
    msg.header.type = MSG_SERVER_MESSAGE;
    msg.header.length = sizeof(msg);
    _stprintf_s(msg.message, _T("*** %s님이 입장했습니다 ***"), username);
    
    BroadcastMessage(&msg, sizeof(msg));
    
    // 유저 목록 전송
    SendUserList();
}

void CChatServerDlg::OnUserLogout(int clientID)
{
    CString username;
    
    // 유저 목록에서 제거
    for (int i = 0; i < m_listUsers.GetItemCount(); i++) {
        CString strID = m_listUsers.GetItemText(i, 0);
        if (_ttoi(strID) == clientID) {
            username = m_listUsers.GetItemText(i, 1);
            m_listUsers.DeleteItem(i);
            break;
        }
    }
    
    // 소켓 제거
    POSITION pos = m_clients.GetHeadPosition();
    while (pos) {
        POSITION prevPos = pos;
        CClientSocket* pSocket = m_clients.GetNext(pos);
        
        if (pSocket->GetID() == clientID) {
            m_clients.RemoveAt(prevPos);
            delete pSocket;
            break;
        }
    }
    
    if (!username.IsEmpty()) {
        CString log;
        log.Format(_T("[퇴장] %s"), username);
        m_listLog.AddString(log);
        
        // 퇴장 메시지 브로드캐스트
        ServerMessage msg;
        msg.header.type = MSG_SERVER_MESSAGE;
        msg.header.length = sizeof(msg);
        _stprintf_s(msg.message, _T("*** %s님이 퇴장했습니다 ***"), username);
        
        BroadcastMessage(&msg, sizeof(msg));
        
        // 유저 목록 갱신
        SendUserList();
    }
}

void CChatServerDlg::OnChatMessage(int clientID, const CString& username, const CString& message)
{
    CString log;
    log.Format(_T("[%s] %s"), username, message);
    m_listLog.AddString(log);
    
    // 브로드캐스트
    ChatMessage msg;
    msg.header.type = MSG_CHAT;
    msg.header.length = sizeof(msg);
    _tcscpy_s(msg.username, username);
    _tcscpy_s(msg.message, message);
    
    BroadcastMessage(&msg, sizeof(msg));
}

void CChatServerDlg::OnWhisperMessage(const CString& fromUser, const CString& toUser, const CString& message)
{
    CString log;
    log.Format(_T("[귓속말] %s%s: %s"), fromUser, toUser, message);
    m_listLog.AddString(log);
    
    WhisperMessage msg;
    msg.header.type = MSG_WHISPER;
    msg.header.length = sizeof(msg);
    _tcscpy_s(msg.fromUser, fromUser);
    _tcscpy_s(msg.toUser, toUser);
    _tcscpy_s(msg.message, message);
    
    // 대상 유저에게만 전송
    SendToUser(toUser, &msg, sizeof(msg));
    
    // 발신자에게도 전송 (확인용)
    SendToUser(fromUser, &msg, sizeof(msg));
}

void CChatServerDlg::BroadcastMessage(const void* pData, int size, int exceptID)
{
    POSITION pos = m_clients.GetHeadPosition();
    while (pos) {
        CClientSocket* pSocket = m_clients.GetNext(pos);
        
        if (pSocket->GetID() != exceptID) {
            pSocket->SendMessage(pData, size);
        }
    }
}

void CChatServerDlg::SendToUser(const CString& username, const void* pData, int size)
{
    POSITION pos = m_clients.GetHeadPosition();
    while (pos) {
        CClientSocket* pSocket = m_clients.GetNext(pos);
        
        if (pSocket->GetUsername() == username) {
            pSocket->SendMessage(pData, size);
            break;
        }
    }
}

void CChatServerDlg::SendUserList()
{
    UserListMessage msg;
    msg.header.type = MSG_USER_LIST;
    msg.header.length = sizeof(msg);
    msg.userCount = (WORD)m_listUsers.GetItemCount();
    
    for (int i = 0; i < msg.userCount && i < 100; i++) {
        CString username = m_listUsers.GetItemText(i, 1);
        _tcscpy_s(msg.users[i], username);
    }
    
    BroadcastMessage(&msg, sizeof(msg));
}

BEGIN_MESSAGE_MAP(CChatServerDlg, CDialog)
    ON_BN_CLICKED(IDC_BTN_START, &CChatServerDlg::OnBnClickedStart)
    ON_BN_CLICKED(IDC_BTN_STOP, &CChatServerDlg::OnBnClickedStop)
END_MESSAGE_MAP()

5. 클라이언트 - 간략 구조

// Client/ChatClientDlg.h
class CChatClientDlg : public CDialog
{
private:
    CClientSocket* m_pSocket;
    CString m_strUsername;
    
    CListBox m_listChat;
    CListBox m_listUsers;
    
public:
    void OnConnected();
    void OnChatMessage(const CString& username, const CString& message);
    void OnWhisperMessage(const CString& fromUser, const CString& toUser, const CString& message);
    void OnUserListUpdate(const UserListMessage& msg);
    void OnServerMessage(const CString& message);
    
protected:
    DECLARE_MESSAGE_MAP()
    
    afx_msg void OnBnClickedConnect();
    afx_msg void OnBnClickedSend();
    afx_msg void OnBnClickedWhisper();
};

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • MFC 소켓 | CAsyncSocket·CSocket 네트워크 완벽 가이드
  • MFC 멀티스레딩 | CWinThread·동기화 완벽 가이드
  • Windows API Winsock | 소켓 네트워크 프로그래밍 완벽 가이드

축하합니다! 🎉 Windows API와 MFC 시리즈를 완료했습니다.

이제 순수 Win32 API부터 MFC 고급 기능까지, 레거시 Windows 데스크톱 애플리케이션 개발의 모든 것을 마스터했습니다!