티스토리 뷰

728x90
반응형

지난 포스팅 : Python의 Requests Library의 동작 Part1 에서 이어집니다.

https://teus-kiwiee.tistory.com/160

 

Python의 Requests Library의 동작 Part1

지난 포스팅을 통해서, Socket Programming을 사용하면 Server Client 간 HTTP 통신이 가능한 것을 확인 하였습니다. https://teus-kiwiee.tistory.com/159 Socket을 이용한 Low Level HTTP 통신 요즘은 많은 분..

teus-kiwiee.tistory.com

 

지난 포스팅까지 해서 HTTPAdapter Class에 대해서 확인하는 과정에서 urllib3이 나왔고, Requests Package를 벗어나는 영역이라 2번째 Part로 나눴습니다.

 

현재까지 상황을 정리하면 아래와 같습니다.

Requests.get에서 request()에 Message전달
-> Requests.request에서 Requests.Session Object에 Message전달
-> Requests.Session.request에서 Request Object 생성
-> Requests.Session.prepare_request에 Message전달
-> Requests.Session의 prep로 결과를 받고, Requests.Session.send에 Message전달
-> Requests.Session.send에서 Requests.Session.get_adapter로 Message전달
-> HTTPAdapter Object가 필요함

지난번, 아래 requests/adapters의 HTTPAdapter Class의 send method에서 conn을 획득해야 했고

 

이 conn은 HTTPAdapter Class의 get_connection method로부터 받아왔습니다.

 

출처 : requests/requests/adapters.py#436

    def send(
        self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
    ):        
        try:
            conn = self.get_connection(request.url, proxies)
        except LocationValueError as e:
            raise InvalidURL(e, request=request)
    ...
    
    def get_connection(self, url, proxies=None):        
        proxy = select_proxy(url, proxies)

        if proxy:
            proxy = prepend_scheme_if_needed(proxy, "http")
            proxy_url = parse_url(proxy)
            if not proxy_url.host:
                raise InvalidProxyURL(
                    "Please check proxy URL. It is malformed "
                    "and could be missing the host."
                )
            proxy_manager = self.proxy_manager_for(proxy)
            conn = proxy_manager.connection_from_url(url)
        else:
            # Only scheme should be lower case
            parsed = urlparse(url)
            url = parsed.geturl()
            conn = self.poolmanager.connection_from_url(url)

        return conn
        
    def init_poolmanager(
        self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs
    ):              
        self._pool_connections = connections
        self._pool_maxsize = maxsize
        self._pool_block = block

        self.poolmanager = PoolManager(
            num_pools=connections,
            maxsize=maxsize,
            block=block,
            strict=True,
            **pool_kwargs,
        )

반환하는 conn은 PoolManager Object의 method의 반환값인 것을 알 수 있고, 해당 객체는 urllib3에 존재하는 class입니다.

 

그럼, 이제 회고는 끝내고 urllib3으로 들어가 보겠습니다.

 

출처 : urllib3/src/urllib3/poolmanager.py#172

