iBetter Books
수정

select로 여러 소켓 다루기

이제 식당의 두 번째 길, 주인 한 명이 모든 테이블을 부지런히 도는 방법을 만들어 봅니다. 종업원을 여럿 두는 대신, 한 사람이 준비된 테이블만 골라 빠르게 처리하는 방식입니다. 이를 가능하게 하는 것이 I/O 멀티플렉싱이고, 그 첫 도구가 select입니다. 발상의 전환이 일어나는 중요한 장이니 천천히 따라오세요.

멈추지 않고 지켜보기

앞 장에서 블로킹이 문제라고 했습니다. recv가 데이터를 기다리며 멈춰 버리니, 그 사이 다른 클라이언트를 못 받았습니다. 스레드는 멈춰도 괜찮은 흐름을 여럿 만들어 이 문제를 피했습니다.

select의 발상은 정반대입니다. 아예 멈추지 않습니다. 어느 한 소켓의 recv에 매달리는 대신, 운영체제에게 이렇게 묻습니다. "내가 지켜보는 이 소켓들 중에, 지금 당장 읽을 데이터가 준비된 것이 있는가." 운영체제가 준비된 소켓들을 알려 주면, 그것들만 골라 처리합니다. 준비된 소켓을 읽는 recv는 곧바로 데이터를 주고 끝나니 멈출 일이 없습니다. 한 흐름으로 여러 소켓을 상대할 수 있는 비결이 바로 이것입니다.

여기서 핵심은 듣기 소켓도 지켜보는 대상에 포함된다는 점입니다. 듣기 소켓이 준비됐다는 것은 새 클라이언트가 연결을 시도했다는 뜻이고, 그때 accept하면 됩니다. 이렇게 새 연결을 받는 일과 기존 클라이언트를 상대하는 일을 한 흐름 안에서 모두 처리합니다.

select 에코 서버

select로 여러 클라이언트를 받는 에코 서버를 만듭니다.

# 새 파일: select_server.pyimport socketimport selectserver = socket.socket(socket.AF_INET, socket.SOCK_STREAM)server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)server.bind(("127.0.0.1", 9002))server.listen(5)server.setblocking(False)sockets = [server]print("select 에코 서버 대기 중")while True:    readable, _, _ = select.select(sockets, [], [])    for s in readable:        if s is server:            conn, addr = s.accept()            conn.setblocking(False)            sockets.append(conn)        else:            data = s.recv(1024)            if data:                s.sendall(data)            else:                sockets.remove(s)                s.close()

한 줄씩 뜯어보겠습니다.

server.setblocking(False)는 이 소켓을 논블로킹으로 바꿉니다. 더는 함수가 멈추지 않게 하겠다는 선언입니다. 새로 받는 클라이언트 소켓도 conn.setblocking(False)로 똑같이 논블로킹으로 만듭니다.

sockets 리스트가 우리가 지켜보는 소켓들의 명단입니다. 처음에는 듣기 소켓 하나뿐이지만, 클라이언트가 연결되면 그 소켓도 명단에 추가됩니다.

select.select(sockets, [], [])가 이 장의 주인공입니다. 첫 인자로 넘긴 소켓들 중 읽을 준비가 된 것들을 골라 돌려줍니다. 뒤의 두 빈 리스트는 쓰기 준비와 예외를 지켜볼 소켓들인데, 지금은 읽기만 신경 쓰므로 비워 둡니다. 준비된 소켓이 하나도 없으면 select는 생길 때까지 기다립니다. 그러나 일단 무언가 준비되면 그것들을 모아 돌려줍니다.

돌려받은 readable 소켓들을 하나씩 처리합니다. 그 소켓이 듣기 소켓이면 새 연결이 온 것이니 accept해서 명단에 추가합니다. 듣기 소켓이 아니면 기존 클라이언트가 데이터를 보낸 것이니 recv로 받아 그대로 돌려줍니다. 만약 빈 데이터가 오면, 앞에서 배웠듯 클라이언트가 떠난 것이므로 명단에서 빼고 소켓을 닫습니다.

이 모든 일이 단 하나의 흐름, 단 하나의 while 반복에서 일어납니다. 스레드도, 프로세스도 없습니다. 주인 한 명이 모든 테이블을 도는 것입니다.

동작 확인하기

서버를 띄우고 세 클라이언트를 동시에 붙이면, 스레드 서버와 똑같이 셋이 함께 처리됩니다.

[A] 받음: A 인사
[B] 받음: B 인사
[C] 받음: C 인사

흐름은 하나뿐인데도 셋이 한 줄로 기다리지 않았습니다. select가 준비된 소켓들을 그때그때 알려 준 덕분입니다.

select의 한계

select는 우아하지만 약점도 있습니다. 고성능을 다루는 PART 06으로 가는 길목이니 미리 짚어 두겠습니다.

첫째, 지켜볼 수 있는 소켓의 수에 한계가 있습니다. 전통적으로 select는 1024개 정도까지만 지켜볼 수 있도록 설계되었습니다. 수만 명을 상대해야 하는 서버에는 이 상한이 곧바로 벽이 됩니다.

둘째, 매번 모든 소켓을 검사합니다. select를 부를 때마다 운영체제는 지켜보는 소켓 전부를 처음부터 끝까지 훑어 누가 준비됐는지 확인합니다. 소켓이 천 개면 천 개를 매번 훑습니다. 그중 실제로 준비된 것이 한두 개뿐이어도 전부를 검사하니, 소켓이 많아질수록 이 검사 비용이 부담스러워집니다.

이 두 한계 때문에 select는 적당한 규모에서는 훌륭하지만 초고성능 서버에는 맞지 않습니다. 다음 장에서 만날 poll이 첫 번째 한계를 일부 풀어 주고, PART 06에서 만날 epoll이 두 한계를 모두 넘어섭니다. 하지만 발상의 뿌리는 모두 같습니다. 멈추지 않고, 준비된 것만 골라 처리한다는 select의 정신입니다. 그 정신을 이해했다면 가장 중요한 고비를 넘은 것입니다.

다음 장에서 select의 사촌인 poll을 짧게 만나 봅니다.