Asynchrones prüfen ob (TCP) Ports offen sind - asyncio

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Marco82
User
Beiträge: 19
Registriert: Samstag 4. April 2020, 21:07

Hallo zusammen,

hoffentlich kann mir hier aus dem Forum jemand einen Tipp geben, ich bin schon langsam am verzweifeln...
Ich versuche asynchron zu prüfen ob eine Reihe von Ports offen ist. Seit zwei Tagen lese ich mich schon durch Internet und ich bekomme das einfach nicht hin.

Ich verwende folgende Funktion um synchron zu prüfen ob ein Port Remote erreichbar ist:

Code: Alles auswählen

def check_port(hostname: str, port: int) -> bool:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(2)
    try:
        s.connect((hostname, int(port)))
        s.shutdown(socket.SHUT_RDWR)
        return True
    except Exception as ex:
        print(ex)
        return False
    finally:
        s.close()
Nun habe ich gelesen, dass man I/O Anfragen, die üblicherweise etwas länger dauern, mit asyncio "entkoppeln" kann. Jetzt habe ich mir schon einige YouTube Videos angesehen und einige Tutorials und Examples durchgelesen. Leider wird in den meisten Beispielen nur eine Wartezeit verwendet. In den Beispielen wird dann immer time.sleep(n) durch asynio.speel(n) ersetzt. Das bringt mich aber leider nicht weiter.

Ich möchte gerne s.connect(hostname, port) aufrufen dann dann per callback informiert werden, wenn das Ergebnis vorliegt. Das geht ganz schnell, wenn der Port offen ist. Ist er nicht erreichbar, dauert das eben so lange wie der Timeout eingestellt ist. Wenn ich eine Liste mit 10 Ports habe, dauert das unter Umständen schon ganz schän lange.

Langsam glaube ich, dass ich mit async io völlig auf dem Holzweg bin und das doch über einen Threadpool oder ähnliches Lösen muss.
Ich kenne mich leider bisher mit Python nicht besonders gut aus. Mit der Funktionsweise von Threads in in den Programmiersprachen C++ oder C# kenne ich mich beispielsweise ganz gut aus. Python habe ich bisher nur für einfache Scripts verwendet. Mit asyncio oder Threading hatte ich in Python noch gar keinen Kontakt.


Ich hoffe mir kann jemand den Entscheidenden Tipp gehen.

Vielen Danlk!
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

Du hast die Seite zu asyncio-Streams in der Dokumentation gelesen: https://docs.python.org/3/library/asyncio-stream.html?

Ich weiß nicht ob das schon bei open_connection asynchron läuft, mußt Du ausprobieren.
Ansonsten, Threads erzeugt man bei dem Problem am besten mit multiprocessing.pool.ThreadPool
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,
Ich weiß nicht ob das schon bei open_connection asynchron läuft, mußt Du ausprobieren.
Laut Doku ist es eine Coroutine und "awaitable".

Ganz gut finde ich die Doku zu asyncio bei PMOTW. Der Teil in der offiziellen Python Doku zu asyncio ist ja immer noch etwas "abstrakt".

Gruß, noisefloor
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich finde ja das `asyncio`-Modul doof und dass das in der Standardbibliothek ist und damit so ”offiziell” aussieht und damit anderen wie `trio` das Wasser abgräbt, obwohl das neuer und weniger komplex und überfrachtet ist.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
__deets__
User
Beiträge: 14536
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich finde das super, dass es endlich eine Standard-Variante gibt, wie man einen mainloop in Python baut gibt. Das wiegt aus meiner Sicht die Nachteile auf.
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,
__blackjack__ hat geschrieben: Sonntag 5. April 2020, 12:48 Ich finde ja das `asyncio`-Modul doof und dass das in der Standardbibliothek ist und damit so ”offiziell” aussieht und damit anderen wie `trio` das Wasser abgräbt, obwohl das neuer und weniger komplex und überfrachtet ist.
AFAIK ist das doch in die Standardbibliothek gewandert, damit es einen einheitlichen Ansatz gibt bzw. einen Ansatz, auf den andere Modulen bauen könnten?
Das Kern"problem" ist IMHO eher, das so Bibliotheken wie Trio es nicht schaffen, ihre Bekanntheit zu verbreitern. Wobei vielleicht auch die Frage ist, wie viele der Gesamtheit der Python-Programmierer asyncio in irgendeiner Form überhaupt brauchen (ich z.B. nicht). Wer sich mit asyncio ein bisschen beschäftigt findet IMHO wahrscheinlich relativ zügig Trio und Curio.

Gruß, noisefloor
Marco82
User
Beiträge: 19
Registriert: Samstag 4. April 2020, 21:07

Sirius3 hat geschrieben: Samstag 4. April 2020, 23:13 Du hast die Seite zu asyncio-Streams in der Dokumentation gelesen: https://docs.python.org/3/library/asyncio-stream.html?

