리눅스에서 TCP 네트워크 응용프로그램 작성하기위한 전반적인 모든 것
CSAPP(Computer Systems: A Program’s Perspective)의 11장을 읽으면서 Client와 Server간의 정리가 한번 필요하다고 생각하여 작성하는 글입니다. 여기서 소켓은 TCP/IP만을 사용하는 소켓이고, 나머지 부분은 필요에 의해한다면 따로 공부할 예정입니다. CSAPP에서 사용하고 있는 echo 서버와 linux의 socket에 대해서 설명하는 글입니다.
먼저 위와 같은 그림을 이해하기 이전에 client와 server 그리고 tcp에 대해서 이해해야합니다.
Client와 Server
프로젝트를 하다보면 Client와 Server를 사용자 기기에 있는지 아닌지 등 한가지로만 구분하게 됩니다. 이 블로그에 필자가 생각하는 Client와 Server의 차이점은 다음과 같습니다.
Client: 신호를 주는 주체
Server: 신호를 받는 객체
즉 여기서 모든 권리는 Client가 가지고 있습니다. Server은 Client가 주는 데이터를 가지고 분석하여 Client가 원하는 대답을 주어야 합니다. 이런 시각은 반대로 Server에서 원하는 데이터를 주지 않을 시, Client가 동작하지 않는 관점으로 해석할 수 있습니다.
여담으로 위와 같은 관점으로 바라보았을 때 API 규격을 어떻게 작성하면 좋을지 한번 더 생각해볼 수 있습니다. Client와 Server가 모두 어떤 상황이고 어떠한 특징을 가졌는지에 따라서 보편적으로 또는 보수적으로 작성하는 것을 고민할 수 있습니다.
TCP
TCP는 전송계층의 한 프로토콜방식이며, 이와 반대되는 것은 UDP라고 생각할 수 있습니다. (여담이지만, 매번 UDP를 보았을 때 스타크래프트가 생각납니다.) TCP는 군사적인 목적으로 어떻게든 동작할 수 있으며, 중간에 데이터 유실이 일어나거나, 너무 전달되는 것을 막기 위해 나온 것입니다. 그래서 최대한 빠른 것이 아니라 안정적인 것이 더 중점으로 만들어졌다고 생각하면 좋습니다.
Mail, game 등 몇몇을 제외하고 인터넷의 대부분의 통신은 TCP/IP에 의해서 돌아간다고 합니다. 왜 그랬을까? 라는 생각을 하면, 아무래도 대부분의 통신은 ms의 단위가 중요하지 않고, 안정적으로 통신을 완료하는 것이 더 중요하기 때문이라고 생각합니다.
TCP Segment(세그먼트)
TCP는 데이터를 일정한 단위로 분할하여 TCP 헤더를 붙여 세그먼트를 생성합니다. 이 세그먼트는 IP 데이터그램에 캡슐화되어 전송합니다. 잠시 헷갈리는 용어를 정리하고 가겠습니다.
- 세그먼트: TCP PDU(프로토콜 데이터 유닛)
- 데이터그램: IP PDU
- 프레임: 데이터 링크 계층 PDU
아래는 간략하게 표시한 TCP의 세그먼트 구조입니다.(자세한 것은 TCP정의 문서인 RFC 9293을 참고하시면 좋습니다, 아래는 정확한 것이 아닙니다.)
TCP세그먼트는 헤더와 데이터 필드로 나뉘어 있습니다.
자세한 내용은 아래 토글을 통해 확인할 수 있습니다.
네트워크의 속도에 따라서 분할된 데이터가 순서대로 도착하지 않거나, 도착하지 않는 경우가 있습니다. 이를 위와 같은 세그먼트의 규약을 통해서 데이터를 안전하고 정확하게 복구할 수 있습니다.
TCP Handshake
TCP 통신을 하려면 네트워크 연결 설정이 먼저 필요합니다. 데이터를 발송하는 애플리케이션, 수신하는 애플리케이션 모두 준비가 되었다는 것을 보장하기 위해 필요합니다. 일반적으로 TCP연결을 생성할 때는 3-Way Handshake를 이용한다.
- SYN: 클라이언트가 서버에서 SYN을 전송합니다.
- SYN-ACK: 서버가 SYN-ACK로 답장합니다. ACK는 클라이언트로부터 전달받은 SYN에 1을 덧셈한 것입니다.
- ACK: 클라이언트가 서버에게 ACK + 1을 전송합니다.
Clinet와 Server의 통신
먼저 위에서 말했다 싶이 통신의 주체는 client입니다. 하지만 이를 보내거나 받기전에 준비해야할 사전작업들이 매우 많습니다. 먼저 애플리케이션이 돌아가기 위해서는 그 아래의 계층인 커널에게 허락을 받아야 합니다. 그 이유는 각 TCP, UDP마다 가용 가능한 Port는 다르고 이를 다루는 것은 커널이기 때문입니다.(여담으로, Ubuntu 내에서는 1024이하의 포트에서는 sudo의 권한이 필요합니다)
여기서 getaddinfo함수를 통하여 열고 싶은 포트와 호스트 등의 관련되어 있는 정보들을 모두 받을 수 있습니다. 이 이후 socket을 열어 실행할 수 있습니다. 즉 커널은 이때 프로세스가 포트를 사용함을 알 수 있습니다.
여기서부터 server쪽에서 bind라는 함수와 listen이라는 함수를 사용합니다. 먼저 bind는 얻어낸 socket의 값을 이용하여 client와 연결을 수립하기 위해 존재합니다. 또 listen은 socket에 연결된 이후 클라이언트의 connection을 기다리는 함수입니다. 이렇게 작성되는 이유는 위에서 보았듯이 client는 능동적이고 server는 수동적이기 때문입니다.
이제 드디어 TCP로 연결할 수 있도록 모두 준비를 하였습니다. client에서 connect의 함수를 이용하여 원격 위치에 있는 server에게 연결을 시도합니다. server에서 listen을 하고 있다면 accept가 되어 위에서 작성된 TCP Handshake가 일어납니다. 그 이후 server는 연결되어 기다리는 상태, client는 터널이 생겨 직접 보낼 수 있는 상태가 됩니다.
위 다이어그램에서는 rio_writen을 통하여 socket에 쓰고, rio_readlineb를 통하여 socket에 들어온 정보를 적습니다. 이 과정이 소켓이 끊어질 때까지 일련의 과정을 지속할 수 있습니다.
맨 마지막에 close 또한 일반적으로 server가 주체가 아닌 client가 주체입니다. client에서 close라는 신호인 EOF를 보내주고 프로그램을 종료합니다. 이 데이터를 server에서 읽은 이후 close로 하고, 다시 listen상태로 되돌아 갑니다
위와 같은 handshake또한 kernel에서 추상화되어 쉽게 쓸 수 있다라는 것을 알 수 있습니다.
이를 가지고 CSAPP의 network:lab을 재미있게 하면 좋을 것 같습니다.