Search
🌐

[Socket] TCP 3-Way-Handshake에서의 Socket State를 관찰해보자

간단소개
ContributorNotionAccount
주제 / 분류
태그
Scrap
팔만코딩경 컨트리뷰터 (Library DB (속성)에 관계됨)에 관계됨
7 more properties
구현을 하기 전에…
OSI 7 계층에 대한 지식이 충분하다.
TCP에 대한 기본적인 지식을 가지고 있다.

TCP 3-Way Handshake?

우선 TCP 3-Way-Handshake(이하 3WHS)과정에 어떤 State들이 있는지 알아보자. 아래 사진은 3WHS과정을 가장 잘 나타낸 사진 중 하나이다.
이번 포스팅은 실제로 State들을 관찰하는 것이 목표이기 때문에, 각각의 State와 Segment에 들어가는 내용들(위 사진의 SYN, ACK 등)에 대한 자세한 정보는 아래의 블로그를 참고하자. 상세하게 설명이 되어 있다.

Socket 프로그래밍을 위한 함수 공부하기

이제 실제로 해당 상태를 관찰하기 위한 프로그램을 구현하기 위해 필요한 시스템콜과 함수들을 공부해보자. 이전에 미리 함수들을 정리한 포스팅이 있어서 링크를 첨부하였다.

소켓 프로그래밍을 통해 socket 상태 관찰하기

[server] LISTEN

아래 코드를 참고해서 간단하게 소켓을 LISTEN상태로 만들어보자. 시스템콜 함수에 대해 공부를 마쳤다면 스스로 구현해 보는 것이 가장 좋다.
#include <iostream> #include <netinet/in.h> #include <sys/socket.h> #include <fcntl.h> // open function #include <unistd.h> // close function #include <strings.h> #include <arpa/inet.h> #define PORT 9000 #define IPADDR "127.0.0.1" char buffer[BUFSIZ] = "hello, world"; char rBuffer[BUFSIZ]; struct sockaddr_in set_socket_addr() { struct sockaddr_in ret; memset(&ret, 0, sizeof(ret)); ret.sin_addr.s_addr = htonl(INADDR_ANY); ret.sin_family = AF_INET; ret.sin_port = htons(PORT); return ret; } int main() { int s_socket; struct sockaddr_in s_addr; s_socket = socket(PF_INET, SOCK_STREAM, 0); s_addr = set_socket_addr(); if (bind(s_socket, (struct sockaddr *)&s_addr, sizeof(s_addr)) == -1) { std::cout << "Can not bind!" << std::endl; return -1; } if (listen(s_socket, 5) == -1) { std::cout << "Listen Fail" << std::endl; return -1; } while (1) ; close(s_socket); }
C++
복사
이제 소켓의 상태를 관찰해야 하는데 가장 간단하게 확인할 수 있는 방법은 lsof명령을 활용하는 것이다. lsof명령을 활용하면 열려있는 fd와 해당 fd의 상태까지 모두 확인할 수 있다.
lsof -p pid
Shell
복사
실행한 서버프로세스의 pid를 확인한 후 lsof명령을 실행해보자. 아래 사진처럼 3번 fd에 TCP소켓이 열려있는 것을 확인할 수 있다. 괄호 뒤에 나오는 것이 해당 소켓의 3WHS State이다. 기대했던 것 처럼 LISTEN 상태로 소켓이 유지되는 것을 볼 수 있다.

[client] SYN_SENT

[server] SYN_RCVD

[client] ESTABLISHED

[server] ESTABLISHED

[client] FIN_WAIT_1

[server] CLOSE_WAIT

만약 server가 먼저 연결된 소켓을 닫았다면?

위에서 설명할 때 이해를 돕기위해 client와 server로 표현하였지만, 실제로 소켓은 구분이 되지 않는다. server라는 소켓이 따로 있는 것 도 아니고, client 소켓이 정해진 것도 아니다. 가장 정확한 표현은 active, passive인데, 요청을 보내는 소켓을 active로, 요청을 받아서 응답을 보내주는 소켓을 passive로 정의한다.
위에 있던 사진을 다시 가져와보자. SYN_SENT옆에 active open이라고 적혀있는 것을 볼 수 있다. LISTEN옆에는 passive open이 적혀있다.
즉 통신이 필요할 때마다 소켓을 여는 쪽이 active가 되는 것이다. 대부분 이러한 행동을 client에서 담당하기 때문에 통념적으로 client라는 표현을 사용한다.
그렇다면 위의 질문을 다시 정확하게 표현해보자.
active open측에서 close요청을 보내기 전에 passive측에서 먼저 소켓을 close해버리면 어떻게 될까?
TCP의 3-Way-Handshake 과정에서는 통신을 종료할 때, 요청을 보내는 client(active) 쪽에서 먼저 close요청을 보내야한다. 만약 server(passive)측에서 먼저 연결 중인 소켓을 close해버리면 어떻게 될까?

passive socket의 TIME_WAIT

여기서 부터는 아래 블로그의 내용을 참고해 정리하였다.
몇가지 개념을 다시 상기해보자.
1.
소켓은 client와 server로 나누는 flag가 없다. 즉 위처럼 나눈 구분은 추상적인 것이다.
2.
passive 측에서는 TIME_WAIT이 남지 않는다. TIME_WAIT은 3WHS의 마지막 단계이다. 즉, 소켓이TIME_WAIT상태가 된다면 연결되었던 소켓들이 다른 어떠한 상태를 가지거나 다른 상태로 진행되면 안된다.
TIME_WAIT 이란 TCP 상태의 가장 마지막 단계이다. 먼저 close() 를 요청한 곳에서 최종적으로 남게 되며, 2*MSL(Maximum Segment Lifetime)동안 유지된다. 이처럼 TIME_WAIT은 상당히 긴 시간동안 유지되는데 왜 이렇게 설정되어 있는 것일까?

첫번째 문제는 지연 패킷이 발생할 경우이다.

만약 이전의 연결이 모두 종료되고 새로운 연결을 진행중일 때, 이전 연결 상태에서의 패킷이 들어오면 큰 문제가 발생한다. 물론 SEQ가 다를 가능성이 크지만, 만에하나 SEQ까지 같아버리면 시스템은 해당 패킷을 처리하게되고 TCP의 신뢰성이 깨지게 된다. 물론 이러한 상황은 극히 드물지만, 발생할 가능성이 전혀 없는 것은 아니다.

두번째 문제는 원격 종단의 연결이 닫혔는지 확인해야 할 경우이다.

마지막 ACK 유실시 상대방은 LAST_ACK 상태에 빠지게 되고 새로운 SYN 패킷 전달시 RST를 리턴한다. 새로운 연결은 오류를 내며 실패한다. 이미 연결을 시도한 상태이기 때문에 상대방에게 접속 오류 메시지가 출력될 것이다.
따라서 반드시 TIME_WAIT이 일정 시간 남아 있어서 패킷의 오동작을 막아야 한다.