아롱이 탐험대

Transmission Control Protocol (TCP) (2) 본문

CS/Computer network

Transmission Control Protocol (TCP) (2)

ys_cs17 2022. 3. 27. 23:53
반응형

State transition diagram

[그림 1] State transition diagram

위 다이어그램은 클라이언트와 서버 간 연결을 요청부터 연결 종료까지의 전 과정을 한눈에 보여주는 다이어그램이다. 위 다이어 그램을 한눈에 살피는 것보다는 차근차근 보는 것이 이해가 더 잘될 것 같아 더 디테일한 다이어그램을 보고 다시 오자.

[그림 2] [그림 1]에 대한 Time-line diagram

[그림 2]는 [그림 1]에 대한 타임 라인으로 정리한 다이어그램이다.

왼쪽이 client이고, 오른쪽은 server이다. 우선 client의 관점으로 상태 변환을 살펴보자.

client에서는 SYN으로 연결 요청을 하고, server로 부터 SYN+ACK을 기다린다. 이 때 client의 상태를 SYN-SENT라고 부른다. SYN+ACK을 받은 후 server에게 ACK을 보내면 client의 상태는 ESTABLISHED로 바뀐다.

server의 관점에서는 clinet의 packet이 연결하기 전까지 LISTEN으로 기다리고 있다. client로부터 SYN이 오면 SYN+ACK을 보내주고 이후 상태는 SYN-RCVD가 된다. 그 후 client로부터 ACK이 오게되면 client와 같이 ESTABLISHED 상태로 변경된다.

지금까지 설명한 상태들은 연결 요청부터 연결까지의 과정이고, 이제는 연결 종료에 대해 알아보자.

보통 연결 종료 요청은 client 측에서 먼저 한다. client가 server에게 FIN을 보낸다. 이때 FIN이 오기 전까지 client의 상태는 FIN-WAIT-1이다. 그 후 server가 ACK을 보내면 client의 상태는 FIN-WAIT-2가 된다.

여기서 다음 단계인 TIME-WAIT에서는 2MSL이 있는데 이는 Maximum Segment Lifetime의 약자이다. 역할은 시스템에 따라 30초에서 1분 정도 타이머를 켜 두었다가 만료되면 CLOSED 상태가 된다.

server 입장에서는 FIN이 오면 ACK을 보내주고 CLOSE-WAIT 상태로 기다리다가 FIN을 다시 보내고 나서는 LAST-ACK 상태가 된다. 이는 client로부터 마지막 ACK을 기다린다는 의미이고, ACK이 오면 CLOSED 상태가 된다.

각 상태마다 어떠한 패킷을 전송하는 액션이 일어나야 한다.

 

위 상태들을 쉽게 정리해보자.

Client의 상태

- SYN-SENT: SYN을 보내고 server로부터 SYN+ACK을 기다리는 상태

- ESTABLISHED: SYN+ACK을 받고 ACK을 보낸 상태. 이때 data 전송이 일어난다.

- FIN-WAIT-1: server에게 FIN packet을 전송한 후 ACK을 기다리는 상태

- FIN-WAIT-2: server에게 ACK을 전달받고, FIN을 기다리는 상태

- TIME-WAIT: server에게 ACK을 전송한 후 CLOSED 전 2MSL 상태

- CLOSED: 연결 종료 상태

 

Server의 상태

- LISTEN: client의 연결 요청을 기다리는 상태

- SYN-RCVD: client로부터 SYN을 받고 SYN+ACK을 보내준 상태

- ESTABLISHED: client로부터 ACK을 받고, 데이터 전송을 하는 상태

- CLOSED-WAIT: client로부터 FIN을 받고 ACK을 전송한 상태

- LAST-ACK: client에게 FIN을 전송한 후 ACK을 기다리는 상태

- CLOSED: client로부터 ACK을 받은 뒤 연결 종료 상태

 

다시 [그림1]로 돌아와서 살펴보자.

[그림 1] State transition diagram

검은색 실선인 client의 오른쪽 위인 Acive open부터 살펴보자.

Active open을 위해 client는 server에게 SYN을 통해 연결 요청을 한다. 그 후 client는 server에게 SYN+ACK이 오기를 기다린다. (SYN-SENT 상태)

그 후 SYN+ACK/ACK이 되는데 이는 SYN+ACK이 오면 ACK을 보내준다는 의미이다.

