티스토리 뷰

이전 포스팅

소켓 프로그래밍 : 소켓을 이용한 채팅 프로그램 만들기 예제(서버)

http://andrew0409.tistory.com/103


클라이언트 역시 대화상자 형식으로 생성하도록 한다.


왼쪽 그림과 같이 만들면 되겠다. 주소, 포트, 채팅, 입력이 써있는 스태틱 텍스트와, 입력할 수 있도록 주소,포트,입력은 에티트 컨트롤, 채팅은 리스트박스를 만들어 두자.

그리고 연결, 끊기, 보내기 세개의 버튼을 만든다.


에디트 컨트롤과 리스트박스는 변수추가를 하도록 하자. 나는 순서대로 mIp, mPort, mHistory, mInput이라는 이름으로 변수를 추가했다.


마지막으로 세개의 버튼을 각각 더블클릭해서 처리기를 만들면 초반 설정은 끝난다. 아 언급하지 않았지만 app에서 WSAStartup이나 WSACleanup을 꼭 하길 바란다. 너무 기본적인것이어서 언급을 안했고, 앞으로도 안 할 예정이다.


dlg의 헤더파일에 CClientSocket 타입의 mClient를 선언하고 시작하자.


1
2
3
4
5
6
7
8
9
10
11
12
BOOL CDay_06_23_Client_ChatDlg::OnInitDialog()
{
    CDialogEx::OnInitDialog();
 
    mIp.SetWindowText(L"127.0.0.1");
    mPort.SetWindowText(L"28000");
 
    GetDlgItem(IDC_DISCONNECT)->EnableWindow(FALSE);
 
    mInput.SetFocus();
    return FALSE;
}
cs

InitDialog의 소스코드이다. 로직적으로 어려운 부분은 없고 프로그램을 실행했을때의 기본값을 SetWindowText를 통해서 설정해두었다.

그리고 처음에는 연결이 되지 않은 상태이기 때문에 끊기 버튼을 누를수 없도록 EnableWindow의 값을 FALSE로 설정하고 입력란에 바로 커서가 깜빡이도록 포커스를 주었다.

버튼을 클릭했을때에 대한 처리를 먼저 하겠다.

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
void CDay_06_23_Client_ChatDlg::OnBnClickedConnect()
{
    USES_CONVERSION;
 
    CString ipText, portText;
    mIp.GetWindowText(ipText);
    mPort.GetWindowText(portText);
 
    if (ipText.GetLength() == || portText.GetLength() == 0)
    {
        MessageBox(L"빈칸이 있습니다.", L"알림");
        return;
    }
 
    if (mClient.Create() == false)
    {
        MessageBox(L"소켓 생성 실패", L"알림");
        return;
    }
 
    if (mClient.Connect(W2A(ipText), atoi(W2A(portText))) == false)
    {
        mClient.Close();
        MessageBox(L"서버 연결 실패", L"알림");
        return;
    }
 
    mHistory.AddString(L"서버에 연결했습니다.");
 
    ::WSAAsyncSelect(mClient.mSock, m_hWnd, WM_CLIENT, FD_READ | FD_CLOSE);
 
    GetDlgItem(IDC_CONNECT)->EnableWindow(FALSE);
    GetDlgItem(IDC_DISCONNECT)->EnableWindow(TRUE);
}
cs


에디트 박스로부터 데이터를 GetWindowText로 전달받기 위해 CString 타입의 변수를 두개 선언하고 바로 전달받아 변수에 저장한다.


빈칸이 있을때에 대한 예외처리를 하고 소켓을 만든다. 


W2A 라는 생소한 명령어가 있다. 제일 첫중에 USES_CONVERSION 이라는 명령어와 세트가 되는데, 이것은 유니코드를 아스키로 바꾸어주는 기능을 한다. W2A(ipText)라고 함은 ipText라는 유니코드 문자열을 아스키코드로 바꾸어 전달한다는 뜻이다. 포트번호 역시 아스키코드로 바꾼뒤 atoi 함수를 통하여 int형으로 바꾸어 전달한다.


