Network

[1% 네트워크 원리] 2장 TCP

shininghyunho 2023. 12. 20. 20:20

프로토콜 스택 내부 구성

1장에서 브라우저가 데이터를 전송할때 OS의 프로토콜 스택에 요청하는것까지 살펴봄.

데이터 전송은 OS의 프로토콜 스택을 통해 LAN 어댑터로 전송됨.

프로토콜 스택 : OS에 내장된 네트워크 제어용 소프트웨어
LAN 어댑터 : 네트워크용 하드웨어

 

프로토콜 스택의 구체적인 구성은 다음과 같다.

 

  • 애플리케이션 계층에서 여러가지 애플리케이션이 데이터를 전송할때
    Socket 라이브러리를 사용함. (리졸버는 DNS 서버로부터 도메인으로 IP를 받아오는 역할을 함.)
  • Socket 라이브러리는 OS의 프로토콜 스택을 호출함.
  • 프로토콜 스택에서는 TCP(일반적인 애플리케이션은 TCP 사용),
    UDP(DNS 서버 조회등 짧은 제어용 데이터 송수신에 사용) 프로토콜을 사용함.
  • 이를 IP 프로토콜이 다시 전달받음. IP 프로토콜은 데이터 운반 단위인 패킷의 송수신을 담당함.
  • IP에는 ICMP(패킷을 운반할때 발생하는 오류 통지 및 제어 역할), ARP(IP 주소로부터 MAC 주소를 반환)이 포함된다.
  • 최종적으로 LAN 어댑터를 사용해하는데 이를 위한 소프트웨어로 LAN 드라이버에게 데이터를 전달함.

소켓의 정체

Socket 라이브러리를 이용해서 우리는 socket을 만들었다.

그렇다면 socket이란 무엇일까?

 

소켓은 네트워크 통신에 필요한 제어 정보 집합

 

  • 통신을 하기위해서는 여러가지 정보들이 필요하다.
  • 예를들어 내 IP, 내 포트번호, 상대 IP, 상대 포트 번호, 통신 진행 상태등이다.
  • 또한 데이터를 송신하고 응답이 돌아오지 않으면 데이터를 재전송해야한다.
    이때 재전송을 위한 송신한 시각이 적혀져 있어야한다.
  • 이렇게 1번의 통신을 위한 여러가지 정보들이 프로토콜 스택에 저장되어 있어야한다.
    이것을 소켓이라고한다.
  • 반대로 말하면 OS의 프로토콜 스택은 소켓을 계속해서 참조하며 통신을 진행한다.

윈도우에서 확인해보는 소켓

터미널에서 netstat -ano 을 검색하면 통신중인 소켓들을 볼 수 있다.

 

다음은 내 PC에서 직접 확인해본것이다.

 

  • 저기서 한 열에 해당하는것이 소켓이다.
  • 예를들어 10.10.10.144:49509 는 10.10.10.144 라는 IP가 49509라는 포트번호로 데이터를 보내는데,
    보낼곳은 121.53.105.234:443 이고 통신상태는 ESTABLISHED(통신중)이고,
    PID 6880 인 프로세스에서 진행중이란 말이다.
  • 0.0.0.0은 아직 통신이 진행중이지 않고 대기중이라는 의미다.

Socket 라이브러리 동작 (with 소켓)

1장에서도 본 그림인데 프로토콜 스택 내용이 추가되었다.

 

  • 1번 socket() 이 호출되면 프로토콜 스택은 socket을 만들기위해 메모리 영역을 할당해놓는다.
    (초기화 단계)
  • 1번 호출이 끝나면 하나의 디스크립터를 받게되는데,
    이는 하나의 소켓을 구분하기위한 구분자 역할을 한다.
  • 이후 데이터 통신이 진행되면 어플리케이션에게 통지없이 프로토콜 스택이
    소켓에 데이터를 저장하며 진행한다.

 

접속

처음 클라이언트에서 소켓을 만들면 아무것도 없이 비어있는 상태다.

그렇다면 아무것도 없는 상태에서 어떻게 상대와 통신할 수 있을까?

알아야 할 내용은 어떻게 클라이언트와 서버가 각자의 제어정보를 완성하는가이다.

 

구체적으로 이 제어정보는 1)소켓의 내용, 2)헤더의 내용이다. 통신에 필요한 정보라고 볼수있다.

 

소켓의 내용은 우리가 앞서 살펴본 netstat 검색시 나오는 정보들이었다. (프로토콜, 내IP, 상대IP 등등)

 

헤더

그렇다면 헤더의 내용을 살펴봐야한다.

