C++Builder Programming Forum
C++Builder  |  Delphi  |  FireMonkey  |  C/C++  |  Free Pascal  |  Firebird
볼랜드포럼 BorlandForum
 경고! 게시물 작성자의 사전 허락없는 메일주소 추출행위 절대 금지
C++빌더 포럼
Q & A
FAQ
팁&트릭
강좌/문서
자료실
컴포넌트/라이브러리
메신저 프로젝트
볼랜드포럼 홈
헤드라인 뉴스
IT 뉴스
공지사항
자유게시판
해피 브레이크
공동 프로젝트
구인/구직
회원 장터
건의사항
운영진 게시판
회원 메뉴
북마크
볼랜드포럼 광고 모집

C++빌더 팁&트릭
C++Builder Programming Tip&Tricks
[262] [팁 ] 소켓 접속이 끊어졌을 때 다시 연결
나그네 [] 22252 읽음    2001-12-13 11:44
이건 하이텔의 비쥬얼툴동에서 가져왔습니다.
문제가 있으면 삭제해 주세요...ㅅㅅ;

[ 팁 ] 소켓 접속이 끊어졌을 때 다시 연결하기

세번째로 올라가는 팁입니다.
이번 내용은 TCP/IP를 이용한 소켓 통신을 할 때 서버 소켓이 어떤
이유로 인해 접속 중인 클라이언트 소켓과의 접속을 모두 끊었을 때
클라이언트 소켓이 서버에 다시 접속할 수 있는 방법입니다. 처음엔
OnDisconnected 이벤트가 발생할 때 소켓의 Active를 다시 true로
해 주면 될 것이 아닌가 생각했는데, 해 보니까 절대 안 되더군요.
왜 안되었는지 그 이유와, 안되는걸 되게 하는 방법을 이제 설명드
리겠습니다.

먼저 소켓 통신을 테스트 해 볼 프로그램을 만들어야겠죠? 원래 제
대로 할려면 실제 통신을 통해 전송할 패킷부터 만들어서 통신하다
가 접속이 끊어질 때 다시 접속을 시도하게 해야 되는데.. 여기서는
그런 귀찮은 거 다 빼고 접속/해제/재시도의 과정만 테스트하도록
하겠습니다. 아.. 참고로 소켓 통신 프로그램을 만들려면 BCB 3.0
C/S 슈트가 필요합니다. 스탠다드에서 테스트 하시려면 통신망에 돌
아다니는 적당한 소켓 컴포넌트 구해서 쓰세요. 그러나 이 팁에서
설명하는 과정과 틀린 설치 방법은 제가 책임 못 집니다. -_-

두개의 프로젝트가 필요합니다. 하나는 서버 프로젝트이고 다른 하나
는 클라이언트 프로젝트입니다. 서버 프로젝트에는 ServerSocket,
Button, ListBox 컴포넌트를 각각 갖다 놓습니다. ServerSocket의 속
성 중 Active=false, Port=4000(아니면 적당한 다른 숫자)으로 각각
설정합니다. Active는 서버소켓이 동작 중인지 나타내는 플래그이고,
Port는 서버 소켓이 통신할 포트 번호입니다. 포트 번호는 다른 소켓
프로그램이 사용하는 포트 번호와 중복되는 안되니까 1000보다 큰 값
중 아무거나 적어주면 재수없게 다른 프로그램이 사용하는 포트 번호
를 적지 않는 한 제대로 동작합니다. 다른 속성은 묻지 마세요. 저도
잘 모릅니다.. 아~ 오늘 다 드러나는 승이의 무식~무식~무식~ ㅠ.ㅠ

버튼의 OnClick 이벤트에 다음과 같은 코드를 적어줍니다.
void __fastcall TServerForm::ActiveBtnClick(TObject *Sender)
{
ServerSocket1->Active = !ServerSocket1->Active;
if (ServerSocket1->Active)
ListBox1->Add("서버를 활성화시켰습니다.");
else
ListBox1->Add("서버를 비활성화시켰습니다.");
}

버튼의 기능이 뭔지 이제 눈치까셨죠? 서버 소켓을 활성/비활성 상태
로 만들어 주는 기능입니다. 서버 소켓의 Active가 true로 설정되면
자동으로 주어진 포트번호에 대해 Listen 상태로 동작합니다.
여기서 Listen 상태란 클라이언트가 서버에 접속을 요청할 때 요청을
수락할 수 있는 상태를 말합니다. Active=true만 해 주면 서버가 다
알아서 해 준다고 생각하시면 되겠습니다.

이제 ServerSocket의 OnAccept 이벤트에 대해 다음과 같은 코드를 추
가합니다.
void __fastcall TServerForm::ServerSocket1Accept(TObject *Sender,
TCustomWinSocket *Socket)
{
ListBox1->Add(Socket->RemoteAddress+"에서 접속했습니다.");
}
위의 기능은 클라이언트가 접속을 요청해서 요청을 받아줬을 때 클라
이언트가 접속한 주소를 표시하는 기능입니다.

