Linux Programmer

TCP의 TIME_WAIT를 없애는 법 본문

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

TCP의 TIME_WAIT를 없애는 법

sunyzero 2013. 4. 15. 16:38

* TCP의 TIME_WAIT는 없애는 방법은 무엇인가?

TCP 소켓 네트워크 프로그래밍을 하다 보면 TIME_WAIT 상황에 대한 고민을 하는 시점이 오게 된다. 학부 시절 네트워크 프로그래밍 수업을 듣고 실습실에서 열심히 프로그래밍 해봤다면 학부 때 맞닥뜨리게 되는 경우도 있다. 만일 학생 때 고민하지 않고 넘어갔다면 회사에서 주먹구구식으로 혼동을 일으키는 내용이기도 하다. 그래서 이에 대해 좀 정확한 정보를 전달하고자 이 글을 쓴다.

 

아래 글은 각종 책과 표준안의 레퍼런스와 실제 코딩으로도 검증했지만, 그래도 혹시 틀린 점이 있다면 개의치 말고 지적해주면 감사하겠다. ^^

  1. TIME_WAIT란 무엇인가?
  2. 이 글을 쓰게 된 계기
  3. TIME_WAIT는 당신을 해치지 않는다. 그래도 없애고 싶다면?
  4. SO_REUSEADDR의 사용
  5. 결론

 


 

1. TIME_WAIT란 무엇인가?

TCP의 TIME_WAIT는 TCP 연결을 종료 할 때 신뢰성을 높이기 위해 존재하는 것으로 자연스럽게 발생하는 상태다. TCP/IP 네트워크 교과서인 TCP/IP Illustrated volume1에 보면 TCP 상태 전이도(TCP state transition diagram)에 표시되어있다.[각주:1] TCP 상태 전이도는 복잡할 수도 있으니 본인의 저서에 좀 쉽게 풀어서 그린 그림을 편집해서 붙여보겠다.[각주:2]

 

TCP state transition
TCP state transition (출처 : 내 책)

 

 

위 그림을 보면 클라이언트측의 마지막 상태에 TIME_WAIT가 발생하는데, 이는 클라이언트측에서 active close를 했다고 가정했기 때문이다. 만일 서버측에서 먼저 active close를 했다면 그림의 아래 부분의 좌우는 바뀌게 되어 , 서버측에 TIME_WAIT가 발생하게 된다. 아주 드물게 simultaneous close 경우에는 서버, 클라이언트 양측에 TIME_WAIT가 발생한다. 

 

* Active close

먼저 접속 종료 행위를 한 측을 말한다.(안녕을 먼저 말한 측이라고 이해하면 쉽다) TCP 프로토콜에서 active close는 FIN 세그먼트를 먼저 전송하는 측을 의미한다. 소켓 프로그래밍에서는 close 혹은 shutdown 함수를 먼저 호출하는 측이 active close를 하는 측이다. 반대로 접속 종료를 수신하는 측을 passive close라고 부른다.

* FIN_WAIT1, FIN_WAIT2 단계

Active close를 한 측에 FIN_WAIT1, FIN_WAIT2의 2가지 단계가 존재하는 것은 TCP 규칙상 passive close를 하는 측은 잘 받았다는 표시인 ACK와 나도 접속을 종료하겠다는 FIN을 보내는 2가지 단계가 있기 때문이다. 동시에 ACK, FIN이 오는 경우도 있지만 대부분은 ACK가 먼저 오고, FIN이 그 다음에 온다. 결과적으로 위 그림을 잘 기억해두자. 의외로 실무에서도 FIN_WAIT2 단계의 오류가 자주 발생하는데, 이건 내 잘못이 아니라 상대의 잘못이므로 말을 잘해서 해결을 봐야한다. (내 잘못인 경우는 CLOSE_WAIT 상태이다. 이 상태가 발생하면 내가 짠 코드의 잘못일 가능성이 매우 높다. 아주 간혹 프레임워크의 잘못일 수도 있다.)

 

