C++/MFC

[MFC] 채팅 프로그램 - 다중접속(멀티) (3/3)

balabala 2021. 8. 26. 23:47
728x90
SMALL
- 설명 - 
① 1, 2번에서 설명한 Server와 Client를 수정
② 설정에 따라 서버만 모든 클라이언트와 통신 가능하거나, 클라이언트도 같은 클라이언트끼리 통신 가능

 

1. Client : 다이얼로그 디자인 및 ID
IDC_EDIT_USERID : 클라이언트가 몇 번째 사용자인지 알려줌(숫자는 0번부터 시작함)

그림1. 클라이언트 프로그램 다이얼로그
그림2. 멤버변수 설정

 

2. Client(SocCom.h) : 사용자 정의 메시지 및 사용자 지정 변수 추가
SOC_CLIENT_CONNECT : 클라이언트(사용자)가 접속 
SOC_CLIENT_DISCONNECT : 클라이언트 종료 시 서버로 보낼 메세지
int m_index : 채팅 클라이언트 번호

그림3. 사용자 정의 메시지 및 변수 추가

  • SocCom.h 소스 보기
더보기
#pragma once

// CSocCom 명령 대상
// 통신용 소켓
#include <afxsock.h>    // 소켓 클래스 사용 위해 include

#define UM_RECEIVE WM_USER+2

#define SOC_CLIENT_CONNECT      "접속성공"            // 클라이언트(사용자)가 접속 
#define SOC_CLIENT_DISCONNECT "클라이언트 종료"        // 클라이언트 종료 시 보낼 메세지

class CSocCom : public CSocket
{
public:
    CSocCom();
    virtual ~CSocCom();

    HWND m_hWnd;                                // 메인 윈도우 핸들
    void CSocCom::Init(HWND hWnd);                // 소켓 클래스와 메인 윈도우를 연결시킴    
    virtual void OnReceive(int nErrorCode);        // 데이터가 도착했다는 것을 알려줌

    int m_index;        // 채팅 클라이언트 번호
};
 
3. Client(ChatClientDlg.cpp) : OnSysCommand 및 OnReceive 메시지 함수 수정
OnSysCommand() : 시스템 명령 처리 메시지 함수. 여기서는 프로그램 종료 시 서버에 메시지를 보냄
OnReceive() : 서버로부터 SOC_CLIENT_CONNECT 메시지를 받으면 사용자 번호를 부여 받음

그림4. OnSysCommand() 수정

  • OnSysCommand() 소스 보기
더보기
void CChatClientDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
    if ((nID & 0xFFF0) == IDM_ABOUTBOX)
    {
        CAboutDlg dlgAbout;
        dlgAbout.DoModal();
    }
    // 위 X키로 종료했을 경우
    else if (nID == SC_CLOSE) {    // 클라이언트 종료시 서버로 메세지 보냄.
        m_socCom.Send(SOC_CLIENT_DISCONNECT, 256);
        this->EndDialog(IDCANCEL);
    }
    else
    {
        CDialogEx::OnSysCommand(nID, lParam);
    }
}
 

그림5. OnReceive() 수정

  • OnReceive() 소스 보기