마지막으로 클라이언트가 접속을 끊은 경우에 대한 표시입니다.
void __fastcall TServerForm::ServerSocket1ClientDisconnect(TObject *Sender,
TCustomWinSocket *Socket)
{
ListBox1->Add(Socket->RemoteAddress+"에서 접속을 끊었습니다.");
}
이 기능은 클라이언트가 접속을 끊었을 때 접속한 주소에서 끊었음을
알려주는 기능입니다. 쉽죠? ^_^

자, 이제 클라이언트 프로젝트입니다. ClientSocket, Button, ListBox
컴포넌트를 폼 위에 이쁘게(서버는 못 생겨도 상관없다. 그러나
클라이언트가 못 생기면..? 보는 내가 눈이 아프다) 배치합니다. 그리
고 ClientSocket의 속성 중 Port는 서버 프로젝트의 ServerSocket에 설
정해 준 Port와 같은 값을 씁니다. 그리고 이제 주소를 적어 줘야 되는
데.. 여기서 문제가 생깁니다. 내 PC는 네트워크 안되는데 어떻게 해야
테스트 할 수 있느냐? 아시는 분은 다 아시겠지만 모르는 분들을 위해
네트워크 안되도 서버에 접속할 수 있는 주소를 알려 드리겠습니다.
어차피 서버가 실행되는 컴퓨터도 자기 꺼, 클라이언트가 실행되는 컴
퓨터도 자기 꺼입니다 --;;; 그러면 자기 PC에서 자기 PC로 네트워크
접속하는 법은? 127.0.0.1, 즉 localhost를 가리키는 주소로 접속하면
됩니다. 따라서 ClientSocket이 접속해야 할 Address에 127.0.0.1을 입
력하시면 자기가 만든 서버에 접근할 수 있습니다.

이제 클라이언트에서 서버에 접속하게 하는 부분입니다. Button의
OnClick 이벤트에 다음의 코드를 추가합니다.
void __fastcall TClientForm::ActiveBtnClick(TObject *Sender)
{
ClientSocket1->Active = !ClientSocket1->Active;
if (ClientSocket1->Active)
ListBox1->Add("서버에 접속 시도합니다.");
else
ListBox1->Add("서버와 연결이 끊어졌습니다.");
}

그리고 실제로 접속 시도 중인지 알아 보기 위해 ClientSocket의
OnConnecting 이벤트에 다음의 코드를 추가합니다.
void __fastcall TClientForm::ClientSocket1Connecting(TObject *Sender,
TCustomWinSocket *Socket)
{
ListBox1->Add("서버에 접속 시도 중입니다.");
}

클라이언트가 서버에 완전히 접속 되었는지 알아 보기 위해 ClientSocket의
OnConnected 이벤트에 다음의 코드를 추가합니다.
void __fastcall TClientForm::ClientSocket1Connect(TObject *Sender,
TCustomWinSocket *Socket)
{
ListBox1->Add("서버에 접속했습니다.");
}

마지막으로 서버와의 접속이 끊어졌는지 알아보기 위해 ClientSocket의
OnDisconnected 이벤트에 다음의 코드를 추가합니다.
void __fastcall TClientForm::ClientSocket1Disconnect(TObject *Sender,
TCustomWinSocket *Socket)
{
ListBox1->Add("서버와 연결이 끊어졌습니다.");
}

이제 서버와 클라이언트의 기본적인 디자인이 끝났습니다. 서버 프로그
램과 클라이언트 프로그램을 각각 실행시키고 서버의 Active 버튼을 먼
저 눌러 서버를 활성화 시키고, 클라이언트의 Active 버튼을 눌러 서버
로 접속을 시도해 보십시오. 그리고 Active 버튼을 다시 눌러 접속을
끊어 보십시오. 접속/해제 메시지가 리스트박스에 이쁘게 나올 겁니다.

여기까지는 앞으로 할 내용의 기본단계이고.. 이제 오늘 내용의 주제인
자동으로 다시 접속하는 법입니다. 클라이언트가 접속 끊으면 별 상관
없는데 서버가 죽으면 클라이언트는 다시 접속하려고 하지요. 이걸 자
동으로 하게 하는 방법입니다.

서버가 접속을 끊던 클라이언트가 스스로 접속을 끊던 ClientSocket에는
OnDisconnected 이벤트가 발생합니다. 자, 이제 단순한 방법 생각해 볼
수 있겠죠? '접속이 끊어졌으니 다시 접속시켜 주면 된다. 다시 접속하
려면? ClientSocket의 Active=true로 다시 해 주면 된다' 이런 결론이
나오게 됩니다. 그래서 다음의 코드와 같이 코딩하게 됩니다.
void __fastcall TClientForm::ClientSocket1Disconnect(TObject *Sender,
TCustomWinSocket *Socket)
{
ListBox1->Add("서버와 연결이 끊어졌습니다.");
ClientSocket1->Active = true;
}