일반적으로 클라이언트가 active close 하도록 설계하므로 TIME_WAIT는 클라이언트측에 발생한다. 하지만 타임 아웃이나 비정상 종료를 처리하는 경우에는 서버측에서 active close하는 경우가 있다(서버에 접속하고 아무것도 하지 않으면 추방당하는 케이스를 생각해보자). 그렇지만 세상일은 항상 일반적인 케이스만 있는 것은 아니다. 간혹 비정상이 일반적인 케이스도 있는데, 웹서버가 그런 경우다. 웹서비스에서 사용하는 HTTP 프로토콜은 구현의 특성상 여러 클라이언트 커넥션을 빠르게 받기 위해 서버측에서 active close를 시도하는 상황이 일반적이다. 그래서 대부분의 TIME_WAIT 이슈는 웹서비스에서 주로 발생한다. 게임이나 증권 거래 시스템들은 time_wait보단 fin_wait2나 close_wait 상태가 문제가 되는 경우가 많다.(실제로 이런 문제를 해결하기 위해 컨설팅을 해준 경우가 많았다.)

참고로 TIME_WAIT 상태의 타임아웃은 시스템마다 다르지만 리눅스의 경우에는 60초로 고정되어있다. 커널 설정으로 바꿀 수 있다고 하는 경우도 있는데 사실과 다르다. 리눅스 커널 코드를 보면 60초로 고정이다. 다만 커널 설정으로 TIME_WAIT 상태를 재사용 하도록 설정할 수 있다.

위의 그림으로 제대로 이해가 안간다면 TCP/IP Illustrated Volume1을 보던가 아니면 설명이 잘 나와있는 다른 사람의 블로그를 소개하니 읽고 오자. : TIME_WAIT를 남기지 않는 세션종료 (Graceful Shutdown)

참고로 TIME_WAIT를 소개한 http://kuaaan.tistory.com/118 은 거의 다 맞는 내용이지만 약간 혼동을 줄 수 있는 내용도 있어 보완 설명을 하겠다. 문제가 되는 부분은 중간에 "2. linger 옵션에 대하여"의 윗 단락 부분이다. 아래 인용 부분을 보자.

 ________________________________________________________________________________

"이 TIME_WAIT라는 상태가 중요한 이유는, 만약 종료절차가 잘못 진행되어 서버쪽에 TIME_WAIT가 남게 되면 심각한 문제가 발생할 수도 있기 때문입니다. 일단 TIME_WAIT가 시작되면 2분여 이상 상태가 지속되게 되는데 모든 클라이언트들의 세션 종료시마다 서버 측에 TIME_WAIT가 발생한다면, 서버측에 부하가 될 뿐만 아니라 최악의 경우 서버에서는 더이상 새로운 연결을 받아들일 수 없는 상황이 발생할 수 있습니다. 말하자면.. 장애 상황이 발생하는 거죠. (실제로 실 운영서버에 이런 일이 발생하는 것을 직접 목격한 적이 있습니다. ) - http://kuaaan.tistory.com/118

___________________________________________________________


인용한 부분에서 서버 측이 TIME_WAIT로 인해 새로운 연결을 받아들이지 못한다는 했지만, 이는 웹서비스와 같은 특수한 케이스에서 주로 나타난다. 지속적인 연결을 사용하는 시스템에서는 발생하지 않을수도 있다. 만일 리눅스 시스템을 사용하고 있고 time wait 때문에 문제가 발생한다면 tcp_max_tw_buckets, tcp_tw_reuse, tcp_tw_recycle, ip_local_port_range를 튜닝하는 것도 방법일 수 있다. (보통 tcp_max_tw_buckets, tcp_tw_reuse 정도만 설정해둬도 문제가 해결된다)

 

그러나 이런 경우라고 할지라도 서버측의 가용 포트가 줄어드는 것은 아니다.(클라이언트측의 경우에는 가용 포트가 줄어들 수 있다.) 다른 블로그나 KLDP에서도 TIME_WAIT로 인해 가용 할 수 있는 port가 줄어들어 서버에 문제가 생긴다는 글이 꽤 많은데, 가용 port는 줄어드는 것이 없다. 왜냐하면 서버측은 listen port만 사용하기 때문이다. 소켓의 주소는 local과 foreign address가 페어(pair)로 되어있는데 서버측은 listen port로 고정되고 클라이언트 주소만 달라진다. 예를 들어 ssh 서버에 접속한 클라이언트가 3개가 있는 그림을 보면 쉽게 이해가 갈 것이다.

netstat 화면
netstat 화면

위 그림을 보면 서버 측의 local address는 모두 22번 포트를 사용하는 것을 볼 수 있다. 이와 반대로 클라이언트측 주소인 foreign address는 모두 포트 번호가 달라진다. 결국 서버 측은 1개의 포트만 사용하므로 TIME_WAIT로 가용 포트가 줄어든다는 것은 사실과 다르다. 

