TCP는 두 종단간 신뢰성 있는 데이터 전송을 보장한다. 이를 위해 TCP에서 어떻게 연결 세션을 맺고 끊는지 알아보며, 마지막에는 Socket API와 TCP flow를 매핑하여 흐름을 알아보자.
1. TCP 3-way handshake
두 장치들 사이에 논리적인 접속을 성립 (establish)하기 위하여 TCP에서 사용하는 방법
- 정확한 전송을 보장하기 위해 상대방 장치와 사전에 세션을 수립하는 과정을 의미한다.
TCP 3-way handshaking 과정
-
클라이언트는 서버에 접속을 요청하는 SYN 패킷을 보낸다. 이때 클라이언트는 SYN 을 보내고 SYN/ACK 응답을 기다리는 SYN_SENT 상태가 된다.
-
서버는 SYN요청을 받고 클라이언트에게 요청을 수락한다는 ACK 와 SYN flag 가 설정된 패킷을 발송한 후, 클라이언트가 다시 ACK으로 응답하기를 기다린다. 이때 서버는 SYN_RECEIVED 상태가 된다.
-
클라이언트는 서버에게 ACK을 보내고 이후로부터는 연결이 이루어지고 데이터의 송수신이 이루어지며, 이때의 클라이언트와 서버의 상태가 ESTABLISHED로 확정된다.
위와 같은 흐름으로 TCP는 두 종단간 신뢰성 있는 연결을 수립한다.
MTU, MSS
TCP 3-Way Handshaking과정을 진행하며 두 종단간에 연결을 확립하는것 이외에도 한번에 보낼수 있는 패킷의 최대 크기를 설정한다.
MTU (Maximum Transmission Unit)
- 전송할 수 있는 패킷의 최대 크기
- Ethernet 환경 default : 1500 byte
MSS (Maximum Segment Size)
- TCP상에서 전송할 수 있는 사용자 데이터의 최대 크기
- MTU값에 의해 결정된다
- MSS = MTU - IP header size - TCP header size
- Ethernet의 경우, MTU 1500에 IP헤더 크기 20byte, TCP 헤더 크기 20byte를 빼면 1460이다.
이 둘의 목적은 네트워크 오버헤드를 줄이고, 단일 패킷이 네트워크를 독점하는 것을 방지하기 위해서 이다.
- MTU 크기는 네트워크 오버헤드를 고려해서 설정한다.
- MTU를 크게 잡으면 더 큰 유저 데이터를 전송할 수 있게 되므로 효율적으로 보일수도 있지만, 그만큼 단위시간당 처리가능한 패킷의 수가 줄어들게 되어 네트워크 딜레이의 원인이 된다.
- 또한 오류제어를 하지 않는 패킷이 로스가 발생할 경우 그만큼 재전송해야하는 패킷이 커지는 문제도 있다.
2. TCP 4-way handshake
두 장치들 사이에 논리적인 연결을 종료하기 위해 TCP에서 사용하는 방법
TCP 4-way handshake 과정
- 클라이언트가 연결을 종료하겠다는 FIN을 전송하고, ACK을 기다리는 FIN-WAIT-1 상태가 된다.
- 서버는 FIN수신 후, 일단 ACK을 보내고, 남아있는 통신 작업을 처리하는 CLOSE-WAIT 상태가 된다.
- 클라이언트는 서버로부터 ACK을 수신한 후, 서버의 FIN을 기다리는 FIN-WAIT-2 상태가 된다.
- 서버는 남아있던 작업을 모두 끝낸 후, 자신도 연결을 종료하겠다는 FIN을 보낸 후, LAST-ACK 상태가 된다.
- 클라이언트는 서버의 FIN을 받은 후, 일정 시간 대기하는 TIME-WAIT상태로 진입하며, 서버에게 ACK을 보낸가.
- 서버는 클라이언트의 ACK을 받은 후, 연결을 종료한다. (close)
Tip!
만약 Server에서 FIN을 전송하기 전에 클라이언트에게 보낸 패킷이 통신 지연으로 인해, FIN보다 늦게 도착하는 상황이 발생하면 클라이언트는 이 패킷들을 수신할 수 없을까?
-> 답은 아니다. 클라이언트는 FIN을 받은 후, 일정시간(Default: 240s)동안 세션을 남겨놓고, 잉여 패킷을 기다리는 과정을 거친다. 이를 TIME_WAIT라고 한다.
3. TCP의 세션을 맺고 끊는 과정을 socket programming과 함께 생각해보기
세션 수립 과정
- Client에서 connect()를 호출하기 전, Server는 socket을 생성한 후, bind(), listen()이 수행되어야 하고, accept()에서 block된 상태이어야 한다.
- Client가 connect()를 호출하게 되면, 먼저 SYN 패킷을 서버에 전송한다.
- Server는 이를 수신한 후, Client에게 SYN, ACK 패킷을 전송한다.
- Client는 Server의 SYN, ACK 패킷을 받은 후, connect()함수의 return값을 받게 된다. ( Client는 서버와 연결되었다고 인식하고, ESTABLISHED상태가 된다.)
- Client는 Server에게 ACK패킷을 보낸다.
- Server는 이를 수신 후, accept() block에서 빠져나오며, 연결을 위한 새로운 연결 소켓을 생성하고, ESTABLISHED상태가 된다.
세션 종료 과정
- Client, Server 둘 중 한곳에서 먼저 세션 종료를 시작한다는 의미로 FIN패킷을 상대방에게 전송하고 FIN_WAIT_1 상태가 된다. (Client가 보냈다고 가정)
- 최초의 FIN패킷은 출력 스트림을 종료하는것으로 보낼 수 있다. (shotdown(SD_SEND), close())
- Server는 FIN패킷을 받은 후, CLOSE_WAIT상태가 되며 Client에게 ACK패킷을 전송한다.
- Client는 ACK패킷을 받은 후, FIN_WAIT_2 상태가 된다.
- Server는 출력 스트림에 남아있는 데이터를 모두 Client에게 보낸 후, FIN패킷을 Client에게 전송하며 이 때, LAST_ACK상태가 된다.
- 서버에서는 FIN패킷을 보내기 위해, 클라이언트와 연결이 끊어진것을 인지하고, 출력스트림을 닫아야 한다.
- 이를 하지 않으면 클라이언트는 fin_wait_2, 서버는 close_wait상태에서 대기 즉 유령 세션이 발생
- Client는 Server로부터 FIN패킷을 받은 후, 일정시간 대기하는 TIME_WAIT상태가 되며, Server에게 ACK패킷을 보낸다.
- Server는 ACK패킷을 받을 후, 소켓을 종료하고 CLOSED 상태가 된다.
4. 간단한 테스트
Client와 Server간 TCP 소켓을 이용하여 통신을 하던 중, Client Process를 kill 명령어로 강제로 죽여 보았다.
1. 정상적인 세션 수립
2. 클라이언트 프로세스를 강제로 kill
3. 새로운 세션 수립
4. 새로운 프로세스 kill
클라이언트 프로세스를 강제 종료하게 되면, 위와 같은 유령 세션이 계속해서 발생한다.
FIN_WAIT_2의 경우 일정시간이 지나면 TIME_WAIT상태로 변하지만, CLOSE_WAIT의 경우 상태변화 없이 계속 남아있기 때문에 서버의 리소스를 낭비하게 된다.
의문점
아래 내용은 TCP Socket Programming에 대한 공부한 내용을 바탕으로 한 내 생각이다.
1. FIN패킷은 shutdown(), close()를 통해 출력스트림을 닫을 때 전송되는데, Client의 프로세스를 kill 했을 때 왜 FIN 패킷이 Server에 전송되는가
FIN, ACK과 같은 TCP의 Flag와 hand-shaking은 Kernel 레이어에서 관리하는 자원(?)이라고 생각한다. 이런 플래그나 hand-shaking과정을 모두 응용프로그램에서 제어하지는 않음 (FIN 제외)
즉, Client의 프로세스를 kill했을 때, 프로세스가 사용하던 Socket의 리소스가 반납되고, 응용프로그램이 아닌 Kernel이 이를 알아채고 Server에게 소켓연결이 종료되었다는 FIN 패킷을 보내는 것이지 않을까
2. Server는 FIN 패킷을 받은 후, ACK를 Client에게 전송하여 CLOSE_WAIT상태에 돌입하는데, 왜 FIN패킷을 다시 클라이언트에게 전송하지 않는가?
너무나도 당연하고 기본적인 이유 때문인데, FIN 패킷은 응용프로그램 레이어에서 shutdown()이나 close()를 통해 출력스트림을 닫았을때 Peer(상대방)에게 전송된다.
즉 Server의 커널은 Client의 FIN패킷을 받을 후, 응용프로그램의 남은 작업을 처리하는 CLOSE_WAIT상태가 되는데, 응용프로그램에서 작업을 다 처리한 후에 shutdoen()이나 close()를 통해 FIN패킷을 Client에게 전송하지 않았기 때문이다.
따라서,
Client는 서버의 FIN을 기다리는 FIN_WAIT_2 상태
Server는 응용프로그램의 작업이 끝나기를 기다리는 CLOSE_WAIT 상태에 머물러 있게 되는 것이다.
이를 해결하기 위해서는, Server에서 Client의 연결 종료 상태를 알아채고, Server도 연결을 종료해야 하는데,
Kernel은 Client의 FIN패킷을 받아서 Client의 연결이 종료되었다는 것은 알지만, 응용프로그램은 이를 알지 못한다.
응용프로그램에서는 recv() 함수의 리턴값을 통해 이를 알아낼 수 있다. recv()의 linux man page를 보면,
recv() 함수는 기본적으로 입력 스트림에서 읽은 데이터의 byte수를 리턴한다. 만약 리턴값이 -1 이라면 error가 발생한 것이며,
0인 경우에는 상대방이 연결을 종료한것이다.
클라이언트가 연결을 종료하거나, kill되면 recv()가 -1을 리턴하고, 에러를 뱉을 줄 알았기 때문에, Server 프로그램에서 recv()의 리턴값이 -1인경우에만 Socket의 연결을 종료하는 예외처리를 해주었다.
하지만, 0을 리턴하는 경우에도 상대방이 연결을 종료했다는 의미이고, 0인경우에도 Socket을 종료하는 예외처리를 해주었더니 Client프로세스를 강제로 종료해도 정상적으로 세션종료가 완료되었다.
위 사진처럼, 예외처리를 해준 후, Client프로세스를 강제로 종료하면 Server의 세션은 종료되고, Client는 TIME_WAIT상태로 진입하여 일정시간 대기 후, 종료되는 정상상태를 보여준다.
'Network' 카테고리의 다른 글
Socket Programming 실습 (0) | 2020.11.18 |
---|---|
Socket Programming - Socket (0) | 2020.11.08 |