데이터는 패킷이라는 단위로 전송이 되는데 이 패킷의 메타데이터가 헤더이다.

 

다음 그림은 TCP 헤더에 관한 내용이다. (IP도 헤더 정보가 존재한다.)

 

 

다음은 헤더의 내용이다.

  • 송신 포트 번호 : 송신(클라이언트) 포트 번호.
  • 수신 포트 번호 : 수신(서버) 포트 번호.
  • 시퀀스 번호 : 해당 패킷이 송신 데이터의 몇번째 바이트인지를 나타냄.
  • ACK 번호 : 데이터가 잘 도착했는지를 나타내는 번호. SYN+1 이 적혀짐.
  • 각종 컨트롤 비트들 : SYN(접속을 알리는 비트), ACK(SYN이 잘도착했다는 비트), FIN(연결 끊김을 나타내는 비트), 등등
  • 윈도우 크기 : 수신측에서 송신측으로 한번에 묶음으로 보낼 데이터 크기(윈도우)를 나타냄.
  • 체크섬 : 오류 유무.
  • 등등...

 

3-way handshake

TCP는 이러한 헤더를 가지고 어떻게 구체적으로 통신할까?

 

통신의 시작을 알리는 접속은 다음과 같은 순서로 이루어진다.

맨처음 접속이 이루어지는 통신이 총 3번 이루어져야하는데,
마치 3번 악수를 나눈다고하여 이를 3-way handshake라고 한다.

1번 전송

  • 먼저 클라이언트가 패킷을 보낸다. 
  • 헤더의 SYN 비트를 1로 나타내고 시퀀스 번호를 설정하여 보낸다.
    (여기서 최초 시퀀스 번호는 Initial sequence number,ISP라고도 한다.)
    (또한 SYN 비트는 말그대로 SYN임을 나태는 비트고 SYN 자체는 1번 패킷을 의미한다.)
  • 그러면 클라이언트의 상태는 closed(또는 listen) -> SYN_SENT 변경된다. 
  • SYN 전송

1번 수신, 2번 전송

  • 서버에서 클라이언트의 SYN 패킷을 받고 답장을 준비한다.
  • 서버는 클라이언트와 마찬가지로 SYN 비트시퀀스 번호를 설정한다.
  • 추가적으로 클라가 보낸 SYN을 받았는지 확인시켜줘야하므로,
    ACK 비트를 1로 설정하고 ACK 번호를 클라가 보낸 시퀀스번호+1로 설정해서 보낸다.
    (그래야 클라이언트는 본인이 보낸 시퀀스 번호가 잘 도착했는지 알 수 있다.)
  • 서버는 답장을 잘받고 응답을 대기한다는 의미로 상태가 SYN_RCVD가 된다.
  • SYN+ACK 전송

2번 수신, 3번 전송

  • 마지막으로 클라도 서버의 SYN를 확인시켜줘야한다.
  • 클라는 서버의 시쿼스번호+1을 ACK로 설정하고,
    ACK 비트를 1로 설정해서 패킷을 보낸다.
  • 이제 클라는 자기의 SYN도 잘갔음을 확인했고, 서버의 SYN도 확인했으니
    상태를 ESTAB(전송 준비 완료)로 변경한다.
  • ACK 전송

3번 수신

  • 서버도 클라에게서 본인이 보낸 시퀀스 번호+1로 ACK와
    ACK 비트가 1임을 확인한다.
  • 그럼 서버도 본인의 SYN가 잘 전송되었음을 확인했으니,
    상태를 ESTAB으로 변경한다.
  • 이제 각자 소켓을 완성할 수 있고 데이터 전송이 가능해졌다.

이렇게 접속 단계가 끝이 난다.

 

추가적으로 봐볼것.

3 way가 아닌 2 way로 하면 어떤일이 일어날까?

3-Way Handshake — 다락방 (tistory.com) 참고

 

송신 ,수신

이제 클라이언트와 서버의 연결이 성사되었다.

 

본격적으로 데이터를 주고받을 차례다.

소켓 라이브러의 write() 명령어를 호출하면 드디어 데이터를 전달할수있게된다.

버퍼 메모리

실제 데이터는 프로토콜 스택이 전송한다.

그렇다면 애플리케이션의 요청 1개가 들어올때마다 이를 패킷으로 만들어 전달해야할까?

만약에 요청이 너무 작다면 패킷을 여러개 만들어 전달해야할까?

(패킷을 너무 많이 생성하면 네트워크 부하가 심할텐데...)

 

다행히도 프로토콜 스택에는 버퍼 메모리가 존재한다.

  • 애플리케이션의 송신 요청이 들어오면 송신 버퍼 메모리에 차곡차곡 쌓아둔다.
  • 그리고 특정 단위로 패킷을 만들어 전송한다.