즉 가용 port 개수가 문제가 아니라 time_wait 버킷의 제한값에 도달하거나 오픈된 파일 개수 제한에 걸려서 문제가 발생하는 경우가 대다수이다.

 

 

* TIME_WAIT 상태를 해결하는 SO_LINGER, SO_REUSEADDR / 혹은 net.ipv4.tcp_tw_reuse

원래 TIME_WAIT가 문제되는 경우는 클라이언트측이 빠르게 접속, 종료를 반복할 때 클라이언트측의 가용 port가 소진되는 것을 의미한다. 주로 네트워크 서버의 스트레스 테스트 클라이언트에서도 이런 문제가 보고된다.(서버측이 active close를 빠르게 반복하는 경우에는 조금 다른 양상의 문제가 발생한다.) 본인도 회사에서 스트레스 테스트를 위한 더미 웹 브라우저 클라이언트를 개발할 때 이런 문제를 겪었었다. 물론 교과서에서 배운대로 SO_LINGER로 간단히 처리했다. (SO_LINGER가 싫다면 SO_REUSEADDR을 이용하여 TIME_WAIT 상태의 주소를 재사용하는 방법도 있다.)

또한 서버측 로그에서는 TIME_WAIT로 인해 가용할 수 있는 포트가 없어서 socket을 bind하는데 실패하면 EADDRNOTAVAIL 에러가 발생하므로 쉽게 인지할 수 있다.

TIME_WAIT로 인해 가용할 포트가 줄어서 EADDRNOTAVAIL이 발생한다면 해결 방법은 2가지가 있는데, active close하는 측의 소스 코드를 수정하거나 리눅스 시스템 설정을 바꾸거나 둘 중에 하나로 해결할 수 있다.(둘 다 해도 된다.) 첫째로 프로그램 코드를 수정할 수 있는 경우라면 바로 앞에서 설명한 SO_LINGERSO_REUSEADDR을 사용하는 방법을 쓰거나, 코드를 건드리지 못한다면 Linux의 경우 net.ipv4.tcp_tw_reuse의 값을 1로 변경하면 된다. root 권한으로 sysctl net.ipv4.tcp_tw_reuse=1을 실행하면 된다. 보통은 요새 tuned를 사용하므로 tuned profile을 만드는게 좋다.

참고로 별다른 조치를 취하지 않을 경우, 클라이언트측이 초당 몇 개의 커넥션을 열면 모든 포트를 TIME_WAIT로 소진하는지 계산한 문제가 있다. 바로 TCP/IP Illustrated 연습 문제로서 다음과 같이 적혀있다.

TCP/IP Illustrated Volume1. p262

Q) Exercises 18.14 With an MSL of 120 seconds, what is the maximum at which a system can initiate new connections and then do an active close?

A) The limit is about 268 connections per second: the maximum number of TCP port numbers (65536-1024 = 64512, ignoring the well-known ports) divided by the TIME_WAIT state of 2MSL.

 

 

2. 이 글을 쓰게 된 계기

 이 글을 포스팅하게 된 계기는 따로 있다. KLDP에서 오래전에 쉰 떡밥(2003년도 떡밥)인 TIME_WAIT 관련 질문, 답변에 새로운 답글이 10년만에 달렸기 때문이다. 도저히 지나칠 수 없는 강력한 떡밥이라서 몇 시간을 들여 관련 글을 포스팅을 하게되었다. 물론 KLDP에는 답글을 달지 않았다. 이유는 나보다 더 까칠하지만 간략하게 오류를 지적한 다른 분이 있어서...

 

* 떡밥 링크 : Linux에서는 TCP_NODELAY이 없나요? - http://kldp.org/node/165

 

그런데 답글을 읽다 보니 둘 중에 누가 맞는지 혼동을 줄 수 있는 내용들이 있어 혹시 검색하는 학생이나 신입 사원들에게 도움이 될까하여 자세히 포스팅 해두기로 했다. 사실 여기 있는 내용들은 TCP/IP Illustrated나 내 책에도 다 있는 내용이다. 책을 열심히 읽은 사람들은 대부분 알고 있는 내용일 것이다. 그러면 이제 논란이 된 부분을 정리하자.  

