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

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

socketserver 모듈은 네트워크 서버 작성 작업을 단순화합니다.

출처: Python documentation

본문

availability: not WASI.

이 모듈은 WebAssembly에서 작동하지 않거나 사용할 수 없습니다.

네 가지 기본적인 구체 서버 클래스가 있습니다.

socketserver.TCPServer(server_address, RequestHandlerClass, bind_and_activate=True)

이것은 클라이언트와 서버 사이에 연속적인 데이터 흐름을 제공하는 인터넷 TCP 프로토콜을 사용합니다. bind_and_activate가 참이면 생성자가 자동으로 server_bind()server_activate()를 호출하려 시도합니다. 다른 매개변수는 BaseServer 기본 클래스에 전달됩니다.

socketserver.UDPServer(server_address, RequestHandlerClass, bind_and_activate=True)

이것은 순서가 뒤섞이거나 전송 중에 손실될 수 있는 개별 정보 패킷인 데이터그램을 사용합니다. 매개변수는 TCPServer와 같습니다.

socketserver.UnixStreamServer(server_address, RequestHandlerClass, bind_and_activate=True)

socketserver.UnixDatagramServer(server_address, RequestHandlerClass, bind_and_activate=True)

덜 자주 사용되는 이 클래스들은 TCP와 UDP 클래스와 유사하지만 Unix 도메인 소켓을 사용합니다. 비-Unix 플랫폼에서는 사용할 수 없습니다. 매개변수는 TCPServer와 같습니다.

이 네 클래스는 요청을 동기적으로 처리합니다. 각 요청은 다음 요청을 시작하기 전에 완료되어야 합니다. 각 요청이 완료되는 데 오래 걸리면(많은 계산이 필요하거나 클라이언트가 처리하기 느린 많은 데이터를 반환하므로) 적합하지 않습니다. 해결책은 각 요청을 처리할 별도의 프로세스나 스레드를 만드는 것입니다. ForkingMixInThreadingMixIn mix-in 클래스를 사용하여 비동기 동작을 지원할 수 있습니다.

서버를 만드는 데는 여러 단계가 필요합니다. 첫째, BaseRequestHandler 클래스를 하위 클래스로 만들고 그 handle() 메서드를 재정의하여 요청 핸들러 클래스를 만들어야 합니다. 이 메서드는 들어오는 요청을 처리합니다. 둘째, 서버 클래스 중 하나를 인스턴스화하고 서버의 주소와 요청 핸들러 클래스를 전달해야 합니다. with 문에서 서버를 사용하는 것이 좋습니다. 그런 다음 서버 객체의 handle_request() 또는 serve_forever() 메서드를 호출하여 하나 또는 여러 요청을 처리합니다. 마지막으로 server_close()를 호출하여 소켓을 닫습니다(with 문을 사용하지 않은 경우).

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

서버 클래스는 어떤 네트워크 프로토콜을 사용하든 동일한 외부 메서드와 속성을 가집니다.

서버 생성 참고 사항

상속 다이어그램에는 다섯 개의 클래스가 있으며 그중 네 개는 네 가지 유형의 동기 서버를 나타냅니다:

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

UnixDatagramServerUnixStreamServer가 아니라 UDPServer에서 파생된다는 점에 유의하세요. IP 서버와 Unix 서버의 유일한 차이는 주소 패밀리입니다.

socketserver.ForkingMixIn

socketserver.ThreadingMixIn

이 mix-in 클래스들을 사용하여 각 유형의 서버에 대한 forking 및 threading 버전을 만들 수 있습니다. 예를 들어 ThreadingUDPServer는 다음과 같이 생성됩니다:

class ThreadingUDPServer(ThreadingMixIn, UDPServer):
    pass

mix-in 클래스가 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로 설정하여 데몬 스레드를 사용하고 스레드가 완료될 때까지 기다리지 않습니다.

versionchanged: 3.7에서 ForkingMixIn.server_closeThreadingMixIn.server_close는 이제 모든 자식 프로세스와 비데몬 스레드가 완료될 때까지 기다립니다. 3.7 이전 동작을 선택하기 위해 새 ForkingMixIn.block_on_close 클래스 속성을 추가하세요.

socketserver.ForkingTCPServer

socketserver.ForkingUDPServer

socketserver.ThreadingTCPServer

socketserver.ThreadingUDPServer

socketserver.ForkingUnixStreamServer

socketserver.ForkingUnixDatagramServer

socketserver.ThreadingUnixStreamServer

socketserver.ThreadingUnixDatagramServer

이 클래스들은 mix-in 클래스를 사용하여 미리 정의되어 있습니다.

