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 데스크톱 애플리케이션 개발의 모든 것을 마스터했습니다!