카테고리 없음

HTTP Chunking 과 테스트 클라이언트 구현

virbr0.net 2024. 6. 22. 01:24

HTTP 1.1은 HTTP 메세지를 나눠서 보낼 수 있는 청크 인코딩을 지원 합니다.

 

HTTP 1.1은 기본적으로 TCP 세션을 지속적으로 유지하며 (Persistent Connection = keep-alive = connection reuse), 이는 새로운 TCP 세션을 생성하기 위한 오버헤드를 제거해 네트워크 대기시간을 줄이는 방법 중 하나 입니다.

 

소켓 프로그래밍을 해본 사람을 알겠지만, 소켓을 재사용하기 위해서는 보내는 메세지의 정확히 길이를 알거나 종료 조건을 알아야  합니다. 따라서 사전에  HTTP 메세지의 본문(body)의 길이를 Content-Length 헤더를 통해 알려 주고, 수신 받는 쪽에서는 해당 바이트수(길이) 만큼 읽어 동일한 소켓을 종료하지 않고 연결을 유지하며 재사용 할 수 있습니다.

 

그러나, 매우 큰 데이터와 같이 사전에 전송할 본문의 길이를 알 수 없다면, 전통적인 HTTP 구현체에서 HTTP 1.0은 Content-Length 헤더를 생략하고 종료 조건인 EOF 표식 -1 이 나올 때까지 읽었습니다.

HTTP 1.1 에서는 이 문제를 해결하기 위해 Transfer-Encoding 이라는 헤더를 사용했습니다. Transfer-Encoding 헤더에 chunked 라는 값을 사용하면, Content-Length 헤더가 생략되며, 각 청크의 앞부분에 현재 청크의 길이가 16진수 형태로 오고 그 뒤 '\r\n'이 오고 그 다음에 청크 (분할 된 본문)이 오며, 길이가 0인 청크로 큰을 나타냅니다. 경우에 따라, 이후 연속된 엔티티 헤더 필드로 구성된 트레일러가 옵니다.

 

다음은 POST 요청을 chunking 해서 테스트한 HTTP 메세지 입니다.

POST /api/test HTTP/1.1
Host: localhost:8080
Accept-Encoding: identity
Transfer-Encoding: chunked
Content-Type: application/json

2c\r\n
{"Person":[{"id":1,"name":"Jaxon","age":35},
21\r\n
{"id":2,"name":"Jhon","age":40}]}
0\r\n
\r\n

 

본문 부분의  첫번째 청크인 {"Person":[{"id":1,"name":"Jaxon","age":35}, 길이는 총 44바이트로 이를 16진수로 표기하면 2c 가 됩니다. 이어 두번째 청크인 {"id":2,"name":"Jhon","age":40}]} 은 길이가 33바이트며, 이를 16진수로 표기하면 21이 됩니다. 마지막으로 길이를 0으로 표기함으로써 종료 청크를 나타냅니다.

 

HTTP Chunking 된 요청의 경우 테스트 시 많이 사용하는 curl 이나 httpie 로는 원하는 대로 본문을 나눠서 전송하는 테스트가 불가능합니다.

 

HTTP Chunking 된 메세지를 보낼수 있는 파이썬 클라이언트 코드는 아래와 같이 간단히 작성할 수 있습니다.

import http.client
import time

if __name__ == "__main__":
    conn = http.client.HTTPConnection('localhost', 8080)
    conn.connect()
    conn.putrequest('POST', '/api/test')
    conn.putheader('Transfer-Encoding', 'chunked')
    conn.putheader('Content-Type', 'application/json')    
    conn.endheaders(encode_chunked=True)
    
    chunk1 = '{"Person":[{"id":1,"name":"Jaxon","age":35},'
    chunk2 = '{"id":2,"name":"Jhon","age":40}]}'

    print(f'{hex(len(chunk1))[2:]}\r\n')
    conn.send(f'{hex(len(chunk1))[2:]}\r\n'.encode())
    print(f'{chunk1}\r\n')
    conn.send(f'{chunk1}\r\n'.encode())
    time.sleep(1)
    
    print(f'{hex(len(chunk2))[2:]}\r\n')
    conn.send(f'{hex(len(chunk2))[2:]}\r\n'.encode())
    print(f'{chunk2}\r\n')
    conn.send(f'{chunk2}\r\n'.encode())
    time.sleep(1)
    
    print("0\r\n\r\n")
    conn.send("0\r\n\r\n".encode())

    r = conn.getresponse()
    print(r.status, r.reason, r.read())

 

요청을 간단히 출력해 주는 서버를 파이썬 플라스크로 작성한 코드 입니다.

from pprint import pprint
from flask import Flask, request, jsonify


app = Flask(__name__)

@app.route('/api/test', methods=['POST'])
def post_body():
    header = dict(request.headers)
    data = request.get_json()
    pprint (header)
    pprint (data)
    res_body = data["Person"]
    return jsonify(res_body)

if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=8080)

 

위를 실행하면, 

Flask 구현체에서는 클라이언트에서 분할 전송한 바디를 하나의 json 으로 정확히 인식하는 것을 확인 할 수 있습니다.

 

 

[참조]

HTTP Chunking, Eric D. Larson, https://www.oracle.com/technical-resources/articles/javame/chunking.html
Transfer-Encoding, mdn web docs, https://developer.mozilla.org/ko/docs/Web/HTTP/Headers/Transfer-Encoding