From 5e4a7d8dc7781a4c0dfacecdc4db0eb34beb1391 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 21 Sep 2015 18:33:43 +0200 Subject: Issue #23630, asyncio: host parameter of loop.create_server() can now be a sequence of strings. Patch written by Yann Sionneau. --- Doc/library/asyncio-eventloop.rst | 13 ++++++++++--- Lib/asyncio/base_events.py | 35 ++++++++++++++++++++++++++++------- Lib/asyncio/events.py | 3 ++- Lib/test/test_asyncio/test_events.py | 33 +++++++++++++++++++++++++++++++++ Misc/ACKS | 1 + 5 files changed, 74 insertions(+), 11 deletions(-) diff --git a/Doc/library/asyncio-eventloop.rst b/Doc/library/asyncio-eventloop.rst index 277e404..78a07a0 100644 --- a/Doc/library/asyncio-eventloop.rst +++ b/Doc/library/asyncio-eventloop.rst @@ -331,9 +331,12 @@ Creating listening connections Parameters: - * If *host* is an empty string or ``None``, all interfaces are assumed - and a list of multiple sockets will be returned (most likely - one for IPv4 and another one for IPv6). + * The *host* parameter can be a string, in that case the TCP server is + bound to *host* and *port*. The *host* parameter can also be a sequence + of strings and in that case the TCP server is bound to all hosts of the + sequence. If *host* is an empty string or ``None``, all interfaces are + assumed and a list of multiple sockets will be returned (most likely one + for IPv4 and another one for IPv6). * *family* can be set to either :data:`socket.AF_INET` or :data:`~socket.AF_INET6` to force the socket to use IPv4 or IPv6. If not set @@ -365,6 +368,10 @@ Creating listening connections The function :func:`start_server` creates a (:class:`StreamReader`, :class:`StreamWriter`) pair and calls back a function with this pair. + .. versionchanged:: 3.4.4 + + The *host* parameter can now be a sequence of strings. + .. coroutinemethod:: BaseEventLoop.create_unix_server(protocol_factory, path=None, \*, sock=None, backlog=100, ssl=None) diff --git a/Lib/asyncio/base_events.py b/Lib/asyncio/base_events.py index c205445..a50e005 100644 --- a/Lib/asyncio/base_events.py +++ b/Lib/asyncio/base_events.py @@ -18,6 +18,7 @@ import collections import concurrent.futures import heapq import inspect +import itertools import logging import os import socket @@ -787,6 +788,15 @@ class BaseEventLoop(events.AbstractEventLoop): return transport, protocol @coroutine + def _create_server_getaddrinfo(self, host, port, family, flags): + infos = yield from self.getaddrinfo(host, port, family=family, + type=socket.SOCK_STREAM, + flags=flags) + if not infos: + raise OSError('getaddrinfo({!r}) returned empty list'.format(host)) + return infos + + @coroutine def create_server(self, protocol_factory, host=None, port=None, *, family=socket.AF_UNSPEC, @@ -795,7 +805,13 @@ class BaseEventLoop(events.AbstractEventLoop): backlog=100, ssl=None, reuse_address=None): - """Create a TCP server bound to host and port. + """Create a TCP server. + + The host parameter can be a string, in that case the TCP server is bound + to host and port. + + The host parameter can also be a sequence of strings and in that case + the TCP server is bound to all hosts of the sequence. Return a Server object which can be used to stop the service. @@ -813,13 +829,18 @@ class BaseEventLoop(events.AbstractEventLoop): reuse_address = os.name == 'posix' and sys.platform != 'cygwin' sockets = [] if host == '': - host = None + hosts = [None] + elif (isinstance(host, str) or + not isinstance(host, collections.Iterable)): + hosts = [host] + else: + hosts = host - infos = yield from self.getaddrinfo( - host, port, family=family, - type=socket.SOCK_STREAM, proto=0, flags=flags) - if not infos: - raise OSError('getaddrinfo() returned empty list') + fs = [self._create_server_getaddrinfo(host, port, family=family, + flags=flags) + for host in hosts] + infos = yield from tasks.gather(*fs, loop=self) + infos = itertools.chain.from_iterable(infos) completed = False try: diff --git a/Lib/asyncio/events.py b/Lib/asyncio/events.py index d5f0d45..1e42ddd 100644 --- a/Lib/asyncio/events.py +++ b/Lib/asyncio/events.py @@ -305,7 +305,8 @@ class AbstractEventLoop: If host is an empty string or None all interfaces are assumed and a list of multiple sockets will be returned (most likely - one for IPv4 and another one for IPv6). + one for IPv4 and another one for IPv6). The host parameter can also be a + sequence (e.g. list) of hosts to bind to. family can be set to either AF_INET or AF_INET6 to force the socket to use IPv4 or IPv6. If not set it will be determined diff --git a/Lib/test/test_asyncio/test_events.py b/Lib/test/test_asyncio/test_events.py index ba1fa5d..24c3f1e 100644 --- a/Lib/test/test_asyncio/test_events.py +++ b/Lib/test/test_asyncio/test_events.py @@ -745,6 +745,39 @@ class EventLoopTestsMixin: self.assertEqual(cm.exception.errno, errno.EADDRINUSE) self.assertIn(str(httpd.address), cm.exception.strerror) + @mock.patch('asyncio.base_events.socket') + def create_server_multiple_hosts(self, family, hosts, mock_sock): + @asyncio.coroutine + def getaddrinfo(host, port, *args, **kw): + if family == socket.AF_INET: + return [[family, socket.SOCK_STREAM, 6, '', (host, port)]] + else: + return [[family, socket.SOCK_STREAM, 6, '', (host, port, 0, 0)]] + + def getaddrinfo_task(*args, **kwds): + return asyncio.Task(getaddrinfo(*args, **kwds), loop=self.loop) + + if family == socket.AF_INET: + mock_sock.socket().getsockbyname.side_effect = [(host, 80) + for host in hosts] + else: + mock_sock.socket().getsockbyname.side_effect = [(host, 80, 0, 0) + for host in hosts] + self.loop.getaddrinfo = getaddrinfo_task + self.loop._start_serving = mock.Mock() + f = self.loop.create_server(lambda: MyProto(self.loop), hosts, 80) + server = self.loop.run_until_complete(f) + self.addCleanup(server.close) + server_hosts = [sock.getsockbyname()[0] for sock in server.sockets] + self.assertEqual(server_hosts, hosts) + + def test_create_server_multiple_hosts_ipv4(self): + self.create_server_multiple_hosts(socket.AF_INET, + ['1.2.3.4', '5.6.7.8']) + + def test_create_server_multiple_hosts_ipv6(self): + self.create_server_multiple_hosts(socket.AF_INET6, ['::1', '::2']) + def test_create_server(self): proto = MyProto(self.loop) f = self.loop.create_server(lambda: proto, '0.0.0.0', 0) diff --git a/Misc/ACKS b/Misc/ACKS index 521468c..d9b32b2 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -1294,6 +1294,7 @@ Adam Simpkins Ravi Sinha Janne Sinkkonen Ng Pheng Siong +Yann Sionneau George Sipe J. Sipprell Kragen Sitaker -- cgit v0.12