반응형

출처 : http://ospace.tistory.com/189


소켓 관련 자세한 내용 joinc를 참고!!

: http://www.joinc.co.kr/modules/moniwiki/wiki.php/Site/Network_Programing/AdvancedComm/SocketOption


들어가기

소켓 프로그래밍에서 비동기 방식으로 처리를 많이 사용하고 있다. 장점도 있고 단점도 있다. 그러나 최근 고성능 서버 프로그램 작성할 때에는 거의 대부분이 비동기 방식으로 처리한다.
이런 부분의 장점과 단점은 인터넷에 잘 나와있으니 알아서 찾아보시고, 여기에서는 서버 보다는 클라이언트에 집중해보려고 한다. 즉 접속를 하는 시스템을 집중하겠다.

사실 서버에서도 접속을 요청할 수 있다. 이는 push방식 인가 pull 방식 인가에 따라서 서버에서도 사용할 가능성 있다. 이런 push와 pull도 인터넷에서 검색하시길 바란다.

이제 본론으로 들어가보자.


Connect 함수

기본적으로 소켓 생성이 끝나고 연결만 남은 상태라고 보자. 그리고, 대부분 소켓 프로그래밍에 대해 기초적인 부분을 알고 있다고 생각해서 진행하겠다.

int connect(int, const struct sockaddr*, socklen_t); // for linux
int connect(SOCKET, const struct sockaddr*, int); // for windows

많이 보던 함수라서 익숙 할 것이다. 각 인자마다 들어가는 타입이 틀리지는 모르지만, 결국 같은 값을 사용한다. 일반적인 동기 접속이 이뤄지는 경우 반환 값은 다름과 같다.

동기접속 Return Value
성공: 0 반환
실패: -1 (Linux), SOCKET_ERROR(Windows)


비동기 인경우는 조금 다르게 처리한다. 리턴 값이 바로 접속이 성공하면 0 이지만, -1 값이 실패로 처리되지 않는다. -1은 비동기 처리에서는 기본적이며, Linux는 ierrno값을 이용하여 현재 처리되는 상태를 확인하고 Windows는 WSAGetLastError()를 사용해서 에러를 확인한다.

비동기접속 Return Value
성공: 0 반환
진행중: -1 반환. errno값을 비교하여 진행 상태 확인
errno == EINPROGRESS(linux), EAGAIN
WsaGetLastError() == WSAEWOULDBLOCK

한 가지 주의할 것은 linux에서는 처리 중인 상태를 errno값에서 EINPROGRESS를 사용한다. 간혹 EAGIN를 사용하는 프로그램이 있다. 이부분을 좀더 검증이 필요하다.



연결 상태 확인

connect를 호출해서 연결을 한다고 해도, errno 값이 EINPROGRESS라고 해서 연결이 성공적으로 완료되었다고는 말하기 힘들다. 비동기이기 때문에 그 결과를 반환 값에 넣을 수 없다. 그 때 필요한 것이 getsockopt()를 이용한 것이다.

int getsockopt(int, int, int, void*, socklen_t*); // for linux
int getsockopt(SOCKET, int, int char*, int*);   // for windows
그럼 비동인 경우 연결 결과를 가져오는 코드는 다음과 같다.
int error = 0;
socklen_t len = sizeof( error );
if( getsockopt(fd, SOL_SOCKET, SO_ERROR, &error, &len) < 0 ) {
    // 값을 가져오는데 에러 발생
    // errno을 가지고 에러 값을 출력
    // 연결 오류로 처리
}

getsockopt()인 경우는 BSD와 Solaris에서 결과가 매우 다르다. Solaris는 확인하기 어렵고 일단 Linux와 Widows을 중심으로 살펴보았다.

Linux Return Value
성공: 0
실패: -1, 에러 종류는 errno에 저장됨

Windows Return Value
성공: 0
실패: SOCKET_ERROR, 에러 종류는 WSAGetLastError()로 가져옴


둘다 SO_ERROR 값 획득에 성공하면, error를 통해 연결 상태에 대한 결과를 확인해 볼 수 있다.

Linux
ECONNREFUSED: 연결 거부
ETIMEDOUT: 연결 대기 시간 초과
Windows
WSAECONNREFUSED: : 연결 거부
WSATIMEDOUT: : 연결 대기 시간 초과


