티스토리 뷰

728x90
반응형

지난 포스팅을 통해서, Socket Programming을 사용하면 Server<->Client 간 HTTP 통신이 가능한 것을 확인 하였습니다.

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

 

Socket을 이용한 Low Level HTTP 통신

요즘은 많은 분들이 Django나 Flask, Spring등의 웹프래임워크를 사용해서 손쉽게 Web App을 제작할 수 있습니다. 일반적으로 이 프래임워크 내에서는 Requests라는 라이브러리를 지원하며, 이 라이브러

teus-kiwiee.tistory.com

 

하지만 의문이 듭니다.

 

"HTTP Message를 Socket으로 보낼 수는 있지만, 라이브러리도 정말 Socket을 이용하나?"

 

Python의 대표 RestAPI Library인 Requests Library 내부의 동작을 확인한다면

 

실제 사용중인 패키지 역시 Socket Programming을 통해서 HTTP 통신을 한다는것이 입증됩니다.

(그러면 Flask, Fast API, Django 역시 Socket을 사용한다는 것이 검증되죠!)

 

Requests 패키의 간단한 활용 방법은 아래와 같습니다

출처 : https://requests.readthedocs.io/en/latest/

import requests
r = requests.get('https://api.github.com/user', auth=('user', 'pass'))
r.status_code
>>200
r.headers['content-type']
>>'application/json; charset=utf8'
r.encoding
>>'utf-8'
r.text
>>'{"type":"User"...'
r.json()
>>{'private_gists': 419, 'total_private_repos': 77, ...}

그렇다면, 이제 Requests Library의 소스코드로 들어가 보겠습니다.

출처 : requests/requests/api.py#14

from . import sessions
def get(url, params=None, **kwargs):
    return request("get", url, params=params, **kwargs)
    
def request(method, url, **kwargs):    
    with sessions.Session() as session:
        return session.request(method=method, url=url, **kwargs)

일단 소스코드 초입에서, requests.get은 request 함수를 부르고, 이 함수 내부에서 requests 패키지의 session을 import 해서 사용하는것을 볼 수 있습니다.

 

결국 **session.request()**내부에서 무엇이 일어나고, 어떤걸 반환하는지 알면 되는 간단한 내용일것 같습니다.

 

출처 : requests/requests/sessions.py#355

class Session(SessionRedirectMixin):   

    def __init__(self):
        self.headers = default_headers()
        self.auth = None
        self.proxies = {}
        self.hooks = default_hooks()
        self.params = {}
        self.stream = False
        self.verify = True
        self.cert = None
        self.max_redirects = DEFAULT_REDIRECT_LIMIT
        self.trust_env = True
        self.cookies = cookiejar_from_dict({})
        self.adapters = OrderedDict()
        self.mount("https://", HTTPAdapter())
        self.mount("http://", HTTPAdapter())

    def __enter__(self):
        return self

    def __exit__(self, *args):
        self.close()
        
    def prepare_request(self, request):        
        cookies = request.cookies or {}
        # Bootstrap CookieJar.
        if not isinstance(cookies, cookielib.CookieJar):
            cookies = cookiejar_from_dict(cookies)
        # Merge with session cookies
        merged_cookies = merge_cookies(
            merge_cookies(RequestsCookieJar(), self.cookies), cookies
        )
        # Set environment's basic authentication if not explicitly set.
        auth = request.auth
        if self.trust_env and not auth and not self.auth:
            auth = get_netrc_auth(request.url)
        p = PreparedRequest()
        p.prepare(
            method=request.method.upper(),
            url=request.url,
            files=request.files,
            data=request.data,
            json=request.json,
            headers=merge_setting(
                request.headers, self.headers, dict_class=CaseInsensitiveDict
            ),
            params=merge_setting(request.params, self.params),
            auth=merge_setting(auth, self.auth),
            cookies=merged_cookies,
            hooks=merge_hooks(request.hooks, self.hooks),
        )
        return p


    def request(
        self,
        method,
        url,
        params=None,
        data=None,
        headers=None,
        cookies=None,
        files=None,
        auth=None,
        timeout=None,
        allow_redirects=True,
        proxies=None,
        hooks=None,
        stream=None,
        verify=None,
        cert=None,
        json=None,
    ):        
        req = Request(
            method=method.upper(),
            url=url,
            headers=headers,
            files=files,
            #data가 []였으면 data = {}가됨
            data=data or {},
            json=json,
            params=params or {},
            auth=auth,
            cookies=cookies,
            hooks=hooks,
        )
        prep = self.prepare_request(req)
        proxies = proxies or {}
        settings = self.merge_environment_settings(
            prep.url, proxies, stream, verify, cert
        )
        # Send the request.
        send_kwargs = {
            "timeout": timeout,
            "allow_redirects": allow_redirects,
        }
        send_kwargs.update(settings)
        resp = self.send(prep, **send_kwargs)
        return resp

