L4에 대한 정리

2021. 10. 31. 22:51게임서버/TCP IP 이론

320x100

안녕하세요 대학생 개발자입니다.

이번엔... 음... L4입니다.

어쩌다 보니 그냥 순서대로 쭉 올라가는 중이네요...

  

이때까지 했던걸 아주 간략하게 적어보겠습니다.

L2는 직접 연결된 두 개의 기기간 통신을 정의해놓은 레이어였습니다.

 

L3는 간접적으로 연결되어있는 두 개의 기기간 통신을 정의해놓은 레이어였습니다.

간접적이라는 건 다른 노드들을 통해서 갈 수 있는 길이 만들어져 있다는 의미구요.

그렇기 때문에 L3부터는 이 데이터를 어디로 보내야 되는지에 대한 내용이 결정되는 구간입니다.

일단... 이번 글에서 제가 정리를 해보려고 하는 건 L4 레이어 중 TCP, UDP 정도입니다.

사실 TCP IP레이어라고 하면 거의 TCP와 UDP가 유일하죠...

 

먼저 L3까지 통과한 데이터는 이제 상대 호스트 내부로 들어가게 됩니다.

그럼 이제 기나긴 여정이 끝나는 게 아닌가? 싶을 수도 있는데요.

하지만 아직 남아있습니다...

과거의 컴퓨터는 멀티태스킹이 되지 않았었지만 현재라고 하긴 좀 그렇고 좀 오래전 이긴 하지만...

째뜬 요즘(약 25년 이상 전부터...) 컴퓨터는 무려 컴퓨터는 멀티태스킹을 지원하기 때문이죠...

이런 환경에서는 여러 가지 프로세스가 동시에 작동하고 각 프로세스를 구분할 필요가 생겼습니다.

그래서 어떤 프로세스에게 전달해야 되는 데이터인지를 구분 짓기 위해 PORT라는 2바이트 주소를 사용합니다.

c에서 데이터 타입으로 보면 unsigned short가 되겠죠

 

L4에서의 하나의 연결의 단위는 SOCKET이라고 불리는 핸들 값입니다.

즉 하나의 연결 = 하나의 소켓이 되는 겁니다.

그럼 이제 L4레이어에서 크게 두 가지로 나뉘어지는 TCP와 UDP에 대한 얘기를 해보도록 하겠습니다.

TCP는 1:1 연결을 원칙으로 하고 있으며 안정성 있는(Reliable) 통신을 추구합니다.

우선 그렇게 될 수 있는 배경에는 SequenceNumber와 Acknowledgement라는 것이 있습니다.

이때까지 IP와 frame에는 내가 받았는지 안 받았는지에 대한 피드백이 존재하지 않았습니다.

그렇기 때문에 만약 내가 보낸 게 중간에서 사라지더라도 다시 보낼 수 있는 방법이 존재하지 않았죠.

 

TCP에서는 Reliable 한 통신을 위해서 어크를 사용해서 내가 받았으면 어디까지 받았는지 답변해주는 방식을 통해 대답을 해주고 만약 그 세그먼트가 돌아오지 않는다면 나의 호스트는 다시 한번 똑같은 데이터를 보내게 됩니다.(물론 계속해서 보내진 않습니다. 정확히 몇 회인지는 모르겠지만 n회정도(큰 값은 아닙니다))

와이어 샤크에서 측정했을땐 약 5회정도 보내는 것으로 확인됬는데요....

뭐 이건 커널마다 다를 수 있는 부분이라고 생각합니다.

재전송 후 답변이 오지 않으면 종료된 것으로 간주하고 소켓을 닫아버립니다.(이것에 대한 의미도 나중에 따로 정리하겠습니다)

어디까지 라는 정보는 TCP 헤더의 Sequence number를 통해 전달하게 됩니다.

이 시퀀스 넘버 또한 0부터 시작하게 되면 중간에서 ip port sequence 넘버를 맞춰서 보내게 되면 중간에 가로채기 당할 수 있으므로 임의의 값을 시작점으로 한다고 합니다.

그럼 TCP의 연결 과정에 대해서 보겠습니다.

