TCP 서버 네트워크 엔진 구현 [4]

TCP 서버 네트워크 엔진 구현 - 4

박범진 (oranze@wemade.com)

IOCP 초기화를 마치고 워커 쓰레드를 생성했으면 이제는 소켓을 IOCP와 연결시켜 주어야 한다. 이 엔진에서는 클라이언트 소켓 객체를 TcpPeer가 감싸고 있으므로, 인터페이스 측면에서 볼 때 TcpPeer 객체를 IOCP에 연결하면 된다. <리스트 4>는 이러한 코드를 보여주고 있다.


<리스트 4> TcpPeer와 IOCP의 연결

void TcpServer::AssociatePeer( TcpPeer *obj )
{
// 10초에 한 번씩, 재전송 주기는 1초에 한 번씩으로 맞춘다.
tcp_keepalive keepAlive = { TRUE, 10000, 1000 };

obj->Lock();
// 킵얼라이브 옵션을 켠다.
// 컴파일하려면 mstcpip.h 헤더 파일이 필요하다(platform sdk 참조).
DWORD tmp;
WSAIoctl( obj->m_sock, SIO_KEEPALIVE_VALS,
&keepAlive, sizeof( keepAlive ), 0, 0, &tmp, NULL, NULL );

// TcpPeer의 소켓을 IOCP와 연결시킨다.
CreateIoCompletionPort(
(HANDLE) obj->m_sock, m_iocpHandle, (DWORD) obj, NULL );

if ( !obj->Recv() || !obj->OnInitComplete() )
{
if ( ClosePeer( obj ) )
return;

}

obj->Unlock();
}


<리스트 4>에서 한 가지 특이한 코드가 보이는데, 소켓과 IOCP를 연결하기 전에 킵얼라이브(keepalive)를 설정하고 있는 부분이다. 흔히 고스트 클라이언트(ghost client)라고 하여 무응답 상태의 클라이언트를 처리하기 위해 NOP(No OPeration) 패킷을 주기적으로 주고받거나 킵얼라이브 세그먼트를 주고받도록 설정한다. 이 코드를 추가 함으로써 원격 호스트의 파워가 갑자기 나가거나 LAN 선이 뽑히는 비정상적인 연결을 감지해 낼 수 있다. 소켓을 IOCP에 연결시킨 이후에 곧바로 TcpPeer::Recv 메쏘드를 호출하는데, 앞서 설명한 것처럼 Recv 메쏘드는 엔진 내부에서 직접 호출하기 때문에, 엔진 사용자는 언제나 패킷의 수신 가능 상태를 보장받을 수 있다.


<리스트 5> TcpServer의 IOCP 워커 쓰레드

void TcpServer::WorkerThread()
{
...

while ( true )
{ // IOCP의 통보를 기다린다.
ret = GetQueuedCompletionStatus(
m_iocpHandle,
&bytesTransferred,
(DWORD *) &obj,
(OVERLAPPED **) &overlapped,
INFINITE );

if ( !obj || !overlapped )
break;

obj->Lock(); // 비동기 통보가 완료됐으므로 참조 카운트를 감소시킨다.
obj->m_refCount--;

if (
!ret ||
!bytesTransferred ||
obj->m_sysClosing ||
!DispatchObject( obj, bytesTransferred, overlapped ) )
{
if ( ClosePeer( obj ) )
continue;
}

obj->Unlock();
}
}


IOCP에 관련된 초기 작업을 모두 마쳤다면 이제는 IOCP를 감시할 워커 쓰레드를 살펴 볼 차례다. <리스트 5>에 워커 쓰레드의 루틴이 있다. 여러 쓰레드로부터의 보호를 위해 크리티컬 섹션에 진입한 뒤, 참조 카운트를 감소시키고 완료된 오버랩드 요청(WSASend, WSARecv)을 처리하고 있다.


<리스트 6> 오버랩드 완료 처리

bool TcpServer::DispatchObject(
TcpPeer *obj, int bytesTransferred, OVERLAPPED *ov )
{
TcpPeer::OVERLAPPEDEX *ovex = (TcpPeer::OVERLAPPEDEX *) ov;
// OVERLAPPEDEX::opcode로 어느 호출인지(WSASend, WSARecv) 구분한다.
switch ( ovex->opcode )
{
case TcpPeer::SEND:
DispatchSend( obj, bytesTransferred );
break;

case TcpPeer::RECV:
DispatchRecv( obj, bytesTransferred );
break;
}

return true;
}

bool TcpServer::DispatchSend( TcpPeer *obj, int bytesTransferred )
{
obj->m_olSend.inProgress = false;
// TcpPeer의 가상 함수를 호출한다.
obj->OnSendComplete(
obj->m_olSend.wsaBuf.buf, obj->m_olSend.wsaBuf.len );
// 송신 버퍼의 크기를 조절한다.
obj->m_sendBufPos -= bytesTransferred;
memmove( obj->m_sendBuf,
&obj->m_sendBuf[ bytesTransferred ], obj->m_sendBufPos );
// 이어서 보낼 패킷이 있는가?
if ( obj->m_sendBufPos || obj->m_usrClosing )
obj->Send( NULL, 0 );

return true;
}


bool TcpServer::DispatchRecv( TcpPeer *obj, int bytesTransferred )
{
obj->m_olRecv.inProgress = false;
obj->m_recvBufPos += bytesTransferred;
// 여기서 패킷을 처리하지 않고 TcpServer_Dispatcher에 등록해 처리한다.
// Dispatcher에 등록하는 것도 일종의 비동기 요청이기 때문에,
// AddPeer 함수 안에서 TcpPeer의 참조 카운트를 증가시킨다.
m_dispatcher.AddPeer( obj );

return true;
}


오버랩드 완료 처리를 어떻게 처리하느냐가 중요한데, 먼저 어떤 요청에 대한 결과인지 앞서 준비해 둔 OVERLAPPEDEX의 opcode를 이용해 구분해야 한다. WSASend에 대한 완료라면 TcpPeer에 패킷의 송신이 완료됐음을 알리고(TcpPeer::OnSendComplete), WSARecv에 대한 완료라면 TcpServer_Dispatcher에 등록해 나중에 처리할 수 있도록 한다. <리스트 6>은 방금 설명한 완료 처리 과정을 보여준다.
TcpPeer의 접속이 끊기거나 다른 비정상적인 이유로 해당 TcpPeer의 완료 통보가 실패로 돌아오는 경우가 있다. 이 때는 TcpPeer를 종료시켜야 하는데 이 때 참조 카운트가 사용된다. 다음은 TcpPeer를 종료시키는 코드를 보여준다.

bool TcpServer::ClosePeer( TcpPeer *obj )
{ ...
// 참조 카운트가 있다는 것은 다른 비동기 통보가 남아 있다는 뜻이다.
// 따라서 지금 삭제하면 안된다.
if ( obj->m_refCount )
return false;
...
// TcpServer의 가상 함수를 호출한다.
OnClosePeer( obj );

return true;
}

지금까지 TcpListener, TcpPeer, TcpServer 객체들이 어떻게 구현되는지 살펴봤다. 이제 엔진 내부에서 사용되는 TcpServer_XXX 객체들에 대해 알아 볼 차례다.
먼저 TcpServer_Dispatcher 객체부터 살펴보자. Dispatcher는 TcpPeer가 수신한 패킷을 처리하기 위한 별도의 쓰레드다. <리스트 6>처럼 TcpPeer가 패킷을 수신하게 되면, 자신을 디스패처(dispatcher)에 등록해 일정 주기로 패킷을 파싱해 처리하도록 한다. <리스트 7>은 디스패처의 쓰레드 루틴을 보여준다.


<리스트 7> TcpServer_Dispatcher

void TcpServer_Dispatcher::Dispatcher()
{
...

while ( true )
{
// dispatchCycle 만큼 쉰다.
if ( WaitForSingleObject(
m_close, m_dispatchCycle ) == WAIT_OBJECT_0 )
break;

Lock();
// m_listWait가 현재 추가된 TcpPeer의 목록이다.
m_listWait.swap( m_listProcess );
Unlock();
// TcpServer::OnBeginDispatch 가상 함수를 호출한다.
m_parent->OnBeginDispatch();

for ( iter = m_listProcess.begin();
iter != m_listProcess.end(); iter++ )
{
obj = *iter;

obj->Lock();
// Dispatcher의 처리도 오버랩드 호출과 같은
// 비동기 호출이기 때문에 증가시켰던 참조 카운트를 감소시킨다.
obj->m_refCount--;
// TcpPeer의 수신 버퍼를 처리한 다음 다시 WSARecv 호출을 한다.
if ( !obj->ProcessRecvBuffer() || !obj->Recv() )
{
m_parent->ClosePeer( obj );
continue;
}

obj->Unlock();
}
// TcpServer::OnEndDispatch 가상 함수를 호출한다.
m_parent->OnEndDispatch();

m_listProcess.clear();
}
}

