윈도우 소켓을 이용한 네트웍 프로그래밍]
*[이벤트 메시지를 받아봐요~]
*[send()/recv()와 큐]
6. 윈도우 소켓을 이용한 데이터 송수신.
[이벤트 메시지를 받아봐요~]
이제부터는 통신상에서 어떤 메시지(접속,끊김,읽기)가 발생했을때 그에 맞는 함
수로 대응하는 방법을 보겠습니다. 그 형식은 아래와 같습니다. 우선은 코드를 보
도록 하죠.
WSANETWORKEVENTS event;
WSAEVENT hRecvEvent = WSACreateEvent();
WSAEventSelect( Socket, hRecvEvent, FD_ACCEPT | FD_READ | FD_CLOSE );
while( 1 )
{
Sleep( 10 ); //루프가 10/1000초에 한번씩 동작합니다.
WSAEnumNetworkEvents( Socket, hRecvEvent, &event);
if((event.lNetworkEvents & FD_ACCEPT) == FD_ACCEPT)
{
accept(); //접속을 받아들임.
}
if((event.lNetworkEvents & FD_READ) == FD_READ)
{
recv(); //데이터를 읽어들임.
}
if((event.lNetworkEvents & FD_CLOSE) == FD_CLOSE)
{
closesocket( Socket ); //해당 소켓이 닫혔음.
}
}
대강 감을 잡으신분들은 접속, recv와 closesocket과 관계된 즉, 데이터가 온 경
우와 소켓이 닫힌 경우에 대한 사건이 발생한 경우라는 걸 이미 눈치채셨을지도...
그럼 위와 같이 소켓에 관련된 부분의 처리를 어디에 넣느냐?는 지난 게시물 마지
막에서 accept부분을 쓰레드로 띄웠던 부분이 좋을듯하군요. 즉, 위의 구조는 하나
의 쓰레드안에 넣어두고 접속, 데이터 수신, 소켓닫힘을 감지하는 기능을 가진 소
켓감시자가 되는겁니다. ^^; 사용된 함수를 보도록 하죠.
WSANETWORKEVENTS event; //네트웍 이벤트를 저장할 구조체.
WSAEVENT hRecvEvent = WSACreateEvent(); //윈속 이벤트 오브젝트를 초기화하고
//그 핸들을 넘깁니다.
//특정소켓에서 얻고 싶은 이벤트를 설정합니다.
int WSAEventSelect ( SOCKET s, WSAEVENT hEventObject,long lNetworkEvents );
* s : 이벤트를 얻고 싶은 소켓.
* hEventObject : 윈속 이벤트 오브젝트의 핸들
* lNetworkEvents : 감지하고 싶은 오브젝트를 정의합니다.
리턴값 : 0 이면 올바로 동작한 겁니다.
//특정 소켓에서 이벤트가 발생했는지를 알아봅니다.
int WSAEnumNetworkEvents ( SOCKET s, WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents );
s : 감시할 소켓
hEventObject : 이벤트와 관련된 핸들
lpNetworkEvents : 에러 기록이나 네트웍에서 발생한 이벤트(접속,송수신,끊김등
)가 저장될 곳.
리턴값 : 0 이면 올바로 동작한 겁니다.
[send/recv와 큐]
*send
이미 앞 게시물에서 send()함수는 한번 다룬적이 있습니다만, 복습을 하죠.
char SendMessage[6] = "Test!";
int SendSize = 6;
int ActualSendSize;
ActualSendSize = send( ClientSocket, SendMessage, SendSize, 0 );
*recv
int ActualRecvSize;
char Buffer[999]; //적당히 큰 버퍼를 잡아줍니다.
ActualRecvSize = recv( ClientSocket, Buffer, sizeof Buffer, 0 );
위와 같아 한쪽에서 send로 보내면 반대쪽에서는 recv로 받으면 됩니다. 당연한
거죠. ^^; 한가지 주의할 점이라면 여러분께서 보낸 크기만큼 끊겨서 데이터가 오
지는 않는다는겁니다. 예를 보죠.
[서버]
send( 5바이트 )
send( 5바이트 )
send( 10바이트 )
[클라이언트]
recv( 5바이트 )
recv( 5바이트 )
recv( 10바이트 )
위와같은 형식이 아니라는거죠. 어느정도의 크기로 모여서 오기 때문입니다.
예를 들면 만약 클라이언트가 10바이트씩 받는다면...
recv( 5바이트 );
다음 5바이트가 오기를 기다린다.
recv( 5바이트 ); 10바이트가 되었으니 온 데이터를 인정한다.
recv( 10바이트 ); 10바이트가 되었으니 온 데이터를 인정한다.
만약 20바이트씩 받는다면?
recv( 5바이트 ) //20바이트가 되지 않았으므로 기다린다.
recv( 5바이트 ) //20바이트가 되지 않았으므로 기다린다.
recv( 10바이트 ) // 20 바이트가 되었으므로 그동안 모인 값을 인정한다.
물론 setsockopt함수를 이용해 오는 족족 받는 방법도 있습니다. 그리고 위의 경
우가 항상 적용되는것은 아닙니다. 일종의 시간 단위로 이루어지는 경우가 대부분
입니다. 그러니까 recv함수가 20바이트씩 받는 구조일때 1바이트씩 19번 보내도 반
응이 전혀 없는냐? 그렇지 않습니다. 지정된 시간내에 20바이트를 채우지 못하면,
그동안 받은 값만을 인정해줍니다. 즉 send와 recv가 항상 1:1로 대응되지 않는 상
황이 발생하기 때문에 보내고 받을 데이터를 임시로 보관해 두었다가 보내주는 큐
가 필요하게 되는거죠. recv에 대한 큐를 어떻게 처리하는지 아래를 보도록하죠.
큐를 만드는 방법도 여러가지가 있겠죠. 아래에 제가 만든 방법말고 다른 방법을
생각해 보심도 좋겠죠. ^^;
unsigned long __stdcall ChannelManagementThread( void *arg )
{
char Buffer[80];
//큐를 위한 배열 적당히 크게 잡아둔다.
char Queue[999];
//현재 큐 안에서의 시작점(위치).
unsigned short QueuePosition = 0;
int retval;
//큐를 초기화한다.
memset( Queue, 0, 999 );
//네트웍 이벤트를 위한 기본설정.
WSANETWORKEVENTS event;
WSAEVENT hRecvEvent = WSACreateEvent();
while( 1 )
{
Sleep( 1 );
//이벤트를 초기화한다.
memset( &event, 0, sizeof event );
//소켓에서 얻고 싶은 이벤트를 초기환한다.
WSAEventSelect( Socket, hRecvEvent, FD_READ );
//소켓에서 발생한 이벤트를 얻어낸다.
WSAEnumNetworkEvents( PlayerSocket, hRecvEvent, &event);
//뭔가 읽을게 있으면? 즉, recv함수를 실행해야하면?
if((event.lNetworkEvents & FD_READ) == FD_READ)
{
//데이터를 읽어본다. 실제 몇 바이트를 읽었는지 retval에
//저장한다.
retval = recv( Socket, Buffer, sizeof Buffer, 0 );
//뭔가 읽은게 있으면?
if( retval > 0 )
{
unsigned short Size;
memcpy( &Queue[QueuePosition], Buffer, retval );
QueuePosition += retval;
while( 1 )
{
//아래는 패킷의 크기를 읽어내는겁니다. 데이터를 보낼때 맨
//앞 2바이트에 읽을 전체 데이터의 크기를 보내는거죠. 앞에
//2바이트에 패킷 전체 사이즈를 보내는 부분은 프로토콜에
//대해서 언급할때 다시 보도록 하죠. 지금은 그냥 넘어가요~
memcpy( &Size, &Queue[0], 2 );
//실제 패킷크기만큼 읽었다면?
if( QueuePosition >= Size )
{
char *Message, ReturnMessage;
Message = (char *)malloc( Size );
memcpy( Message, &Queue[0], Size );
//받은 데이터를 인정하고 그에 따른 처리를한다.
QueuePosition-=Size;
memcpy( &Queue[0], &Queue[Size], QueuePosition );
free( Message );
}
else
break;
}
}
}
}
WSACloseEvent( hRecvEvent );
ExitThread( 1 );
}
휴우.... 단지, 받기하나에 대한 큐 처리인데 젤 길군요. 위의 예도 뭐 그리 좋은
건 아닙니다만, 참고하시고 더 좋은 방법을 생각해 보심도 좋겠죠. ^^;
send도 비슷합니다만, 위와는 조금 다릅니다. 언제 받을지 모르는 경우하고 언제
보낼지를 확실히 아는 경우는 당연히 다릅니다. 생각해 보면 여러가지 방법이 있겠
죠? 현재상태가 전송 가능한 상태인지를 감지하고 전송 가능하면 보내는거죠. 그렇
지 않을시에는 데이터를 큐에 저장해 두었다가 전송이 가능해진 시점부터 큐에서
데이터를 읽어내서 보내주는 것도 하나의 방법이 될겁니다. 앞에서도 이야기했지만
, send/recv 함수가 1:1 대응되는 것도 아니고 원하는 만큼이 보내지는 경우가 아
닐때도 있습니다. send의 경우는 아래와 같이만 해줘도 원하는 만큼을 보내줄수는
있을겁니다. 하지만 전송 가능한 상태인지를 확인해 주면 더 완벽해 질겁니다.
bool SendData( char *SendBuffer, int SendBufferSize, int timeout )
{
int StartTime;
int CurrentTime;
int SendedDataSize = 0;
StartTime = timeGetTime();
while( SendedDataSize <= SendBufferSize )
{
CurrentTime = timeGetTime();
SendedBufferSize += send( SendBuffer, SendBufferSize, 0 );
if( CurrentTime-StartTime < timeout )
{
return false;
}
}
return true;
}
위의 경우는 원하는 만큼 원하는 시간안에 보냈는지를 확인하는 함수입니다. 가능
하면, 현재가 send가능한 상황인지도 파악해서 생각하고 send큐에 그 데이터를 넣
어두는 것도 방법입니다. 처음부터 전송이 가능한지 알고 있다면 굳이 send를 호출
하지 않아도 되기 때문이죠. ^^; 뭐 그건 여러분들께서 설계하기 나름입니다.
보통 send후 다른 소켓 작업때문에 전송이 불가능한 경우는 send함수에서 리턴되는
값은 당연히 SOCKET_ERROR죠. WSAGetLastError()함수를 이용해 어떤 에러였는지를
판단해 봅니다. 그 값이 WSAEWOULDBLOCK(에러라기 보다는 경고죠. 함수가 호출된
순간에 즉시 수행이 불가능해 뒤로 밀린 경우라고 보시면 됩니다.)이면 다시 기다
려 보면 되고 그렇지 않을시엔 거의 연결이 끊긴 경우라고 보면 됩니다만, 에러코
드에 따른 처리를 적절히 해 주셔야 되겠죠? 만약 즉시 수행이 불가능해 뒤로 밀린
경우라면, if((event.lNetworkEvents & FD_WRITE) == FD_WRITE)부분를 네트웍 이벤
트 관리자에 추가하셔서 전송이 가능해진 시점을 알아내고 그때 여러분께서 만든
send 큐에 있는 데이터를 전부 보내버리면 되겠죠. ^^; 위의 Time-Out개념은 보다
는 이 방법이 더 직관적이고 유리할듯 합니다. 실제 이에 대한 예는 마지막 글이
될 채팅 클라이언트/서버 프로그램 제작에서 자세히 다루게 됩니다.
예를 보죠.
void SendData( .., .. )
{
int Result;
Result = send();
if( Result == SOCKET_ERROR )
{
if( WSAGetLastError() == WSAEWOULDBLOCK )
{
//큐에 데이터를 넣어두고 나중에 다시 보냅니다.
}
else
{
//연결을 종료합니다.
}
}
}
send는 위의 두가지 원칙만 잘 지킨다면 큰 무리없이 수행될겁니다
*[이벤트 메시지를 받아봐요~]
*[send()/recv()와 큐]
6. 윈도우 소켓을 이용한 데이터 송수신.
[이벤트 메시지를 받아봐요~]
이제부터는 통신상에서 어떤 메시지(접속,끊김,읽기)가 발생했을때 그에 맞는 함
수로 대응하는 방법을 보겠습니다. 그 형식은 아래와 같습니다. 우선은 코드를 보
도록 하죠.
WSANETWORKEVENTS event;
WSAEVENT hRecvEvent = WSACreateEvent();
WSAEventSelect( Socket, hRecvEvent, FD_ACCEPT | FD_READ | FD_CLOSE );
while( 1 )
{
Sleep( 10 ); //루프가 10/1000초에 한번씩 동작합니다.
WSAEnumNetworkEvents( Socket, hRecvEvent, &event);
if((event.lNetworkEvents & FD_ACCEPT) == FD_ACCEPT)
{
accept(); //접속을 받아들임.
}
if((event.lNetworkEvents & FD_READ) == FD_READ)
{
recv(); //데이터를 읽어들임.
}
if((event.lNetworkEvents & FD_CLOSE) == FD_CLOSE)
{
closesocket( Socket ); //해당 소켓이 닫혔음.
}
}
대강 감을 잡으신분들은 접속, recv와 closesocket과 관계된 즉, 데이터가 온 경
우와 소켓이 닫힌 경우에 대한 사건이 발생한 경우라는 걸 이미 눈치채셨을지도...
그럼 위와 같이 소켓에 관련된 부분의 처리를 어디에 넣느냐?는 지난 게시물 마지
막에서 accept부분을 쓰레드로 띄웠던 부분이 좋을듯하군요. 즉, 위의 구조는 하나
의 쓰레드안에 넣어두고 접속, 데이터 수신, 소켓닫힘을 감지하는 기능을 가진 소
켓감시자가 되는겁니다. ^^; 사용된 함수를 보도록 하죠.
WSANETWORKEVENTS event; //네트웍 이벤트를 저장할 구조체.
WSAEVENT hRecvEvent = WSACreateEvent(); //윈속 이벤트 오브젝트를 초기화하고
//그 핸들을 넘깁니다.
//특정소켓에서 얻고 싶은 이벤트를 설정합니다.
int WSAEventSelect ( SOCKET s, WSAEVENT hEventObject,long lNetworkEvents );
* s : 이벤트를 얻고 싶은 소켓.
* hEventObject : 윈속 이벤트 오브젝트의 핸들
* lNetworkEvents : 감지하고 싶은 오브젝트를 정의합니다.
리턴값 : 0 이면 올바로 동작한 겁니다.
//특정 소켓에서 이벤트가 발생했는지를 알아봅니다.
int WSAEnumNetworkEvents ( SOCKET s, WSAEVENT hEventObject,
LPWSANETWORKEVENTS lpNetworkEvents );
s : 감시할 소켓
hEventObject : 이벤트와 관련된 핸들
lpNetworkEvents : 에러 기록이나 네트웍에서 발생한 이벤트(접속,송수신,끊김등
)가 저장될 곳.
리턴값 : 0 이면 올바로 동작한 겁니다.
[send/recv와 큐]
*send
이미 앞 게시물에서 send()함수는 한번 다룬적이 있습니다만, 복습을 하죠.
char SendMessage[6] = "Test!";
int SendSize = 6;
int ActualSendSize;
ActualSendSize = send( ClientSocket, SendMessage, SendSize, 0 );
*recv
int ActualRecvSize;
char Buffer[999]; //적당히 큰 버퍼를 잡아줍니다.
ActualRecvSize = recv( ClientSocket, Buffer, sizeof Buffer, 0 );
위와 같아 한쪽에서 send로 보내면 반대쪽에서는 recv로 받으면 됩니다. 당연한
거죠. ^^; 한가지 주의할 점이라면 여러분께서 보낸 크기만큼 끊겨서 데이터가 오
지는 않는다는겁니다. 예를 보죠.
[서버]
send( 5바이트 )
send( 5바이트 )
send( 10바이트 )
[클라이언트]
recv( 5바이트 )
recv( 5바이트 )
recv( 10바이트 )
위와같은 형식이 아니라는거죠. 어느정도의 크기로 모여서 오기 때문입니다.
예를 들면 만약 클라이언트가 10바이트씩 받는다면...
recv( 5바이트 );
다음 5바이트가 오기를 기다린다.
recv( 5바이트 ); 10바이트가 되었으니 온 데이터를 인정한다.
recv( 10바이트 ); 10바이트가 되었으니 온 데이터를 인정한다.
만약 20바이트씩 받는다면?
recv( 5바이트 ) //20바이트가 되지 않았으므로 기다린다.
recv( 5바이트 ) //20바이트가 되지 않았으므로 기다린다.
recv( 10바이트 ) // 20 바이트가 되었으므로 그동안 모인 값을 인정한다.
물론 setsockopt함수를 이용해 오는 족족 받는 방법도 있습니다. 그리고 위의 경
우가 항상 적용되는것은 아닙니다. 일종의 시간 단위로 이루어지는 경우가 대부분
입니다. 그러니까 recv함수가 20바이트씩 받는 구조일때 1바이트씩 19번 보내도 반
응이 전혀 없는냐? 그렇지 않습니다. 지정된 시간내에 20바이트를 채우지 못하면,
그동안 받은 값만을 인정해줍니다. 즉 send와 recv가 항상 1:1로 대응되지 않는 상
황이 발생하기 때문에 보내고 받을 데이터를 임시로 보관해 두었다가 보내주는 큐
가 필요하게 되는거죠. recv에 대한 큐를 어떻게 처리하는지 아래를 보도록하죠.
큐를 만드는 방법도 여러가지가 있겠죠. 아래에 제가 만든 방법말고 다른 방법을
생각해 보심도 좋겠죠. ^^;
unsigned long __stdcall ChannelManagementThread( void *arg )
{
char Buffer[80];
//큐를 위한 배열 적당히 크게 잡아둔다.
char Queue[999];
//현재 큐 안에서의 시작점(위치).
unsigned short QueuePosition = 0;
int retval;
//큐를 초기화한다.
memset( Queue, 0, 999 );
//네트웍 이벤트를 위한 기본설정.
WSANETWORKEVENTS event;
WSAEVENT hRecvEvent = WSACreateEvent();
while( 1 )
{
Sleep( 1 );
//이벤트를 초기화한다.
memset( &event, 0, sizeof event );
//소켓에서 얻고 싶은 이벤트를 초기환한다.
WSAEventSelect( Socket, hRecvEvent, FD_READ );
//소켓에서 발생한 이벤트를 얻어낸다.
WSAEnumNetworkEvents( PlayerSocket, hRecvEvent, &event);
//뭔가 읽을게 있으면? 즉, recv함수를 실행해야하면?
if((event.lNetworkEvents & FD_READ) == FD_READ)
{
//데이터를 읽어본다. 실제 몇 바이트를 읽었는지 retval에
//저장한다.
retval = recv( Socket, Buffer, sizeof Buffer, 0 );
//뭔가 읽은게 있으면?
if( retval > 0 )
{
unsigned short Size;
memcpy( &Queue[QueuePosition], Buffer, retval );
QueuePosition += retval;
while( 1 )
{
//아래는 패킷의 크기를 읽어내는겁니다. 데이터를 보낼때 맨
//앞 2바이트에 읽을 전체 데이터의 크기를 보내는거죠. 앞에
//2바이트에 패킷 전체 사이즈를 보내는 부분은 프로토콜에
//대해서 언급할때 다시 보도록 하죠. 지금은 그냥 넘어가요~
memcpy( &Size, &Queue[0], 2 );
//실제 패킷크기만큼 읽었다면?
if( QueuePosition >= Size )
{
char *Message, ReturnMessage;
Message = (char *)malloc( Size );
memcpy( Message, &Queue[0], Size );
//받은 데이터를 인정하고 그에 따른 처리를한다.
QueuePosition-=Size;
memcpy( &Queue[0], &Queue[Size], QueuePosition );
free( Message );
}
else
break;
}
}
}
}
WSACloseEvent( hRecvEvent );
ExitThread( 1 );
}
휴우.... 단지, 받기하나에 대한 큐 처리인데 젤 길군요. 위의 예도 뭐 그리 좋은
건 아닙니다만, 참고하시고 더 좋은 방법을 생각해 보심도 좋겠죠. ^^;
send도 비슷합니다만, 위와는 조금 다릅니다. 언제 받을지 모르는 경우하고 언제
보낼지를 확실히 아는 경우는 당연히 다릅니다. 생각해 보면 여러가지 방법이 있겠
죠? 현재상태가 전송 가능한 상태인지를 감지하고 전송 가능하면 보내는거죠. 그렇
지 않을시에는 데이터를 큐에 저장해 두었다가 전송이 가능해진 시점부터 큐에서
데이터를 읽어내서 보내주는 것도 하나의 방법이 될겁니다. 앞에서도 이야기했지만
, send/recv 함수가 1:1 대응되는 것도 아니고 원하는 만큼이 보내지는 경우가 아
닐때도 있습니다. send의 경우는 아래와 같이만 해줘도 원하는 만큼을 보내줄수는
있을겁니다. 하지만 전송 가능한 상태인지를 확인해 주면 더 완벽해 질겁니다.
bool SendData( char *SendBuffer, int SendBufferSize, int timeout )
{
int StartTime;
int CurrentTime;
int SendedDataSize = 0;
StartTime = timeGetTime();
while( SendedDataSize <= SendBufferSize )
{
CurrentTime = timeGetTime();
SendedBufferSize += send( SendBuffer, SendBufferSize, 0 );
if( CurrentTime-StartTime < timeout )
{
return false;
}
}
return true;
}
위의 경우는 원하는 만큼 원하는 시간안에 보냈는지를 확인하는 함수입니다. 가능
하면, 현재가 send가능한 상황인지도 파악해서 생각하고 send큐에 그 데이터를 넣
어두는 것도 방법입니다. 처음부터 전송이 가능한지 알고 있다면 굳이 send를 호출
하지 않아도 되기 때문이죠. ^^; 뭐 그건 여러분들께서 설계하기 나름입니다.
보통 send후 다른 소켓 작업때문에 전송이 불가능한 경우는 send함수에서 리턴되는
값은 당연히 SOCKET_ERROR죠. WSAGetLastError()함수를 이용해 어떤 에러였는지를
판단해 봅니다. 그 값이 WSAEWOULDBLOCK(에러라기 보다는 경고죠. 함수가 호출된
순간에 즉시 수행이 불가능해 뒤로 밀린 경우라고 보시면 됩니다.)이면 다시 기다
려 보면 되고 그렇지 않을시엔 거의 연결이 끊긴 경우라고 보면 됩니다만, 에러코
드에 따른 처리를 적절히 해 주셔야 되겠죠? 만약 즉시 수행이 불가능해 뒤로 밀린
경우라면, if((event.lNetworkEvents & FD_WRITE) == FD_WRITE)부분를 네트웍 이벤
트 관리자에 추가하셔서 전송이 가능해진 시점을 알아내고 그때 여러분께서 만든
send 큐에 있는 데이터를 전부 보내버리면 되겠죠. ^^; 위의 Time-Out개념은 보다
는 이 방법이 더 직관적이고 유리할듯 합니다. 실제 이에 대한 예는 마지막 글이
될 채팅 클라이언트/서버 프로그램 제작에서 자세히 다루게 됩니다.
예를 보죠.
void SendData( .., .. )
{
int Result;
Result = send();
if( Result == SOCKET_ERROR )
{
if( WSAGetLastError() == WSAEWOULDBLOCK )
{
//큐에 데이터를 넣어두고 나중에 다시 보냅니다.
}
else
{
//연결을 종료합니다.
}
}
}
send는 위의 두가지 원칙만 잘 지킨다면 큰 무리없이 수행될겁니다