error 값을 가지고 위의 값과 비교해보면 알 수 있다.
당연히 error 값이 0이면 에러가 없으므로 성공적으로 접속했다는 의미이다.

최종코드

다음은 위의 결과를 정리한 코드이다. 물론 직접 테스트해본 코드가 아니기 때문에 컴파일시 에러가 발생할 수 있다. 그리고 기본 플랫폼은 Linux로 정했다.

int fd = socket(AF_INET, SOCK_STREAM, 0); // tcp socket
if( fcntl( fd, F_SETFL, O_NONBLOCK) == -1 ) {
    return -1; // error
}
// Windows인 경우
// unsigned long nonblock = 1; 
// nonblock 설정
// ioctlsocket(fd, FIONBIO, &nonblock);
// struct sockaddr_in 형의 peer를 초기화함
int result = connect (fd, (struct sockaddr*)&peer, sizeof(peer));
if( 0 == result ) {
    // 연결 성공
} else if ( EINPROGRESS == errno ) {
    // 비동기 연결 이벤트로 등록
} else {
    // 연결 실패
}

비동기 연결 이벤트에 의해서 해당 이벤트가 활성화되어 실행하는 경우, getsockopt()를 이용해서 결과를 확인한다.

// 연결 이벤트 헨들러
int error = 0;
socklen_t err_len = sizeof(error);
if( getsockopt(fd, SOL_SOCKET, SO_ERROR, (void*) &error, &len) < 0 ) {
    // 결과값 갖고 오는데 에러 발생
    // 연결중 에러 발생으로 errno 값으로 결과 확인
    // 에러 리턴
}
if( 0 != error ) {
    if( ECONNREFUSED == error ) {
        // 연결 거부로 연결 실패
        // 에러 처리
    } else if( ETIMEOUT == error ) {
        // 연결 대기 시간 초과로 연결 실패
        // 에러 처리
    }
    // 원인 모를 에러?
    // 알아서 처리 ㅡ.ㅡ;
}
// error가 0이기 때문에 연결 성공 ^^


이상입니다. 안에 코드는 두서없이 작성한 것이라서 나름대로 적당히 수정하면 되겠죠. error값은 switch문을 사용하는 식으로 말입니다.



결론

비동기 연결에 대해서 다룬 경우는 거의 없다. Stevnes 아저씨의 책을 많이 참고 했다. 이를 이용해서 다중 연결 요청을 만드는 프로그램을 작성할 수 있었다.

마지막으로 위에서 사용한 헤더 파일에 대해서 정리해보았다. 물론 다 알고 계신분은 상관없지만, 그 때마다 찾아쓰는 나와 같은 사람은 헤더파일 알아내느라 참 힘들다. ㅡ.ㅡ;

Windows
WinSock2.h: connect(), getsockopt(), WSAGetlastError(), WSAECONNREFUSED, WSAETIMEOUT

Linux
sys/socket.h: connect(), getsockopt()
errno.h: ECONNREFUSED, ETIMEOUT 정의

이상입니다. 모드 즐프하세요. ospace.

참조

[1] Stevens, Unix Network Programming Volumn1 2ed
[2] man, connect, getsockopt
[3] MSDN, connect, getsockopt


-------------------------------------------------------------------------------------------------------
안녕하세요.

저 같은경우 비동기 커넥트시

connect 후 커넥트가 진행중이면 read 셋,write 셋, except 셋 에다
소켓을 셋한후 select 후 write 셋을 검사하여 write 셋에 소켓이
셋되있으면 접속된걸로 간주하고 except 셋에 소켓이 셋 되잇으면
실패한걸로 간주합니다.

여기서 궁금한거는 최초의 select 에서 아무런 이벤트(접속성공,실패)를
감지하지 못했을경우 다시 감지를 하기 위해서 write 셋과 except 셋에
다시 소켓을 셋해야 하는지 궁금합니다. 당연히 그래야 할것 같은데
제가 테스트(윈도우에서)할때는 최초의 select 에서 접속성공 또는 실패
를 알수 있어서 그런가보다 하고(최초의 select에서 접속성공실패여부 확인)
알고 있었습니다.

리눅스(SuSE) 에서 상황을 접속실패로 해놓고(접속할서버를닫아놓고) 테스트
해보면 최초의 select 호출후 read,write,except 의 각 fd_set 의 변화를
살펴봤더니 read 셋과 write 셋에 소켓이 셋 되있었습니다
(connect 호출후 read,write,except 에 소켓을 셋해두었습니다)