TcpServer_Acceptor 객체는 이 글에서 가장 먼저 언급했던 TcpListener를 관리하여 클라이언트의 접속 요청을 처리하기 위한 쓰레드다. 이 쓰레드는 소켓의 이벤트 감지를 위해 WSAEvent Select 계열의 함수를 사용한다. 이들 함수의 자세한 사용법은 각자 알아보기로 하고, 지금은 코드의 내용을 토대로 전체적인 개념만 잡아내도록 하자. <리스트 8>은 Acceptor의 쓰레드 루틴을 보여준다.


<리스트 8> TcpServer_Acceptor

void TcpServer_Acceptor::Acceptor()
{
...
for ( int i = 2; i < MAXIMUM_WAIT_OBJECTS; i++ )

events[i] = CreateEvent( NULL, FALSE, FALSE, NULL );

while ( true )
{ // TcpListener를 위한 윈속 이벤트를 대기한다.
ret = WSAWaitForMultipleEvents(
eventCount, events, FALSE, INFINITE, FALSE );

// 감지된 TcpListener의 접속을 처리한다.
ProcessWinsockEvent( events, eventCount );
}

for ( i = 2; i < MAXIMUM_WAIT_OBJECTS; i++ )
CloseHandle( events[i] );
}

void TcpServer_Acceptor::AddNewListener( HANDLE *events, int *eventCount )
{
...
for ( ; iter != m_listListener.end(); iter++ )
{ // 새 TcpListener를 등록한다.
WSAResetEvent( events[ *eventCount ] );
WSAEventSelect( (*iter)->m_sock, events[ *eventCount ], FD_ACCEPT );

(*eventCount)++;
}
}

void TcpServer_Acceptor::ProcessWinsockEvent( HANDLE *events, int eventCount )
{
...
for ( int i = 2; i < eventCount; i++, iter++ )
{
listener = *iter;

WSAEnumNetworkEvents( listener->m_sock, events[i], &eventResult );

if ( !eventResult.lNetworkEvents )
continue;
// 클라이언트의 접속을 수락한다.
addrLen = sizeof( addr );
sock = accept( listener->m_sock, &addr, &addrLen );
...
// TcpServer::OnAccept 가상 함수를 호출한다.
obj = m_parent->OnAccept( listener, sock );
if ( !obj )
{
closesocket( sock );
continue;
}
// 새 접속으로 생성된 TcpPeer를 IOCP와 연결시킨다.
m_parent->AssociatePeer( obj );
}
}


마지막으로 TcpServer_Connector 객체는 TcpPeer를 이용해 원격 호스트에 접속을 하기 위한 쓰레드다. 이 쓰레드 역시 소켓의 이벤트 감지를 위해 WSAEventSelect 계열의 함수를 이용하며, 전체 코드 구성이 TcpServer_Acceptor와 유사하다. 지면 관계상 소스는 생략하도록 하겠다.


분산 네트워크 서버에 대한 궁금증

이로써 TCP 서버 네트워크 엔진에 대한 인터페이스 구조부터 구현까지의 모든 설명을 마쳤다. 다음 호부터는 분산 네트워크 서버에 대해 알아볼텐데, 그 동안 독자들의 날카로운 지적을 부탁하며 이만 글을 줄이고자 한다.

by ART1ST | 2005/10/25 23:57 | Socket Programming | 트랙백 | 덧글(1)

TCP 서버 네트워크 엔진 구현 [3]

TCP 서버 네트워크 엔진 구현 - 3

박범진 (oranze@wemade.com)

<리스트 2> TcpPeer의 중요 메쏘드

bool TcpPeer::Send( char *p, int len )
{
...
memset( &m_olSend, 0, sizeof( OVERLAPPED ) );
m_olSend.wsaBuf.buf = m_sendBuf;
m_olSend.wsaBuf.len = m_sendBufPos;

DWORD bytesSent;

if ( WSASend( m_sock, &m_olSend.wsaBuf, 1, &bytesSent,
0, (OVERLAPPED *) &m_olSend, NULL ) == SOCKET_ERROR )
{
if ( WSAGetLastError() != WSA_IO_PENDING )
return OnError( WSAGetLastError() );
}

// WSASend OVERLAPPED 사용 중 상태로 바꾼다.
m_olSend.inProgress = true;
// 비동기 요청을 했기 때문에 참조 카운트를 증가시킨다.
m_refCount++;

return true;
}

bool TcpPeer::Recv()
{
...
// 수신 버퍼의 위치를 조정한다.
// TCP의 특성상 쪼개지거나 뭉쳐서 올 수 있기 때문이다.
memset( &m_olRecv, 0, sizeof( OVERLAPPED ) );

m_olRecv.wsaBuf.buf = m_recvBuf + m_recvBufPos;
m_olRecv.wsaBuf.len = m_recvBufMax - m_recvBufPos;

DWORD bytesReceived;
DWORD flag = 0;

if ( WSARecv( m_sock, &m_olRecv.wsaBuf, 1, &bytesReceived,
&flag, (OVERLAPPED *) &m_olRecv, NULL ) == SOCKET_ERROR )
{
if ( WSAGetLastError() != WSA_IO_PENDING )
return OnError( WSAGetLastError() );
}

// WSARecv OVERLAPPED 사용 중 상태로 바꾼다.
m_olRecv.inProgress = true; // 비동기 요청을 했기 때문에 참조 카운트를 증가시킨다.
m_refCount++;

return true;
}

bool TcpPeer::CloseConnection( bool graceful )
{
...
// 송신 버퍼를 비운 후 접속을 끊게 한다.
m_usrClosing = true;

// 현재 송신 버퍼가 비어 있는 상태라면 바로 끊는다.
if ( graceful )
{

if ( !m_olSend.inProgress )
shutdown( m_sock, SD_SEND );
}
else
shutdown( m_sock, SD_SEND );

return true;
}

<리스트 2>는 TcpPeer의 핵심 메쏘드인 Send, Recv, CloseCon nection의 구현부를 보여준다. Send나 Recv 메쏘드에서 주의깊게 살펴 볼 부분은, OVERLAPPEDEX를 초기화하고 버퍼 포인터를 지정하는 부분과 참조 카운트를 증가시키는 부분이다. 특히 TCP의 특성상 네트워크를 경유하면서 패킷이 쪼개지거나 합쳐질 수 있기 때문에 이러한 처리를 해두는 것은 필수적이다.
Send 메쏘드와 달리 Recv 메쏘드는 엔진 사용자가 직접 호출하지 않는다. Recv 메쏘드는 엔진 내부에서 직접 호출해 주는 것으로, 엔진 사용자는 자신의 TcpPeer가 언제나 패킷을 수신할 수 있다고 가정하면 된다.
CloseConnection 메쏘드는 이름 그대로 호스트와의 접속을 끊는 데 사용한다. graceful 파라미터를 눈여겨 볼 필요가 있는데, 이 파라미터는 접속을 끊기 전에 송신 버퍼에 있는 패킷들을 모두 전송할 것인지 결정해 준다. 가령 인사말과 함께 클라이언트의 접속을 끊고 싶은 경우에 사용할 수 있다.
다음으로 이 엔진의 중심 객체인 TcpServer를 살펴보자. 윈속 및 IOCP 초기화 등의 작업을 포함해 Dispatcher, Accepter, Connector 객체를 생성, 관리한다. 또한 앞서 가상 함수를 설명하면서 언급했듯 대부분의 중요한 이벤트 통보들이 이 객체를 통해 알려진다.
지난 시간에 IOCP 프로그래밍의 흐름을 간략하게 살펴봤는데, 이번엔 구체적인 코드를 보며 하나하나 따라가 보도록 하자. 먼저 <리스트 3>은 IOCP를 초기화하고 워커 쓰레드를 생성하는 코드를 보여준다. 여기에 있는 Init 메쏘드의 dispatchCycle 파라미터는 Disp atcher가 TcpPeer의 패킷을 처리할 주기를 말한다. 엔진의 기본 값은 100인데, 이것은 TcpPeer가 수신한 패킷을 초당 최대 10회까지 처리할 수 있다는 것을 뜻한다. 이 값이 크면 클수록 이어지는 WSARecv 호출의 주기가 늦어지기 때문에, 서버의 성능이 올라가는 대신 응답 속도는 떨어지게 된다. 나머지 파라미터는 지난 시간에 설명한 대로 IOCP를 감시하기 위한 쓰레드의 수를 조절하는 역할을 한다.