1. TCP_NODELAY는 TIME_WAIT와 상관이 없다. TCP_NODELAY는 소켓에 Nagle's algorithm을 on/off 시키는 기능이다.

2. 원 글을 쓴 사람은 웹 서버를 개발하는데 있어서 서버 측에서 active close하여 발생하는 TIME_WAIT를 문제 삼은 것이다. 그러나 앞서 이야기 한 것처럼 TIME_WAIT는 문제가 되지 않는다. Java 서버 개발자분이 TIME_WAIT 를 문제 삼은 것은 아마도 TCP/IP에 대해 잘 몰라서 그런 것이 아닐까 생각된다.

 

KLDP의 떡밥글에 10년 만에 새롭게 달린 굉장히 공격적인 답글이 보이는데, 정답이 아니라서 상당히 유감이다. 해당 답글에서는 서버측에서는 shutdown을 하고, 클라이언트 측에서는 SO_LINGER를 사용하면 TIME_WAIT이 발생되지 않는다는 말이 나오는데 절반 정도만 맞은 사실상 오답이다. 정답은 클라이언트든 서버든 무조건 SO_LINGER로만 TIME_WAIT을 없앨 수 있다. 

리눅스 서버, 즉 POSIX 시스템에서 shutdown과 close의 차이는 linked channel이 있는 경우 무시하고 close를 할 것인지 말 것 인지의 차이만 있을 뿐 기능은 같다. 즉 shutdown으로 소켓을 닫는다면 fork로 파생된 프로세스에서 공유하고 있던 파일서술자(file descriptor) 소켓까지 모조리 닫힌다. 고로 정답을 말한 답글은 다음과 같다.

 

KLDP 정답 답글
KLDP 정답 답글

참고로 글을 쓰기 전에 네이버나 구글로 TIME_WAIT를 검색해보니 다른 블로그들에도 비슷한 내용이 있지만 틀린 내용을 적은 블로그들이 절반이 넘었다. 그래서 교과서가 아닌 웹 검색으로만 공부하는 학생들은 틀린 내용을 알고 있는 경우도 많을 것 같다. 정확한 검증을 위해 웹 검색보다는 교과서나 표준안, 매뉴얼을 먼저 보도록 하자.

 

 

3. TIME_WAIT은 당신을 해치지 않는다. 그래도 없애고 싶다면?

TIME_WAIT는 정상적인 상태이며 당신과 당신의 시스템을 해치지 않지만, 그래도 꼭 없애고 싶다면 SO_LINGER를 사용하면 된다. 이것도 kuaaan님이 쓰신 블로그 글에 포스팅 되어있다. 인터넷엔 워낙 검증되지 않은 글들이 많으니 의심이 많은 사람은 이 글을 믿지 못할 수 있다. 

그래서 공신력 있는 스티븐스의 TCP/IP Illustrated의 설명을 인용해 보겠다.

 

W. Richard Stevens. TCP/IP Illustrated Volume 1, p247 Aborting a Connection

Aborting a connection provides two features to the application: 

(1) any queued data is thrown away and the reset is send immediately, and (2) the receiver of the RST can tell that the other end did an abort instead of a normal close. The API begin used by the application must provide a way to generate the abort instead of a normal close.

We can watch this abort sequence happen using our sock program. The sockets API provides this capability by using the "linger on close" socket option(SO_LINGER).

 

위 인용한 부분을 코드로 쓰면 다음과 같다. 아래 코드에서 cfd는 클라이언트와 연결된 소켓 파일 기술자다.

struct linger solinger = { 1, 0 }; 
if (setsockopt(cfd, SOL_SOCKET, SO_LINGER, &solinger, sizeof(struct linger)) == -1) {
    perror("setsockopt(SO_LINGER)"); 
}

위와 같이 SO_LINGER를 설정한 상태에서 active close를 하면 정말 스티븐스 아저씨 말대로 TIME_WAIT가 생기지 않는지 확인해봐야 한다. 확증을 위해 예제를 작성해서 살펴보면 된다. 

패킷은 wireshark로 캡쳐했다. 그림이 크니 꼭 클릭해서 보자. 예제 코드는 너무 간단해서 첨부하지 않았는데, 혹시 필요한 사람이 있다면 나중에라도 첨부해 놓겠다.

 

wireshark : TCP SO_LINGER option
wireshark : TCP SO_LINGER option 확인

 

