본문 바로가기

Robotics/Software Tech.

LAN을 위한 소켓 프로그래밍 #2

2.소켓 프로그래밍 클래스 CAsyncSocket

CAsyncSocket는 MFC로 소켓프로그래밍을 할때 가장 베이스 형태의 클래스입니다. 이클래스를 이용하여 소켓 프로그램을 작성할수가 있습니다. CAsyncSocket로 소켓 프로그래밍을 하는 방법은 그렇게 어렵지 않습니다. (과거 SDK로 프로그램 할때에 비해서) 본항목에서는 CAsyncSocket를 이용하여 소켓 프로그램을 작성하는 방법을 보겠습니다.

<1> CAsyncSocket을 이용하여 서버를 만들고 클라이언트를 만드는 OverView

서버는 소켓이 두 개 필요하고 클라이언트에서는 한 개만 필요합니다. 서버 영역에서 보면 제일 먼저 CAsyncSocket이라는 pSocket을 하나 설정합니다.
서버는 바로 정보를 제공하는 쪽입니다. 즉 “나는 이런 정보를 제공하겠다. 그리고 이 정보를 2000번 정보라고 설정하겠다”라고 해주면 2000번 정보가 필요한 클라이언트 컴퓨터가 연결하고자 할 것입니다. 이렇게 번호를 설정하는 것을 포트 번호라고 합니다. 보통 0번~100번 까지의 몇 개 번호는 많이 사용하는 프로토클에서 사용하였기 때문에 쓰지 않는것이좋습니다.
pSocket에 2000번을 포트 번호로 부여한 다음 pSocket->Create(2000)라고 실행하면 서버가 만들어집니다. 이렇게 서버가 만들어진 다음 pSocket->Listen() 하면 클라이언트가 접속하기를 기다려 줍니다. 클라이언트가 접속을 하면 새로운 CAsyncSocket을 만듭니다. 이렇게 해서 만들어진 Sock에 현재 접속하자고 들어온 클라이언트를 연결시켜 줍니다.

pSocket->Accept(&Sock);

왜 새로운 소켓을 만드는 것일까요? 서버와 클라이언트가 1대 1 통신이 아니라 1대 다 통신을 할 수 있기 때문입니다. 그렇다면 새로운 클라이언트가 들어오나 안 오나를 체크해야 하고 기존에 들어온 클라이언트와는 통신을 해야 하고 이렇게 하기에는 한 클래스로는 힘들다는 것입니다.
메인소켓은 클라이언트가 접근을 하나 안 하나를 기다리고 있다가 새로운 클라이언트가 들어오면 자식 소켓을 만들어서 “너는 지금 들어온 이 클라이언트와 통신을 해라!” 하고 일임한 후에 다시 새로운 클라이언트가 들어오는 것을 기다려야 하기 때문에 새로운 소켓을 만드는 것이죠.
이제 클라이언트와 연결된 모든 주권자는 Sock입니다. 이 Sock에 데이터가 들어오면

Sock->Receive(data,datalen); 해주어서 데이터를 받고

만약 보낼 데이터가 있으면 Sock->Send(data,datalen); 해주어서 클라이언트에 보내면 됩니다.
클라이언트 영역은 서버보다는 단순합니다. 우선 메인소켓을 하나 만들고

CAsyncSocket pSocket;

자신이 클라이언트라고 설정하여 만듭니다. 이 때는 pSocket->Create()를 하면 됩니다. 이제 서버와 연결을 해야겠지요. 2000번의 포트로 연결을 하고자 할 때 자신의 주소와 함께 포트 번호를 인자에 넣어 서버에 보냅니다. 만일 자신의 주소가 “120.130.32.2”이라고 할 경우

pSocket->Connect("120.130.32.2",2000);

이라고 하여 연결을 합니다. 연결이 끝난 다음 데이터를 받고 데이터를 주는 방법은 Receive와 Send 함수를 사용하면 됩니다. 이렇게 함으로써 네트워킹을 할 수 있게 되는 것이죠. 이것만 설명해도 프로그램하기는 쉽겠지요?
여기서 의문점이 없나요? 지금까지 설명에서 한 가지 의문점이 있을텐데...


<2>이벤트는 어떻게 알 수 있는가?

