관리 메뉴

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가지만 주의해도 큰 난관은 피해갈 수 있을 것이다.

5 Comments
  • 프로필사진 v명월v 2013.03.13 22:50 신고 안녕하세요.. 좋은 글 읽고 갑니다..
    궁금한 점이 있는데...
    저같은 경우는 닷넷에서 상대방이 커넥션을 종료 시키면 close를 안시키고 바로 accept 대기 모드로 전환시켜버립니다
    이럴 경우에도 커넥은 끊기겠지만..
    메모리 할당이 남아 있을까요??

    또 궁금한게 있는데 accept 대기 상태에 빠지면 커넥이 일어나기 전엔 종료가 되지 않더라구요 c++ 도 그렇고 닷넷도 그렇고.. 저는 자신이 커넥을 날려서 끊어 버립니다.. 혹시 그것 말고 더 좋은 해법이 있는지
    많은 가르침 부탁드려요^^
  • 프로필사진 sunyzero 2013.03.13 23:43 신고 accept뿐 아니라 blocking mode로 작동하는 모든 IO함수는 이벤트를 기다리며 무한 대기를 하죠. 이를 방지하기 위해서 I/O multiplexing 기법을 사용하게 됩니다.

    닷넷에서는 I/O 멀티플렉싱을 구현하기 위해 비동기(asynchronous socket)을 사용합니다.
    http://msdn.microsoft.com/en-us/library/5w7b7x5f.aspx

    참고로 유닉스(or 리눅스) 계열에서는 poll, select 함수를 사용합니다.

    그리고 명시적으로 close를 해주지 않으면 자원 누수가 발생합니다.
  • 프로필사진 v명월v 2013.03.14 00:04 신고 좋은 정보 감사합니다..
    한번 구현해봐야겠네요... ^^
  • 프로필사진 축구사랑 2018.08.11 10:23 좋은글 잘 읽었습니다.
    덕분에 큰 오류를 피할수 있을듯합니다.
    윈속에서 소켓 close를 안해주어서 고생한기억이 너네요. 잘돌던게 한 이틀만 지나면 죽어서 확인해보니 소켓핸들 개수가 계속 늘어나더군요. 클로즈의 중요성을 새삼 느끼고 갑니다.
    감사합니다.
  • 프로필사진 sunyzero 2018.08.12 18:22 신고 맞습니다. 소켓 프로그래밍은 의외로 함정이 많죠. 컨설팅이나 교육을 다녀보면 현업에서 실무 하는 분들도 TCP쪽에서 실수하는 분들이 꽤 많습니다. ^^
댓글쓰기 폼
Prev 1 2 3 4 5 6 7 8 Next