그림의 클라이언트측(192.168.0.10)과 서버측(192.168.0.100)은 연결 후 몇 초 뒤 서버측에서 active close하도록 프로그래밍 한 상황이다. 서버측(192.168.0.100)에서는 SO_LINGER 옵션에서 타임아웃을 0초로 설정하였다. 그 결과 10번 프레임을 보면 서버측이 FIN을 먼저 보내는 것을 볼 수 있다.

그리고 11번 프레임에서 ACK를 수신 받고, 12번 프레임에서 서버측은 RST를 송신하여 연결을 취소하는 것을 볼 수 있다. 이 과정을 설명하면 다음과 같다.

앞서 서버측은 FIN을 보낸 다음에 기다리지 않고 즉시 소켓 연결을 파괴해버렸다. 그러나 클라이언트는 FIN을 수신받고 잘 받았음을 의미하는 ACK를 발송하게 된다. 하지만 서버측에선 이미 파괴된 연결에 ACK가 수신되었으므로 "이미 없는 연결에 왜 패킷을 보내니?" 하면서 클라이언트에게 RST를 보낸 것이다. 이에 클라이언트는 RST를 수신하고 취소 작업에 들어가게 된다. 

그 다음에 서버에 도착한 FIN(13번 프레임)은 클라이언트가 워낙 빠르게 반응해서, 첫 번째 RST가 수신되기 전에 close()를 호출한 결과이다. 따라서 두 번째 RST의 등장은 클라이언트의 반응 속도와 RTT에 따라 생길 수도 있고 아닐 수도 있다. 만일 위와 같이 로컬에서 테스트하면 거의 대부분 생긴다. 이에 서버는 두 번째 RST를 보내서 "뭔 소리야? 아까 내가 보낸 RST 몰라? 응? 응?" 하면서 다시 RST를 보내게 된 것이다. 

이렇게 linger가 설정되면 서버측은 active close를 하고도 TIME_WAIT가 발생되지 않는다.[각주:3]

 

(2024년에 새롭게 질문하신 분이 있어서 약간 첨언하자면 위 시스템은 리눅스 구형 TCP구현에서 재현이 가능하며, 90년대 말의 리눅스 커널 구현에서는 LINGER timeout시 FIN, RST순서로 보내도록 되어있다. 그러나 이후 2000년대 초에 RFC2525의 영향을 받아서 RST를 즉시 보내도록 변경되었다. 따라서 2024년 현재 사용되는 kernel 4.x 혹은 kernel 5.x, 6.x의 버전의 경우 FIN이 생략되고 RST를 보내도록 되어있다. 과거 FIN이 보내졌던 이유와 현재는 FIN이 생략되는 것은 POSIX close 시멘틱을 해석하는 부분에서 RFC문서와 충돌이 있었기 때문이다. 이에 대해서는 close가 fd를 닫는 시멘틱을 생각해보면 FIN이 보내져야 하는 이유에 대해 이해할 수 있을 것이다)

 

비교를 위해 SO_LINGER를 설정하지 않은 경우에는 어떻게 되는지도 살펴보자. 예상한 대로라면 분명 FIN을 서로 주고받고 끝날 것이다. 마찬가지로 wireshark로 캡쳐를 해봤다.

 

wireshark : TCP normal close
wireshark : TCP normal close

 

위 그림을 보면 앞서 SO_LINGER를 설정한 경우와는 다르게 RST을 주고 받는 부분이 보이지 않는다. 이것이 TIME_WAIT를 발생시키는 정상적인 close의 패킷 흐름이다. 두 그림을 비교하면서 살펴보면 그 차이를 쉽게 알 수 있을 것이다.

 

 

4. SO_REUSEADDR의 사용

앞서 SO_LINGER의 설정을 보았는데, 이번에는 SO_REUSEADDR에 대해서 살펴보자.

SO_REUSEADDR은 주로 서버측 listener socket (listen을 호출하는 대상이 되는 서버측 소켓)에 사용하는 것으로만 아는 분들이 많은데, 사실은 클라이언트에도 사용 가능하다. 클라이언트측에서 사용하는 경우에는 주로 TIME_WAIT 상태에 빠진 소켓을 재사용할 때 사용한다.

TCP 클라이언트측은 주로 socket, connect, send or recv, close 순서로 함수 호출이 이뤄지는데, connect 이전에 bind를 하여 socket, bind, connect, send or recv, close 순서가 되기도 한다. 이렇게 직접 binding을 하는 경우를 explicit binding이라고 한다. 이때 바인딩하는 소켓이 TIME_WAIT 상태의 주소라면 SO_REUSEADDR을 사용하여 재사용을 할 수 있다.

