오늘 배울 것
- HTTP는 어떻게 TCP 커넥션을 사용하는가
- TCP 커넥션의 지연, 병목, 막힘
- 병렬 커넥션, keep-alive 커넥션, 커넥션 파이프라인을 활용한 HTTP의 최적화
- 커넥션 관리를 위해 따라야 할 규칙들
TCP 커넥션
- 전 세계 모든 HTTP 통신은 패킷 교환 네트워크 프로토콜들의 계층화된 집합인 TCP/IP를 통해 이루어짐.
- 커넥션이 맺어지면 클라와 서버 컴퓨터 간에 주고 받는 메시지들은 손실되거나 순서가 바뀌지 않고 안전하게 전달됨.

- TCP는 HTTP에게 신뢰할 만한 통신 방식을 제공함. 이때 전송되는 바이트들은 순서에 맞게 반대로 전달됨.
- TCP 스트림은 세그먼트로 나뉘어 IP 패킷을 통해 전송됨.
- IP 헤더는 발신지, 목적지 IP 주소, 크기, 기타 플래그를 가짐.
- TCP 세그먼트 헤더는 TCP 포트 번호, TCP 제어 플래그, 데이터의 순서와 무결성을 검사하기 위한 숫자 값을 가짐.


- 컴퓨터는 여러 개의 TCP 커넥션을 가짐. TCP는 포트 번호를 통해서 이런 여러 개의 커넥션을 유지함.
- TCP 커넥션은
<발신지 IP 주소, 발신지 포트, 수신지 IP 주소, 수신지 포트>
로 식별함.
- TCP 커넥션의 생성을 하기 위한 기능으로
소켓 API
를 사용함. - 소켓 api를 사용하면, tcp endpoint 데이터 구조를 생성하고, 원격 서버의 tcp endpoint에 데이터 구조를 연결하여 데이터 스트림을 읽고 쓸 수 있음.
- 네트워크 프로토콜의 핸드셰이킹, TCP 데이터 스트림, IP 패킷 간의 분할 및 재조립에 대한 모든 세부사항을 외부로부터 숨김.
Sockets API call | Description |
s = socket(<parameters>) | Creates a new, unnamed, unattached socket. |
bind(s, <local IP:port>) | Assigns a local port number and interface to the socket. |
connect(s, <remote IP:port>) | Establishes a TCP connection to a local socket and a remote host and port. |
listen(s,...) | Marks a local socket as legal to accept connections. |
s2 = accept(s) | Waits for someone to establish a connection to a local port. |
n = read(s,buffer,n) | Tries to read n bytes from the socket into the buffer. |
n = write(s,buffer,n) | Tries to write n bytes from the buffer into the socket. |
close(s) | Completely closes the TCP connection. |
shutdown(s,<side>) | Closes just the input or the output of the TCP connection. |
getsockopt(s, . . . ) | Reads the value of an internal socket configuration option. |
setsockopt(s, . . . ) | Changes the value of an internal socket configuration option. |

TCP 성능에 대하여
고성능의 HTTP 소프트웨어를 개발하고 있는 게 아니라면 완벽히 이해할 필요는 없음.
- HTTP는 TCP 바로 위에 있는 계층이기 때문에 HTTP 트랜잭션의 성능은 그 아래 계층인 TCP 성능에 영향을 받음.
- 일반적인 TCP 지연
- TCP 커넥션의 핸드셰이크 설정.
- 어떤 데이터를 전송하든 새로운 TCP 커넥션을 열 때면, TCP 커넥션을 맺기 위한 조건을 맞추기 위해 연속으로 IP 패킷을 교환함. 하지만 작은 크기의 데이터 전송에 커넥션이 사용된다면 이런 성능 교환은 HTTP 성능을 크게 저하시킴.
- 개발자는 이 패킷을 보지 못하기 떄문에 왜 지연이 되는지 한 눈에 보이지 않을 수 있음.
- HTTP가 이미 존재하는 커넥션을 재활용하는 방법으로 극복할 수 있음.
- TCP의 편승 확인응답을 위한 확인응답 지연 알고리즘.
- 인터넷 자체가 패킷 전송을 완벽히 보장하지는 않기 때문에(인터넷 라우터는 과부하가 걸렸을 때 패킷을 마음대로 파기함), TCP는 성공적인 데이터 전송을 보장하기 위해서 자체적인 확인 체계를 가짐.
- 송신자가 특정 시간 안에 확인응답을 받지 못하면 패킷에 문제가 있는 것으로 판단하고 데이터를 다시 전송함.
- 이때 확인응답이 같은 방향으로 가는 데이터 패킷에 편승되는 경우를 늘리기 위해서, 많은 TCP 스택은 확인응답 지연 알고리즘을 구현함.
- 하지만 요청과 응답 두 가지 형식으로만 이루어지는 HTTP 동작 방식은, 확인 응답이 송출 데이터 패킷에 편승할 기회를 감소시킴. 막상 편승할 패킷을 찾으려 하면 해당 방향으로 송출될 패킷이 많지 않기 때문에, 확인응답 지연 알고리즘으로 인한 지연이 자주 발생함.
- 인터넷의 혼잡을 제어하기 위한 TCP의 느린 시작.
- TCP 느린 시작은 TCP가 한 번에 전송할 수 있는 패킷의 수를 제한함.
- 인터넷의 급작스러운 부하, 혼잡을 방지하는 데 쓰임.
- 패킷이 성공적으로 전달되는 각 시점에 송신자는 확인응답을 받으면 추가로 2개, 4개, 6개 순서로 패킷을 전송할 수 있는 권한을 추가로 얻음.
- 데이터를 한데 모아 한 번에 전송하기 위한 네이글(nagle) 알고리즘.
- 네이글 알고리즘은 네트워크 효율을 위해서, 패킷을 전송하기 전 많은 양의 TCP 데이터를 한 개의 덩어리로 합침.
- 이떄 효율은 TCP가 작은 크기의 데이터를 포함한 많은 수의 패킷을 전송한다면 네트워크 성능이 떨어지기 때문에 이를 방지하는 것을 말함.
- 하지만 이는 크기가 작은 HTTP 메시지는 패킷을 채우지 못하기 때문에, 앞으로 생길지 생기지 않을지 모르는 추가적은 데이터를 기다려야 함.
- 또한 확인응답지연과 함께 쓰일 경우 네이글 알고리즘은 확인응답이 도착할 때까지 데이터 전송을 멈추고 있는 반면, 확인응답 지연 알고리즘은 확인응답을 100~200밀리초 지연시킴.
- HTTP 스택에
TCP_NODELAY
파라미터 값을 설정하여 네이글 알고리즘을 비활성화할 수 있지만, 작은 크기의 패킷이 너무 많이 생기지 않도록 큰 크기의 데이터 덩어리를 만들어야 함. - TIME_WAIT 지연과 포트 고갈.
TIME_WAIT
이란 TCP 상태의 가장 마지막 단계에서 발생함.- TCP 커넥션 종단에서 TCP 커넥션을 끊으면 커넥션의 IP 주소와 포트 번호를 메모리의 작은 제어영역에 기록해 놓음. 2분정도 유지되는데 같은 IP 주소와 포트 번호를 가지는 커넥션이 2분 이내에 또 생성되는 것을 막아줌.
- 이는 성능시험을 한느 상황에 문제가 될 수 있음. 성능 측정 대상 서버는 클라이언트가 접속할 수 있는 IP 주소의 개수를 제한하고, 그 서버에 접속하여 부하를 발생시킬 컴퓨터의 수는 적기 때문임.
HTTP 커넥션 관리
커넥션을 생성하고 최적화하는 HTTP 기술에 대해 알아보자.
- 순차적인 커넥션은 느리다!
- 각 트랜잭션이 새로운 커넥션을 필요로 한다면, 커넥션을 맺는데 발생하는 지연과 함께 느린 시작 지연이 필요함.
- 물리적 지연 뿐만 아니라 하나의 이미지를 내려받고 있는 중에는 웹페이지의 나머지 공간에 아무런 변화가 없어서 느껴지는 심리적 지연도 있음.
- 특정 브라우저의 경우 객체를 화면에 배치하려면 객체의 크기를 알아야 하기 때문에 모든 객체를 내려받기 전까지는 텅 빈 화면을 보여줌.

병렬 커넥션


- 여러 개의 TCP 커넥션을 통한 동시 HTTP 요청(병렬 처리).
- 각 커넥션의 지연 시간을 겹치게 하면 총 지연 시간을 줄일 수 있고, 클라이언트의 인터넷 대역폭을 한 개의 커넥션이 다 써버리는 것이 아니라면 나머지 객체를 내려받는 데에 남은 대역폭을 사용할 수 있음.
- 하지만 클라의 네트워크 대역폭이 좁을 때 대부분 시간을 데이터 전송에만 쓰기 때문에 성능상의 장점은 없음. 그리고 실제로 여러 개의 커넥션을 생성하면서 생기는 부하 때문에 객체들을 순차적으로 내려받는 것보다 더 오래 걸릴 수 있음.
- 또한 다수의 커넥션은 메모리를 많이 소모하고 자체적인 성능 문제를 발생시킴.
- 결론적으로 병렬 커넥션은 더 빠르게 느껴질 수 있음. 페이지 총 다운로드 시간이 더 걸려도 병렬 커넥션은 객체가 동시에 보이면서 내려받기 때문에 더 빠르다고 느낄 수 있음.
지속 커넥션
- 처리가 완료된 후에도 계속 연결된 상태로 있는 TCP 커넥션.
- 사이트 지역성에 장점이 있음.
- 사이트 지역성 : HTTP 요청을 하기 시작한 애플리케이션은 웹페이지 내의 이미지 등을 가져오기 위해 그 서버에 또 요청을 하는 속성.
- 해당 서버에 이미 맺어져 있는 지속 커넥션을 재활용함으로써, 커넥션을 맺기 위한 준비작업에 따르는 시간을 절약할 수 있음.
- 하지만 지속 커넥션을 잘못 관리할 경우, 계속 연결된 상태로 있는 수많은 커넥션이 쌓이게 됨 → 클라와 서버 리소스의 불필요한 소모.
- 지속 커넥션은 병렬 커넥션과 함께 사용될 때 효과적임. 오늘 날 많은 웹 애플리케이션은 적은 수의 병렬 커넥션을 맺고 지속 커넥션으로 유지함.
- 지속 커넥션에는
keep-alive
,지속 커넥션
이 있음.
파이프라인 커넥션

- HTTP/1.1은 지속 커넥션을 통해 요청을 파이프라이닝할 수 있음.
- 다만 파이프라인에는 제약사항이 있음.
- HTTP 클라는 커넥션이 지속 커넥션인지 확인하기 전까지는 파이프라인을 이어서는 안 됨.
- 메시지에 순번이 없어서 응답은 요청 순서와 같게 와야 함.
- 클라는 커넥션이 언제 끊어지더라도, 완료되지 않은 요청이 파이프라인에 있으면 언제든 다시 요청을 보낼 준비가 되어 있어야 함.
- POST같이 비멱등성을 가진 요청은 보내면 안 됨. 에러가 발생하면 파이프라인을 통한 요청 중에 어떤 것들이 서버에서 처리되었는지 클라가 알 방법이 없음. 제공한다 하더라도 자동으로 재시도가 아닌 팝업창으로 의사를 물어야 함.
커넥션을 끊는 방법
- TCP 커넥션은 양방향임.

- 전체 끊기
- 단순한 HTTP 애플리케이션은 전제 끊기만 할 수 있음.
close()
를 호출하면 입출력 커넥션을 모두 끊음.
- 절반 끊기
- 입출력 채널 중 하나만 개별적으로 끊으려면
shutdown()
을 호출하면 됨. - 애플리케이션이 각기 다른 HTTP 클라이언트, 서버, 프락시와 통신할 때, 그리고 파이프라인 지속 커넥션을 사용할 때, 기기들이 예상치 못한 쓰기 에러를 발생하는 것을 예방하기 위해 사용함.
- 보통 커넥션의 출력 채널을 끊는 것이 안전함. 클라에서 이미 끊긴 입력 채널에 데이터를 전송하면 운영체제는
connection reset by peer
라는 메시지와 함께 읽히지 않는 데이터 모두를 삭제함.
keep-alive에 대하여

- HTTP/1.0 keep-alive를 구현한 클라는 커넥션을 유지하기 위해서 요청에
connection:Keep-Alive
헤더를 포함시킴.
- 이 요청을 받은 서버는 그다음 요청에도 이 커넥션을 통해 받고자 한다면, 응답 메시지에 같은 헤더를 포함시켜 응답함.
- 이 헤더가 없으면 클라는 서버가 keep-alive를 지원하지 않으며, 응답 메시지가 전송되고 나면 서버 커넥션을 끊을 것이라 추정함.
- keep-alive는 커넥션을 유지하기 바라는 요청일 뿐 클라나 서버는 언제든 현재의 keep-alive 커넥션을 끊을 수 있음.
- keep-alive의 동작은 옵션들로 제어할 수 있음.
- 커넥션이 끊어지기 전에 엔티티 본문의 길이를 알 수 있어야 커넥션을 유지할 수 있음 → 엔티티 본문이 정확한 Content-Length 값과 함께 멀티파트 미디어 형식을 가지거나 청크 전송 인코딩으로 인코드 되어야 함을 뜻함.
- 프락시에 따라 문제가 발생할 수 있음.
- 멍청한 프락시는 connection 헤더를 이해하지 못해 해당 헤더들을 삭제하지 않고 요청 그대로를 다음 프락시에 전달함(현재 커넥션만을 위한 정보이기 때문에 다음 커넥션에 전달하면 안 됨).
- 영리한 프락시는 proxy-connection 헤더로 connection과 분리함.
- 하지만 클라와 서버 사이의 멍청한 프락시와 영리한 프락시가 모두 존재한다면 다음과 같이 잘못된 헤더를 만들어냄.
- HTTP/1.1에서는 지속 커넥션을 기본으로 활성화시켜 별도 설정을 하지 않는다면 모든 커넥션을 지속 커넥션으로 취급함(선택이 아닐 뿐만 아니라 지원 자체를 하지 않음).
- 커넥션을 끊으려면 connection:close 헤더를 명시해야 함.