먼저 TCP에서는 연결을 할 때 세 번의 통신을 주고받습니다.

Three way handshake

연결 과정을 보고 3way hand shake라고 부릅니다.

먼저 연결을 요청하는 측(클라이언트)에서는 SYN bit을 1로 세팅한 뒤 함께 자신의 Sequence number를 전송합니다.

그러면 연결을 요청받는 측(서버)에서는 받았던 Sequence number+1 돌려보내는 Ack에 기록해 자신의 Sequence Number와 같이 보내게 됩니다.

이때 TCP header의 TCP flag에 SYN bit와 ACK bit를 1로 세팅 후 보내는 것입니다.

이런 연결이 되고 나면 이제 SOCKET이라는 연결 단위를 통해서 그저 send를 호출하면 데이터가 따라 흐르는데요.

이 부분을 보고 stream이라고 부릅니다.

우린 그냥 소켓에다가 대고 send만 하면 알아서 데이터가 전송되게 보장을 해준다는 의미입니다.

그리고 이 보장은 물리적인 연결이 성사되었다는 아닙니다.

실제 연결이라는 건 과연 그럼 어떤 식으로 정의가 되는지 말씀드리자면 "내 메모리에 저장해두는 것"이 끝입니다.

그저 TCP에서 연결이라는 개념은 아이피와 포트를 소켓이라는 곳 안에 저장해두는 것 그게 다입니다.

연결이라고 해서 뭔가 특별한 게 있을 줄 알았었는데 정말 이게 끝이에요!

 

그냥 단지 소켓이 관리하고 있는 메모리 어딘가에 아이피와 포트 기입하는 것이 그저 연결의 과정이라니 허무하죠...

그럴 수밖에 없는 게 경로라는 것이 확정될 수가 없기 때문입니다...

그리고 연결의 주체는 L4이고 L4이하의 영역에서는 연결의 개념이 존재하지 않기 때문입니다.

매번 바뀌는 게 경로이기도 할뿐더러 그 많은 경로를 기억할 순 없기 때문입니다...

그리고 이미 라우팅 알고리즘이라는 효율적인 알고리즘을 통해서 이미 경로 배정을 하고 있는데 굳이 정해진 경로로만 가는 건 효율적이지 못할 것 같다는 생각도 들고요.(뒷부분은 뇌피셜입니다)

반대로 연결 종료는 그 메모리에서 지워버리는 게 끝입니다.

사실 "stream이 형성되었다" 라는 것은 좀 굉장한 무언가가 있을 것 같습니다.

근데 실상은 그저 메모리에 내 포트 하나에 상대방의 아이피와 포트가 물려있다는 것을 적어놓는 게 끝이라는 것...

있을법한 뭔가가 있을줄 알았습니다만 쪼금 허무하긴 합니다.

 

 

그럼 시작 과정을 봤으니 반대로 종료 과정도 봐야겠죠

네 반대로 종료하는 과정은 4 way handshake로 4번의 데이터 전송이 있습니다.

제가 예전에 궁금했던 게 왜 ACK와 FIN을 한꺼번에 보낼 수 없는 것일까라고 고민을 했었는데요...

지금은 당연하게도 ACK를 보내면서 연결 종료를 준비하고 준비가 완료되면 FIN을 보내는구나 하고 생각하고 있습니다.

생각해보면 바로 답이 나오는 건데도 그때는 왜 그렇게 그게 궁금했었는지 모르겠습니다.

송신 측에서 TIME_WAIT을 거는 이유도 당연하게도 ACK를 보냈는데 유실이 되었다면 수신 측에서 다시 FIN을 보낼 거고 그 FIN을 한 번 더 받게 된다면 상대가 ACK를 못 받았었다는 의미가 되므로 그 사이의 시간을 기다리는 것이라고 생각하면 될 것 같습니다.

 

그럼 연결과 종료의 과정을 간단하게 체크를 했으니 ACK번호와 SEQ번호에 대한 얘기를 좀 해볼까 합니다.

ACK번호는 SEQ번호와 동기화가 되어야 하므로 모든 데이터 전송에는 SEQ번호가 들어가게 됩니다.

