상세 컨텐츠

본문 제목

네트워크 프로그래밍 - TCP State Transition Diagram

개발/Network

by 2021. 10. 3. 08:27

본문

반응형

TCP State Transition Diagram

TCP가 연결을 요청하고 수락하는 과정 각각의 상태에 대해서 알아보도록 하자. 

TCP의 상태들을 나타낸 위 그림의 첫인상은 굉장히 복잡하다. 아래 그림을 통해 먼저 알아보도록 한다.

Time-line diagram for connection establishment and half-close termination

왼쪽의 클라이언트가 SYN을 보내고 서버에게서 SYN + ACK을 받을 때 까지의 기간을 SYN-SENT라고 한다. 

SYN + ACK을 받고 나면 클라이언트는 ACK을 보내는데, ACK을 보내고 난 후 부터를 ESTABLISHED (연결완료 상태)라고 한다. 

서버의 경우 클라이언트로부터 첫 SYN을 받기 위해 연결을 준비하는 상태를 LISTEN이라고 한다. 클라이언트로 부터 SYN을 받으면 SYN + ACK을 보내고, 클라이언트로부터 ACK이 올 때까지 기다리는 상태를 SYN-RCVD 상태라고 하고, ACK이 들어오면 ESTABLISHED(연결) 상태가 된다.

 서로가 ESTABLISHED 상태가 되어야한 한쪽이 데이터를 보내는 것에 대해서 주고받을 수 있다. 

 

 연결이 된 (ESTABLISHED)상태에서 클라이언트가 FIN을 보낸다.(보통은 클라이언트가 먼저 연결종료 요청을 보내는 것이 일반적임) 클라이언트가 보낸 FIN에 대한 ACK이 오기 전까지의 상태를 FIN-WAIT-1이라고 한다. 서버에게서 ACK을 받으면 FIN-WAIT-2가 되고 서버로부터 데이터를 받다가 서버의 FIN을 받으면 그에 대한 ACK을 보내고 클라이언트는 TIME-WAIT상태가 된다. 

 위 그림에서 2MSL 이란 Maximum Segment Lifetime이다. 이는 1MSL당 대략 (30s~1m)정도인데 2MSL이므로 대략 1m~2m 정보를 timer로 대기하다가 시간이 만료되면 CLOSED 상태가 된다. 

 

 서버의 경우 클라이언트에게 FIN이 오면 AKC을 보내고 CLOSE-WAIT 상태가 된다. 서버에서 보낸 FIN에 대해서 ACK이 올 때 까지를 LAST-ACK 이라고 한다. 

 

State Transition Diagram

이렇게 본 상태에서 아래 그림을 다시 보자.

익숙한 상태들이 보인다. 여기서 실선은 클라이언트가 가는 상태를 표시한 것이다. 실선을 먼저 보도록 하자. 

 

CLOSED에서 실선으로 이어지는 Active open / SYN은 말 그대로 활동을 시작하기 위해서 SYN을 보내는 것을 말한다. 

클라이언트가 SYN을 보내고 SYN + ACK이 올 때까지의 상태를 위에서 SYN-SENT라고 한다고 했었다. 

 SYN-SENT 상태에서 SYN + ACK이 도착하면 그에 대한 ACK을 보낸다. 그러면 클라이언트는 ESTABLISHED(연결) 상태로 들어간다.

 

 이번에는 서버가 가는 상태를 표시한 점선을 보자.

서버는 Passive open 즉 수동적으로 오픈하여 LISTEN상태에서 대기한다. 

LISTEN 상태에서 SYN이 오면 LISTEN상태가 끝나고 SYN + ACK을 보내준다. 그리고 상태는 SYN-RCVD 상태로 변경된다. 

SYN-RCVD 상태에서 클라이언트에게 ACK이 올 때까지 기다리다가 ACK이 오게되면 ESTABLISHED 상태로 들어간다.

 

현재 위 그림에서 컬러로 표현된 화살표는 특수한 상황을 나타내는 것이므로 지금은 다루지 않는다. 

 

이번에는 연결 상태에서 종료로 가는 과정을 보자.

클라이언트가 먼저 Close함수를 통해 FIN을 보내준다. 클라이언트는 FIN-WAIT-1 상태가 된다. 

서버에서는 FIN + ACK이 올 수도 있고(컬러선) ACK만 올 수도 있다.(흑색 실선)

ACK만 온경우 클라이언트는 FIN-WAIT-2 상태가 되고 서버의 FIN을 기다린다. 

서버에게서 FIN이 오면 ACK을 보내어 TIME-WAIT상태로 바뀐다. 

(컬러선) 만약 FIN + ACK이 온 경우에는 FIN-WAIT-1에서 ACK을 보내고 바로 TIME-WAIT상태가 된다. 

위에서 TIME-WAIT 상태가 되면 2MSL만큼(대략1~2분)을 timer로 대기한다고 하였다. 대기 후 CLOSED 상태로 들어간다. 

 

이번에는 서버의 종료 과정을 보자.

서버는 ESTABLISHED 상태에서 클라이언트에게서 FIN이 오면, ACK을 보내주고 CLOSE-WAIT상태로 변경한다. 

클라이언트의 종료 과정에서 보았듯 서버가 FIN + ACK을 보내는 경우도 있는데, 이는 ACK을 보낸 시점으로부터 FIN을 보내는 간격이 상당히 짧을 경우에 그렇게 된다. 

서버가 CLOSE-WAIT상태에서 Close 함수를 호출하면 FIN이 전송된다. FIN이 전송되면 클라이언트로부터 마지막 ACK을 기다리는데 이 상태가 LAST ACK상태이다. 

