이전글에 작성해놓은 범용 소켓 클래스를 이용하여 MFC 채팅 프로그램을 만들어보는 예제이다. 이번 예제에서 Visual Studio 2019를 처음으로 이용해 보는 것이라 시행착오 과정도 같이 메모해 보려고 한다.
* 개발 환경
Visual Studio 2019
다이얼로그 기반 MFC 프로젝트 생성
- ListBoxCtrl (m_ctrlListBox) : Sort=False
- EditCtrl (m_strEdit-CString Value) : Multiline=True(엔터키 입력을 받기 위함)
- ButtonCtrl
멀티바이트 문자 집합 사용

1. 소켓 관련 헤더 include
소켓 관련 기능을 사용하기 위해 Stdafx.h(pch.h)에 다음을 추가한다.
#include <afxsock.h>
* Visual Studio 2019에서는 Stdafx.h 파일이 pch.h로 변경되었다.
Visual Studio에서 새 프로젝트를 만들 때 pch. h 라는 미리 컴파일된 헤더 파일 이 프로젝트에 추가됩니다. (Visual Studio 2017 이하 버전에서는 이 파일을 stdafx.h 라고 합니다.)
https://docs.microsoft.com/ko-kr/cpp/build/creating-precompiled-header-files?view=msvc-160
소켓 초기화를 하기위해 InitInstance 함수에서 AfxSocketInit 함수를 호출하면 소켓을 사용할 준비는 끝난다.
BOOL CChatApp::InitInstance()
{
...
if (!AfxSocketInit())
{
AfxMessageBox("소켓 초기화 실패");
return FALSE;
}
...
}
2. 소켓 클래스 포함 시키기
범용으로 만들었던 소켓 클래스를 현재 프로젝트에 추가해준다.
Data.h
Data.cpp
DataSocket.h
DataSocket.cpp
ServerSocket.h
ServerSocket.cpp
pch.h에 헤더 파일을 include 한다.
#include "ServerSocket.h"
#include "DataSocket.h"
소켓 클래스를 멤버 변수로 선언한다.
class CChatDlg : public CDialogEx
{
...
CServerSocket m_ServerSocket;
CDataSocket m_DataSocket;
};
3. 서버 기능 구현
OnInitDialog 함수에서 Init 함수를 호출하여 서버 소켓을 초기화한다.
BOOL CChatDlg::OnInitDialog()
{
...
m_ServerSocket.Init(this, 2000); // 서버 소켓 초기화
return TRUE;
}
클라이언트의 접속 요청을 받아들이는 작업을 하는 OnAccept 함수를 추가한다.
클라이언트가 접속 요청을 하면 CServerSocket의 OnAccept 함수가 호출되고, 여기서 메인 프레임에 UM_ACCEPT 메시지를 보낸다.
먼저 헤더에 메시지 처리기를 선언한다.
class CChatDlg : public CDialogEx
{
...
afx_msg LRESULT OnAccept(WPARAM wParam, LPARAM lParam);
};
그리고 메시지 맵에 등록하고 함수를 정의한다.
BEGIN_MESSAGE_MAP(CChatDlg, CDialogEx)
...
ON_MESSAGE(UM_ACCEPT, OnAccept)
END_MESSAGE_MAP()
LRESULT CChatDlg::OnAccept(WPARAM wParam, LPARAM lParam)
{
// 연결을 받아들이고, 클라이언트와 m_DataSocket을 연결시켜 줌
if (!m_ServerSocket.Accept(m_DataSocket))
{
AfxMessageBox("Accept 실패");
return 0;
}
// 데이터 소켓 초기화
m_DataSocket.Init(this);
m_ctrlListBox.AddString("*** 연결되었습니다. ***");
return 0L;
}
4. 클라이언트 기능 구현
클라이언트가 서버에 접속 요청을 하는 기능을 구현한다.
IP 주소와 포트 번호를 인자로 하여 Connect 함수를 호출한다.
void CChatDlg::OnBnClickedConnectButton()
{
// IP 주소를 입력 받는 대화상자 출력
CConnectDlg dlg;
if (dlg.DoModal() != IDOK) return;
// 클라이언트 소켓 생성
if (!m_DataSocket.Create())
{
AfxMessageBox("클라이언트 소켓 생성 실패");
return;
}
// 접속 요청
if (!m_DataSocket.Connect(dlg.m_strAddress, 2000))
{
AfxMessageBox("서버 접속 실패");
return;
}
// 클라이언트 소켓 초기화
m_DataSocket.Init(this);
m_ctrlListBox.AddString("*** 연결되었습니다. ***");
}
5. 데이터 포맷 설정
주고 받을 데이터를 CData에 정의한다. 채팅 프로그램에서 문자열만 주고 받을 것이므로, CString을 CData 클래스의 멤버 변수로 설정한다.
class CData : public CObject
{
...
CString m_strData; // 텍스트 데이터만 전송하므로
};
6. 데이터 송신
에디트 컨트롤을 추가하여 데이터를 입력한 뒤 엔터를 누르면 데이터가 전송되도록 만든다.
class CChatDlg : public CDialogEx
{
...
afx_msg void OnEnterKey();
};
그리고 메시지 맵에 등록하고 함수를 정의한다.
BEGIN_MESSAGE_MAP(CChatDlg, CDialogEx)
...
ON_COMMAND(ID_ENTERKEY, OnEnterKey)
END_MESSAGE_MAP()
CData 클래스 형 변수 data에 전송하고자 하는 데이터를 설정한 후 m_DataSocket에 << 연산자를 이용하여 밀어 넣으면 데이터가 전송된다. << 연산자는 Send 함수를 호출한다.
void CChatDlg::OnEnterKey()
{
UpdateData();
// 데이터 전송
CData data;
data.m_strData = m_strEdit;
m_DataSocket << data;
// 전송된 문자열을 상단 윈도우에도 표시
m_ctrlListBox.AddString(m_strEdit);
m_ctrlListBox.SetCurSel(m_ctrlListBox.GetCount() - 1);
m_ctrlListBox.SetCurSel(-1);
// Edit 컨트롤 초기화
m_strEdit.Empty();
UpdateData(FALSE);
}
추가로, 엔터키를 누르면 IDOK 이벤트가 발생해서 대화상자가 종료되는 문제가 있으므로 이를 막기 위해 OnOK 함수를 재정의한다.
void CChatDlg::OnOK()
{
//CDialogEx::OnOK();
}
7. 데이터 수신
새로운 데이터가 수신되면 CDataSocket의 OnReceive 함수가 호출되고, 여기서 메인 프레임에 UM_DATARECEIVE 메시지를 보내준다.
먼저 헤더에 메시지 처리기를 선언한다.
class CChatDlg : public CDialogEx
{
...
afx_msg LRESULT OnReceive(WPARAM wParam, LPARAM lParam);
};
그리고 메시지 맵에 등록하고 함수를 정의한다.
BEGIN_MESSAGE_MAP(CChatDlg, CDialogEx)
...
ON_MESSAGE(UM_DATARECEIVE, OnReceive)
END_MESSAGE_MAP()
m_DataSocket으로부터 >> 연산자를 이용하여 CData 형 변수에 데이터를 끌어 옴으로써 데이터를 수신할 수 있다. >> 연산자는 Receive 함수를 호출한다.
LRESULT CChatDlg::OnReceive(WPARAM wParam, LPARAM lParam)
{
// 통신 버퍼에 있는 데이터를 전부 수신할 때까지...
do {
// 데이터 수신
CData data;
m_DataSocket >> data;
// 수신된 데이터 표시
m_ctrlListBox.AddString(data.m_strData);
m_ctrlListBox.SetCurSel(m_ctrlListBox.GetCount() - 1);
m_ctrlListBox.SetCurSel(-1);
} while (!m_DataSocket.m_pArchiveIn->IsBufferEmpty());
return 0L;
}
데이터가 짧은 시간 안에 연속적으로 수신되면 소켓을 이를 차례로 받아놓고, OnReceive 함수는 한번만 호출한다. 따라서 OnReceive 함수가 한번만 호출되었어도 새로 수신된 데이터는 여러개 일 수 있다. 따라서 do-while을 이용하여 수신 버퍼가 빌 때까지 데이터를 가져와야 한다.
kimgopa/mfc_project
visual c++ mfc project. Contribute to kimgopa/mfc_project development by creating an account on GitHub.
github.com
'C, C++, MFC' 카테고리의 다른 글
| 동적 링크 라이브러리(DLL) - DLL 생성과 암시적/명시적 연결 (0) | 2021.02.16 |
|---|---|
| 모달(Modal)과 모달리스(Modeless) (0) | 2021.02.05 |
| TCP/IP 프로그래밍 - 범용 소켓 클래스 (0) | 2020.12.31 |
| TCP/IP 프로그래밍 - 소켓 통신 (0) | 2020.12.19 |
| ODBC를 이용한 MFC 데이터베이스 프로그래밍 (1) | 2020.12.02 |