서버가 Listen() 함수를 실행시켜서 클라이언트를 기다리고 있을 때 클라이언트가 접속을 하면 어떻게 알 수가 있을까요? 또한 본 컴퓨터로 어떤 데이터가 들어왔을 때 어떻게 알 수가 있을까요?
여기에서 CAsyncSocket 클래스의 특정 멤버 함수를 볼까 합니다.
OnAccept : 클라이언트가 Connect를 실행하여 서버에 접속하고자 할 때 서버에서는 새로운 클라이언트가 들어오면  OnAccept 함수가 실행됩니다.
OnReceive : 새로운 데이터가 들어왔을 경우에는 OnReceive라는 함수가 실행됩니다.
즉 새로운 클라이언트가 접속을 실행하면 메시지를 날려 주는 것이 아니라 OnAccept 함수가 실행되는 것입니다. 또한 새로운 데이터가 들어왔을 경우에는 OnReceive 함수가 실행되고요.
그렇다면 어떻게 OnAccept 함수나 OnReceive 함수를 건드릴 수 있을까요? 이 때 객체 지향 언어의 특징인 over ride를 사용합니다.

CMySocket::public CAsyncSocket{
                     :
  public:
  virtual void OnAccept( int nErrorCode );
  virtual void OnReceive( int nErrorCode );
                     :
};

CAsyncSocket의 클래스를 상속받은 CMySocket에 virtual 함수로 OnAccept와 OnReceive 함수를 만들어 놓으면 새로운 클라이언트가 들어온 것을 알 수도 있으며 또한 새로운 데이터가 들어온 것을 알 수도 있지요.

void CMySocket::OnAccept(int nErrorCode)
{
 //이곳에서 연결하고자 하는 클라이언트가 왔을 때 새로운 연결을 해준다.
}

void CMySocket::OnReceive(int nErrorCode)
{
//이곳에서 데이터가 들어왔을 때 Receive 함수를 실행하거나 다른 방법으로 데이터를 받는다.
}

이와 같이 하여 통지(notification)을 받아서 실행할 수 있게 됩니다.

<3> 서버 클래스와 클라이언트 클래스 만들기

앞의 내용을 보면 소켓은 새로운 클래스를 만들고 상속을 받아서 사용하기가 편합니다. 아니 상속을 받아서 사용을 해야 하겠지요.
여기서는 소켓 프로그램을 이용할 때 단순히 연결하고 데이터를 보내고 받는 부분만을 알아보기로 합니다. 그렇다면 클라이언트 클래스는 어떤 일을 하면 될까요? 연결을 시도하고 나서는 데이터를 받고 보내는 일만 하겠지요. 그런 클라이언트 클래스를 CClientSocket로 만들었습니다.

/////////////////////////////////////////////////////////////////////////////
//clientsok.h
//클라이언트용 소켓 프로그램 헤더
#define WM_RECEIVE_DATA WM_USER+2

class CClientSock : public CAsyncSocket
{

public:
  CClientSock();
  //메시지를 전달할 HWND를 설정하는 함수
  void SetWnd(HWND hwnd);
  //새로운 데이터가 들어왔을 때 실행되는 함수
  virtual void OnReceive( int nErrorCode );
public:
  //메시지를 전달할 HWND
  HWND m_pHwnd;
};
/////////////////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////////////////
//Clientsok.cpp
//클라이언트용 소켓 클래스 함수
#include "stdafx.h"
#include "ClientSok.h"
//생성자는 아무일도 안 하고 모든 생성을 CAsyncSocket에 넘겨준다.
CClientSock::CClientSock()
{
 CAsyncSocket::CAsyncSocket();
}
//데이터가 들어왔을 때 현재 hwnd에 새로운 데이터가 들어
//왔다는 메시지를 날려 주기 위한 hwnd를 설정한다.
void CClientSock::SetWnd(HWND hwnd)
{
 m_pHwnd=hwnd;
}

//새로운 데이터가 들어왔을 때 현재의 hwnd에
//USER+2인 메시지 WM_RECEIVE를 보낸다.

void CClientSock::OnReceive( int nErrorCode )
{
  TRACE("Errocode = %d",nErrorCode);
  SendMessage(m_pHwnd,WM_RECEIVE_DATA,0,0);
}
/////////////////////////////////////////////////////////////////////////////

서버 클래스는 제일 먼저 클라언트를 기다린 다음 새로운 클라이언트가 오면 클라이언트와 같은 클래스를 만들어서 링크시켜 주면 되지요. 이런 내용의 클래스를 CSeverSocket으로 만들었습니다.

///////////////////////////////////////////////////////////////////
//sversok.h
//서버용 메인소켓 프로그램
#include "clientsok.h"
#define  WM_ACCEPT_SOCKET WM_USER+1

