티스토리 뷰

반응형

소켓 서버가 소켓을 listen()하던 중 클라이언트가 접속하면 sock.accept()를 호출하여 connection과 address를 얻게됩니다. 이 때 connection은 접속된 클라이언트와 통신할 수 있는 새로운 소켓으로, 1:1 통신을 가정한 상황에서는 일단 클라이언트가 접속된 후에는 리스닝 소켓에서 accept()가 다시 호출될 수 없는 구조이며, 1:N 통신을 사용하려면 접속 소켓을 통해 데이터를 주고 받는 코드는 별도의 스레드에서 작동해야 합니다.

대신 이러한 구조는 클라이언트가 접속할 때마다 새로운 스레드를 만들게 되고, 상대적으로 비싼 작업이 됩니다. 따라서 간단한 데이터 교환과 같은 경우에는 싱글 스레드 기반에서 다중 접속을 관리하는 것이 (가능하다면) 훨씬 권장되는 방법입니다.

싱글스레드를 사용하여 소켓을 다중화(멀티플렉싱)하기 위해서는 셀렉터(분배기)를 사용합니다. 분배기는 여러 소켓을 등록하여 등록된 소켓 중 하나에서 이벤트(읽을 준비가 됨)가 발생하면, 그러한 소켓에 대한 정보를 리턴하는 방식으로 스레드와 같은 동시성 수단 없이 하나의 스레드에서 여러 접속을 동시에 관리할 수 있게 합니다. 파이썬에서는 selectors 모듈을 사용하여 셀렉터를 사용할 수 있는데, 셀렉터를 통해 소켓을 다중화하는 기본적인 방법은 다음과 같습니다.

  1. DefaultSelector 타입의 셀렉터를 생성합니다.
  2. 소켓을 생성하고 포트에 바인딩합니다.
  3. 소켓의 listen()을 호출하여 수신대기 모드를 준비합니다.
  4. 소켓을 셀렉터에 등록합니다.
  5. 루프를 돌며 셀렉터로부터 이벤트가 발생한 소켓을 얻습니다.

셀렉터로부터 전달된 소켓이 리스닝 소켓이면 accept()를 사용하여 클라이어트와 연결된 소켓을 얻게 됩니다. 연결 소켓 역시 셀렉터에 다시 등록합니다. 셀렉터에 소켓을 등록할 때에는 (파일디스크립터, 이벤트, 데이터) 를 전달하는데, 이 때 데이터에 이벤트를 처리할 콜백 함수를 전달하면 소켓에 따라 다른 동작을 수행하도록 할 수 있습니다.

셀렉터의 select() 메소드는 블러킹 함수이며, 이벤트가 발생한 소켓의 키와 이벤트 마스크르 리턴합니다. 따라서 이 예에서는 사실 소켓을 non-blocking으로 설정할 필요는 없습니다.

import selectors
import socket
import time

# 클라이언트로부터 데이터를 수신 받는 이벤트 핸들러
# 'bye'를 수신하면 연결 종료

def read_hander(conn: socket.socket, sel: selecters.BaseSelector):
    data: bytes = conn.recv(1024)
    mes = data.decode()
    if mes.upper() == 'BYE':
        sel.unregister(conn)
        conn.close()
    time.sleep(.5)
    conn.sendall(data)


def run_server(port=5559):
    sel = selectors.DefaultSelector()
    # 소켓 생성. 파라미터를 생략하면 `socket.AF_INET, socket.SOCK_STREAM`이 기본값이서 생략가능
    sock = socket.socket()
    sock.bind(('localhost', port))
    sock.setblocking(False)
    sock.listen(5)
    sel.register(sock, selectors.EVENT_READ, None)

    # 셀렉터가 인터럽트하는 것을 처리
    while True:
        for (key, mask) in sel.select():
            # key에는 'fileObj', 'data' 프로퍼티가 있고, 이 값들은 각각 register() 시에 전달된 값이다. 
            # 리스닝 소켓으로 접속 요청이 들어왔을 때 처리
            if key.fileObj is sock:
                conn, addr = sock.accept()
                sel.register(conn, selectors.EVENT_READ, read_handler)
            else:
                # 클라이언트와 연결된 소켓인 경우 콜백을 호출한다.
                key.data(key.fileObj, sel)

싱글 스레드에서 소켓을 멀티플렉싱하는 방법에는 이외에도 asyncio 모듈을 사용하는 방법이 있습니다. asyncio에서는 서버와 클라이언트의 역할이 명확하게 구분되며, 하나의 연결에 대해 읽기 스트림과 쓰기 스트림이 구분되기 때문에 동시에 읽으면서 쓰는 작업이 수행될 수 있어서 동시접속이 더 많은 경우에 더 빠르게 작동할 수 있는 장점이 있습니다.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
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 31
글 보관함