Skip to content

Commit eb7e29f

Browse files
authored
bpo-35934: Add socket.create_server() utility function (GH-11784)
1 parent 58721a9 commit eb7e29f

File tree

17 files changed

+289
-88
lines changed

17 files changed

+289
-88
lines changed

Doc/library/socket.rst

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -595,6 +595,50 @@ The following functions all create :ref:`socket objects <socket-objects>`.
595595
.. versionchanged:: 3.2
596596
*source_address* was added.
597597

598+
.. function:: create_server(address, *, family=AF_INET, backlog=0, reuse_port=False, dualstack_ipv6=False)
599+
600+
Convenience function which creates a TCP socket bound to *address* (a 2-tuple
601+
``(host, port)``) and return the socket object.
602+
603+
*family* should be either :data:`AF_INET` or :data:`AF_INET6`.
604+
*backlog* is the queue size passed to :meth:`socket.listen`; when ``0``
605+
a default reasonable value is chosen.
606+
*reuse_port* dictates whether to set the :data:`SO_REUSEPORT` socket option.
607+
608+
If *dualstack_ipv6* is true and the platform supports it the socket will
609+
be able to accept both IPv4 and IPv6 connections, else it will raise
610+
:exc:`ValueError`. Most POSIX platforms and Windows are supposed to support
611+
this functionality.
612+
When this functionality is enabled the address returned by
613+
:meth:`socket.getpeername` when an IPv4 connection occurs will be an IPv6
614+
address represented as an IPv4-mapped IPv6 address.
615+
If *dualstack_ipv6* is false it will explicitly disable this functionality
616+
on platforms that enable it by default (e.g. Linux).
617+
This parameter can be used in conjunction with :func:`has_dualstack_ipv6`:
618+
619+
::
620+
621+
import socket
622+
623+
addr = ("", 8080) # all interfaces, port 8080
624+
if socket.has_dualstack_ipv6():
625+
s = socket.create_server(addr, family=socket.AF_INET6, dualstack_ipv6=True)
626+
else:
627+
s = socket.create_server(addr)
628+
629+
.. note::
630+
On POSIX platforms the :data:`SO_REUSEADDR` socket option is set in order to
631+
immediately reuse previous sockets which were bound on the same *address*
632+
and remained in TIME_WAIT state.
633+
634+
.. versionadded:: 3.8
635+
636+
.. function:: has_dualstack_ipv6()
637+
638+
Return ``True`` if the platform supports creating a TCP socket which can
639+
handle both IPv4 and IPv6 connections.
640+
641+
.. versionadded:: 3.8
598642

599643
.. function:: fromfd(fd, family, type, proto=0)
600644

@@ -1778,7 +1822,6 @@ sends traffic to the first one connected successfully. ::
17781822
data = s.recv(1024)
17791823
print('Received', repr(data))
17801824

1781-
17821825
The next example shows how to write a very simple network sniffer with raw
17831826
sockets on Windows. The example requires administrator privileges to modify
17841827
the interface::

Doc/whatsnew/3.8.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,15 @@ contain characters unrepresentable at the OS level.
294294
(Contributed by Serhiy Storchaka in :issue:`33721`.)
295295

296296

297+
socket
298+
------
299+
300+
Added :meth:`~socket.create_server()` and :meth:`~socket.has_dualstack_ipv6()`
301+
convenience functions to automate the necessary tasks usually involved when
302+
creating a server socket, including accepting both IPv4 and IPv6 connections
303+
on the same socket. (Contributed by Giampaolo Rodola in :issue:`17561`.)
304+
305+
297306
shutil
298307
------
299308

Lib/ftplib.py

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -302,26 +302,7 @@ def sendeprt(self, host, port):
302302

