1. Socket Class의 구조
|
0. 강의에 들어가면서
|
Þ
|
1. Socket Class의 구조
|
|
2. Socket이 접속할 때
|
|
3. Socket이 종료될 때
|
|
4. Socket에 Data가 전달될 때
|
|
5. Socket에서 Error 처리
|
Winsock을 이용한 프로그래밍에는 I/O Model에 따라 프로그램의 구조가 많이 바뀌게 된다.
볼랜드 소켓(TServerSocket과 TClientSocket을 이하 Borland Socket이라 칭함)은 ServerType이라는 프로퍼티에서
Thread-Blocking모드와 None-Blocking모드 2가지를 제공한다. 하지만 여기서는 일반적으로 많이 사용되는
None-Blocking Mode에 대해서만 설명하도록 하겠다.
WSAAsyncSelect방식은 언제 일어날지 모르는 Socket I/O 작업의 통지를 사용자가 정의한
Window Message를 이용하는 방식이다. WSAAsyncSelect API에 대해 잠시 보도록 하자.
int WSAAsyncSelect(
SOCKET s,
HWND
hWnd,
unsigned int wMsg,
long
lEvent
);
간단히 설명을 하자면 SOCKET s에서 일어나는 일련의 상황들을 HWND hWnd 윈도우에게 long
lEvent에서 정의한 플래그들에 따라서 unsigned int wMsg의 사용자 정의 메시지로 통보하라는 방식이다. 위의
WSAAsyncSelect방식은 대부분의 네트워크 프로그램에서 만족할만한 성능으로 동작을 하게 된다.
Server를 프로그래밍 하는 과정에서 중요한 것들 중 하나는 클라이언트들의 효율적인 관리에 있을 것이다.
이는 VC++의 CAsyncSocket과 가장 차이가 많이 나는 부분이기도 하다(성능의 차이가 아니라 구현방법의 차이를 말하는 것이다.). 사실
Client용으로 Socket을 사용하게 되면 TClientSocket이나 CAsyncSocket이나 비슷하게 사용할수 있다. 하지만 서버의 경우엔
다른 면이 많기 때문에 자세히 설명을 하고자 한다. 또한 앞으로 winsock API를 사용하여 직접 서버를 설계해야 할 사람들에게도 참고가 되리라
생각한다.
우선 Borland Socket의 구성요소부터 살펴보기로 하자. Borland Socket은
TServerSocket과 TClientSocket 2개로 구성된다. 각각은 이름에서 의미하듯 서버와 클라이언트 기능을 최적화 시켜놓은 컴포넌트이다.
TServerSocket은 다수의 클라이언트 요청을 처리해야 하는 책임이 있다. 당연히 ClientSocket에 비하여 다소 복잡하게 구성이 될수밖에
없는 것이다.
다음의 소스를 살펴 보자.
void __fastcall
TfrmMain::btnButton1Click(TObject *Sender)
{
for (int i = 0; i <
ServerSocket->Socket->ActiveConnections; i++)
ServerSocket->Socket->Connections[i]->SendText("Hello");
}
|
코드의 내용은 현재 접속된 모든 사용자에게 “Hello”라는 문자를 전송하는
것이다.이제부터 우리는 어떠한 내부적인 구현으로 위와
같은 작업이 가능한지에 대해서 설명을 할 것이다. 또한 TServerSocket의 이벤트 발생에 관해서도 약간의
언급을 할것이다.
우선 OnClientRead이벤트가 발생하는 부분을 보기위해 scktcomp.pas파일에서 OnClientRead를 찾아보면
1568번째 라인에서 다음과 같은 코드를 접할수 있다.
procedure
TServerWinSocket.ClientRead(Socket: TCustomWinSocket);
begin
if Assigned(FOnClientRead) then
FOnClientRead(Self, Socket);
end;
|
필자가
처음 소스코드를 봤을땐 가장 이해가 가지 않았던 부분이기도 하다. 어떻게 self(c++에서 얘기하는 this 포인터)만을
넘겨서 서버가 수많은 클라이언트들을 처리한단 말인가? 해답은 바로 TCustomWinSocket이 들고 있었다. TCustomWinSocket이
생성되면 WSAAsyncSelect 스타일의 소켓으로 설정하게 되며 이때 AllocateHwnd 함수를 호출하게 되는데 여기가 중요하다. 왜냐하면
AllocateHwnd를 이용하여 각각의 TCustomWinSocket은 새로운 윈도우 핸들을 가지기 때문이다. 즉 하나의
TCustomWinSocket이 하나의 윈도우 핸들을 가지게 되는 것이다. 이렇게 생성된 Client들은 FConnections라는 Tlist에
추가된다. 일반적으로 scktcomp.pas 소스파일을 분석해 보지 않은 분들은 OnClientRead등의 이벤트에 파라메터로 넘어오는
TcustomSocket을 찾기 위해 FConnections를 루프로 돌아서 찾아준다고 생각하는 분들이 많다. 하지만 Borland
Socket은 그런 방법을 사용하지 않으며 조금은 특이한 방법을 사용하게 된다.
쉬운 이해를 돕기 위해 아래의 그림을 참조하자.
<그림 1>
TServerSocket의 Client 관리방법
TServerSocket에 Client로부터 접속요청이 오게되면 FD_ACCEPT 메시지에서
GeTClientSocket함수를 이용하여 TServerClientWinSocket라는(TCustomWinSocket에서 상속받은,
Server에서 Client를 관리하기 위한 클래스) 새로운 클래스가 생성된다. TServerClientWinSocket이 생성되면
FConnections에 등록됨으로써 Client의 접속에 관련된 작업을 완료하게 된다. 이런형태로 Client가 3개가 접속하게 되면
<그림 1>과 같은 상태가 된다. 이로써 우리가 사용했던 ServerSocket.Socket.Connections의 사용이 가능하게
되는 것이다. 핵심은 무엇인가? TServerSocket이 수많은 Client들을 관리하기 위해 특별한 자료구조를 사용하는 것이 아니라 각각의
Socket Class에 윈도우 핸들을 할당함으로써 I/O가 발생한 Client를 찾는 일을 O/S에게 떠넘기게 되는 것이다.
그럼 이번엔 (1)번 소켓의 동작에 관해서 설명하겠다. 1번 소켓이 대기상태에서 데이터를 수신하게 되면 OnClientRead 이벤트를
발생시킬 때 Socket Class은 어떻게 찾게 될까? 위의 그림을 보면 알겠지만 Socket Class가 Wiindow Handle을 직접
가지고 있으므로 데이터를 수신한 Socket Class가 직접 OnClientRead를 호출하게 된다. 이렇게 된 배경은 앞에서도 설명했지만
TCustomWinSocket이 각각의 윈도우 핸들을 가지고 있기 때문이다. MSDN에서 제공해주는 예제 소스를 분석해보면 VC++은 다른 방식을
취하고 있다.Chatsrvr라는 이름으로 포함되어 있는 예제 프로그램인데 CAsyncSocket을 이용한 방법이다.
Chatsrvr에서는 다음과 같은 형식으로 Client를 구현한다.
<그림 2>
CAsyncSocket의 Client 관리방법
Main Window Handle이란 엄밀히 말해서 Listen Socket의 핸들이다.
Listen Socket에서 Accept한 Socket들은 우선 m_pmapSocketHandle이라는 CmapPtrToPtr 형태의 자료구조를
사용한다(MAP은 키값에 해당하는 항목을 최대한 빨리 찾을 수 있도록 만들어진 것이다.). 즉 Borland Socket에서 Socket
Class에 각각의 윈도우 핸들을 적용하여 O/S가 호출하게 만들었던 부분들을 m_pmapSocketHandle로 관리를 하게 되는 것이다. M_pmapSocketHandle에서는
Socket의 핸들을 이용하여 CAsyncSocket의 Class Pointer를 찾게 된다.
CAsyncSocket에서는 Client의 관리측면에서 Borland Socket보다는 적은 부분을
관리하고 있다. 즉, Accpet된 소켓들을 m_pmapSocketHandle을 이용하여 사용자에게 제어할수 있는 기회를 주는 것 만으로 의무를
다하는 것이다. Borland Socket에서 사용하는 FConnections와 같은 기능을 사용하기 위해서는 직접 Client관리를 해주어야
한다. 이런 직접적인 관리를 위해 Chatsrvr에서는 m_connectionList를 이용하고 있다.
위의 두가지
예를 통해 각각의 특징에 대해서 대충 이해를 하였으리라 생각한다. 위의 내용으로 딱히 어떠한 방식이 우수하다고 말을
할 수는 없다. 나름대로의 장단점이 있기 때문이다. 하지만 VC++에서는 CAsyncSocket의 내용을 비교적 자세히 알아야지 실제 작업을 시작할수
있는 부담이 있는건 사실이다.
이번 강의에서
좀더 많은걸 다루고 싶었지만 개인적인 사정으로 인해 깊이 파고들지 못한점이 아쉽다. 하지만 필자보다 초보자였던 분들에겐
도움이 되었으리라 생각한다. 전반적인 구조에 대한 글은 이만 줄이도록 하겠다. 다음에는 소켓이 접속할 때 주의해야 될 내용에 대해서 다루도록 하겠다.