class CSeverSock : public CAsyncSocket
{

 public:
  CSeverSock();
  //메시지를 전달할 HWND 설정
  void SetWnd(HWND hwnd); 
  //현재 클라이언트와 연결된 소켓을 받는다.
  CClientSock* GetAcceptSocket();

  //새로운 클라이언트가 연결되었을 때
  virtual void OnAccept( int nErrorCode );

 public:
  //새로운 클라이언트가 연결되었을 때
  //Accept할 소켓 클래스 변수
  CClientSock  m_pChild;
  HWND m_pHwnd;
  
};
/////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////
//SverSok.cpp
//서버 클래스 함수 소스
#include "stdafx.h"
#include "sversok.h"
//생성자
CSeverSock::CSeverSock()
{
 CAsyncSocket::CAsyncSocket();
}
//메시지를 전달할 윈도우 설정
void CSeverSock::SetWnd(HWND hwnd)
{
 m_pHwnd=hwnd;
}

//새로운 클라이언트가 연결되었을 때
//실행되는 함수
void CSeverSock::OnAccept( int nErrorCode )
{
  TRACE("Errocode = %d",nErrorCode);
  //클라이언트 클래스인 m_pChild로 연결시켜 준다.
  Accept(m_pChild);
  //새로운 클라이언트가 연결되었다는
  //WM_ACCEPT_SOCKET 메시지를 전달한다.
  SendMessage(m_pHwnd,WM_ACCEPT_SOCKET,0,0);
}
//OnAccept 함수 실행 후 새로운 클라이언트와 연결된
//m_pChild 소켓을 넘겨준다.
CClientSock* CSeverSock::GetAcceptSocket()
{
 return &m_pChild;
}
/////////////////////////////////////////////////////////////////////////

<4> 서버 프로그램 Ex3101 만들기

이제 서버 프로그램을 만들어 보겠습니다. 서버 프로그램은 Ex3101입니다. 이 프로그램은 SDI로 CEx3101View가 CFormView 클래스로 설정되어 있습니다. 클라이언트에서 들어온 데이터가 보여지는 에디트 상자와 클라이언트에 보낼 데이터를 기록하는 에디트 상자 2개를 설정해 놓고 보낼 데이터를 기록하는 에디트 상자에 글자를 입력한 후 보내기 버튼을 눌러서 클라이언트에 데이터를 전송하는 프로그램입니다.
이 프로그램에서 위에서 만든 CSeverSocket와 CClientSocket 두 개를 사용했습니다. 뷰가 제일 먼저 업데이트될 때 다음과 같이 하여 클라이언트를 기다립니다.

void CEx3101View::OnInitialUpdate()
{
 CFormView::OnInitialUpdate();
 //서버 소켓을 만든다.
 m_pSeverSock = new CSeverSock;
 //서버 소켓의 현재 뷰의 hwnd를 설정한다.
 //이렇게 설정해 놓아야 현 뷰로 메시지가 전달됨.
 m_pSeverSock->SetWnd(this->m_hWnd);

 

 // 2000번 포트 번호로 소켓을 만든다.
 m_pSeverSock->Create(2000);
 //클라이언트가 오기를 기다린다.
 m_pSeverSock->Listen();
}

새로운 클라이언트가 들어올 때 CSeverSocket은 WM_ACCEPT_SOC- KET(WM_USER+1)인 메시지를 발생시킵니다. 이 메시지가 발생될 때 실행하는 함수 OnAccept가 있다고 한다면 우선 BEGIN_MESSAGE_MAP에 다음과 같이 설정합니다.

 ON_MESSAGE(WM_ACCEPT_SOCKET,OnAccept)

헤더에는 다음과 같이 설정합니다.

 afx_msg LONG OnAccept(UINT,LONG);

이렇게 설정을 하고 OnAccept 함수를 만듭니다.

//새로운 클라이언트가 접속했을 때
LONG CEx3101View::OnAccept(UINT wParam,LONG lParam)
{
 //새로운 클라이언트와 연결할 소켓을 만들고
 m_pChildSock = new CClientSock;
 //서버 소켓으로부터 현재 연결된 소켓을
 //받는다.
 m_pChildSock = m_pSeverSock->GetAcceptSocket();
 //hwnd 설정
 m_pChildSock->SetWnd(this->m_hWnd);
 char temp[200];
 //“안녕하세요”라는 데이터를 보낸다.
 sprintf(temp,"안녕하세요?");
 m_pChildSock->Send((LPSTR)temp,200);
  return TRUE;
}

위와 마찬가지로 새로운 데이터가 들어왔을 때 발생하는 메시지 WM_RECEIVE_ DATA와 연결될 함수 OnReceive를 만듭니다.

//새로운 데이터가 들어왔을 때
LONG CEx3101View::OnReceive(UINT wParam,LONG lParam)
{
 char temp[200];
 //소켓으로부터 데이터를 받은 다음
 m_pChildSock->Receive((LPSTR)temp,200);
 //화면과 연결된 m_strReceive에 설정한다.
 m_strReceive=temp;
 UpdateData(FALSE);
  return TRUE;
}


<5>프로그램 소스

(프로그램 소스시작)
// Ex3101Vw.h : interface of the CEx3101View class
//
///////////////////////////////////////////////////////////////////////////

class CEx3101View : public CFormView
{
protected: // create from serialization only
 CEx3101View();
 DECLARE_DYNCREATE(CEx3101View)

public:
 //{{AFX_DATA(CEx3101View)
 enum{ IDD = IDD_EX3101_FORM };
  // NOTE: the ClassWizard will add data members here
 //}}AFX_DATA

// Attributes
public:
 CEx3101Doc* GetDocument();
 CSeverSock *m_pSeverSock;
 CClientSock *m_pChildSock;
 CString m_strReceive;
 CString m_strSend;

// Operations
public:

// Overrides
 // ClassWizard generated virtual function overrides
 //{{AFX_VIRTUAL(CEx3101View)
 public:
 virtual BOOL PreCreateWindow(CREATESTRUCT& cs);
 virtual void OnInitialUpdate();

 protected:
 virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support
 virtual BOOL OnPreparePrinting(CPrintInfo* pInfo);
 virtual void OnBeginPrinting(CDC* pDC, CPrintInfo* pInfo);
 virtual void OnEndPrinting(CDC* pDC, CPrintInfo* pInfo);
 virtual void OnPrint(CDC* pDC, CPrintInfo*);
 //}}AFX_VIRTUAL

// Implementation
public:
 virtual ~CEx3101View();
#ifdef _DEBUG
 virtual void AssertValid() const;
 virtual void Dump(CDumpContext& dc) const;
#endif

protected:

// Generated message map functions
protected:
 //{{AFX_MSG(CEx3101View)
 afx_msg void OnSend();
 afx_msg LONG OnAccept(UINT,LONG);
 afx_msg LONG OnReceive(UINT,LONG);
 //}}AFX_MSG
 DECLARE_MESSAGE_MAP()
};

#ifndef _DEBUG  // debug version in Ex3101Vw.cpp
inline CEx3101Doc* CEx3101View::GetDocument()
   { return (CEx3101Doc*)m_pDocument; }
#endif

///////////////////////////////////////////////////////////////////////////
// Ex3101Vw.cpp : implementation of the CEx3101View class
//

#include "stdafx.h"
#include "Ex3101.h"

#include "Ex3101Dc.h"
#include "sversok.h"
#include "Ex3101Vw.h"


#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

///////////////////////////////////////////////////////////////////////////
// CEx3101View

IMPLEMENT_DYNCREATE(CEx3101View, CFormView)

BEGIN_MESSAGE_MAP(CEx3101View, CFormView)
 //{{AFX_MSG_MAP(CEx3101View)
 ON_BN_CLICKED(IDC_SEND, OnSend)
 //}}AFX_MSG_MAP
 // Standard printing commands
 ON_COMMAND(ID_FILE_PRINT, CFormView::OnFilePrint)
 ON_COMMAND(ID_FILE_PRINT_DIRECT, CFormView::OnFilePrint)
 ON_COMMAND(ID_FILE_PRINT_PREVIEW, CFormView::OnFilePrintPreview)
 ON_MESSAGE(WM_ACCEPT_SOCKET,OnAccept)
 ON_MESSAGE(WM_RECEIVE_DATA,OnReceive)
END_MESSAGE_MAP()

///////////////////////////////////////////////////////////////////////////
// CEx3101View construction/destruction

CEx3101View::CEx3101View()
 : CFormView(CEx3101View::IDD)
{
 //{{AFX_DATA_INIT(CEx3101View)
  // NOTE: the ClassWizard will add member initialization here
 //}}AFX_DATA_INIT
 // TODO: add construction code here

}

CEx3101View::~CEx3101View()
{
}

