TCP 통신 규악에서의 SYN, ACK, FIN 패킷을 통해 네트워크 통신 시작부터 종료까지 자세히 알아본다.
클라이언트와 서버간의 SYN, ACK, FIN 패킷을 보냈을 때의 시점을 개념화하여 자세히 알아보고, 각각의 특징, 이해를 돕는 소켓 프로그래밍, 발생할 수 있는 문제점들을 알아본다.
각각의 예시(이미지)를 통해 시점을 개념화하고 이해하자.
- 클라이언트 SYN 패킷 전송, connet 함수 호출(blocked)
Clinet: SYN_SENT,- 서버 SYN 패킷 받은 후 SYN + ACK 패킷 전송
Server: SYN_RCVD(accept blocked)- 클라이언트 SYN + ACK 패킷 받은 후, ACK 패킷 전송
Client: ESTABLISHED(통신 준비 완료, connect reuturns)- 서버 ACK 패킷 받음
Server: ESTABLISHED(통신 준비 완료, accept returns)
- 클라이언트 FIN 패킷 전송
Clinet: FIN_WAIT_1(ACK 패킷을 기다림)- 서버 FIN 패킷 받은 후 ACK 패킷 전송
Server: CLOSE_WAIT(close 기다림)- 클라이언트 ACK 패킷 수신
Client: FIN_WAIT_1(FIN 패킷을 기다림)- 서버 FIN 패킷 전송
Server: LAST_ACK(마지막 ACK 패킷을 기다림)- 클라이언트 ACK 패킷 전송
Client:TIME_WAIT
(중요한 이유로 잠시 기다림)- Closed
/을 기준으로 왼쪽은 받을 패킷, 오른쪽은 전달할 패킷이다.
여기서 유심히 봐야할 점은, 서버의 입장에서 CLOSE_WAIT 상태가 되기 이전에, FIN을 받고 ACK을 보내야하는데, 이 사이의 시간이짧으면 한 번에 보내고, 길다면 두 번 나눠서 보낸다.
위의 경우는 클라이언트가 FIN 패킷을 보낸 후 서버가 더 보낼 데이터가 남아있을 때의 모습이다.
FIN 패킷을 보낸 후 클라이언트의 sending buffer는 삭제되지만 Receiving buffer는 살아있어, 서버가 전송한 데이터를 받을 수 있다.
for(i=0; i<5; i++) { //새로운 client 여기서 serv_sock은 연결만 받아주는 소켓 clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr,&clnt_adr_sz); if(clnt_sock==-1) error_handling("accept() error"); else printf("Connected client %d \n", i+1); //클라이언트로부터 정보를 받아옴 read가 0을 반환할 경우는 FIN 패킷을 받았을 경우 while((str_len=read(clnt_sock, message, BUF_SIZE))!=0) //그대로 다시 클라이언트에게 전달 write(clnt_sock, message, str_len); //종료 close(clnt_sock); }
read가 0인 경우 이론 책에 EOF를 데이터에 추가하여 전송한다 나와있지만 사실 FIN 패킷을 보내야함
위의 코드는 하나의 클라이언트씩 순차적으로 통신하는 서버이다.
클라이언트가 송신한 데이터를 서버가 그대로 클라이언트에게 전달한다.
(두 번 이상의 요청이 오면 queue에 대개시켜놓는다.)
TIME_WAIT은 MSL(Maximum Segment Lifetime)이 지나면 상태가 종료되고 close된다.
MSL은 대개 30s~1m 인데, 2가 곱해져있으니 1m~2m이 된다.
close 상태가 됐다는 것은 네트워크 통신에 필요한 모든 데이터를 삭제한다는 것이다.
위의 사진을 보면 다수의 클라이언트와 하나의 서버가 통신하는 방식이다.
서버와 가장 가까운 소켓은 클라이언트의 연결 요청을 받고 통신하기 위한 개별적인 소켓을 만들어주는 역할을 한다.
이 클라이언트는 자신들과 소통하는 개별적 소켓이 있다는 것은 모르고, 그냥 서버의 소켓이랑 통신한다.(개별적 소켓으로 가기 이전에 서버의 소켓을 먼저 거친다.)
서버 소켓이 클라이언트를 구별하는 방식은 클라이언트의 IP 주소와 PORT 번호이다.
여기서 서버는 IP 주소와 PORT 번호를 통해 클라이언트를 식별한다는 것을 이해했다.
Client 1이 data를 담은 segment와 FIN packet을 순차적으로 전송했을 때, 도착 순서는 보장되지 않는다. 이러한 이유로 연결 종료가 먼저 발생할 수 있다.(네트워크 혼잡 등)
이후에 바로 클라이언트가 재 연결을 했을 때, 시스템이 클라이언트의 포트넘버를 이전 연결에 쓴 포트넘버로 할당해줄 가능성이 있다.
우연으로 seq 번호도 7654와 가까운 seq number가 임의로 선택되어 데이터 송신을 할 때, 이전 연결에 전송되지 못한 segment가 서버로 전송된다면 IP 주소와 PORT 번호만으로 클라이언트를 식별하는 서버는 데이터를 받을 수 밖에 없을 것이다.
이를 방지하기 위해 TIME_WAIT
state를 사용한다. 1m~2m 시간동안 완전히 연결을 해제하지 않는다.
그렇다면 PORT 번호는 여전히 남아있을 것이고 새로 연결을 한다 하면 새로운 포트넘버가 할당될 것이다.
또한 종료 과정에서 server가 FIN을 보내고나서 마지막으로 ACK을 받아야 할 때, client로부터 전송된 ACK이 소멸될 수 있다. 이 상황을 위해서도 TIME_WAIT 상태가 필요하다.
이러한 상황을 대비하여 TCP는 타이머가 존재한다. sender가 요청 또는 데이터 패킷을 보내면 이를 받았다고 알려주는 ACK packet을 receiver가 전송한다. 이 과정이 걸리는 시간을 sender의 입장에서 왕복시간(Round Trip Time)이라 볼 수 있다. Fin packet을 보내는 server가 RTT 시간을 재는데, 여기서 ACK packet이 클라이언트로부터 오지 않을 경우 다시 FIN packet을 전송한다.
이 때 클라이언트는 TIME_WAIT 상태가 존재하지 않는다면 그냥 프로그램을 종료해버릴 것이다.
첫 번째 이미지는 RST 패킷을 통해 SYN 패킷을 거절하는 모습이다.
두 번째 이미지는 RST + ACK 패킷을 통해 연결을 바로 종료해버린다.(바로 close 상태로)