디폴트 생성자를 이용해서 Session 객체를 만들고, Session의 __enter__에서 특별한 동작 없이 self를 주는것을 확인할 수 있습니다.

 

그다음, 해당 객체의 request Method를 사용하게 되는데, 이때 Request Class의 Object를 만드는 것을 볼 수 있습니다

 

출처 : requests/requests/models.py#230

class Request(RequestHooksMixin):
    def __init__(
        self,
        method=None,
        url=None,
        headers=None,
        files=None,
        data=None,
        params=None,
        auth=None,
        cookies=None,
        hooks=None,
        json=None,
    ):

        # Default empty dicts for dict params.
        data = [] if data is None else data
        files = [] if files is None else files
        headers = {} if headers is None else headers
        params = {} if params is None else params
        hooks = {} if hooks is None else hooks

        self.hooks = default_hooks()
        for (k, v) in list(hooks.items()):
            self.register_hook(event=k, hook=v)

        self.method = method
        self.url = url
        self.headers = headers
        self.files = files
        self.data = data
        self.json = json
        self.params = params
        self.auth = auth
        self.cookies = cookies

굉장히 감싸고, 감싸지고 있지만, 이 모두 HTTP Message를 처리하기 위한 Context를 만든는 과정 및 전처리 과정이라고 볼 수 있습니다.

 

매개변수를 전달하고, Request Object를 사용해서 Session Class prepare_request Method Request Object를 매개변수로 넣어줍니다.

 

prepare_request 내에서는 기본적으로 cookie에 대한 사전 설정을 하고, 인증관련 일부 전처리가 진행 됩니다. 그 이후 PreparedRequest Class Object를 생성하고, 이 객체의 독특한 초기화가 이뤄집니다.

(생성자와 함께 attribute가 설정되는 것이 아니라, 별도의 prepare를 통해서 값을 초기화 합니다)

 

출처 : requests/requests/models.py#314

class PreparedRequest(RequestEncodingMixin, RequestHooksMixin):
    def __init__(self):
        self.method = None
        self.url = None
        self.headers = None
        self._cookies = None
        self.body = None
        self.hooks = default_hooks()
        self._body_position = None

    def prepare(
        self,
        method=None,
        url=None,
        headers=None,
        files=None,
        data=None,
        params=None,
        auth=None,
        cookies=None,
        hooks=None,
        json=None,
    ):
        self.prepare_method(method)
        self.prepare_url(url, params)
        self.prepare_headers(headers)
        self.prepare_cookies(cookies)
        self.prepare_body(data, files, json)
        self.prepare_auth(auth, url)
        self.prepare_hooks(hooks)

(사실 개인적인 생각으로, 바로 __init__에서 prepare를 실행해도 이상은 없을거락 생각합니다)

위 과정을 통해서, HTTP Request Message에 필요한 Component들이 준비되는걸 알 수 있습니다.

 

url은 python의 bytes -> str로 변경 후 unicode로 미리 정해주고, 이를 parse_url을 활용해서 쪼개줍니다. 그 이후 scheme, auth, host, port, path, query, fragment의 string pattern을 검사한 다음, 다시 합치는 방법을 사용합니다.

 

header의 경우 이 단계에서는 Python dict형태로 저장합니다

 

body의 경우 json일 경우 json을 dump한 다음 utf-8 형태로 encode하고, 그 이외에도 유사하에 encoding 된 body를 준비하는 것을 볼 수 있습니다.

 

