데보션앱 소개페이지 바로가기
로그인 선택

신고하기

CLOSE
신고사유 (대표 사유 1개)
상세내용 (선택)
0/200
  • 신고한 게시글은 더 이상 보이지 않습니다.
  • 이용약관과 운영정책에 따라 신고사유에 해당하는지 검토 후 조치됩니다.
  • 허위 신고인 경우, 신고자의 서비스 이용이 제한될 수 있으니 유의하시어 신중하게 신고해 주세요.
(이 회원이 작성한 모든 댓글과 커뮤니티 게시물이 보이지 않고, 알림도 오지 않습니다.)

미리보기

커뮤니티

      1,234

      badge 23.06.15

      글 등록

      카테고리를 선택해주세요.

      DEVOTEE를 활성화 시키면
      지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.

      버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.

      임시저장함에 저장되었습니다. 저장일시 : 2022.5.17 14:29:08

      임시저장함

      제목을 선택하시면 이어서 작성이 가능하며,
      최대 20건까지 저장합니다.
      컨텐츠 유형, 제목, 저장일시, 삭제로 이뤄진 임시저장 목록
      컨텐츠 유형 제목 저장일 삭제

      데보션 블로그 게재 요청

      CLOSE
      • *
      • *

      본인인증

      효율적인 데보션 서비스 이용 및
      고객님의 소중한 개인정보보호를 위해
      본인인증을 진행해주세요. 본인인증 미 진행 시 로그인이 제한됩니다.
      본인인증 실패

      본인인증 로그인에 실패하였습니다.
      회원이 아니시거나 본인인증 등록이
      완료되지 않은 사용자입니다.

      회원정보 연결

      [네트워크] Python의 Requests Library의 동작 Part2

      Teus 22.09.01
      7,190 10 0

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

      http.png


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

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

      댓글 0

      DEVOTEE를 활성화 시키면
      지금 작성한 댓글에 AI가 댓글을 달아줍니다.

      Teus 님의 최신 블로그

      더보기