class PoolManager(RequestMethods):    
    proxy: Optional[Url] = None
    proxy_config: Optional[ProxyConfig] = None

    def __init__(
        self,
        num_pools: int = 10,
        headers: Optional[Mapping[str, str]] = None,
        **connection_pool_kw: Any,
    ) -> None:
        super().__init__(headers)
        self.connection_pool_kw = connection_pool_kw

        def dispose_func(p: Any) -> None:
            p.close()

        self.pools: RecentlyUsedContainer[PoolKey, HTTPConnectionPool]
        self.pools = RecentlyUsedContainer(num_pools, dispose_func=dispose_func)

        self.pool_classes_by_scheme = pool_classes_by_scheme
        self.key_fn_by_scheme = key_fn_by_scheme.copy()
        

    def connection_from_url(
        self, url: str, pool_kwargs: Optional[Dict[str, Any]] = None
    ) -> HTTPConnectionPool:       
        u = parse_url(url)
        return self.connection_from_host(
            u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs
        )
        
    def connection_from_host(
        self,
        host: Optional[str],
        port: Optional[int] = None,
        scheme: Optional[str] = "http",
        pool_kwargs: Optional[Dict[str, Any]] = None,
    ) -> HTTPConnectionPool:
            
        if not host:
            raise LocationValueError("No host specified.")

        request_context = self._merge_pool_kwargs(pool_kwargs)
        request_context["scheme"] = scheme or "http"
        if not port:
            port = port_by_scheme.get(request_context["scheme"].lower(), 80)
        request_context["port"] = port
        request_context["host"] = host

        return self.connection_from_context(request_context)
        
    def connection_from_context(
        self, request_context: Dict[str, Any]
    ) -> HTTPConnectionPool:
            
        if "strict" in request_context:
            warnings.warn(
                "The 'strict' parameter is no longer needed on Python 3+. "
                "This will raise an error in urllib3 v3.0.0.",
                DeprecationWarning,
            )
            request_context.pop("strict")

        scheme = request_context["scheme"].lower()
        pool_key_constructor = self.key_fn_by_scheme.get(scheme)
        if not pool_key_constructor:
            raise URLSchemeUnknown(scheme)
        pool_key = pool_key_constructor(request_context)

        return self.connection_from_pool_key(pool_key, request_context=request_context)


    def connection_from_pool_key(
        self, pool_key: PoolKey, request_context: Dict[str, Any]
    ) -> HTTPConnectionPool:
            
        with self.pools.lock:
            # If the scheme, host, or port doesn't match existing open
            # connections, open a new ConnectionPool.
            pool = self.pools.get(pool_key)
            if pool:
                return pool

            # Make a fresh ConnectionPool of the desired type
            scheme = request_context["scheme"]
            host = request_context["host"]
            port = request_context["port"]
            pool = self._new_pool(scheme, host, port, request_context=request_context)
            self.pools[pool_key] = pool

        return pool

일단 순서대로.

  1. connection_from_url에서 url을 parsing하고, url을 host, port, scheme 단위로 쪼갭니다.
  2. connection_from_host에서 host정보를 한번 검토하고, 정상이면 host와 port, scheme정보를 request context에 업데이트 시킵니다.
  3. connection_from_context에서 pool_key_constructor를 통해서 connection pool을 만들기 위한 context를 준비시킵니다.(PoolKey Class)
  4. connection을 위한 pool을 생성하고, 이 pool 을 반환합니다.

이때, 4번의 pool에 대해서 좀더 확인해 보겠습니다.

출처 : urllib3/src/urllib3/poolmanager.py#242

    def _new_pool(
        self,
        scheme: str,
        host: str,
        port: int,
        request_context: Optional[Dict[str, Any]] = None,
    ) -> HTTPConnectionPool:
        
        pool_cls: Type[HTTPConnectionPool] = self.pool_classes_by_scheme[scheme]
        if request_context is None:
            request_context = self.connection_pool_kw.copy()

        if request_context.get("blocksize") is None:
            request_context["blocksize"] = _DEFAULT_BLOCKSIZE

        for key in ("scheme", "host", "port"):
            request_context.pop(key, None)

        if scheme == "http":
            for kw in SSL_KEYWORDS:
                request_context.pop(kw, None)

        return pool_cls(host, port, **request_context)
    ...
    
    pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool}
    self.pool_classes_by_scheme = pool_classes_by_scheme

poolmanager의 _new_pool method에서 기존에 context를 받고

 

이 context의 schem, host, port부분을 일부 수정한 다음에 HTTP/HTTPSConnectionPool Object를 만들어 반환하는 것을 볼 수 있습니다.

 

출처 : urllib3/src/urllib3/connectionpool.py#116

