Source code for hio.core.tcp.serving

# -*- encoding: utf-8 -*-
"""
hio.core.tcp.serving Module

Accepter listens and accepts incoming TCP socket connections
Server is subclass of Acceptor
Server creates Remoters
Remoter is accepted incoming socket connection

ServerTls is subclass of Server
RemoterTls is subclass of Remoter

"""

import sys
import os
import errno
import socket
import ssl
from collections import deque
from contextlib import contextmanager

from ... import help
from ...base import tyming, doing
from .. import coring

[docs]logger = help.ogler.getLogger()
@contextmanager
[docs]def openServer(cls=None, **kwa): """ Wrapper to create and open TCP Server instances When used in with statement block, calls .close() on exit of with block Parameters: cls is Class instance of subclass instance Usage: with openServer() as server0: server0. with openServer(cls=ServerTls) as server0: server0. """ server = None cls = cls if cls is not None else Server try: server = cls(**kwa) server.reopen() # opens accept socket yield server finally: if server: server.close()
[docs]class Acceptor(tyming.Tymee): """ Acceptor Base Class for Server. Nonblocking TCP Socket Acceptor Class. Listen socket for incoming TCP connections See tyming.Tymee for inherited attributes, properties, and methods Attributes: .ha is (host,port) duple (two tuple) host = "" or "0.0.0.0" means listen on all interfaces .eha is normalized (host, port) duple for incoming TLS connections as external facing address for TLS context .bs is buffer size .ss is server listen socket for incoming accept requests .axes is deque of accepte connection duples (ca, cs) .opened is boolean, True if listen socket .ss opened. False otherwise """ def __init__(self, ha=None, bs=8096, **kwa): """ Initialization method for instance. ha is host address duple (host, port) listen interfaces host = "" or "0.0.0.0" means listen on all interfaces bs = buffer size """ super(Acceptor, self).__init__(**kwa) self.ha = ha or (host, port) # ha = host address eha = self.ha host, port = eha # expand so can normalize host host = coring.normalizeHost(host) if host in ('0.0.0.0',): host = '127.0.0.1' # need specific interface for tls elif host in ("::", "0:0:0:0:0:0:0:0"): host = "::1" # need specific interface for tls self.eha = (host, port) self.bs = bs self.ss = None # listen socket for accepts self.axes = deque() # deque of duple (ca, cs) accepted connections self.opened = False
[docs] def actualBufSizes(self): """ Returns duple of the the actual socket send and receive buffer size (send, receive) """ if not self.ss: return (0, 0) return (self.ss.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF), self.ss.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF))
[docs] def open(self): """ Opens binds listen socket in non blocking mode. if socket not closed properly, binding socket gets error OSError: (48, 'Address already in use') """ #create server socket ss to listen on self.ss = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # make socket address reusable. # the SO_REUSEADDR flag tells the kernel to reuse a local socket in # TIME_WAIT state, without waiting for its natural timeout to expire. self.ss.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # Linux TCP allocates twice the requested size if sys.platform.startswith('linux'): bs = 2 * self.bs # get size is twice the set size else: bs = self.bs if self.ss.getsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF) < bs: self.ss.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, self.bs) if self.ss.getsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF) < bs: self.ss.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, self.bs) self.ss.setblocking(0) #non blocking socket # bind to listen socket (host, port) to receive connections self.ss.bind(self.ha) self.ss.listen(5) #try: # bind to listen socket (host, port) to receive connections #self.ss.bind(self.ha) #self.ss.listen(5) #except OSError as ex: #return False self.ha = self.ss.getsockname() # get resolved ha after bind self.opened = True return True
[docs] def reopen(self): """ Idempotently opens listen socket """ self.close() return self.open()
[docs] def close(self): """ Closes listen socket. """ if self.ss: try: self.ss.shutdown(socket.SHUT_RDWR) # shutdown socket except OSError as ex: pass self.ss.close() #close socket self.ss = None self.opened = False
[docs] def accept(self): """ Accept new connection nonblocking Returns duple (cs, ca) of connected socket and connected host address Otherwise if no new connection returns (None, None) """ # accept new virtual connected socket created from server socket try: cs, ca = self.ss.accept() # virtual connection (socket, host address) except OSError as ex: if ex.errno in (errno.EAGAIN, errno.EWOULDBLOCK): return (None, None) # nothing yet raise # re-raise return (cs, ca)
[docs] def serviceAccepts(self): """ Service any accept requests Adds to .cxes dict key by ca """ while True: cs, ca = self.accept() if not cs: break self.axes.append((cs, ca))
[docs]class Server(Acceptor): """ Nonblocking TCP Socket Server Class. Listen socket for incoming TCP connections that generates Remoter sockets for accepted connections See tyming.Tymee for inherited attributes, properties, and methods Inherited Attributes: .ha is (host,port) duple (two tuple) host = "" or "0.0.0.0" means listen on all interfaces .eha is normalized (host, port) duple for incoming TLS connections as external facing address for TLS context .bs is buffer size .ss is server listen socket for incoming accept requests .axes is deque of accepte connection duples (ca, cs) .opened is boolean, True if listen socket .ss opened. False otherwise Attributes: .tymeout is tymeout in seconds for connection refresh .wl is WireLog instance if any .ixes is dict of incoming connections indexed by remote (host, port) duple """
[docs] Tymeout = 1.0 # tymeout in seconds virtual tyme
def __init__(self, ha=None, host="", port=56000, tymeout=None, wl=None, **kwa): """ Initialization method for instance. Parameters: ha is TCP/IP (host, port) duple for listen socket host is default TCP/IP host address for listen socket "" or "0.0.0.0" is listen on all interfaces port is default TCP/IP port tymeout is default tymeout for to pass to remoters for incoming connections wl is WireLog instance if any """ ha = ha or (host, port) super(Server, self).__init__(ha=ha, **kwa) self.tymeout = tymeout if tymeout is not None else self.Tymeout self.wl = wl self.ixes = dict() # ready to rx tx incoming connections, Remoter instances
[docs] def wind(self, tymth): """ Inject new tymist.tymth as new ._tymth. Changes tymist.tyme base. Updates winds .tymer .tymth """ super(Server, self).wind(tymth) for rm in self.ixes.values(): # remotoer rm.wind(tymth)
[docs] def serviceAxes(self): """ Service axes For each newly accepted connection in .axes create Remoter and add to .ixes keyed by ca """ self.serviceAccepts() # populate .axes while self.axes: cs, ca = self.axes.popleft() if ca != cs.getpeername(): #or self.eha != cs.getsockname(): raise ValueError("Accepted socket host addresses malformed for " "peer. eha {0} != {1}, ca {2} != {3}\n".format( self.eha, cs.getsockname(), ca, cs.getpeername())) remoter = Remoter(tymth=self.tymth, ha=cs.getsockname(), ca=ca, cs=cs, bs=self.bs, wl=self.wl, timeout=self.tymeout) if ca in self.ixes and self.ixes[ca] is not remoter: self.shutdownIx[ca] self.ixes[ca] = remoter
[docs] def serviceConnects(self): """ Service connects is method name to be used """ self.serviceAxes()
[docs] def shutdownIx(self, ca, how=socket.SHUT_RDWR): """ Shutdown remoter given by connection address ca """ if ca not in self.ixes: emsg = "Invalid connection address '{0}'".format(ca) raise ValueError(emsg) self.ixes[ca].shutdown(how=how)
[docs] def shutdownSendIx(self, ca): """ Shutdown send on remoter given by connection address ca """ if ca not in self.ixes: emsg = "Invalid connection address '{0}'".format(ca) raise ValueError(emsg) self.ixes[ca].shutdownSend()
[docs] def shutdownReceiveIx(self, ca): """ Shutdown send on remoter given by connection address ca """ if ca not in self.ixes: emsg = "Invalid connection address '{0}'".format(ca) raise ValueError(emsg) self.ixes[ca].shutdownReceive()
[docs] def closeIx(self, ca): """ Shutdown and close remoter given by connection address ca """ if ca not in self.ixes: emsg = "Invalid connection address '{0}'".format(ca) raise ValueError(emsg) self.ixes[ca].close()
[docs] def closeAllIx(self): """ Shutdown and close all remoter connections """ for rm in self.ixes.values(): # remoter rm.close()
[docs] def close(self): """ Close all sockets """ super(Server, self).close() # call super close self.closeAllIx()
[docs] def removeIx(self, ca, close=True): """ Remove remoter given by connection address ca """ if ca not in self.ixes: emsg = "Invalid connection address '{0}'".format(ca) raise ValueError(emsg) if close: self.ixes[ca].close() # shutdown and close socket del self.ixes[ca]
[docs] def serviceReceivesIx(self, ca): """ Service receives for remoter by connection address ca """ if ca not in self.ixes: emsg = "Invalid connection address '{0}'".format(ca) raise ValueError(emsg) self.ixes[ca].serviceReceives()
[docs] def serviceReceivesAllIx(self): """ Service receives for all remoters in .ixes """ for ix in self.ixes.values(): ix.serviceReceives()
[docs] def transmitIx(self, data, ca): ''' Queue data onto .txbs for remoter given by connection address ca ''' if ca not in self.ixes: emsg = "Invalid connection address '{0}'".format(ca) raise ValueError(emsg) self.ixes[ca].tx(data)
[docs] def serviceSendsAllIx(self): """ Service transmits for all remoters in .ixes """ for rm in self.ixes.values(): # remoter rm.serviceSends()
[docs] def service(self): """ Service connects and service receives and sends for all ix. """ self.serviceConnects() self.serviceReceivesAllIx() self.serviceSendsAllIx()
[docs]def initServerContext(context=None, version=None, certify=None, keypath=None, certpath=None, cafilepath=None ): """ Initialize and return context for TLS Server IF context is None THEN create a context IF version is None THEN create context using ssl library default ELSE create context with version If certify is not None then use certify value provided Otherwise use default context = context object for tls/ssl If None use default version = ssl protocol version If None use default certify = cert requirement If None use default ssl.CERT_NONE = 0 ssl.CERT_OPTIONAL = 1 ssl.CERT_REQUIRED = 2 keypath = pathname of local server side PKI private key file path If given apply to context certpath = pathname of local server side PKI public cert file path If given apply to context cafilepath = Cert Authority file path to use to verify client cert If given apply to context """ if context is None: # create context if not version: # use default context with default protocol version context = ssl.create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) context.verify_mode = certify if certify is not None else ssl.CERT_REQUIRED else: # create context with specified protocol version context = ssl.SSLContext(protocol=version) # disable bad protocols versions context.options |= ssl.OP_NO_SSLv2 context.options |= ssl.OP_NO_SSLv3 # disable compression to prevent CRIME attacks (OpenSSL 1.0+) context.options |= getattr(ssl._ssl, "OP_NO_COMPRESSION", 0) # Prefer the server's ciphers by default fro stronger encryption context.options |= getattr(ssl._ssl, "OP_CIPHER_SERVER_PREFERENCE", 0) # Use single use keys in order to improve forward secrecy context.options |= getattr(ssl._ssl, "OP_SINGLE_DH_USE", 0) context.options |= getattr(ssl._ssl, "OP_SINGLE_ECDH_USE", 0) # disallow ciphers with known vulnerabilities context.set_ciphers(ssl._RESTRICTED_SERVER_CIPHERS) context.verify_mode = certify if certify is not None else ssl.CERT_REQUIRED if cafilepath: context.load_verify_locations(cafile=cafilepath, capath=None, cadata=None) elif context.verify_mode != ssl.CERT_NONE: context.load_default_certs(purpose=ssl.Purpose.CLIENT_AUTH) if keypath or certpath: context.load_cert_chain(certfile=certpath, keyfile=keypath) return context
[docs]class ServerTls(Server): """ Server with Nonblocking TLS/SSL support Nonblocking TCP Socket Server Class. Listen socket for incoming TCP connections RemoterTLS sockets for accepted connections See tyming.Tymee for inherited attributes, properties, and methods Inherited Attributes: .ha is (host,port) duple (two tuple) host = "" or "0.0.0.0" means listen on all interfaces .eha is normalized (host, port) duple for incoming TLS connections as external facing address for TLS context .bs is buffer size .ss is server listen socket for incoming accept requests .axes is deque of accepte connection duples (ca, cs) .opened is boolean, True if listen socket .ss opened. False otherwise .timeout is timeout in seconds for connection refresh .wl is WireLog instance if any .ixes is dict of incoming connections indexed by remote (host, port) duple Attributes: .context is TLS context instance .version is TLS version .certify is boolean, True to client certify, False otherwise .keypath is path to key file .certpath is path to cert file .cafilepath is path to ca file """ def __init__(self, context=None, version=None, certify=None, keypath=None, certpath=None, cafilepath=None, **kwa): """ Initialization method for instance. """ super(ServerTls, self).__init__(**kwa) self.cxes = dict() # accepted incoming connections, RemoterTLS instances self.context = context self.version = version self.certify = certify self.keypath = keypath self.certpath = certpath self.cafilepath = cafilepath self.context = initServerContext(context=context, version=version, certify=certify, keypath=keypath, certpath=certpath, cafilepath=cafilepath )
[docs] def serviceAxes(self): """ Service accepteds For each new accepted connection create RemoterTLS and add to .cxes Not Handshaked """ self.serviceAccepts() # populate .axes while self.axes: cs, ca = self.axes.popleft() if ca != cs.getpeername() or self.eha != cs.getsockname(): raise ValueError("Accepted socket host addresses malformed for " "peer ha {0} != {1}, ca {2} != {3}\n".format( self.ha, cs.getsockname(), ca, cs.getpeername())) remoter = RemoterTls(tymth=self.tymth, ha=cs.getsockname(), ca=ca, bs=self.bs, cs=cs, wl=self.wl, timeout=self.tymeout, context=self.context, version=self.version, certify=self.certify, keypath=self.keypath, certpath=self.certpath, cafilepath=self.cafilepath, ) self.cxes[ca] = remoter
[docs] def serviceCxes(self): """ Service handshakes for every remoter in .cxes If successful move to .ixes """ for ca, cx in list(self.cxes.items()): if cx.serviceHandshake(): self.ixes[ca] = cx del self.cxes[ca]
[docs] def serviceConnects(self): """ Service accept and handshake attempts If not already accepted and handshaked Then make nonblocking attempt For each successful handshaked add to .ixes Returns handshakeds """ self.serviceAxes() self.serviceCxes()
[docs]class Remoter(tyming.Tymee): """ Class to service an incoming nonblocking TCP connection from a remote client. Should only be used from Acceptor subclass """
[docs] Tymeout = 0.0 # virtual tymeout in seconds
def __init__(self, ha, ca, cs, tymeout=None, refreshable=True, bs=8096, wl=None, **kwa ): """ Initialization method for instance. ha = host address duple (host, port) near side of connection. cs.getsockname() useful for debugging after cs is closed ca = connection address used as key in severs's ixes. Need this to know how to delete from .ixes when connection closed as .cs loses cs.getpeername() after its closed. cs = connection socket object tymeout = tymeout for .tymer refreshable = True if tx/rx activity refreshes timer False otherwise bs = buffer size wl = WireLog object if any """ super(Remoter, self).__init__(**kwa) self.ha = ha # connection address of server self.ca = ca # connection address of peer used to index in server.ixes self.cs = cs # connection socket if self.cs: self.cs.setblocking(0) # linux does not preserve blocking from accept self.tymeout = tymeout if tymeout is not None else self.Tymeout self.tymer = tyming.Tymer(tymth=self.tymth, duration=self.tymeout) self.cutoff = False # True when detect connection closed on far side self.refreshable = refreshable self.bs = bs self.txbs = bytearray() # bytearray of data to send self.rxbs = bytearray() # bytearray of data received self.wl = wl
[docs] def wind(self, tymth): """ Inject new tymist.tymth as new ._tymth. Changes tymist.tyme base. Updates winds .tymer .tymth """ super(Remoter, self).wind(tymth) self.tymer.wind(tymth)
[docs] def shutdown(self, how=socket.SHUT_RDWR): """ Shutdown connected socket .cs """ if self.cs: try: self.cs.shutdown(how) # shutdown socket except OSError as ex: pass
[docs] def shutdownSend(self): """ Shutdown send on connected socket .cs """ if self.cs: try: self.shutdown(how=socket.SHUT_WR) # shutdown socket except OSError as ex: pass
[docs] def shutdownReceive(self): """ Shutdown receive on connected socket .cs """ if self.cs: try: self.shutdown(how=socket.SHUT_RD) # shutdown socket except OSError as ex: pass
[docs] def close(self): """ Shutdown and close connected socket .cs """ if self.cs: self.shutdown() self.cs.close() #close socket self.cs = None
[docs] def refresh(self): """ Restart tymer """ self.tymer.restart()
[docs] def receive(self): """ Perform non blocking receive on connected socket .cs If no data then returns None If connection closed then returns '' Otherwise returns data data is string in python2 and bytes in python3 """ try: data = self.cs.recv(self.bs) except OSError as ex: # ex.args[0] == ex.errno for better os compatibility. # the value of a given errno.XXXXX may be different on each os if ex.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK): return None # keep trying elif ex.args[0] in (errno.ECONNRESET, errno.ENETRESET, errno.ENETUNREACH, errno.EHOSTUNREACH, errno.ENETDOWN, errno.EHOSTDOWN, errno.ETIMEDOUT, errno.ECONNREFUSED): self.cutoff = True # this signals need to close/reopen connection return bytes() # data empty else: raise # re-raise if data: # connection open if self.wl: # log over the wire rx self.wl.writeRx(data, self.ca) if self.refreshable: self.refresh() else: # data empty so connection closed on other end self.cutoff = True return data
[docs] def serviceReceives(self): """ Service receives until no more """ while not self.cutoff: data = self.receive() if not data: break self.rxbs.extend(data)
[docs] def serviceReceiveOnce(self): ''' Retrieve from server only one reception ''' if not self.cutoff: data = self.receive() if data: self.rxbs.extend(data)
[docs] def clearRxbs(self): """ Clear .rxbs """ del self.rxbs[:]
[docs] def send(self, data): """ Perform non blocking send on connected socket .cs. Return number of bytes sent data is string in python2 and bytes in python3 """ try: count = self.cs.send(data) #result is number of bytes sent except OSError as ex: # ex.args[0] == ex.errno for better compat # the value of a given errno.XXXXX may be different on each os if ex.args[0] in (errno.EAGAIN, errno.EWOULDBLOCK): count = 0 # blocked try again elif ex.args[0] in (errno.ECONNRESET, errno.ENETRESET, errno.ENETUNREACH, errno.EHOSTUNREACH, errno.ENETDOWN, errno.EHOSTDOWN, errno.ETIMEDOUT, errno.ECONNREFUSED): self.cutoff = True # this signals need to close/reopen connection count = 0 else: raise if count: if self.wl: self.wl.writeTx(data[:count], self.ca) if self.refreshable: self.refresh() return count
[docs] def tx(self, data): ''' Queue data onto .txbs ''' self.txbs.extend(data)
[docs] def serviceSends(self): """ Service transmits For each tx if all bytes sent then keep sending until partial send or no more to send If partial send reattach and return """ while self.txbs and not self.cutoff: count = self.send(self.txbs) del self.txbs[:count] break # try again later
[docs]class RemoterTls(Remoter): """ Class to service an incoming nonblocking TCP/TLS connection from a remote client. Should only be used from Acceptor subclass Provides nonblocking TLS/SSL support """ def __init__(self, context=None, version=None, certify=None, keypath=None, certpath=None, cafilepath=None, **kwa): """ Initialization method for instance. context = context object for tls/ssl If None use default version = ssl version If None use default certify = cert requirement If None use default ssl.CERT_NONE = 0 ssl.CERT_OPTIONAL = 1 ssl.CERT_REQUIRED = 2 keypath = pathname of local server side PKI private key file path If given apply to context certpath = pathname of local server side PKI public cert file path If given apply to context cafilepath = Cert Authority file path to use to verify client cert If given apply to context """ super(RemoterTls, self).__init__(**kwa) self.connected = False # True once ssl handshake completed self.context = initServerContext(context=context, version=version, certify=certify, keypath=keypath, certpath=certpath, cafilepath=cafilepath ) self.wrap()
[docs] def close(self): """ Shutdown and close connected socket .cs """ if self.cs: self.shutdown() self.cs.close() #close socket self.cs = None self.connected = False
[docs] def wrap(self): """ Wrap socket .cs in ssl context """ self.cs = self.context.wrap_socket(self.cs, server_side=True, do_handshake_on_connect=False)
[docs] def handshake(self): """ Attempt nonblocking ssl handshake to .ha Returns True if successful Returns False if not so try again later """ try: self.cs.do_handshake() except ssl.SSLError as ex: if ex.errno in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): return False elif ex.errno in (ssl.SSL_ERROR_EOF, ): self.shutclose() raise # should give up here nicely else: self.shutclose() raise except OSError as ex: self.shutclose() if ex.errno in (errno.ECONNABORTED, ): raise # should give up here nicely raise except Exception as ex: self.shutclose() raise self.connected = True return True
[docs] def serviceHandshake(self): """ Service connection and handshake attempt If not already accepted and handshaked Then make nonblocking attempt Returns .handshaked """ if not self.connected: self.handshake() return self.connected
[docs] def receive(self): """ Perform non blocking receive on connected socket .cs If no data then returns None If connection closed then returns '' Otherwise returns data data is string in python2 and bytes in python3 """ try: data = self.cs.recv(self.bs) except OSError as ex: # ssl.SSLError is a subtype of OSError # ex.args[0] == ex.errno for better compat # the value of a given errno.XXXXX may be different on each os if ex.args[0] in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): return None # blocked waiting for data elif ex.args[0] in (errno.ECONNRESET, errno.ENETRESET, errno.ENETUNREACH, errno.EHOSTUNREACH, errno.ENETDOWN, errno.EHOSTDOWN, errno.ETIMEDOUT, errno.ECONNREFUSED, ssl.SSLEOFError): self.cutoff = True # this signals need to close/reopen connection return bytes() # data empty else: raise # re-raise if data: # connection open if self.wl: # log over the wire rx self.wl.writeRx(data, who=self.cs.getpeername()) else: # data empty so connection closed on other end self.cutoff = True return data
[docs] def send(self, data): """ Perform non blocking send on connected socket .cs. Return number of bytes sent data is string in python2 and bytes in python3 """ try: result = self.cs.send(data) #result is number of bytes sent except OSError as ex: # ssl.SSLError is a subtype of OSError # ex.args[0] == ex.errno for better compat # the value of a given errno.XXXXX may be different on each os if ex.args[0] in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): result = 0 # blocked try again elif ex.args[0] in (errno.ECONNRESET, errno.ENETRESET, errno.ENETUNREACH, errno.EHOSTUNREACH, errno.ENETDOWN, errno.EHOSTDOWN, errno.ETIMEDOUT, errno.ECONNREFUSED, ssl.SSLEOFError): self.cutoff = True # this signals need to close/reopen connection result = 0 else: raise if result: if self.wl: self.wl.writeTx(data[:result], who=self.cs.getpeername()) return result
[docs]class ServerDoer(doing.Doer): """ Basic TCP Server Doer See Doer for inherited attributes, properties, and methods. Attributes: .server is TCP Server instance Properties: """ def __init__(self, server, **kwa): """ Initialize Parameters: server is TCP Server instance """ super(ServerDoer, self).__init__(**kwa) self.server = server if self.tymth: self.server.wind(self.tymth)
[docs] def wind(self, tymth): """ Inject new tymist.tymth as new ._tymth. Changes tymist.tyme base. Updates winds .tymer .tymth """ super(ServerDoer, self).wind(tymth) self.server.wind(tymth)
[docs] def enter(self): """""" self.server.reopen()
[docs] def recur(self, tyme): """""" self.server.service()
[docs] def exit(self): """""" self.server.close()
[docs]class EchoServerDoer(ServerDoer): """ Echo TCP Server Just echoes back to client whatever it receives from client See Doer for inherited attributes, properties, and methods. Inherited Attributes: .server is TCP Server instance """
[docs] def enter(self): """""" self.server.reopen()
[docs] def recur(self, tyme): """""" self.server.service() for ca, ix in self.server.ixes.items(): if ix.rxbs: ix.tx(bytes(ix.rxbs)) # echo back ix.clearRxbs()
[docs] def exit(self): """""" self.server.close()