출처 : requests/requests/models.py#495

    def prepare_body(self, data, files, json=None):
        body = None
        content_type = None

        if not data and json is not None:
            content_type = "application/json"
            try:
                body = complexjson.dumps(json, allow_nan=False)
            except ValueError as ve:
                raise InvalidJSONError(ve, request=self)
            if not isinstance(body, bytes):
                body = body.encode("utf-8")

        is_stream = all(
            [
                hasattr(data, "__iter__"),
                not isinstance(data, (basestring, list, tuple, Mapping)),
            ]
        )

        if is_stream:
            try:
                length = super_len(data)
            except (TypeError, AttributeError, UnsupportedOperation):
                length = None

            body = data

            if getattr(body, "tell", None) is not None:
                try:
                    self._body_position = body.tell()
                except OSError:
                    self._body_position = object()

            if files:
                raise NotImplementedError(
                    "Streamed bodies and files are mutually exclusive."
                )

            if length:
                self.headers["Content-Length"] = builtin_str(length)
            else:
                self.headers["Transfer-Encoding"] = "chunked"
        else:
            # Multi-part file uploads.
            if files:
                (body, content_type) = self._encode_files(files, data)
            else:
                if data:
                    body = self._encode_params(data)
                    if isinstance(data, basestring) or hasattr(data, "read"):
                        content_type = None
                    else:
                        content_type = "application/x-www-form-urlencoded"

            self.prepare_content_length(body)
            if content_type and ("content-type" not in self.headers):
                self.headers["Content-Type"] = content_type
        self.body = body

이 과정에서 흥미로운점은 'HTTP 완벽가이드'에서 볼 수 있는 Transfer-Encoding이나 multipart file uploads가 if문을 통해서 사전에 준비된다는 점 정도가 있습니다.

 

여튼, 이제 준비가된 context와 send시 사용할 args를 정리한 다음, Session class send method를 통해서 http message를 보냅니다.

 

출처 : requests/requests/sessions.py#671

    def send(self, request, **kwargs):        
        kwargs.setdefault("stream", self.stream)
        kwargs.setdefault("verify", self.verify)
        kwargs.setdefault("cert", self.cert)
        if "proxies" not in kwargs:
            kwargs["proxies"] = resolve_proxies(request, self.proxies, self.trust_env)

        if isinstance(request, Request):
            raise ValueError("You can only send PreparedRequests.")

        allow_redirects = kwargs.pop("allow_redirects", True)
        stream = kwargs.get("stream")
        hooks = request.hooks

        # Get the appropriate adapter to use
        adapter = self.get_adapter(url=request.url)

        # Start time (approximately) of the request
        start = preferred_clock()

        # Send the request
        r = adapter.send(request, **kwargs)

        # Total elapsed time of the request (approximately)
        elapsed = preferred_clock() - start
        r.elapsed = timedelta(seconds=elapsed)

        # Response manipulation hooks
        r = dispatch_hook("response", hooks, r, **kwargs)

        # Persist cookies
        if r.history:

            # If the hooks create history then we want those cookies too
            for resp in r.history:
                extract_cookies_to_jar(self.cookies, resp.request, resp.raw)

        extract_cookies_to_jar(self.cookies, request, r.raw)

        # Resolve redirects if allowed.
        if allow_redirects:
            # Redirect resolving generator.
            gen = self.resolve_redirects(r, request, **kwargs)
            history = [resp for resp in gen]
        else:
            history = []

        # Shuffle things around if there's history.
        if history:
            # Insert the first (original) request at the start
            history.insert(0, r)
            # Get the last request made
            r = history.pop()
            r.history = history

        # If redirects aren't being followed, store the response on the Request for Response.next().
        if not allow_redirects:
            try:
                r._next = next(
                    self.resolve_redirects(r, request, yield_requests=True, **kwargs)
                )
            except StopIteration:
                pass

        if not stream:
            r.content

        return r

send method에서 context의 일부 key값들을 점검 및 update하고, redirect관련 전처리를 하면

 

adapter = self.get_adapter(url=request.url)

...

r = adapter.send(request, **kwargs)

 

부분에서 adapter 객체를 돌려받고, 이 객체의 send를 통해서 받았던 매개변수들을 그대로 넘겨주는것을 볼 수가 있습니다.

 