class HTTPConnectionPool(ConnectionPool, RequestMethods):
    """
    Thread-safe connection pool for one host.    
    """
    scheme = "http"
    ConnectionCls: Type[Union[HTTPConnection, HTTPSConnection]] = HTTPConnection
    ResponseCls = HTTPResponse

    def __init__(
        self,
        host: str,
        port: Optional[int] = None,
        timeout: Optional[_TYPE_TIMEOUT] = _DEFAULT_TIMEOUT,
        maxsize: int = 1,
        block: bool = False,
        headers: Optional[Mapping[str, str]] = None,
        retries: Optional[Union[Retry, bool, int]] = None,
        _proxy: Optional[Url] = None,
        _proxy_headers: Optional[Mapping[str, str]] = None,
        _proxy_config: Optional[ProxyConfig] = None,
        **conn_kw: Any,
    ):
        ConnectionPool.__init__(self, host, port)
        RequestMethods.__init__(self, headers)

        if not isinstance(timeout, Timeout):
            timeout = Timeout.from_float(timeout)

        if retries is None:
            retries = Retry.DEFAULT

        self.timeout = timeout
        self.retries = retries
        self.pool: Optional[queue.LifoQueue[Any]] = self.QueueCls(maxsize)
        self.block = block
        self.proxy = _proxy
        self.proxy_headers = _proxy_headers or {}
        self.proxy_config = _proxy_config

        # Fill the queue up so that doing get() on it will block properly
        for _ in range(maxsize):
            self.pool.put(None)

        # These are mostly for testing and debugging purposes.
        self.num_connections = 0
        self.num_requests = 0
        self.conn_kw = conn_kw

        if self.proxy:            
            self.conn_kw.setdefault("socket_options", [])
            self.conn_kw["proxy"] = self.proxy
            self.conn_kw["proxy_config"] = self.proxy_config

결국 HTTPAdapter Class의 get_connections의 반환값으로 requests Context로 만들어진 HTTPConnectionPool Object가 만들어 졌습니다.

 

이 HTTPConnectionPool같은 경우 ThreadSafe한 Connection을 Queue에 넣고, Pool 형태로 관리하는것을 확인할 수 있습니다.

 

이제 conn의 정체를 알았으니, 다시 requests/adapters.py의 send로 돌아가 보겠습니다.

출처 : requests/requests/adapters.py#436

    def send(
        self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None
    ):
        try:
            conn = self.get_connection(request.url, proxies)
        except LocationValueError as e:
            raise InvalidURL(e, request=request)

        self.cert_verify(conn, request.url, verify, cert)
        url = self.request_url(request, proxies)
        self.add_headers(
            request,
            stream=stream,
            timeout=timeout,
            verify=verify,
            cert=cert,
            proxies=proxies,
        )
        chunked = not (request.body is None or "Content-Length" in request.headers)

        if isinstance(timeout, tuple):
            try:
                connect, read = timeout
                timeout = TimeoutSauce(connect=connect, read=read)
            except ValueError:
                raise ValueError(
                    f"Invalid timeout {timeout}. Pass a (connect, read) timeout tuple, "
                    f"or a single float to set both timeouts to the same value."
                )
        elif isinstance(timeout, TimeoutSauce):
            pass
        else:
            timeout = TimeoutSauce(connect=timeout, read=timeout)
        #==============요기까지 connection pool을 획득하고, Req Header를 정리======
        try:
        #==============Chunk Encoding이 적용되지 않은 경우======
            if not chunked:
                resp = conn.urlopen(
                    method=request.method,
                    url=url,
                    body=request.body,
                    headers=request.headers,
                    redirect=False,
                    assert_same_host=False,
                    preload_content=False,
                    decode_content=False,
                    retries=self.max_retries,
                    timeout=timeout,
                )
        #==============Chunk Encoding이 적용된 경우======
            # Send the request.
            else:
                if hasattr(conn, "proxy_pool"):
                    conn = conn.proxy_pool

                low_conn = conn._get_conn(timeout=DEFAULT_POOL_TIMEOUT)

                try:
                    skip_host = "Host" in request.headers
                    low_conn.putrequest(
                        request.method,
                        url,
                        skip_accept_encoding=True,
                        skip_host=skip_host,
                    )

                    for header, value in request.headers.items():
                        low_conn.putheader(header, value)

                    low_conn.endheaders()

                    for i in request.body:
                        low_conn.send(hex(len(i))[2:].encode("utf-8"))
                        low_conn.send(b"\r\n")
                        low_conn.send(i)
                        low_conn.send(b"\r\n")
                    low_conn.send(b"0\r\n\r\n")

                    # Receive the response from the server
                    r = low_conn.getresponse()

                    resp = HTTPResponse.from_httplib(
                        r,
                        pool=conn,
                        connection=low_conn,
                        preload_content=False,
                        decode_content=False,
                    )