void CEx3101View::DoDataExchange(CDataExchange* pDX)
{
 CFormView::DoDataExchange(pDX);
 //{{AFX_DATA_MAP(CEx3101View)
 
 // NOTE: the ClassWizard will add DDX and DDV calls here
 //}}AFX_DATA_MAP
 DDX_Text(pDX,IDC_RECEIVE,m_strReceive);
 DDX_Text(pDX,IDC_SENDDATA,m_strSend);
}

BOOL CEx3101View::PreCreateWindow(CREATESTRUCT& cs)
{
 // TODO: Modify the Window class or styles here by modifying
 //  the CREATESTRUCT cs

 return CFormView::PreCreateWindow(cs);
}

///////////////////////////////////////////////////////////////////////////
// CEx3101View printing

BOOL CEx3101View::OnPreparePrinting(CPrintInfo* pInfo)
{
 // default preparation
 return DoPreparePrinting(pInfo);
}

void CEx3101View::OnBeginPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
 // TODO: add extra initialization before printing
}

void CEx3101View::OnEndPrinting(CDC* /*pDC*/, CPrintInfo* /*pInfo*/)
{
 // TODO: add cleanup after printing
}

void CEx3101View::OnPrint(CDC* pDC, CPrintInfo*)
{
 // TODO: add code to print the controls
}

///////////////////////////////////////////////////////////////////////////
// CEx3101View diagnostics

#ifdef _DEBUG
void CEx3101View::AssertValid() const

{
 CFormView::AssertValid();
}

void CEx3101View::Dump(CDumpContext& dc) const
{
 CFormView::Dump(dc);
}

CEx3101Doc* CEx3101View::GetDocument() // non-debug version is inline
{
 ASSERT(m_pDocument->IsKindOf(RUNTIME_CLASS(CEx3101Doc)));
 return (CEx3101Doc*)m_pDocument;
}
#endif //_DEBUG

///////////////////////////////////////////////////////////////////////////
// CEx3101View message handlers
//새로운 클라이언트가 접속했을 때
LONG CEx3101View::OnAccept(UINT wParam,LONG lParam)
{
 //새로운 클라이언트와 연결할 소켓을 만들고
 m_pChildSock = new CClientSock;
 //서버 소켓으로부터 현재 연결된 소켓을 받는다.
 m_pChildSock = m_pSeverSock->GetAcceptSocket();
 //hwnd 설정
 m_pChildSock->SetWnd(this->m_hWnd);
 char temp[200];
 //“안녕하세요”라는 데이터를 보낸다.
 sprintf(temp,"안녕하세요?");
 m_pChildSock->Send((LPSTR)temp,200);
  return TRUE;
}
//새로운 데이터가 들어왔을 때
LONG CEx3101View::OnReceive(UINT wParam,LONG lParam)
{
 char temp[200];
 //소켓으로부터 데이터를 받은 다음
 m_pChildSock->Receive((LPSTR)temp,200);
 //화면과 연결된 m_strReceive에 설정한다.
 m_strReceive=temp;
 UpdateData(FALSE);
  return TRUE;
}

//데이터 보내기 버튼을 눌렀을 때
void CEx3101View::OnSend()
{
 UpdateData(TRUE);
 //에디트에 입력한 데이터를 받아서
 char temp[200];
 lstrcpy((LPSTR)temp,(LPSTR)m_strSend.operator const char*());
 //클라이언트에 보낸다.
 m_pChildSock->Send(temp,200);
}

void CEx3101View::OnInitialUpdate()
{
 CFormView::OnInitialUpdate();
 //서버 소켓을 만든다.
 m_pSeverSock = new CSeverSock;
 //서버 소켓의 현재 뷰의 hwnd를 설정한다.
 //이렇게 설정해 놓아야 현 뷰로 메시지가 전달됨.
 m_pSeverSock->SetWnd(this->m_hWnd);
 // 2000번 포트 번호로 소켓을 만든다.
 m_pSeverSock->Create(2000);
 //클라이언트가 오기를 기다린다.
 m_pSeverSock->Listen();
}

'Robotics > Software Tech.' 카테고리의 다른 글

MFC Tip  (0) 2007.11.21
네트워크 영상/음성 전송 프로그램  (0) 2007.11.20
LAN을 위한 소켓 프로그래밍 #1  (0) 2007.11.17
[MFC]Log파일 만들기  (0) 2007.10.22
for문의 스코프 문제  (0) 2007.09.29