그 특정 단위는 다음과 같다.
프로토콜 스택은 MTU(패킷 최대 크기), MSS(실제 데이터 최대 크기)를 참고해 패킷을 만든다.

 

 

그럼 데이터를 무조건 패킷 최대 크기만큼 채워서 보내는게 최선일까?

만약 요청 크기가 작고 빠르게 요청을 보내야 한다면?

 

이렇게 너무 오래 패킷이 보내지지 않는것을 방지하기위해,

프로토콜 스택에는 타이머가 존재한다.

그래서 일정시간이 지나면 강제로 패킷을 만들어 전송한다.

 

데이터의 양과 시간

결국 패킷을 만들때는 데이터의 양시간이라는 두 요소를 고려해야한다.

마치 택배를 운반할때 운반량과 운송 시간의 개념과 비슷하다.

 

많은 양을 전달하면 시간이 오래걸리고

빠른 시간안에 전달하면 양이 적어진다.

 

그래서 이와 같은 수치는 OS 마다 설정값이 다르다.

 

하지만 대화형 애플리케이션처럼 응답속도가 중요할때는

프로토콜 스택에 송신을 요청할 송신 타이밍을 설정할 수 있는 옵션도 존재한다.

 

수신버퍼 메모리

위에 내용은 송신 버퍼에 관한 내용이었다.

그렇다면 수신용 버퍼 메모리도 필요할까?

 

  • 패킷은 전달할때 네트워크 환경에따라 패킷이 순서대로 도착한다는 보장이 없다.
  • 심지어 특정 패킷은 유실되어(ACK 보내지 않음) 다시 받아야한다.
  • 그래서 수신 버퍼에서 받은 패킷을 다시 순서대로 조립해서 애플리케이션에 보내야한다.
  • 프로토콜 스택이 데이터를 프로세스 메모리 영역에 전달하는 과정은 일종의 인터럽트이므로 과도하게 실행하면 안된다.

 

타임아웃

송신측에서 패킷을 보내면 수신측도 ACK를 보내야한다.

만약 ACK가 돌아오지 않는다면 송신측은 다시 패킷을 보낸다.

 

그렇다면 다시 돌아오지 않는다는 기준은 어떻게 정할까?

  • 송신측에서 패킷을 보낼때 보낸 시간을 기록한다.
  • 그리고 ACK가 임계시간(타임아웃 값)안에 도착하지 않으면 타임아웃이 발생한다.

타임아웃 값은 너무 짧지도 길지도 않아야한다.

  • 타임아웃이 짧을 때 : 수신측에서 제대로 ACK를 보내기도전에 타임아웃이 발생한다.
    이로인해 중복된 패킷이 계속 보내지고,
    수신측에선 더욱더 많은 패킷을 처리하느라 다시 보내는 시간이 오래걸려 속도가 계속해서 느려진다.
  • 타임아웃이 길 때 : 패킷이 유실되어 다시 송신해야하는 경우에 긴 타임아웃 시간까지 오래 대기해야한다.

그렇다고 타임아웃 값을 고정하는것도 비효율적이다.

환경에 따라 응답시간이 다를 수 있기 때문이다. (서버의 거리에 따라, LAN 환경, 인터넷 환경 등)

 

그래서 TCP는 ACK가 빨리 도착하면 타임아웃 값을 늘리고, ACK가 늦게 도착하면 값을 줄이는 방식으로,
유동적인 타임아웃값을 갖는다.

 

윈도우 제어

접속 단계에서 패킷을 하나 보내면 이에 해당하는 ACK를 받고 다시 패킷을 보냈었다.

접속 단계가 끝나고 전송 단계에서는 보낼 패킷이 많을텐데 이렇게 하나씩 주고받아도 될까?

 

그래서 나온 개념이 윈도우다. 한번에 여러개의 패킷을 동시다발로 보내는것이다.

(나는 윈도우(창) 크기만큼 한꺼번에 보내는걸로 이해함)

 

 

하나씩 보내는 핑퐁 방식에 비해 윈도우 제어 방식은 ACK를 기다리느라 시간을 낭비하지 않는다.

 

윈도우 사이즈

그렇다면 패킷을 무작정 한꺼번에 많이 보내도 괜찮을까?

 

  • 패킷이 한번에 너무 많이 도착해 수신 버퍼의 크기보다 커진다면,
    overwrite가 발생해 데이터가 누락될것이다.
  • 그래서 TCP 헤더에는 수신 가능한 메모리 크기인 윈도우 사이즈가 존재한다.
    (윈도우 사이즈는 윈도우 필드값 중 하나.)