더보기
// 데이터를 보내는 것은 소켓 클래스의 멤버 함수인 Send를 이용
// 데이터를 받을 때는 통신 소켓 클래스에 오버라이딩한 OnReceive 메시지 함수를 사용
LPARAM CChatClientDlg::OnReceive(UINT wParam, LPARAM lParam) {
    
    // 접속된 곳에서 데이터가 도착했을 때
    UpdateData(TRUE);
    char pTmp[256];
    CString strTmp;
    memset(pTmp, '\0', 256);

    // 데이터를 pTmp에 받는다.
    m_socCom.Receive(pTmp, 256);
    strTmp.Format("%s", pTmp);
    
    // 서버로 부터 연결완료 메세지를 받으면
    if(strTmp.Find(SOC_CLIENT_CONNECT) == 0){
        // Right 함수를 이용해 가장 오른쪽에 있는 번호 추출. 0은 \0이다.
        m_strUserID = "사용자 : " + strTmp.Right(1);
    }
    else {
        // 리스트박스에 보여준다.
        int i = m_list.GetCount();
        m_list.InsertString(i, strTmp);
    }
    UpdateData(FALSE);
    return TRUE;
 
4. Client : [종료] 버튼 클릭 메시지 함수 생성
OnSysCommand()에 추가한 내용은 오직 창의 "X" 버튼을 눌러 종료했을 경우 수행
그러므로 [종료] 버튼을 눌렀을 경우에도 서버에게 종료 메시지를 보내야 함

그림6. [종료] 버튼 클릭 메시지 함수

  • 메시지 함수 소스 보기
더보기
void CChatClientDlg::OnBnClickedCancel()
{
    // TODO: 여기에 컨트롤 알림 처리기 코드를 추가합니다.
    m_socCom.Send(SOC_CLIENT_DISCONNECT, 256);        // 서버로 종료 메시지 보냄
    this->EndDialog(IDCANCEL);                        // Dialog 닫기
    CDialogEx::OnCancel();
}
 

 


 

5. Server(SocCom) : 사용자 정의 메시지 및 사용자 지정 변수 추가 + OnReceive() 수정
SOC_CLIENT_CONNECT, SOC_CLIENT_DISCONNECT, int m_index 추가
MAX_CLIENT_COUNT : 최대 수용 가능한 클라이언트(사용자) 수
OnReceive() : SendMessage()의 3번째 인자 값에 m_index 추가

그림7. SocCom.h 수정

  • SocCom.h 소스 보기
더보기
#pragma once

// CSocCom 명령 대상
// 통신용 소켓
#define UM_RECEIVE WM_USER+2
#define MAX_CLIENT_COUNT 3        // 서버 수용 가능 최대 클라이언트 수

#define SOC_CLIENT_CONNECT      "접속성공"            // 클라이언트(사용자)가 접속 
#define SOC_CLIENT_DISCONNECT "클라이언트 종료"        // 클라이언트 종료 시 보낼 메세지

class CSocCom : public CSocket
{
public:
    CSocCom();
    virtual ~CSocCom();

    HWND m_hWnd;                            // 메인 윈도우 핸들
    void CSocCom::Init(HWND hWnd);            // 소켓 클래스와 메인 윈도우를 연결시킴    
    virtual void OnReceive(int nErrorCode);    // 데이터가 도착했다는 것을 알려줌

    int m_index;
};
 

그림8. SocCom.cpp 수정

  • SocCom.cpp 소스 보기
더보기
// 데이터가 도착했다는 것을 알려주는 가상 함수
void CSocCom::OnReceive(int nErrorCode)
{
    // TODO: 여기에 특수화된 코드를 추가 및/또는 기본 클래스를 호출합니다.
    SendMessage(m_hWnd, UM_RECEIVE, m_index, 0);
    CSocket::OnReceive(nErrorCode);
}
 

 

6. Server(SocServer) : STL List 추가 및 수정
CSocCom m_socCom[MAX_CLIENT_COUNT] : 배열로 변환하여 관리(여기서는 3개)
std::list<int> m_index : STL List로 소켓 관리
위 2개 변수를 활용하여 OnAccept(), GetAcceptSocCom() 수정

그림9. SocServer.h 수정

  • SocServer.h 소스 보기
더보기
#pragma once

#include "SocCom.h"
#include <list>
#define UM_ACCEPT WM_USER+1

// CSocServer 명령 대상
// 서버용 소켓
class CSocServer : public CSocket
{
public:
    CSocServer();
    virtual ~CSocServer();

    CSocCom m_socCom[MAX_CLIENT_COUNT];        // 연결 요청을 한 클라이언트 서버와 실제 연결이 되는 소켓
    CSocCom* GetAcceptSocCom();                // 통신 소켓 리턴

    std::list<int> m_index;

    HWND m_hWnd;                            // 메인 윈도우 핸들
    void CSocServer::Init(HWND hWnd);        // 소켓 클래스와 메인 윈도우를 연결시킴    
    virtual void OnAccept(int nErrorCode);    // 클라이언트 접속 요청 처리
};
 

그림10. SocServer.cpp 수정

  • SocServer.cpp 소스 보기
더보기
// 클라이언트에서 접속 요청이 올 경우 OnAccept 함수가 호출됨
// OnAccept 함수가 호출되면 접속 요청할 한 소켓과 다른 소켓을 연결하기 위해 Accept 함수를 호출한 뒤 메인 윈도우에 OnAccept 함수가 호출되었다는 것을 알려줌
void CSocServer::OnAccept(int nErrorCode)
{
    // TODO: 여기에 특수화된 코드를 추가 및/또는 기본 클래스를 호출합니다.
    Accept(m_socCom[m_index.front()]);                        // m_socCom은 연결 요청을 한 클라이언트 서버와 실제 연결이 되는 소켓
    SendMessage(m_hWnd, UM_ACCEPT, 0, 0);
    CSocket::OnAccept(nErrorCode);
}

// 메인 윈도우에서는 m_socCom을 얻어서 통신을 처리
CSocCom* CSocServer::GetAcceptSocCom() {
    // 통신소켓을 return
    // 반환되는 통신 소켓은 클라이언트와 연결됨
    return &m_socCom[m_index.front()];
}
 
SMALL
7. Server(ChatSeverDlg) 
통신용 소켓 CSocCom* m_socComCSocCom* m_socCom[MAX_CLIENT_COUNT] 으로 변경
std::list<int> m_using : STL List로 소켓 관리
OnInitDialog() 코드 수정
OnAccept() 코드 수정
OnReceive() 코드 수정 
OnClickedButtonSend() 코드 수정

그림11. ChatServerDlg.h 수정
그림12. OnInitDialog() 수정

  • OnInitDialog() 소스 보기
더보기
for (int i = 0; i < MAX_CLIENT_COUNT; i++) {
        m_socServer.m_index.push_back(i);
    }

    // 서버 소켓을 생성(포트번호 5000)
    m_socServer.Create(5000);
    // 클라이언트의 접속을 기다림
    m_socServer.Listen();
    // 소켓 클래스와 메인 윈도우(여기에서는 CChatServerDlg)를 연결
    m_socServer.Init(this->m_hWnd);
 
  • OnAccept() 소스 보기
더보기
// 클라이언트 연결 요청이 왔기 때문에 Accept 함수로 접속
// 실제 접속을 담당하는 것은 CSocServer
// 이렇게 접속한 소켓은 GetAcceptSocCom을 이용해 얻어옴
// OnAccept 실행 이후 서버용 소켓인 m_socServer의 역활은 끝나고, 실제 모든 통신은 통신용 소켓인 m_socCom을 이용
LPARAM CChatServerDlg::OnAccept(UINT wParam, LPARAM lParam) {
    // 클라이언트에서 접속 요청이 왔을 때
    
    try {
        // 통신용 소켓을 생선한 뒤
        int tmp = m_socServer.m_index.front();

        CString number;                // 클라이언트 번호
        number.Format("%d", tmp);

        m_socCom[tmp] = new CSocCom();
        // 서버소켓과 통신소켓을 연결한다.
        m_socCom[tmp] = m_socServer.GetAcceptSocCom();

        m_socServer.m_index.pop_front();
        m_using.push_back(tmp);

        m_socCom[tmp]->m_index = tmp;
        m_socCom[tmp]->Init(this->m_hWnd);

        // 클라이언트(사용자)에게 연결 성공 메시지를 보낼때 클라이언트 번호도 같이 보냄.
        m_socCom[tmp]->Send((SOC_CLIENT_CONNECT + number), 256);    
    }
    catch (CException* ex) {
        ex->ReportError();
    }
    UpdateData(FALSE);
    return TRUE;
}
 
  • OnReceive() 소스 보기
더보기
// 데이터를 보내는 것은 소켓 클래스의 멤버 함수인 Send를 이용
// 데이터를 받을 때는 통신 소켓 클래스에 오버라이딩한 OnReceive 메시지 함수를 사용
LPARAM CChatServerDlg::OnReceive(UINT wParam, LPARAM lParam) {
    // 접속된 곳에서 데이터가 도착했을 때
    char pTmp[256];
    CString strTmp;
    memset(pTmp, '\0', 256);

    // 데이터를 pTmp에 받는다.
    m_socCom[wParam]->Receive(pTmp, 256);    // wParam = 클라이언트 번호
    strTmp.Format("%s", pTmp);

    if (strTmp.Compare(SOC_CLIENT_DISCONNECT) == 0) {
        m_socServer.m_socCom[wParam].Close();
        m_socCom[wParam]->Close();
        m_socServer.m_index.push_back(wParam);
        m_using.erase(std::remove(m_using.begin(), m_using.end(), wParam), m_using.end());
    }
    else {
        // 리스트박스에 보여준다.
        CString id;
        id.Format("%d", wParam);

        int i = m_list.GetCount();
        m_list.InsertString(i, ("사용자" + id + " : " + strTmp));

        // 이 부분 제외하면 서버만 다중 클라이언트로 부터 채팅 가능.
        for each (int i in m_using) {
            if (i != _ttoi(id)) {    // 보낸 클라이언트 제외 모든 클라이언트한테 보냄
                m_socCom[i]->Send(("사용자" + id + " : " + strTmp), 256);
            }
        }
    }
    return TRUE;
}
 
  • OnClickedButtonSend() 소스 보기
더보기
// [전송] 버튼을 클릭했을 때
void CChatServerDlg::OnClickedButtonSend()
{
    // TODO: 여기에 컨트롤 알림 처리기 코드를 추가합니다.
    UpdateData(TRUE);
    char pTmp[256];
    CString strTmp;

    // pTmp에 전송할 데이터 입력
    memset(pTmp, '\0', 256);
    strcpy_s(pTmp, "관리자 : " + m_strSend);
    m_strSend = "";

    // 전송
    for each (int i in m_using) {
        m_socCom[i]->Send(pTmp, 256);
    }
    // 전송한 데이터도 리스트박스에 보여준다.
    strTmp.Format("%s", pTmp);
    int i = m_list.GetCount();
    m_list.InsertString(i, strTmp);

    UpdateData(FALSE);
}
 

 

8. 실행 화면


관련 글
 

[MFC] 채팅 프로그램 - 서버 (1/3)

- 설명 - 프로젝트 명 : ChatServer(대화 상자) ① CSocket 클래스 이용 : 동기식(블로킹)으로 동작 ② 해당 글은 서버용 채팅 프로그램 만드는 법 설명 1. MFC 애플리케이션 옵션 설정 ① 애플리케이션 종

balabala.tistory.com

 

 

[MFC] 채팅 프로그램 - 클라이언트 (2/3)

- 설명 - 프로젝트 명 : ChatClient(대화 상자) ① CSocket 클래스 이용 : 동기식(블로킹)으로 동작 ② 서버 클라이언트 1:1 통신 ③ 해당 글은 클라이언트 채팅 프로그램 만드는 법 설명 1. MFC 애플리케이

balabala.tistory.com

 

728x90
LIST