4부. 중급 네트워크 프로그래밍 Ⅱ: 윈도우
GUI 기반의 네트워크 프로그래밍
스레드 사용 방법
Win32를 이용한 윈속 프로그래밍
윈도우의 기반의 다중 접속 처리 서버 구현
10장. MFC 윈도우 네트워크 프로그래밍
MFC의 소켓 기능은 윈도우에서 제공되는 막강한 소켓 라이브러리인 윈속 API(Winsock API)를 좀더 편리하고 안전하게 사용할 수 있게 만든 클래스 라이브러리다.
01. 간단한 MFC CSocket TCP 프로그램
. Win32 윈속 API를 사용해서 구현하는 방법
. 비주얼 C++의 MFC 라이브러리를 사용하는 방법
MFC에는 소켓 기능을 담당하는 클래스
. CSocket
. CAsyncSocket
CAsyncSocket의 멤버 함수 구현은 대부분 윈속 API의 함수를그대로 호출
CSocket은 부모 CAsyncSocket의 멤버 함수를 재정의해서 좀더 사용하기 쉽게 되어 있다.
Conect()
Send()
Receive()
Close()
그런데 두 클래스에서 Connect, Send, Receive 등의 함수 동작은 서로 다르다. 바로 이 차이점을 이해하는 것이 CAsyncSocket과 CSocket을 이해하는 핵심이다.
| 간단한 MFC CSocket TCP 클라이언트 프로그램
1. [파일]->[New]
[Project]-"MFC AppWizard(exe)"
2. "Dialog based"
3. "Windows Socket"
[캡션 바]-"TcpClient"
4. "MFC 라이브러리 링크 설정하기“-‘As a statically linked library'
1. 작업공간 창
2. 소스 창
3. 출력 창
StdAfx.h
#include <afxsock.h> // MFC socket extensions
TcpClient.h
class CTcpClientApp : public CWinApp
{
public:
CTcpClientApp();
public:
virtual BOOL InitInstance();
DECLARE_MESSAGE_MAP()
};
TcpClient.cpp
CTcpClientApp::CTcpClientApp(){}
CTcpClientApp theApp;
BOOL CTcpClientApp::InitInstance()
{
if (!AfxSocketInit()) {}
}
TcpClient.rc
리소스에 대한 정보를 담고 있는 리소스 파일
대화상자 기반 애플리케이션에서 AppWizard에 의해서 생성된 기본 창을 일반적으로 ‘메인 대화상자 창’이라고 부른다.
TcpClientDlg.cpp 는 메인 대화상자 창을 정의하는 CTcpClientDlg 클래스를 담고 있다. 메인 대화상자 창에 접속을 위한 버튼이나 소켓으로 메시지를 전송하는 버튼 등을 놓고 그 버튼이 눌리면 처리할 소켓 동작에 관련된 코드를 이 파일에 작성
CTcpClientDlg 클래스는 대화상자 창의 기본 동작이나 모양을 처리하는 CDialog라는 MFC 클래스에서 상속받았다. 버튼이 눌렸을 때라든지 정적(static) 컨트롤을 변경
TCP 클라이언트는 서버에 접속 - 소켓 객체
TcpClientDlg.h
class CTcpClientDlg : public CDialog
{
public:
CSocket m_Socket;
CTcpClientDlg(CWnd* pParent = NULL); // standard constructor
서버에 접속
CSocket - Connect, <접속> 버튼을 클릭하면 이 함수가 호출
1. ‘WorkSpace 바’ - [ResourceView] - 'Dialog' - 'IDD_TCPCLIENT_DIALOG'
2. 'Control 바‘ : “버튼” - ’드래그 앤 드롭‘ - ’Properties' - "IDC_CONNECT"
'Caption' : ‘접속’
사용자가 버튼을 눌렀을 때 동작하는 코드
1. 대화상자 위의 <접속> - [ClassWizard]
2. 'Objects ID' : ‘IDC_CONNECT' - 선택
'Messages' : 'BN_Clicked' - 선택
<Add Function> - 버튼 클릭
OnConnect라는 함수명이 입력, CTcpClientDlg의 멤버 함수
3. 멤버 함수 추가 확인 창에서 <OK> 버튼을 클릭
‘ClassWizard' - ’Member functions' -
[W]OnConect ON_IDC_CONNECT:BN_CLICKED'가 추가된 것을 확인
4. ‘[W]OnConnect..' 항목을 더블클릭
CTcpClientDlg::OnConnect 함수에 코드 작성
void CTcpClientDlg::OnConnect() {
if( m_Socket.Create() == FALSE ) {}
if( m_Socket.Connect( "localhost", 6565 ) == FALSE ) {}
}
서버에 “hello"라고 메시지를 보내는 <보내기>버튼
1. ‘Controls 바’ - 대화상자
2. ‘IDC_SEND' - "보내기“
ID |
이름 |
종류 |
IDC_CONNECT |
접속 |
버튼 |
IDC_SEND |
보내기 |
버튼 |
IDC_RECV |
받기 |
버튼 |
IDC_CLOSE |
끊기 |
버튼 |
3. CTcpClientDlg
OnConnect, OnSend, OnRecv, OnClose
<보내기>, <받기>, <끊기>
OnSend 함수
void CTcpClientDlg::OnSend() {
m_Socket.Send( "hello", 6 );
}
OnRecv 함수
void CTcpClientDlg::OnRecv() {
char buf[100];
ZeroMemory( buf, 100);
m_Socket.Receive( buf, 100 );
AfxMessageBox( buf );
}
OnClose 함수
void CTcpClientDlg::OnClose() {
m_Socket.Close();
}
Send 함수로 “hello"를 보내는데 5바이트를 보내지 않고 6바이트를 보냈다는 것이다.
“hello" 문자열 끝에는 널 문자 ‘\n'
유용한 단축키인 <Ctrl>+<Tab> |
| 간단한 MFC CSocket TCP 서버 프로그램
1. ‘MFC AppWizard (exe)' - 'TcpServer'
2. 대화상자를 기초로 창을 만든다. ‘Windows Sockets'를 선택하고 프로젝트 생성
StdAfx.h 파일 안에 추가
#include <afxsock.h>
TcpServer.cpp - 코드 추가
BOOL CTcpServerApp::InitInstance() {
if (!AfxSocketInit()) {
AfxMessageBox(IDP_SOCKETS_INIT_FAILED);
return FALSE;
3. 버튼
<리슨>, <억셉트>
4. ‘WorkSpace 바’의 ‘ResourceView' 항목에서 ’IDD_TCPSERVER_DIALOG'를 더블클릭
ID |
이름 |
종류 |
IDC_LISTEN |
리슨 |
버튼 |
IDC_ACCEPT |
업셉트 |
버튼 |
IDC_SEND |
보내기 |
버튼 |
IDC_RECV |
받기 |
버튼 |
IDC_CLOSE |
끊기 |
버튼 |
5. CTcpServerDlg의 멤버 함수로 해당 버튼의 이벤트 핸들링 함수
OnListen, OnAccept, OnSend, OnRecv, OnClose
'리슨‘ 소켓
1. TcpServerDlg.h에 ‘리슨’을 담당하는 소켓 객체
2. 클라이언트 소켓 - m_Client
class CAboutDlg : public CDialog {
CTcpServerDlg(CWnd* pParent = NULL);
CSocket m_Listen;
CSocket m_Client;
3. TcpServerDlg.cpp
OnListen 함수
void CTcpServerDlg::OnListen() {
if( !m_Listen.Create(6565) ) {}
if( !m_Listen.Listen() ) {}
}
OnAccept 함수
void CTcpServerDlg::OnAccept() {
if( m_Listen.Accept( m_Client ) ) {}
}
OnSend 함수
void CTcpServerDlg::OnSend() {
m_Client.Send( "world", 6);
}
OnRecv 함수
void CTcpServerDlg::OnRecv() {
char buf[100];
ZeroMemory( buf, 100);
m_Client.Receive( buf, 100 );
AfxMessageBox( buf );
}
OnClose 함수
void CTcpServerDlg::OnClose() {
m_Client.Close();
}
02. 제대로 된 MFC CSocket TCP 프로그램
‘멈춤 현상’
CSocket
OnAccept, OnReceive, OnClose
| 제대로 된 MFC CSocket TCP 클라이언트 프로그램
1. 'TcpClient2'
2. 대화상자 기반 , 'Windows Sockets' 옵션 사용
CSocket을 상속
[Insert]-[New Class]
Base class : CSocket
ClassWizard를 사용해서 오버라이드
1. <Ctrl>+<W> or [View]-[ClassWizard]
2. CClientSocket에 OnReceive, OnClose 오버라이드 - <Add Function>
OnReceive 함수 |
void CClientSocket::OnReceive(int nErrorCode) { char buf[100]; ZeroMemory( buf, 100); Receive( buf, 100 ); AfxMessageBox( buf );
CSocket::OnReceive(nErrorCode); } |
OnClose 함수 |
void CClientSocket::OnClose(int nErrorCode) { Close(); AfxMessageBox( "접속이 종료되었음" );
CSocket::OnClose(nErrorCode); } |
CClientSocket에서 서버로 접속하는 코드나 데이터를 보내는 코드
1. Resource - IDD_TCPCLIENT2_DIALOG
<접속>, <보내기>, <끊기>
2.
IDC_CONNECT |
접속 |
버튼 |
IDC_SEND |
보내기 |
버튼 |
IDC_CLOSE |
끊기 |
버튼 |
3. CTcpClient2Dlg
#include "ClientSocket.h"
class CTcpClient_2Dlg : public CDialog { public: CTcpClient_2Dlg(CWnd* pParent = NULL); // standard constructor
CClientSocket m_Socket; |
4. 메시지 핸들링 함수
IDC_CONNECT - onConnect
IDC_SEND - OnSend
IDC_CLOSE - OnClose
OnConnect 함수 |
void CTcpClient_2Dlg::OnConnect() { if( m_Socket.Create() == FALSE ) {} if( m_Socket.Connect( "localhost", 6565 ) == FALSE ) {} } |
OnSend 함수 |
void CTcpClient_2Dlg::OnSend() { m_Socket.Send( "hello", 5); } |
OnClose 함수 |
void CTcpClient_2Dlg::OnClose() { m_Socket.Close(); } |
| 제대로 된 MFC CSocket TCP 서버 프로그램
'MFC AppWiard (exe)' - 'TcpServer2'
대화상자 기반의 ‘Windows Sockets'
1. 클라이언트로부터 들어오는 역결을 받아들이기 위한 소켓 - Listen 함수를 호출해서 리슨하고 있다가 접속 시도가 들어오면 OnAccept 함수가 호출
2. 클라이언트 담당 소켓
- 클라이언트에서 데이터가 도착하면 OnReceive 함수가 호출
- OnClose 함수를 오버라이드하면 해당 클라이언트와 접속이 끊어쪘음을 알 수 있다.
CSocket을 상속받아 CListenSocket과 CClientSocket
1. CListenSocket은 서버에서 리슨을 담당하며 새로운 클라이언트가 접속해오면 오버라이드된 OnAccept 함수를 호출시킨다. 그러면 이 함수는 새로운 클라이언트와 통신을 담당하는 CClientSocket의 객체로 Accept시키는 일을 한다.
2. CClientSocket 클래스는 OnReceive 함수를 오버라이드해서 클라이언트로부터 데이터가 언제 도착했는지 알 수 있고, 그 클라이언트와 연결이 종료되면 OnClose 함수를 오버라이드해서 데이터가 언제 도착했는지를 알 수 있게 된다.
클래스 생성
1. CListenSocket, CClientSocket - CSocket 클래스 상속
2. CListenSocket의 멤버 함수 중 OnAccept 함수 오버라이드
3.
#include "ClientSocket.h"
class CListenSocket : public CSocket { public: CClientSocket m_Client; |
4.
OnAccept 함수 |
void CListenSocket::OnAccept(int nErrorCode) { if( Accept( m_Client ) ) {} CSocket::OnAccept(nErrorCode); } |
Accept 함수의 인자로 들어가는 m_Client는 접속된 클라이언트를 담당할 전용 클라이언트 소켓의 객체다.
클라이언트와 통신을 담당하는 CClientSocket
OnReceive
OnClose
OnReceive 함수 |
void CClientSocket::OnReceive(int nErrorCode) { char buf[100]; ZeroMemory( buf, 100 ); Receive( buf, 100 ); AfxMessageBox( buf );
CSocket::OnReceive(nErrorCode); } |
OnClose 함수 |
void CClientSocket::OnClose(int nErrorCode) { Close(); AfxMessageBox( "접속이 종료되었음" );
CSocket::OnClose(nErrorCode); } |
여기서 OnReceive에서 Receive 함수를 한 번만 호출했다는 것을 확인한 후 지나가자, 이와 관련된 내용은 CAsyncSocket을 설명하면서 보충하겠다.
1. Resource - IDD_TCPSERVER2_DIALOG
IDC_LISTEN |
리슨 |
버튼 |
IDC_SEND |
보내기 |
버튼 |
IDC_CLOSE |
끊기 |
버튼 |
2.
OnListen 함수 |
void CTcpServer_2Dlg::OnListen() { if( !m_Listen.Create(6565) ) {} if( !m_Listen.Listen() ) {} } |
OnSend 함수 |
void CTcpServer_2Dlg::OnSend() { m_Listen.m_Client.Send( "world", 6); } |
OnClose 함수 |
void CTcpServer_2Dlg::OnClose() { m_Listen.m_Client.Close(); } |
3. CListenSocket
#include "ListenSocket.h"
class CTcpServer_2Dlg : public CDialog { public: CTcpServer_2Dlg(CWnd* pParent = NULL); // standard constructor CListenSocket m_Listen; |
프 로그램의 멈춤 현상이 없이 네트워크 이벤트를 바로 바로 처리할 수 있게 되었다. 즉, 클라이언트에서 새로운 연결이 시도되면 서버는 이를 바로 알아채서 클라이언트 담당 소켓으로 연결시킨다. 그리고 클라이언트에서 데이터를 보내게 되면 서버가 이를 알아채서 데이터를 바로 받게 된다. 물론, 접속이 종료되어도 자동적으로 상대편에서도 연결이 끊어짐을 알 수 있어 Close 함수로 소켓을 적절하게 닫을 수 있게 되었다.
간단한 테스트 코드를 짜야 할 때가 많다. CSocket 상속
CSocket을 사용한 간단한 웹 HTML 소스 보기
CSocket만을 사용해서 웹 서버로부터 웹 HTML 파일의 소스를 간단하게 얻어오기
void CWebSourceDlg::OnButton1() { CSocket s;
if( !s.Create() ) {}
if( !s.Connect( "www.empas.com", 80 ) ) {}
char* cmd = "GET / HTTP/1.0\r\n\r\n";
s.Send( cmd, strlen( cmd ) );
char buf[MAX_BUF]; ZeroMemory( buf, MAX_BUF );
while( s.Receive( buf, MAX_BUF ) ) { AfxMessageBox( buf );
ZeroMemory( buf, MAX_BUF ); } } |
웹 서버는 일반적으로 80번 포트에서 대기하기 때문에 접속하려는 주소와 함께 80번 포트로 접속을 시도해보면 된다. 그런 후 받으려는 파일의 경로를 GET 명령어에 포함시켜 보내면 해당 파일의냉ㅇ을 웹 서버가 내려주게 된다.
HTTP 1.0 GET 명령 형식
상대 URL의 경우 /로 넣을 경우, 해당 메인 index HTML 파일을 가리키는 것과 같다. 예를 들어, ‘엠파스’의 서비스 약관의 절대 URL 경로가 다음과 같다고 한다.
http://login.empas.com/info/rule.html
그러면 접속해야 할 주소는 login.empas.com이 되는 것이고, 접속 후 웹 서버로 전송해야 할 명령어는 다음과 같이 하면 된다.
char* cmd = "GET /into/rule.html HTTP/1.0\r\n\r\n";
CScoket의 장점과 한계
1. 데이터를 보낼 때 많은 양의 데이터를 보내더라도 CSocket이 알아서 다 보내준다.
CSocket::Send - CSocket::SendCheck
CAsyncSocket - WSAEWOULDBLOCK
2. Receive 함수로 데이터를 받을 때에도 CSocket은 WSAEWOULDBLOCK 오류가 발생되지 않는다.
3. 접속을 시도하는 Connect 함수의 경우에도 접속이 될 때까지 Conect 함수의 다음 코드로 진행이 이뤄지지 않고 기다리게 된다. 따라서 프로그래머는 접속 성공 또는 실패를 Connect 리턴 값으로 쉽게 알 수 있다.
CSocket의 OnMessagePendding 함수를 오버라이드함으로써 Connection에 대한 타임아웃을 설정할 수 있다. 그렇지만 잠시 동안 멈춰 있는 현상은 어쩔 수 없는 단점이다.
참고 : CSocket Connection Time out
CSocket에서 Connect 함수나 Receive 함수 또는 Send 함수가 호출되는 도중에 윈도우와 관련된 메시지가 발생되면 OnMessagePending 함수가 자동으로 호출된다.
클라이언트에서 서버로 접속하기위해서 CSocket을 상속받은 소켓 클라이언트를 CClientSocket이라고 가정한다면 헤더부에 다음처럼 ConnectTimeout 함수를 추가한다.
class CClientSocket : public CSocket {
public:
BOOL ConnectTimeout( LPCTSTR lpszHostAddress,
UINT nHostPort,
DWORD dwTimeout );
Message - 'OnMessagePending'
ConnectTimeout 함수 |
BOOL CClientSocket::ConnectTimeout( LPCTSTR lpszHostAddress, UINT nHostPort, DWORD dwTimeout) { BOOL ret; AfxGetMainWnd()->SetTimer( 65, dwTimeout, NULL ); ret = Connect( lpszHostAddress, nHostPort ); AfxGetMainWnd()->KillTimer( 65 ); return ret; } |
OnMessagePending 함수 |
BOOL CClientSocket::OnMessagePending() { MSG Message; if( ::PeekMessage( &Message, NULL, WM_TIMER, WM_TIMER, PM_NOREMOVE)) { if( Message.wParam == 65 ) { ::PeekMessage( &Message, NULL, WM_TIMER, WM_TIMER, PM_REMOVE ); CancelBlockingCall();
AfxGetMainWnd()->KillTimer( 65 ); } } return CSocket::OnMessagePending(); } |
위와 같이 작성한 후 ConnectTimeout( "www.empas.com", 2000);
1. SetTimer
2. AfxGetMainWnd
3. Connect - WM_TIMER
- OnMessagePending
4. OnMessagePending
Connect - CancelBlockingCall
5. '메시지 큐‘
PeekMessage - WM_TIMER
65번 타이머라면 타임아웃으로 인식하고 PeekMessage를 PM_REMOVE 인자와 함께 호출해서 해당 메시지는 메시지 큐에서 제거한 후 현재의 Connect라는 블로킹 호출을 취소하게 했다.
03. MFC CAsyncSocket TCP 프로그램
CAsyncSocket은 Winsock 함수들을 그대로 호출하는 클래스다.
| MFC CAsyncSocket TCP 클라이언트 프로그램
먼저 CAsyncSocket은 비동기 소켓이라는 점을 인식해야 한다.
Connect - OnConnect
OnConnect 함수의 인자를 통해서 이를 꼭 확인해봐야 한다.
Connect 함수를 호출하면 Connect 함수는 0을 리턴하며, GetLastError로 값을 보면 WSAEWOULDBLOCK이 나오게 된다. GetLastError 함수는 최근에 발생된 오류를 리턴하는 CAsyncSocket의 멤버 함수다.
CAsyncSocket::GetLastError - 윈속 API 중 WSAGetLastError 함수를 호출하도록 구현되어 있다.
CAsyncSocket을 상속받은 CMyAsyncSocket 클래스를 정의
if( m_Client.Connect( "localhost", 6565 ) == 0 ) { if( m_Client.GetLastError() != WSAWOULDBLOCK ) { AfxMessageBox( "소켓 접속 실패!“ ); return; } } |
void CMyAsyncSocket::OnConnect( int nErrorCode ) { if( nErrorCode == 0 ) AfxMessageBox( "접속 성공“ ); else AfxMessageBox( "접속 실패“ ); CAsncSocket::OnConnect( nErrorCode ); } |
접속 실패가 일어나면 인자인 nErrorCode를 통해서 해당 네트워크 오류 값을 확인하면 된다. 접속이 성공되었다면 nErrorCode 값은 0을 갖게 된다.
또 한 비동기 소켓이기 때문에 Send 함수를 호출하면 한번에 보낼 수 있는 양만큼만 보낸 후 그냥 바로 리턴해 버린다. 따라서 CAsyncSocket에서는 보내고자 하는 데이터를 기억ㅤㅎㅐㄷ었다가 Send 함수를 한번 호출해보고 리턴되는 값으로 실제 보내진 양을 체크한 후 다음 보내야 할 데이터를 다시 보내야 한다. 약 500바이트 이하의 데이터를 보낼 때는 보통 한 번에 가게 되지만 그 이상이 되면 네트워크 상황에 따라서 한 번에 보낼 수 없다는 것을 기억하자.
또 다른 중요한 잊지 말아야 할 사항은 CAsyncSocket에서는 데이터가 도착했을 때 호출되는 OnReceive 함수 안에서 Receive 함수를 한 번만 호출해야 한다는 것이다.
MSDN 'Q185728 SAMPLE: MFCSocs.exe Avoids Two Common MFC Socket Mistakes'
CAsyncSocket::Receive - WSAEWOULDBLOCK
CSocket::Send - WSAEWOULDBLOCK 처리도 하면서 보낼 데이터를 계속 보내기 위해서 CAsyncSocket::Send를 호출하는 CSocket::SendChunk 함수의 루프로 구현되어 있다.
CAsyncSocket은 클라이언트 접속이 들어왔는지 그리고 받은데이터가 있는지 등의 네트워크 이벤트를 받기 위해서 WSAAsyncSelect라는 윈속 API를 이용해서 구현되어 있다.
WSAAsyncSelect는 윈도우의 메시지 처리 방식을 이용해서 네트워크 이벤트를 소켓에게 전달하는 함수다. 그리고 이 함수는 소켓을비블로킹 모드로 만드는 역할을 한다.
윈속 API에서 소켓은 socket 함수나 WSASocket 함수를 통해서 생성된다. 일단 생성되면 소켓은 블로킹 모드로 동작하게 된다.
connect - WSAConnect 블록킹 모드
send - WSASend 함수를 호출할 경우 내부 전송 버퍼가 현재 사용할 수 없으면 멈춰 있게 된다. 비블록킹 모드라면 이 경우 WSAEWOULDBLOCK 오류가 발생했을 것이다.
TCP 소켓은 TCP 스택 안에 내부 전송 버퍼가 존재한다.
send buffer
receive buffer
WSAEWOULDBLOCK
"TCP 스택에 데이터가 도착했다“
FD_READ
MFC의 CSocket이나 CAsyncSocket은 OnReceive 함수를 호출한다. 이 함수를 오버라이드해서 데이터가 도착했는지 알 수 있었던 것이다.
그 런데 Winsock은 한번의 FD_READ 이벤트에 대해 여러 번의 Receive 함수를 호출해서는 안 된다. 빠르게 데이터가 전송되는 상황에서 한번의 OnReceive 함수 안에서 여러 번의 Receive 함수를 호출하게 되면 FD_READ 이벤트를 읽거나, 읽어갈 데이터가 있음에도 FD_READ가 발생되지 않을 수 있기 때문이다.
그 리고 마지막으로 Receive 함수를 호출할 때 꼭 리턴 값을 확인해서 SOCK_ERROR가 리턴된다면 CAsyncSocker::GetLastError를 호출해야 한다. 그리고 유류 값이 WSAEWOULDBLOCK이면 단지 현재 네트워크 리소스를 사용할 수 없다는 말이므로 소켓을 종료하지 말고 그냥 리턴해버리면 나중에 읽어갈 수 있을 때 OnReceive 함수가 다시 호출된다. 이처럼 소켓 프로그래밍에서는 어느 상황에서든 오류 값을 확인하는 습관을 가져야 한다.
CAsyncSocket에서는 OnSend의 의미도 제대로 파악하고 있어야 한다
CAsyncSocket 의 Send 함수를 호출할 때 꼭 리턴 값을 확인해서 SOCKET_ERROR인지 확인하고 만약, SOCKET_ERROR라면 CAsyncSocket::GetLastError를 체크해서 WSAEWOULDBLOCK이면 다음 번에 데이터를 보낼 수 있는 순간에 그때 보내야 할 데이터들을 순서대로 보내야 한다.
데이터를 보낼 수 있는 상황이 되면 내부적으로 네트워크 이벤트 FD_WRITE가 한 번 발생된다. MFC CAsyncSocket에서는 이 때 OnSend 함수가 호출된다.
보내야 할 데이터가 있다면 일단 보내려는 데이터를 일괄적으로 관리하는 버퍼에 추가한 후 쌓여있는 보낼 버퍼를 일괄적으로 CAsyncSocket::Send를 계속 호출해서 보낼 버퍼에서 없앤다.
만 약, 보내는 도중 리턴 값이 SOCK_ERROR이며 CAsyncSocket::GetLastError가 WSAEWOULDBLOCK으로보낼 수 없는 상황이 된다면 계속 보낼 버퍼에 쌓기만 하다가 보낼 수 있는 상황이 되면 OnSend 함수가 호출될 것이고, 이 함수 안에서 그 동안 쌓인 보낼 버퍼를 또 다시 CAsyncSocket::Send 함수로 없애는 것이다. 보통 이런 보낼 버퍼는 배열이나 리스트로 구현할 수 있다.
CAsyncSocket 클라이언트
1. ‘TcpCAsyncClient' - 'MFC AppWizard (exe)'
2. 대화상자 기반, Windows Sockets 옵션
3. [Insert]->[New Class]
'Base Class' : CAsyncSocket
4. CMyAsycSocket : ClassWizard - OnConnect, OnSend, OnReceive
OnConnect의 구현
Connect 호출 후 실제 접속 성공 결과에 대해서는 내부 네트워크 이벤트인 FD_CONNECT가 발생된 후 호출되는 OnConnect에서 알 수 있다.
0 : 접속
void CMyAsyncSocket::OnConnect(int nErrorCode) { if( nErrorCode == 0 ) { AfxMessageBox( "접속 성공" ); } else { int err = GetLastError(); AfxMessageBox( "접속 실패" ); }
CAsyncSocket::OnConnect(nErrorCode); } |
OnReceive의 구현
CMyAsyncSocket은 OnReceive 안에서 Receive 함수를 한 번만 호출
Receive 함수로 실제 읽어온 데이터 길이를 의미하는 리턴 값은 세 가지 경우로 판단
1. 0이면 연결이 끊어졌을 의미하므로 Close 함수를 호출한다.
2. SOCKET_ERROR라면 다시 GetLastError 또는 ::WSAGetLastError를 호출해서 실제 네트워크 오류 값을 살펴봐야 한다. WSAEWOULDBLOCK이면 현재 잠시 동안 Receive 할 수 없다는 얘기일 뿐이므로 그냥 OnReceive 함수에서 빠져나오면 된다. FD_READ가 발생되어 OnReceive가 다시 호출. WSAEWOULDBLOCK이 아닌 다른 값이면, Close 함수를 호출해서 역시 소켓을 닫아주면 된다.
3. Receive 함수가 실제 읽어온 데이터의 길이인 경우이면 CMyAscSocket에서는 그냥 문자열을 받았다는 의미로 AfxMessageBox 함수를 통해 메시지 박스를 출력하게 된다.
void CMyAsyncSocket::OnReceive(int nErrorCode) { char buf[4096]; ZeroMemory( buf, 4096 ); int nRead = Receive( buf, 4096 );
if( nRead == 0 ) // 연결이 끊어졌다. Close(); else if( nRead == SOCKET_ERROR ) { int nErr = GetLastError(); // 네트워크 리소스를 잠시 사용 못하는 경우가 아닌 모든 오류시 연결을 끊는다. if( nErr != WSAEWOULDBLOCK ) Close(); } else { // 데이터를 받았다. AfxMessageBox( buf ); }
CAsyncSocket::OnReceive(nErrorCode); } |
Receive : 버퍼의 크기 4Kbyte(4096byte) 또는 8Kbyte(8192byte)
최대한 크게 받기를 시도해서 TCP 스택의 Receive 버퍼에 쌓여있는 데이터를 한번에 많이 받아보자는 것이다.
OnSend의 구현
데 이터를 보내기 버퍼에 담아 두게 되면 이 데이터를 루프로 계속 보내는 DoSendBuf 함수에 의해서 일괄적으로 버퍼를비울 수 있다. 만일, 보내다가 보낼 수 없는 상황(WSAEWOULDBLOCK)이 된다면 나중에 보낼 수 있는 상황을 알리는 OnSend 함수가 호출되고 OnSend 함수 안에서도 역시 DoSendBuf 함수를 호출하게 했다.
MyAsyncSocket의 헤더
class CMyAsyncSocket : public CAsyncSocket { public: CMyAsyncSocket(); virtual ~CMyAsyncSocket();
char m_SendBuf[4096]; // 보낼 데이터 버퍼 char* m_pSendCurBuf; // 보내야 할 버퍼의 현재 위치 int m_nSendLeft; // 보내기 위해 나은 길이 BOOL AsyncSend( char* pBuf, int nLen ); void DoSendBuf(); |
AsyncSend 함수
CMyAsyncSocket::CMyAsyncSocket() { m_nSendLeft = 0; m_pSendCurBuf = m_SendBuf; } |
BOOL CMyAsyncSocket::AsyncSend( char* pBuf, int nLen ) { // 현재 이 버전은 보낼 데이터를 버퍼 큐로 쌓아 놓지 않는다. if( m_nSendLeft != 0) { AfxMessageBox( "아직 보내야 할 데이터를 다 보내지 못했습니다." ); return FALSE; }
// 현재 버전은 고정 크기의 버퍼에 담아 보내기 때문에 버퍼 크기보다 큰 데이터를 한번에 보낼 수 없다. if( nLen > 4096 ) { AfxMessageBox( "버퍼 크기보다 큰 데이터를 보낼 수 없습니다." ); return FALSE; }
// 보낼 데이터를 보내기 버퍼에 복사한다. memcpy( m_SendBuf, pBuf, nLen ); m_pSendCurBuf = m_SendBuf; m_nSendLeft = nLen;
// 버퍼 보내기를 시도한다. DoSendBuf();
return TRUE; } |
DoSendBuf 함수
void CMyAsyncSocket::DoSendBuf() { while( m_nSendLeft > 0 ) { int nSend;
nSend = Send( m_pSendCurBuf, m_nSendLeft );
if( nSend == SOCKET_ERROR ) { int nErr = GetLastError(); if( nErr == WSAEWOULDBLOCK ) break; else { Close(); m_nSendLeft = 0; AfxMessageBox( "보내기 실패, 연결을 끊습니다." ); return; } } else { // 보내기 성공으로 현재 보내야 할 위치와 남은 양을 변경합니다. m_pSendCurBuf += nSend; m_nSendLeft -= nSend; } } } |
다음은 OnSend 함수의 코드다. OnSend 함수에서는 보내야 할 나머지 데이터를 모두 보내는 DoSendBuf 함수를 호출함으로써 끝난다.
OnSend 함수
void CMyAsyncSocket::OnSend(int nErrorCode) { DoSendBuf();
CAsyncSocket::OnSend(nErrorCode); } |
비 블록킹 소켓에서 접속에 관한 네트워크 이벤트는 FD_CONNECT이며, 보낼 수 있는 상황임을 알리는 네트워크 이벤트는 FD_WRITE라고 했다. FD_WRITE는 접속이 되면 이때도 보낼 수 있는 상황이기 때문에 일단 한 번 떨어지게 된다. 그 후 Send 함수의 실패가 WSAEWOULDBLOCK인 경우에 다시 떨어지게 되는 것이다. FD_WRITE나 FD_READ 등의 네트워크 이벤트에 대한 자세한 설명은 이후의 메시지 매커니즘을 사용하는 API WSAAsyncSelect I/O를 설명할 때 다룬다.
OnClose 구현
void CMyAsyncSocket::OnClose(int nErrorCode) { Close(); AfxMessageBox( "접속 종료" );
CAsyncSocket::OnClose(nErrorCode); } |
대화상자 구성
IDD_TCPCASYNCCLIENT_DIALOG
IDC_CONNECT |
접속 |
버튼 |
IDC_SEND |
보내기 |
버튼 |
IDC_CLOSE |
끊기 |
버튼 |
OnConnect 함수 |
void CTcpCAsyncClientDlg::OnConnect() { if( !m_Client.Create() ) { AfxMessageBox( "소켓 생성 실패!" ); return; }
if( m_Client.Connect( "localhost", 6565 ) == 0 ) { if( m_Client.GetLastError() != WSAEWOULDBLOCK ) { AfxMessageBox( "소켓 접속 실패!" ); return; } } } |
OnSend 함수 |
void CTcpCAsyncClientDlg::OnSend() { char buf[1024]; ZeroMemory( buf, 1024 ); strcpy( buf, "[client] CMyAsyncSocket : AsyncSending..." ); m_Client.AsyncSend( buf, strlen(buf) + 1 ); } |
OnClose 함수 |
void CTcpCAsyncClientDlg::OnClose() { m_Client.ShutDown(); } |
<끊기> 버튼을 눌렀을 때 소켓 연결을 종료하는 함수가 m_Client.Close가 아닌 m_Client.ShutDown인 것만 확인하자.
| MFC CAsyncSocket TCP 서버 프로그램
‘TcpCAsyncServer'
대화상자 기반, ‘Windows Sockets' 옵션
CAsyncSocket을 상속받은 두 개의 클래스
리슨 소켓 : CListenSocket
클라이언트와의 통신을 담당하는 소켓 : CMyAsyncSocket
리슨 소켓 구현
새로운 클라이언트 접속이 오면 그때 클라이언트 소켓을 new해서 생성한 후 이것을 Accept하는 버전을 만들 것이다.
클라이언트 담당 소켓인 CMyAsyncSocket을 대화상자 클래스(CTcpCAsyncServerDlg)의 멤버변수로 사용하는 방법
CTcpCAsyncServerDlg의 헤더에 리슨 소켓과 클라이언트 담당 소켓 선언
#include "MyAsyncSocket.h" #include "ListenSocket.h"
class CTcpCAsyncServerDlg : public CDialog { CMyAsyncSocket* m_pClient; CListenSocket m_Listen; |
m_pClient는 포인터이므로 초기에 NULL이어야 한다. CTcpCAsyncServerDlg.cpp
CTcpCAsyncServerDlg::CTcpCAsyncServerDlg(CWnd* pParent /*=NULL*/) : CDialog(CTcpCAsyncServerDlg::IDD, pParent) { m_pClient = NULL } |
리슨 소켓은 OnAccept 멤버 함수를 오버라이드
#include "TcpCAsyncServerDlg.h"
void CListenSocket::OnAccept(int nErrorCode) { CTcpCAsyncServerDlg* pDlg = (CTcpCAsyncServerDlg*)AfxGetMainWnd();
if( pDlg->m_pClient ) { CAsyncSocket TempSocket; Accept( TempSocket ); TempSocket.Close();
AfxMessageBox( "이미 접속되어 있음" ); return; }
pDlg->m_pClient = new CMyAsyncSocket; Accept( *pDlg->m_pClient );
AfxMessageBox( "억셉트 성공" );
CAsyncSocket::OnAccept(nErrorCode); } |
대 화상자의 멤버 함수로 m_pClient라는 포인터를 마련했다. m_pClient는 접속이 되면 CMyAsyncSocket 객체를 new해서 그 주소를 갖게 했다. m_pClient는 클라이언트가 연결이 되어 있지 않다면 NULL일 것이고, 연결되어 있다면 CMyAsyncSocket 객체의 주소를 갖고 있을 것이다.
또 다른 클라이언트가 접속해왔는데, 이미 연결된 상태라면 새로 들어온 클라이언트를 그냥 두면 안 되고 임시 소켓을 만들어 Accept를 시킨 후 바로 Close를 시키길 권한다. 그렇게 하지 않으며 새로 접속해 온 클라이언트는 서버에서 Accept하지 않고 무시하고 있어 접속 대기 상태로 타임아웃이 될 때까지 계속 기다리게 되기 때문이다. 만약, 아직 연결된 클라이언트가 없다면 CMyAsynSocket 객체를 new해서 힙(Heap)에 마련한 후 이 객체의 주소를 대화상자의 멤버 변수 m_pClient에 넣고, 이 객체로 Accept시켜 버리면 된다.
우 리는 클라이언트 전담 소켓을 메인 대화상자의 멤버 함수로 두었다. 이 소켓에 접근하기 이해서는 먼저 대화상자9CTcpCAsyncServerDlg)의 객체를 얻어 와야 하므로 AfxGetMainWnd 함수를 사용했다. 이 함수는 일전에 얘기했듯이 MFC의 전역 함수로 애플리케이션의 메인 창의 주소를 리턴하는 함수다. 현재 메인 창은 대화상자 창이므로 CTcpCAsyncServerDlg의 객체 주소다. 그런데 리턴되는 형식이 CWnd*이므로 우리가 원하는 CTcpCAsyncServerDlg의 포인터로 캐스팅하면 된다(CTcpCAsyncServerDlg는 CDialog를 상속받은 클래스이며 CDialog는 CWnd를 상속받은 클래스라서 변환할 수 있다.)
1. AfxGetMainWnd
CWnd* m_pMainWnd
2. AfxGetApp
CWinApp
CTcpCAsyncServerApp가 CWindApp를 상속받았으므로, 이 객체의 주소가 리턴된다.
CMyAsyncSocket 구현
먼 저 CMyAsyncSocket.cpp를 열어 윗 부분을 수정. TcpCAsyncClient.h인것을 TcpCAsyncServer.h로 수정하고 대화상자 형식을 사용할 것이므로 TcpCAsyncServerDlg.h도 포함해야 한다.
#include "stdafx.h" #include "TcpCAsyncServer.h" #include "MyAsyncSocket.h" #include "TcpCAsyncServerDlg.h" |
OnClose 구현
이 번 프로젝트는 대화상자의 멤버 변수로 CMyAsyncSocket의 포인터로 두었다. 포인터이기 때문에 만약, CMyAsyncSocket에 OnClose가 떨어져서 Close 함수를 호출해야 한다면 자기 자신을 delete한 후 메인 대화상자의 m_pClient를 NULL로 초기화해야 한다.
void CListenSocket::OnClose(int nErrorCode) { CTcpCAsyncServerDlg* pDlg = (CTcpCAsyncServerDlg*)AfxGetMainWnd();
Close();
if( pDlg->m_pClient ) { delete pDlg->m_pClient; pDlg->m_pClient = NULL; } AfxMessageBox( "접속 종료" );
CAsyncSocket::OnClose(nErrorCode); } |
대화상자 편집
<리슨>
<보내기>
<끊기>
IDC_LISTEN |
리슨 |
버튼 |
IDC_SEND |
보내기 |
버튼 |
IDC_CLOSE |
끊기 |
버튼 |
OnListen 함수 |
void CTcpCAsyncServerDlg::OnListen() { if( !m_Listen.Create( 6565 ) ) { AfxMessageBox( _T("리슨 소켓 생성 실패") ); return; }
if( !m_Listen.Listen() ) { AfxMessageBox( _T("리슨 실패") ); return; }
} |
OnSend 함수 |
void CTcpCAsyncServerDlg::OnSend() { if( m_pClient ) m_pClient->Send( "world", 6 ); } |
OnClose 함수 |
void CTcpCAsyncServerDlg::OnClose() { if( m_pClient ) m_pClient->ShutDown(); } |
CAsyncSocket을 사용한 서버와 클리아언트 테스트
04. MFC CAsyncSocket UDP 프로그램
SendTo
ReceiveFrom
CAsyncSocket을 사용해서 UDP 통신을 만들 수 있다.
CAsyncSocket 을 상속받은 UDP 서버는 Create 함수를 호출할 때 리슨할 UDP 포트 값과 함께 SOCK_DGRAM을 인자로 주어 해당 UDP 포트를 bind할 것이라고 정해준다. UDP 클라이언트의 경우에도 데이터를 받기 위해 포트를 적어주어야 한다. UDP는 클라이언트라는 개념 없이 모두 서버처럼 자신의 포트를 갖고 생성되며 연결 없이 그때그때 데이터를 보내려는 상대편 IP와 포트를 사용해서 보내게 된다.
만 약, 상대편에서 데이터를 받게 되면 OnReceive 함수에서 ReceiveFrom 함수를 호출하므로써 도착한 데이터를 받을 수 있다. 이때 ReceiveFrom 함수의 인자로 데이터를 보내온 호스트의 IP와 포트 정보를 받을 수 있다.
1. UdfMfc , Windows Sockets 옵션, 대화상자 기반 프로젝트
2. CAsyncSocket을 상속받는 CUdpSocket
3.
IDC_CREATE |
UDP 생성 |
버튼 |
IDC_SEND |
보내기 |
버튼 |
IDC_CLOSE |
끊기 |
버튼 |
IDC_STATIC |
생성하려는 포트 |
스태틱 |
IDC_STATIC |
상대방 IP |
스태틱 |
IDC_STATIC |
포트 |
스태틱 |
IDC_PORT |
(없음) |
에디트 |
IDC_REMOTE_IP |
(없음) |
에디트 |
IDC_REMOTE_PORT |
(없음) |
에디트 |
4.
#include "UdpSocket.h"
class CUdpMfcDlg : public CDialog { public: CUdpMfcDlg(CWnd* pParent = NULL); // standard constructor
CUdpSocket m_Udp; |
5. 리소스 편집 창에서 UDP 생성과 <보내기> 버튼, <닫기> 버튼을 더블클릭해서 각각 버튼에 대한 이벤트를 핸들링하는 함수를 마련하고 아래와 같이 작성해보자.
void CUdpMfcDlg::OnSend() { // TODO: Add your control notification handler code here CString sRemoteIP, sRemotePort; GetDlgItemText( IDC_REMOTE_IP, sRemoteIP ); GetDlgItemText( IDC_REMOTE_PORT, sRemotePort );
m_Udp.SendTo( "hello udp", 10, _ttoi(sRemotePort), sRemoteIP ); }
void CUdpMfcDlg::OnCreate() { // TODO: Add your control notification handler code here CString sPort; GetDlgItemText( IDC_PORT, sPort );
if( m_Udp.Create( _ttoi(sPort), SOCK_DGRAM ) ) AfxMessageBox( _T("생성 성공!") ); else AfxMessageBox( _T("생성 실패!") ); }
void CUdpMfcDlg::OnClose() { // TODO: Add your control notification handler code here m_Udp.Close(); } |
CUdpSocket 구현
CAsyncSocket을 상속받은 CUdpSocket을 FileView에서 헤더 파일이나 소스 파일을 더블클릭으로 소스 창에 연 후 <Ctrl>+<W>
void CUdpSocket::OnReceive(int nErrorCode) { CString sIP, sMsg; UINT nPort; char buf[100]; ZeroMemory( buf, 100 ); ReceiveFrom( buf, 100, sIP, nPort );
sMsg.Format( _T("%s (%d):%s"), sIP, nPort, buf );
AfxMessageBox( sMsg );
CAsyncSocket::OnReceive(nErrorCode); } |
브로드캐스트 메시지 받기
if( m_Udp.Create( _ttoi(sPort), SOCK_STREAM ) ) { BOOL bBroadcast = TRUE; m_Udp.SetSockOpt( SO_BROADCAST, &bBroadcast, sizeof(BOOL) ); AfxMessageBox( _T("생성 성공!") ); } else AfxMessageBox( _T("생성 실패!") ); |
브로트캐스트 메시지 보내기
브로드캐스트 메시지를 보내기 위해서는 SendTo 함수 사용은 같지만 목적지 IP를 특별한 IP 값인 255.255.255.255‘로 지정해주면 로컬 서브 네트워크에 브로드캐스트하게 된다.
m_Udp.SendTo( "broadcast udp", 14, _ttoi(sRemotePort), _T("255.255.255.255") ); |
[]