개발놀이터
TCP 프로토콜을 OS레벨에서 뜯어보자 본문
TCP프로토콜은 전세계적으로 가장 많이 사용되는 프로토콜 중 하나인데요. 이 TCP프로토콜이 OS레벨에서는 어떻게 움직이는지 살짝 궁금해져서 공부해보고 정리해봤습니다!
OS레벨에서 개요를 살펴보고 여기에 Nginx, Tomcat이 들어가고 데이터베이스가 어떻게 데이터를 가져오는지 흐름에 대해서 정리해봤습니다.
해당 포스팅은 TCP/IP에 대한 내용이 생략되어있습니다. 아래의 링크에 부족하지만 어느정도 정리가 되어있으니 참고해주시면 감사하겠습니다.
https://coding-review.tistory.com/466
OS레벨에서 TCP
TCP를 연결하기 위해선 3 way handshake가 필요한데, 이 과정을 OS레벨에서 어떻게 이뤄냈는지에 대해서 정리해보겠습니다.
UNIX체계에서 socket
유닉스 체계 (편하게 리눅스라고 부르겠습니다) 에서는 모든 동작을 파일로 관리한다는 사실, 알고 계셨나요? 저도 이번에 공부하면서 알게 되었는데요.
이 파일들을 관리하기위해 file descriptor라는 것을 가지고 있으며 아마도 엄청나게 많을 것으로 예상되는 파일들을 관리하기 위해 epoll이라는 것도 가지고 있습니다. 이번 포스팅에선 자세히 다루지 않습니다.
그런데 윈도우도 그렇고 dll처럼 대부분 파일로 관리하지 않나? 라는 생각이 들었지만 다음 기회에 더 자세히 공부해보도록 하겠습니다.
아무튼 리눅스에선 네트워크 연결을 위한 소켓 또한 파일로 관리하고 있습니다. 이 파일을 가지고 네트워크 연결을 할 것인지 말것인지 충돌이 나면 어떻게 행동할 것인지 정의하고 있죠.
먼저 리눅스에 네트워크 요청이 들어오면 socket이라는 시스템 콜을 호출합니다.
socket(AF_INET, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, IPPROTO_IP) = 3
이 요청으로 하나의 소켓 파일이 만들어지는데 첫 번째 인자로 AF_INET은 해당 네트워크 요청이 IPv4로 들어왔다는 것을 알려주는 것이고 뒤이어 SOCK_STREAM은 TCP통신을 하겠다는 의미입니다.
| 기호 뒤에 있는 것들은 해당 파일이 어떤 방식으로 동작해야하는지 가이드라인을 주는 것입니다. 세 번째 인자로 IPPOROTO_IP라는 값을 넘겨주면 우리가 소켓 파일을 만들 때 정의했던 동작 방식에 따라 TCP프로토콜을 자동적으로 선택합니다.
그리고 유니크한 아이디를 부여받죠. 위의 예제에선 3이라는 값을 받았네요.
여담이지만 SOCK_STREAM대신 SOCK_DGRAM이라는 것을 넘겨주면 UDP통신을 하겠다는 것으로 간주하고 준비합니다.
뭐 다른건 다 필요없고 그냥 소켓이라는 파일을 하나 만들었으며 이게 TCP프로토콜을 자동으로 선택하도록 동작 방식을 설정했고 해당 파일에 유니크한 값인 3을 부여받았다는 것이 중요합니다. 인자는 이번 포스팅에서 중요하지 않습니다.
네트워크 드라이버와 바인드
소켓을 만들었다고 해서 바로 네트워크 연결이 이루어진 것은 아닙니다. 네트워크 드라이버와 소켓이 바인딩이 되어야 준비가 완료된 것인데요.
리눅스는 소켓 파일을 기반으로 아래의 시스템 콜을 호출하여 바인딩을 합니다.
bind(3, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("192.168.1.10")}, 16) = 0
굳이 해석하자면 IPv4로 80포트에 192.168.1.10인 IP를 바인딩하겠다는 의미입니다.
이 과정에서 성공했다면 0을, 실패했다면 소켓 아이디를 넘겨줍니다.
실패한 경우는 동시에 TCP요청을 한 경우가 대부분이고 보통 실패한 경우 소켓을 닫아줍니다. 이 때 close 시스템 콜이 호출됩니다.
close(3)
만약 성공했다면 이제 준비가 완료된 것입니다.
아직까지 네트워크를 받지 않은 상황이고 준비의 준비를 거쳐 준비가 완료되었다고 볼 수 있습니다.
네트워크 요청 허용
리눅스는 네트워크 드라이버와 소켓을 바인딩하고 이어서 직접 요청을 받을 준비를 진행합니다.
listen(3, 4096)= 0
epoll_ctl(5, EPOLL_CTL_ADD, 3, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data={u32=4271898626, u64=9220810217688662018}}) = 0
3번 소켓을 열어두고 epoll에게 해당 소켓을 관리하라고 명령을 내립니다.
그리고 해당 동작이 완료되면 본격적으로 네트워크 요청을 받을 수 있도록 허용해주는 작업이 들어갑니다.
accept4(3, {sa_family=AF_INET, sin_port=htons(55402), sin_addr=inet_addr("192.168.1.10")}, [112 => 16], SOCK_CLOEXEC|SOCK_NONBLOCK) = 4
epoll_ctl(5, EPOLL_CTL_ADD, 4, {events=EPOLLIN|EPOLLOUT|EPOLLRDHUP|EPOLLET, data={u32=4255645697, u64=9220810217672409089}}) = 0
setsockopt(4, SOL_TCP, TCP_NODELAY, [1], 4) = 0
setsockopt(4, SOL_SOCKET, SO_KEEPALIVE, [1], 4) = 0
setsockopt(4, SOL_TCP, TCP_KEEPINTVL, [15], 4 <unfinished ...>
시스템 콜은 별로 중요하지 않습니다. 무슨소리인지 하나도 모르겠네요. 아무튼 우리는 accept를 한다는 것이 중요한 것입니다.
사용자의 요청
사용자는 GET혹은 POST와 같은 다양한 요청들을 진행합니다. 그 때 알맞는 데이터를 가져오게 됩니다.
만약 사용자가 GET요청을 했다면 다음과 같은 시스템 콜이 호출됩니다.
read(4, "GET / HTTP/1.1\r\nHost: 192.168.1.10\r\nUser-Agent: curl/7.87.0\r\nAccept: */*\r\n\r\n", 4096) = 76
이 read의 의미는 우리가 만든 소켓 파일을 읽겠다는 의미입니다. 그럼 write도 있겠네요. 이 둘은 쌍으로 붙어다니니까요.
write(4, "HTTP/1.1 200 OK\r\nDate: Tue, 26 Sep 2023 11:43:47 GMT\r\nContent-Length: 12\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nHello kitty!", 129 <unfinished ...>
근데.. 소켓 파일에서 뭘 읽고 뭘 쓴다는 걸까요? 파일이니까 뭔가 쓰여있긴 할텐데..
소켓파일을 읽을 때 다양한 메타데이터들이 적혀있는데요. 요청이 들어온 HTTP header같은 것들이 적혀있죠. 그리고 소켓 파일에 데이터를 쓸 때는 HTTP status나 HTTP response header, HTTP body 등을 파일에 적습니다.
연결 해제
만약 TCP연결이 끊어지면 close 시스템 콜을 호출하여 TCP연결을 해제합니다.
close(4)
이 때 살짝 궁금했던게 어떻게 TCP연결이 끊어진 것을 알 수 있을까? 싶었는데 OS내부적으로 설정해둔 시간이 있어서 이 시간이 지나면 저절로 close를 한다더군요.
아니면 개발자가 직접 TCP연결이 종료되었다고 시스템 콜을 호출해주는 경우도 있을 수 있겠죠.
위의 상황은 일반적인 상황이 아니다
우리가 GET, POST요청을 보통 어디에 보내는지 생각해보면 위의 상황은 잘 와닿지 않습니다.
우리는 보통 Nginx와 같은 Web Server에 요청을 보내고 그 Web Server가 WAS에 그 요청을 위임하고 WAS서버는 데이터베이스와 통신하면서 데이터를 가져다 줍니다.
이 과정을 위의 상황과 결합해서 다시 한 번 정리해보도록 하겠습니다.
- 사용자가 POST요청을 보내면서 데이터를 요청합니다.
- OS가 socket 시스템 콜로 소켓 파일을 만든다.
- Nginx가 bind 시스템 콜을 이용해서 소켓 파일을 바인딩한다. 동시에 listen 시스템 콜로 연결을 받을 준비를 하고 accpet 시스템 콜로 네트워크를 허용한다. 그리고 이후 과정을 WAS에 위임한다.
- 이 과정에서 소켓 파일에는 어떤 IP주소로 요청이 왔는지 어떤 포트로 요청이 왔는지 어떤 HTTP method가 요청됐는지에 대한 정보가 있습니다.
이 정보를 토대로 어떤 WAS에 가야하는지 어떤 포트로 가야하는지 어떤 URI로 매핑되어야 하는지 어떤 HTTP method인지 어떤 데이터가 넘어오는지를 확인합니다. - 예를 들어서 localhost:8080에 POST요청이 들어왔다면 해당 정보를 매핑하고 데이터베이스와 연결을 시도합니다.
- 데이터베이스 엔진은 read() 시스템 콜을 호출합니다. 데이터베이스는 버퍼 풀에 데이터가 있는지 확인하고 없다면 VFS를 이용해서 파일 시스템에 접근합니다.
- VFS는 하드디스크에 접근하기 위해 I/O Scheduler를 이용해서 스케줄링을 하고 이를 스토리지 컨트롤러에 전달합니다.
- 스토리지 컨트롤러는 HDD의 경우 직접 헤드를 움직이고 SSD의 경우 데이터가 있는 위치를 가리키는 포인터를 이용해서 데이터를 가져오라는 전기적 신호를 보냅니다.
- MDA는 스토리지에 있는 데이터를 버퍼에 담아 메모리에 전달합니다.
- 메모리는 데이터베이스 버퍼 풀에 데이터를 담고 이를 WAS에 전달합니다.
- WAS에서 데이터 가공을 마치고 HTTP response header나 body를 작성한대로 리턴하게 되고 이 값을 소켓 파일에 적습니다.
- 이 값이 프론트로 넘어갑니다. 이하 생략..
이렇게 TCP 요청이 OS -> Web Server -> WAS -> DB의 흐름을 가지며 이동하게 됩니다.
마치며
이번 포스팅에서는 TCP프로토콜을 OS레벨에서 공부해보고 정리해봤습니다. 리눅스 커널은 정말 쉽지 않네요... 단순히 흐름만 따라가는 정도라 머리로는 이해하지만 가슴으로 와닿지는 않는 경우가 많은 것 같네요.
OS커널을 직접 뜯어보는 사람이 엄청나게 대단해보이는 오늘입니다...
이렇게 긴 포스팅을 마치도록 하겠습니다. 오늘도 즐거운 하루되세요!
출처
https://deploy.equinix.com/blog/tcp-and-the-os-kernel-networking-basics-for-developers/
'CS 지식 > 운영체제' 카테고리의 다른 글
리눅스의 파일 입출력 (feat. 파일시스템) (0) | 2024.10.31 |
---|---|
리눅스 기본 명령어 동작 원리 : 디렉토리 구조편 (cd, ls, rm, mv, cp) (0) | 2024.10.29 |
데이터베이스 쿼리를 실행하면 내부적으로는 어떤 일이 벌어질까? (0) | 2024.10.27 |
리눅스 alias로 파일, 폴더 휴지통으로 이동시키기 (0) | 2024.06.06 |
리눅스 파일, 폴더 권한 부여 (0) | 2024.06.03 |