303303
def makeport(self):
304304
'''Create a new socket and send a PORT command for it.'''
305-
err = None
306-
sock = None
307-
for res in socket.getaddrinfo(None, 0, self.af, socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
308-
af, socktype, proto, canonname, sa = res
309-
try:
310-
sock = socket.socket(af, socktype, proto)
311-
sock.bind(sa)
312-
except OSError as _:
313-
err = _
314-
if sock:
315-
sock.close()
316-
sock = None
317-
continue
318-
break
319-
if sock is None:
320-
if err is not None:
321-
raise err
322-
else:
323-
raise OSError("getaddrinfo returns an empty list")
324-
sock.listen(1)
305+
sock = socket.create_server(("", 0), family=self.af, backlog=1)
325306
port = sock.getsockname()[1] # Get proper port
326307
host = self.sock.getsockname()[0] # Get proper host
327308
if self.af == socket.AF_INET:

Lib/socket.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@
6060
EAGAIN = getattr(errno, 'EAGAIN', 11)
6161
EWOULDBLOCK = getattr(errno, 'EWOULDBLOCK', 11)
6262

63-
__all__ = ["fromfd", "getfqdn", "create_connection",
64-
"AddressFamily", "SocketKind"]
63+
__all__ = ["fromfd", "getfqdn", "create_connection", "create_server",
64+
"has_dualstack_ipv6", "AddressFamily", "SocketKind"]
6565
__all__.extend(os._get_exports_list(_socket))
6666

6767
# Set up the socket.AF_* socket.SOCK_* constants as members of IntEnums for
@@ -728,6 +728,89 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT,
728728
else:
729729
raise error("getaddrinfo returns an empty list")
730730

731+
732+
def has_dualstack_ipv6():
733+
"""Return True if the platform supports creating a SOCK_STREAM socket
734+
which can handle both AF_INET and AF_INET6 (IPv4 / IPv6) connections.
735+
"""
736+
if not has_ipv6 \
737+
or not hasattr(_socket, 'IPPROTO_IPV6') \
738+
or not hasattr(_socket, 'IPV6_V6ONLY'):
739+
return False
740+
try:
741+
with socket(AF_INET6, SOCK_STREAM) as sock:
742+
sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0)
743+
return True
744+
except error:
745+
return False
746+
747+
748+
def create_server(address, *, family=AF_INET, backlog=0, reuse_port=False,
749+
dualstack_ipv6=False):
750+
"""Convenience function which creates a SOCK_STREAM type socket
751+
bound to *address* (a 2-tuple (host, port)) and return the socket
752+
object.
753+
754+
*family* should be either AF_INET or AF_INET6.
755+
*backlog* is the queue size passed to socket.listen().
756+
*reuse_port* dictates whether to use the SO_REUSEPORT socket option.
757+
*dualstack_ipv6*: if true and the platform supports it, it will
758+
create an AF_INET6 socket able to accept both IPv4 or IPv6
759+
connections. When false it will explicitly disable this option on
760+
platforms that enable it by default (e.g. Linux).
761+
762+
>>> with create_server((None, 8000)) as server:
763+
... while True:
764+
... conn, addr = server.accept()
765+
... # handle new connection
766+
"""
767+
if reuse_port and not hasattr(_socket, "SO_REUSEPORT"):
768+
raise ValueError("SO_REUSEPORT not supported on this platform")
769+
if dualstack_ipv6:
770+
if not has_dualstack_ipv6():
771+
raise ValueError("dualstack_ipv6 not supported on this platform")
772+
if family != AF_INET6:
773+
raise ValueError("dualstack_ipv6 requires AF_INET6 family")
774+
sock = socket(family, SOCK_STREAM)
775+
try:
776+
# Note about Windows. We don't set SO_REUSEADDR because:
777+
# 1) It's unnecessary: bind() will succeed even in case of a
778+
# previous closed socket on the same address and still in
779+
# TIME_WAIT state.
780+
# 2) If set, another socket is free to bind() on the same
781+
# address, effectively preventing this one from accepting
782+
# connections. Also, it may set the process in a state where
783+
# it'll no longer respond to any signals or graceful kills.
784+
# See: msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx
785+
if os.name not in ('nt', 'cygwin') and \
786+
hasattr(_socket, 'SO_REUSEADDR'):
787+
try:
788+
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1)
789+
except error:
790+
# Fail later on bind(), for platforms which may not
791+
# support this option.
792+
pass
793+
if reuse_port:
794+
sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1)
795+
if has_ipv6 and family == AF_INET6:
796+
if dualstack_ipv6:
797+
sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0)
798+
elif hasattr(_socket, "IPV6_V6ONLY") and \
799+
hasattr(_socket, "IPPROTO_IPV6"):
800+
sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1)
801+
try:
802+
sock.bind(address)
803+
except error as err:
804+
msg = '%s (while attempting to bind on address %r)' % \
805+
(err.strerror, address)
806+
raise error(err.errno, msg) from None
807+
sock.listen(backlog)
808+
return sock
809+
except error:
810+
sock.close()
811+
raise
812+
813+
731814
def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0):
732815
"""Resolve host and port into list of address info entries.
733816

Lib/test/_test_multiprocessing.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3334,9 +3334,7 @@ def _listener(cls, conn, families):
33343334
new_conn.close()
33353335
l.close()
33363336

3337-
l = socket.socket()
3338-
l.bind((test.support.HOST, 0))
3339-
l.listen()
3337+
l = socket.create_server((test.support.HOST, 0))
33403338
conn.send(l.getsockname())
33413339
new_conn, addr = l.accept()
33423340
conn.send(new_conn)
@@ -4345,9 +4343,7 @@ def _child_test_wait_socket(cls, address, slow):
43454343

43464344
def test_wait_socket(self, slow=False):
43474345
from multiprocessing.connection import wait
4348-
l = socket.socket()
4349-
l.bind((test.support.HOST, 0))
4350-
l.listen()
4346+
l = socket.create_server((test.support.HOST, 0))
43514347
addr = l.getsockname()
43524348
readers = []
43534349
procs = []

Lib/test/eintrdata/eintr_tester.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -285,12 +285,9 @@ def test_sendmsg(self):
285285
self._test_send(lambda sock, data: sock.sendmsg([data]))
286286

287287
def test_accept(self):
288-
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
288+
sock = socket.create_server((support.HOST, 0))
289289
self.addCleanup(sock.close)
290-
291-
sock.bind((support.HOST, 0))
292290
port = sock.getsockname()[1]
293-
sock.listen()
294291

295292
code = '\n'.join((
296293
'import socket, time',

Lib/test/test_asyncio/functional.py

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,13 @@ def tcp_server(self, server_prog, *,
6060
else:
6161
addr = ('127.0.0.1', 0)
6262

63-
sock = socket.socket(family, socket.SOCK_STREAM)
64-
63+
sock = socket.create_server(addr, family=family, backlog=backlog)
6564
if timeout is None:
6665
raise RuntimeError('timeout is required')
6766
if timeout <= 0:
6867
raise RuntimeError('only blocking sockets are supported')
6968
sock.settimeout(timeout)
7069

71-
try:
72-
sock.bind(addr)
73-
sock.listen(backlog)
74-
except OSError as ex:
75-
sock.close()
76-
raise ex
77-
7870
return TestThreadedServer(
7971
self, sock, server_prog, timeout, max_clients)
8072

Lib/test/test_asyncio/test_events.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -667,9 +667,7 @@ def data_received(self, data):
667667
super().data_received(data)
668668
self.transport.write(expected_response)
669669

670-
lsock = socket.socket()
671-
lsock.bind(('127.0.0.1', 0))
672-
lsock.listen(1)
670+
lsock = socket.create_server(('127.0.0.1', 0), backlog=1)
673671
addr = lsock.getsockname()
674672

675673
message = b'test data'
@@ -1118,9 +1116,7 @@ def connection_made(self, transport):
11181116
super().connection_made(transport)
11191117
proto.set_result(self)
11201118

1121-
sock_ob = socket.socket(type=socket.SOCK_STREAM)
1122-
sock_ob.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1123-
sock_ob.bind(('0.0.0.0', 0))
1119+
sock_ob = socket.create_server(('0.0.0.0', 0))
11241120

11251121
f = self.loop.create_server(TestMyProto, sock=sock_ob)
11261122
server = self.loop.run_until_complete(f)
@@ -1136,9 +1132,7 @@ def connection_made(self, transport):
11361132
server.close()
11371133

11381134
def test_create_server_addr_in_use(self):
1139-
sock_ob = socket.socket(type=socket.SOCK_STREAM)
1140-
sock_ob.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
1141-
sock_ob.bind(('0.0.0.0', 0))
1135+
sock_ob = socket.create_server(('0.0.0.0', 0))
11421136

11431137
f = self.loop.create_server(MyProto, sock=sock_ob)
11441138
server = self.loop.run_until_complete(f)

Lib/test/test_asyncio/test_streams.py

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -592,8 +592,7 @@ async def handle_client(self, client_reader, client_writer):
592592
await client_writer.wait_closed()
593593

594594
def start(self):
595-
sock = socket.socket()
596-
sock.bind(('127.0.0.1', 0))
595+
sock = socket.create_server(('127.0.0.1', 0))
597596
self.server = self.loop.run_until_complete(
598597
asyncio.start_server(self.handle_client,
599598
sock=sock,
@@ -605,8 +604,7 @@ def handle_client_callback(self, client_reader, client_writer):
605604
client_writer))
606605

607606
def start_callback(self):
608-
sock = socket.socket()
609-
sock.bind(('127.0.0.1', 0))
607+
sock = socket.create_server(('127.0.0.1', 0))
610608
addr = sock.getsockname()
611609
sock.close()
612610
self.server = self.loop.run_until_complete(
@@ -796,10 +794,7 @@ def test_drain_raises(self):
796794

797795
def server():
798796
# Runs in a separate thread.
799-
sock = socket.socket()
800-
with sock:
801-
sock.bind(('localhost', 0))
802-
sock.listen(1)
797+
with socket.create_server(('localhost', 0)) as sock:
803798
addr = sock.getsockname()
804799
q.put(addr)
805800
clt, _ = sock.accept()

Lib/test/test_epoll.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@
4141
class TestEPoll(unittest.TestCase):
4242

4343
def setUp(self):
44-
self.serverSocket = socket.socket()
45-
self.serverSocket.bind(('127.0.0.1', 0))
46-
self.serverSocket.listen()
44+
self.serverSocket = socket.create_server(('127.0.0.1', 0))
4745
self.connections = [self.serverSocket]
4846

4947
def tearDown(self):

0 commit comments

Comments
 (0)