이제 HTTP Header의 추가적인 정비작업을 하고, Chunk Encoding 여부에 따라서 다르게 Response를 받아오는 것을 알 수 있습니다.

(Chunk Encoding같은 경우 Body를 Chunk단위로 쪼개서 오기 때문에, 이 Chunk를 pool마다 따로따로 받아서 합치는 것을 유추해 볼 수가 있습니다)

 

이제 고지가 가까워 보입니다. 이제 저 urlopen의 반환 형식으로 BaseHTTPResponse 객체를 반환하고

 

Response를 받았다는 것은 Client <-> Server간 통신이 끝났다는것을 의미하니깐요!

 

그럼 HTTPConnectionPool.urlopen 소스코드를 살펴보겠습니다.

출처 : urllib3/src/urllib3/connectionpool.py#537

    def urlopen(  # type: ignore[override]
        self,
        method: str,
        url: str,
        body: Optional[_TYPE_BODY] = None,
        headers: Optional[Mapping[str, str]] = None,
        retries: Optional[Union[Retry, bool, int]] = None,
        redirect: bool = True,
        assert_same_host: bool = True,
        timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
        pool_timeout: Optional[int] = None,
        release_conn: Optional[bool] = None,
        chunked: bool = False,
        body_pos: Optional[_TYPE_BODY_POSITION] = None,
        **response_kw: Any,
    ) -> BaseHTTPResponse:
        
        parsed_url = parse_url(url)
        destination_scheme = parsed_url.scheme

        if headers is None:
            headers = self.headers

        if not isinstance(retries, Retry):
            retries = Retry.from_int(retries, redirect=redirect, default=self.retries)

        if release_conn is None:
            release_conn = response_kw.get("preload_content", True)

        # Check host
        if assert_same_host and not self.is_same_host(url):
            raise HostChangedError(self, url, retries)

        # Ensure that the URL we're connecting to is properly encoded
        # target url의 query들을 encoding하고, 이 encoding된 결과는 최종적으로 to_str을 사용해서 python의 unicode형태로 반환해줌
        if url.startswith("/"):
            url = to_str(_encode_target(url))
        else:
            url = to_str(parsed_url.url)

        conn = None

        release_this_conn = release_conn

        http_tunnel_required = connection_requires_http_tunnel(
            self.proxy, self.proxy_config, destination_scheme
        )

        if not http_tunnel_required:
            headers = headers.copy()  # type: ignore[attr-defined]
            headers.update(self.proxy_headers)  # type: ignore[union-attr]

        err = None
        clean_exit = False
        body_pos = set_file_position(body, body_pos)

        try:
            # Request a connection from the queue.
            timeout_obj = self._get_timeout(timeout)
            conn = self._get_conn(timeout=pool_timeout)

            conn.timeout = timeout_obj.connect_timeout  # type: ignore[assignment]

            is_new_proxy_conn = self.proxy is not None and not getattr(
                conn, "sock", None
            )
            if is_new_proxy_conn:
                assert isinstance(self.proxy, Url)
                conn._connecting_to_proxy = True
                if http_tunnel_required:
                    try:
                        self._prepare_proxy(conn)
                    except (BaseSSLError, OSError, SocketTimeout) as e:
                        self._raise_timeout(
                            err=e, url=self.proxy.url, timeout_value=conn.timeout
                        )
                        raise

            # Make the request on the httplib connection object.
            httplib_response = self._make_request(
                conn,
                method,
                url,
                timeout=timeout_obj,
                body=body,
                headers=headers,
                chunked=chunked,
            )