versionadded: 3.12에서 ForkingUnixStreamServerForkingUnixDatagramServer 클래스가 추가되었습니다.

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

물론 여전히 머리를 써야 합니다! 예를 들어 서비스가 다른 요청에 의해 수정될 수 있는 상태를 메모리에 포함하면 forking 서버를 사용하는 것은 무의미합니다. 자식 프로세스의 수정이 부모 프로세스에 유지되고 각 자식에게 전달되는 초기 상태에는 결코 도달하지 않기 때문입니다. 이 경우 threading 서버를 사용할 수 있지만 공유 데이터의 무결성을 보호하려면 아마도 잠금을 사용해야 합니다.

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

어떤 경우에는 요청의 일부를 동기적으로 처리하지만 요청 데이터에 따라 forked 자식에서 처리를 완료하는 것이 적절할 수 있습니다. 이것은 동기 서버를 사용하고 요청 핸들러 클래스의 handle() 메서드에서 명시적 fork를 수행하여 구현할 수 있습니다.

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

서버 객체

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 속성을 무시합니다. 또한 하위 클래스나 mixin이 특정 서비스에 특정한 작업을 제공하는 데 사용할 수 있는 service_actions()를 호출합니다. 예를 들어 ForkingMixIn 클래스는 service_actions()를 사용하여 좀비 자식 프로세스를 정리합니다.

    versionchanged: 3.3에서 service_actions 호출이 serve_forever 메서드에 추가되었습니다.

  • service_actions()serve_forever() 루프에서 호출됩니다. 이 메서드는 정리 작업과 같은 특정 서비스에 특정한 작업을 수행하기 위해 하위 클래스나 mixin 클래스가 재정의할 수 있습니다.

    versionadded: 3.3.

  • shutdown()serve_forever() 루프에 멈추고 멈출 때까지 기다리라고 지시합니다. shutdown()serve_forever()가 실행되는 동안 다른 스레드에서 호출되어야 하며, 그렇지 않으면 교착 상태가 됩니다.
  • server_close() — 서버를 정리합니다. 반드시 serve_forever()가 종료된 후에 호출해야 합니다.
  • server_activate() — 서버의 실질적인 시작을 수행하기 위해 생성자에서 호출됩니다. listen() 메서드가 호출됩니다. 기본 listen() 메서드는 request_queue_size 속성에 따라 리스닝 백로그를 설정합니다. 이 메서드는 각 구체 클래스에서 재정의되어 해야 할 추가 작업이 있으면 수행합니다.
  • server_bind() — 생성자에서 소켓을 특정 주소에 바인딩하기 위해 호출됩니다.
  • verify_request(request, client_address) — 요청을 처리해야 하는지 여부를 반환합니다. 이 메서드가 False를 반환하면 요청은 처리되지 않습니다. 이 메서드는 요청 핸들러를 실행하기 전에 호출됩니다. 요청 승인을 하는 데 사용할 수 있습니다.
  • process_request(request, client_address) — 요청을 처리하기 위해 finish_request()를 호출합니다. 이 메서드는 process_request에 대한 더 복잡한 동작을 제공하기 위해 ForkingMixInThreadingMixIn 클래스에서 재정의됩니다.
  • finish_request(request, client_address) — 주어진 클라이언트 주소에서 실제로 수신된 데이터로 RequestHandlerClass를 인스턴스화하여 요청을 처리합니다.
  • handle_error(request, client_address)handle_request() 중 예외가 발생하면 호출됩니다. 기본 구현은 traceback을 표준 오류로 출력합니다.
  • handle_timeout() — 요청이 timeout 속성의 시간 내에 도착하지 않으면 handle_request()가 호출하는 기본 시간 초과 핸들러입니다. timeoutNone이면 handle_request()는 블록되어 handle_timeout()을 호출하지 않습니다. 이 메서드는 재정의할 수 있습니다.
  • address_family — 서버 소켓의 주소 패밀리. 예: socket.AF_INET, socket.AF_INET6, socket.AF_UNIX.
  • socket — 들어오는 요청이 수신되는 서버 소켓입니다.
  • allow_reuse_address — 서버가 주소를 재사용할 수 있게 할지 여부. 기본값은 False이며, 이 기본값을 재정의할 수 있습니다.
  • request_queue_size — 들어오는 요청의 크기입니다. listen()에 전달됩니다. 기본 listen() 메서드가 재정의되지 않으면 이 클래스를 사용하는 서버에 들어오는 요청의 최대 크기를 추가로 제한해야 합니다. 이것은 BaseServer의 하위 클래스 사이에서 다를 수 있습니다.
  • daemon_threadsForkingMixInThreadingMixIn이 파생되는 멤버 변수입니다. ThreadingMixIn을 사용하는 경우 이 클래스 속성은 기본값이 False이며 서버가 스레드 종료를 기다릴지 여부를 나타냅니다. 서버가 자율적으로 동작하게 하려면 True로 설정해야 합니다. 그러지 않으면 모든 스레드가 종료될 때까지 서버가 종료되지 않습니다.
  • block_on_closeForkingMixInThreadingMixIn이 파생되는 멤버 변수입니다. True(기본값)이면 server_close()가 모든 자식 프로세스(각각 비데몬 스레드)가 완료될 때까지 차단합니다. False로 설정하면 server_close()가 즉시 반환합니다.

    versionadded: 3.7.

  • timeouthandle_request()가 반환하기 전에 기다릴 시간 초(초). timeoutNone이면 handle_request()는 블록되어 요청이 도착할 때까지 기다립니다.
  • RequestHandlerClass — 들어오는 요청 핸들러의 사용자 제공 클래스입니다.
  • server_address — 서버가 수신하는 주소. socket.getsockname()의 결과로 다를 수 있습니다.