<리스트 3> IOCP 초기화 및 워커 쓰레드 생성

bool TcpServer::Init(
int dispatchCycle, int numConcurrentThreads, int numWorkers )
{
m_iocpHandle = CreateIoCompletionPort(
INVALID_HANDLE_VALUE, NULL, 0, numConcurrentThreads );
if ( !m_iocpHandle )
return false;

if ( !InitWorkerThread( numWorkers ) )
return false;
...
}

bool TcpServer::InitWorkerThread( int numWorkers )
{
...

m_numWorkers = numWorkers;
// 워커 쓰레드를 만든다.
// Thread 객체는 쓰레드 루틴을 좀 더 다루기 쉽게 랩핑한 클래스다.
m_workerThreads = new Thread[ numWorkers ];
if ( !m_workerThreads )
return false;

for ( int i = 0; i < numWorkers; i++ )
{
if ( !m_workerThreads[i].Create( ThreadEntry, this ) )
return false;
}

return true;
}

by ART1ST | 2005/10/25 23:56 | Socket Programming | 트랙백 | 덧글(0)

TCP 서버 네트워크 엔진 구현 [2]

TCP 서버 네트워크 엔진 구현 - 2

박범진 (oranze@wemade.com)

<표 2> TCP 서버 객체목록
객체 역할
TcpListener
(서버소켓 객체) 서비스할 TCP 포트를 바인딩한 후 클라이언트의 접속을 받기 위해 사용한다.
TcpPeer
(클라이언트 소켓 객체) 개념적으로 하나의 TCP 연결을 나타낸다. ① TcpListener로부터 반환받은 소켓, 또는 ② 원격 호스트로의 접속에 성공한 소켓을 이용해 패킷의 입출력을 처리하는 데 사용한다.
TcpServer
(TCP 서버 중심 객체) 원속 및 IOCP 초기화, TcpListener, TcpPeer 객체들의 총 관리를 포함하는 엔진의 중심 객체이다. 응용 프로그램은 이 객체를 가장 먼저 생성해야 한다.
TcpSever_Acceptor
(라이언트의 접속을 받기 위한 객체) 하나의 응용 프로그램 안에는 다수의 TcpListener들의 네트워크 이벤트를 처리하고 다음 동작으로 신속히 전환할 수 있도록 도와준다.
TcpSever_Connector
(원격 호스트로의 접속을 위한 객체) 서버라고 해서 클라이언트의 접속만 받아 처리하는 것은 아니다. 부하 분산을 위해 때로는 다른 서버에 접속하기도 하는데, 이 쓰레드 객체는 이러한 접속 문제를 해결하기 위한 것이다.
TcpSever_Dispatcher
(패킷을 처리하기 위한 객체) 여러 쓰레드에서 무작위로 수신되고 있는 패킷들을 즉시 처리해 버리면 작은 조각들을 하나하나씩 처리하는 것이 되어 효울이 떨어질 뿐 아니라, 자칫 잘못하면 상호 참조로 인한 데드락을 초래할 수 도 있다. 이 객체는 이러한 문제를 해결하기 위한 것이다.



구현 상태 보기
TcpListener 객체부터 살펴보자. 부모 클래스인 Monitor 객체는 멀티 쓰레드의 접근을 동기화하기 위해 크리티컬 섹션(critical section)을 랩핑해 놓은 클래스다. TcpPeer도 마찬가지지만, 여러 쓰레드가 언제든지 이들 객체에 접근할 수 있기 때문에 동기화 작업은 반드시 필요하다.
TcpListener 객체는 서버 소켓을 감싸고 있다. Init 메쏘드에서 쓰이는 SockAddr 객체는 윈속의 SOCKADDR 구조체를 랩핑해 놓은 클래스다. 비록 윈속이 인터넷 프로토콜 주소를 위해 SOCKAD DR_IN 구조체를 제공한다고 해도, 하나의 주소를 설정하기 위한 작업이 여간 불편한 것이 아니다. 따라서 앞으로 소켓의 주소를 지정하는 데 SockAddr 객체를 사용할 것이다.


<표 3> TcpSever의 가상 함수 목록
가상 함수 역할
OnAccept 클라이언트의 접속이 감지됐음
TcpListener 객체가 반환한 소켓으로 TcpPeer 객체를 만 든다.
OnAcceptError 주어진 TcpListener 객체로 접속을 받아들일 수 없음
소켓이 닫혔거나 시스템 리소스가 부족한 경우 등 여러 가지 경우에 발생할 수 있다.
OnConnect 원격 호스트로의 접속이 성공했음
TcpPeer가 요청한 원격 호스트로의 접속이 성공했음을 알린다.
OnConnectError 원격 호스트로의 접속이 실패했음
네트워크에 이상이 있거나 다른 시스템적인 문제가 발생할 경우 호출될 수 있다.
OnClosePeer TcpPeer의 접속이 끊겼음
말 그대로 어떤 이유로 인해 접속이 끊겼을 때 알려진다.
OnBeginDispatch TcpPeer의 패킷 처리를 시작함
TcpSever_Dispatcher는 일정 주기로 TcpPeer가 수신한 패킷을 처리하는데, 처리하기 직전 TcpSever에 이 사실을 알린다.
OnEndDispatch TcpPeer의 패킷 처리가 끝났음
TcpPeer의 패킷 처리가 끝났음을 TcpSever에 알린다.



<표 4> TcpPeer의 가상 함수 목록
가상 함수 역할
OnInitComplete 클라이언트의 연결이 초기화됐음
① 클라이언트가 접속했거나 ② 원격 호스트에 접속이 성공한 경우, 즉 T CP 연결이 성립됐을 때 (connection established) 호출된다.
OnSendComplete 패킷 송신이 완료됐음
요청한 패킷의 송신이 완료됐음을 알린다.
OnExtractPacket 패킷 수신이 완료됐음
단 1바이트라도 패킷이 수신됐으면 이 함수가 먼저 호출된다. 이름이 OnExtracPacket인 이유는, 엔진이 엔진 사용자의 편의를 위해 ① 패킷의 완료 여부를 판단하는 분석부(parsing part)와 ② 완료 패킷을 처리하는 부 (processing part)로 명확히 구분하여 처리하기 때문이다. 이 함수는 패킷의 완료 여부를 판단하기 위해 사용된다.
OnRecvComplete 완료 패킷이 수신됐음
완료 패킷이 수신됐을 때 호출된다.
OnError 패킷 송/수신을 실패했음
소켓이 닫혔거나 시스템 리소스가 부족한 경우 등 여러 가지 경우에 발생할 수 있다.



class TcpListener : public Monitor
{
SOCKET m_sock;

bool Init( SockAddr *addr );
void Uninit();
};

다음은 TcpListener 객체를 초기화하는 메쏘드를 보여준다. 간단한 함수이므로 그냥 넘어가기로 하고, 잠시 후 TcpServer_Acceptor 객체를 설명할 때 TcpListener를 이용해 어떻게 접속을 받아들이는지 살펴보도록 하겠다.

bool TcpListener::Init( SockAddr *addr )
{
m_sock = socket( AF_INET, SOCK_STREAM, IPPROTO_IP );
if ( m_sock == INVALID_SOCKET )
return false;

if ( bind( m_sock, addr, sizeof( SockAddr ) ) == SOCKET_ERROR )
return false;

// TcpListener 객체는 TcpServer_Acceptor 객체가 처리한다.
// 내부적으로 하나의 쓰레드에서 처리되기 때문에
// 여러 TcpListener 객체를 사용하려면 넌-블럭 모드로 설정해야 한다.
unsigned long nonblock = 1;
if ( ioctlsocket( m_sock, FIONBIO, &nonblock ) == SOCKET_ERROR )
return false;

return true;
}

이제 TcpPeer 객체를 살펴보자(<리스트 1>).


<리스트 1> TcpPeer 클래스
Class TcpPeer : public Monitor
{
struct OVERLAPPEDEX : public OVERLAPPED
{
OPCODE opcode;
WSABUF wsaBuf;
bool inProgress;
};

SOCKET m_sock;
SockAddr m_remoteAddr;
char m_refCount;
...

TcpPeer( int recvBufMax = 8192 );
virtual ~TcpPeer();

bool Send( char *p, int len );
bool Recv();
bool CloseConnection( bool graceful = true );
...
};