이곳에서 확인할 수 있는것은, target url의 query 부분을 처리하는 부분이 추가되어있는 것을 알 수가 있습니다.

 

해당 소스코드의 아래 부분은 retry를 처리하기 위한 에러 처리 부분이고, 이제 HTTPConnectionPool.urlopen._make_request를 찾아가게 됩니다.

 

출처 : urllib3/src/urllib3/connectionpool.py#371

    def _make_request(
        self,
        conn: HTTPConnection,
        method: str,
        url: str,
        timeout: _TYPE_TIMEOUT = _DEFAULT_TIMEOUT,
        chunked: bool = False,
        **httplib_request_kw: Any,
    ) -> _HttplibHTTPResponse:
        
        self.num_requests += 1

        timeout_obj = self._get_timeout(timeout)
        timeout_obj.start_connect()
        conn.timeout = timeout_obj.connect_timeout  # type: ignore[assignment]

        '''
        connection validation 부분
        '''
        try:
            if chunked:
                conn.request_chunked(method, url, **httplib_request_kw)
            else:
                conn.request(method, url, **httplib_request_kw)

여기서는 request의 timeout을 위한 time 설정 및 기존 만들어진 HTTPConnection이 정상인지 검토하고, HTTPConnection.request로 넘어가게 됩니다.

 

