socketserver — 네트워크 서버를 위한 프레임워크

socketserver — 네트워크 서버를 위한 프레임워크

socketserver 모듈은 네트워크 서버를 작성하는 작업을 단순화해요.

가용성: WASI 아님. 이 모듈은 WebAssembly에서는 동작하지 않거나 사용할 수 없어요.

기본적인 구체 서버 클래스는 네 가지가 있어요.

  • class socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True) — 클라이언트와 서버 사이에 지속적인 데이터 스트림을 제공하는 인터넷 TCP 프로토콜을 사용해요. bind_and_activate가 참이면 생성자가 자동으로 server_bind()server_activate()를 호출하려 해요. 다른 파라미터들은 BaseServer 기반 클래스에 전달돼요.
  • class socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True) — 순서가 뒤바뀌어 도착하거나 전송 중에 손실될 수 있는 개별 정보 패킷인 데이터그램을 사용해요. 파라미터는 TCPServer와 같아요.
  • class socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True) / class socketserver.UnixDatagramServer(server_address, RequestHandlerClass, bind_and_activate=True) — 덜 자주 쓰이는 이 클래스들은 TCP·UDP 클래스들과 비슷하지만 Unix 도메인 소켓을 사용해요. Unix가 아닌 플랫폼에서는 쓸 수 없죠. 파라미터는 TCPServer와 같아요.

이 네 클래스는 요청을 동기적으로 처리해요. 각 요청은 다음 요청을 시작하기 전에 완료되어야 하죠. 각 요청이 계산을 많이 요구하거나, 클라이언트가 처리하기 느린 많은 데이터를 반환해서 완료하는 데 오래 걸린다면 적합하지 않아요. 해결책은 각 요청을 처리하는 별도 프로세스나 스레드를 만드는 거예요. ForkingMixInThreadingMixIn 믹스-인 클래스가 비동기 동작을 지원하는 데 쓰일 수 있어요.

서버를 만드는 데는 몇 단계가 필요해요. 먼저 BaseRequestHandler 클래스를 상속받고 그 handle() 메서드를 오버라이드해 요청 핸들러 클래스를 만들어야 해요. 이 메서드가 들어오는 요청을 처리하겠죠. 둘째, 서버 클래스 중 하나를 인스턴스화해서 서버의 주소와 요청 핸들러 클래스를 전달해요. 서버는 with 문에서 사용하는 것을 권장해요. 그런 다음 서버 객체의 handle_request() 또는 serve_forever() 메서드를 호출해 하나 또는 많은 요청을 처리해요. 마지막으로 server_close()를 호출해 소켓을 닫아요(with 문을 쓰지 않았다면).

ThreadingMixIn에서 상속해 스레드 연결 동작을 쓸 때는 갑작스러운 종료 시 스레드가 어떻게 동작하길 원하는지 명시적으로 선언해야 해요. ThreadingMixIn 클래스는 서버가 스레드 종료를 기다려야 하는지 나타내는 daemon_threads 속성을 정의해요. 스레드가 자율적으로 동작하길 원하면 이 플래그를 명시적으로 설정해야 해요. 기본값은 False로, ThreadingMixIn이 만든 모든 스레드가 종료될 때까지 Python이 빠져나가지 않음을 의미해요.

서버 클래스들은 어떤 네트워크 프로토콜을 쓰든지 같은 외부 메서드와 속성을 가져요.

출처: Python 표준 라이브러리

본문

서버 생성 참고 사항

상속 다이어그램에 다섯 개의 클래스가 있고, 그중 네 개는 네 종류의 동기 서버를 나타내요:

+------------+
| BaseServer |
+------------+
      |
      v
+-----------+        +------------------+
| TCPServer |------->| UnixStreamServer |
+-----------+        +------------------+
      |
      v
+-----------+        +--------------------+
| UDPServer |------->| UnixDatagramServer |
+-----------+        +--------------------+