TcpPeer는 패킷의 송수신을 위해 각각의 OVERLAPPED 구조체를 이용하여 WSASend, WSARecv 호출을 하게 된다. 여기서 OVERLAPPED 구조체를 확장한 OVERLAPPEDEX 구조체를 사용하는 이유는 ? IOCP로부터 완료 통보를 받을 때 어떤 호출의 완료인지 식별할 수 있어야 하고(opcode), ? 오버랩드 호출에서는 시스템이 인식할 버퍼의 포인터가 있어야 하며(wsaBuf), ? 동일한 오퍼레이션(WSA Send, WSARecv)의 중복 호출을 막기 위해서(inProgress)다.
TcpPeer 객체는 연결된 호스트의 소켓과 주소 정보를 감싸며 비동기 호출에 대한 참조 카운트를 갖는다. 잠시 후에 알아보겠지만, 참조 카운트는 TcpPeer 객체를 안전하게 종료시키는 데 필요하기 때문에 모든 비동기 호출에 대해 반드시 정확하게 유지돼야 한다.
TcpPeer의 생성자를 보면 수신 버퍼의 최대 크기를 명시하도록 되어 있다. 응용 프로그램에 따라 다르겠지만, 보통 일정 크기의 버퍼를 할당해 서버의 성능이나 보안 문제를 일차적으로 처리해 둔다. 생성자의 기본 값이 8K로 되어 있는 것은, 대부분의 응용 프로토콜 크기에 적합하기 때문이다.

by ART1ST | 2005/10/25 23:56 | Socket Programming | 트랙백 | 덧글(0)

TCP 서버 네트워크 엔진 구현 [1]

TCP 서버 네트워크 엔진 구현 - 1

박범진 (oranze@wemade.com)

지난 호에는 윈속 및 오버랩드 I/O, IOCP 등에 대한 이론적인 내용들을 알아봤다. 이번 호에는 이러한 내용들을 바탕으로 TCP 서버를 위한 핵심 네트워크 엔진을 구현해 보도록 하겠다. 이는 앞으로 다룰 분산 네트워크 서버의 기반이 되는 부분이므로 반드시 숙지하고 넘어가야 할 것이다.

서버 프로그래밍에서의 핵심은 역시 잘 다듬어진 네트워크 엔진(모듈)이라고 할 수 있다. 이것이 잘 받쳐주지 않으면 훌륭한 기획에 어느 수준의 개발까지 별 탈 없이 진행된다 하더라도 결국 서버의 안정성이 확보되지 않아 개발이 실패하는 일이 벌어지기도 한다. 이처럼 네트워크 엔진은 곧 서버의 안정성 문제와 직결되기 때문에, 수많은 시행착오를 겪어 가며 다양한 방법으로의 검증이 필요한 부분이기도 하다.
모든 프로그램이 마찬가지겠지만, 특히 서버의 네트워크 엔진은 매우 유연하고 견고하게 설계돼야 한다. 또한 네트워크 엔진은 보통 서버 프로세스의 공통 모듈로서 존재하며 사용 빈도가 매우 높기 때문에 접근하기 쉬운 인터페이스와 일정 수준 이상의 성능이 보장돼야 한다. 이번 시간에는 이러한 네트워크 엔진을 직접 구현해 볼텐데, 먼저 엔진의 인터페이스를 구성해 보고, 이어서 실전 코드들이 어떻게 엔진에 적용될 수 있는지 살펴보도록 하겠다.

어떤 형태의 서버를 만들 것인가
설계에 앞서 어떤 형태의 서버에 초점을 맞춰 설계할 것인지 그 범위를 한정해 둘 필요가 있다. 서비스 차원에서 분류해 보면 <표 1>처럼 크게 두 갈래의 서버로 나눠진다.
여기서는 채팅이나 메신저 서버와 같은 일반적인 형태의 서비스, 즉 대량의 접속을 처리할 수 있는 서버에 초점을 맞추어 설계할 것이다. 물론 이렇게 범위를 한정해 둔다고 해서 다른 형태의 서버에 쓸 수 없는 것은 아니다. 충분히 활용할 수 있으며, 다만 엔진의 내부 구현상 미미하게나마 성능 차이가 발생하는 것뿐이다.

인터페이스
엔진 자체의 성능도 중요하지만, 접근하기 쉬운 인터페이스를 제공하는 것 역시 매우 중요하다. 접근하기 쉽다는 것은 그만큼 오류를 줄일 수 있다는 것을 뜻한다. 따라서 엔진 제작자는 직관적이고 사용하기 편리한 인터페이스를 설계하는 데 충분한 시간을 투자해야 한다.
이번 시간에 구현해 볼 샘플 엔진의 객체 구조를 <표 2>에 나타냈다. 혹시 ‘남이 만들어 놓은 괴이한 구조의 엔진을 이해해야 하는 것 아닌가’ 하는 독자들의 우려 섞인 목소리가 벌써부터 들리는 듯 한데, 그건 걱정하지 않아도 좋다. 일반적인 TCP 서버 프로그래밍에서 설명되고 있는 개념들을 네트워크 객체들로 정리한 것뿐이다.
필자를 포함해 대다수의 프로그래머들이 그러하듯 소스를 당장 뿌리지 않으면 대단히 불편한 심기를 드러내곤 한다. 필자도 이 점 충분히 공감하고 있다. 그러나 지금은 갑갑하더라도 잠시만 참아주기 바란다. 엔진의 전체 구조와 인터페이스의 정확한 의도를 파악하는 것이 전체를 분석하는 데 더욱 중요하기 때문이다.
<표 2>에서 설명한 것처럼 TcpServer가 이 엔진의 중심 객체로서 나머지 객체들을 총 관리하는 역할을 한다. 또한 내부적으로 IOCP의 워커 쓰레드를 포함하고 있기 때문에 패킷의 송/수신 등의 통보가 최초로 알려지는 곳이기도 하다. TcpServer_XXX로 시작하는 객체들은 TcpServer의 기능을 분산시키기 위해 떼어 놓은 쓰레드이며, 엔진 사용자는 이들 객체를 직접 생성하거나 접근할 수 없다. 결과적으로 엔진 사용자가 사용할 수 있는 객체는 TcpListener, TcpPeer, TcpServer 뿐이며, 이 세 가지의 객체로 거의 모든 TCP 서버 처리를 할 수 있게 되는 것이다.


<표 1> 서로 다른 형태의 서버
형태 종류
대량의 패킷을 처리해야 하는 서버 FTP, POP3, SMTP, ...
대량의 클라이언트 접속을 처리해야 하는 서버 HTTP, 채팅, 메신저, ...



이 엔진 구조를 토대로 TCP 서버에서 일어날 수 있는 가상 시나리오를 몇 가지 세워 보았다. 각 시나리오의 흐름을 따라가면서 천천히 살펴보도록 하자.
첫 번째, 간단한 형태의 에코 서버를 만든다고 하자. 에코 서버(Echo Server)란 클라이언트가 보낸 데이터를 그대로 되돌려 주는 서버(말 그대로 메아리쳐 주는 서버)를 말하며, 이를 구현할 때의 코드 흐름은 다음과 같다(굵게 표시된 부분은 엔진이 엔진 사용자에게 알려 주는 비동기 통보를 뜻한다).

① TcpServer 객체를 생성한다.
② TcpListener 객체를 이용해 에코 서버를 위한 TCP 포트를 바인딩한다.
③ 앞서 만든 TcpListener를 TcpServer에 등록한다.
④ TcpServer_Acceptor가 클라이언트의 접속을 감지한다.
⑤ 접속받은 소켓을 이용해 TcpPeer 객체를 생성한다.
⑥ 앞서 생성된 TcpPeer 객체로 통신(패킷 입출력)을 시작한다.

개념적으로 간단히 나열해 보았는데, 실제 구현을 위한 코드도 크게 다르지는 않다. 당연한 것이지만, 엔진 사용자가 윈속 및 시스템의 API를 직접 호출하는 일은 절대 없으며, 이러한 모든 코드는 엔진이 은닉(encapsulation)하고 있다.
두 번째, 앞의 에코 서버가 특수한 처리를 위해 원격 호스트에 직접 접속할 일이 생겼다고 하자. 이 때의 흐름은 다음과 같다.

① TcpPeer를 생성한다.
② TcpServer에 원격 호스트의 주소와 함께 TcpPeer를 등록한다.
③ TcpServer_Connector가 접속 성공 여부를 감지한다.
④ 접속 성공 후 TcpPeer 객체로 해당 서버와의 통신(패킷 입출력)을 시작한다.

