ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [MFC]TCP/IP 소켓프로그래밍
    프로그래밍/C, C++ 2018.02.07 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



    댓글 23

    • 2018.02.20 16:28

      비밀댓글입니다

    • 박현수 2018.02.20 16:31

      위 설명에서 서버구현에 관한 설명에서 다이얼로그에 대한 설명이 없네요.. 리스트박스 생성후에 변수를 추가해주어야 하나요?

      • JAY-JI 2018.02.27 08:33 신고

        답변이 늦어서 죄송합니다. 변수를 추가해줘도 되고 따로 리스트박스의 ID를 읽어서 사용해도 됩니다. 클라이언트 쪽과 동일합니다~:D 즐거운 코딩하세요~

    • 2018.03.04 00:56

      비밀댓글입니다

    • bom 2018.03.04 00:58

      서버부분에 dlg안에 있는 소켓추가부분 설명좀 해주실수있을까요?ㅠ

      • JAY-JI 2018.04.20 14:38 신고

        안녕하세요! 답변이 늦어서 죄송합니다. 현재 추가로 설명 업데이트 해놓았습니다. 그래도 잘 모르시는부분을 말씀해 주시면 다시한번 설명드릴게요~:D

    • cha 2018.03.04 01:44

      서버생성이 안되네요.. 게시자님과 같은방법으로 만들었는데 어느부분이 문제인지 알수가없어서 질문드립니다...

    • 지나가는 선비 2018.04.17 09:13

      서버클래스 신나게 만들어 놓고 정작 클래스 멤버함수를 쓰는곳을 게시를 안해놨네 ㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋㅋ이게왜 구글상위에 떠있는건지 이해할수가 없음.

      • JAY-JI 2018.04.20 14:39 신고

        안녕하세요! 내용이 부족했던 점 죄송합니다. 피드백 잘받았습니다! 한번더 읽어보면서 추가설명 하였습니다. 그래도 부족한 부분이 있으면 말씀해주세요!! 감사합니다:D

    • Mr.Jeon 2018.04.27 10:43

      잘봤습니다 감사합니다~ ^.^

    • 2018.05.10 09:32

      비밀댓글입니다

    • 고려왕건 2018.07.12 17:37

      해당 프로그램 동일하게 서버 생성해서 확인해 보니, 잘 구동 되네요. 1:1 소켓 통신만 간단히 tool 개발하는데 사용하다가, 다중 연결일 경우 어떻게 하는거지 생각했는데, Client Socket 을 Accept 할때마다 생성해서 사용하도록 되어 있는 것 같습니다.

      궁금한 점이 있는대요. Client 에서 접속을 유지하다가 끊어버리면, Server 쪽에서 Accept 할때 생성된 Client와 연결 Socket은 계속 유지되고 있는 건가요?

      이부분이 조금 궁금하네요. ^^

      • JAY-JI 2018.07.16 17:22 신고

        댓글 감사해요:D 어느한쪽이라도 끊기면 둘다 끊깁니다~소켓 연결이끊기면 CloseClientSocket()에서 끊긴 클라이언트를 삭제시켜줍니다~~~^^

      • JAY-JI 2018.07.16 17:23 신고

        pClient->ShutDown(); // 연결된 상대방 소켓에 연결이 종료됨을 알린다

        ShutDown() 함수로 종료를 알려주네요!ㅎㅎ 그리고 그 종료를 인식하고 함수에서 종료한느 소켓을 지워주는것까지 구현을 합니다. 만약 종료를 인식하고도 소켓을 삭제하지 않으면 메모리상에 남아있을겁니다.

    • 잼잼 2018.07.16 17:31

      주석에 ::ZeroMemory와 ZeroMemory 차이가 뭔가요......

      • 잼잼 2018.07.16 17:33

        그리구 소켓 닫을때
        꼭 shutdown과 Close를 같이 써주어야 하나요?? ㅠㅠ

      • JAY-JI 2018.07.18 16:53 신고

        ::는 글로벌 네임스페이스를 지정하는 겁니다. 그냥 ZeroMemory 쓰게되면 중복될 수도 있으니 ::를 붙여서 글로벌 네임스코프의 ZeroMemory 함수를 사용한다는 의미입니다.
        http://iblog.or.kr/hungi/it/software/programing/466
        여기들어가보시면 잘 설명되어있습니다!

      • 잼잼 2018.07.23 13:14

        설명 감사합니다!

    • Woo 2018.10.23 20:24

      CClientSocket.cpp 에서 SetListenSocket()함수의 역할이 정확이 무엇이지 가르쳐 주실수 있나요?

    • 2018.12.06 21:03

      비밀댓글입니다

    • N 2018.12.11 15:50

      안녕하세요 코딩을 시작한지 얼마안된 초보입니다.
      위에서 idc_list에 변수를 추가할경우 dlc.cpp 에 변수가 추가되지만 식별자 IDC_List1이 정의되어 있지 않습니다라는 에러가 뜹니다.
      어떠

    • HS 2019.07.03 16:09

      소켓 프로그래밍 코딩을 해야하는데 제 지식이 아주 기초적이라서 서버 만드는 MFC창은 어떻게 띄우나요?

      그리고 C 로 작업을 해야하는데 비쥬얼 스투디오로 하는 방법이나 기초적인 도움 주실 수 있으신가요?

운동하는 개발자 JAY-JI