성공적으로 연결에 성공하면 리스트박스에 서버에 연결했습니다. 라는 문자열이 출력된다.


1
    ::WSAAsyncSelect(mClient.mSock, m_hWnd, WM_CLIENT, FD_READ | FD_CLOSE);
cs

그리고 WSAAsyncSelect 함수를 이용해서 기존에 블로킹 모드이던 소켓을 논 블로킹 모드로 바꿀 것이다.
어렵고 헷갈리는 부분이기 때문에 침착하게 천천히 읽어보자 함수 구조에 대해서 살펴보자

1
2
3
4
5
6
7
int WSAAsyncSelect
{
    SOCKET s,
    HWND hWnd,
    unsigned int uMsg,
    long iEvent
};
cs

설정하고자 하는 소켓, 메세지 전달처리를 위한 윈도우 핸들러, 확인할 윈도우 메세지, 네트워크 이벤트 설정 이렇게 4가지 전달인자를 가지고 있다.

네트워크 이벤트의 종류에 대해서도 간략하게 알아보자.

FD_ACCEPT 클라이언트가 접속하면 윈도우 메세지를 발생시킨다.
FD_READ 데이터를 수신하면,
FD_WRITE 데이터를 송신하면,
FD_CLOSE 상대가 접속을 종료하면
FD_CONNECT 접속을 완료하면
FD_OOB OOB데이터가 도착하면.

그래서 우리는 선언해놓은 mClient.mSock의 소켓을 논블로킹 소켓으로 설정하며, m_hWnd를 핸들러로 사용하며, 데이터를 수신하거나 종료할때 WM_CLIENT 메세지를 발생시킬 것이다. 라는 뜻이다.

이제 정상적으로 WM_CLIENT메세지를 발생시키기 위해서 몇가지 작업을 해야한다.

dlg 헤더파일로 가서 다음 한줄을 디파인 하자.

1
#define WM_CLIENT (WM_USER+1)
cs

그리고 메세지맵에 다음을 입력해서 등록한다.(헤더부분)

1
afx_msg LRESULT OnClient(WPARAM wParam, LPARAM lParam);
cs

cpp부분
1
ON_MESSAGE(WM_CLIENT, &CDay_06_23_Client_ChatDlg::OnClient)
cs

위 세가지 작업을 수행했다면 이제 WM_CLIENT 메세지가 발생했을때 처리할 OnClient 함수를 만들어야 한다. 생각지도 못하게 WSAAsyncSelect 함수가 튀어나와서 샛길로 조금 샌듯한데, 마지막 두줄을 설명하고 다시 OnClient 함수를 만들어보자.

마지막 두줄의 의미는 연결버튼이 눌리면 끊기버튼의 disable을 able로 바꾸어주고, 연결버튼의 able을 반대로 disable로 바꾸어 주는 코드이다.

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
LRESULT CDay_06_23_Client_ChatDlg::OnClient(WPARAM wParam, LPARAM lParam)
{
    int event = WSAGETSELECTEVENT(lParam);
 
    if (event == FD_READ)
    {
        wchar_t buf[256];
 
        int total = 0;
        while (true)
        {
            int ret = mClient.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 < packetSize)
                continue;
 
            //패킷 길이인 4바이트 skip.
            mHistory.AddString(buf + 2);
 
            total -= packetSize;
        }
    }
    else if (event == FD_CLOSE)
    {
        mClient.Close();
        mHistory.AddString(L"쫒겨났습니다.");
    }
 
    return 0;
}
cs


이제 OnClient 함수에 대해서 알아보자. 이번 포스팅의 메인이라고 할 수 있겠다.

조금전 FD_READ나 FD_CLOSE가 발생했을때 WM_CLIENT 메세지를 발생시킨다고 했다.