UnixDatagramServerUDPServer에서 파생된다는 걸 주목하세요(UnixStreamServer가 아니라). IP 서버와 Unix 서버의 유일한 차이는 주소 패밀리예요.

class socketserver.ForkingMixIn / class socketserver.ThreadingMixIn — 이 믹스-인 클래스들로 각 타입 서버의 forking·threading 버전을 만들 수 있어요. 예를 들어 ThreadingUDPServer는 다음과 같이 만들어요:

class ThreadingUDPServer(ThreadingMixIn, UDPServer):
    pass

믹스-인 클래스가 먼저 옵니다. UDPServer에 정의된 메서드를 오버라이드하니까요. 다양한 속성을 설정하는 것도 밑에 깔린 서버 메커니즘의 동작을 바꿔요.

ForkingMixIn과 아래 언급하는 Forking 클래스들은 fork()를 지원하는 POSIX 플랫폼에서만 쓸 수 있어요.

  • block_on_closeForkingMixIn.server_closeblock_on_close 속성이 False가 아닌 한 모든 자식 프로세스가 완료될 때까지 기다려요. ThreadingMixIn.server_closeblock_on_close 속성이 False가 아닌 한 모든 비-데몬 스레드가 완료될 때까지 기다려요.
  • max_childrenForkingMixIn이 한 번에 요청을 처리하기 위해 존재할 자식 프로세스 수를 지정해요. 한도에 도달하면, 한 자식 프로세스가 끝날 때까지 새 요청이 기다려요.
  • daemon_threadsThreadingMixIn에서 ThreadingMixIn.daemon_threadsTrue로 설정해 데몬 스레드를 사용하면 스레드가 완료될 때까지 기다리지 않아요.

버전 3.7에서 변경: ForkingMixIn.server_closeThreadingMixIn.server_close가 이제 모든 자식 프로세스와 비-데몬 스레드가 완료될 때까지 기다림. 3.7 이전 동작을 선택할 수 있는 새 ForkingMixIn.block_on_close 클래스 속성 추가.

class socketserver.ForkingTCPServer, ForkingUDPServer, ThreadingTCPServer, ThreadingUDPServer, ForkingUnixStreamServer, ForkingUnixDatagramServer, ThreadingUnixStreamServer, ThreadingUnixDatagramServer — 이 클래스들은 믹스-인 클래스들로 미리 정의되어 있어요. (버전 3.12에서 추가: ForkingUnixStreamServerForkingUnixDatagramServer.)

서비스를 구현하려면 BaseRequestHandler에서 클래스를 파생하고 그 handle() 메서드를 재정의해야 해요. 그런 다음 서버 클래스 중 하나를 요청 핸들러 클래스와 결합해 서비스의 다양한 버전을 실행할 수 있어요. 요청 핸들러 클래스는 데이터그램 서비스와 스트림 서비스가 달라야 해요. 이것은 StreamRequestHandler 또는 DatagramRequestHandler 핸들러 서브클래스를 써서 숨길 수 있어요.

물론 여러분도 머리를 써야 해요. 예를 들어 서비스가 서로 다른 요청에 의해 수정될 수 있는 상태를 메모리에 담고 있다면 forking 서버는 말이 안 돼요. 자식 프로세스에서의 수정은 부모 프로세스가 유지하고 각 자식에 전달하는 초기 상태에 절대 도달하지 못하니까요. 이런 경우 threading 서버를 쓸 수 있지만, 공유 데이터의 무결성을 보호하기 위해 잠금을 써야 할 거예요.

반대로 모든 데이터가 외부(예: 파일 시스템)에 저장되는 HTTP 서버를 만든다면, 동기 클래스는 한 요청을 처리하는 동안 서비스를 본질적으로 "귀머거리"로 만들 거예요 — 클라이언트가 요청한 모든 데이터를 받기 느리다면 매우 오래일 수도 있죠. 여기선 threading 또는 forking 서버가 적절해요.

어떤 경우에는 요청의 일부를 동기적으로 처리하고, 요청 데이터에 따라 forked 자식에서 처리를 마치는 게 적절할 수 있어요. 동기 서버를 쓰고 요청 핸들러 클래스의 handle() 메서드에서 명시적으로 fork를 하는 방식으로 구현할 수 있어요.

스레드도 fork()도 지원하지 않는 환경(또는 그것들이 너무 비싸거나 서비스에 부적절한 곳)에서 여러 동시 요청을 처리하는 또 다른 접근은 부분 완료된 요청의 명시적 테이블을 유지하고 selectors로 다음에 어떤 요청을 처리할지(또는 새 들어오는 요청을 처리할지)를 결정하는 거예요. 특히 각 클라이언트가 잠재적으로 오래 연결될 수 있는 스트림 서비스에서 중요해요(스레드나 서브프로세스를 쓸 수 없다면).

서버 객체

class socketserver.BaseServer(server_address, RequestHandlerClass) — 이 모듈의 모든 Server 객체의 슈퍼클래스예요. 아래 주어진 인터페이스를 정의하지만 대부분의 메서드는 구현하지 않고 서브클래스에서 해요. 두 파라미터는 각각 server_addressRequestHandlerClass 속성에 저장돼요.

  • fileno() — 서버가 수신 대기하는 소켓의 정수 파일 디스크립터를 반환해요. 이 함수는 같은 프로세스에서 여러 서버를 모니터링하기 위해 selectors에 가장 흔히 전달돼요.
  • handle_request() — 단일 요청을 처리해요. 이 함수는 순서대로 get_request(), verify_request(), process_request() 메서드를 호출해요. 핸들러 클래스의 사용자 제공 handle() 메서드가 예외를 발생시키면 서버의 handle_error() 메서드가 호출돼요. timeout 초 안에 요청을 받지 못하면 handle_timeout()이 호출되고 handle_request()가 반환돼요.
  • serve_forever(poll_interval=0.5) — 명시적 shutdown() 요청이 올 때까지 요청을 처리해요. poll_interval 초마다 종료를 폴링해요. timeout 속성은 무시해요. 또한 서브클래스나 믹스인이 특정 서비스에 특화된 동작을 제공할 수 있게 하는 service_actions()를 호출해요. 예를 들어 ForkingMixIn 클래스는 service_actions()로 좀비 자식 프로세스를 정리해요. 버전 3.3에서 변경: serve_forever 메서드에 service_actions 호출 추가.
  • service_actions()serve_forever() 루프에서 호출돼요. 서브클래스 또는 믹스인 클래스가 정리 동작 같은 특정 서비스 관련 동작을 수행하도록 오버라이드할 수 있어요. (버전 3.3에 추가됨.)
  • shutdown()serve_forever() 루프에 멈추라고 지시하고 멈출 때까지 기다려요. serve_forever()가 다른 스레드에서 실행 중일 때 shutdown()을 호출해야 해요. 그렇지 않으면 교착 상태(deadlock)에 빠져요.
  • server_close() — 서버를 정리해요. 오버라이드할 수 있어요.
  • address_family — 서버 소켓이 속한 프로토콜의 패밀리예요. 흔한 예는 socket.AF_INET, socket.AF_INET6, socket.AF_UNIX예요. IPv6 서버 클래스를 원하면 이 모듈의 TCP·UDP 서버 클래스를 address_family = AF_INET6 클래스 속성으로 서브클래싱해요.
  • RequestHandlerClass — 사용자 제공 요청 핸들러 클래스. 각 요청마다 이 클래스의 인스턴스가 생성돼요.
  • server_address — 서버가 수신 대기하는 주소. 주소의 형식은 프로토콜 패밀리에 따라 달라져요. 자세한 내용은 socket 모듈 문서를 보세요. 인터넷 프로토콜의 경우 주소를 주는 문자열과 정수 포트 번호를 담은 튜플이에요: 예: ('127.0.0.1', 80).
  • socket — 서버가 들어오는 요청을 수신 대기할 소켓 객체.

서버 클래스들은 다음 클래스 변수들을 지원해요.

  • allow_reuse_address — 서버가 주소의 재사용을 허용할지 여부. 기본값은 False이고, 서브클래스에서 설정해 정책을 바꿀 수 있어요.
  • request_queue_size — 요청 큐의 크기. 단일 요청을 처리하는 데 오래 걸리면, 서버가 바쁠 때 도착하는 요청들은 request_queue_size 요청까지 큐에 들어가요. 큐가 가득 차면 클라이언트의 추가 요청은 "Connection denied" 오류를 받아요. 기본값은 보통 5지만 서브클래스가 오버라이드할 수 있어요.
  • socket_type — 서버가 쓰는 소켓의 타입. socket.SOCK_STREAMsocket.SOCK_DGRAM이 두 흔한 값이에요.
  • timeout — 초 단위로 측정한 타임아웃 기간, 또는 타임아웃을 원하지 않으면 None. handle_request()가 타임아웃 기간 안에 들어오는 요청을 받지 못하면 handle_timeout() 메서드가 호출돼요.

TCPServer 같은 기본 서버 클래스의 서브클래스가 오버라이드할 수 있는 다양한 서버 메서드가 있어요. 이 메서드들은 서버 객체의 외부 사용자에게는 유용하지 않아요.

  • finish_request(request, client_address)RequestHandlerClass를 인스턴스화하고 그 handle() 메서드를 호출해 요청을 실제로 처리해요.
  • get_request() — 소켓에서 요청을 받아들이고, 클라이언트와 통신하는 데 쓰일 새 소켓 객체와 클라이언트 주소를 담은 2-튜플을 반환해야 해요.
  • handle_error(request, client_address)RequestHandlerClass 인스턴스의 handle() 메서드가 예외를 발생시키면 이 함수가 호출돼요. 기본 동작은 표준 오류에 traceback을 출력하고 추가 요청 처리를 계속하는 거예요. 버전 3.6에서 변경: 이제 Exception 클래스에서 파생된 예외에만 호출됨.
  • handle_timeout()timeout 속성이 None이 아닌 값으로 설정되었고 요청이 수신되지 않은 채 타임아웃 기간이 지나면 이 함수가 호출돼요. forking 서버의 기본 동작은 종료된 자식 프로세스의 상태를 수집하는 것이고, threading 서버에서는 이 메서드가 아무것도 하지 않아요.
  • process_request(request, client_address)finish_request()를 호출해 RequestHandlerClass 인스턴스를 만들어요. 원하면 이 함수가 요청을 처리할 새 프로세스나 스레드를 만들 수 있어요. ForkingMixInThreadingMixIn 클래스가 이렇게 해요.
  • server_activate() — 서버의 생성자가 서버를 활성화하기 위해 호출해요. TCP 서버의 기본 동작은 서버 소켓에서 listen()을 호출하는 것뿐이에요. 오버라이드할 수 있어요.
  • server_bind() — 서버의 생성자가 소켓을 원하는 주소에 바인딩하기 위해 호출해요. 오버라이드할 수 있어요.
  • verify_request(request, client_address) — Boolean 값을 반환해야 해요. 값이 True면 요청이 처리되고, False면 거부돼요. 이 함수는 서버의 접근 제어를 구현하도록 오버라이드할 수 있어요. 기본 구현은 항상 True를 반환해요.

버전 3.6에서 변경: 컨텍스트 매니저 프로토콜 지원 추가. 컨텍스트 매니저를 빠져나가는 것은 server_close()를 호출하는 것과 동등해요.

요청 핸들러 객체

class socketserver.BaseRequestHandler — 모든 요청 핸들러 객체의 슈퍼클래스예요. 아래 주어진 인터페이스를 정의해요. 구체적인 요청 핸들러 서브클래스는 새 handle() 메서드를 정의하고 다른 메서드들을 오버라이드할 수 있어요. 요청마다 서브클래스의 새 인스턴스가 생성돼요.

  • setup() — 필요한 초기화 동작을 수행하기 위해 handle() 메서드 앞에서 호출돼요. 기본 구현은 아무것도 하지 않아요.
  • handle() — 이 함수는 요청을 서비스하기 위해 필요한 모든 작업을 해야 해요. 기본 구현은 아무것도 하지 않아요. 여러 인스턴스 속성이 사용 가능해요. 요청은 request로, 클라이언트 주소는 client_address로, 서버 인스턴스는 server로 쓸 수 있어요(서버별 정보에 접근해야 할 때를 대비). request의 타입은 데이터그램 또는 스트림 서비스에 따라 달라요. 스트림 서비스에서 request는 소켓 객체이고, 데이터그램 서비스에서 request는 문자열과 소켓의 쌍이에요.
  • finish() — 필요한 정리 동작을 수행하기 위해 handle() 메서드 뒤에서 호출돼요. 기본 구현은 아무것도 하지 않아요. setup()이 예외를 발생시키면 이 함수는 호출되지 않아요.
  • request — 클라이언트와 통신하는 데 쓰일 새 socket.socket 객체.
  • client_addressBaseServer.get_request()가 반환한 클라이언트 주소.
  • server — 요청 처리에 쓰이는 BaseServer 객체.

class socketserver.StreamRequestHandler / class socketserver.DatagramRequestHandler — 이 BaseRequestHandler 서브클래스들은 setup()finish() 메서드를 오버라이드하고 rfilewfile 속성을 제공해요.

  • rfile — 요청이 수신될 파일 객체. io.BufferedIOBase 읽기 가능 인터페이스를 지원해요.
  • wfile — 답장이 쓰이는 파일 객체. io.BufferedIOBase 쓰기 가능 인터페이스를 지원해요. 버전 3.6에서 변경: wfileio.BufferedIOBase 쓰기 가능 인터페이스를 지원함.

예제

socketserver.TCPServer 예제

서버 쪽:

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    """
    The request handler class for our server.

    It is instantiated once per connection to the server, and must
    override the handle() method to implement communication to the
    client.
    """

    def handle(self):
        # self.request is the TCP socket connected to the client
        pieces = [b'']
        total = 0
        while b'\n' not in pieces[-1] and total < 10_000:
            pieces.append(self.request.recv(2000))
            total += len(pieces[-1])
        self.data = b''.join(pieces)
        print(f"Received from {self.client_address[0]}:")
        print(self.data.decode("utf-8"))
        # just send back the same data, but upper-cased
        self.request.sendall(self.data.upper())
        # after we return, the socket will be closed.

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999

    # Create the server, binding to localhost on port 9999
    with socketserver.TCPServer((HOST, PORT), MyTCPHandler) as server:
        # Activate the server; this will keep running until you
        # interrupt the program with Ctrl-C
        server.serve_forever()

스트림(표준 파일 인터페이스를 제공해 통신을 단순화하는 파일류 객체)을 활용하는 대안적 요청 핸들러 클래스:

class MyTCPHandler(socketserver.StreamRequestHandler):

    def handle(self):
        # self.rfile is a file-like object created by the handler.
        # We can now use e.g. readline() instead of raw recv() calls.
        # We limit ourselves to 10000 bytes to avoid abuse by the sender.
        self.data = self.rfile.readline(10000).rstrip()
        print(f"{self.client_address[0]} wrote:")
        print(self.data.decode("utf-8"))
        # Likewise, self.wfile is a file-like object used to write back
        # to the client
        self.wfile.write(self.data.upper())

차이점은 두 번째 핸들러의 readline() 호출이 개행 문자를 만날 때까지 recv()를 여러 번 호출하는 반면, 첫 번째 핸들러는 개행까지 데이터를 누적하기 위해 recv() 루프를 직접 써야 했다는 거예요. 루프 없이 단일 recv()만 썼다면 지금까지 클라이언트로부터 받은 것만 반환했을 거예요. TCP는 스트림 기반이에요: 데이터는 보낸 순서대로 도착하지만, 클라이언트의 send()/sendall() 호출과 그것을 받는 데 필요한 서버의 recv() 호출 횟수 사이에는 상관관계가 없어요.

클라이언트 쪽:

import socket
import sys

HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])

# Create a socket (SOCK_STREAM means a TCP socket)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
    # Connect to server and send data
    sock.connect((HOST, PORT))
    sock.sendall(bytes(data, "utf-8"))
    sock.sendall(b"\n")

    # Receive data from the server and shut down
    received = str(sock.recv(1024), "utf-8")

print("Sent:    ", data)
print("Received:", received)

예제의 출력은 대략 이렇게 보일 거예요.

서버:

$ python TCPServer.py
127.0.0.1 wrote:
b'hello world with TCP'
127.0.0.1 wrote:
b'python is nice'

클라이언트:

$ python TCPClient.py hello world with TCP
Sent:     hello world with TCP
Received: HELLO WORLD WITH TCP
$ python TCPClient.py python is nice
Sent:     python is nice
Received: PYTHON IS NICE

socketserver.UDPServer 예제

서버 쪽:

import socketserver

class MyUDPHandler(socketserver.BaseRequestHandler):
    """
    This class works similar to the TCP handler class, except that
    self.request consists of a pair of data and client socket, and since
    there is no connection the client address must be given explicitly
    when sending data back via sendto().
    """

    def handle(self):
        data = self.request[0].strip()
        socket = self.request[1]
        print(f"{self.client_address[0]} wrote:")
        print(data)
        socket.sendto(data.upper(), self.client_address)

if __name__ == "__main__":
    HOST, PORT = "localhost", 9999
    with socketserver.UDPServer((HOST, PORT), MyUDPHandler) as server:
        server.serve_forever()

클라이언트 쪽:

import socket
import sys

HOST, PORT = "localhost", 9999
data = " ".join(sys.argv[1:])

# SOCK_DGRAM is the socket type to use for UDP sockets
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

# As you can see, there is no connect() call; UDP has no connections.
# Instead, data is directly sent to the recipient via sendto().
sock.sendto(bytes(data + "\n", "utf-8"), (HOST, PORT))
received = str(sock.recv(1024), "utf-8")

print("Sent:    ", data)
print("Received:", received)

예제의 출력은 TCP 서버 예제와 정확히 같을 거예요.

비동기 믹스인

비동기 핸들러를 만들려면 ThreadingMixInForkingMixIn 클래스를 사용해요. ThreadingMixIn 클래스의 예:

import socket
import threading
import socketserver

class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler):

    def handle(self):
        data = str(self.request.recv(1024), 'ascii')
        cur_thread = threading.current_thread()
        response = bytes("{}: {}".format(cur_thread.name, data), 'ascii')
        self.request.sendall(response)

class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
    pass

def client(ip, port, message):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        sock.connect((ip, port))
        sock.sendall(bytes(message, 'ascii'))
        response = str(sock.recv(1024), 'ascii')
        print("Received: {}".format(response))

if __name__ == "__main__":
    # Port 0 means to select an arbitrary unused port
    HOST, PORT = "localhost", 0

    server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler)
    with server:
        ip, port = server.server_address

        # Start a thread with the server -- that thread will then start one
        # more thread for each request
        server_thread = threading.Thread(target=server.serve_forever)
        # Exit the server thread when the main thread terminates
        server_thread.daemon = True
        server_thread.start()
        print("Server loop running in thread:", server_thread.name)

        client(ip, port, "Hello World 1")
        client(ip, port, "Hello World 2")
        client(ip, port, "Hello World 3")

        server.shutdown()

예제의 출력은 대략 이렇게 보일 거예요:

$ python ThreadedTCPServer.py
Server loop running in thread: Thread-1
Received: Thread-2: Hello World 1
Received: Thread-3: Hello World 2
Received: Thread-4: Hello World 3

ForkingMixIn 클래스는 서버가 각 요청마다 새 프로세스를 생성한다는 점만 빼고 같은 방식으로 사용돼요. fork()를 지원하는 POSIX 플랫폼에서만 쓸 수 있어요.