LAST ACK상태에서 ACK을 받으면 CLOSED 상태가 된다. 

 

CLOSED상태는 무엇인가

클라이언트와 서버는 각각 데이터를 주고받기 위한 버퍼를 Sending / Receiving 두 개씩 가지고 있다.

CLOSED 상태는 이 버퍼들을 각자 다 운영체제에 반납하여 없애버린 상태를 말한다. 

 

TIME-WAIT STATUS

TIME-WAIT가 필요한 이유에 대해서 알아보자.

클라이언트가 FIN을 보내고 서버가 FIN + ACK을 보냈을 때 클라이언트가 ACK을 받자마자 CLOSE한다고 가정해보자. 그렇다면 서버의 FIN에 대한 ACK이 없어진 것이다.

 일정 시간 패킷 응답이 오지 않으면 패킷 분실로 간주하고 패킷을 재전송하는데, 서버 입장에서 보낸 FIN에 대해 ACK이 일정 시간이 지나도 돌아오지 않으니까 FIN 재전송을 시도한다.

하지만 다시보낸 FIN에 대해서 받을 클라이언트는 없으므로 응답은 가지 않는 상황이 발생한다.

 

또 한가지 상황을 더 알아보자

위와 같이 클라이언트의 IP/PORT번호, 서버의 IP/PORT번호가 있다. IP가 같고 PORT번호가 다른 경우는 한 컴퓨터의 다른 브라우저로 서버에 접속한 경우라고 볼 수 있다. 

클라이언트가 서버에 SYN을 보내고 서버가 SYN + ACK을 보내고 클라이언트가 ACK을 보내어 연결이 되면, 서버는 소켓을 새로 만들어서 만들어진 소켓이 상대방과 대화를 한다. 

또 다른 클라이언트가 서버와 연결을 시도하면, 마찬가지로 또 하나의 소켓을 서버는 만들어서 클라이언트와 연결시킨다. 

 

for(i=0; i<5; i++)
{
	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);

	while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
		write(clnt_sock, message, str_len);

	close(clnt_sock);
}

위 코드는 서버측에서 실행되는 코드이다. 

serv_sock는 socket함수를 통해서 생성된 file descriptor값이 담겨있다. serv_sock은 클라이언트에서 서버가 보낸 SYN에 대한 ACK이 왔을 때 넣어준다. 

accept의 return은 새로운 소켓번호(file descriptor)가 clnt_sock으로 반환된다. 

clnt_sock에서 read를 하게 되면 상대방이 보내는 데이터는 clnt_sock의 소켓 버퍼로 들어오게 된다. 

write함수를 통해 clnt_sock에 데이터를 쓰면 상대방으로 보낸다.

위 경우 서버는 2개의 소켓을 사용하고 있으므로 file descriptor 번호는 3,4번을 사용하고 있을 것이다. (0,1,2는 IO 관련)

또한 프로세서가 하나이기 때문에 서버는 추가적인 소켓을 1개 사용하고 있어 총 2개의 소켓을 사용중이다. 

 

read함수는 clnt_sock의 내용을 BUF_SIZE만큼 message에 저장하고 읽은 양을 str_len으로 return한다. 

write함수는 message의 내용을 str_len byte만큼 clnt_sock에 보내준다. 

 

read를 통해 receiving buffer의 내용을 어플리케이션 buffer로 가지고 올라오는 것이고, write를 통해 어플리케이션 buffer의 내용을 sending buffer로 옮 겨준다.

 

read의 return이 0이 아니라는 것은 데이터가 존재해서 받고 어플리케이션 buffer로 올렸다는 것이다. 데이터를 받다가 상대방이 FIN을 보내면 read의 return 값이 0이 나오게 된다. 

위 코드는 총 5번을 도는 반복문이므로 5명의 클라이언트를 처리하는 프로그램이다.

 

사실 서버는 5번을 순차적으로 하는것이 아니라 동시에 다룰 수 있어야 하는데 이는 추후 에 알아보자.

 

다시 이 그림을 보자. 

서버는 어떤 클라이언트가 보냈는지를 보고 어떤 소켓이 해당 클라이언트를 담당할 지 즉 어떤 프로세스가 클라이언트를 담당할 지를 보고 클라이언트의 요청을 프로세스로 보낸다.

클라이언트와 서버가 연결되었다가 FIN 과 ACK를 주고받아 연결이 끊어졌다가 다시 연결할 일이 생겼다고 해보자.

만약 이 상황에서 TIME-WAIT없이 CLOSE를 해버리면 클라이언트가 요청을 다시 보내야할 때 기존에 쓰던 포트번호를 반납하고 같은 포트번호를 다시 할당한다고 하자. 

sequence number가 7654인 패킷을 보내고 연결을 끊었는데, 이 패킷이 네트워크를 방황하다가 다시 연결된 후에 도착했다고 해보자. 다시 연결되었을 때 선정된 random sequence number의 수가 7654로부터 굉장히 멀면 상관없지만, 7000처럼 비교적 가까운 수가 나왔을 경우, 연결된 패킷으로 착각할 수 있다.

만약 다시 연결한 경우의 포트번호가 이전과 다르면 패킷 구분이 가능하다. 

이처럼 연결 종료 후 새로운 연결을 같은 포트번호로 열게 되면 이전 connection에서 보냈던 패킷이 새로운 connection에 전송될 경우, 서버가 패킷이 이전 연결 패킷인지 현 연결 패킷인지 구분할 방법이 없게된다. 

 TIME-WAIT을 하게되면 이전 연결 패킷이 네트워크를 돌다가 소멸할 때까지 기다리는 것이다.

반응형

관련글 더보기