단 주의할 점이 있는데 SO_REUSEADDR로 TIME_WAIT에 빠진 소켓을 재사용하려면 RFC-1323 TCP TS(Timestamp)가 켜져 있어야만 한다는 점이다. 리눅스에서 TCP TS는 커널의 net.ipv4.tcp_timestamps이며 기본값으로 켜져있다. 간혹 이 설정이 꺼진 시스템이 있는 경우도 있는데 그때는 TIME_WAIT관련 이슈가 SO_REUSEADDR로 해결되지 않을 수 있으니 조심해야 한다.

혹시 코드로 검증하고자 하는 사람들을 위해 작성된 C코드를 첨부한다. 참고로 이 예제는 어디서 가져온게 아니라, 내가 직접 작성한 코드이다. 특히 이 예제는 국제 표준인 POSIX 1003.1의 2001년 이후 개정판의 제안에 맞춰 구닥다리 inet_addr이나 gethostbyname 같은 함수를 사용하지 않고 작성하였다. 그 대신에 새로 제안된 표준 함수인 getaddrinfo, getnameinfo 등을 사용하여 작성하였다. 혹시라도 과거의 소켓 프로그래밍을 배울 사용한 구식 함수인 gethostbyname, gethostbyaddr, inet_addr같은 함수를 사용하고 있다면 지양하고 아래처럼 작성하도록 하자.

 

#define _XOPEN_SOURCE 700#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netdb.h>
#include <errno.h>
#include <arpa/inet.h>

int main(int argc, char * argv[]) {
    int fd, rc_gai, flag_once = 0;
    struct addrinfo ai_dest, * ai_dest_ret;
    if (argc != 4) {
        printf("%s <hostname> <port> <0(SO_REUSEADDR off) | 1(SO_REUSEADDR on)>\n", argv[0]);
        exit(EXIT_FAILURE);
    }
    struct sockaddr_storage sae_local;
    socklen_t len_sae_local = sizeof(sae_local);
    char addrstr[INET6_ADDRSTRLEN], portstr[8];
    for (int i = 0;; i++) {
        memset( & ai_dest, 0, sizeof(ai_dest));
        ai_dest.ai_family = AF_UNSPEC;
        ai_dest.ai_socktype = SOCK_STREAM;
        ai_dest.ai_flags = AI_ADDRCONFIG;
        if ((rc_gai = getaddrinfo(argv[1], argv[2], & ai_dest, & ai_dest_ret)) != 0) {
            fprintf(stderr, "FAIL: getaddrinfo():%s", gai_strerror(rc_gai));
            exit(EXIT_FAILURE);
        }
        if ((fd = socket(ai_dest_ret -> ai_family, ai_dest_ret -> ai_socktype, ai_dest_ret -> ai_protocol)) == -1) {
            perror("[Client] : FAIL: socket()");
            exit(EXIT_FAILURE);
        }
        if (argv[3][0] != '0') {
            int sockopt = 1;
            if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, & sockopt, sizeof(sockopt)) == -1) {
                exit(EXIT_FAILURE);
            }
            printf("Socket option = SO_REUSEADDR(on)\n");
        } else {
            printf("Socket option = SO_REUSEADDR(off)\n");
        }
        if (flag_once) {
            /* explicit binding to the previous address */
            if (bind(fd, (struct sockaddr * ) & sae_local, len_sae_local) == -1) {
                perror("FAIL: bind()");
                exit(EXIT_FAILURE);
            }
        }
        if (connect(fd, ai_dest_ret -> ai_addr, ai_dest_ret -> ai_addrlen) == -1) {
            perror("FAIL: connect()");
            exit(EXIT_FAILURE);
        } /* if first connection */
        if (flag_once == 0) {
            if (getsockname(fd, (struct sockaddr * ) & sae_local, & len_sae_local) == -1) {
                perror("FAIL: getpeername()");
                exit(EXIT_FAILURE);
            }
            if ((rc_gai = getnameinfo((struct sockaddr * ) & sae_local, len_sae_local, addrstr, sizeof(addrstr), portstr, sizeof(portstr), NI_NUMERICHOST | NI_NUMERICSERV))) {
                perror("FAIL: getnameinfo()");
                exit(EXIT_FAILURE);
            }
        }
        flag_once = 1;
        printf("Connection established\n");
        printf("\tLocal(%s:%s) => Destination(%s:%s)\n", addrstr, portstr, argv[1], argv[2]);
        printf(">> Press any key to disconnect.");
        getchar();
        close(fd);
        printf(">> Press any key to reconnect.");
        getchar();
    }
    return 0;
}

