iBetter Books
수정

연결의 시작과 끝

TCP는 연결 지향 프로토콜입니다. 데이터를 주고받기 전에 먼저 두 소켓 사이에 길을 뚫고, 다 쓴 뒤에는 그 길을 정중하게 거둬들입니다. 이 장에서는 그 시작과 끝의 의식을 들여다봅니다. 평소에는 운영체제가 알아서 처리해 주지만, 그 속을 알아 두면 다음 장의 에러들과 그 악명 높은 TIME_WAIT을 이해할 수 있습니다.

연결의 시작, 3-way 핸드셰이크

클라이언트가 connect를 호출하면, 그 한 줄 뒤에서 세 번의 인사가 오갑니다. 이를 3-way 핸드셰이크라 부릅니다.

먼저 클라이언트가 서버에게 "연결하고 싶다"는 신호를 보냅니다. 이 신호를 SYN이라 합니다. 서버는 "좋다, 나도 준비됐다"는 답을 보냅니다. 이 답에는 수락의 의미와 함께 서버 쪽의 연결 요청도 같이 실리는데, 이를 SYN과 ACK가 합쳐진 신호라 합니다. 마지막으로 클라이언트가 "확인했다"는 ACK를 보내면 길이 완성됩니다. 세 번의 신호가 오가야 비로소 연결이 맺어지므로 3-way라 부르는 것입니다.

이 모든 과정이 connect() 한 줄 안에서 일어납니다. 우리 코드에서는 그저 함수 하나를 부른 것처럼 보이지만, 그 짧은 순간에 세 번의 왕복이 숨어 있습니다. 서버 쪽에서는 이 핸드셰이크가 끝난 연결이 accept의 대기열에 들어가고, accept가 그것을 꺼내 옵니다.

연결의 끝, close와 shutdown

대화가 끝나면 연결을 닫습니다. 닫는 방법에는 두 가지가 있고, 그 차이를 아는 것이 의외로 중요합니다.

close는 소켓을 완전히 닫습니다. 더는 읽지도 쓰지도 않겠다는 선언이며, 소켓이라는 자원 자체를 운영체제에 반납합니다. 우리가 지금까지 써 온 방식입니다.

shutdown은 조금 더 섬세합니다. 소켓의 양방향 통로 중 한쪽만 골라서 닫을 수 있습니다. 예를 들어 "나는 더 보낼 말이 없지만, 네 말은 계속 듣겠다"고 할 수 있습니다. 보내는 방향만 닫으면, 상대는 그 사실을 알아채면서도 자기 데이터는 마저 보낼 수 있습니다. 파일을 다 보낸 뒤 "전송 끝"을 알리면서도 상대의 확인 응답은 받아야 할 때 이런 반쪽 닫기가 쓸모 있습니다. 이렇게 한 방향만 닫는 것을 반이중 종료라 부릅니다.

당장은 close만으로 충분합니다. 다만 shutdown이라는 더 정교한 도구가 있다는 것, 그리고 보내는 방향을 먼저 닫아 상대에게 "내 할 말은 끝났다"고 알리는 기법이 있다는 것을 기억해 두세요. 견고한 서버를 다루는 PART 07에서 다시 만납니다.

TIME_WAIT의 정체

이제 앞 장에서 예고한 수수께끼를 풀 차례입니다. 서버를 껐다가 곧바로 다시 켜면 왜 Address already in use가 났을까요. 범인은 TIME_WAIT입니다.

연결을 먼저 닫는 쪽은, 닫은 뒤에도 곧장 사라지지 않고 한동안 TIME_WAIT이라는 상태에 머뭅니다. 직접 관찰해 보겠습니다. 서버가 연결을 받자마자 먼저 닫는 작은 프로그램을 돌려 두고, 그 사이에 연결 상태를 들여다봅니다.

리눅스에서는 ss 명령, macOS에서는 netstat 명령으로 볼 수 있습니다.

# 파일: 터미널 (macOS)netstat -an | grep 9401
tcp4       0      0  127.0.0.1.9401         127.0.0.1.53762        TIME_WAIT

상태 칸에 또렷이 TIME_WAIT이 찍혀 있습니다. 연결을 닫았는데도 운영체제가 이 연결을 일정 시간 붙들고 있는 것입니다. 리눅스에서 ss -tan으로 보면 주소 표기가 127.0.0.1:9401 형식으로 조금 다를 뿐 상태는 똑같이 TIME_WAIT으로 나옵니다.

왜 기다리는가

운영체제는 왜 이 귀찮은 상태를 유지할까요. 안전 때문입니다.

연결을 닫는 마지막 인사가 네트워크 어딘가에서 지연되거나, 상대가 보낸 늦은 데이터가 뒤늦게 도착할 수 있습니다. 만약 같은 주소와 포트로 새 연결을 곧바로 만들어 버리면, 이 늦둥이 데이터가 새 연결로 잘못 섞여 들어올 위험이 있습니다. TIME_WAIT은 그런 유령 데이터가 네트워크에서 완전히 사라질 때까지 기다리는 안전 장치입니다. 보통 수십 초간 유지됩니다.

그래서 서버를 막 껐다가 다시 켜면, 그 포트가 아직 TIME_WAIT에 잡혀 있어 다시 묶지 못하고 에러가 났던 것입니다. 앞 장에서 켠 SO_REUSEADDR 옵션이 바로 이 상황을 위한 것입니다. "TIME_WAIT으로 잠깐 붙들려 있어도 다시 묶게 해 달라"고 운영체제에 요청해, 개발 중의 껐다 켜기를 매끄럽게 만들어 줍니다.

TIME_WAIT은 평소에는 고마운 안전장치지만, 짧은 연결을 1초에 수천 개씩 맺고 끊는 고부하 서버에서는 이 상태가 쌓여 포트가 동날 수도 있습니다. 그 이야기는 고성능 서버를 다루는 PART 06에서 다시 꺼내겠습니다.

연결이 어떻게 시작되고 끝나는지 알았으니, 이제 그 과정에서 일이 틀어졌을 때, 즉 에러가 났을 때 어떻게 대처하는지를 배울 차례입니다.