구체적인 시나리오는 다음과 같다.

  • 먼저 수신측에서 받을 수 있는 메모리 크기(윈도우 크기)를 송신측에 알린다.
  • 그러면 송신측은 이를 윈도우 크기값으로 설정해두고,
    조금씩 보낼때마다 윈도우 값을 수정한다.
  • 송신측의 윈도우 크기값이 0이 되면 송신을 멈춘다.
  • 반대로 수신측도 애플리케이션 영역에 메모리를 올린후,
    빈 공간만큼 다시 송신측에 '다시 이만큼 사용가능합니다.' 를 보낸다.
  • 그러면 송신측은 윈도우 크기값을 다시 설정해 데이터를 보낸다.

윈도우값과 ACK

윈도우 방식으로 패킷 송신을 묶어서 보낼 수 있게됐다.

 

원래대로라면 패킷을 하나 받을때마다 ACK로 응답을 보내야한다.(오류 검출)

하지만 패킷이 윈도우 방식으로 여러개가 도착한다면 ACK도 여러개 반환해야한다.

 

굳이 ACK를 다 보내지 말자. 마지막 번호만 보내자.

만약에 ACK를 10, 20, 30, ..., 100 보내야할때 마지막 ACK 번호(100번)만 보내도
송신측은 모든 패킷이 정상 도착했음을 알 수 있다.

 

윈도우값도 같이 보내자.

ACK 뿐만 윈도우 값도 주기적으로 보내줘야한다.

그래야 송신측에서 윈도우 크기만큼 패킷을 전송하기 때문이다.

 

윈도우값은 수신 버퍼를 비울때마다 보낼수있다.

하지만 ACK와 마찬가지로 어느정도 버퍼 크기가 여유있을때 윈도우값을 보내는것이다.

 

이때 패킷 1개에 윈도우값과 ACK를 같이 보내면 네트워크 부하가 확실히 줄어든다.

 

해체

이제 데이터를 모두 주고 받았으면 연결을 끊을 단계다.

 

연결을 끊는건 간단하다. 단순히 FIN 비트를 1로 설정하고 ACK를 받을뿐이다.

단 연결을 끊는 주체는 서버, 클라이언트 둘다 될수있다.

 

서버가 FIN 을 보내면 클라이언트에서는 어떤일이 일어날까?

  • 클라이언트는 소켓에 서버가 연결 끊기에 들어갔음을 표시한다.
  • FIN에 대한 ACK를 서버로 송신한다.
  • 애플리케이션에서 read() 를 호출할때 프로토콜 스택은 서버로부터 데이터 수신이 모두 완료됐음을 알린다.
  • 이후 애플리케이션은 종료 절차에 들어가 close() 를 호출한다.
  • 그러면 프로토콜 스택은 다시 FIN 비트를 1로 설정해 서버에 보낸다.
  • ACK가 무사히 돌아오면 완전히 끝나게된다.

소켓 말소는 천천히

마지막 해체 단계까지 끝나면 소켓을 메모리에서 완전히 지워도 된다.

단 소켓의 내용을 바로 지우지 않고 몇분정도 유지한다.

 

그 이유는 FIN을 받자마자 소켓 내용을 바로 지우면

패킷이 유실될 경우 다시 FIN을 보낼수가 없게된다.

 

  • 무슨 말이냐면 클라이언트가 FIN을 받고 소켓의 내용을 다 지웠다.
  • 이후 ACK를 보냈는데, 서버에서 받질 못했다.
  • 하지만 클라이언트는 소켓 내용이 없기때문에 다시 FIN을 보낼수가없다.
  • 서버는 계속 접속이 연결된 상태로 유지된다.

서버가 FIN을 보냈는데 이미 클라이언트 소켓이 소멸된 상태라면 어떨까?

  • 클라이언트는 메모리를 모두 반납했다.
  • 포트 자리가 비어서 다른 프로세스가 포트를 할당받는다.
  • 이러한 상황을 모르는 서버는 FIN을 보낸다.
  • 그러면 새롭게 할당받은 프로세스가 FIN을 받고 연결이 끊나게된다.

이러한 이유들로 인해 소켓 말소는 바로 진행되지 않고 몇분정도 유예기간을 둔다.

 

전체 동작

여태까지 서술했던 접속,송수신,해체 동작을 하나의 그림으로 표현하면 다음과 같다.

'Network' 카테고리의 다른 글

[1% 네트워크 원리] 2장 IP와 이더넷, UDP  (0) 2024.05.10
[1% 네트워크 원리] 1장 웹 브라우저  (0) 2023.12.11