검은색 점선인 server의 입장을 보자 처음에 passive open을 통해 서버를 열어 둔다. (LISTEN 상태) 그 후 SYN이 오는 것을 기다린 후 SYN이 오면 SYN+ACK을 보내주며 SYN-RCVD 상태로 바뀐다. 그 후 client로부터 ACK이 오면 ESTABLISTED 상태로 변경된다.

이때까지의 상태를 normal 상태라고 부른다.

빨간색 선은 특이한 케이스이므로 무시해도 좋다.

자 그럼 이제 연결 상태에서 종료 과정을 보자. server의 입장에서는 3-way 또는 4-way로 갈 수 있다. 연결된 상태에서 client가 먼저 FIN을 보낸다. (close 함수 호출) FIN을 보낸 client는 FIN-WAIT-1로 바뀐다. FIN-WAIT-1에서 FIN+ACK 혹은 ACK을 기다린다. 3-way 기준으로 보자. FIN+ACK이 오면 client는 TIME-WAIT가 되고, 4-way에서 ACK이 먼저 오면 FIN-WAIT-2가 된다. FIN이 오면 ACK을 보내 준 후 TIME-WAIT가 된다. 그 후 2 MLS에 의해 1~2분 있다가 CLOSED 상태로 변한다.

server의 입장에서 보자. FIN이 오면 FIN+ACK을 보내준다. 이때 server의 전송에서 FIN이랑 ACK이 간격이 작으면 같이 전송이 되고,  크면 따로 보낸다. 그 후 CLOSED-WAIT로 기다렸다가 함수 안에서 close 함수를 호출하면 FIN이 전송된다. 그 후 LAST-ACK 상태로 바뀐다. client로부터 마지막 ACK이 오면 close 상태가 된다.

close 상태는 무엇을 의미할까? client랑 server는 데이터 전송을 위해 buffer가 각 2개씩 필요하다. (sending buffer, recieving buffer), 이때 close 상태가 되었다는 것은 client와 server의 buffer를 다 없앤 상태를 의미한다.

 

TIME WAIT는 왜 필요할까?

Time wait가 필요한 이유는 크게 2가지 이유가 있다.

[그림 3] Time line for a common scenario

마지막으로 서버로 보내는 ACK이 없어지는 경우를 대비해 time wait을 진행한다. client 입장에서 마지막 ACK을 보내면 할 일을 다한 것이다. CLOSED 상태에서는 버퍼가 다 지워진 상태인데, 이때 서버 입장에서는 FIN에 대한 ACK을 기다리는데 만약 ACK이 없어지면 서버는 대기하게 된다. 서버는 재 시간 안에 ACK이 오지 않으면 packet이 없어졌다고 생각한다. 그래서 FIN을 다시 재전송한다. 하지만 이 상황에서 client는 버퍼를 다 지운 상태이다. 따라서 받아줄 상대가 없는 상황이라 이를 방지하고자 Time wait이 있다.

 

2번째 이유를 이해하려면 사전적 지식이 필요하다.

[그림 4] 다중 접속 예시

[그림 4]와 같이 3개의  client가 server에 접속하는 상황을 생각해보자. client 1, 2는 같은 IP를 사용하고 있다. 이는 한 컴퓨터에서 2개의 브라우저가 접속하는 상황과 같다. 각 소켓에 해당 client와 연결을 해야 되니깐  오른쪽에 있는 server 소켓들은 recieving, sending 버퍼가 필요하다. 새로운 연결 요청이 오면 server에서는 새로운 소켓을 만들어 해당 소켓으로 client의 소켓과 연결을 한다. 만약 client 3이 연결 요청을 하면 server에서도 내부적으로 소켓을 하나 더 생성한다. 아래 코드 예시를 보고 오자.

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);
	}

해당 코드는 client에서 message를 보내면 server에서는 해당 message를 다시 client에게 전달해주는 echo server의 예시 코드이다.

코드에서 accept 함수를 보면 첫 번째 인자인 serv_sock은 socket()을 통해 생성한 server의 파일 디스트립터 값을 bind()로 바인딩 한 변수이다. 이를 받는 함수가 accept()이다. SYN이 들어오면 새로운 clnt_sock이 return 된다.