앞의 두 시나리오의 마지막에 TcpPeer의 통신을 시작한다고 했는데, 구체적으로 어떤 흐름을 거쳐 패킷을 주고받는지 살펴보자.

① TcpServer가 패킷의 수신 완료 사실을 알아낸다.
② TcpServer_Dispatcher가 패킷 처리를 시작한다.
③ TcpPeer에 패킷이 수신되었음을 알린다.
④ 에코 서버이므로 받은 그대로를 돌려준다.
⑤ TcpServer가 패킷의 송신 완료 사실을 알아낸다.
⑥ TcpPeer에 패킷이 송신되었음을 알린다.

여기까지 이 엔진이 가지는 TCP 서버로서의 기능을 몇 가지 시나리오를 통해 간략히 살펴봤다. 이번엔 앞의 시나리오에서 언급됐던 ‘~를 감지한다, ~를 알린다’ 등의 통보가 실제로 어떻게 처리되는지, 또 어떤 종류의 통보들이 있는지 알아보자.
이 엔진에서는 이벤트 통보를 위해 C++ 가상 함수를 이용한다. 패킷의 입출력 완료 통보는 TcpPeer 객체의 가상 함수에, 그 외의 모든 통보는 TcpServer 객체의 가상 함수를 통해 알려지게 된다. 쉽게 말해 TcpServer와 TcpPeer는 네트워크 객체로서의 역할도 하지만, 이벤트 통보를 위한 인터페이스의 역할도 함께 하는 것이다. 따라서 앞의 에코 서버를 실제로 구현한다고 하면, TcpServer를 상속받은 EchoServer, TcpPeer를 상속받은 EchoPeer 등의 객체들을 만들어 각각의 가상 함수를 구현해야 할 것이다. <표 3>은 TcpServer가 가진 가상 함수의 종류를, <표 4>는 TcpPeer가 가진 가상 함수의 종류를 나열한 것이다.
지금까지 엔진의 전체 구조와 인터페이스, 접근 방법에 대해 간략하게 살펴봤다. 이제 구조에 대한 설명은 이만 마치기로 하고, 지난 호에 다루었던 윈속 및 오버랩드 I/O, IOCP 등의 이론들이 이 엔진을 통해 어떻게 적용되어 있는지 살펴보자.

by ART1ST | 2005/10/25 23:52 | Socket Programming | 트랙백 | 덧글(0)

윈속 에러 값 설명

winsock.h 에 이렇게 되어 있다


#define WSABASEERR 10000

#define WSAEINTR (WSABASEERR+4)

#define WSAEBADF (WSABASEERR+9)

#define WSAEACCES (WSABASEERR+13)

#define WSAEFAULT (WSABASEERR+14)

#define WSAEINVAL (WSABASEERR+22)

#define WSAEMFILE (WSABASEERR+24)

. . . . .


대충 이렇게 되어 있다. 참고도 다시 도표로 정리를 했다. 나중에 소켓으로 프로그래밍을 할 때 소켓 함수가 에러가 발생할 경우 그 에러

코드를 얻은 후 에러 코드에 따라 함수가 실패한 원인을 확인 해보면 될 것이다

소켓은 각 함수가 에러가 발생하면 유닉스의 경우는 -1이 리턴되고 윈속은 INVALID_SOCKET 아니면 SOCKET_ERROR을 리턴한다. 소켓 함
수가 핸들값을 얻는 함수라면 INVALID_SOCKET으로 에러를 비교를 해야하고 다른 일반적인 소켓 함수는 SOCKET_ERROR 라고 검사를
하면 된다. 여기서 단순히 함수가 성공 이냐 실패냐 만 나온다면 프로그래머 입장에서 에러 잡기가 상당히 힘들어 진다.

그래서 소켓 함수들은 정확하게 원인이 무엇인지를 가리켜 준다. 그러니까 에러 종류들을 미리 숫자 값으로 정의를 다 해놓고 있다.
에러가 났을 때 그 에러 코드를 리턴 해준다. 유닉스 소켓은 전역변수인 errno 란 변수에 에러코드가 들어가 있고
윈속은 WSAGetLastError( ) 함수를 호출해서 알수가 있다. 다음 코드를 보자


먼저 유닉스 소켓 상에서 얻는 방법은
int sock;

//얻기 실패하면 -1 을 리턴 한다
sock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
if( socket < 0 )
{
printf( "socket() Error..--> Error Code %d ", errno );
}

이고, 윈속은

SOCKET sock;

//얻기 실패하면 INVALIDE_SOCKET 을 리턴한다
sock = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );
if( socket == INVALID_SOCKET )
{
printf( "socket() Error..Error --> Code %d ", WSAGetLastError() );
}


윈도우 소켓 에러 값 설명

WSAEINTR 10004

블럭킹 윈속이 WSACancelBlockingCall 함수에서 취소되었습니다




WSAEBADF 10009

잘못된 기술자(소켓 핸들)이다


WSAEACCES 10013

브로드캐스트 어드레스를 위한 데이터그램 소켓의 접속시도가 setsockopt 함수로 SO_BROADCAST가 설정되어있지 않은 상태에서 실패 했습니다.




WSAEFAULT 10014

name 또는 namelen 매개변수가 올바른 형태가 아닙니다.


WSAEINVAL 10022

accept 하기 전에 listen 함수가 불려지지 않았습니다.


WSAEMFILE 10024

새로운 소켓에 할당하기 위한 소켓 기술자가 더 이상 남아있지 않습니다



WSAEWOULDBLOCK 10035

소켓 함수가 비블럭킹 모드로 동작중이다



WSAEINPROGRESS 10036

블록화 함수가 호출 되는 동안 부적절한 소켓 함수가 호출되었다



WSAEALREADY 10037

이미 완료된 비동기 명령에 대한 취소가 시도됨



WSAENOTSOCK 10038

지정한 기술자가 소켓 기술자가 아닙니다



WSAEDESTADDRREQ 10039

해당 함수에 목적지 어드레스가 필요하지만 제공되지 않았음



WSAEMSGSIZE 10040

수신된 메시지가 지정된 버퍼에 저장하기에 너무 커서 손실 되었습니다



WSAEPROTOTYPE 10041

지정된 프로토콜이 잘못되었거나 이 소켓에 대해서 잘못된 형식입니다



WSAENOPROTOOPT 10042

알 수 없는 옵션이거나, 지원지지 않는 옵션을 사용했습니다.



WSAEPROTONOSUPPORT 10043

지정된 프로토콜이 지원되지 않는 형식입니다



WSAESOCKTNOSUPPORT 10044

지정된 소켓 타입이 지정한 어드레스 체계에서 지원되지 않는 형식입니다



WSAEOPNOTSUPP 10045

socket이 연결지향형 서비스(SOCK_STREAM)형태가 아닙니다. ex) listen이 UDP socket에서 호출



WSAEPFNOSUPPORT 10046

지정된 프로토콜 체계가(PF_*) 지원되지 않습니다



WSAEAFNOSUPPORT 10047

지정된 어드레스 체계가(AF_*) 지원되지 않습니다



WSAEADDRINUSE 10048

지정한 어드레스(IP)가 이미 사용중이다



WSAEADDRNOTAVAIL 10049

지정된 어드레스는 로컬 머신에서 사용할 수가 없다



WSAENETDOWN 10050

네트웍 서브 시스템에 에러가 발생했습니다



WSAENETUNREACH 10051

원격 시스템까지 네트웍이 도달할 수 없습니다



WSAENETRESET 10052

연산이 진행되고 있는 도중 접속이 끊겨버렸습니다.



WSAECONNABORTED 10053

연결이 out-of-band나 다른 실패 때문에 끊어져 버렸습니다.



WSAECONNRESET 10054

원격 연결지에서 "hard"나 "abortive" 종료를 수행해서 리셋되었습니다.



WSAENOBUFS 10055

윈도우 소켓 시스템의 버퍼 공간이 모자라거나, 애플리케이션에 의해 API에게 제공된 공간이 너무 작아서 요청된 정보를 저장 할 수가 없음



WSAEISCONN 10056

지정된 소켓이 이미 연결 되어 있음



WSAENOTCONN 10057

지정된 소켓이 이미 연결 되어 있지 않음



WSASHUTDOWN 10058

소켓이 셧다운(shutdown()) 되었습니다.



WSAETOOMANYREFS 10059

지정한 함수에 대한 인자가 너무 많음



WSAETIMEDOUT 10060