Ich weiß nicht ob das schon bei open_connection asynchron läuft, mußt Du ausprobieren.
Ansonsten, Threads erzeugt man bei dem Problem am besten mit multiprocessing.pool.ThreadPool
Danke für den Tipp, aber auch diesen Teil der Doku habe ich schon gelesen. Hat mich leider auch nicht weiter gebracht. Die meisten Beispiele öffnen einen Port um dann asynchron darauf zu lesen oder zu schreiben. Das brauche ich nicht. Ich will nur sehen, ob sich der Port öffnen lässt.
__deets__
User
Beiträge: 14536
Registriert: Mittwoch 14. Oktober 2015, 14:29

Aber die betreffende Funktion ist doch asynchron. Was sollte denn da deiner Meinung nach anders sein?
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,
Das brauche ich nicht. Ich will nur sehen, ob sich der Port öffnen lässt.
??? - das eine bedingt doch das andere? Du kannst doch dann einfach die Schreiboperation einfach weglassen.
`asyncio.open_connection()` liefert dir nun mal ein Reader- und ein Writer-Objet zurück, nachdem der Port erfolgreich geöffnet wurde. Wenn du beide nicht brauchst -> binde sie an _.

Gruß, noisefloor
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@__deets__: Meinetwegen eine Standardvariante einer Mainloop in der Standardbibliothek. Aber dann nur die, und die Möglichkeit darauf dann andere Projekte aufzubauen. Und dann IMHO nicht die Mainloop von `asyncio`. Wer `asyncio` mag, kann das ja dann darauf aufbauen. Falls man beispielsweise `trio`\s Mainloop als Grundlage nimmt, zeigt `trio_asyncio` dass das geht.

@noisefloor: Das mag sein, aber `asyncio` bringt halt nicht nur einen Kern mit sondern auch alles andere, womit es keine Konkurrenz gibt wo man dann das für sich Beste aussuchen kann.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Marco82
User
Beiträge: 19
Registriert: Samstag 4. April 2020, 21:07

noisefloor hat geschrieben: Sonntag 5. April 2020, 11:43 Hallo,
Ich weiß nicht ob das schon bei open_connection asynchron läuft, mußt Du ausprobieren.
Laut Doku ist es eine Coroutine und "awaitable".

Ganz gut finde ich die Doku zu asyncio bei PMOTW. Der Teil in der offiziellen Python Doku zu asyncio ist ja immer noch etwas "abstrakt".

Gruß, noisefloor
Die Quelle ist wirklich toll. Ich probiere schon eine Weile mit den verschiedenen Beispielen herum.
Besten Dank!
Marco82
User
Beiträge: 19
Registriert: Samstag 4. April 2020, 21:07

noisefloor hat geschrieben: Sonntag 5. April 2020, 13:29 Hallo,
Das brauche ich nicht. Ich will nur sehen, ob sich der Port öffnen lässt.
??? - das eine bedingt doch das andere? Du kannst doch dann einfach die Schreiboperation einfach weglassen.
`asyncio.open_connection()` liefert dir nun mal ein Reader- und ein Writer-Objet zurück, nachdem der Port erfolgreich geöffnet wurde. Wenn du beide nicht brauchst -> binde sie an _.

Gruß, noisefloor
Es scheint hier keinen Socket-Timeout zu geben. In meinem System dauert das jetzt 21 Sekunden bis ich eine OSException bekomme: "[WinError 121] Das Zeitlimit für die Semaphore wurde erreicht"

In meinem sequenziellen Code hat es nur 3 Sekunden gedauert bis ich mitbekommen habe, dass ein Port nicht erreichbar ist. Da kann ich 7 Ports sequentiell prüfen bis ich so langsam bin wie diese asyncio Lösung...
Marco82
User
Beiträge: 19
Registriert: Samstag 4. April 2020, 21:07

__blackjack__ hat geschrieben: Sonntag 5. April 2020, 12:48 Ich finde ja das `asyncio`-Modul doof und dass das in der Standardbibliothek ist und damit so ”offiziell” aussieht und damit anderen wie `trio` das Wasser abgräbt, obwohl das neuer und weniger komplex und überfrachtet ist.
Ich mittlerweile auch :lol:
Marco82
User
Beiträge: 19
Registriert: Samstag 4. April 2020, 21:07

Jetzt habe ich mir mal ein anderes Beispiel vorgenommen:
Das Beispiel ist von https://pymotw.com/3/asyncio/index.html. Ich habe es um eine simple sequenzielle Variante und den Zeitvergleich erweitert.

Code: Alles auswählen

import asyncio
import socket
import time


TARGETS = [
    ('pymotw.com', 'https'),
    ('doughellmann.com', 'https'),
    ('python.org', 'https'),
    ('google.de', 'https'),
    ('stackoverflow.de', 'https'),
    ('beolingus.de', 'https'),
    ('leo.de', 'https'),
]


async def main(loop, targets):
    for target in targets:
        info = await loop.getaddrinfo(
            *target,
            proto=socket.IPPROTO_TCP,
        )

        for host in info:
            print('{:20}: {}'.format(target[0], host[4][0]))

print('----- Resolve hostname using asyncio -------------------')
start_io = time.perf_counter()
event_loop = asyncio.get_event_loop()
try:
    event_loop.run_until_complete(main(event_loop, TARGETS))