출처 : requests/requests/sessions.py#780

    self.adapters = OrderedDict()
    self.mount("https://", HTTPAdapter())
    self.mount("http://", HTTPAdapter())
    
    def get_adapter(self, url):
        '''
        :rtype: requests.adapters.BaseAdapter
        '''
        for (prefix, adapter) in self.adapters.items():
            if url.lower().startswith(prefix.lower()):
                return adapter
        raise InvalidSchema(f"No connection adapters were found for {url!r}")
        
    def mount(self, prefix, adapter):
        self.adapters[prefix] = adapter
        keys_to_move = [k for k in self.adapters if len(k) < len(prefix)]

        for key in keys_to_move:
            self.adapters[key] = self.adapters.pop(key)

Session Class의 생성자 부분에서 adaptets를 선언하고, 이 adapters를 key:value 형태로 가능한 adapter를 등록한 것을 알 수 있습니다.

 

그럼 이제, HTTPAdapter Class가 뭔지를 알아야 합니다.

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

from urllib3.poolmanager import PoolManager, proxy_from_url

class HTTPAdapter(BaseAdapter):
    __attrs__ = [
        "max_retries",
        "config",
        "_pool_connections",
        "_pool_maxsize",
        "_pool_block",
    ]

    def __init__(
        self,
        pool_connections=DEFAULT_POOLSIZE,
        pool_maxsize=DEFAULT_POOLSIZE,
        max_retries=DEFAULT_RETRIES,
        pool_block=DEFAULT_POOLBLOCK,
    ):
        if max_retries == DEFAULT_RETRIES:
            self.max_retries = Retry(0, read=False)
        else:
            self.max_retries = Retry.from_int(max_retries)
        self.config = {}
        self.proxy_manager = {}

        super().__init__()

        self._pool_connections = pool_connections
        self._pool_maxsize = pool_maxsize
        self._pool_block = pool_block

        self.init_poolmanager(pool_connections, pool_maxsize, block=pool_block)

        
    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,
        )

HTTPAdapter Class Object는 시스템의 설정에 따라 http connection pool을 만들어서 관리하는 object라고 추측해 볼 수 있습니다.

 

일단 여기서 HTTPAdapter Object가 생성되고, 이 Object의 send method를 통해서 HTTP Message가 보내지는걸 예상해볼 수 있습니다.

 

문제는 PoolManager부터는 Requests Package가 아니라 urllib3으로 넘어갑니다.

 

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

    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 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)

        try:
            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,
                )

            # 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,
                    )
                except Exception:
                    # If we hit any problems here, clean up the connection.
                    # Then, raise so that we can handle the actual exception.
                    low_conn.close()
                    raise

        except (ProtocolError, OSError) as err:
            raise ConnectionError(err, request=request)

        except MaxRetryError as e:
            if isinstance(e.reason, ConnectTimeoutError):
                # TODO: Remove this in 3.0.0: see #2811
                if not isinstance(e.reason, NewConnectionError):
                    raise ConnectTimeout(e, request=request)

            if isinstance(e.reason, ResponseError):
                raise RetryError(e, request=request)

            if isinstance(e.reason, _ProxyError):
                raise ProxyError(e, request=request)

            if isinstance(e.reason, _SSLError):
                # This branch is for urllib3 v1.22 and later.
                raise SSLError(e, request=request)

            raise ConnectionError(e, request=request)

        except ClosedPoolError as e:
            raise ConnectionError(e, request=request)

        except _ProxyError as e:
            raise ProxyError(e)

        except (_SSLError, _HTTPError) as e:
            if isinstance(e, _SSLError):
                # This branch is for urllib3 versions earlier than v1.22
                raise SSLError(e, request=request)
            elif isinstance(e, ReadTimeoutError):
                raise ReadTimeout(e, request=request)
            elif isinstance(e, _InvalidHeader):
                raise InvalidHeader(e, request=request)
            else:
                raise

        return self.build_response(request, resp)

send 역시 get_connection() method로 부터 conn을 받아와야 하고, 이 conn은 Poolmanger 역시 urllib3에서 존재하는 Class 입니다.

 

따라서, 여기까지 Part1 끊고, Part 2에서는 python의 내장 패키지인 urllib와 urllib3까지 확인해볼 예정입니다.

 

전체적으로 Requests가 보내지고, Response를 받는지 Flow를 정리하고 실체를 확인해 보도록 하겠습니다.



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
글 보관함