Skip to content
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

Add high-level Listener interface #282

Merged
merged 21 commits into from
Aug 18, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
# Except for these ones, which we expect to point to unknown targets:
nitpick_ignore = [
("py:obj", "CapacityLimiter-like object"),
# trio.abc this is documented at random places scattered throughout the
# docs
("py:obj", "bytes-like"),
# trio.abc is documented at random places scattered throughout the docs
("py:mod", "trio.abc"),
]

Expand Down
70 changes: 25 additions & 45 deletions docs/source/reference-io.rst
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,17 @@ Abstract base classes

.. autoexception:: ClosedStreamError

.. currentmodule:: trio.abc

.. autoclass:: trio.abc.Listener
:members:
:show-inheritance:

.. currentmodule:: trio

.. autoexception:: ClosedListenerError



Generic stream implementations
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -140,12 +151,18 @@ abstraction.
:members:
:show-inheritance:

.. autofunction:: socket_stream_pair

.. autofunction:: open_tcp_stream

.. autofunction:: open_ssl_over_tcp_stream

.. autoclass:: SocketListener
:members:
:show-inheritance:

.. autofunction:: open_tcp_listeners

.. autofunction:: open_ssl_over_tcp_listeners


SSL / TLS support
~~~~~~~~~~~~~~~~~
Expand All @@ -167,6 +184,12 @@ create a :class:`SSLStream`:
:show-inheritance:
:members:

And if you're implementing a server, you can use :class:`SSLListener`:

.. autoclass:: SSLListener
:show-inheritance:
:members:


.. module:: trio.socket

Expand Down Expand Up @@ -325,49 +348,6 @@ Socket objects
returns IPv6 addresses). In particular, a hostname of ``None`` is
mapped to the localhost address.

**Modern defaults:** And finally, we took the opportunity to update
the defaults for several socket options that were stuck in the
1980s. You can always use :meth:`~socket.socket.setsockopt` to
change these back, but for trio sockets:

1. Everywhere except Windows, ``SO_REUSEADDR`` is enabled by
default. This is almost always what you want, but if you're in
one of the `rare cases
<https://idea.popcount.org/2014-04-03-bind-before-connect/>`__
where this is undesireable then you can always disable
``SO_REUSEADDR`` manually::

sock.setsockopt(trio.socket.SOL_SOCKET, trio.socket.SO_REUSEADDR, False)

On Windows, ``SO_EXCLUSIVEADDR`` is enabled by
default. Unfortunately, this means that if you stop and restart
a server you may have trouble reacquiring listen ports (i.e., it
acts like Unix without ``SO_REUSEADDR``). To get the Unix-style
``SO_REUSEADDR`` semantics on Windows, you can disable
``SO_EXCLUSIVEADDR``::

sock.setsockopt(trio.socket.SOL_SOCKET, trio.socket.SO_EXCLUSIVEADDR, False)

but be warned that `this may leave your application vulnerable
to port hijacking attacks
<https://msdn.microsoft.com/en-us/library/windows/desktop/ms740621(v=vs.85).aspx>`__.

2. ``IPV6_V6ONLY`` is disabled, i.e., by default on dual-stack
hosts a ``AF_INET6`` socket is able to communicate with both
IPv4 and IPv6 peers, where the IPv4 peers appear to be in the
`"IPv4-mapped" portion of IPv6 address space
<http://www.tcpipguide.com/free/t_IPv6IPv4AddressEmbedding-2.htm>`__. To
make an IPv6-only socket, use something like::

sock = trio.socket.socket(trio.socket.AF_INET6)
sock.setsockopt(trio.socket.IPPROTO_IPV6, trio.socket.IPV6_V6ONLY, True)

This makes trio applications behave more consistently across
different environments.

See `issue #72 <https://github.com/python-trio/trio/issues/72>`__ for
discussion of these defaults.

The following methods are similar to the equivalents in
:func:`socket.socket`, but have some trio-specific quirks:

Expand Down
6 changes: 6 additions & 0 deletions docs/source/reference-testing.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ Inter-task ordering
Streams
-------

Connecting to an in-process socket server
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. autofunction:: open_stream_to_socket_listener


Virtual, controllable streams
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
67 changes: 67 additions & 0 deletions notes-to-self/time-wait-windows-exclusiveaddruse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# On windows, what does SO_EXCLUSIVEADDRUSE actually do? Apparently not what
# the documentation says!
# See: https://stackoverflow.com/questions/45624916/
#
# Specifically, this script seems to demonstrate that it only creates
# conflicts between listening sockets, *not* lingering connected sockets.

import socket
from contextlib import contextmanager

@contextmanager
def report_outcome(tagline):
try:
yield
except OSError as exc:
print("{}: failed".format(tagline))
print(" details: {!r}".format(exc))
else:
print("{}: succeeded".format(tagline))

# Set up initial listening socket
lsock = socket.socket()
lsock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
lsock.bind(("127.0.0.1", 0))
sockaddr = lsock.getsockname()
lsock.listen(10)

# Make connected client and server sockets
csock = socket.socket()
csock.connect(sockaddr)
ssock, _ = lsock.accept()

print("lsock", lsock.getsockname())
print("ssock", ssock.getsockname())

