New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
bpo-35934: Add socket.create_server() utility function #11784
Changes from 35 commits
ffa07b3
5618b21
6b3e634
cbaa3c1
9832435
17a6b7b
29b1d69
e4063f2
057d831
b4883cb
fb0e442
3e876c5
e9fb489
231455f
fbdce4e
2e9e48c
4f28c47
281b914
e003dfe
0a893ca
2d247a2
2c3c85c
1931e7c
2364a89
1d13a9c
46a562e
9cd6f01
d0e69bb
f93058b
63d762e
e95da59
22ea974
ad8a219
3b3df83
d75a600
265b225
426907d
d62499a
7b75345
0cf3bb1
12bbf0c
caa7605
ecac919
f786884
e19b28f
5b49125
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -595,6 +595,58 @@ The following functions all create :ref:`socket objects <socket-objects>`. | |
.. versionchanged:: 3.2 | ||
*source_address* was added. | ||
|
||
.. function:: create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, dualstack_ipv6=False) | ||
|
||
Convenience function which creates a :data:`SOCK_STREAM` type socket | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason why you don't just say it creates a TCP socket? I think this is guaranteed by Posix for AF_INET and AF_INET6 with SOCK_STREAM and proto set to zero. |
||
bound to *address* (a 2-tuple ``(host, port)``) and return the socket | ||
object. | ||
|
||
*family* should be either :data:`AF_INET` or :data:`AF_INET6`. | ||
*backlog* is the queue size passed to :meth:`socket.listen`. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Describe what backlog=None means. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good idea. Updated doc (including your suggestion of making it default to 0 instead of None):
|
||
*reuse_port* dictates whether to set :data:`SO_REUSEPORT` socket option. | ||
|
||
If *dualstack_ipv6* is true and the platform supports it the socket will | ||
gpshead marked this conversation as resolved.
Show resolved
Hide resolved
|
||
be able to accept both IPv4 and IPv6 connections. | ||
In this case the address returned by :meth:`socket.getpeername` when an IPv4 | ||
connection occurs will be an IPv6 address represented as an IPv4-mapped IPv6 | ||
address like ``":ffff:127.0.0.1"``. | ||
If *dualstack_ipv6* is false it will explicitly disable this functionality | ||
on platforms that enable it by default (e.g. Linux). | ||
For platforms not supporting this functionality natively you could use this | ||
`MultipleSocketsListener recipe <http://code.activestate.com/recipes/578504/>`__. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use an https link. (also, activestate appears to be down or DOSed right now) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch. Done. |
||
This parameter can be used in conjunction with :func:`has_dualstack_ipv6`. | ||
|
||
Here's an echo server example listening on all interfaces, port 8888, | ||
accepting both IPv4 and IPv6 connections (if supported): | ||
|
||
:: | ||
|
||
import socket | ||
|
||
with socket.create_server(("", 8888), | ||
dualstack_ipv6=socket.has_dualstack_ipv6()) as server: | ||
conn, addr = server.accept() | ||
with conn: | ||
while True: | ||
data = conn.recv(1024) | ||
if not data: | ||
break | ||
conn.send(data) | ||
|
||
.. note:: | ||
On POSIX :data:`SO_REUSEADDR` socket option is set in order to immediately | ||
reuse previous sockets which were bound on the same *address* and remained | ||
in TIME_WAIT state. | ||
|
||
.. versionadded:: 3.8 | ||
|
||
.. function:: has_dualstack_ipv6() | ||
|
||
Return ``True`` if the platform supports creating a :data:`SOCK_STREAM` | ||
socket which can handle both :data:`AF_INET` or :data:`AF_INET6` | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Describe how one creates such a socket when this returns There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is already described in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was assuming someone will want to use this in combination with plain There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd leave it out as a low-level detail. As of now, the only thing that the doc mentions is the existence of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think AF_INET and AF_INET6 are mutually exclusive for a given socket. Perhaps it is more accurate to say the socket can handle both IP v4 and v6. |
||
(IPv4 / IPv6) connections. | ||
|
||
.. versionadded:: 3.8 | ||
|
||
.. function:: fromfd(fd, family, type, proto=0) | ||
|
||
|
@@ -1779,6 +1831,40 @@ sends traffic to the first one connected successfully. :: | |
print('Received', repr(data)) | ||
|
||
|
||
The two examples above can be rewritten by using :meth:`socket.create_server` | ||
and :meth:`socket.create_connection` convenience functions. | ||
If the platform supports it, :meth:`socket.create_server` has the extra | ||
advantage of creating an agnostic IPv4/IPv6 server: | ||
|
||
:: | ||
|
||
# Echo server program | ||
import socket | ||
|
||
HOST = None | ||
PORT = 50007 | ||
s = socket.create_server((HOST, PORT), dualstack_ipv6=socket.has_dualstack_ipv6()) | ||
conn, addr = s.accept() | ||
with conn: | ||
print('Connected by', addr) | ||
while True: | ||
data = conn.recv(1024) | ||
if not data: break | ||
conn.send(data) | ||
|
||
:: | ||
|
||
# Echo client program | ||
import socket | ||
|
||
HOST = 'daring.cwi.nl' | ||
PORT = 50007 | ||
with socket.create_connection((HOST, PORT)) as s: | ||
s.sendall(b'Hello, world') | ||
data = s.recv(1024) | ||
print('Received', repr(data)) | ||
|
||
|
||
The next example shows how to write a very simple network sniffer with raw | ||
sockets on Windows. The example requires administrator privileges to modify | ||
the interface:: | ||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -214,6 +214,15 @@ contain characters unrepresentable at the OS level. | |||||
(Contributed by Serhiy Storchaka in :issue:`33721`.) | ||||||
|
||||||
|
||||||
socket | ||||||
------ | ||||||
|
||||||
Added :meth:`~socket.create_server()` and :meth:`~socket.has_dualstack_ipv6()` | ||||||
convenience functions to automatize the necessary tasks usually involved when | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You may name tasks:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "automatize" isn't a word. :) I'd suggest "to automate" or "that encapsulate" perhaps. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. False friend betrayed me. =) |
||||||
creating a server socket, including accepting both IPv4 and IPv6 connections | ||||||
on the same socket. (Contributed by Giampaolo Rodola in :issue:`17561`.) | ||||||
|
||||||
|
||||||
shutil | ||||||
------ | ||||||
|
||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -530,9 +530,8 @@ class RPCClient(SocketIO): | |
nextseq = 1 # Requests coming from the client are odd numbered | ||
|
||
def __init__(self, address, family=socket.AF_INET, type=socket.SOCK_STREAM): | ||
self.listening_sock = socket.socket(family, type) | ||
self.listening_sock.bind(address) | ||
self.listening_sock.listen(1) | ||
self.listening_sock = socket.create_server( | ||
address, family=family, type=type, backlog=1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is type a valid keyword? If not, maybe this needs a test case :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ouch! I missed that, thanks. I just removed this part and left idlelib alone in f786884. I will probably get back at this in a separate PR because it's more controversial. |
||
|
||
def accept(self): | ||
working_sock, address = self.listening_sock.accept() | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -60,8 +60,8 @@ | |
EAGAIN = getattr(errno, 'EAGAIN', 11) | ||
EWOULDBLOCK = getattr(errno, 'EWOULDBLOCK', 11) | ||
|
||
__all__ = ["fromfd", "getfqdn", "create_connection", | ||
"AddressFamily", "SocketKind"] | ||
__all__ = ["fromfd", "getfqdn", "create_connection", "create_server", | ||
"has_dualstack_ipv6", "AddressFamily", "SocketKind"] | ||
__all__.extend(os._get_exports_list(_socket)) | ||
|
||
# Set up the socket.AF_* socket.SOCK_* constants as members of IntEnums for | ||
|
@@ -728,6 +728,93 @@ def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, | |
else: | ||
raise error("getaddrinfo returns an empty list") | ||
|
||
|
||
def has_dualstack_ipv6(): | ||
"""Return True if the platform supports creating a SOCK_STREAM socket | ||
which can handle both AF_INET and AF_INET6 (IPv4 / IPv6) connections. | ||
""" | ||
if not has_ipv6 \ | ||
or not hasattr(_socket, 'IPPROTO_IPV6') \ | ||
or not hasattr(_socket, 'IPV6_V6ONLY'): | ||
return False | ||
try: | ||
with socket(AF_INET6, SOCK_STREAM) as sock: | ||
gpshead marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) | ||
return True | ||
except error: | ||
return False | ||
|
||
|
||
def create_server(address, *, family=AF_INET, backlog=None, reuse_port=False, | ||
dualstack_ipv6=False): | ||
"""Convenience function which creates a SOCK_STREAM type socket | ||
bound to *address* (a 2-tuple (host, port)) and return the socket | ||
object. | ||
|
||
*family* should be either AF_INET or AF_INET6. | ||
*backlog* is the queue size passed to socket.listen(). | ||
*reuse_port* dictate whether to use the SO_REUSEPORT socket option. | ||
*dualstack_ipv6*: if true and the platform supports it, it will | ||
create a socket able to accept both IPv4 and IPv6 connections. | ||
When false it will explicitly disable this option on platforms | ||
that enable it by default (e.g. Linux). | ||
|
||
>>> with create_server((None, 8000)) as server: | ||
... while True: | ||
... conn, addr = server.accept() | ||
... # handle new connection | ||
""" | ||
if reuse_port and not hasattr(_socket, "SO_REUSEPORT"): | ||
raise ValueError("SO_REUSEPORT not supported on this platform") | ||
if dualstack_ipv6: | ||
if not has_dualstack_ipv6(): | ||
raise ValueError("dualstack_ipv6 not supported on this platform") | ||
if family != AF_INET6: | ||
raise ValueError("dualstack_ipv6 requires AF_INET6 family") | ||
gpshead marked this conversation as resolved.
Show resolved
Hide resolved
|
||
sock = socket(family, SOCK_STREAM) | ||
try: | ||
# Note about Windows. We don't set SO_REUSEADDR because: | ||
# 1) It's unnecessary: bind() will succeed even in case of a | ||
# previous closed socket on the same address and still in | ||
# TIME_WAIT state. | ||
# 2) If set, another socket will be free to bind() on the same | ||
# address, effectively preventing this one from accepting | ||
# connections. Also, it may set the process in a state where | ||
# it'll no longer respond to any signals or graceful kills. | ||
# See: msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx | ||
if os.name not in ('nt', 'cygwin') and \ | ||
hasattr(_socket, 'SO_REUSEADDR'): | ||
try: | ||
sock.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) | ||
except error: | ||
# Fail later on bind(), for platforms which may not | ||
# support this option. | ||
pass | ||
if reuse_port: | ||
sock.setsockopt(SOL_SOCKET, SO_REUSEPORT, 1) | ||
if has_ipv6 and family == AF_INET6: | ||
if dualstack_ipv6: | ||
sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 0) | ||
elif hasattr(_socket, "IPV6_V6ONLY") and \ | ||
hasattr(_socket, "IPPROTO_IPV6"): | ||
# Disable IPv4/IPv6 dual stack support (enabled by | ||
# default on Linux) which makes a single socket | ||
# listen on both address families in order to | ||
# eliminate cross-platform differences. | ||
sock.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) | ||
try: | ||
sock.bind(address) | ||
except error as err: | ||
err_msg = '%s (while attempting to bind on address %r)' % \ | ||
(err.strerror, address) | ||
raise error(err.errno, err_msg) from None | ||
sock.listen(backlog or 0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why not just set the backlog default value to 0? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I think that makes sense. Done. |
||
return sock | ||
except Exception: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use plain except to catch all exceptions (e.g. KeyboardInterrupt could be raised during a slow DNS lookup) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good point. I think it's saner to just catch |
||
sock.close() | ||
raise | ||
|
||
|
||
def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): | ||
"""Resolve host and port into list of address info entries. | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Creating a new API in 2019 that defaults to IPv4 without dualstack seems backwards. People will use these defaults without realizing that they're shutting out a big part of the world.
Our defaults should require users to opt-out of dualstack IPv6 and that the dualstack parameter should perhaps be a tri-state rather than a bool: 'no', 'required', 'allow'. the default I'd look for would be allow. such that it would not be an error on a mythical non-dualstack capable host.
When dualstack is enabled, does the family= value have a useful meaning?
On non-dualstack-socket capable hosts (what are those even?) where the user needs a server listening on both IPv4 and IPv6, isn't it true that they need to create two server sockets?
create_server()
doesn't really have a way to address that case. Supporting such a situation is going to require ahas_dualstack_ipv6()
conditional and wrangling multiple sockets no matter what.For a situation where people just want a single "most capable" server socket, your four line conditional code seems like it belongs inside of
create_server
itself.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The rationale for having dual-stack disabled by default IMO are 2, consistency and desirableness. In details:
getpeername()
will return an IPv4 mapped address like'::ffff:127.0.0.1'
, which is surprising (more here). To me this is not desirable regardless of how much spread IPv6 adoption is, or even if dual-stack was supported on all platforms. Not all users who want IPv6 necessarily want the dual stack (honestly, initially I was even tempted to exclude this option completely because of all this platform differences - and I'm still happy to take that into consideration BTW).Also note that asyncio disables dual-stack by default, basically for the same reasons. asyncio case is different though. It does not expose an option to enable dual-stack because it would be useless: it is already able to serve both families with 2 distinct sockets which are handled by the IO loop. We cannot do that here though, unless we use the external recipe I mentioned in the doc (and which should not end up in socket.py IMO).
I hear you. I also considered that, but I couldn't come up with an API which made sense. Basically that's why the API evolved into being "explicit" by forcing you to specify both family and dualstack_ipv6.
Windows does not support dual-stack. Linux, OSX and FreeBSD do. I updated the doc adding:
"Most POSIX platforms are supposed to support this option."
As this currently stands, family has to be explicitly set to
AF_INET6
. One may argue that should be implicit ifdualstack_ipv6=True
. As of now I'm more inclined to think it should be set explicitly because:socket.socket()
default family isAF_INET
, so the same way you dosocket.socket(AF_INET6)
you should dosocket.create_server(addr, family=AF_INET6)
.But I agree this is indeed debatable and I have mixed feelings about it.
Correct. If dual-stack is not supported natively by the kernel you have to create 2 sockets. The ActiveState recipe I mentioned in the doc includes a class which does that. The reason for not including it in socket.py is because the API is non-obvious / incompatible. E.g.
fileno()
,detach()
,family
and possibly others wouldn't know what to return.