serv_sock은 보통 3이고, clnt_sock은 아마 4번 일 것이다. (0: read, 1: write, 2: error를 담당하기 때문에 file descriptor는 3번부터 할당 가능하다.) 3번 소캣은 보통 SYN을 받는 소켓이고, 4번은 read를 통해 상대방이 보낸 message를 읽는 데 사용한다. (4번은 sending, recieving buffer가 존재한다.) 여기서 write를 하면 client에게 다시 전달한다. 서버는 소캣을 동시에 2개 움직인다. (3번, 4번) 하지만 실질적으로는 1개의 스레드로 처리한다.

여기서 강조하는 내용은 최소한 소캣이 2개 이상 동작한다는 것이다.

코드에 대해 추가적으로 for문이 5번 도는 이유는 read()를 통해 client가 보내는 message를 계속 읽고, 읽을 값을 message array에 저장하고, return 은 읽은 message의 바이트 수가 된다. write 함수는 message의 데이터를 str_len만큼 보낸다.

read는 recieving buffer에 들어가 있고, read()는 recieving buffer에 있는 것을 app단에 가져오는 역할을 한다. write는 반대로 app단에 있는 message를 sending buffer에 보내주는 역할을 한다. 우리가 하는 네트워크 프로그래밍은 app단에서 진행하는 것이다.

message가 들어오기 전 read()는 block 함수이다. block 함수란 무슨 일이 실행되고, 해결이 될 때까지 대기하는 함수이다. 만약 read를 호출했는데 buffer에 저장된 데이터가 없으면 read()는 그대로 가만히 있는다. 데이터가 존재하면 바이트 길이를 return 하고 종료한다.  따라서 read가 return 했다는 의미는 1바이트라도 데이터가 들어왔다는 뜻이 된다.

close()를 통해 처음으로 돌아가 다른 client와 연결을 하게 된다. 이때 serv_sock은 4번이 되고, 이는 첫 번째 loop의 4번이랑은 다른 file descriptor이다.

 

[그림 4] 다중 접속 예시

[그림 4]로 다시 돌아가서 서버는 여러 개의 client를 다루어야 한다. client가 연결될 때마다 소켓이 하나씩 생긴다. 초록색 소켓은 SYN만 받는 소켓이라 서버에 1개만 존재한다. (#80번 소켓) 아래 사파이어 색이 read, write를 담당하는 소켓이다.

서버는 내부적으로 5개 요소를 가지고 flow를 구분한다. 그것은 바로 [그림 4] 맨 아래 있는 5가지 요소이다. 각 요소는 상대방 IP 및 port number, 나의 IP 및 port number, 그리고 프로토콜 유형이다. 

이 정보들을 테이블에 저장하고, 위 번호에 따라 소켓을 구분한다.

[그림 5] time wait이 필요한 2번째 이유

지금까지 사전 지식에 대해 알아보았다.

[그림 5]의 상황을 보자. 현재 파란색 선인 client에서 마지막 ACK을 보낸 상황이다. 하지만 갑자기 다시 연결해야 할 경우가 생겼다고 가정하자. 이때 client는 다시 SYN을 보내고 SYN+ACK을 받고 다시 ACK을 보내면서 데이터 통신을 재게 한다.

client의 포트는 시스템이 알아서 지정한 번호이다. 만약 time wait을 안 두고 close를 하면 소켓이 다시 생성되고 3456으로 다시 할당이 되는 경우가 생긴다. 이때 문제가 생기게 되는데, 소켓이 가는 경로는 항상 같지가 않다. 문제가 생기면 기존 경로와 다른 경로를 통해 목적지에 갈 경우도 있다. 이때 이전 연결에서 보냈던 데이터가 어떤 문제가 생겨 다른 경로로 갔다가 나중에 들어왔다고 생각해보자. [그림 5]에서 7654번 소켓이 나중에 들어왔다고 생각하자. 이때는 기존 소켓 3456이랑은 거리가 멀어서 무시할 수 있다. 하지만 기존 소켓의 번호가 3456이 아니고 7000번대라면 buffer 입장에서 헷갈릴 수도 있다. 이를 방지하고자 포트 번호를 3456에서 다른 번호로 바꿔준다.

따라서 최종적인 내용은 time wait이 있어야 3456번인 소켓을 할당을 안 하고 재연 결시 3456을 제외한 다른 번호로 할당하기 때문이다. 이로 인해 나중에 들어온 데이터를 무시할 수 있다.

이로써 time wait이 필요한 2가지 경우를 살펴보았다.

 

Reference

[그림 1], [그림 2], [그림 3], [그림 4], [그림 5]: TCP/IP Protocol Suite (McGraw-Hill Forouzan Networking) 4th Edition 서적

반응형
Comments