출처 : urllib3/src/urllib3/cocnnection.py#286

    def request(  # type: ignore[override]
        self,
        method: str,
        url: str,
        body: Optional[_TYPE_BODY] = None,
        headers: Optional[Mapping[str, str]] = None,
        chunked: bool = False,
    ) -> None:

        if headers is None:
            headers = {}
        header_keys = frozenset(to_str(k.lower()) for k in headers)
        skip_accept_encoding = "accept-encoding" in header_keys
        skip_host = "host" in header_keys
        self.putrequest(
            method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host
            
        chunks_and_cl = body_to_chunks(body, method=method, blocksize=self.blocksize)
        chunks = chunks_and_cl.chunks
        content_length = chunks_and_cl.content_length

        # When chunked is explicit set to 'True' we respect that.
        if chunked:
            if "transfer-encoding" not in header_keys:
                self.putheader("Transfer-Encoding", "chunked")
        else:            
            if "content-length" in header_keys:
                chunked = False
            elif "transfer-encoding" in header_keys:
                chunked = True

            else:
                chunked = False
                if content_length is None:
                    if chunks is not None:
                        chunked = True
                        self.putheader("Transfer-Encoding", "chunked")
                else:
                    self.putheader("Content-Length", str(content_length))

        if "user-agent" not in header_keys:
            self.putheader("User-Agent", _get_default_user_agent())
        for header, value in headers.items():
            self.putheader(header, value)
        #============= 실질적으로 request header가 완성되는 부분)
        self.endheaders()
        #Thread Safe를 위해서 self.__state를 _CS_REQ_STARTED -> SENT로 변경하고
        #self._send_output() method로 HTTP Message를 전송함

        '''
        Chunk True일 경우 Chunk Message를 전송하는 부분
        '''

여기서, putrequest같은 경우 super().putrequest를 사용하게 되고, 이 super().putrequest는 Python의 내장 Library인 http.client의 HTTPConnection.putrequest가 됩니다.

 

이때 Official Docs상 해당 부분이 sent를 한다고 되어 있으나, 사실 __state를 변경한 다음 HTTP Message를 생성하는 단계일뿐, 아직 Client -> Server로 HTTP Message가 전송되지는 않습니다.

 

실질적으로 HTTPConnection.endheaders에서 object의 state를 바꾸고, 이 다음 _send_output method를 호출하여 HTTP Message 전송을 시도하게 됩니다.

출처 : CPython/Lib/http/client.py#1266

    def endheaders(self, message_body=None, *, encode_chunked=False):
        if self.__state == _CS_REQ_STARTED:
            self.__state = _CS_REQ_SENT
        else:
            raise CannotSendHeader()
        self._send_output(message_body, encode_chunked=encode_chunked)
        
    def _send_output(self, message_body=None, encode_chunked=False):
        """Send the currently buffered request and clear the buffer.
        Appends an extra \\r\\n to the buffer.
        A message_body may be specified, to be appended to the request.
        """
        self._buffer.extend((b"", b""))
        msg = b"\r\n".join(self._buffer)
        del self._buffer[:]
        self.send(msg)
        '''
        이 이하는 chunk 전송 시 처리
        '''

이제 거의 끝이 보입니다. endheaders부분까지 HTTP Requests Message의 검증이 완료된 상태가 되었고, 이 Message는 HTTPConnection.send method를 통해서 전송이 되게 됩니다.

 

출처 : Cpython/Lib/http/client.py#967

    def send(self, data):
        if self.sock is None:
        #self.sock 이 없을경우, self.connect를 통해서 self.sock을 다시 정의해줌
            if self.auto_open:
                self.connect()
            else:
                raise NotConnected()

        if self.debuglevel > 0:
            print("send:", repr(data))
        if hasattr(data, "read") :
            if self.debuglevel > 0:
                print("sendIng a read()able")
            encode = self._is_textIO(data)
            if encode and self.debuglevel > 0:
                print("encoding file using iso-8859-1")
            while 1:
                datablock = data.read(self.blocksize)
                if not datablock:
                    break
                if encode:
                    datablock = datablock.encode("iso-8859-1")
                sys.audit("http.client.send", self, datablock)
                self.sock.sendall(datablock)
            return
        sys.audit("http.client.send", self, data)
        try:
            self.sock.sendall(data)
        except TypeError:
            if isinstance(data, collections.abc.Iterable):
                for d in data:
                    self.sock.sendall(d)
            else:
                raise TypeError("data should be a bytes-like object "
                                "or an iterable, got %r" % type(data))
                                
    def connect(self):
        """Connect to the host and port specified in __init__."""
        sys.audit("http.client.connect", self, self.host, self.port)
        #self.sock이 없을 때 새로 정의해 주는 부분
        self.sock = self._create_connection(
            (self.host,self.port), self.timeout, self.source_address)
            ...

드디어 다 왔습니다.

 

결국 HTTP Message를 보내는 순간에 socket의 존재 여부를 확인하고, 없을 경우 self.connect를 실행하여 self.sock을 다시 정의하는것을 볼 수가 있습니다.

 

출처 : Cpython/lib/socket.py#808

def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
                      source_address=None):
    host, port = address
    err = None
    for res in getaddrinfo(host, port, 0, SOCK_STREAM):
        af, socktype, proto, canonname, sa = res
        sock = None
        try:        
        #=================실제 socket이 만들어 지는 부분========================#
            sock = socket(af, socktype, proto)
            if timeout is not _GLOBAL_DEFAULT_TIMEOUT:
                sock.settimeout(timeout)
            if source_address:
                sock.bind(source_address)
            sock.connect(sa)
            # Break explicitly a reference cycle
            err = None
            return sock
        #====만들어진 socket을 target Address에다 Bind후 Socket OBject를 반환===#

        except error as _:
            err = _
            if sock is not None:
                sock.close()

    if err is not None:
        try:
            raise err
        finally:
            # Break explicitly a reference cycle
            err = None
    else:
        raise error("getaddrinfo returns an empty list")