심지어 ACK를 보낼 때도 SEQ번호가 같이 보내지게 됩니다.

근데 ACK번호는 받은 메시지의 사이즈만큼 증가하게 되는데 만약 헤더만 보내는 케이스(SYN, FIN, RST, ACK)와 같은 이런 시그널만 담은 헤더가 도착하게 된다면 어떻게 될까요..?

이런 경우에는 데이터가 사이즈가 0이라서 증가를 안 시킬 것 같지만 이전 시퀀스와 동일한 번호가 들어오게 되면 이전 정보를 받았다는 것인지 혹은 이번에 보낸 헤더를 받았다는 의미인지 알 수 없기 때문에 1을 증가시켜서 ACK를 돌려보내게 된다고 합니다.

 

TCP는 신기하게 내부에서 타이머를 재고 있답니다.

시간을 재고 있다가 ACK가 와야 될 시간이 지났는데 돌아오지 않는다면 유실이 되었구나 하고 생각하고는 다시 보냅니다.

이 시간은 제가 예전에 수업 때 배운 바로는 RTT의 평균을 이용해서 구한다고 들었어요.

TCP라는 친구는 꾸준히 데이터를 주고받는데요.

KEEP_ALIVE를 체크하는 거죠.

그 KEEP_ALIVE를 체크하면서 주고받은 패킷이 왔다 갔다 하는 RTT를 통해서 그 타이머를 계속 맞춰준다고 합니다.

 

추가로 TCP는 소심한 성격입니다.

TCP는 데이터가 들어오면 왁하고 쏟아 붙지 않습니다.

살짝살짝 발을 담가보는 형태로 간을 보다가 아 갈 수 있겠다 싶으면 전송하는데 아까 KEEP_ALIVE 및 모든 헤더를 전송할 때 Window Size라는 것을 전송한답니다.

여기서 Window Size란 것은 TCP 수신 버퍼의 크기를 의미하는 것이고 이 버퍼의 크기를 기반으로 상대방이 몇 바이트를 더 보내도 되는지 아니면 지금은 보내면 안 되는지를 결정한다고 합니다.

이름이 Window인 이유는 창문이 열리는 것과 같이 동작한다고 해서인데요 ACK에 대한 포인터와 SEQ에 대한 포인터 두 개를 두고

Window Sliding

ACK가 도착한 데이터와 보냈지만 아직 도착하지 않은 데이터를 구분하고 있기 때문에 이 모습이 마치 창문이 닫히고 열리는 모습을 연상한다고 해서 그렇게 불리게 됐답니다.

그리고 추가로 송신 버퍼의 데이터는 상대의 ACK가 도착하기 전까지는 지워지지 않습니다.

윈도 사이즈가 지금 헤더엔 2바이트로 되어있는데 사실 65535byte는 최근의 데이터 속도나 크기로 봤을 땐 많이 모자라요 그래서 옵션에 윈도 스케일이라는 것을 붙여서 MB단위로까지 만들어 버린다고 하네요.

이 부분은 IP계층에서 옵션으로 붙는 것과 비슷한 경우라고 볼 수 있을 것 같아요.

추가

{

윈도우 스케일이라는 것은 처음에 보내는 헤더에만 붙고 그다음부터는 알아서 몇 승 해서 받아라 라는 정도로 받아 들이시면 됩니다.

SYN을 보낼 때 그 헤더의 옵션으로 붙습니다.

}

 

사실 API에 윈도 사이즈라는 것을 설정하는 방법이 있긴한데 0으로도 세팅이 가능합니다.

근데 0으로 세팅하면 우리가 API를 통해 확인하면 0이라고 뜨는데 희한하게 데이터는 또 주고받아집니다.

 

그 이유가 API가 구라를 치고 있기 때문인데요.

 

그래서 정확한 윈도 사이즈를 확인하려고 한다면 패킷을 캡처해서 헤더를 직접 보는 방법이 제일 정확하고 API는... 음 정확하지 않다는 말씀을 또 드리고 싶네요

그리고 아까 종료에 대한 얘기를 살짝 했었는데 종료에는 2가지 방법이 있어요 RST라는 reset signal이 있고 fin이라는 나름 깔끔하고 신사적인 방법이 있어요.