# Can't make a second listener while the first exists
probe = socket.socket()
probe.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
with report_outcome("rebind with existing listening socket"):
probe.bind(sockaddr)

# Now we close the first listen socket, while leaving the connected sockets
# open:
lsock.close()
# This time binding succeeds (contra MSDN!)
probe = socket.socket()
probe.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
with report_outcome("rebind with live connected sockets"):
probe.bind(sockaddr)
probe.listen(10)
print("probe", probe.getsockname())
print("ssock", ssock.getsockname())
probe.close()

# Server-initiated close to trigger TIME_WAIT status
ssock.send(b"x")
assert csock.recv(1) == b"x"
ssock.close()
assert csock.recv(1) == b""

# And does the TIME_WAIT sock prevent binding?
probe = socket.socket()
probe.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1)
with report_outcome("rebind with TIME_WAIT socket"):
probe.bind(sockaddr)
probe.listen(10)
probe.close()
110 changes: 110 additions & 0 deletions notes-to-self/time-wait.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# what does SO_REUSEADDR do, exactly?

# Theory:
#
# - listen1 is bound to port P
# - listen1.accept() returns a connected socket server1, which is also bound
# to port P
# - listen1 is closed
# - we attempt to bind listen2 to port P
# - this fails because server1 is still open, or still in TIME_WAIT, and you
# can't use bind() to bind to a port that still has sockets on it, unless
# both those sockets and the socket being bound have SO_REUSEADDR
#
# The standard way to avoid this is to set SO_REUSEADDR on all listening
# sockets before binding them. And this works, but for somewhat more
# complicated reasons than are often appreciated.
#
# In our scenario above it doesn't really matter for listen1 (assuming the
# port is initially unused).
#
# What is important is that it's set on *server1*. Setting it on listen1
# before calling bind() automatically accomplishes this, because SO_REUSEADDR
# is inherited by accept()ed sockets. But it also works to set it on listen1
# any time before calling accept(), or to set it on server1 directly.
#
# Also, it must be set on listen2 before calling bind(), or it will conflict
# with the lingering server1 socket.

import socket
import errno

import attr

@attr.s(repr=False)
class Options:
listen1_early = attr.ib(default=None)
listen1_middle = attr.ib(default=None)
listen1_late = attr.ib(default=None)
server = attr.ib(default=None)
listen2 = attr.ib(default=None)

def set(self, which, sock):
value = getattr(self, which)
if value is not None:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, value)

def describe(self):
info = []
for f in attr.fields(self.__class__):
value = getattr(self, f.name)
if value is not None:
info.append("{}={}".format(f.name, value))
return "Set/unset: {}".format(", ".join(info))

def time_wait(options):
print(options.describe())

# Find a pristine port (one we can definitely bind to without
# SO_REUSEADDR)
listen0 = socket.socket()
listen0.bind(("127.0.0.1", 0))
sockaddr = listen0.getsockname()
#print(" ", sockaddr)
listen0.close()

listen1 = socket.socket()
options.set("listen1_early", listen1)
listen1.bind(sockaddr)
listen1.listen(1)

options.set("listen1_middle", listen1)

client = socket.socket()
client.connect(sockaddr)

options.set("listen1_late", listen1)

server, _ = listen1.accept()

options.set("server", server)

# Server initiated close to trigger TIME_WAIT status
server.close()
assert client.recv(10) == b""
client.close()

listen1.close()

listen2 = socket.socket()
options.set("listen2", listen2)
try:
listen2.bind(sockaddr)
except OSError as exc:
if exc.errno == errno.EADDRINUSE:
print(" -> EADDRINUSE")
else:
raise
else:
print(" -> ok")

time_wait(Options())
time_wait(Options(listen1_early=True, server=True, listen2=True))
time_wait(Options(listen1_early=True))
time_wait(Options(server=True))
time_wait(Options(listen2=True))
time_wait(Options(listen1_early=True, listen2=True))
time_wait(Options(server=True, listen2=True))
time_wait(Options(listen1_middle=True, listen2=True))
time_wait(Options(listen1_late=True, listen2=True))
time_wait(Options(listen1_middle=True, server=False, listen2=True))
19 changes: 11 additions & 8 deletions trio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,26 +40,29 @@
from ._threads import *
__all__ += _threads.__all__

from ._streams import *
__all__ += _streams.__all__
from ._highlevel_generic import *
__all__ += _highlevel_generic.__all__

from ._signals import *
__all__ += _signals.__all__

from ._network import *
__all__ += _network.__all__
from ._highlevel_socket import *
__all__ += _highlevel_socket.__all__

from ._file_io import *
__all__ += _file_io.__all__

from ._path import *
__all__ += _path.__all__

from ._open_tcp_stream import *
__all__ += _open_tcp_stream.__all__
from ._highlevel_open_tcp_stream import *
__all__ += _highlevel_open_tcp_stream.__all__

from ._ssl_stream_helpers import *
__all__ += _ssl_stream_helpers.__all__
from ._highlevel_open_tcp_listeners import *
__all__ += _highlevel_open_tcp_listeners.__all__

from ._highlevel_ssl_helpers import *
__all__ += _highlevel_ssl_helpers.__all__

# Imported by default
from . import socket
Expand Down
Loading