요청 핸들러 객체가 처리됩니다.

socketserver.BaseRequestHandler

이것은 요청이 도달하면 각각 생성되는 처리기 클래스의 슈퍼클래스입니다. 서비스 주소(서버)와 들어오는 요청을 나타내는 클라이언트 주소가 있습니다.

  • setup() — 각 요청을 처리하기 전에 호출됩니다. StreamRequestHandlerDatagramRequestHandler의 기본 구현은 이 메서드를 재정의합니다. 하위 클래스에서 재정의할 수 있으며, 이 경우 setup()을 호출해야 합니다.
  • handle() — 이 함수는 요청을 실제 처리해야 합니다. BaseRequestHandler는 이 메서드를 구현하지 않으며 하위 클래스가 재정의해야 합니다. 실제로 이 메서드는 서버의 서비스를 정의하는 것입니다.
  • finish()handle() 메서드가 반환한 후 호출됩니다. StreamRequestHandlerDatagramRequestHandler의 기본 구현은 이 메서드를 재정의합니다. 하위 클래스에서 재정의할 수 있으며, 이 경우 finish()를 호출해야 합니다.
  • request — 수신된 클라이언트 요청입니다. handle()에 인자로 전달될 것입니다.
  • client_address — 클라이언트 주소입니다. 서버와 컨테이너 주소 서버가 다를 수 있습니다. 각 경우 TCP의 주소는 (host, port)이며, Unix 도메인 소켓의 경우 경로입니다. UDP와 Unix 데이터그램 서버의 경우 이는 일치하는 sendto() 호출에 대한 수신 주소입니다. 이것은 외부 소스에서 악의적일 수 있으므로 신뢰하지 마세요.
  • server — 요청을 처리하는 서버 객체입니다.

socketserver.StreamRequestHandler

BaseRequestHandler의 구현으로, 읽기-쓰기 스트림으로 처리됩니다. 요청 처리에 rfilewfile 속성을 사용할 수 있습니다.

  • rfile — 요청을 읽는 파일 객체입니다.
  • wfile — 요청을 쓰는 파일 객체입니다.
  • rbufsize — 읽기 버퍼 크기. 기본값은 1024.
  • wbufsize — 쓰기 버퍼 크기. 기본값은 0.
  • disable_nagle_algorithm — TCP 네이글 알고리즘을 비활성화할지 여부. 기본값은 False.

socketserver.DatagramRequestHandler

BaseRequestHandler의 구현으로, 데이터그램으로 처리됩니다. handle() 메서드에서 rfile은 클라이언트가 보낸 패킷을 읽습니다.

  • rfile — 요청을 읽는 파일 객체입니다.
  • wfile — 요청을 쓰는 파일 객체입니다.
  • packet — 수신된 패킷을 포함하는 바이트 객체입니다.
  • socket — 요청을 보내는 데 사용되는 소켓입니다.

요청 처리기 예제

socketserver.TCPServer 예제

다음 요청 핸들러는 데이터를 수신하고, 그것을 대문자로 변환한 다음 클라이언트로 다시 보냅니다:

import socketserver

class MyTCPHandler(socketserver.BaseRequestHandler):
    """
    The RequestHandler 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
        self.data = self.request.recv(1024).strip()
        print("{} wrote:".format(self.client_address[0]))
        print(self.data)
        # 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()

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

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("{} wrote:".format(self.client_address[0]))
        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("{} wrote:".format(self.client_address[0]))
        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 서버 예제와 정확히 같아야 합니다.

비동기 mix-in

비동기 핸들러를 구축하려면 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 플랫폼에서만 사용할 수 있습니다.

더 알아보기 (Learn more)