티스토리 뷰

이번에는 채팅을 할 수 있는 프로그램을 예제로 공부하겠다.


1부 : 윈도우즈 MFC 소켓프로그래밍 : 소켓 기본 클래스 소스코드 1 (헤더파일)

http://andrew0409.tistory.com/97


2부 : 윈도우즈 MFC 소켓프로그래밍 : 소켓 기본 클래스 소스코드 2 (서버cpp파일)

http://andrew0409.tistory.com/99


3부 : 윈도우즈 MFC 소켓프로그래밍 : 소켓 기본 클래스 소스코드 3 (클라이언트cpp파일)

http://andrew0409.tistory.com/100


기본적으로 MFC 프로젝트를 하나 생성하고, 지난 포스팅에서 만들어놓은 CClientSocket 과 CServerSocket의 헤더와 cpp파일을 프로젝트 폴더로 복사해와서 include 한다. 잘 모르겠다면 위 포스팅을 참조해서 클래스를 먼저 만들어 놓도록 하자.


프로젝트는 간단하게 대화상자 형식으로 생성하도록 한다.



좌측 그림과 같은 형태의 다이어로그를 그릴수 있으면 된다. 메세지 창 이라고 적힌 스태틱 텍스트와, 리스트 박스, 버튼 하나로 간결하게 구성되어있다.


스태틱 텍스트에서는 테두리 선을 그리기 위해서 Modal Frame을 true로 설정하고 caption이 메세지 창, 또 캡션을 가운데에 위치시키기 위해서 Align Text와 Center Image를 각각 center, true로 설정했다.


리스트 박스에는 채팅 내용이 올라가게 만들어볼건데, 채팅으로 전달하는 메세지가 리스트 박스에서 정렬이 되면 곤란하므로 Sort 속성을 false로 설정했다.


이제 스태틱 텍스트와 리스트 박스에 콘트롤형식으로 각각 변수를 만들어 놓는다 스태틱 텍스트의 변수이름은 mMessage 이고 리스트 박스의 변수 이름은 mUserList이다.


이번 포스팅은 MFC를 설명하는 포스팅이 아니기 때문에 변수를 만드는 방법이나 도구상자에서 꺼내서 배치하는것 등은 다루지 않겠다.


마지막으로 버튼을 더블클릭해서 이벤트 처리기를 만들어 주면 리소스에서의 준비는 모두 마치게 된다.


이제 stdafx.h로 이동해서 추가한 CServerSocket과 CClientSocket을 include하고, app.cpp로 이동하여 InitInstance와 ExitInstance에서 WSADATA를 생성하여 스타트업을 하고, 종료할때 클린업을 하는 코드를 작성하기 바란다. 잘 모르겠다면 위에 링크해 놓은 이전 포스팅들을 참조하면 되겠다.


이것으로 채팅프로그램을 만들 사전 준비는 모두 끝났다. 이제 본격적으로 시작해보자.


서버부분 먼저 코딩해보겠다.

우선은 프로그램을 구동하자 마자 서버가 동작할 수 있도록 app의 OnInitDialog에서 스레드를 실행 시킨다.


1
2
3
4
5
6
7
8
BOOL CDay_06_23_Server_ChatDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();
    
    AfxBeginThread(ServerProc, this);
 
    return TRUE;
}
cs


전달인자로 this를 보내어 아까 추가해놓은 mMessage와 mUserList등을 사용할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
UINT ServerProc(void *p)
{
    auto dlg = (CDay_06_23_Server_ChatDlg*)p;
 
    dlg->mServer.Create(28000);
    dlg->showMessage(L"서버를 구동했습니다.");
 
    sockaddr_in addr;
    int addrSize;
    char userText[256];
 
    while (true)
    {
        memset(&addr, 0sizeof(addr));
        addrSize = sizeof(addr);
 
        SOCKET client = dlg->mServer.Accept(&addr, &addrSize);
 
        if (client == INVALID_SOCKET)
            break;
 
        CClientSocket *= new CClientSocket(client);
        dlg->mClients.push_back(p);
 
        sprintf(userText, "%s_%d", ::inet_ntoa(addr.sin_addr), addr.sin_port);
        dlg->addUserInList(userText, L"kim");
 
        AfxBeginThread(ChatProc,
            new std::pair<void *, CClientSocket *>(dlg, p));
    }
 
    return 0;
}
cs


서버 프로시저에서는 일단 p로 전달했던 this를 형변환하여 로컬변수에 다시 저장하고, 저장한 dlg를 이용하여 서버를 생성한다.


1
2
3
4
void CDay_06_23_Server_ChatDlg::showMessage(wchar_t *message)
{
    mMessage.SetWindowText(message);
}
cs


중간에 들어있는 ShowMessage 함수이다. 유니코드형 문자를 전달하면 해당 문자로 mMessage를 바꿔주는 부분이다.


이렇게 서버를 구동한 뒤에 Accept를 한다. 반환값을 저장한 client 를 이용하여 CClientSocket을 포인터의 형태로 만들고, 클라이언트 소켓의 벡터타입인 mClients에 푸시백한다.


