티스토리 뷰
지난 포스팅을 통해서, Socket Programming을 사용하면 Server<->Client 간 HTTP 통신이 가능한 것을 확인 하였습니다.
https://teus-kiwiee.tistory.com/159
하지만 의문이 듭니다.
"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를 정리하고 실체를 확인해 보도록 하겠습니다.
'Python 잡지식 > 소스코드 톱아보기' 카테고리의 다른 글
[Pandas]inplace=True동작에 대해서 (0) | 2023.12.06 |
---|---|
[Pandas] Series의 구조에 대해서 알아보자2 (1) | 2023.12.06 |
[Pandas] Series의 구조에 대해서 알아보자1 (0) | 2023.12.06 |
[Python]Requests Library의 동작 Part2 (0) | 2022.08.12 |
[CPython]CPython의 List 구현 살펴보기 (0) | 2022.02.16 |
- Total
- Today
- Yesterday
- 병렬처리
- Search알고리즘
- SIMD
- 자료구조
- git
- 이분탐색
- AVX
- 컴퓨터그래픽스
- 분할정복
- 사칙연산
- 프로그래머스
- Sort알고리즘
- prime number
- GDC
- 알고리즘
- hash
- 코딩테스트
- 완전탐색 알고리즘
- Python
- 동적계획법
- heap
- Greedy알고리즘
- C++
- stack
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |