파이썬 프록시 서버란 무엇인가요?

Python 프록시 서버는 Python 코드를 통해 방대한 IP 네트워크로 HTTP/S 요청을 라우팅할 수 있게 합니다. IP 로테이션, 세션 지속성, 지리적 위치 타겟팅 등의 기능을 지원합니다.
4 분 읽기
Python Proxy Server illustration with laptop and servers.

이 튜토리얼에서는 다음을 배웁니다:

  • 파이썬 프록시 서버의 정의와 작동 방식
  • 파이썬으로 HTTP 프록시 서버를 구축하는 데 필요한 단계
  • 이 접근법의 장단점

자, 시작해 보겠습니다!

파이썬 프록시 서버란 무엇인가요?

파이썬 프록시 서버는 클라이언트와 인터넷 사이의 중개자 역할을 하는 파이썬 애플리케이션입니다. 클라이언트의 요청을 가로채 대상 서버로 전달하고, 응답을 클라이언트에게 다시 보냅니다. 이를 통해 대상 서버에 클라이언트의 신원을 숨깁니다.

프록시 서버의 개념과 작동 방식을 자세히 알아보려면 저희 글을 읽어보세요.

파이썬의 소켓 프로그래밍 기능 덕분에 기본 프록시 서버 구현이 쉬워져 사용자가 네트워크 트래픽을 검사, 수정 또는 리디렉션할 수 있습니다. 프록시 서버는 웹 스크래핑 시 캐싱, 성능 향상, 보안 강화에 탁월합니다.

파이썬으로 HTTP 프록시 서버 구현하기

아래 단계를 따라 Python 프록시 서버 스크립트를 구축하는 방법을 알아보세요.

1단계: Python 프로젝트 초기화

시작하기 전에 컴퓨터에 Python 3 이상이 설치되어 있는지 확인하세요. 설치되어 있지 않다면 설치 프로그램을 다운로드하여 실행하고 설치 마법사를 따르세요.

다음으로 아래 명령어를 사용하여 python-http-proxy-server 폴더를 생성하고 가상 환경이 포함된 Python 프로젝트를 초기화하세요:

mkdir python-http-proxy-server

cd python-http-proxy-server

python -m venv env

Python IDE에서 python-http-proxy-server 폴더를 열고 빈 proxy_server.py 파일을 생성하세요.

자, 이제 Python으로 HTTP 프록시 서버를 구축하는 데 필요한 모든 준비가 완료되었습니다.

2단계: 수신 소켓 초기화

먼저, 들어오는 요청을 수락하기 위한 웹 소켓 서버를 생성해야 합니다. 소켓 개념에 익숙하지 않다면, 소켓은 클라이언트와 서버 간 양방향 데이터 흐름을 가능하게 하는 저수준 프로그래밍 추상화입니다. 웹 서버 환경에서 서버 소켓은 클라이언트의 들어오는 연결을 수신 대기하는 데 사용됩니다.

Python에서 소켓 기반 웹 서버를 생성하려면 다음 코드를 사용하세요:

port = 8888
# 프록시 서버를 특정 주소와 포트에 바인딩
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 최대 10개의 동시 연결 허용
server.bind(('127.0.0.1', port))
server.listen(10)

