ZMQ + Asyncio 적용하기

파이썬에서 ZMQ를 사용할 때, asyncio를 사용할 수 있게 되었다. asyncio에 적용한다고 해서 크게 달라지는 것은 없고 소켓의 사용방법은 대동소이하다. (실제 IO 시점에 작업 전환이 일어날 수 있게 await를 붙이는 것 정도의 차이만 있다. 대략의 사용법을 정리해보면 다음과 같다.

  • zmq.asyncio 모듈로부터 컨텍스트를 생성한다.
  • send~, recv~는 모두 비동기 코루틴으로 되어있다.
  • 따라서 실제 노드의 동작도 비동기 코루틴으로 작성한다.

따라서 코드가 크게 바뀔 부분이 사실 없다… 실제 예로 ZMQ의 기본 메시지 패턴 구현에서 소개한 다중 포트 접속 에코 서버/클라이언트의 코드를 아래에 소개하겠다. 참고로 아래 예제는 파이썬 3.7 기준으로 작성되었다. 파이썬 3.7에서는 asyncio.run() 함수가 추가되어 런루프를 명시적으로 액세스하지 않고 간단한 구문으로 비동기 런루프를 호출할 수 있게 되었다.

REP 서버

import sys
import asyncio
import zmq
import zmq.asyncio
import random

ctx = zmq.asyncio.Context()

async def run_server(*ports):
    sock = ctx.socket(zmq.REP)
    for port in ports:
        sock.bind(f'tcp://*:{port}')
    while True:
        request = await sock.recv_string()
        print(f'RECEIVED:\t{request}')
        print(f'PROCESSING...')
        await asyncio.sleep(random.randrange(5,30) / 10)
        print(f'SENDING:\t{request}')
        await sock.send_string(request)

if __name__ == '__main__':
    ports = sys.argv[1:] if len(sys.argv) > 1 else (5556,)
    asyncio.run(run_server(*ports))

REQ 클라이언트

import sys
import asyncio
import zmq
import zmq.asyncio

ctx = zmq.asyncio.Context()

async def run_client(*ports):
    sock = ctx.socket(zmq.REQ)
    for port in ports:
        sock.connect(f'tcp://localhost:{port}')
    while True:
        line = input('>>')
        print(f'SENDING:\t{line}')
        await sock.send_string(line)
        reply = await sock.recv_string()
        print(f'RECEIVED:\t{reply}')
        if line == 'bye':
            break

if __name__ == '__main__':
    ports = sys.argv[1:] if len(sys.argv) > 1 else (5556,)
    asyncio.run(run_client(*ports))