접속 시도가 시간초과 되었습니다.



WSAECONNREFUSED 10061

접속시도가 강제로 종료되었습니다



WSAELOOP 10062



WSAENAMETOOLONG 10063



WSAEHOSTDOWN 10064

원격 호스트가 다운 되었음



WSAHOSTUNREACH 10065

네트웍 시스템 장애 등에 의해서 원격호스트까지도 달 할 수 없습니다.


WSASYSNOTREADY 10091

네트워크 서브 시스템이 아직 통신할 준비가 되어 있지 않음(WSAStartup()이 반환)


WSAVERNOTSUPPORTED 10092

요청한 윈도우즈 소켓 버전이 현재 윈도우즈 소켓 시스템에서 지원하지 않습니다.


WSANOTINITIALISED 10093

이 함수를 사용하기 전에 성공적인 WSAStartup 함수의 호출이 없었습니다.


WSAHOST_NOT_FOUND 11001

호스트를 찾아낼 수 없습니다.


WSATRY_AGAIN 11002

요청된 정보가 발견 되지 않음



WSANO_RECOVERY 11003

회복할 수 없는 에러발생


WSANO_DATA 11004

잘못된 이름(name)으로 아무런 데이터가 기록되지 않았습니다.

by ART1ST | 2005/10/25 23:39 | Socket Programming | 트랙백 | 핑백(1) | 덧글(0)

영어 최상위권의 MUST DO 공부법

1. Matching - 문장은 동사와 명사를 함께 생각하라.
영어 공부에 있어 매우 중요한 부분을 차지하는 것이 바로
동사입니다. 동사만 제대로 구사할 줄 알면 영어의 많은
부분을 끝냈다고 해도 과언이 아닐 것입니다. 우리는 과연
동사를 얼마나 제대로 알고 있는 것일까요. 혹시 피상적인
의미만 알고 있지는 않을까요? 실제로 어떻게 동사가
쓰이는지를 알아야 할 것입니다.


시중에서 판매되고 있는 영어 단어장을 펼쳐 보면 단어 하나하나의 의미에만 치중되어 있는
경우가 많습니다. 물론 단어가 어떻게 활용되는지를 보여주기 위한 예문이 함께 수록된
단어장도 요즘에는 많이 출시되었습니다. 그러나 여전히 예문은 단순한 보조에 그치는 경우가 많습니다. 다음은 어느 단어장에 나온 예시를 옮겨 본 것입니다.



아마 대부분의 학생들은 위의 단어를 모두 알고 있을 것입니다. Survey는 '조사', conduct는 시행하다'라는 정도로 말이죠. 하지만 위와 같이 단어를 따로따로 분리해서 외워서는 실제 영어에서는 전혀 사용할 수가 없습니다. 예를 들어 다음과 같은 영작 상황을 생각해보도록 합시다.


한국인의 이해 과정



'설문 조사를 시행하다'의 올바른 영작 표현은 conduct a survey 입니다. 위에서 본 것과 같이 단어를 따로따로 나눠서 외우면 아무리 각각을 잘 알고 있더라도 막상 survey를 어떻게
사용해야 하는지를 알 수가 없는 것입니다. 그렇다면 공부를 어떻게 해야 하는 게 맞을까요?
문장을 동사와 명사를 서로 Matching 해서 통째로 생각해야 합니다. 이런 공부법은 아마 다른 곳에서도 많이 들었을 겁니다. '문장을 통째로 외워라', '500문장만 알면 미국에서 살 수 있다.'와 같은 말들이 다 이와 같은 맥락에서 나온 것입니다.

우리가 한국말을 하는 과정을 잘 생각해 보면 왜 문장을 생각할 때 동사와 명사를
묶어서 접근해야 하는지를 더 잘 이해할 수 있습니다. Phone him 은 한국어로 '그에게
전화를 걸다'라는 것을 우리는 쉽게 생각할 수 있습니다. 그런데 과연 "전화를 걸다"라는
표현은 외국인에게 결코 만만한 표현은 아닐 겁니다. '걸다'라는 뜻을 국어사전에서 찾아보면 '물건을 걸쳐 놓거나 드리워지게 하다.', '문이 열리지 않도록 쇠/못/고리 따위를 꽂거나
지르다.'… 와 같이 매우 다양한 용례를 갖고 있으며 막상 '전화를 걸다'와 연관된 뜻은 9번째 용례에서나 찾아볼 수 있습니다. 그러나 우리는 '전화를 걸다'라는 표현을 동사와 명사가
결합된 채로 통째로 외우고 있기 때문에 10가지가 넘는 용례를 일일이 생각하지 않아도 곧바로 사용할 수 있는 것입니다. 외국인이라면 아마도 '전화를 걸다'라는 표현을 다음과 같은 과정을 통해서 이해할 것입니다.


외국인의 이해 과정



참 답답하죠? '전화를 걸다'라는 표현은 '전화'와 '걸다'를 따로 생각하지 않고 우리는 한꺼번에 사용하고 있는데도 불구하고 저렇게 따로 떼어서 생각하니 답답할 수 밖에요. 그러나 거꾸로 우리가 영어를 배울 때는 위의 conduct a survey 의 경우와 같이 똑 같은
실수를 범한다는 것을 잊으면 안됩니다. 그렇게 때문에 문장을 공부할 때는 동사와 명사를 꼭 한꺼번에 Matching 해서 외워야 한다는 것을 꼭 기억해 두시기 바랍니다. 참고로 동사와 명사의 Matching 이 잘 파악 안될 때 collocation 사전을 활용하면 도움이 된답니다.

2. Underline - 독해는 밑줄을 치며 읽어라.

영어 독해 실력을 빨리 향상하는 방법은 뭐가 있을까요?
흔히들 말하는 '양치기'에 의존하고 있지는 않나요? 물론 많은 양의 글을 읽고 경험을 쌓는 것은 꼭 필요한 작업입니다.
그러나 독해 문제집 1권을 풀었을 때 학생이 얻는 효과는
서로 다를 수밖에 없습니다. 같은 시간을 투여해도 다른
사람의 2배의 성과를 얻는다! 이번엔 MUST DO 법칙의
두 번째 Underlining에 대해서 알아 보도록 합시다.



지금 빨리 여러분이 풀고 계시는 영어 독해 교재를 하나 갖고 와 보시기 바랍니다.
그리고 아무 페이지나 펼쳐 보세요. 자, 어떻습니까? 독해 지문에 밑줄이나 기타 다른 표시가 되어 있나요? 밑줄이나 다른 표시가 되는 경우는 Type A이고 그렇지 않고 독해 지문이 깨끗한 경우는 Type B입니다.



여러분이 풀고 계신 교재의 모습은 Type A 인가요 아니면 Type B인가요? 왼쪽의 Type A는
실제 영어 최상위권 학생이 사용한 영어 교재이고 오른쪽의 Type B는 일반 고등학생이 사용한 문제집의 모습입니다. 우선은 한눈에 봐도 누가 더 열심히 공부를 했는지를 알 수 있습니다.
물론 교재가 꼭 더럽혀져야만 열심히 공부했다는 뜻은 아닙니다. 경우에 따라서는 깔끔하게
남겨두는 편이 더 효과적일 때도 있으니까요. 그러나 영어 독해의 경우는 이런 밑줄 치기인 Underlining 이 매우 효과적이라는 사실을 알고 있는 사람은 많지 않습니다.



Underlining의 장점은 무엇이 있을까요? 우선 첫째로는 주제 찾기 능력이 향상됩니다.
밑줄만 치는데 어떻게 주제 찾기 능력이 향상되는지 궁금하시겠네요. 밑줄을 치는 Underlining 을 아무렇게나 하면 물론 주제가 잘 보이는 것은 아닙니다. 특히 착각하기 쉬운 부분은 그냥
생각 없이 밑줄을 치는 경우입니다. 글을 읽을 때 습관처럼 밑줄을 치는 것은 Underlining 이
아니라 단순한 버릇일 뿐입니다. Underlining 은 생각을 하면서 밑줄을 치는 것을 말합니다.
자신이 생각하기에 중요하다고 생각되는 문장에 밑줄을 확 긋는 것이죠. 물론 핵심 문장을 잘못 파악했을 수도 있습니다. 그러나 설사 그렇다 하더라도 독해를 하는 데에는 큰 도움이 됩니다. 왜냐하면 머릿속으로 '무엇이 핵심문장일까'를 계속 생각하기 때문에 필자의 의도를 쉽게 읽을 수 있기 때문입니다.