그래서 함수는 시작하고 곧바로 event를 선언하여 WSAGETSELECTEVENT 함수를 통해 발생한 이벤트가 무엇인지 입력받는다. (만약 사용하는 네트워크 이벤트가 하나라면 꼭 이벤트를 입력받을 필요가 없다, 반면 두개보다 많을때는 if문으로 구분하기 보다는 switch 문으로 구분하는것이 사용에 더 용이할 것이다.)


그래서 이벤트가 FD_READ일때를 먼저 살펴보자.

이전 포스팅에서 공부했던 메인코드와 흡사하다. 유니코드형 버퍼를 생성하고, 토탈을 선언하여 0으로 초기화 시킨다.

클라이언트를 통해서 버퍼에 값을 전달받고, 버퍼의 첫번째 방은 패킷 사이즈이기 때문에 형변환을 통해서 저장하고 그 다음 방부터 mHistory에 저장시킨다. buf + 1이 아니라 +2 인 이유는 유니코드를 사용했기 때문에 한글자가 1바이트가 아니라 2바이트이기 때문이다. 이전 포스팅에서 설명한것과 똑같은 내용이기 때문에 추가로 설명할 부분은 없어 보인다.


event가 FD_CLOSE라면 클라이언트 소켓을 닫고 쫒겨났습니다 메세지를 리스트 박스에 출력한다.


이제 나머지 코드에 대한 설명을 하겠다.


1
2
3
4
5
6
7
8
void CDay_06_23_Client_ChatDlg::OnBnClickedDisconnect()
{
    mClient.Close();
    mHistory.AddString(L"서버와 연결을 끊었습니다.");
 
    GetDlgItem(IDC_CONNECT)->EnableWindow(TRUE);
    GetDlgItem(IDC_DISCONNECT)->EnableWindow(FALSE);
}
cs

끊기 버튼을 눌렀을때, 소켓을 닫고 메세지를 리스트박스에 올린뒤 연결을 눌렀을때처럼 버튼의 able, disable 상태를 서로 바꾸어준다. 이 기능을 쉽게 구현하기 위해서 토글버튼을 사용해도 좋다.

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
void CDay_06_23_Client_ChatDlg::OnBnClickedSend()
{
    USES_CONVERSION;
 
    if (mClient.IsValid() == false)
        return;
 
    CString input;
    mInput.GetWindowText(input);
 
    if (input.GetLength() == 0)
        return;
 
    wchar_t buf[256];
 
    int *p1 = (int*)buf;
    wchar_t *p2 = buf + 2;
 
    wcscpy(p2, input.GetBuffer());
    *p1 = + wcslen(p2) * + 2//패킷길이 + 실제 데이터
 
    mClient.Send((char *)buf, *p1);
 
    mInput.SetWindowText(L"");
    mInput.SetFocus();
}
cs


input 창으로부터 메세지를 가져오고, int형 포인터를 선언해서 buf의 첫번째 방을 가리키게 한다.

그리고 유니코드 문자열 포인터를 선언하여 buf의 두번째방을 가리키게 한다(정확히 말하자면 세번째 방이나, 유니코드는 한개의 글자를 2바이트로 처리하므로 편의상 두번째 방이라 하겠다)


이제 input에서 읽어온 데이터를 buf의 두번째방에서부터 저장하고

첫번째 방에는 4를 더하고(첫번째 방이 int형이므로) p2의 길이에 2를 곱하고(한글자당 바이트) 2를 더해준다.(null문자 또한 2바이트)

이렇게 패킷사이즈를 설정한뒤에 서버로 send한다.


메세지를 보냈으므로 input 에디트 박스는 빈칸으로 초기화하고 다시 포커스를 준다.


이상으로 채팅프로그램 예제를 공부해 보았다.


예제부터 급하게 푸느라 소켓에서의 각종 메세지나 기타 개념에 대한 정리를 못했는데 다음 포스팅에서는 소켓의 여러가지에 대하여 다루어 볼 예정이다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
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
글 보관함