select() > 0 인데, recv() < 0 - 어떤 경우에?
일반적인 TCP 소켓 프로그램에서 제목처럼
select() > 0 인데, recv() < 0 인 경우가 어떤 때 나오는지 궁금해서 질문드립니다.
RTSP 클라이언트 프로그램을 작성중인데요,
소켓에서 한 바이트씩 읽어서 /r/n/r/n이 나올 때까지 읽는 프로그램입니다.
소켓 옵션은 특별히 주지 않았구요. 그러니까 blocked socket 이네요.
뭐 이건 non-block 소켓으로도 바꿔 보고 이것저것 해 봤습니다.
아래 소스에서 block_until_readable()의 결과 값이 > 0 (그러니까... select() > 0) 인데,
recv() < 0 이어서
perror("Fail to recv :");
라인에 걸리는 경우가 있습니다.
errno = 104 (ECONNRESET - Connection reset by peer)
라고 나오는군요.
원래대로라면
RTSP/1.0 200 OK
CSeq: 4
Session: 135514808124572
Range: npt=now-
이 받아져야 하는데, 첫 번째 줄의
RTSP/1.0 200 OK (\r이나 \n 없이)
까지만 오고 나서 위와 같은 에러가 뜹니다.
항상 같은 결과...
(혹시 RTSP를 아시는 분을 위해 좀더 부언하면, OPTIONS ~ SETUP 까지는 주고받고 잘 하는데,
위의 응답은 PLAY에 대한 겁니다. 뭐 이건 질문의 요지와는 크게 관계는 없습니다만...)
그런데 이 경우 select()에서 한참을 머물다가 return 되어 돌아옵니다.
보통은 select()에서 바로바로 return되어 돌아오는데,
이 에러가 나기 직전에는 timeout 값보다 조금 안 되는 시간 만큼을 select()에서 머물렀다가
돌아오는 것 같습니다. 그리고서는 select()는 1을 리턴합니다.
직후에 recv()를 하면 -1이 리턴되면서 위와 같은 에러가 나는 것이구요.
불행히도 서버 쪽 소스는 저희가 볼 수 없네요.
다른 RTSP 클라이언트 (Live555의 openRTSP 같은 ...)
를 사용하면 정상적으로 주고받고 하는 것으로 보아 서버쪽이 문제가 없는 것 같기도 하고...
참고로 사용한 소스를 필요한 부분들을 추려 아래에 적습니다.
/* timeout을 두어 소켓의 상태가 읽기 가능한지 검사합니다. */ int block_until_readable(int s, struct timeval *p_timeout) { int result = -1; fd_set rd_set; do { FD_ZERO(&rd_set); FD_SET(s, &rd_set); result = select(s+1, &rd_set, NULL, NULL, p_timeout); if (result == 0) { printf("Select timeout\n"); break; } else if (result < 0) { if (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) { continue; } else { perror("select() error: "); break; } } else { if (!FD_ISSET(s, &rd_set)) { printf("select() error - !FD_ISSET)\n"); return -1; } else { /* Socket is readable. Normal case */ ; } } } while (0); return result; } /* sock에서 \r\n\r\n이 나올때까지 읽어 buf에 저장합니다. */ int read_socket(int sock, char buf[], int buf_len) { int read_bytes, result, recv_len; char *buf_ptr; struct timeval timeout; buf_ptr = buf; recv_len = 0; memset(buf, 0, buf_len); while (1) { /* Set timeout : 5 seconds */ timeout.tv_sec = 5; timeout.tv_usec = 0; result = block_until_readable(sock, &timeout); if (result == 0) // timeout { return -1; } else if (result < 0) // select error { printf("Select error in read_socket()\n"); return -1; } /* Socket is readable. Normal case. Read 1 byte */ read_bytes = recv( sock, buf_ptr, 1, MSG_NOSIGNAL ); if( read_bytes == 0 ) { printf("0 returned in recv()\n"); return -1; } else if( read_bytes < 0 ) { perror("Fail to recv :"); // <== 여기서 걸림 return -1; } /* Read success */ recv_len += read_bytes; buf_ptr += read_bytes; /* String ends with "\r\n\r\n" - Find it! */ if ( recv_len >= 4) { if (*(buf_ptr-4) == '\r' && *(buf_ptr-3) == '\n' && *(buf_ptr-2) == '\r' && *(buf_ptr-1) == '\n') { *buf_ptr = '\0'; // I've got complete string. Terminate it. break; } } else { ; // I have to read more. } if (recv_len >= buf_len) { printf("Too long(%d)\n", recv_len); *buf_ptr = '\0'; break; } } return recv_len; }
질문이 길어졌는데...
요약하자면,
어떤 경우에 select() > 0 이면서 recv() < 0 이 나올까 하는 겁니다.
- 답을 안 주셔도 읽어 주신 모든 분께 감사드립니다.
음 ..
select() 가 return 하면, timeout 값이 남은 시간으로 변경됩니다.
그냥 쓰다보면 언젠가는 0 이 될테고, 그러면 호출되는 즉시 리턴되겠죠.
select() 에 들어가는 timeout 값은 매번 초기화 해줘야 합니다.
되면 한다! / feel no sorrow, feel no pain, feel no hurt, there's nothing gained.. only love will then remain.. 『 Mizz 』
select timeout 수정
음... 감사합니다.
그런 것도 있었군요. 그건 수정하겠습니다.
하지만 문제는 그게 아닌 것 같습니다.
perror ("Fail to recv:")
에 걸릴 때 select에 한참 머물다 왔습니다.
그리고 타임아웃 걸린 것도 아니고, select()는 1이 리턴되었습니다.
궁금한 건, select()가 1인데, recv()는 -1인 경우가 어떤 경우인지 ...
음 ..
recv() 는 socket 이 shutdown 이나 reset 일 때, 0 또는 -1 을 리턴합니다.
데이터를 읽어들이는 도중이나 다 읽었더라도, 서버에서 연결을 종료해 버리면..
read_socket() 에서 0 또는 -1 을 리턴할 가능성이 있을 것 같습니다.
그리고 recv() 의 두번째 인자로, buf 를 넘기시는데, 얘는 buf_ptr 로 바꾸는게 맞을 것 같네요.
전체 버퍼의 첫번째 바이트에만 데이터를 덮어 쓰고 있습니다.
되면 한다! / feel no sorrow, feel no pain, feel no hurt, there's nothing gained.. only love will then remain.. 『 Mizz 』
그 실수도 있었군요
제가 지금 작업하고 있는 소스를 그대로 올린 게 아니라,
질문을 위해 불필요한 부분을 걷어내고, 컴파일을 해보지 않은 상태로 올린 거라
그런 실수도 있었군요. 감사합니다.^^
recv()에서 -1이 리턴되고, ECONNRESET이 뜬 걸로 보아,
TCP 레이어에서 RST가 온 건 맞는 것 같습니다.
서버가 close() 외의 다른 방법으로 종료한 것 같습니다.
문제는 왜 그랬는가 하는 건데, 서버쪽 소스를 저희가 구할 수 없어 그 이유는 알 수가 없구요.
알려주신 댓글에 의하면,
select()가 읽기 버퍼를 보니 읽을 게 있고, 그래서 1이 리턴되었고...
상대편 소켓의 상태나 connection 상태는 모르고, 관심도 없고, select()의 임무도 아니고...
그런데, 읽는 도중 recv()가 상대편 소켓으로부터 RST를 받아서 -1이 리턴되었다...
그런 말씀이신가요?
항상
RTSP/1.0 200 OK
까지만 읽히는데, 항상 서버가 정확히 거기까지만 send()하고
close()가 아닌 다른 어떤 식으로 소켓을 끊어 버렸다는 그런
얘기가 되나요?
갑자기 궁금해 지는 게 있네요.
select()는 정말 connection의 상태는 관심없고, 오로지 읽기(또는 쓰기) 버퍼만 쳐다보는 건가요?
이런 경우처럼 상대편이 10바이트 정도를 보내고 (Peer 프로그램 crash 등으로 인해) 갑자기 연결이 끊어지면
select() 리턴값이 -1이 되어 주면 좋겠는데
그렇지 않은 것 같군요.
제가 이해하고 있는 게 맞는지 모르겠는데,
그 외에 select() 의 리턴값이 1이 되고, recv()의 리턴값은 -1이 되는 또 다른 어떤 시나리오가 있을까요?
하여간 ymir님 계속 관심 가져 주셔서 고맙구요.
혹시 생각나시는 거 있으면 한 번 더 댓글 주시면 감사하겠습니다.
음 ..
select 는 fdset 의 상태 변화를 모니터링 합니다. socket close 시에도 이벤트가 발생한 fd 수를 리턴하는 걸로 알고 있습니다.
그래서 이벤트가 발생한 fd 에 read/write 등의 리턴값을 통해 close 여부를 확인할 수 있구요.
select 가 -1 을 리턴하는 경우는 man page 보시면 아시겠지만.. 몇 개 없습니다.
socket close 역시 socket 의 상태가 바뀐것이지, 에러가 아니니 -1 을 리턴할 이유가 없는거죠.
buf 의 첫번째 바이트에만 계속 덮어쓰다보니, 프로토콜 상의 terminate 조건을 만나지 못하고 계속해서 누적됩니다.
데이터 전송 후 상대가 socket 을 종료할 텐데, graceful shutdown 이라면 recv 가 0 을 리턴할 것이고...
그 이외의 상황이라면 -1 을 리턴하겠죠.. 어쨌든 둘 다 socket close 라고 보시면 됩니다.
읽은 데이터가 항상 "RTSP/1.0 200 OK" 라고 하셨는데...
이전 버퍼를 클리어 하지 않아서 우연히 남은 값이거나 하지 않을까 추정됩니다.
strace 로 시스템 콜을 떠 보시면 recv 가 어디까지 읽었는지 확인할 수 있을텐데..
적어도 다른 클라이언트로 테스트 했을때 서버에서 아무런 문제를 발견하지 못했다면...
위의 코드 역시 데이터를 읽기는 다 읽어들일겁니다. 단지 읽어들인 값을 정확하게 리턴하지 못했을 뿐..
되면 한다! / feel no sorrow, feel no pain, feel no hurt, there's nothing gained.. only love will then remain.. 『 Mizz 』
음냐;
헛소리 써놓아서 죄송합니다. -_-;
몇시간 지나고 보니 헛소리 써놓았더군요.
감사합니다.
select()를 다시 뒤져 읽어 보았습니다.
"A socket is ready for reading"의 의미 중에 "socket error is pending"
도 있군요.
select()는 recv() 호출을 해도 block되지 않을 것이다. 라는 것만 알려 주는 것 같습니다.
소켓에 문제가 생긴 걸 감지하는 게 아니라...
(그나저나 서버 쪽에서는 왜 소켓을 reset 시켜 버렸을까... 쩝...)
그리고 한 가지 재미있는 것도 알게 됐는데,
select() 호출시 마지막 파라미터인 timeout 값이 리눅스에서는 남은 타임아웃 값으로 업데이트 되는데, 유닉스에서는 그런 것 같지 않네요.
Stevens 의 Unix Network Program 2nd Ed.의 p.150에 나오는 함수 원형에는 마지막 파라미터 앞에 const가 있습니다.
Linux man 페이지에는 없구요.
하여간 ymir님 여러 모로 감사합니다.
* 지적해 주신 버그들은 수정하여 본문의 코드를 다시 올렸습니다. (자꾸 마음에 걸려 하시는 것 같아서... ^^; 누가 다시 볼 일은 별로 없겠지만요...)