asyncio内置支持在socket上启用SSL通信。将SSLContext实例传递给创建服务器或客户端连接的协程将启用该支持,并在socket准备好供应用程序使用之前,确保SSL协议设置得当。
来自上一节的基于协程的echo服务器和客户端将在这里进行一些小修改。第一步是创建证书和密钥文件。自签名证书可以使用如下命令创建:
1
| $ openssl req -newkey rsa:2048 -nodes -keyout pymotw.key -x509 -days 365 -out pymotw.crt
|
openssl命令将提示用于生成证书的多个值,然后生成请求的输出文件。
在以前的服务器示例中,不安全的socket设置使用start_server()来创建监听socket。
1 2
| factory = asyncio.start_server(echo, *SERVER_ADDRESS) server = event_loop.run_until_complete(factory)
|
要添加加密,请使用刚刚生成的证书和密钥创建一个SSLContext,然后将该上下文传递给start_server()。
1 2 3 4 5
| ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.check_hostname = False ssl_context.load_cert_chain('pymotw.crt', 'pymotw.key') factory = asyncio.start_server(echo, *SERVER_ADDRESS, ssl=ssl_context)
|
客户端需要进行类似的更改。旧版本使用open_connection()来创建连接到服务器的socket。
1
| reader, writer = await asyncio.open_connection(*address)
|
SSLContext需要再次保护socket的客户端。客户端身份没有被强制执行,所以只需要加载证书。
1 2 3 4
| ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,) ssl_context.check_hostname = False ssl_context.load_verify_locations('pymotw.crt') reader, writer = await asyncio.open_connection(*address, ssl=ssl_context)
|
另外一个需要在客户端做一些修改的是,由于SSL连接不支持发送文件结尾(EOF),客户端将使用NULL字节作为消息终止符。
旧版本的客户端使用write_eof()发送循环。
1 2 3 4 5 6
| for msg in messages: writer.write(msg) log.debug('sending {!r}'.format(msg)) if writer.can_write_eof(): writer.write_eof() await writer.drain()
|
新版本发送0字节(b’\x00’)。
1 2 3 4 5
| for msg in messages: writer.write(msg) log.debug('sending {!r}'.format(msg)) writer.write(b'\x00') await writer.drain()
|
服务器中的echo()协程必须查找NULL字节并在收到时关闭客户端连接。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| async def echo(reader, writer): address = writer.get_extra_info('peername') log = logging.getLogger('echo_{}_{}'.format(*address)) log.debug('connection accepted') while True: data = await reader.read(128) terminate = data.endswith(b'\x00') data = data.rstrip(b'\x00') if data: log.debug('received {!r}'.format(data)) writer.write(data) await writer.drain() log.debug('sent {!r}'.format(data)) if not data or terminate: log.debug('message terminated, closing connection') writer.close() return
|
在一个窗口中运行服务器,在另一个窗口中运行客户端,生成此输出。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| $ python3 asyncio_echo_server_ssl.py asyncio: Using selector: KqueueSelector main: starting up on localhost port 10000 echo_::1_53957: connection accepted echo_::1_53957: received b'This is the message. ' echo_::1_53957: sent b'This is the message. ' echo_::1_53957: received b'It will be sent in parts.' echo_::1_53957: sent b'It will be sent in parts.' echo_::1_53957: message terminated, closing connection $ python3 asyncio_echo_client_ssl.py asyncio: Using selector: KqueueSelector echo_client: connecting to localhost port 10000 echo_client: sending b'This is the message. ' echo_client: sending b'It will be sent ' echo_client: sending b'in parts.' echo_client: waiting for response echo_client: received b'This is the message. ' echo_client: received b'It will be sent in parts.' echo_client: closing main: closing event loop
|
附上完整源码
TCP Echo Server:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| import asyncio import logging import sys import ssl logging.basicConfig( level=logging.DEBUG, format='%(name)s: %(message)s', stream=sys.stderr, ) log = logging.getLogger('main') async def echo(reader, writer): address = writer.get_extra_info('peername') log = logging.getLogger('echo_{}_{}'.format(*address)) log.debug('connection accepted') while True: data = await reader.read(128) terminate = data.endswith(b'\x00') data = data.rstrip(b'\x00') if data: log.debug('received {!r}'.format(data)) writer.write(data) await writer.drain() log.debug('sent {!r}'.format(data)) if not data or terminate: log.debug('message terminated, closing connection') writer.close() return SERVER_ADDRESS = ('localhost', 10000) event_loop = asyncio.get_event_loop() ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) ssl_context.check_hostname = False ssl_context.load_cert_chain('pymotw.crt', 'pymotw.key') factory = asyncio.start_server(echo, *SERVER_ADDRESS, ssl=ssl_context) server = event_loop.run_until_complete(factory) log.debug('starting up on {} port {}'.format(*SERVER_ADDRESS)) try: event_loop.run_forever() except KeyboardInterrupt: pass finally: log.debug('closing server') server.close() event_loop.run_until_complete(server.wait_closed()) log.debug('closing event loop') event_loop.close()
|
TCP Echo Client:
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 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52
| import asyncio import logging import sys import ssl MESSAGES = [ b'This is the message. ', b'It will be sent ', b'in parts.', ] logging.basicConfig( level=logging.DEBUG, format='%(name)s: %(message)s', stream=sys.stderr, ) log = logging.getLogger('main') async def echo_client(address, messages): log = logging.getLogger('echo_client') log.debug('connecting to {} port {}'.format(*address)) ssl_context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH,) ssl_context.check_hostname = False ssl_context.load_verify_locations('pymotw.crt') reader, writer = await asyncio.open_connection(*address, ssl=ssl_context) for msg in messages: writer.write(msg) log.debug('sending {!r}'.format(msg)) writer.write(b'\x00') await writer.drain() log.debug('waiting for response') while True: data = await reader.read(128) if data: log.debug('received {!r}'.format(data)) else: log.debug('closing') writer.close() return SERVER_ADDRESS = ('localhost', 10000) event_loop = asyncio.get_event_loop() try: event_loop.run_until_complete( echo_client(SERVER_ADDRESS, MESSAGES) ) finally: log.debug('closing event loop') event_loop.close()
|
本文翻译自《The Python3 Standard Library By Example》asyncio相关章节