상황이 접속 실패다 보니 except 에 소켓이 셋 되길 바라고 있었지만
황당하게도 write 셋 말고도 read 셋까지 소켓이 셋되있어서 머리에서
쥐가날 지경입니다. 혹시 이런 현상을 겪으신분 계시는지 궁금합니다.

man 페이지를 살펴보면

------------------
The socket is non-blocking and the connection cannot be completed
immediately. It is possible to select(2)
or poll(2) for completion by selecting the socket for
writing. After select indicates writability, use get?
sockopt(2) to read the SO_ERROR option at level
SOL_SOCKET to determine whether connect completed success?
fully (SO_ERROR is zero) or unsuccessfully (SO_ERROR is
one of the usual error codes listed here, explaining
the reason for the failure).

-----------------------

위와같이 소켓이 write able( 셀렉트 호출이후 write set 에 셋되었을경우 )
되었을경우
getsockopt 로 옵션을 SO_ERROR 로 줘서 결과를 얻어 접속성공이냐 실패냐를
알아보라고 했는데 성공해야 할경우나 실패햐애 할 경우에나 항상 결과는
성공으로 나옵니다.

코드는
int ret = getsockopt( sock, SOL_SOCKET, SO_ERROR, &error, &error_size );
이며 error 값과 ret 값이 0 이 아닐경우를 접속실패로 간주했습니다.

사실상 getsockopt 를 이용한 비동기 커넥션 성공여부 판단은 포기하고 있는
실정이지만 혹시 이에대해 좀 아시는분 참고말씀 부탁 드립니다.

아래글에도 이와비슷한 글을 올렸는데 어느분께서
접속성공 실패 여부를 떠나서 일단 무조건 select 이후 write 셋에는
소켓이 셋되있다고 하시더군요(UNP 에 나온다고말씀)
만약 접속이 실패든 성공이든 무조건 write 셋에 걸린다면 select 이후
이벤트체크에서 걸린 소켓(즉 write 셋에 걸려버린소켓)은 어떻게
접속이 성공했는지 실패했는지 알수가 있는지 궁금합니다.

말이 길었는데 요약하자면
보통 select 를 이용한 비동기 소켓 커넥션은 어떤식으로 이루어지는지와
최초의 select 호출이후 아무런 이벤트를 감지하지 못햇을경우
다시 read,write,except 셋에 접속성공 여부를 감지할 소켓을 셋하고
감지할때까지 이와같은일을 반복하는지..

또 저같은경우 접속실패 상황에서 왜 read 와 write 셋이 셋되서
나오는지...

조그만 참고말씀이라도 귀담아 듣겠습니다.
감기조심하시고 읽어주셔서 감사합니다.


--------------------------------------------------------------------------------------------------------

UNIX에서는 Winsock과 다르다고 하네요
UNIX에서는 연결이 성공했는지의 여부를 알리기 위해서
읽기와 쓰기가 모두 가능하게 만듭니다.
커널 구현이 그런걸 ... 왜 그렇냐고 따지기 보다 ㅎㅎ 그냥 쓰셔야 겠죠

일단 select() 리턴후

readable도 아니고 writable도 아니라면
소켓은 연결이 되어 있는 상태로 판단가능하구요

그다음엔
getpeername()을 호출해서 정상리턴이면 연결된 상태입니다.

만약 getpeername()이 ENOTCONN을 리턴하면 연결안된상태이구요.
getsockopt() 호출해줘서 아직 가지고 오지 않은 에러코드를 가지고 오신후에 error코드를 살펴보시면 됩니다.
만약 getsockopt() 자체가 실패하면
errno가 getsockopt()호출에 대한 실패를 저장하겠죠?(맞나 ㅡ.ㅡ, 불확실..)

제가 말씀 드린 부분을 isconnected() 함수로 만들어서

사용하시면 될것 같네요


--------------------------------------------------------------------------------------------------------

일단 저의 상황에 대해 말씀드리자면

전 커넥트 타임 아웃 함수를 만드는것이 아닙니다.

UNP 책이 없어 게시판뒤져서 다른분들이 예제올리신거 봤는데

비동기 커넥트 타임아웃 함수더군요.

저는 반응시간에 민감한 (중간에 거의 멈춤없이 계속돌아가는)

서버프로그램을 짜고있고 다른 서버와의 연동을 위해 타 서버로

접속하는 클라이언트 모듈을 짜고 있습니다. 여기서 타 서버로 접속시

블럭 또는 시간지연이 안되게 하기 위해 비동기 connect 를 이용하며

이 클라이언트모듈을 모든 서버 소켓및 차일드소켓과 같이 관리 합니다.

일단 비동기 connect 에 대한 방법은 많이 파악하고 있었는데 계속 이상한

오동작을 해 많이 혼란스러웠던것 같습니다. 지금 오동작을 하는 상황을 판단

했는데 다음과 같습니다.

접속하는 서버가 꺼져있는상황에서.
(이 서버프로그램은 타서버로 접속하는 클라이언트 소켓만 관리 하는
것이 아니라 차일드 소켓들도 관리합니다. 그래서 따로 접속용 select 를
만들어서 돌리지 않으며 테스트 할때는 클라이언트 소켓만 관리해서
테스트 했습니다)

1. 목적 서버가 로칼호스트일경우(즉 같은 컴퓨터) ex)127.0.0.1

select 를 통과하면 read 셋과 write 셋이 동시에 셋 됩니다.(select 리턴값 2 )

2. 목적 서버가 타 호스트일경우(remote) ex)192.168.1.1

select 를 통과하면 아무 fd_set 도 셋 되지 않습니다.

그래서 접속실패 경우 어떤 상황이 정상인것으로 판단 해야 하는것인가
에 대한 고민을 많이 하다 위 1번상황(목적 서버 꺼짐, 목적서버 로칼호스트)
을 고려하여 다음과 같이 했습니다

일단 read set 체크 루틴은 데이타가 오거나 접속이 끊긴것을 처리해야
하는 루틴이므로 비동기 접속에 대한 실패여부를 판단하는 루틴을
넣는것은 안좋은것 같아 어차피 접속이 안됬으면 recv 에서 접속 끊긴것
으로 판단하여 소켓을 닫는 루틴이 호출될것이므로 그냥 둠.

read set 이 셋됨과 동시에 write set 도 셋 되므로 별도의 처리가 필요하
다고 판단.

write set 이벤트 발생처리 루틴은
write set 될때 접속요청중인 소켓이였으면(따로 소켓클래스에 셋팅해둠)
접속성공처리를 하는 루틴임.
하지만 접속 실패의 경우도 write set 이 발생하므로 어떻게 할까 고민하다
위 write set 이벤트를 처리하기전 read set 에서 소켓을 닫으므로
소켓이 닫혀 있으면 continue 처리.

이렇게 하였습니다. 일단 이렇게 하니 문제없이 잘 동작은 하는데

마지마으로 궁금한점!

왜 접속 실패하엿을경우 exception set 은 발생하지 않을까??

exception set 은 실제로 거의 쓰이지 않는 set 일까 하는점입니다..

긴글 읽어주셔서 감사합니다. ^^ 참고 말씀 기달리겠습니다.

--------------------------------------------------------------------------------------------------------


UNP 에서는 넌블러킹 소켓으로 connect 를 했을 때 생기는 문제점과 connect 후에 처리해야 할 일 등을 기술하고 있습니다. 그 예제 코드만을 가지고서 말씀하시지 마시고, 먼저 읽어보시는 것이 도움이 됩니다.

UNP 구현과 달리 select 를 중앙집중식으로 하시는가 봅니다. UNP 구현의 select 부터를 작성하시는 코드에 적절히 배치하면 될 것이구요.

그리고, 1. 번의 경우에 어떻게 처리해야 하는지 UNP에 얘기되어 있고, 2. 번의 경우 select 에 시간을 주어서 대기했다면 어떻게든 (접속되든 안되든) 해당 fd_set 을 (최소한 writable) 세팅해줍니다. 만약 기다리지 않게 하고 조사하거나 시간이 만료되었다면, 접속성공/실패 여부를 모르고 시간만료에 해당하는 0을 리턴하고 아무런 fd_set 을 설정하지않고 리턴될 것입니다. 중앙에서 select 한다면 판단이 애매해질 수 있겠습니다. 다른 fd와 섞여서 select의 리턴값만으로는 넌블러팅 connect의 시간만료인지를 알 수 없을테니, 해당 fd 의 타임아웃 시간인지를 판단하는 코드가 추가되고 시간만료이면 fd_set 세팅이 없을 때라는 조건이 있어야 실패로 확인되겠습니다.

정 책이 없다면... 서점에서라도 보실 수 있지 않습니까. 해당하는 페이지가 몇페이지 안되니까요. 보기만해도 좋은 결론에 도달할 수 있다고 생각합니다.

UNP 를 먼저 꼭 구해서 읽어보시면 좋겠습니다. 먼저 경험한 사람의 충실한 경험담이 있습니다. 혼자서 고생하시지 마시고, 쉽게 정보를 구할 수 있다면 이용하는게 맞다고 봅니다.

UNP 는 내용을 본다면 책값이 너무 싼 편에 속한다고 생각합니다.


--------------------------------------------------------------------------------------------------------

예 그렇습니다. 저같은경우 timeval 안에 값을 0,0 으로 셋팅합니다.
그래서 read 나 write 이 셋 되지 않고 셀렉트가 0을 리턴하는군요.
궁금한점은 select 가 0을 리턴했어도 다음번,또 다음번 select 호출시에는
read 나 write 가 왜 셋되지 않을까 입니다. timeval 값을 0,0 으로 하면
select 가 read 나 write 을 셋하기에는 너무 짧은 시간인지요?

UNP 번역판을 전 회사에서 본적이 있는데 번역기를 돌린건지
번역이 이상하더군요.. 살려고 했는데 번역본에 대한 실망때문에
망설이고 있었습니다. 이참에 맘먹고 장만해야겠네요..^^


--------------------------------------------------------------------------------------------------------

마지막 글에 질문이 있었군요... 처제네 집들이를 하는 와중에 쓴글이라 경황이... -_-;

넌블러킹 connect 를 호출 후에 성공이냐 실패냐를 정확하게 판단할 수 있는 시점은 select 에서 넘어오는 순간입니다. 만약 select 를 대기없이 조사만하도록 했다면 (만료시간 0) 이것은 connect 시도의 상태만을 알아보는 것이므로 성공/실패 여부를 알아보기에는 조금 힘든 방법 같습니다.

일반적으로 상대편 서버가 다운되어 있을 때, connect 를 시도하면 보통 60초정도 (환경마다 다르겠죠) 있다가 성공/실패 여부를 돌려준다고 합니다. 여러번 select 를 호출했다고 해도 이 시간이 되기전에는 어떻게 된 것인지 알 수가 없는 것이고, rasungboy 님처럼 만료시간 0으로 한다면 그 시간이 될 때까지는 계속 0만을 리턴하는 상황이겠습니다. (같은 호스트라면 즉시 성공/실패를 판단)

결국 넌블러킹 connect 후에 select 로 일정 시간을 대기해야 한다는 결론입니다. 하지만...

지금 하시고자 하는 것이 여러 다른 일반 fd 들과 함께 connect 시도도 병행하고자 하는 것으로 보입니다. 이럴 경우 문제가 있는데, select 에 주어지는 시간 만료는 여러 fd 들에게 독립적인 것이 아니라 select 자체에 대한 시간 만료이기 때문에 시간에 대한 처리를 따로 하셔야 한다는 것입니다.

각 fd 별로 타임 아웃 시간을 각각 적용하기 위해서는 각 fd 별 타임 아웃을 우선 순위 큐 등을 이용해서 가장 짧은 시간을 select 의 시간 만료값으로 주고 처리해야 합니다. 이것을 제대로 구현하기가 좀 까다롭고 테스트하기도 귀찮고... 좀 그렇습니다...

select 로 분기하는 구조로 간다면 이래저래 처리해야 할 것도 많아지고 상태 머신이 아주 복잡해지는 경향이 있습니다. (간단한 처리 서버라면 상관없겠지만요...) 그래서 대부분 그냥 일반 쓰레드를 사용하거나 특수하게 서버용 사용자-공간 쓰레드를 사용하기도 합니다.

쓰레드라는 것이 막 쓰기에는 그렇지만 좀 신경을 쓴다면 그리 효율을 떨어뜨리지 않고서도 로직의 흐름대로 프로그래밍할 수 있어서 좋다고 생각합니다. (물론 남용은 금물...)


반응형

+ Recent posts