finally:
    event_loop.close()
end_io = time.perf_counter()
duration_io = end_io - start_io
print('--> asyncio duration: {}\n'.format(duration_io))

print('----- Resolve hostname sequential ----------------------')
start_seq = time.perf_counter()
for target in TARGETS:
    address = socket.gethostbyname(target[0])
    print('{:20}: {}'.format(target[0], address))
end_seq = time.perf_counter()
duration_seq = end_seq - start_seq
print('--> sequential duration: {}\n'.format(duration_seq))

print('==> sequential is {}x faster than asyncio'.format(duration_io / duration_seq))
Hier das Ergebnis:

Code: Alles auswählen

----- Resolve hostname using asyncio -------------------
pymotw.com          : 66.33.211.242
doughellmann.com    : 66.33.211.240
python.org          : 45.55.99.72
google.de           : 172.217.23.67
stackoverflow.de    : 91.204.46.10
beolingus.de        : 134.109.133.9
leo.de              : 80.190.158.9
--> asyncio duration: 0.03597080000000001

----- Resolve hostname sequential ----------------------
pymotw.com          : 66.33.211.242
doughellmann.com    : 66.33.211.240
python.org          : 45.55.99.72
google.de           : 172.217.23.67
stackoverflow.de    : 91.204.46.10
beolingus.de        : 134.109.133.9
leo.de              : 80.190.158.9
--> sequential duration: 0.0034908999999999912

==> sequential is 10.304162250422555x faster than asyncio

Process finished with exit code 0
Mache ich hier irgendetwas falsch, oder taugt asyncio einfach für diesen Anwendungsfall nix?

Danke & Gruß
__deets__
User
Beiträge: 14536
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich wuerde ja mal vermuten, dass im zweiten Fall gechachte Informationen geliefert werden. Dreh's mal um. Und natuerlich wie immer mehrfach ausfuehren.
Marco82
User
Beiträge: 19
Registriert: Samstag 4. April 2020, 21:07

__deets__ hat geschrieben: Sonntag 5. April 2020, 18:16 Ich wuerde ja mal vermuten, dass im zweiten Fall gechachte Informationen geliefert werden. Dreh's mal um. Und natuerlich wie immer mehrfach ausfuehren.
Stimmt. Umgedreht ist meine Variante zwar immer noch schneller, aber nicht mehr ganz so deutlich.
Mehrfach ausgeführt hatte ich das sowieso immer.

Danke für den Tipp
__deets__
User
Beiträge: 14536
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und dann musst du natuerlich auch noch wirklich asynchron parallel was machen. Momentan ist alles was du erreicht hast, den gleichen Code mit etwas mehr overhead seriell ablaufen zu lassen. JEDE Abfrage muss ihr EIGNER TASK sein, und dann wartest du bis die alle zuende sind.

Code: Alles auswählen

import sys
import asyncio
import socket
import time


TARGETS = [
    ('pymotw.com', 'https'),
    ('doughellmann.com', 'https'),
    ('python.org', 'https'),
    ('google.de', 'https'),
    ('stackoverflow.de', 'https'),
    ('beolingus.de', 'https'),
    ('leo.de', 'https'),
]


async def resolve_address(loop, address, protocol):
    return await loop.getaddrinfo(
        address, protocol,
        proto=socket.IPPROTO_TCP,
    )

event_loop = asyncio.get_event_loop()


async def main():
    res = await asyncio.gather(*(resolve_address(event_loop, *t) for t in TARGETS))
    print(res)


event_loop.run_until_complete(main())
Marco82
User
Beiträge: 19
Registriert: Samstag 4. April 2020, 21:07

So, ich fasse meinen Stand jetzt mal zusammen.

In ca. 20 Minuten habe ich folgende Threadpool Lösung aufgesetzt.

Code: Alles auswählen

from multiprocessing import Pool
import socket
import time

def check_port(port: int) -> bool:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.settimeout(3)
    try:
        s.connect(('hostname', int(port)))
        s.shutdown(socket.SHUT_RDWR)
        return port, True
    except socket.timeout as ex:
        return port, False
    finally:
        s.close()

if __name__ == '__main__':
    start = time.perf_counter()
    p = Pool(5)
    print(p.map(check_port, [4839, 4840, 4841]))
    end = time.perf_counter()
    print('Duration: {}'.format(end - start))
Kurz und übersichtlich wie ich finde.
Mit Threading habe ich in Python übrigens genau so wenig Erfahrung wie mit asyncio.

Ich, als Gelegenheitsprogrammierer, werde asyncio zukünftig meiden.

Danke für die vielen meist sehr hilfreichen Tipps!
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

Das schöne an Typannotationen ist ja, dass sie noch so falsch sein können. Am besten komplett löschen und vergessen. Ist nur Festplattenplatzverschwendung.
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Marco82: Das ist keine Lösung mit Threads sondern mit Prozessen. Ich würde für so etwas ja nicht `multiprocessing` nehmen sondern `concurrent.futures`. Da haben Thread- und Prozesspools die gleiche API und man kann sich leichter zwischen beiden entscheiden.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten