네트워크를 공부하다 보면 "그래서 이걸 어디다 써먹지"라는 생각을 할 때가 많다. HTTP에 대해 공부하던 중 1.1 버젼의 persistent-connection을 실제로 구현해서 확인해보는 글이 있어 가져왔다. (가져왔다 == 번역 + 주관적 의견 추가)
구현 언어는 Go다. (최근에 Go를 공부해서 이해할 수 있는 건진 모르지만 코드가 갱장히 짧으니 이해할 수 있을 것이다)
1. HTTP의 stateless
HTTP 1.0에 들어서며 http의 구조에 content-type을 적을 수 있는 Header 타입이 추가되며, 인터넷상의 많은 데이터 정보를 url을 통해 주고 받을 수 있게 됐다.
이것이 의미하는 바는, 이전 버전에 비해 server와 data를 주고 받을 일이 많아졌다는 것이다. 하지만 Http의 connectionless한 특성으로 인해 1.0까지는 두 기기간의 통신마다 TCP 연결을 맺어줘야 했다. (3-way-handshake to establish tcp connection & 4-way-handshake to close tcp connection).
결국 매 요청마다 수행되는 tcp connection RTT는 latency를 유발하게 됨. 그래서 이를 해결하고자 나온 것이 persistent-connection임. (pipelining도 있지만 이는 나중에..)
2. HTTP 1.1
HTTP 1.1에서 1.0에서의 문제를 해결하기 위해 persistent-connection 기능이 추가됨. 백문이 불여일견이라고 바로 그림으로 보자.
1.0에서와의 차이점은 말안해도 알것이다. 그래도 굳이 말하자면 매 request&response마다 tcp connection을 생성하지 않는다. 즉, 하나의 tcp connection만으로 여러 request를 처리할 수 있게 된 것이다.
여기까지가 persistent-connection에 대한 등장 배경과 사용 이유이다.
부가.
pipelining
이 방식은 request에 대한 response가 와야 다음 request를 전달하는 방식임. 즉, 클라이언트는 다음 request를 보낼 수 있는 상황이지만 server의 response를 기다리느라 못보내고 있음.. => 걍 다 보낼래!!!!
근데 이것도 문제가 있어서 HTTP/2.0에서 multiplexing이라는 기능이 나옴. ㅋㅋ 끝도 없음. (문제 : 1,2,3 순서로 받은 요청을 처리하던 서버가 2를 처리하다 문제가 생기면 3까지 그 영향을 받는 문제임 Head Of Line Blocking이라 불림)
3. 사용 예시
서론이 길었다... 이제 Go를 통해 만든 조그만 웹서버와 네트워크를 관리할 수 있는 도구를 통해 persistent-connection을 테스트 해보자.
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func startHTTPserver() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Duration(50) * time.Microsecond)
fmt.Fprintf(w, "Hello world")
})
go func() {
http.ListenAndServe(":8080", nil)
}()
}
func startHTTPRequest() {
counter := 0
for i := 0; i < 10; i++ {
_, err := http.Get("http://localhost:8080/")
if err != nil {
panic(fmt.Sprintf("Error: %v", err))
}
log.Printf("HTTP request #%v", counter)
counter += 1
time.Sleep(time.Duration(1) * time.Second)
}
}
func main() {
startHTTPserver()
startHTTPRequest()
}
기본 코드이고 간단히 설명하자면 startHTTPserver()를 이용해 "/" 경로로 들어오는 요청에 대한 처리 핸들러와 요청을 Listen할 포트를 열어둔 것이고, startHTTPRequest()를 통해 "/" 경로로 10번의 요청을 보낸 것이다. 이것만 알면 된다. "열었고, 요청을 했다."
이에 대한 결과는 이렇다고 한다.
netstat -n | grep 8080
을 통해 우리가 8080 포트에 대한 확인하면
이는 곧, 10번의 connection close가 발생했다는 것이고 이는 다시 10번의 connection을 establish 했다는 것을 의미한다.
sudo tcpdump -i any -n host localhost
이 명령어를 한번 더 확인해보면
여러개의 SYN flag를 확인할 수 있다. 이는 곧 여러 connection establish가 일어났다는 뜻이다.
이제 persistent-connection을 이용해 단 한번의 connection으로 요청을 처리해보자!!
4. Persistent-connection으로 TCP 조금 맺기
package main
import (
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"time"
)
func startHTTPServer() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Duration(50) * time.Microsecond)
fmt.Fprintf(w, "Hello world")
})
go func() {
http.ListenAndServe(":8080", nil)
}()
}
func startHTTPRequest() {
counter := 0
for i := 0; i < 10; i++ {
resp, err := http.Get("http://localhost:8080/")
if err != nil {
panic(fmt.Sprintf("Error: %v", err))
}
io.Copy(ioutil.Discard, resp.Body) // read the response body
resp.Body.Close() // close the response body
log.Printf("HTTP request #%v", counter)
counter += 1
time.Sleep(time.Duration(1) * time.Second)
}
}
func main() {
startHTTPServer()
startHTTPRequest()
}
위 코드와 다른점은 startHTTPRequest() 부분에 원본글의 작성자님이 주석을 달아주셨다. 그 이유는 다음과 같다.
In fact,this is a well known issue in Golang ecosystem:
- If the returned error is nil, the Response will contain a non-nil Body which the user is expected to close. If the Body is not both read to EOF and closed, the Client’s underlying RoundTripper (typically Transport) may not be able to re-use a persistent TCP connection to the server for a subsequent “keep-alive” request.
요약하자면 Go에서는 Response Body를 EOF까지 읽지도 않고 close하지도 않았다면 기본적으로 persistent-connection을 사용하지 않는다는 것이다.
그래서 위 두가지 원인인 1. Body 끝까지 읽기 2. Body 닫기 를 수행해 준것이다.
그럼 이제 Go에서 http요청에서 persistent-connection을 사용할 것이다. 결과를 한번 보자!
5. 결과
netstat을 통해 확인한 결과는 다음과 같다
tcpdump를 통해 확인한 결과는 다음과 같다
그렇다.
단 하나의 tcp connection으로 무자비한 10개의 요청을 먹어치운 것이다.
요롷게 간단하게만 확인해봤다. 원본글에 대한 정보와 보다 많은 정보를 원한다면 하단의 링크를 확인해줘라.
Understand HTTP/1.1 persistent connection: A Hands-on Approach
I will show you how persistent connection works based on a Golang application. We will do some experiments based on the demo app.
levelup.gitconnected.com
(뒤에 보면 Go에서 10개의 concurrency에서 10개의 request를 보내는 작업에 대한 내용이 나옴. Go를 학습하고자 한다면 굉장히 유용할듯)
'IT 일기 > GO' 카테고리의 다른 글
Go 채널이란? (0) | 2023.04.02 |
---|---|
What is GoRoutine? (0) | 2023.03.31 |
Go Slice 사용법, 구조 그리고 append() 원리까지 Deep Dive (0) | 2023.03.29 |
Understand SOLID in Go (0) | 2023.03.24 |
vscode Terminal 설정 변경 (Go에서 sqlite3 사용하기) (0) | 2023.03.23 |