[WinAPI] WinSock2를 이용한 소켓 예제 ( 비동기 방식 – 서버 )

[WinAPI] WinSock2를 이용한 소켓 예제 ( 비동기 방식 – 서버 )

소켓 프로그래밍의 전반적인 개념은 이 블로그를 참고해주세요.
동기 방식 소켓 프로그래밍은 여기를 참고해주시길 바랍니다.

오늘은 WinSock2를 이용해서 비동기 소켓 프로그래밍을 해보겠습니다.

비동기 방식은 객체 지향 프로그램에 적합하므로 이번에는 MFC로 만들어 보겠습니다.

서버를 만들기 위해 아래와 같이 MFC 프로젝트를 추가합니다.

stdafx.h 맨 위에 아래와 같이 define을 해줍니다.

#pragma once
#define _WINSOCK_DEPRECATED_NO_WARNINGS	// 정의

대화상자 헤더파일에 아래와 같이 SOCKET 변수를 추가합니다.

SOCKET m_socketServer;
SOCKET m_sockClient;

리소스 뷰에서 생성, 보내기, 종료 버튼을 생성 합니다. ( 더블 클릭하여 이벤트 처리 함수까지 생성하게 합니다. )

생성 버튼을 함수에 아래와 같이 코딩합니다.

void CASyncSockServerDlg::OnBnClickedButton1()
{
	// 생성 버튼 이벤트 처리 함수
	WSADATA wsdata;
	int iRes = ::WSAStartup( MAKEWORD( 0x02, 0x02 ), &wsdata );
	if ( ERROR_SUCCESS != iRes )
		return;

	m_socketServer = ::socket( PF_INET, SOCK_STREAM, 0 );

	sockaddr_in srv_addr;
	srv_addr.sin_family = AF_INET;
	srv_addr.sin_addr.s_addr = inet_addr( "122.199.199.25" );
	srv_addr.sin_port = htons( 1234 );

	iRes = ::bind( m_socketServer, (LPSOCKADDR)&srv_addr, sizeof( srv_addr ) );

	iRes = ::listen( m_socketServer, SOMAXCONN );

	// 	FD_ACCEPT: 클라이언트가 접속하면 윈도우 메시지를 발생시킨다.
	iRes = ::WSAAsyncSelect( m_socketServer, m_hWnd, 10000, FD_ACCEPT );
}

클라이언트가 접속하거나, 패킷을 보내거나, 종료했을 때 발생되는 메시지를 처리하기 위해 아래와 같이 WindowProc 함수를 추가합니다.

그리고 내부에 아래와 같이 코딩합니다.

LRESULT CASyncSockServerDlg::WindowProc( UINT message, WPARAM wParam, LPARAM lParam )
{
	// TODO: 여기에 특수화된 코드를 추가 및/또는 기본 클래스를 호출합니다.
	if ( 10000 == message )
	{
		SOCKET hSocket = (SOCKET)wParam;
		switch ( lParam )
		{
        	// 클라이언트 접속
			case FD_ACCEPT:
			{
				sockaddr accept_addr;
				int iLen = sizeof( accept_addr );
				SOCKET hSocket = ::accept( m_socketServer, &accept_addr, &iLen );
				if ( INVALID_SOCKET != hSocket )
				{
                	// 클라이언트 소켓의 이벤트도 받을 수 있게 접속 시 등록한다.
					int iRes = ::WSAAsyncSelect( hSocket, m_hWnd, 10000, FD_CONNECT | FD_READ | FD_WRITE | FD_CLOSE );
					m_sockClient = hSocket;
				}
			}
			break;
            // 데이터 수신
			case FD_READ:
			{
				char cBuff;
				memset( &cBuff, 0, sizeof( cBuff ) );
				int iLen = ::recv( hSocket, &cBuff, sizeof( cBuff ), 0 );
                
           		if ( 0 < iLen )
                {
                	// 데이터 수신
                }
			}
			break;
            // 소켓 종료
			case FD_CLOSE:
			{
				int iRes = ::closesocket( hSocket );
			}
			break;
		}
	}

	return CDialogEx::WindowProc( message, wParam, lParam );
}

보내기 버튼 함수에 아래와 같이 코딩합니다.

void CASyncSockServerDlg::OnBnClickedButton2()
{
	// 보내기 버튼 이벤트 처리 함수
	char cBuff = 1;
	int iSend = ::send( m_sockClient, &cBuff, sizeof(cBuff), 0 );
	if ( SOCKET_ERROR == iSend )
	{
		// Send error
	}
}

종료 버튼 함수에 아래와 같이 코딩합니다.

void CASyncSockServerDlg::OnBnClickedButton3()
{
	// TODO: 여기에 컨트롤 알림 처리기 코드를 추가합니다.
	::closesocket( m_sockClient );
	::closesocket( m_socketServer );

	::WSACleanup();
}

여기서 핵심은 생성 버튼 이벤트 처리 함수에 있는 WSAAsyncSelect 입니다.

이 함수를 사용하여 서버와 클라이언트 소켓을 논블로킹 모드로 전환 해주는 겁니다.

int
WSAAPI
WSAAsyncSelect(
    _In_ SOCKET s,		// 소켓
    _In_ HWND hWnd,		// 네트워크 이벤트가 발생할때 메시지를 받을 윈도우 핸들
    _In_ u_int wMsg,	// 네트워크 이벤트가 발생할때 수신되는 메시지
    _In_ long lEvent	// 네트워크 이벤트
    );

msdn을 참고하여 WSAAsyncSelect의 인자 값을 설명하면 위 와 같습니다.

거기서 lEvent, 네트워크 이벤트에 대한 설명은 아래와 같습니다. 여러 가지가 있는데… 대략적인건 이정도가 되겠네요.

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

다음 글엔 클라이언트 코딩에 대해 알아보겠습니다.