이 코드는 수신 소켓 서버를 초기화하고 로컬 주소(http://127.0.0.1:8888)에 바인딩합니다. 이후 listen() 메서드를 통해 서버가 연결을 수락할 수 있도록 합니다.

참고: 웹 프록시가 수신 대기할 포트 번호는 자유롭게 변경할 수 있습니다. 최대의 유연성을 위해 해당 정보를 명령줄에서 읽도록 스크립트를 수정할 수도 있습니다.

socket은 Python 표준 라이브러리에서 제공됩니다. 따라서 스크립트 상단에 다음과 같은 import 문이 필요합니다:

import socket

파이썬 프록시 서버가 정상적으로 시작되었는지 확인하려면 다음 메시지를 기록하세요:

 print(f"프록시 서버가 포트 {port}에서 대기 중...")

3단계: 클라이언트 요청 수락

클라이언트가 프록시 서버에 연결하면, 해당 클라이언트와의 통신을 처리하기 위해 새 소켓을 생성해야 합니다. Python에서 이를 구현하는 방법은 다음과 같습니다:

# 들어오는 요청 수신 대기

while True:

    client_socket, addr = server.accept()

    print(f"{addr[0]}:{addr[1]}에서 연결 수락됨")

    # 클라이언트 요청 처리용 스레드 생성

    client_handler = threading.Thread(target=handle_client_request, args=(client_socket,))

    client_handler.start()

여러 클라이언트 요청을 동시에 처리하려면 위와 같이 멀티스레딩을 사용해야 합니다. Python 표준 라이브러리에서 threading을 임포트하는 것을 잊지 마세요:

import threading

보시다시피 프록시 서버는 사용자 정의 handle_client_request() 함수를 통해 들어오는 요청을 처리합니다. 다음 단계에서 이 함수가 어떻게 정의되는지 살펴보겠습니다.

4단계: 들어오는 요청 처리

클라이언트 소켓이 생성되면 이를 사용하여 다음 작업을 수행해야 합니다:

  1. 들어오는 요청의 데이터를 읽습니다.
  2. 해당 데이터에서 대상 서버의 호스트와 포트를 추출합니다.
  3. 해당 정보를 사용하여 클라이언트 요청을 대상 서버로 전달합니다.
  4. 응답을 수신하여 원본 클라이언트로 전달합니다.

이 섹션에서는 처음 두 단계에 집중하겠습니다. handle_client_request() 함수를 정의하고 이를 사용하여 들어오는 요청의 데이터를 읽습니다:

def handle_client_request(client_socket):

    print("Received request:n")

    # 클라이언트가 요청에서 보낸 데이터 읽기

    request = b''

    client_socket.setblocking(False)

    while True:

        try:

            # 웹 서버로부터 데이터 수신

            data = client_socket.recv(1024)

            request = request + data

            # 원래 대상 서버로부터 데이터 수신

            print(f"{data.decode('utf-8')}")

        except:

            break

setblocking(False) 는 클라이언트 소켓을 비차단 모드로 설정합니다. 그런 다음 recv() 를 사용하여 들어오는 데이터를 읽고 바이트 형식으로 request에 추가합니다. 들어오는 요청 데이터의 크기를 알 수 없으므로 한 번에 한 덩어리씩 읽어야 합니다. 이 경우 1024바이트 크기의 청크가 지정되었습니다. 비차단 모드에서 recv()가 데이터를 찾지 못하면 오류 예외를 발생시킵니다. 따라서 except 문은 작업의 종료를 표시합니다.

파이썬 프록시 서버의 동작을 추적하려면 기록된 메시지를 확인하세요.

들어오는 요청을 가져온 후, 대상 서버의 호스트와 포트를 추출해야 합니다:

host, port = extract_host_port_from_request(request)

특히, extract_host_port_from_request() 함수는 다음과 같습니다:

def extract_host_port_from_request(request):

    # "Host:" 문자열 뒤의 값을 가져옴

    host_string_start = request.find(b'Host: ') + len(b'Host: ')

    host_string_end = request.find(b'rn', host_string_start)

    host_string = request[host_string_start:host_string_end].decode('utf-8')

    webserver_pos = host_string.find("/")

    if webserver_pos == -1:

        webserver_pos = len(host_string)

    # 특정 포트가 지정된 경우

    port_pos = host_string.find(":")

    # 포트가 지정되지 않은 경우

    if port_pos == -1 or webserver_pos < port_pos:

        # 기본 포트

        port = 80

        host = host_string[:webserver_pos]

    else:

        # 호스트 문자열에서 특정 포트 추출

        port = int((host_string[(port_pos + 1):])[:webserver_pos - port_pos - 1])

        host = host_string[:port_pos]

    호스트, 포트를 반환

이 함수의 동작을 더 잘 이해하려면 아래 예시를 참고하세요. 일반적으로 들어오는 요청의 인코딩된 문자열은 다음과 같은 내용을 포함합니다:

GET http://example.com/your-page HTTP/1.1

Host: example.com

User-Agent: curl/8.4.0

Accept: */*

Proxy-Connection: Keep-Alive

extract_host_port_from_request()는 “Host:” 필드에서 웹 서버의 호스트와 포트를 추출합니다. 이 경우 호스트는 example.com이고 포트(특정 포트가 지정되지 않았으므로)는 80입니다.

5단계: 클라이언트 요청 전달 및 응답 처리

대상 호스트와 포트를 확보했으므로 이제 클라이언트 요청을 목적지 서버로 전달해야 합니다. handle_client_request()에서 새 웹 소켓을 생성하고 이를 통해 원본 요청을 원하는 목적지로 전송합니다:

# 원래 대상 서버에 연결할 소켓 생성

destination_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 대상 서버에 연결

destination_socket.connect((host, port))

# 원래 요청 전송

destination_socket.sendall(request)

그런 다음 서버 응답을 수신할 준비를 하고 이를 원래 클라이언트에 전달합니다:

# 서버로부터 수신된 데이터를 읽습니다

# 한 번에 한 청크씩 읽고 클라이언트에 전송합니다

print("응답 수신:n")

while True:

    # 웹 서버로부터 데이터 수신

    data = destination_socket.recv(1024)

    # 원래 대상 서버로부터 데이터 수신

    print(f"{data.decode('utf-8')}")

    # 전송할 데이터가 더 이상 없음

    if len(data) > 0:

        # 클라이언트로 다시 전송

        client_socket.sendall(data)

    else:

        break

반복해서 말씀드리지만, 응답 크기를 알 수 없으므로 한 번에 한 덩어리씩 처리해야 합니다. 데이터가 비어 있으면 더 이상 수신할 데이터가 없으므로 작업을 종료할 수 있습니다.

함수 내에서 정의한 두 소켓을 닫는 것을 잊지 마십시오:

# 소켓 닫기

destination_socket.close()

client_socket.close()

훌륭합니다! 방금 Python으로 HTTP 프록시 서버를 만들었습니다. 전체 코드를 확인하고 실행하여 예상대로 작동하는지 검증해 볼 시간입니다!

6단계: 모든 것 통합하기

다음은 Python 프록시 서버 스크립트의 최종 코드입니다:

import socket

import threading

def handle_client_request(client_socket):

    print("Received request:n")

    # 클라이언트가 요청으로 보낸 데이터 읽기

    request = b''

    client_socket.setblocking(False)

    while True:

        try:

            # 웹 서버로부터 데이터 수신

            data = client_socket.recv(1024)

            request = request + data

            # 원래 목적지 서버로부터 데이터 수신

            print(f"{data.decode('utf-8')}")

        except:

            break

    # 요청에서 웹 서버의 호스트와 포트 추출

    host, port = extract_host_port_from_request(request)

    # 원래 목적지 서버에 연결할 소켓 생성

    destination_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 목적지 서버에 연결

    destination_socket.connect((host, port))

    # 원래 요청 전송

    destination_socket.sendall(request)

    # 서버에서 수신된 데이터 읽기

    # 한 번에 한 덩어리씩 읽고 클라이언트에 전송

    print("응답 수신:n")

    while True:

        # 웹 서버로부터 데이터 수신

        data = destination_socket.recv(1024)

        # 원래 대상 서버로부터 데이터 수신

        print(f"{data.decode('utf-8')}")

        # 전송할 데이터 없음

        if len(data) > 0:

            # 클라이언트에게 다시 전송

            client_socket.sendall(data)

        else:

            break

    # 소켓 닫기

    destination_socket.close()

    client_socket.close()

def extract_host_port_from_request(request):

    # "Host:" 문자열 뒤의 값 가져오기

    host_string_start = request.find(b'Host: ') + len(b'Host: ')

    host_string_end = request.find(b'rn', host_string_start)

    host_string = request[host_string_start:host_string_end].decode('utf-8')

    webserver_pos = host_string.find("/")

    if webserver_pos == -1:

        webserver_pos = len(host_string)

    # 특정 포트가 지정된 경우

    port_pos = host_string.find(":")

    # 포트가 지정되지 않은 경우

    if port_pos == -1 or webserver_pos < port_pos:

        # 기본 포트

        port = 80

        host = host_string[:webserver_pos]

    else:

        # 호스트 문자열에서 특정 포트 추출

        port = int((host_string[(port_pos + 1):])[:webserver_pos - port_pos - 1])

        host = host_string[:port_pos]

    return host, port

def start_proxy_server():

    port = 8888

    # 프록시 서버를 특정 주소와 포트에 바인딩

    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server.bind(('127.0.0.1', port))

    # 최대 10개의 동시 연결 허용

    server.listen(10)

    print(f"프록시 서버가 포트 {port}에서 대기 중...")

    # 들어오는 요청 수신 대기

    while True:

        client_socket, addr = server.accept()

        print(f"{addr[0]}:{addr[1]}에서 연결 수락됨")

        # 클라이언트 요청 처리용 스레드 생성

        client_handler = threading.Thread(target=handle_client_request, args=(client_socket,))

        client_handler.start()

if __name__ == "__main__":

    start_proxy_server()

다음 명령어로 실행하세요:

python proxy_server.py

터미널에 다음과 같은 메시지가 표시됩니다:

Proxy server listening on port 8888...

서버 작동 여부를 확인하려면 cURL로 프록시 요청을 실행하세요. 프록시와 함께 cURL 사용 방법에 대한 자세한 내용은 가이드를 참조하세요.

새 터미널을 열고 다음을 실행하세요:

curl --proxy "http://127.0.0.1:8888" "http://httpbin.org/ip"

이 명령은 http://127.0.0.1:8888 프록시 서버를 통해 http://httpbin.org/ip 목적지로 GET 요청을 수행합니다.

다음과 같은 결과가 표시됩니다:

{

  "origin": "45.12.80.183"

}

이는 프록시 서버의 IP 주소입니다. 왜냐하면 HTTPBin 프로젝트의 /ip 엔드포인트는 요청이 발생한 IP를 반환하기 때문입니다. 서버를 로컬에서 실행 중이라면 “origin”은 사용자의 IP와 일치할 것입니다.

참고: 여기서 구축한 Python 프록시 서버는 HTTP 대상만 지원합니다. HTTPS 연결을 처리하도록 확장하는 것은 상당히 까다롭습니다.

이제 프록시 서버 Python 애플리케이션이 작성한 로그를 살펴보세요. 다음과 같은 내용이 포함되어 있어야 합니다:

수신 요청:

GET http://httpbin.org/ip HTTP/1.1

Host: httpbin.org

User-Agent: curl/8.4.0

Accept: */*

Proxy-Connection: Keep-Alive

수신 응답:

HTTP/1.1 200 OK

Date: Thu, 14 Dec 2023 14:02:08 GMT

Content-Type: application/json

Content-Length: 31

Connection: keep-alive

Server: gunicorn/19.9.0

Access-Control-Allow-Origin: *

Access-Control-Allow-Credentials: true

{

  "origin": "45.12.80.183"

}

이는 프록시 서버가 HTTP 프로토콜에서 지정된 형식으로 요청을 수신했음을 알려줍니다. 그런 다음 해당 요청을 대상 서버로 전달하고, 응답 데이터를 기록한 후 클라이언트에게 응답을 다시 보냈습니다. 왜 그렇게 확신할 수 있을까요? “origin”의 IP 주소가 동일하기 때문입니다!

축하합니다! 방금 파이썬으로 HTTP 프록시 서버를 구축하는 방법을 배웠습니다!

사용자 정의 Python 프록시 서버 사용의 장단점

이제 파이썬으로 프록시 서버를 구현하는 방법을 알았으니, 이 접근법의 장점과 한계를 살펴볼 준비가 되었습니다.

장점:

  • 완전한 통제: 이와 같은 맞춤형 Python 스크립트를사용하면 프록시 서버의 동작을 완전히 통제할 수 있습니다. 의심스러운 활동이나 데이터 유출이 없습니다!
  • 사용자 정의: 성능 향상을 위해 요청 로깅 및 캐싱과 같은 유용한 기능을 프록시 서버에 추가할 수 있습니다.

단점:

  • 인프라 비용: 프록시 서버 아키텍처를구축하는 것은 쉽지 않으며 하드웨어나 VPS 서비스 측면에서 많은 비용이 듭니다.
  • 유지 관리의 어려움: 프록시 아키텍처, 특히 확장성과 가용성을 유지 관리할 책임이있습니다 . 이는 경험 많은 시스템 관리자만이 처리할 수 있는 작업입니다.
  • 신뢰성 부족: 이 솔루션의 주요 문제는 프록시 서버의 출구 IP가 절대 변경되지 않는다는 점입니다. 결과적으로 봇 방지 기술이 해당 IP를 차단하여 서버가 원하는 요청에 접근하지 못하게 할 수 있습니다. 즉, 프록시는 결국 작동하지 않게 됩니다.

이러한 한계와 단점들로 인해 실제 운영 환경에서 맞춤형 Python 프록시 서버를 사용하는 것은 바람직하지 않습니다. 해결책은? Bright Data와 같은 신뢰할 수 있는 프록시 제공업체를 이용하는 것입니다! 계정을 생성하고, 본인 인증을 완료한 후, 무료 프록시를 받아 선호하는 프로그래밍 언어로 사용하세요. 예를 들어, Python 스크립트에 requests 모듈을 활용해 프록시를 통합할 수 있습니다 .

우리의 방대한 프록시 네트워크는 전 세계 수백만 개의 빠르고 안정적이며 안전한 프록시 서버로 구성됩니다. 우리가 최고의 프록시 서버 제공업체인 이유를 확인해 보세요.

결론

이 가이드에서는 프록시 서버가 무엇이며 Python에서 어떻게 작동하는지 배웠습니다. 구체적으로 웹 소켓을 사용하여 처음부터 프록시 서버를 구축하는 방법을 익혔습니다. 이제 여러분은 Python 프록시의 전문가가 되었습니다. 이 접근법의 주요 문제점은 프록시 서버의 고정된 출구 IP가 결국 차단될 수 있다는 점입니다. Bright Data의 회전 프록시로 이를 피하세요!

Bright Data는 포춘 500대 기업 및 20,000명 이상의 고객에게 서비스를 제공하는 세계 최고의 프록시 서버를 운영합니다. 다양한 프록시 유형을 제공합니다:

이 안정적이고 빠르며 글로벌한 프록시 네트워크는 또한 어떤 사이트에서든 손쉽게 데이터를 추출할 수 있는 다양한 웹 스크래핑 서비스의 기반이 됩니다.