결국, 내부에서 socket을 만들고, 이 socket을 반환한 다음 외부 url과 HTTP Message를 주고받는 것을 확인할 수 있습니다!

 

이제, 두개의 포스팅에 걸친 과정을 정리해보면 아래와 같습니다.

Requests.get에서 request()에 Msg 전달
-> Requests.request에서 Requests.Session Object에 Msg 전달
    -> Requests.Session.prepare_request에 Msg 전달
        -> PreparedRequest Object 생성 후 Requests.Session로 복귀
    -> Requests.Session.send에 Msg 전달
        -> Requests.Session.get_adapter로 Msg 전달
            -> HTTPAdapter Object를 생성 후 Requests.Session.send로 복귀
    -> HTTPAdapter.send에 Msg 전달
        -> HTTPAdapter.get_connection에 Msg 전달
            -> HTTPAdapter.poolmanager(urllib3.PoolManager Object).connection_from_url에 Msg 전달
                -> HTTPAdapter.poolmanager.connection_from_host에 Msg전달
                    -> HTTPAdapter.poolmanager.connection_from_context에 Msg 전달
                        -> HTTPAdapter.poolmanager.connection_from_pool_key에 Msg 전달
                            -> HTTPAdapter.poolmanager._new_pool에 Msg 전달
                                -> urllib3.HTTPConnectionPool Class Object(=conn) 생성 후 반환
        -> conn Object를 생성 후 HTTPAdapter.send 로 복귀
            -> conn.urlopen에 Msg 전달
                -> conn._make_request에 Msg 전달
                    -> HTTPconnection.request에 Msg 전달
                        -> HTTPconnection.endheaders에 Msg 전달
                            -> HTTPconnection._send_output에 Msg 전달
                                -> HTTPconnection.send에 Msg 전달
                                    -> HTTPconnection.connect에 Msg 전달
                                        -> HTTPconnection._create_connection에 Msg 전달
                                            -> HTTPconnection._create_connection 내부에서 socket 을 생성하고, 외부로 전달 후 통신 진행

결국 복잡한 내부의 과정을 거쳤지만,

 

Socket통신을 하기 전까지 HTTP Message를 정비하고,

 

HTTP Message Header에 명시된 동작(Transfer Encoding, Retry...)을 수행하기 위해서 다양한 작업들이 사전에 수행 되는것을 볼 수 있습니다.

 

그리고, 모든 준비가 완료되었을 때 HTTP Message를 Socket수준의 Low Level 통신을 통해서 외부와 통신을 하는것을 볼 수 있습니다 :)

 

이제 맨 첫 예제의 HTTP Message가 어떻게 이뤄져 있는지 확인해 보겠습니다.

 

requests가 어떻게 동작하는지 알았으니, Cpython의 http.client.py에서 print를 추가하면

 

Bytes형태로 Socket으로 보내지기 직전의 HTTP Message를 볼 수가 있습니다

    def send(self, data):       
        if self.sock is None:
            if self.auto_open:
                self.connect()
            else:
                raise NotConnected()
        #output stream을 추가하여, 소켓으로 보내는 HTTP Message를 확인
        print(data)
import requests
r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
>>b'GET /user HTTP/1.1\r\nHost: api.github.com\r\nUser-Agent: python-requests/2.27.1\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nAuthorization: Basic --------\r\n\r\n'
"""
requests 예제가 실제 동작할때 사용된 HTTP Message
GET /user HTTP/1.1
Host: api.github.com
User-Agent: python-requests/2.27.1
Accept-Encoding: gzip, deflate
Accept: */*
Connection: keep-alive
Authorization: Basic --------
"""



728x90
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함