서버는 테스트 목적이므로 간단하게 netcat을 사용하여 띄우면 된다. 예를 들어 nc -k -l 5000으로 띄워놓으면 현재 서버의 5000번 포트에서 간이 서버로 작동된다. 그 다음에 다른 터미널을 하나 열어서 watch -n 1 "ss -4tan" 을 명령해둔다. 이렇게 하면 1초마다 자동으로 ss -4tan 명령을 실행하므로 연결을 쉽게 확인할 수 있다. 이 명령은 IPv4의 TCP소켓 전부를 1초마다 출력한다.

그런 다음에 위 예제를 실행시킨다. 예제 실행 파일이 tcp_client 라면 ./tcp_client 192.168.10.120 5000 1 로 실행하면 된다. 주소를 192.168.10.120으로 가정했다. 맨 뒤에 1은 SO_REUSEADDR을 켤 것인지를 결정하는 옵션이다. 0이면 끄는 것이고 non-zero이면 SO_REUSEADDR을 켠다. 

 

이제 접속 후에 엔터를 계속 쳐보면 접속이 끊기고 다시 붙는 것을 볼 수 있다. SO_REUSEADDR 끄고 똑같은 실험을 해보면 바로 주소 재사용에 실패한다.

 

 

5. 결론

TIME_WAIT에 대해 인터넷에 돌아다니는 정보 중에는 틀린 내용도 많다. 교과서를 먼저 보자.

 

그리고 TIME_WAIT는 당신의 서버를 해치지 않는다는 것을 명심하자. 간혹 서버측에서 TIME_WAIT로 인해 재시동시에 socket bind 실패로 에러(errno : EADDRNOTAVAIL)가 발생하는데, 이것은 SO_REUSEADDR 옵션으로 간단하게 해결 된다. 클라이언트측에서도 SO_REUSEADDR을 사용하여 바인드(explicit binding)가 가능하다. 

참고로 LINGER 기능은 나쁜 것이 아니다.(즉 RST는 나쁜게 아니다) 몇몇 책이나 블로그에서 RST를 잘못된 상태로 표현하는데 반은 맞고 반은 틀리다. 심지어 프로그램에 버그가 발생해서 죽으면 RST가 발생한다고 하는 경우도 있는데, 아니다. RST는 명시적으로 application layer의 패킷에서 약속된 종료 메시지를 주고 받는 시스템이나 IoT시스템, 스트리밍의 경우에 종종 사용된다. (이런 경우의 설계에서도 idle socket을 처리하는 타임아웃을 꼭 설계하는게 좋다. 좋은 네트워크 프로그래머는 idle과 heartbeat, 명시적 데이터 보존이나 파괴 이벤트를 잘 설계하는 것이다.) 따라서 프로그래머는 RST를 특이한 친구라고 생각해야만 한다. RST를 나쁜 친구라고 인식하면 안된다. RST는 특정한 소켓 상태를 해결해주는 매우 좋은 도구라는 것을 명심하자.

 

 

* 히스토리

2024.03.03 3번의 FIN, RST상태에 대한 질문 및 답변 내용 업데이트, 결론 부분의 LINGER 관련 첨언

2021.02.07 소스코드 블록 깨짐 수정

2016.05.21 SO_REUSEADDR의 예제 추가

2016.04.22 SO_REUSEADDR과 TCP TS의 내용 보강

2015.10.08 time wait bucket에 대한 설명 추가

 

* 레퍼런스

 

  1. W. Richard Stevens. (1994). TCP/IP Illustrated Volume 1. p240 TCP state Transition Diagram. [본문으로]
  2. 김선영. (2012). Advanced 리눅스 시스템 네트워크 프로그래밍 2판. 가메출판사. p250 TCP 소켓 관련 함수와 TCP 상태 전이 [본문으로]
  3. Clark Satter, Reset은 어디서 오는 것일까? (황새가 물어다 주지 않습니다). http://blogs.technet.com/b/escape/archive/2010/01/06/reset.aspx [본문으로]
반응형
Comments