Last Updated on December 12, 2023
Asyncio coroutines in Python can be used to scan multiple ports on a server concurrently.
This can dramatically speed up the process compared to attempting to connect to each port sequentially, one by one.
In this tutorial, you will discover how to develop a concurrent port scanner with asyncio in Python.
After completing this tutorial, you will know:
- How to open a socket connection to each port sequentially and how slow it can be.
- How to execute coroutines concurrently to scan ports and wait for them to complete.
- How to scan port numbers concurrently and report results dynamically as soon as they are available
- How to limit the number of ports scanned concurrently using a queue.
Let’s dive in.
Develop and Asyncio Port Scanner
We can connect to other computers by opening a socket, called socket programming.
Opening a socket requires both the name or IP address of the server and a port number on which to connect.
For example, when your web browser opens a web page on python.org, it is opening a socket connection to that server on port 80, then uses the HTTP protocol to request and download (GET) an HTML file. Well, it used to. Now it is port 443 using the HTTPS protocol.
Socket programming or network programming is a lot of fun.
A good first socket programming project is to develop a port scanner.
This is a program that reports all of the open sockets on a given server.
A simple way to implement a port scanner is to loop over all the ports you want to test and attempt to make a socket connection on each. If a connection can be made, we disconnect immediately and report that the port on the server is open.
For example, we know that port 80 is open on python.org, but what other ports might be open?
Historically, having many open ports on a server was a security risk, so it is common to lock down a public-facing server and close all non-essential ports to external traffic. This means scanning public servers will likely yield few open ports in the best case or will deny future access in the worst case if the server thinks you’re trying to break in.
As such, although developing a port scanner is a fun socket programming exercise, we must be careful in how we use it and what servers we scan.
Next, let’s look at how we can open a socket connection on a single port.
Run loops using all CPUs, download your FREE book to learn how.
Open a Socket Connection on a Port
We can open a socket connection in asyncio using the asyncio.open_connection() function.
This takes the host and port number and returns a StreamReader and StreamWriter for interacting with the server via the socket.
The asyncio.open_connection() function is a coroutine and must be awaited. It will return once the connection is open.
For example:
1 2 3 |
... # open a socket connection reader, writer = asyncio.open_connection('python.org', 80) |
If a connection can be made, the port is open.
Otherwise, if the connection cannot be made, the port is not open.
The problem is, how do we know a connection cannot be made?
If a port is not open, the call may wait for a long time before giving up.
We need a way to give up after a time limit.
This can be achieved using the asyncio.wait_for() function.
This is a coroutine that will execute an awaitable and wait a fixed interval in seconds before giving up and raising an asyncio.TimeoutError exception.
You can learn more about the asyncio.wait_for() function in the tutorial:
We can create the coroutine for the asyncio.open_connection() function and pass it to the wait_for() coroutine.
This will allow us to attempt to make a socket connection on a given port for a fixed interval, such as one or three seconds.
For example:
1 2 3 4 5 6 7 8 9 10 |
... # create coroutine for opening a connection coro = asyncio.open_connection('python.org, 80) # execute the coroutine with a timeout try: # open the connection and wait for a moment _ = await asyncio.wait_for(coro, 1.0) # ... except asyncio.TimeoutError: # ... |
If the connection can be made within the time limit we can then close the connection.
This can be achieved by calling the close() method on the StreamWriter object returned from asyncio.open_connection().
For example:
1 2 3 |
... # close connection once opened writer.close() |
Otherwise, if the asyncio.TimeoutError exception is raised, we can assume that the port is probably not open.
We can tie all of this together into a coroutine function that tests one port on one host and returns True if the port is open or False otherwise.
The test_port_number() coroutine function below implements this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# returns True if a connection can be made, False otherwise async def test_port_number(host, port, timeout=3): # create coroutine for opening a connection coro = asyncio.open_connection(host, port) # execute the coroutine with a timeout try: # open the connection and wait for a moment _,writer = await asyncio.wait_for(coro, timeout) # close connection once opened writer.close() # indicate the connection can be opened return True except asyncio.TimeoutError: # indicate the connection cannot be opened return False |
Scan a Range of Ports on a Server
We can scan a range of ports on a given host.
Many common internet services are provided on ports between 0 and 1024.
The viable range of ports is 0 to 65535, and you can see a list of the most common port numbers and the services that use them in the file /etc/services on POSIX systems.
Wikipedia also has a page that lists the most common port numbers:
To scan a range of ports, we can repeatedly call our test_port_number() coroutine function that we developed in the previous section and report any ports that permit a connection as ‘open’.
The main() coroutine function below implements this reporting any open ports that are discovered.
1 2 3 4 5 6 7 8 |
# main coroutine async def main(host, ports): # report a status message print(f'Scanning {host}...') # scan ports sequentially for port in ports: if await test_port_number(host, port): print(f'> {host}:{port} [OPEN]') |
Finally, we can call this function and specify the host and range of ports.
In this case, we will port scan python.org (out of love for python, not malicious intent).
1 2 3 4 5 |
# define a host and ports to scan host = 'python.org' ports = range(1, 1024) # start the asyncio program asyncio.run(main(host, ports)) |
We would expect that at the least port 80 would be open for HTTP connections.
Tying this together, the complete example of port scanning a host in Python in asyncio is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
# SuperFastPython.com # example of an asyncio sequential port scanner import asyncio # returns True if a connection can be made, False otherwise async def test_port_number(host, port, timeout=3): # create coroutine for opening a connection coro = asyncio.open_connection(host, port) # execute the coroutine with a timeout try: # open the connection and wait for a moment _,writer = await asyncio.wait_for(coro, timeout) # close connection once opened writer.close() # indicate the connection can be opened return True except asyncio.TimeoutError: # indicate the connection cannot be opened return False # main coroutine async def main(host, ports): # report a status message print(f'Scanning {host}...') # scan ports sequentially for port in ports: if await test_port_number(host, port): print(f'> {host}:{port} [OPEN]') # define a host and ports to scan host = 'python.org' ports = range(1, 1024) # start the asyncio program asyncio.run(main(host, ports)) |
Running the example attempts to make a connection for each port number between 1 and 1023 (one minus 1024) and reports all open ports.
In this case, we can see that port 80 for HTTP is open as expected, and port 443 is also open for HTTPS.
The program works fine, but it is painfully slow.
On my system, it took about 51 minutes. This makes sense. If we test 1023 ports and most ports are closed then we expect to wait 3 seconds on each attempt or 1023 * 3 which equals 3069 seconds. Converting this to minutes 3069/60 equals about 51.15 minutes.
1 2 |
> python.org:80 [OPEN] > python.org:443 [OPEN] |
The benefit of asyncio is that it can execute coroutines concurrently, specifically coroutines that perform non-blocking I/O.
Next, we will look at how to run coroutines concurrently to speed up this port scanning process.
Free Python Asyncio Course
Download your FREE Asyncio PDF cheat sheet and get BONUS access to my free 7-day crash course on the Asyncio API.
Discover how to use the Python asyncio module including how to define, create, and run new coroutines and how to use non-blocking I/O.
Asyncio Concurrent Port Scanner
We can scan ports concurrent with asyncio.
Each port that is being tested is a coroutine that can run concurrently. The three seconds while the test_port_number() coroutine is running it will be suspended, allowing other coroutines to run.
There are perhaps two main approaches we can use to achieve this:
- Run coroutines concurrently with asyncio.gather().
- Issue coroutines as independent tasks and wait with asyncio.wait().
The first is to create a coroutine for each port to scan, then execute them and wait for them to complete. This can be achieved using the asyncio.gather() function.
The second is to issue each port to scan as an independent task, then wait for all tasks to complete. This can be achieved using the asyncio.wait() function.
Let’s take a closer look at each approach in turn.
Concurrent Port Scanner with gather()
We can create one coroutine per port to scan and then execute them all concurrently.
This requires first creating the coroutines. With one coroutine per port, this would be a collection of more than 1,000 coroutines.
We can achieve this using a list comprehension.
For example:
1 2 3 |
... # create all coroutines coros = [test_port_number(host, port) for port in ports] |
Next, we can execute all of these coroutines concurrently.
This can be achieved using the asyncio.gather() function.
This function takes awaitables as arguments and will not return until the awaitables are complete. It does not take a list of awaitables, therefore we must expand our list into separate expressions using the star (*) operator.
For example:
1 2 3 |
... # execute all coroutines concurrently results = await asyncio.gather(*coros) |
You can learn more about the asyncio.gather() function in the tutorial:
This will execute all coroutines concurrently and will return an iterable of return values from each coroutine in the order provided.
We can then traverse the list of return values along with the list of ports and report the results.
Recall that we can traverse two or more iterables together using the built-in zip() function.
For example:
1 2 3 4 5 |
... # report results for port,result in zip(ports, results): if result : print(f'> {host}:{port} [OPEN]') |
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# SuperFastPython.com # example of an asyncio concurrent port scanner using gather import asyncio # returns True if a connection can be made, False otherwise async def test_port_number(host, port, timeout=3): # create coroutine for opening a connection coro = asyncio.open_connection(host, port) # execute the coroutine with a timeout try: # open the connection and wait for a moment _,writer = await asyncio.wait_for(coro, timeout) # close connection once opened writer.close() # indicate the connection can be opened return True except asyncio.TimeoutError: # indicate the connection cannot be opened return False # main coroutine async def main(host, ports): # report a status message print(f'Scanning {host}...') # create all coroutines coros = [test_port_number(host, port) for port in ports] # execute all coroutines concurrently results = await asyncio.gather(*coros) # report results for port,result in zip(ports, results): if result : print(f'> {host}:{port} [OPEN]') # define a host and ports to scan host = 'python.org' ports = range(1, 1024) # start the asyncio program asyncio.run(main(host, ports)) |
Running the example executes the main() coroutine as the entry point into the asyncio program.
A list of coroutines is first created.
The coroutines are then all executed concurrently using asyncio.gather().
This suspends the main() coroutine until all coroutines are completed. Each coroutine tests one port, attempting to open a connection and suspending it until either the connection is open or the timeout is elapsed.
Once all tasks are completed the main() coroutine resumes and all results are reported.
Two open ports are reported the same as before.
The big difference is the speed of execution. In this case, it takes about 3.1 seconds, compared to more than 50 minutes in the previous example.
That is about 3063 seconds faster or a 3063x speed-up.
1 2 3 |
Scanning python.org... > python.org:80 [OPEN] > python.org:443 [OPEN] |
Next, let’s explore how we might scan ports concurrently as independent tasks.
Concurrent Port Scanner with wait()
Each port to be scanned can be issued as a separate asyncio task.
The task will execute independently, whenever possible, e.g. when other coroutines are not running.
We can then wait for all tasks to complete and report results, as before.
This can be achieved by calling the asyncio.create_task() function for each coroutine. This will wrap the coroutine in a task and schedule it for execution.
For example:
1 2 3 |
... # create all tasks tasks = [asyncio.create_task(test_port_number(host, port)) for port in ports] |
You can learn more about issuing asyncio tasks in the tutorial:
This will return a list of asyncio.Task objects immediately.
We can then wait for all of these tasks to complete via the asyncio.wait() function.
This will take a collection of awaitables and by default, it will not return until all awaitables are done.
For example:
1 2 3 |
... # execute all coroutines concurrently _ = await asyncio.wait(tasks) |
You can learn more about the asyncio.wait() function in the tutorial:
We can then traverse the list of tasks and ports in the same order and report the results from each.
We can get the result from each task either by awaiting it or getting its return value or via the result() method.
For example:
1 2 3 4 5 6 |
... # report results for port,task in zip(ports, tasks): # check the return value from each task if await task: print(f'> {host}:{port} [OPEN]') |
Or:
1 2 3 4 5 6 |
... # report results for port,task in zip(ports, tasks): # check the return value from each task if task.result(): print(f'> {host}:{port} [OPEN]') |
You can learn more about getting results from asyncio tasks in the tutorial:
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# SuperFastPython.com # example of an asyncio concurrent port scanner using wait() import asyncio # returns True if a connection can be made, False otherwise async def test_port_number(host, port, timeout=3): # create coroutine for opening a connection coro = asyncio.open_connection(host, port) # execute the coroutine with a timeout try: # open the connection and wait for a moment _,writer = await asyncio.wait_for(coro, timeout) # close connection once opened writer.close() # indicate the connection can be opened return True except asyncio.TimeoutError: # indicate the connection cannot be opened return False # main coroutine async def main(host, ports): # report a status message print(f'Scanning {host}...') # create all tasks tasks = [asyncio.create_task(test_port_number(host, port)) for port in ports] # execute all coroutines concurrently _ = await asyncio.wait(tasks) # report results for port,task in zip(ports, tasks): # check the return value from each task if await task : print(f'> {host}:{port} [OPEN]') # define a host and ports to scan host = 'python.org' ports = range(1, 1024) # start the asyncio program asyncio.run(main(host, ports)) |
Running the example executes the main() coroutine as the entry point into the asyncio program.
The coroutines are created and issued as independent tasks in a list comprehension.
This returns immediately with a list of asyncio.Task objects that provide a handle on each issued task.
The main() coroutine then suspends and waits for all tasks to complete. This allows the tasks to begin executing.
Each coroutine tests one port, attempting to open a connection and suspending it until either the connection is open or the timeout is elapsed.
Once all tasks are completed the main() coroutine resumes and all results are reported.
Two open ports are reported the same as before and the example completes in about the same time as the above concurrent example.
1 2 3 |
Scanning python.org... > python.org:80 [OPEN] > python.org:443 [OPEN] |
Next, let’s look at how we might report results dynamically.
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
Asyncio Dynamic Port Scanner
In the previous examples, we executed the coroutines concurrently and reported the results after all tasks had been completed.
An alternative approach would be to report results as the tasks are completed.
This would allow the program to be more responsive and show results to the user as they are available.
We could achieve this by having the test_port_number() report its result directly.
Another approach is to traverse coroutines in the order they are completed, as they complete.
This can be achieved using the asyncio.as_completed() function.
This function takes a collection of awaitables. If they are coroutines, they are issued as tasks.
The function then returns an iterable of the coroutines that are yielded in the order that they are completed.
We can traverse this iterable directly, we do not need to use the “async for” expression reserved for asynchronous iterables.
For example:
1 2 3 4 5 |
... # execute coroutines and handle results as they complete for coro in asyncio.as_completed(coros): # check the return value from the coroutine # ... |
You can learn more about the asyncio.as_completed() function in the tutorial:
The downside is that we don’t have an easy way to relate the coroutine to the port that was tested. Therefore, we can update our test_port_number() coroutine to return whether the port is open and the port was tested.
For example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# returns True if a connection can be made, False otherwise async def test_port_number(host, port, timeout=3): # create coroutine for opening a connection coro = asyncio.open_connection(host, port) # execute the coroutine with a timeout try: # open the connection and wait for a moment _,writer = await asyncio.wait_for(coro, timeout) # close connection once opened writer.close() # indicate the connection can be opened return True,port except asyncio.TimeoutError: # indicate the connection cannot be opened return False,port |
We can then traverse the coroutines in the order they are completed and get the details of the port and whether it is open from each and report it.
For example:
1 2 3 4 5 6 7 |
... # execute coroutines and handle results as they complete for coro in asyncio.as_completed(coros): # check the return value from the coroutine result, port = await coro if result: print(f'> {host}:{port} [OPEN]') |
This will execute all coroutines concurrently and will report open ports as they are discovered, rather than all at the end.
Tying this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# SuperFastPython.com # example of an asyncio concurrent port scanner using as_completed() import asyncio # returns True if a connection can be made, False otherwise async def test_port_number(host, port, timeout=3): # create coroutine for opening a connection coro = asyncio.open_connection(host, port) # execute the coroutine with a timeout try: # open the connection and wait for a moment _,writer = await asyncio.wait_for(coro, timeout) # close connection once opened writer.close() # indicate the connection can be opened return True,port except asyncio.TimeoutError: # indicate the connection cannot be opened return False,port # main coroutine async def main(host, ports): # report a status message print(f'Scanning {host}...') # create all coroutines coros = [test_port_number(host, port) for port in ports] # execute coroutines and handle results as they complete for coro in asyncio.as_completed(coros): # check the return value from the coroutine result, port = await coro if result: print(f'> {host}:{port} [OPEN]') # define a host and ports to scan host = 'python.org' ports = range(1, 1024) # start the asyncio program asyncio.run(main(host, ports)) |
Running the example executes the main() coroutine as the entry point into the asyncio program.
A list of coroutines is first created.
The coroutines are then passed to the asyncio.as_completed() function.
This wraps each in another coroutine and executes them all concurrently and independently.
It returns immediately with an iterable of coroutines.
Internally, it awaits and yields coroutines as they are completed.
The return value from each coroutine is retrieved and results are reported as they are made available.
The example shows the same ports and executes in about the same time as the previous concurrent examples, except the program is more responsive.
Ports are shown as open almost immediately, as opposed to after all ports in the range have been checked and timed out.
1 2 3 |
Scanning python.org... > python.org:80 [OPEN] > python.org:443 [OPEN] |
Next, let’s explore the case where we might want to limit the number of concurrent port scanning coroutines.
Asyncio Port Scanner Limit Concurrent Connections
A limitation of the above concurrent port scanning examples is that all ports are scanned concurrently.
This is reasonable as we are only scanning about 1,000 port numbers.
What if we wanted to scan the entire domain of possible port numbers, e.g. nearly 65,000?
We may be able to run 65,000 concurrent coroutines easily, but maybe not. Perhaps attempting to open so many concurrent connections to a server will be seen as a hostile act, alerting the server administrator or our internet service provider.
As such, we may prefer to limit the number of open connections attempted at the same time.
There are many ways we could achieve this.
One approach is to start a fixed number of port scanning coroutines. Each runs a loop that checks one port number per iteration.
This way, the program never attempts to open more than a fixed number of socket connections at any one time.
We can achieve this by defining a queue of port numbers to check. We can then have port scanning coroutines read one port number per iteration from this queue, scan it and report the result. This can continue until there are no further ports to scan and the coroutines can terminate.
This also has the benefit of reporting results as soon as they are available, e.g. dynamically, as we did in the previous section.
Firstly, we can define a coroutine that will scan ports.
This will take the host as an argument and a queue from which to read port numbers to check.
1 2 3 4 |
... # coroutine to scan ports as fast as possible async def scanner(host, task_queue): # ... |
It will then loop forever, first getting a port number to check from the queue.
This can be achieved using an asyncio.Queue and call the get() method.
For example:
1 2 3 4 5 |
... # read tasks forever while True: # read one task from the queue port = await task_queue.get() |
You can learn more about working with asyncio queues in the tutorial:
Next, we can check if the port read is in fact a special message indicating that there are no further ports to scan. If so, we can exit our out loop scanning ports.
1 2 3 4 5 6 7 |
... # check for a request to stop scanning if port is None: # add it back for the other scanners await task_queue.put(port) # stop scanning break |
Otherwise, we can scan the port and if the port is open, report this fact with a message.
1 2 3 4 5 |
... # scan the port if await test_port_number(host, port): # report the report if open print(f'> {host}:{port} [OPEN]') |
Finally, once the port number has been processed, we can mark it as such using the task_done() method.
This will be needed later when the main() coroutine needs to know that all port numbers have been processed.
1 2 3 |
... # mark the item as processed task_queue.task_done() |
Tying this together, the complete scanner() coroutine is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# coroutine to scan ports as fast as possible async def scanner(host, task_queue): # read tasks forever while True: # read one task from the queue port = await task_queue.get() # check for a request to stop scanning if port is None: # add it back for the other scanners await task_queue.put(port) # stop scanning break # scan the port if await test_port_number(host, port): # report the report if open print(f'> {host}:{port} [OPEN]') # mark the item as processed task_queue.task_done() |
Next, we can update the main() coroutine.
First, we can create the shared queue used to share port numbers to scan with the scanner() coroutines.
1 2 3 |
... # create the task queue task_queue = asyncio.Queue() |
Next, we can create a fixed number of scanner() coroutines and have them run independently as asyncio tasks.
1 2 3 |
... # start the port scanning coroutines workers = [asyncio.create_task(scanner(host, task_queue)) for _ in range(limit)] |
We can then put all port numbers to the queue for the scanner() coroutines to consume and scan as fast as they are able.
1 2 3 4 5 |
... # issue tasks as fast as possible for port in ports: # add task to scan this port await task_queue.put(port) |
Finally, we can wait for all ports to be scanned.
Because port numbers are marked as scanned via the task_done() method, the main() coroutine can call the join() method which will block until all port numbers on the queue have been marked as done.
For example:
1 2 3 |
... # wait for all tasks to be complete await task_queue.join() |
The main() coroutine can then signal all scanner() coroutines that no further port numbers are to be expected by sending a None value. This will cause the scanner() coroutines to exit their loop and terminate.
1 2 3 |
... # signal no further tasks await task_queue.put(None) |
Tying this together, the updated main() coroutine is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# main coroutine async def main(host, ports, limit=100): # report a status message print(f'Scanning {host}...') # create the task queue task_queue = asyncio.Queue() # start the port scanning coroutines workers = [asyncio.create_task(scanner(host, task_queue)) for _ in range(limit)] # issue tasks as fast as possible for port in ports: # add task to scan this port await task_queue.put(port) # wait for all tasks to be complete await task_queue.join() # signal no further tasks await task_queue.put(None) |
Tying all of this together, the complete example is listed below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# SuperFastPython.com # example of an asyncio concurrent port scanner that limits concurrent connections import asyncio # returns True if a connection can be made, False otherwise async def test_port_number(host, port, timeout=3): # create coroutine for opening a connection coro = asyncio.open_connection(host, port) # execute the coroutine with a timeout try: # open the connection and wait for a moment _,writer = await asyncio.wait_for(coro, timeout) # close connection once opened writer.close() # indicate the connection can be opened return True except asyncio.TimeoutError: # indicate the connection cannot be opened return False # coroutine to scan ports as fast as possible async def scanner(host, task_queue): # read tasks forever while True: # read one task from the queue port = await task_queue.get() # check for a request to stop scanning if port is None: # add it back for the other scanners await task_queue.put(port) # stop scanning break # scan the port if await test_port_number(host, port): # report the report if open print(f'> {host}:{port} [OPEN]') # mark the item as processed task_queue.task_done() # main coroutine async def main(host, ports, limit=100): # report a status message print(f'Scanning {host}...') # create the task queue task_queue = asyncio.Queue() # start the port scanning coroutines workers = [asyncio.create_task(scanner(host, task_queue)) for _ in range(limit)] # issue tasks as fast as possible for port in ports: # add task to scan this port await task_queue.put(port) # wait for all tasks to be complete await task_queue.join() # signal no further tasks await task_queue.put(None) # define a host and ports to scan host = 'python.org' ports = range(1, 1024) # start the asyncio program asyncio.run(main(host, ports)) |
Running the example executes the main() coroutine as the entry point into the asyncio program.
Firstly, the shared queue is created.
Then 100 scanner() coroutines are created and passed the host and shared queue from which to read port numbers as fast as they are able.
The main() coroutine then adds all of the port numbers to the queue, then suspends, waiting for all port numbers to be processed.
The scanner() coroutines consume and test port numbers as fast as they are able, reporting open ports as they are discovered.
Once all port numbers are checked and marked as done, the main() coroutine resumes and adds a None to the queue that signals the scanner() coroutines to stop processing.
The None message is read, re-added to the queue and causes the scanner() coroutines to break their loop and terminate. Re-adding the message allows each coroutine to consume and respond to the message in turn.
The results are the same as before, showing 2 open ports.
The example is slower, completing in about 33 seconds, or about 10x slower than the concurrent version. This is to be expected as we are scanning 10x fewer ports concurrently, e.g. 100 vs 1,000.
This highlights how we can limit the number of concurrent open connections in the asyncio program.
1 2 3 |
Scanning python.org... > python.org:80 [OPEN] > python.org:443 [OPEN] |
Further Reading
This section provides additional resources that you may find helpful.
Python Asyncio Books
- Python Asyncio Mastery, Jason Brownlee (my book!)
- Python Asyncio Jump-Start, Jason Brownlee.
- Python Asyncio Interview Questions, Jason Brownlee.
- Asyncio Module API Cheat Sheet
I also recommend the following books:
- Python Concurrency with asyncio, Matthew Fowler, 2022.
- Using Asyncio in Python, Caleb Hattingh, 2020.
- asyncio Recipes, Mohamed Mustapha Tahrioui, 2019.
Guides
APIs
- asyncio — Asynchronous I/O
- Asyncio Coroutines and Tasks
- Asyncio Streams
- Asyncio Subprocesses
- Asyncio Queues
- Asyncio Synchronization Primitives
References
Takeaways
You now know how to develop a concurrent port scanner using asyncio in Python.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Wolf Schram on Unsplash
Do you have any questions?