둘째는 어려운 문장을 쉽게 해석할 수 있습니다. 이것은 영어 학원을 다녀 본 학생이라면 누구든지 한번쯤은 보았음직한 것입니다. 어려운 독해 지문을 보다 보면 매우 길고 문법적으로 복잡하게 꼬여 있는 문장이 있습니다. 이런 문장을 하나만 만나면 독해를 하는데 있어 매우
시간을 낭비하게 되는 방해요소가 되기 마련입니다. 이럴 때 Underlining 을 통해 복잡한
문장을 쉽게 정리하는 습관이 있었다면 아무리 어려워도 금방 해결 할 수 있겠죠. 한번 다음의 문장을 직접 보면서 Underlining의 실례를 보도록 합시다.


물론 위의 문장은 저렇게까지 정리할 필요가 없는 간단한 문장이긴 합니다. 그러나 장문 독해 같은 경우에는 정리를 하지 않으면 읽기가 힘든 경우가 종종 있습니다. 앞에서 말했던 주제
찾기 Underlining 의 경우는 어느 지문을 읽던 간에 적용해야 하고 이런 정리로써의 Underlining 은 자신이 없는 문장을 해석할 때 선택적으로 적용하면 독해 시간을 반으로 줄일 수
있습니다. 또한 잘 몰랐던 단어에는 나중에 복습할 때 다시 확인할 수 있도록 알아보기 쉬운 표기를 해 놓으면 매우 편리하답니다.

3. Shadowing - 말하기는 들은 것을 곧 바로 해라.

영어 말하기 연습을 따로 해 보신 적이 있나요? 말하기
연습에도 빨리 익힐 수 있는 좋은 방법이 있답니다. 말하기
연습을 할 때 있어서 가장 빠지기 쉬운 오류 중의 하나는
말하기 연습과 듣기와는 서로 별개라는 착각입니다. 그러나
잘 생각해 보시기 바랍니다. 보통 말하기와 듣기는 따로
부르는 경우도 있지만 '말하기/듣기'와 같은 식으로 한꺼번에
묶어서 말하는 경우도 있죠? 그것에는 괜히 그러는 것이
아니라 특별한 이유가 있기 때문입니다.



서문에서도 밝혔지만 수능만을 준비하는 학생은 말하기 연습을 해 본 적이 전혀 없을 수도 있을 겁니다. 그러나 막상 다른 것은 다 잘할 수 있는데 외국인 앞에만 서면 말문이 딱 막힌다면
정말 헛공부를 한 셈입니다. 서울대 법대를 합격했더라도 영어 한마디 제대로 할 줄
모른다면 과연 인정 받을 수 있을까요? 절대로 그렇지 않습니다. 영어는 실제 사회에서
사용하기 위해서 배우는 것이지 수능 만점을 배우기 위해서 배우는 것이 아니기 때문입니다.

말하기 연습에도 매우 여러 가지가 있습니다. 대본을 보면서 따라 읽는 방법, 특정 주제를 놓고 이야기를 하는 방법, 정해진 상황에서의 대화 등등 여러 가지 연습 방법이 있죠. 그러나 그
중에서 가장 추천할 만한 방법은 바로 Shadowing 입니다. Shadowing 이란 다른 사람이
말하는 것을 2~3초 후 곧바로 따라 말하는 것을 말합니다. Shadow 의 뜻에는 '그림자'
라는 뜻도 있지만 '그림자처럼 따라가다'라는 뜻도 있습니다. 다른 사람이 말하는 것을 곧바로 따라서 말하니까 Shadowing 이 되는 것이죠.

Shadowing 의 기본은 우선 듣기 테이프와 그것을 틀 수 있는 카세트 플레이어를
준비하는 것입니다. 말하기를 연습하는데 듣기 자료를 준비하니 조금 이상하지요? 아래
사진에 보이는 것이 바로 '찍찍이'입니다. 영어 듣기를 공부하는 학생들을 위한 전용 카세트
플레이어를 흔히 '찍찍이'라고 합니다.


영어 말하기의 필수품 찍찍이


테이프를 앞으로 뒤로 빨리 돌리다 보면 이어폰에서 찍찍 소리가 나죠? 그래서 '찍찍이'라고 불린답니다. 어느
유명한 영어 강사는 이 '찍찍이'를 10개씩 고장 낼 정도로 열심히 사용했다고 하니 영어 학습에는 정말 꼭 필요한
도구라고 할 수 있겠습니다. 테이프에서 나오는 말의
속도는 조금 빠를 수 있으니 재생 속도를 약간만 줄여주면 어렵지 않게 따라 할 수 있습니다. 실제로 어떻게 활용하는지 아직 감이 잡히지 않을 수도 있으므로 다음의 예시를
통해서 Shadowing이 어떤 것인지 설명해 드리겠습니다.



W : Jack, are these pictures from your school trip?
나 : Jack, are these pictures from your school trip?

M : Yes, they are. Have a look.
나 : Yes, they are. Have a look.

W : Oh, you rode the cable car! I've never ridden a cable car. What was it like?
나 : Oh, you rode the cable car! I've never ridden a cable car. What was it like?

M : At first, I was a little bit scared, but the scene below the car was fantastic.
나 : At first, I was a little bit scared, but the scene below the car was fantastic.




쉽죠? 이런 대화의 경우는 화자가 바뀔 때 마다 한번씩 끊어주면서 따라 말하면 됩니다. 그리고 이런 대화가 아닌 경우에는 자신의 실력에 따라 알맞은 타이밍에 끊어주면 됩니다. 처음에는
한 문장부터 시작해서 익숙해지기 시작하면 거의 실시간으로 따라 할 수 있게 될 겁니다.

이런 Shadowing 이 좋은 이유는 다음의 세 가지 이유 입니다. 첫째는 말하기 연습을 매우
집중해서 할 수 있게 됩니다. 보통 스크립트만 보면서 따라 말하기 연습을 하는 경우가
많은데 이러면 연습하는 것도 재미가 없고 그다지 노력을 하지 않으면서 시간을 보내기
일쑤입니다. 그러나 자신이 들은 것을 정확하게 따라 해야 하기 때문에 자연스럽게 집중을 하게
되는 것이죠.

둘째는 원어민의 발음과 억양에 가까워지게 됩니다. 아까 말했듯이 스크립트만 보면서
말하기 연습을 할 경우 자신의 발음이 정확한지 아닌지에 대한 검증이 되지 않습니다. 그리고 원어민 강사와 1:1로 한다고 하더라도 강사가 일일이 지적해 주기란 상당히 어렵습니다. 결국 가장 좋은 발음으로 녹음된 영어를 자신이 그대로 따라 해 보는 것이 가장 좋은 방법인
것입니다.

마지막 셋째는 듣기 능력을 동시에 키울 수 있습니다. Shadowing 은 말하기와 듣기가
동시에 이루어지므로 일석이조입니다. 말하기와 듣기는 서로 맞닿아 있습니다. 말하기는 잘
되는데 듣기가 되지 않는다든지 듣기는 되는데 말하기가 되지 않는 경우는 매우 잘못되었다고 볼 수 있습니다. 따라서 어느 한쪽을 소홀히 해서는 안되기 때문에 기왕이면 말하기와 듣기를 동시에 학습할 수 있는 방식을 택하는 것이 유리합니다.

다음 내용 보기 >>

by ART1ST | 2005/10/24 13:57 | Article | 트랙백(1) | 덧글(0)

STL port 설치 가이드.

*VC 6++용 STLport의 빠른 설치 가이드
(Quick Install Guide for STLport on VC 6++)

• 문서 정보

• 최초 작성 :2001/03/02

• 최종 업데이트 : 2001/05/15

• 포스팅 : 2001/03/07

• 적용 플랫폼 : Win32 / VC++ 6

• 작성자 : 곽용재 (kwak101@hitel.net)


1. 들어가기 전에

본 문서는 Win32 환경에서 Visual C++ 를 사용하시는 분들이 STLport의

STL 라이브러리를 설치하여 사용하는데 도움을 주기 위하여

작성되었습니다. 가장 최소한의 설치 과정만을 싣는데 초점을 두었으며,

Visual C++ 6 이하의 버전에서는 테스트하지 않았음을 미리 알려

드립니다. 기타 세세한 정보는 외부 자료 문서화 페이지 혹은 게시판에서

얻고, 나누어 주시기 바랍니다. 감사합니다.

STLport 라이브러리는 SGI(실리콘 그래픽스)의 STL을 여러 가지 운영체제

및 개발 도구에서 쓸 수 있도록 포팅한 것으로, ANSI 표준안을 충실히

따르고 있으며 이외의 비표준 라이브러리도 충실히 구비해 놓고 있는

공개 라이브러리입니다.

잘 아시겠지만, 본 문서는 읽으시는 분께서 Visual C++ 개발 환경과

C++ 사용에 불편해하지 않고 DOS 화면을 두려워하지 않는다는

가정 하에 작성했고, 윈도우 환경을 최대한 사용하는 쪽으로

작성하였습니다. :)

2. 라이브러리 받아 놓기


가) STLport 패키지의 압축 파일을 받습니다. 현재의 안정화 릴리즈는

4.5.3 (STLport-4.5.3.zip)입니다.

http://www.stlport.org/download.html


나) 만만해 보이는 디렉토리에 압축을 풉니다. 되도록 개발도구가 설치된

디렉토리와는 멀수록 덜 불안하겠지요?
(참고로, 제 Visual Studio는 D:Programming Files2 에 있습니다)




다) 순서대로 간단히 디렉토리 설명을 드리면 다음과 같습니다.

일단 훑어만 보시죠.

• doc : 라이브러리 설치 도움말 및 라이브러리 레퍼런스가 들어 있는

• 디렉토리.

• etc : 말 그대로 기타 잡다구리한 파일들이 있는 디렉토리

• lib : 컴파일된 STLport 재사용 바이너리(lib, dll)가 들어가는 디렉토리

• 처음엔 없다가 나중에 생길겁니다.

• src: 재사용 바이너리를 빌드하기 위한 플랫폼 별 프로젝트 파일이

• 있는 디렉토리.
(주의: vc6.mak 파일이 있는지 확인해 보세요. 빌드에 중요합니다)

• stlport : STL 헤더와 바이너리 소스가 들어 있는 디렉토리.

• STLport 라이브러리의 몸통입니다.

• test : STLport가 설치된 후 제대로 설치되었나 확인하기 위한

• 테스트 프로그램 소스.

라) 다 되었으면, 3으로 넘어갑니다.

3. 라이브러리 컴파일하기



Visual C++에서 STLport가 제대로 쓰이는데 필요한 재사용 바이너리를

만드는 과정입니다.

가) Visual C++를 열고, File >Open 메뉴로 srcvc6.mak 메이크파일을

읽어 들입니다.

아래와 같은 대화 상자가 나와 "프로젝트를 만들겠냐고 물어 보는데,

당연히 [예] 하시고, 적당한 이름(예: STLP_vc6)을 짓죠.
.dsp와 .dsw가 만들어 지면서 재사용 바이너리를 빌드할 수 있게 됩니다.



나) 재사용 바이너리는 STLport 패키지에 있는 표준 C++ 헤더를 사용하여

빌드되므로, VC++이 이 헤더를 가장 먼저 참조할 수 있도록 해야 합니다.

따라서 이 짓을 해 줍시다.

Tools >Options 메뉴 >Directories 탭에서, Include Files 목록에

stlport 디렉토리를 추가하고 나서 이것을 첫 줄로 올립니다.




다) 이제 Rebuild All을 하시든지 해서 라이브러리 빌드로 들어 갑니다,
이때, stlport ew 헤더 파일의 2줄에서 에러를 일으키는 경우가

있는데요, 네임스페이스 std::를 붙여 주도록 합시다.
(아래의 그림에서 if 다음의 2줄입니다)

※ 빌드시에 에러가 발생되면, 문서 맨 하단의 참고사항을 참고하여 해결할 것.

라) 이제 다시 빌드합니다( - -; ). 시스템에 따라 차이는 있을 수 있으나

1시간은 잘도 걸립니다.

참고로 저희 집에서는 주변에서 남는 부품을 모아 만든

Celeron 566Mhz / 128 MB 시스템을 쓰고 있습니다.
이 동안엔 인터넷도 할 수 없고 MSN 도 할 수 없어서 매우 답답합니다.

마침 휴일(31절)이라, 저는 오우거배틀64를 했습니다 :)
미션 하나를 끝내고 나니 빌드가 모두 끝나 있더군요.




마) 빌드한 라이브러리를 확인합니다. 별다른 조정을 해 주지 않았다면

아래와 같이 lib 디렉토리와 함께 만들어질 것입니다.



재사용 바이너리는 총 6개입니다.
DLL은 debug/(release)의 2개이며, LIB은 debug/(release),

debug_static/(release)_static의 4개입니다.

4. 라이브러리 설치하기

만들어진 STLport의 재사용 바이너리와 표준 헤더를 비주얼 스튜디오에서

쓸 수 있도록 해주는 과정입니다.


가) DOS 창을 열고, STLport 패키지가 풀린 위치를 기준으로

src 디렉토리(위에서 확인하셨죠?)를 찾아 갑니다.


E:STLport-4.5.3src

나) 도스 프롬프트에서 nmake install을 입력합니다.

E:STLport-4.5.3src make install

E:STLport-4.5.3src make -f vc6.mak

시스템이 알고 있는 비주얼 스튜디오의 인클루드 패스와 라이브러리

패스에 STLport 헤더와 바이너리가 복사될 겁니다.



참고) 만약에 nmake가 실행되는데 문제가 있거나 라이브러리 설치가

제대로 되어 있지 않다면, 비주얼 스튜디오에 관련된 환경 변수가

시스템에 제대로 등록되지 않았기 때문이므로, VCVARS32.BAT를

실행한 후에 다시 nmake install을 해 보세요.

(이파일은 C:Program FilesMicrosoft Visual StudioVC98Bin

여기에 있습니다.. 폐인약간수정..)


그래도 안될경우 (다른곳에 있던것 가져옴)

vcvars32.bat

copy vc6.mak makefile

nmake clean all

nmake install


다) 비주얼 C++가 이것을 가장 먼저 참조할 수 있도록 인클루드 경로를

조정합니다.
Tools >Options 메뉴 >Directories 탭에서, Include Files 목록에

방금 추가된 stlport 디렉토리

(대개 C:/Program Files/Microsoft Visual Studio/VC98/include/stlport

이겠지요)를 추가하고 나서 이것을 첫 줄로

올립니다.

5. 테스트!


이제 다 끝났습니다. STLport가 제대로 설치되었는지 확인하는 의미에서,

패키지에 들어 있는

testehvc6.mak
test egressionvc6.mak

메이크 파일을 Visual C++을 사용하여 읽은 후에 빌드해 보세요. 자동으로

테스트까지 해 줄 겁니다.


6. 빌드시 참고할점


가) "중복된 심볼" 링크 에러나는 경우

설치한 STLport를 가지고 실제 프로그램을 만들 때 링크에러가 나는

경우가 있습니다. 이것은 STLport의 입출력 라이브러리와 기존의

VC6 입출력 라이브러리의 심볼이 충돌하기 때문인데, STLport의 헤더에

정의된 것과 VC6의 헤더에 정의된 것이 완전히 일치하지 않기 때문에

(STLport에서 커버하지 못하는 부분을 VC6의 표준헤더 - STL 부분만이

아니라 - 가지고 있기도 해서) VC6의 링커가 이 두 개에 대한

라이브러리를 모두 링크하려고 하는 것입니다.
이 문제는 Project >Setting 메뉴에서 해결할 수 있습니다.

C/C++ 탭 대화 상자의 콤보 박스에서 Code Generation을 선택하고,

Use Runtime Library에서 Multithread DLL을 선택해 줍니다.

나) 플랫폼 SDK과 같이 사용할 경우 "InterlockedIncrement" 관련 컴파일

에러가 날 때

생기는 에러는 이런 류의 메시지입니다:

e:microsoft visual studiovc98includestlportstl_threads.h(122) : error C2733: second C linkage of overloaded function 'InterlockedIncrement' not allowed
e:microsoft visual studiovc98includestlportstl_threads.h(122) : see declaration of 'InterlockedIncrement'


이 컴파일 에러를 막으려면, STLport가 설치된 디렉토리(대개 C:/Program

Files/Microsoft Visual Studio/VC98/include/stlport이겠지요)

에서 stl_user_config.h를 찾아 열고, 다음 부분을 주석 해제합니다.

// # define _STLP_NEW_PLATFORM_SDK 1



by ART1ST | 2005/10/24 13:26 | Article | 트랙백(1) | 덧글(0)

방명록.



고독은 행복의 반대편에 있는 것이 아니다.
항상 행복과 고독은 공존한다.

by. ART1ST

by ART1ST | 2005/10/21 10:50 | 방명록 | 트랙백(3) | 덧글(1)

◀ 이전 페이지          다음 페이지 ▶