그러나 이 코딩의 결과를 말씀드리자면 백날 해도 접속이 안 됩니
다. 물론 Active=true로 설정하는 것 자체야 되죠. 그러나 그렇게 해도
접속은 안 됩니다(접속 시도 중 메시지는 뜹니다). 왜 안되느냐?

볼랜드 온라인 도움말에서 OnDisconnedted 이벤트에 대해 찾아 보면
OnDisconnected 이벤트는 클라이언트 소켓이 서버 소켓과의 연결을 닫기
전에 발생한다고 되어 있습니다. 이때 소켓 프로퍼티 Active=false로
설정된 뒤에 발생하지만 연결 그 자체는 아직 완전히 닫히지 않은 상태
라고 설명되어 있습니다. 즉 OnDisconnected 이벤트 핸들러 함수를 완
전히 종료한 뒤에야 연결을 완전히 닫아 버리기 때문에 OnDisconnect 안
에서 아무리 Active=true를 해 줘도 OnDisconnect 종료 순간 다시 닫혀
버리는 것입니다.

여기서 이렇게 생각해 볼 수도 있습니다. '지가 연결 닫는 걸 기다리지
말고 내가 먼저 연결 닫으면 안되느냐. Socket의 메써드 중에 Close()가
있던데 Active=true로 하기 전에 Socket->Close()를 실행하면 될 것 아닌
가'. 아주 좋은 아이디어입니다. 박수~~ (캬캬.. 사실은 제가 해 본 방법
이죠) 이 코드는 아마 다음과 같겠죠?
void __fastcall TClientForm::ClientSocket1Disconnect(TObject *Sender,
TCustomWinSocket *Socket)
{
ListBox1->Add("서버와 연결이 끊어졌습니다.");
Socket->Close(); // 또는 Socket->Connected = false;
ClientSocket1->Active = true;
}

그런데 이 코드 역시 결과를 말씀드리자면 안됩니다(으하하.. 좋은 아이
디어라고 박수 쳐 놓고 안된다고 뻔뻔스럽게 말하는 나는 누구일까..)
Socket->Close()를 실행하면 Socket의 OnDisconnected 이벤트가 다시 발
생합니다(왜 다시 발생하는지 자세한 과정은 생략합니다. 저도 컴포넌트
제작을 공부하면서 알게 된 것이기에 추측만 할 수 있을 뿐입니다).
Socket->Close() 하자 마자 OnDisconnected 발생해서 또 Socket->Close(),
또 OnDisconnected 발생, Socket->Close().. 이런식으로 무한히 Close()
만 실행하다가 결국엔 리소스 부족으로 PC가 맛이 가게 됩니다.

그래서 다른 방법을 시도해 봐야겠지요. 저는 이 사태를 해결하기 위해
타이머를 사용했습니다(물론 2가 사용한 방법이 최선이라고 생각지는
않습니다. 더 좋은 방법을 알고 계시는 분이 계시면 정보 교류를 위해
수고를 감수하시고 제시해 주시면 고맙겠습니다).

타이머를 클라이언트 폼 위에 갖다 놓고 이름을 RetryTimer로 바꿉니다.
주기는 빨라도 상관없지만 1초(Interval=1000)면 적당합니다. 그리고
타이머의 Enabled=false로 해 주고, 이벤트에 다음의 코드를 추가합니다.
void __fastcall TClientForm::RetryTimerTimer(TObject *Sender)
{
if (!ClientSocket1->Socket->Connected)
{
RetryTimer->Enabled = false;
ClientSocket1->Active = true;
}
}

OnDisconnected 이벤트 핸들러 함수에 대해서도 수정해야겠지요?
void __fastcall TClientForm::ClientSocket1Disconnect(TObject *Sender,
TCustomWinSocket *Socket)
{
ListBox1->Add("서버와 연결이 끊어졌습니다.");
RetryTimer->Enabled = true;
}

타이머 이벤트에서 if 조건문을 사용한 것은 혹시라도 있을지 모르는
타이머 이벤트가 발생했을 때 아직 접속 중인 상태에서 접속 시도하려
고 하는 것을 피하기 위해서입니다. 아직 접속 중이라면 접속이 완전히
해제될 때까지 계속 타이머 이벤트를 발생시키다가, 접속이 완전히 끊
어지면 타이머를 중단시키고 재접속하려고 합니다.