msdn에서는 graceful closing이라고도 소개한답니다.

우리의 TCP는 KEEP_ALIVE 보내거나 다른 데이터를 보낼 때 여러 번 ACK가 돌아오지 않으면 그 클라이언트가 종료한 것으로 간주하고 연결을 끊어버려요.

즉 RST를 보내고 그 소켓을 폐기해 버린답니다.

 

사실 종료는 저 정도만 알고 있어도 될 것 같아요.

이전에 설명할 때도 간략하게 정리했고 이번에도 간략하게 정리했으니 대략적인 종료에 관한 내용은 다 정리가 되었다고 생각합니다.

그럼 TCP 송신 버퍼에 데이터를 넣게 되면 어떤 식으로 전달이 될까에 대해서 정리를 해보려고 하는데요.

그전에 우리가 send라는 API를 호출하게 된다면 과연 이건 어디까지의 전송을 의미하는 거일지에 대한 부분부터 체크를 해봐야 할 것 같습니다.

우리는 Application layer를 다루는 입장입니다.

즉 L7영역이죠 거기서 send라는 API를 호출하게 된다면,

우리는 그저 TCP의 송신 버퍼에 데이터를 집어넣는 게 끝입니다.

그 뒤로는 커널이 알아서 처리할 부분 입니다.

 

반대로 recv 또한 마찬가지입니다.

커널에서 처리가 다되어서 frame, ip, tcp 헤더를 다 떼어내고 나서 남은 메세지만 TCP 수신 버퍼에 덩그러니 남겨두고 recv를 호출하면 거기 있는 데이터를 복사(pumping)하는 과정으로 데이터를 받아온답니다.

즉.. 우리가 호출하는 API는 실제 상대에게 전달이 되었는지 안되었는지는 보장해주지 않아요.

그저 TCP에서 보장을 하고 있으니 우리가 안전하게 사용하는 것뿐이랍니다.

그럼 여기서 send(1byte) send (10byte) send(7byte)를 하게 된다면(실제 이렇게 API가 되어있지는 않습니다) 어떤 식으로 데이터가 날아가게 될까요.

뭐... 일반적인 사람들이라면 1바이트 송신 10바이트 송신 7바이트 송신이라고 생각하겠지만 TCP의 디폴트 세팅은 그게 아닙니다.

 

바로 네이글 알고리즘이라는 것 때문인데요.

네이글 알고리즘은 TCP송신 버퍼가 가득 차거나 혹은 상대에게 ACK가 돌아왔을 시 가지고 있던 데이터들을 보내는 그런 역할을 하는 알고리즘입니다.

그렇기 때문에 send(1) send(10), send(7)을 하게 되면 1을 보내고 그 ACK가 오기 전에 남은 17바이트가 다 송신 버퍼에 들어와 있다면 17바이트를 보내겠지만 10바이트만 들어와 있다면 10바이트만 보내고 땡일 수도 있는 것입니다.

반대로 ACK가 오지 않은 상황에서 송신 버퍼에 가득 차도 보내게 되는 경우가 있는데요.

또 가득이란 어느 정도를 의미하는가에 대한 고민을 해보지 않을 수가 없습니다.

 

이것에 대한 힌트는 제가 L3를 얘기하면서 조금 드렸습니다.

IP는 MTU(일반적으로 1500)라는 크기를 넘어서게 된다면 fragmentation을 하게 됩니다.

즉 분할을 해서 데이터가 보내지게 되고 그 분할된 데이터를 다시 합치는 과정을 상대 호스트에서 거치게 됩니다.

하지만 분할이 많이 일어나면 일어날수록 패킷 유실의 가능성이 더더욱 높아지는 상황에서 TCP는 이런 부분도 세심하게 캐치해줍니다.

 

IP의 헤더는 일반적으로 20바이트입니다.

TCP의 헤더도 일반적으로 20바이트죠.

그래서 헤더의 크기를 제외한 1460바이트 (고정값은 아님 헤더의 크기에 따라 변경됨)이 값이 MSS입니다.

