Linux Programmer

TCP/IP 소켓 프로그래밍 주의할 점 본문

컴퓨터 관련/프로그래밍 일반

TCP/IP 소켓 프로그래밍 주의할 점

sunyzero 2012. 10. 16. 22:48

TCP/IP 소켓 프로그래밍을 하다보면 초보자는 물론이고 중급이상의 숙련된 프로그래머도 실수하는 경우가 많다. 초급자는 경험이 없어서 실수하고, 중급자는 까먹어서 실수한다.


최근에는 미들웨어나 각종 통신 관련 라이브러리가 많아서 직접 TCP/IP 소켓 프로그래밍을 하는 경우는 줄어들고 있지만, 그렇다고 아예 없어지는 것은 아니니 주의할 점을 한 번 정도는 읽어두면 좋다.


* 목차

1. TCP 소켓의 연결 종료가 감지되면 꼭 close를 해야만 한다.

2. SIGPIPE 시그널 처리

3. 재전송



1. TCP 소켓의 연결 종료가 감지되면 꼭 close를 해야만 한다.

TCP 소켓은 1:1 스트림 연결이기 때문에 상대편이 소켓 연결을 종료하면 이쪽도 같이 종료해야 한다. 자동으로 닫히는 것이 아니므로 꼭 닫아줘야만 한다.


연결 종료를 감지하는 방법으로는 2가지가 있다. recv()를 호출했을 때 0이 리턴되거나, send를 호출했을 때 -1로 에러 리턴하면서 errno가 EPIPE이면 상대편(peer) 연결이 종료된 것이다.


이처럼 recv()나 send()를 할 때 상대편의 연결 종료가 감지되면 close()를 호출하여 꼭 연결을 닫아줘야만 한다. 닫지 않은 소켓은 계속해서 프로세스 자원에 남기 때문에 소켓 최대 개수에 도달하면 서버측 소켓 생성이 실패 할 수 있다. 만일 소켓을 이용한 서버 프로세스에서 "더 이상 파일을 열 수 없습니다(EMFILE : Too many open files)" 에러가 발생한다면 제 때 close()를 해주지 않은 경우다. 이런 상황은 에러 메시지를 출력하지 않도록 하더라도 네트워크 상태나 로그 메시지로 알아낼 수 있다.





위 그림은 client측에서 먼저 close()를 호출한 것인데, server측에서 먼저 연결을 종료한다면 중간의 close() 이하의 그림은 반대가 된다. 연결 종료는 주로 recv() 함수에서 감지하는데, 상대편이 close()를 호출하여 FIN 세그먼트를 보내면 이쪽의 recv() = 0 리턴이 된다. 즉 recv가 0을 리턴하면 상대편의 연결 종료가 요청되었다고 보면 된다. 


만일 연결 종료가 감지하고도 close()를 호출하지 않는다면 CLOSE_WAIT 상태로 소켓 상태가 표현된다. 아래 그림은 netstat 실행 화면인데 CLOSE_WAIT 상태가 close()를 호출하지 않아서 발생하는 상태를 보여준 것이다.



netstat CLOSE_WAITnetstat CLOSE_WAIT


만일 네트워크 프로그램에서 CLOSE_WAIT 상황이 보인다면 필시 연결 종료 부분에 문제가 있는 것이니 코드를 검토해봐야 한다.



2. SIGPIPE 시그널 처리

앞서 언급했던 것처럼 소켓 연결이 종료되었는데 send()를 하면 -1을 리턴하며 errno에는 EPIPE로 설정된다. 하지만 여기에 더해 SIGPIPE 시그널이 발생한다. SIGPIPE는 기본 동작이 프로세스의 종료이므로 해당 시그널을 블록하지 않으면 프로세스는 즉시 종료된다. 


따라서 SIGPIPE 시그널 핸들러를 설치하거나 무시해야 한다. 일반적으로는 무시하는 형식으로 코드를 작성한다. 하지만  몇몇 책이나 블로그에 있는 예제에서는 SIGPIPE 시그널 핸들러에서 close를 호출하도록 코딩하는 경우도 있으나 실무에서는 사용되지 않는 기법이다. 왜냐하면 시그널이 중첩되는 경우도 있을 수 있고, 세션을 관리하는 복잡한 작업의 경우에는 thread safety를 만족해야 하는 경우도 있을 수 있어서 SIGPIPE 시그널 핸들러로 작동시키면 심각한 오류를 발생시킬 수 있다.


따라서 SIGPIPE 시그널은 무시(sigaction에서 SIG_IGN 설정)하도록 하거나 send를 호출할 때 MSG_NOSIGNAL 플래그를 사용하는 경우가 많다. 이렇게 하면 SIGPIPE 시그널에 의해 문제가 생기지 않는다. 참고로 MSG_NOSIGNAL은 SUSv4 2006부터 표준에 도입되었으므로 오래된 시스템에는 존재하지 않을 수 있다.

ret = send(fd, buf, len_buf, MSG_NOSIGNAL);
if (ret == -1) {
    /* error */
}



3. 재전송

TCP를 넌블록킹(nonblocking) 모드로 사용하면 send() 호출시 일부만 전송되는 경우가 있다. 예를 들어 send(fd, buf, 50000, 0); 을 호출했다고 하자. 


블록킹 모드라면 50000바이트 모두 전송될 때까지 블록되겠지만 넌블록킹 모드라면 일부만 전송될 수 있다. 따라서 send()의 리턴값을 검사하여 전송 요청한 50000바이트보다 작다면 나머지 부분 계산 후 재전송하도록 해야 한다.


이 때 즉각 나머지 부분을 재전송을 하는 것은 좋지 못한 방법이다. 왜냐하면 일부분만 전송된 경우는 상대편(peer)의 라인 속도가 느린 경우이므로 즉각 재전송을 해봐야 어차피 실패할 확률이 높다. 따라서 타이머를 이용해서 조금 뒤에 전송하거나, 혹은 poll(), epoll과 같은 멀티플렉서로 재전송이 가능한지 감지한 뒤에 전송하는 기법을 주로 사용한다.



이외에도 TCP/IP 네트워크 프로그래밍을 하다보면 여러 문제에 봉착할 수 있지만 위의 3가지만 주의해도 큰 난관은 피해갈 수 있을 것이다.

반응형
Comments