ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [MFC]TCP/IP 소켓프로그래밍
    💻 프로그래밍/C, C++ 2018. 2. 7. 13:24

    안녕하세요! 코딩하는 남자 JAY입니다~!! 잘지내셨나요 여러분?! 

    오늘은 'TCP/IP 소켓프로그래밍'에 대해서 알아보도록 하겠습니다. 소켓프로그래밍에 대한 간단한 개념과 MFC로 소켓프로그래밍을 구현하는 과정을 순서대로 진행하겠습니다.


    1. TCP/IP 소켓프로그래밍 이란??

    - TCP/IP : 가장최근에 발명된 컴퓨터와 컴퓨터간의 지역네트워크(LAN) 혹은 광역네트워크(WAN)에서 원할한 통신을 가능하도록 하기 위한 통신규약(Protocol) 으로 정의할 수 있습니다. 

    - 소켓 : 간단하게 말하면 통신을 위한 일종의 통로이다. 두 프로그램이 네트워크를 통해 서로 통신을 할 수 있도록, 양쪽에 생성되는 링크의 단자입니다. 두 소켓이 연결되면 서로다른 프로세스끼리 데이터를 전달할 수 있다. 실용적인 관점에서 소켓은 TCP/UDP로 구분됩니다.


    일반적으로 네트워크 프로그래밍 이라 하면 TCP/IP 소켓프로그래밍을 말합니다. 소켓통신을 하기위해서는 몇가지 단계가 필요하며, MFC에서 제공하는 API를 기준으로 설명하면 아래와 같습니다.


    1) '서버'에서 소켓생성(포트번호, TCP/UDP, 네트워크번호 등 설정)

    2) '클라이언트'에서 소켓 생성 후, Connect 시도

    3) 서버에서 연결 수락(조건이 맡을시), 소켓리스트추가

    4) 메시지 전송(클라이언트)

    5) '서버'에서 수신한 메시지 체크 후, 모든 클라이언트로 전송


    나름 그림으로 정리해봤는데...이해가 잘 되실지 모르겠네요 ㅠㅠ 궁금한점 있으시거나 제가 정리한게 틀리면 댓글 부탁드립니다.


    2. 구현(서버)

    자, 이제 실제로 구현을 해보도록 하겠습니다. 



    먼저 '대화상자기반'으로 서버쪽 프로그램을 만들어 보겠습니다:D 다음을 누르시다가 '고급기능'에서 'Windows 소켓' 기능에 체크를 하시고 프로젝트를 생성하시면됩니다. 서버쪽에서 필요한 내용은 크게 두 가지입니다.


    1. 클라이언트 접속을 기다리고, 메시지를 입력받는 클래스(비동기) 

    2. 클라이언트의 메시지를 수신하는 클래스(동기) 이 두가지 입니다. 


    즉 두가지 클래스를 만들어야한다는 말인데요. 'MFC클래스 추가마법사'를 사용하여 ,1번은 CAsynSocket을 상속받아서 CListenSocket이라는 클래스를 만들고, 2번은 CSocket클래스를 상속받아서 CClientSocket이라는 클래스를 만들면됩니다. 



    CSocketServerDlg.h에 소켓을 생성하기 위한 CListenSocket객체인 m_ListenSocket 현재 접속한 클라이언트들을 보여주는 CListBox객체인 clientList를 추가합니다. CListenSocket.h에는 CPtrList객체인 m_ptrClientSocketList변수를 추가합니다. m_ptrClientSocketList는 클라이언트를 관리하는 리스트로 사용됩니다. 


    - 화면구성 및 리스트 변수 추가 

    기본 화면구성은 위와 같습니다. 메시지와 클라이언트번호를 볼 수 있는 두개의 ListBox를 넣어줍니다. 이 리스트 박스들을 사용하는 방법은 두가지 방법이 있습니다. IDC_LIST1과 IDC_CLIENT_LIST를 다른 방식으로 사용해보도록 하겠습니다.


    1. ListBox 오른쪽 버튼 클릭 후, 변수 추가하여 사용하는 방법(IDC_LIST1)

    ListBox를 클릭하고, 마우스 오른쪽 버튼을 누르면 '변수추가'라는 항목이 나옵니다. 

    '변수추가'항목을 클릭하면 '멤버 변수 추가 마법사' 창이 뜹니다. 변수형식, 변수이름등을 적은 뒤 확인을 누르면 Dlg클래스에 자동으로 변수가 등록되는것을 확인 할 수 있습니다.

    헤더파일에 추가된 m_List(IDC_LIST1) 변수

    DoDataExchange 함수에 추가된 m_List 

    DDX : Dialog Data Exchange 의 약자로 ' Exchange : 교환, 주고받음 맞바꿈 ' 으로 컨트롤과 변수간의 데이터 교환


    2. GetDlgItem()으로 아이템의 포인터를 가져와 사용하는 방법(IDC_CLIENT_LIST)

    먼저 클래스안에 CListBox 포인터변수로 clientList를 선언해줍니다.

    다음은 OnInitDialog()에서 GetDlgItem()을 이용헤 사용하고자 하는 아이템의 ID를 입력하여 아이템의 포인터를 가져오면 끝! GetDlgItem()의 기본 반환형은 CWnd* 이며, clientList는 ListBox이기때문에 형변환을 사용해줍니다. 



    - CListenSocket.cpp 

    먼저 접속되는 클라이언트 Socket을 저장하는 리스트인 m_ptrClientSocketList를 추가해줍니다. 


    CListenSocket클래스에는 3가지 함수를 추가해야합니다.


    1. 서버-클라이언트의 연결을 담당하는 OnAccept()

    2. 클라이언트의 연결을 종료하는 CloseClientSocket()

    3. 클라이언트의 메시지를 다른 클라이언트에게 전송하는 SendAllMessage()

    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
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    // CListenSocket 멤버 함수
    void CListenSocket::OnAccept(int nErrorCode)
    {
        CClientSocket* pClient = new CClientSocket;
        CString str;
        
        if(Accept(*pClient)) { // 클라이언트 접속 요청이 오면 서버-클라이언트를 연결시켜준다
            pClient->SetListenSocket(this);
            m_ptrClientSocketList.AddTail(pClient); // 리스트에 클라이언트 소켓 저장
     
            CSocketServerDlg* pMain = (CSocketServerDlg*)AfxGetMainWnd(); // CSocketServerDlg의 핸들을 가져옴
            str.Format(_T("Client (%d)"), (int)m_ptrClientSocketList.Find(pClient)); // 클라이언트 번호(POSITION(주소) 값)
            pMain->clientList->AddString(str); // 클라이언트가 접속할때마다 리스트에 클라이언트 이름 추가
            
        } else {
            delete pClient;
            AfxMessageBox(_T("ERROR : Failed can't accept new Client!"));
        }
     
        CAsyncSocket::OnAccept(nErrorCode);
    }
     
    // 클라이언트 연결 종료함수
    void CListenSocket::CloseClientSocket(CSocket* pClient)
    {
        POSITION pos;
        pos = m_ptrClientSocketList.Find(pClient);
        
        if(pos != NULL) {
            if(pClient != NULL) {
                // 클라이언트 연결중지후 종료
                pClient->ShutDown();
                pClient->Close();
            }
     
            CSocketServerDlg* pMain = (CSocketServerDlg*)AfxGetMainWnd();
            CString str1, str2;        
            UINT indx = 0, posNum;    
            pMain->clientList->SetCurSel(0);
            // 접속 종료되는 클라이언트 찾기
            while(indx < pMain->clientList->GetCount()) {
                posNum = (int)m_ptrClientSocketList.Find(pClient); 
                pMain->clientList->GetText(indx, str1);
                str2.Format(_T("%d"), posNum);
                // 소켓리스트, 클라이언트리스트를 비교해서 같은 클라이언트 번호(POSITION 값) 있으면 리스트에서 삭제
                if(str1.Find(str2) != -1) {
                    AfxMessageBox(str1 + str2);
                    pMain->clientList->DeleteString(indx);
                    break;
                }
                indx ++;
            }
     
            m_ptrClientSocketList.RemoveAt(pos);
            delete pClient;
        }
    }
     
    void CListenSocket::SendAllMessage(TCHAR* pszMessage)
    {
        POSITION pos;
        pos = m_ptrClientSocketList.GetHeadPosition();
        CClientSocket* pClient = NULL;
     
        while(pos != NULL) {
            pClient = (CClientSocket*)m_ptrClientSocketList.GetNext(pos);
            if(pClient != NULL) {
                // Send함수의 두번째 인자는 메모리의 크기인데 유니코드를 사용하고 있으므로 *2를 한 크기가 된다.
                // 이 함수는 전송한 데이터의 길이를 반환한다.
                int checkLenOfData = pClient->Send(pszMessage, lstrlen(pszMessage) * 2);
                if(checkLenOfData != lstrlen(pszMessage) * 2) {
                    AfxMessageBox(_T("일부 데이터가 정상적을 전송되지 못했습니다!"));
                }
            }
        }


    - CClientSocket.cpp 

    CClientSocket 클래스는 클라이언트를 생성하고 메시지를 전달받습니다. 

    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
    // CClientSocket 멤버 함수
    void CClientSocket::SetListenSocket(CAsyncSocket* pSocket)
    {
        m_pListenSocket = pSocket;
    }
    // CClientSocket 멤버 함수
     
     
    void CClientSocket::OnClose(int nErrorCode)
    {
        CSocket::OnClose(nErrorCode);
     
        CListenSocket* pServerSocket = (CListenSocket*)m_pListenSocket;
        pServerSocket->;CloseClientSocket(this);
    }
     
    void CClientSocket::OnReceive(int nErrorCode)
    {
        CString strTmp = _T(""), strIPAddress = _T("");
        UINT uPortNumber = 0;
        TCHAR strBuffer[1024];
        ::ZeroMemory(strBuffer, sizeof(strBuffer)); // :: 붙이고 안붙이고 차이 알아보기
        
        GetPeerName(strIPAddress, uPortNumber);
        if(Receive(strBuffer, sizeof(strBuffer)) > 0) { // 전달된 데이터(문자열)가 있을 경우
            CSocketServerDlg* pMain = (CSocketServerDlg*)AfxGetMainWnd();
            strTmp.Format(_T("[%s:%d]: %s"), strIPAddress, uPortNumber, strBuffer);
            pMain->m_List.AddString(strTmp);  // 메시지 리스트(메시지창?)에 입력받은 메시지 띄우기
            pMain->m_List.SetCurSel(pMain->m_List.GetCount() - 1);
     
            CListenSocket* pServerSocket = (CListenSocket*)m_pListenSocket;
            pServerSocket->SendAllMessage(strBuffer); // 다른 클라이언트들에게 메시지 전달
        }
     
        CSocket::OnReceive(nErrorCode);
    }


    - CSocketServerDlg.cpp 

    CSocketServerDlg클래스에서는 소켓을 생성하고, 프로그램이 종료될때 소켓을 닫아버리는 기능을 추가합니다.

    OnInitDialog()의 맨 아래에 다음과 같이 소켓을 생성하는 코드를 추가합니다.

    먼저 소켓생성을 위에서 만든 CListenSocket객체인 m_ListenSocket을 선언해줍니다. m_ListenSocket으로 소켓을 생성하고 클라이언트 접속을 기다리다가 클라이언트 접속 요청이 들어오면 Accept하여 소켓을 연결시키는 코드를 만들 것입니다. 


    1
    2
    3
    4
    5
    6
    7
    8
    9
    // TODO: 여기에 추가 초기화 작업을 추가합니다.
    if(m_ListenSocket.Create(21000, SOCK_STREAM)) { // 소켓생성
        if(!m_ListenSocket.Listen()) {
            AfxMessageBox(_T("ERROR:Listen() return False"));
        }
    else {
        AfxMessageBox(_T("ERROR:Failed to create server socket!"));
    }
     
    cs

    m_ListenSocket은 CAsyncSocket 클래스를 상속받은 CListenSocket 객체이며, 소켓생성을 위해 CAsyncSocket::Create()를 사용합니다. 매개변수 입력으로는 포트, 데이터전송타입을 선택합니다. 제가 사용할 데이터 전송타입은 SOCK_STREAM(TCP) 입니다. IP의 경우 IP가 2개 이상인 컴퓨터에서만 의미가 있고 기본 NULL값을 사용하면 됩니다.


    마지막으로 프로그램이 종료될때 소켓을 닫아버리는 코드를 추가합니다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    void CSocketServerDlg::OnDestroy()
    {
        CDialog::OnDestroy();
     
        POSITION pos;
     
        pos = m_ListenSocket.m_ptrClientSocketList.GetHeadPosition();
        CClientSocket* pClient = NULL;
            
        // 생성되어있는 클라이언트 소켓이 없을때까지 체크하여 소켓닫기
        while(pos != NULL) {
            pClient = (CClientSocket*)m_ListenSocket.m_ptrClientSocketList.GetNext(pos);
            if(pClient != NULL) {
                pClient->ShutDown(); // 연결된 상대방 소켓에 연결이 종료됨을 알린다. 
                pClient->Close(); // 소켓을 닫는다
     
                delete pClient;
            }
        }
        m_ListenSocket.ShutDown();
        m_ListenSocket.Close();
    }
     


    여기까지가 서버측 구현이었습니다.  프로젝트를 실행시켜보면 아래와 같은 다이얼로그창이 나올겁니다:D



    다음은 CAsyncSocket Class에 대한 설명이 잘 나와있는 블로그입니다. OnAccept(), OnConnect() 같은 콜백함수의 내용이 정리되어 있으니 보시는걸 추천드립니다.

    CAsyncSocket 관련 내용 블로그http://nenunena.tistory.com/59


    3. 구현(클라이언트)

    다음은 클라이언트측을 구현해볼건데요, 특별히 다른점은 없습니다. 클라이언트측에서는 서버측에 접속요청만 하면되니 CSocket클래스만 이용하면 됩니다. 마찬가지로 프로젝트 생성시 'Windows 소켓' 기능에 체크하고 프로젝트를 생성하면 됩니다.



    1. CSocket클래스를 상속받는 CConnectSocket 클래스를 추가합니다.

    2. CSocketClientDlg.h에 CConectSocket객체인 m_Socket을 추가합니다.


    - CConnectSocket.cpp 

    프로그램이 종료됬을때 소켓을 꺼버리는 Onclose()와 받은 메시지를 메시지창에 출력하는 OnReceive()를 아래와 같이 재정의 합니다.

    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
    // CConnectSocket 멤버 함수
    void CConnectSocket::OnClose(int nErrorCode)
    {
        ShutDown();
        Close();
        
        CSocket::OnClose(nErrorCode);
     
        AfxMessageBox(_T("ERROR:Disconnected from server!"));
        ::PostQuitMessage(0);
    }
     
    void CConnectSocket::OnReceive(int nErrorCode)
    {
        TCHAR szBuffer[1024];
        ::ZeroMemory(szBuffer, sizeof(szBuffer));
     
        if(Receive(szBuffer, sizeof(szBuffer)) > 0) { 
            CChatClientDlg* pMain = (CChatClientDlg*)AfxGetMainWnd();
            pMain->m_List.AddString(szBuffer); // 리스트에 문자열을 추가한다.
            pMain->m_List.SetCurSel(pMain->m_List.GetCount() - 1);
        }
        CSocket::OnReceive(nErrorCode);
    }
     


    - CSocketClientDlg.cpp

    소켓 Connection을 위한 CConnectSocket 객체인 m_Socket을 멤버변수로 추가합니다.

    OinitDialog()에 서버에 접속하는 코드를 추가합니다. 자기 자신의 컴퓨터로 접속하기 때문에 IP를 127.0.0.1로 설정합니다.

    1
    2
    3
    4
    5
    6
    7
    8
        // TODO: 여기에 추가 초기화 작업을 추가합니다.
        m_Socket.Create();
        if(m_Socket.Connect(_T("127.0.0.1"), 21000== FALSE) {
            AfxMessageBox(_T("ERROR : Failed to connect Server"));
            PostQuitMessage(0);
            return FALSE;
        }
     


    마지막으로 메시지를 입력할 Edit Control과 메시지를 전송하는 Button을 아래와 같이 추가합니다.



    메시지버튼 함수를 생성후 입력창의 텍스트를 전송하는 코드를 구현합니다. Edit Control의 텍스트를 가져오는 방법은 2가지가 있습니다. 아래코드에 보면 방법1과 방법2가 있는데, 둘 중 하나만 사용하시면됩니다. 두개의 차이는 객체를 직접생성하면 UpdateData()를 사용하는 불편함을 없앨 수 있습니다. 저는 방법2를 주로 사용하는 편입니다:D

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    void CSocketClientDlg::OnBnClickedButton1()
    {
        CString str;
     
        // 방법1, 2 둘 중 하나만 사용!!!!
        // 방법1, 리소스뷰에서 만든 거 그대로 쓸때
        UpdateData(TRUE);
        GetDlgItemText(IDC_INPUT, str );   
        m_Socket.Send((LPVOID)(LPCTSTR)str, str.GetLength() * 2);
        str.Format(_T(""));
        SetDlgItemText(IDC_INPUT, str );
        UpdateData(FALSE);
                
        // 방법2. 객체생성해서 쓸때
        CEdit* test = (CEdit*)GetDlgItem(IDC_INPUT);
        test->GetWindowTextW(str);
        m_Socket.Send((LPVOID)(LPCTSTR)str, str.GetLength() * 2);
        test->SetWindowTextW(_T(""));
    }



    4. 실행

    이제 서버와 클라이언트 프로그램을 실행시켜보겠습니다. 



    실행시켜 보시면 접속한 클라이언트 만큼 클라이언트 리스트에 제대로 나오는걸 확인 할 수 있습니다.


    이번 포스팅은 꽤나 길었던 것 같습니다. 그래서 조금 이해가 힘드셨을수도 있을 것 같습니다 ㅠㅠ 앞으로 좀 더 나은 글솜씨를 갖도록 노력해보겠습니다!! 무튼 이번 포스팅은 여기서 마치며, 틀린점이 있으면 댓글로 알려주시면 감사하겠습니다.

    본 포스팅의 소스코드는 첨부하도록 하겠습니다.(용량관계로 cpp, h파일만 첨부합니다.) 다들 재밌는 코드 생산하세요~~:D


    TCP_IP 소켓프로그래밍.zip



    댓글

운동하는 개발자 JAY-JI