1
std::vector<CClientSocket *> mClients;
cs
헤더에서 mClients의 선언부분.


그 다음 만들어놓은 sockaddr_in 타입의 addr이 가지고있는 변수들을 userText라는 char형 배열에 저장하고 유저리스트에 넣는다.


1
2
3
4
5
6
void CDay_06_23_Server_ChatDlg::addUserInList(const char *ip, wchar_t *name)
{
    USES_CONVERSION;
 
    m_userList.AddString(CString(A2W(ip)) + L"\t" + name);
}
cs


addUserInList의 정의부분


예제 코드에서는 임의로 kim이라는 이름을 통일시켰는데, 실제로 채팅프로그램을 만들때는 이 부분도 다이어로그 에디트 컨트롤을 이용해서 입력받아서 사용하면 좋을 것이다.


이제 ChatProc를 스레드로 실행하는데 전달인자로 dlg와 p로 보낸다. 예제에서는 전달할 매개변수가 두개라서 pair를 만들어서 보냈지만 struct를 사용해도 좋고 편한대로 전달하면 되겠다. 참고로 pair는 first와 second 두개의 변수로 구성되어있는 구조체이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
UINT ChatProc(void *p)
{
    auto param = *(std::pair<void *, CClientSocket *> *) p;
    delete (std::pair<void *, CClientSocket *> *) p;
 
    auto dlg = (CDay_06_23_Server_ChatDlg *)param.first;
    auto client = (CClientSocket *)param.second;
 
    wchar_t buf[256];
 
    int total = 0;
 
    while (true)
    {
        int ret = client->Recv(((char *)buf)+total, sizeof(buf)-total);
 
        if (ret == || ret == SOCKET_ERROR)
            break;
 
 
        //여기서부터 ------------------- 반복문 처리 필요 //
        total += ret;
 
        if (total < 4)
            continue;
 
        int packetSize = *((int *)buf); 
//리틀앤디안 빅앤디안때문에 뒤집어서넣고 뒤집어서 받아야함
 
        if (total < 4)
            continue;
 
        dlg->SendAll((char *)buf, packetSize);
 
        memcpy(buf, buf + packetSize, total - packetSize);
        total -= packetSize;
        // ------------------ 여기까지 루프처리해야함 //
    }
 
    //여기까지 코드가 들어온다는것은 연결이 끊어졌다는 것임
    //채팅방을 빠져나간 클라이언트 데이터 제거
    //데이터를 삭제하는 부분은 임계영역 크리티컬 섹션으로 묶어줘야함 안그러면 충돌남
    //
 
    return 0;
}
cs

13번 라인까지는 설명할 필요가 없어보인다 지난 포스팅에서 아주 자세하기 설명했기 때문에..
15번 라인 역시 마찬가지이다. 아주 중요한 부분이기는 하나 지난 포스팅에서 다루었던 부분이기 때문에 넘어가겠다.


24라인에 보면 total이 4보다 작을때 다시 전달받으라고 한다. 왜냐하면 지난 포스팅에서는 클라이언트가 12바이트를 보낼것을 가정하고 코드를 짰지만, 채팅이라는 것은 상대방이 몇글자의 문자열을 보낼지 알 수 없다. 그렇기 때문에 클라이언트는 buf의 가장 앞부분에 몇바이트를 보냈는지 같이 전달해주게 되는데 그렇기 때문에 4바이트를 내어주고 시작한다. 그렇다면 total이 4바이트보다 작다는것은 아무것도 전달하지 않았다는 뜻으로 볼 수 있겠다.


그래서 27번 라인에서 처럼과 같이 char형 주소인 buf의 시작주소를 int*로 형변환하고 그것의 값을 받을 수 있도록 앞에 *로 붙여서 packetSize를 저장하는 것이다.


서버는 이렇게 문자열과 문자열의 길이를 받아서 모든 클라이언트로 전달한다. 왜냐하면 하나의 채팅방에 여러개의 클라이언트가 접속했을때 한명이 하는 말을 여러명이 볼 수 있어야 하기 때문이다.


1
2
3
4
5
6
7
void CDay_06_23_Server_ChatDlg::SendAll(const char *p, int packetSize)
{
    for (auto it = mClients.begin(); it != mClients.end(); ++it)
    {
        (*it)->Send(p, packetSize);
    }
}
cs

SendAll 함수의 정의부이다.
반복문을 돌면서 해당 클라이언트에 데이터를 전달하는 것을 볼 수 있다.

코드에는 등장하지 않았지만 마지막 주석달아놓은 부분에서 클라이언트를 Close 해주면 적절할 것 같다. 중간에 임계영역이라는 말이 있는데 이것에 대해서는 다음에 블로킹, 논블로킹 소켓모드와 함께 자세하게 다루어 보도록 하겠다.

우선 이것으로 서버부분에 대한 코드풀이를 마치고 다음 포스팅에서 클라이언트에 관한 내용을 짧게 다루어 보도록 하겠다.


댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함