이제 자동 재접속 기능이 만들어졌습니다. 프로젝트를 저장하고 다시
컴파일해서 실행해 봅니다. 서버를 동작시키고 클라이언트를 서버에 접
속시킨 후 서버를 비활성화 시킵니다. 그러면 클라이언트가 서버에 재
접속을 시도해야 하는데.. Asynchronous 에러가 한번 뜨고는 더이상 접
속을 시도하지 않을 겁니다. 뭐가 문제일까요?

서버가 죽은 후 다시 살아나기까지는 시간이 좀 걸립니다. 그런데 클라
이언트에서는 서버와의 접속이 끊어진 후 다시 접속시도하기까지는 길
어도 절대 2초를 넘지 않습니다. 그 시간에 서버가 살아나면 다행이지
대개의 경우 여전히 죽어 있는 상태죠. 그래서 서버와의 연결이 실패하
면 다시 접속을 해 주어야 하는데.. 지금까지의 코딩 내용에는 그런 것
이 없었지요? OnDisconnected는 연결되어 있는 것이 끊어졌을 때 발생
하는 것이니까 안되고..

자, 그래서 ClientSocket의 OnError 이벤트를 건드리는 겁니다. 다음의
내용은 OnError 이벤트의 코드입니다.
void __fastcall TMonitorMainForm::ClientSocket1Error(TObject *Sender,
TCustomWinSocket *Socket, TErrorEvent ErrorEvent, int &ErrorCode)
{
if ( (ErrorEvent == eeConnect) )
{
ListBox1->Add("접속시도가 실패했습니다.");
ErrorCode = 0;
RetryTimer->Enabled = true;
}
}

소켓을 사용하던 중에 에러가 발생하는 ErrorEvent에 에러가 발생한
이벤트가 저장되어 오고, ErrorCode에 에러의 코드가 넘겨져 옵니다.
클라이어트가 서버에 재접속할 때 발생했던 Asynchronous 에러는 클
라이언트가 서버에 접속을 할 때 발생한 에러입니다. 그래서 ErrorEvent
에는 eeConnect라는 값이 저장되어 있습니다. 자세한 내용은 도움말
을 참조하시기 바랍니다. 따라서 접속시도 중에 에러가 발생한 경우
에러 이벤트가 eeConnect인지 검사해 보고 만일 맞으면 재접속 시도
를 다시 하면 됩니다(RetryTimer를 다시 동작시키는 거죠). 이때
ErrorCode에 에러의 ID가 저장되어 있는데 그냥 둘 경우 화면에 계속
Asynchronous 에러 대화상자가 뜹니다. 이것을 막기 위해서 '에러가
발생했을 때의 처리를 해 줬으니 에러코드를 무시해라'라는 뜻으로
ErrorCode=0으로 해 줍니다. 그러면 더이상 에러 대화상자가 뜨지
않을 겁니다.

이제 소켓이 끊어진 경우의 자동 재접속에 대한 내용을 모두 설명드
렸습니다. 다시 테스트 해 봅시다.
1. 서버, 클라이언트 프로그램을 각각 실행
2. 서버 소켓을 활성화 한 후 클라이언트 소켓을 활성화
3. 서버 소켓을 비활성화하고 클라이언트에서 접속시도 실패 메시
지가 발생하는지 확인
4. 클라이언트에서 접속시도 실패 메시지가 발생하면 서버 소켓을
다시 활성화 한 후 클라이언트에서 재접속했는지 확인
5. 모두 잘 되었으면 박수~

남은 것은 재접속 기능을 좀 더 깔끔하게 다듬는 것입니다. 단순히
재접속 타이머만 동작시켜 바로 재접속시키는 것은 사용자가 더이
상 접속을 하고 싶지 않다는 의사를 무시하는 것이 됩니다. 그래서
자동 재접속을 하기 전에 'x초 후에 재접속합니다' 메시지박스를
화면에 보여주고 재접속 의사를 물어 보는 것이 좋죠.
어떻게 다듬느냐는 여러분의 미적 감각에 달렸습니다. ^_^

제가 올리는 팁의 내용은 별로 어렵지 않고 전문가 분들이 쉽게 구현
할 수 있는 내용들입니다(거뤠~! 무식을 팍팍 드러내는 거야~ T.T).
때문에 제가 올리는 내용이 최선의 방법이라는 보장은 절대 없으니까
제가 알려 드리는 내용에 구애받지 마시고, 더 좋은 방법이 있으면
그 방법을 쓰시고 같이 정보를 교류하였으면 합니다.
김중현 [resume97]   2007-10-16 15:50 X
좋은정보가 되었습니다. 감사합니다 ㅎㅎ

+ -

관련 글 리스트
262 [팁 ] 소켓 접속이 끊어졌을 때 다시 연결 나그네 22252 2001/12/13
Google
Copyright © 1999-2015, borlandforum.com. All right reserved.