max segment size의 약자죠 그래서 TCP에서는 친절하게 fragmentation이 일어나지 않게 하기 위해서 스스로 1460바이트로 데이터를 분할시켜서 보내기 때문에 패킷 하나가 유실이 된다고 해도 송신 측 입장에서는 부담이 덜 합니다.

잃어버린 패킷(세그먼트) 하나만 다시 보내게 되면 되니까요 정말 효율적이지 않나요??

윗선에서 미리다 처리해서 보내줌으로써 데이터의 유실 확률을 획기적으로 줄여버리니까 말이에요.

그렇기 때문에 우리가 TCP를 쓸 때는 mss를 걱정 안 하고 따로 우리가 fragmentation을 해주지 않아도 된다고 합니다.

 

반면에 UDP의 경우에는 우리가 그걸 고민을 해야 되는데요.

TCP는 바이트 스트림 형식으로 데이터의 경계가 뚜렷하지 않고 하나의 데이터가 두 번에 걸쳐서 잘려갈 수도 있는 반면에 UDP의 경우에는 무조건 한 번에 쏴버리는 형태로 가게 됩니다.

즉 한건에 L7의 데이터 하나 라는겁니다.

물론 IP에서 MTU를 넘으면 fragmentation이 일어나겠지만 TCP처럼 안정적인 보장을 해주지 않기 때문에 UDP의 경우에는 유실 확률이 높다고 하는 이유겠죠.

그러므로 UDP를 쓸 때는 우리가 mss를 고려해서 mss보다 작게 보내는 형식으로 하는 게 좋다고 합니다.

추가로 UDP의 경우에는 받을 때도 우리가 제공하는 버퍼보다 더 많은 사이즈를 받으려고 한다면 나머지는 버려지게 되므로 이 부분을 주의해야 됩니다.

그래서 버퍼도 넉넉히 잡아두셔야 되고요 TCP의 경우에는 내가 만든 버퍼 이상의 데이터가 오면 그 이상은 그대로 둔 상태로 내가 할당한 버퍼 크기만큼만 받아온답니다^_^...

만약 그럼 제가 send(500), send(700), send(200) (숫자는 다 바이트입니다.)를 하게 됐을 때 상대가 recv를 했을 때 한 번에 뭉쳐져서 받는 케이스를 정리해보면

1. 네이글 알고리즘에 의해서 송신 측에서 모았다가 보내게 된다.

2. 상대의 Window size가 0이었다가 풀렸을 때 보낸 것이다.

3. 상대의 recv가 늦어서 다 보내고 나서 받았기 때문에.

반대로 우리가 데이터를 받았을 때 200 1000 200처럼 하나의 데이터가 끊겨서 받아지는 경우도 있겠죠?

그런 경우의 수를 살펴본다면

1. 상대의 Window size가 0에서 200으로 증가했을 경우 200을 보내고 그다음 1000을 보낼 수 있게 되었을 때.

2. 네이글 알고리즘에 의해 이전 데이터 + 200바이트가 들어온 시점에서 MSS가 차 버렸을 때

의 경우가 있습니다.

그럼 우리는 이런 경우를 대비해서 L7에서 버퍼를 마련해서 오는 데이터는 다 버퍼에 받고 거기서 우리가 필요한 만큼 꺼내서 쓰는 방식으로 나가야 될 것입니다.

하지만 버퍼는 무한정 클 수는 없겠죠.

그렇기 때문에 적절한 크기의 원형 큐가 필요합니다.

우리는 이것을 링 버퍼라고 부를 것입니다.

즉 L7에서 TCP에서의 Window slide를 구현하는 것이 되는 거죠.

 

뭐 말을 하다 보니까 여기저기 다른 길로 많이 셌습니다...

사실 더 정리해야 되는 내용이 있긴 한데...

너무 길이 길어지면(이미 길긴 합니다만...) 제가 나중에 정리해놓고 안 보게 될까 봐... 이쯤에서 마무리를 해야 될 것 같습니다.

오늘도 긴 글 읽어주셔서 감사드립니다.

다음에 L4에 대한 내용 정리 추가로 한 번 더 하겠습니다.

그럼 안녕히계세요.

320x100