asyncore – 非同期 I/O ハンドラ¶
目的: | 非同期 I/O ハンドラ |
---|---|
利用できるバージョン: | 1.5.2 以上 |
asyncore モジュールは、(例えば、スレッドを使用せずに)非同期で制御できるように、ソケットのような非同期 I/O オブジェクトを扱うツールを提供します。主なクラスは dispatcher です。そのクラスは loop() のようなメインループ関数から実行されるコネクション接続や読み書きといったイベントを操作するフックを提供するソケット周りのラッパーです。
クライアント¶
asyncore ベースのクライアントを作成するには dispatcher のサブクラスで、ソケットの作成、読み書きの実装を提供します。標準ライブラリドキュメントで紹介されている、この HTTP クライアントを検証してみましょう。
import asyncore
import logging
import socket
from cStringIO import StringIO
import urlparse
class HttpClient(asyncore.dispatcher):
def __init__(self, url):
self.url = url
self.logger = logging.getLogger(self.url)
self.parsed_url = urlparse.urlparse(url)
asyncore.dispatcher.__init__(self)
self.write_buffer = 'GET %s HTTP/1.0\r\n\r\n' % self.url
self.read_buffer = StringIO()
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
address = (self.parsed_url.netloc, 80)
self.logger.debug('connecting to %s', address)
self.connect(address)
def handle_connect(self):
self.logger.debug('handle_connect()')
def handle_close(self):
self.logger.debug('handle_close()')
self.close()
def writable(self):
is_writable = (len(self.write_buffer) > 0)
if is_writable:
self.logger.debug('writable() -> %s', is_writable)
return is_writable
def readable(self):
self.logger.debug('readable() -> True')
return True
def handle_write(self):
sent = self.send(self.write_buffer)
self.logger.debug('handle_write() -> "%s"', self.write_buffer[:sent])
self.write_buffer = self.write_buffer[sent:]
def handle_read(self):
data = self.recv(8192)
self.logger.debug('handle_read() -> %d bytes', len(data))
self.read_buffer.write(data)
if __name__ == '__main__':
logging.basicConfig(level=logging.DEBUG,
format='%(name)s: %(message)s',
)
clients = [
HttpClient('http://www.python.org/'),
HttpClient('http://www.doughellmann.com/PyMOTW/contents.html'),
]
logging.debug('LOOP STARTING')
asyncore.loop()
logging.debug('LOOP DONE')
for c in clients:
response_body = c.read_buffer.getvalue()
print c.url, 'got', len(response_body), 'bytes'
まずソケットはベースクラスのメソッド create_socket() を使用して __init__() で作成されます。このメソッドの代替実装もあるでしょうが、このケースでは TCP/IP ソケットが必要なのでベースクラスのメソッドで十分です。
handle_connect() のフックは、この処理が呼び出されたことが単純に分かるために実装しています。何らかのハンドシェイクか、プロトコルネゴシエーションをする必要があるクライアントの場合は handle_connect() で処理を実装します。
同様に handle_close() もこのメソッドが呼び出されたときにそのことを表示する目的で実装しています。ベースクラスバージョンは正常にソケットをクローズするので、クローズ時に追加のクリーンアップ処理をする必要がないなら、このメソッドを取り除けます。
asyncore ループは、それぞれのディスパッチャがどのような処理をするかを決めるために writable() とその兄弟メソッドの readable() を使用します。実際には、それぞれのディスパッチャが管理するソケットかファイルディスクリプタ上で poll() か select() を使用して asyncore コードの内部で操作されます。そのため、自分でそういった処理を行う必要はありません。ディスパッチャに読み込みか、書き込みかを単純に指示してください。この HTTP クライアントのケースでは、 writable() はサーバへ送られるリクエストにデータが存在する限り、True を返します。 readable() は、全てのデータを読み込むので常に True を返します。
そのループを通して確実に writable() が応答するときに handle_write() が実行されます。このバージョンでは、 __init__() で作成された HTTP リクエストの文字列はサーバへ送られて、正常に送られることで書き込みバッファが減少します。
同様に readable() が確実に応答するときは読み込むデータがあり handle_read() が実行されます。
サンプルの __main__() は、デバッグ用のロギング設定をテストしてから、2つの web ページをダウンロードする2つのクライアントを作成します。クライアントを作成するには、内部的に asyncore が管理する “map” にこれらのクライアントを登録します。ダウンロード処理はクライアントを繰り返し処理するループとして作成します。クライアントが読み込み用のソケットから 0 バイト読み込むときに、コネクションがクローズされたと解釈されて handle_close() が呼び出されます。
このクライアントがどのように実行されるかの1つの例です
$ python asyncore_http_client.py
http://www.python.org/: connecting to ('www.python.org', 80)
http://www.doughellmann.com/PyMOTW/contents.html: connecting to ('www.doughellmann.com', 80)
root: LOOP STARTING
http://www.python.org/: readable() -> True
http://www.python.org/: writable() -> True
http://www.doughellmann.com/PyMOTW/contents.html: readable() -> True
http://www.doughellmann.com/PyMOTW/contents.html: writable() -> True
http://www.doughellmann.com/PyMOTW/contents.html: handle_connect()
http://www.doughellmann.com/PyMOTW/contents.html: handle_write() -> "GET http://www.doughellmann.com/PyMOTW/contents.html HTTP/1.0
"
http://www.python.org/: readable() -> True
http://www.python.org/: writable() -> True
http://www.doughellmann.com/PyMOTW/contents.html: readable() -> True
http://www.doughellmann.com/PyMOTW/contents.html: handle_read() -> 583 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: writable() -> True
http://www.doughellmann.com/PyMOTW/contents.html: readable() -> True
http://www.doughellmann.com/PyMOTW/contents.html: handle_close()
http://www.doughellmann.com/PyMOTW/contents.html: handle_read() -> 0 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: writable() -> True
http://www.python.org/: handle_connect()
http://www.python.org/: handle_write() -> "GET http://www.python.org/ HTTP/1.0
"
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 2896 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 4344 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 287 bytes
http://www.python.org/: readable() -> True
http://www.python.org/: handle_close()
http://www.python.org/: handle_read() -> 0 bytes
root: LOOP DONE
http://www.python.org/ got 22007 bytes
http://www.doughellmann.com/PyMOTW/contents.html got 583 bytes
サーバ¶
次のサンプルは、 SocketServer のサンプルから EchoServer を再実装することで、サーバでの asyncore の使用方法を説明します。3つのクラスがあります。 EchoServer はクライアントからコネクションを受け取り、それぞれのコネクションを扱う EchoHandler インスタンスを作成します。 EchoClient は、前説で紹介した HTTP クライアントと同様の asyncore のディスパッチャです。
import asyncore
import logging
class EchoServer(asyncore.dispatcher):
"""Receives connections and establishes handlers for each client.
"""
def __init__(self, address):
self.logger = logging.getLogger('EchoServer')
asyncore.dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.bind(address)
self.address = self.socket.getsockname()
self.logger.debug('binding to %s', self.address)
self.listen(1)
return
def handle_accept(self):
# クライアントがソケットへ接続したときに呼び出される
client_info = self.accept()
self.logger.debug('handle_accept() -> %s', client_info[1])
EchoHandler(sock=client_info[0])
# 一度に一クライアントのみを扱うのでハンドラを設定したらクローズする
# 普通はクローズせずにサーバは停止命令を受け取るか、永遠に実行される
self.handle_close()
return
def handle_close(self):
self.logger.debug('handle_close()')
self.close()
return
class EchoHandler(asyncore.dispatcher):
"""Handles echoing messages from a single client.
"""
def __init__(self, sock, chunk_size=256):
self.chunk_size = chunk_size
self.logger = logging.getLogger('EchoHandler%s' % str(sock.getsockname()))
asyncore.dispatcher.__init__(self, sock=sock)
self.data_to_write = []
return
def writable(self):
"""We want to write if we have received data."""
response = bool(self.data_to_write)
self.logger.debug('writable() -> %s', response)
return response
def handle_write(self):
"""Write as much as possible of the most recent message we have received."""
data = self.data_to_write.pop()
sent = self.send(data[:self.chunk_size])
if sent < len(data):
remaining = data[sent:]
self.data.to_write.append(remaining)
self.logger.debug('handle_write() -> (%d) "%s"', sent, data[:sent])
if not self.writable():
self.handle_close()
def handle_read(self):
"""Read an incoming message from the client and put it into our outgoing queue."""
data = self.recv(self.chunk_size)
self.logger.debug('handle_read() -> (%d) "%s"', len(data), data)
self.data_to_write.insert(0, data)
def handle_close(self):
self.logger.debug('handle_close()')
self.close()
class EchoClient(asyncore.dispatcher):
"""Sends messages to the server and receives responses.
"""
def __init__(self, host, port, message, chunk_size=512):
self.message = message
self.to_send = message
self.received_data = []
self.chunk_size = chunk_size
self.logger = logging.getLogger('EchoClient')
asyncore.dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.logger.debug('connecting to %s', (host, port))
self.connect((host, port))
return
def handle_connect(self):
self.logger.debug('handle_connect()')
def handle_close(self):
self.logger.debug('handle_close()')
self.close()
received_message = ''.join(self.received_data)
if received_message == self.message:
self.logger.debug('RECEIVED COPY OF MESSAGE')
else:
self.logger.debug('ERROR IN TRANSMISSION')
self.logger.debug('EXPECTED "%s"', self.message)
self.logger.debug('RECEIVED "%s"', received_message)
return
def writable(self):
self.logger.debug('writable() -> %s', bool(self.to_send))
return bool(self.to_send)
def handle_write(self):
sent = self.send(self.to_send[:self.chunk_size])
self.logger.debug('handle_write() -> (%d) "%s"', sent, self.to_send[:sent])
self.to_send = self.to_send[sent:]
def handle_read(self):
data = self.recv(self.chunk_size)
self.logger.debug('handle_read() -> (%d) "%s"', len(data), data)
self.received_data.append(data)
if __name__ == '__main__':
import socket
logging.basicConfig(level=logging.DEBUG,
format='%(name)s: %(message)s',
)
address = ('localhost', 0) # カーネルにポート番号を割り当てさせる
server = EchoServer(address)
ip, port = server.address # 与えられたポート番号を調べる
client = EchoClient(ip, port, message=open('lorem.txt', 'r').read())
asyncore.loop()
EchoServer と EchoHandler は、違う処理を行うので別々のクラスで定義されます。EchoServer がコネクションを受け付けると、新たなソケットが作成されます。EchoServer 内部から個々のクライアントへディスパッチしようと試みるというよりも、asyncore が管理するソケットマップを活用して EchoHandler が作成されます。
$ python asyncore_echo_server.py
EchoServer: binding to ('127.0.0.1', 59605)
EchoClient: connecting to ('127.0.0.1', 59605)
EchoClient: writable() -> True
EchoClient: handle_connect()
EchoClient: handle_write() -> (512) "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque vel arcu. Vivamus
purus orci, iaculis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra f"
EchoClient: writable() -> True
EchoServer: handle_accept() -> ('127.0.0.1', 59606)
EchoServer: handle_close()
EchoClient: handle_write() -> (225) "ringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
"
EchoClient: writable() -> False
EchoHandler('127.0.0.1', 59605): writable() -> False
EchoHandler('127.0.0.1', 59605): handle_read() -> (256) "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque ve"
EchoClient: writable() -> False
EchoHandler('127.0.0.1', 59605): writable() -> True
EchoHandler('127.0.0.1', 59605): handle_read() -> (256) "l arcu. Vivamus
purus orci, iaculis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra f"
EchoHandler('127.0.0.1', 59605): handle_write() -> (256) "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque ve"
EchoHandler('127.0.0.1', 59605): writable() -> True
EchoClient: writable() -> False
EchoHandler('127.0.0.1', 59605): writable() -> True
EchoClient: handle_read() -> (256) "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque ve"
EchoHandler('127.0.0.1', 59605): handle_read() -> (225) "ringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
"
EchoHandler('127.0.0.1', 59605): handle_write() -> (256) "l arcu. Vivamus
purus orci, iaculis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra f"
EchoHandler('127.0.0.1', 59605): writable() -> True
EchoClient: writable() -> False
EchoHandler('127.0.0.1', 59605): writable() -> True
EchoClient: handle_read() -> (256) "l arcu. Vivamus
purus orci, iaculis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra f"
EchoHandler('127.0.0.1', 59605): handle_write() -> (225) "ringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
"
EchoHandler('127.0.0.1', 59605): writable() -> False
EchoHandler('127.0.0.1', 59605): handle_close()
EchoClient: writable() -> False
EchoClient: handle_read() -> (225) "ringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
"
EchoClient: writable() -> False
EchoClient: handle_close()
EchoClient: RECEIVED COPY OF MESSAGE
EchoClient: handle_read() -> (0) ""
このサンプルのサーバ、ハンドラ、クライアントオブジェクトは、1つのプロセス内で asyncore が全て同じソケットマップで管理します。クライアントとサーバを分割するには、単純にスクリプトを分割して両方のスクリプトで asyncore.loop`() を実行してください。あるディスパッチャがクローズされると、asyncore が管理するマップから削除されて、そのマップが空っぽになったときにループが終了します。
その他のイベントループを扱う¶
時々、親アプリケーションのイベントループから asyncore のイベントループを統合する必要があります。例えば、GUI アプリケーションは、全ての非同期通信の処理されるまで待つように UI をブロックしたくありません。ブロックしてしまうと、非同期に処理させる意味がありません。このような統合を簡単にするために、 asyncore.loop() の引数に timeout を設定して、そのループを実行する時間を制限します。
import asyncore
import logging
from asyncore_http_client import HttpClient
logging.basicConfig(level=logging.DEBUG,
format='%(name)s: %(message)s',
)
clients = [
HttpClient('http://www.doughellmann.com/PyMOTW/contents.html'),
HttpClient('http://www.python.org/'),
]
loop_counter = 0
while asyncore.socket_map:
loop_counter += 1
logging.debug('loop_counter=%s', loop_counter)
asyncore.loop(timeout=1, count=1)
このクライアントは asyncore.loop() 内で呼び出すごとにデータを読み込むかを訪ねます。その UI が他のイベントハンドラの処理を行っていないとき、小さな処理を行うには GUI ツールキットのアイドルハンドラかその他の仕組みから、独自の while ループではなく asyncore.loop() を呼び出せます。
$ python asyncore_loop.py
http://www.doughellmann.com/PyMOTW/contents.html: connecting to ('www.doughellmann.com', 80)
http://www.python.org/: connecting to ('www.python.org', 80)
root: loop_counter=1
http://www.doughellmann.com/PyMOTW/contents.html: readable() -> True
http://www.doughellmann.com/PyMOTW/contents.html: writable() -> True
http://www.python.org/: readable() -> True
http://www.python.org/: writable() -> True
http://www.doughellmann.com/PyMOTW/contents.html: handle_connect()
http://www.doughellmann.com/PyMOTW/contents.html: handle_write() -> "GET http://www.doughellmann.com/PyMOTW/contents.html HTTP/1.0
"
root: loop_counter=2
http://www.doughellmann.com/PyMOTW/contents.html: readable() -> True
http://www.python.org/: readable() -> True
http://www.python.org/: writable() -> True
http://www.doughellmann.com/PyMOTW/contents.html: handle_read() -> 583 bytes
root: loop_counter=3
http://www.doughellmann.com/PyMOTW/contents.html: readable() -> True
http://www.python.org/: readable() -> True
http://www.python.org/: writable() -> True
http://www.doughellmann.com/PyMOTW/contents.html: handle_close()
http://www.doughellmann.com/PyMOTW/contents.html: handle_read() -> 0 bytes
root: loop_counter=4
http://www.python.org/: readable() -> True
http://www.python.org/: writable() -> True
http://www.python.org/: handle_connect()
http://www.python.org/: handle_write() -> "GET http://www.python.org/ HTTP/1.0
"
root: loop_counter=5
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=6
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=7
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=8
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=9
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 2896 bytes
root: loop_counter=10
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=11
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=12
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=13
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=14
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=15
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=16
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=17
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1448 bytes
root: loop_counter=18
http://www.python.org/: readable() -> True
http://www.python.org/: handle_read() -> 1735 bytes
root: loop_counter=19
http://www.python.org/: readable() -> True
http://www.python.org/: handle_close()
http://www.python.org/: handle_read() -> 0 bytes
ファイルを扱う¶
普通はソケットで asyncore を使用しますが、非同期にファイルを読み込めると便利なときもあります(ネットワーク設定せずにネットワークサービスをテストするときにファイルを使用する、もしくは巨大なデータファイルを読み書きする処理の一部等)。このような処理のために asyncore は file_dispatcher と file_wrapper クラスを提供します。
import asyncore
import os
class FileReader(asyncore.file_dispatcher):
def writable(self):
return False
def handle_read(self):
data = self.recv(256)
print 'READ: (%d) "%s"' % (len(data), data)
def handle_expt(self):
# 帯域外のデータにみえるイベントを無視する
pass
def handle_close(self):
self.close()
lorem_fd = os.open('lorem.txt', os.O_RDONLY)
reader = FileReader(lorem_fd)
asyncore.loop()
このサンプルは Python 2.5.2 でテストしたので、ファイルディスクリプタを取得するために os.open() を使用しました。Python 2.6 以上では file_dispatcher がファイルディスクリプタへの fileno() メソッドで自動的に変換されます。
$ python asyncore_file_dispatcher.py
READ: (256) "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque ve"
READ: (256) "l arcu. Vivamus
purus orci, iaculis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra f"
READ: (225) "ringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
"
READ: (0) ""
See also
- asyncore
- 本モジュールの標準ライブラリドキュメント
- asynchat
- asyncore 上で構築されるサーバクライアント間でメッセージのやり取りに基づいたプロトコルを簡単に実装する asynchat モジュール
- SocketServer
- スレッドとフォークを使用して別の EchoServer のサンプルを提供する SocketServer モジュール