Skip to content

Commit 4ce02df

Browse files
committed
add network usage monitor tutorial
1 parent 16872c4 commit 4ce02df

File tree

6 files changed

+242
-0
lines changed

6 files changed

+242
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ This is a repository of all the tutorials of [The Python Code](https://www.thepy
109109
- [How to Convert Pandas Dataframes to HTML Tables in Python](https://www.thepythoncode.com/article/convert-pandas-dataframe-to-html-table-python). ([code](general/dataframe-to-html))
110110
- [How to Make a Simple Math Quiz Game in Python](https://www.thepythoncode.com/article/make-a-simple-math-quiz-game-in-python). ([code](general/simple-math-game))
111111
- [How to Make a Button using PyGame in Python](https://www.thepythoncode.com/article/make-a-button-using-pygame-in-python). ([code](general/button-in-pygame))
112+
- [How to Make a Network Usage Monitor in Python](https://www.thepythoncode.com/article/make-a-network-usage-monitor-in-python). ([code](general/network-usage))
112113

113114

114115

general/network-usage/README.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# [How to Make a Network Usage Monitor in Python](https://www.thepythoncode.com/article/make-a-network-usage-monitor-in-python)
2+
To run the scripts:
3+
- `pip3 install -r requirements.txt`
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import psutil
2+
import time
3+
4+
UPDATE_DELAY = 1 # in seconds
5+
6+
def get_size(bytes):
7+
"""
8+
Returns size of bytes in a nice format
9+
"""
10+
for unit in ['', 'K', 'M', 'G', 'T', 'P']:
11+
if bytes < 1024:
12+
return f"{bytes:.2f}{unit}B"
13+
bytes /= 1024
14+
15+
# get the network I/O stats from psutil
16+
io = psutil.net_io_counters()
17+
# extract the total bytes sent and received
18+
bytes_sent, bytes_recv = io.bytes_sent, io.bytes_recv
19+
20+
while True:
21+
# sleep for `UPDATE_DELAY` seconds
22+
time.sleep(UPDATE_DELAY)
23+
# get the stats again
24+
io_2 = psutil.net_io_counters()
25+
# new - old stats gets us the speed
26+
us, ds = io_2.bytes_sent - bytes_sent, io_2.bytes_recv - bytes_recv
27+
# print the total download/upload along with current speeds
28+
print(f"Upload: {get_size(io_2.bytes_sent)} "
29+
f", Download: {get_size(io_2.bytes_recv)} "
30+
f", Upload Speed: {get_size(us / UPDATE_DELAY)}/s "
31+
f", Download Speed: {get_size(ds / UPDATE_DELAY)}/s ", end="\r")
32+
# update the bytes_sent and bytes_recv for next iteration
33+
bytes_sent, bytes_recv = io_2.bytes_sent, io_2.bytes_recv
34+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import psutil
2+
import time
3+
import os
4+
import pandas as pd
5+
6+
UPDATE_DELAY = 1 # in seconds
7+
8+
def get_size(bytes):
9+
"""
10+
Returns size of bytes in a nice format
11+
"""
12+
for unit in ['', 'K', 'M', 'G', 'T', 'P']:
13+
if bytes < 1024:
14+
return f"{bytes:.2f}{unit}B"
15+
bytes /= 1024
16+
17+
# get the network I/O stats from psutil on each network interface
18+
# by setting `pernic` to `True`
19+
io = psutil.net_io_counters(pernic=True)
20+
21+
while True:
22+
# sleep for `UPDATE_DELAY` seconds
23+
time.sleep(UPDATE_DELAY)
24+
# get the network I/O stats again per interface
25+
io_2 = psutil.net_io_counters(pernic=True)
26+
# initialize the data to gather (a list of dicts)
27+
data = []
28+
for iface, iface_io in io.items():
29+
# new - old stats gets us the speed
30+
upload_speed, download_speed = io_2[iface].bytes_sent - iface_io.bytes_sent, io_2[iface].bytes_recv - iface_io.bytes_recv
31+
data.append({
32+
"iface": iface, "Download": get_size(io_2[iface].bytes_recv),
33+
"Upload": get_size(io_2[iface].bytes_sent),
34+
"Upload Speed": f"{get_size(upload_speed / UPDATE_DELAY)}/s",
35+
"Download Speed": f"{get_size(download_speed / UPDATE_DELAY)}/s",
36+
})
37+
# update the I/O stats for the next iteration
38+
io = io_2
39+
# construct a Pandas DataFrame to print stats in a cool tabular style
40+
df = pd.DataFrame(data)
41+
# sort values per column, feel free to change the column
42+
df.sort_values("Download", inplace=True, ascending=False)
43+
# clear the screen based on your OS
44+
os.system("cls") if "nt" in os.name else os.system("clear")
45+
# print the stats
46+
print(df.to_string())
47+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
from scapy.all import *
2+
import psutil
3+
from collections import defaultdict
4+
import os
5+
from threading import Thread
6+
import pandas as pd
7+
8+
# get the all network adapter's MAC addresses
9+
all_macs = {iface.mac for iface in ifaces.values()}
10+
# A dictionary to map each connection to its correponding process ID (PID)
11+
connection2pid = {}
12+
# A dictionary to map each process ID (PID) to total Upload (0) and Download (1) traffic
13+
pid2traffic = defaultdict(lambda: [0, 0])
14+
# the global Pandas DataFrame that's used to track previous traffic stats
15+
global_df = None
16+
# global boolean for status of the program
17+
is_program_running = True
18+
19+
def get_size(bytes):
20+
"""
21+
Returns size of bytes in a nice format
22+
"""
23+
for unit in ['', 'K', 'M', 'G', 'T', 'P']:
24+
if bytes < 1024:
25+
return f"{bytes:.2f}{unit}B"
26+
bytes /= 1024
27+
28+
29+
def process_packet(packet):
30+
global pid2traffic
31+
try:
32+
# get the packet source & destination IP addresses and ports
33+
packet_connection = (packet.sport, packet.dport)
34+
except (AttributeError, IndexError):
35+
# sometimes the packet does not have TCP/UDP layers, we just ignore these packets
36+
pass
37+
else:
38+
# get the PID responsible for this connection from our `connection2pid` global dictionary
39+
packet_pid = connection2pid.get(packet_connection)
40+
if packet_pid:
41+
if packet.src in all_macs:
42+
# the source MAC address of the packet is our MAC address
43+
# so it's an outgoing packet, meaning it's upload
44+
pid2traffic[packet_pid][0] += len(packet)
45+
else:
46+
# incoming packet, download
47+
pid2traffic[packet_pid][1] += len(packet)
48+
49+
50+
def get_connections():
51+
"""A function that keeps listening for connections on this machine
52+
and adds them to `connection2pid` global variable"""
53+
global connection2pid
54+
while is_program_running:
55+
# using psutil, we can grab each connection's source and destination ports
56+
# and their process ID
57+
for c in psutil.net_connections():
58+
if c.laddr and c.raddr and c.pid:
59+
# if local address, remote address and PID are in the connection
60+
# add them to our global dictionary
61+
connection2pid[(c.laddr.port, c.raddr.port)] = c.pid
62+
connection2pid[(c.raddr.port, c.laddr.port)] = c.pid
63+
# sleep for a second, feel free to adjust this
64+
time.sleep(1)
65+
66+
67+
def print_pid2traffic():
68+
global global_df
69+
# initialize the list of processes
70+
processes = []
71+
for pid, traffic in pid2traffic.items():
72+
# `pid` is an integer that represents the process ID
73+
# `traffic` is a list of two values: total Upload and Download size in bytes
74+
try:
75+
# get the process object from psutil
76+
p = psutil.Process(pid)
77+
except psutil.NoSuchProcess:
78+
# if process is not found, simply continue to the next PID for now
79+
continue
80+
# get the name of the process, such as chrome.exe, etc.
81+
name = p.name()
82+
# get the time the process was spawned
83+
try:
84+
create_time = datetime.fromtimestamp(p.create_time())
85+
except OSError:
86+
# system processes, using boot time instead
87+
create_time = datetime.fromtimestamp(psutil.boot_time())
88+
# construct our dictionary that stores process info
89+
process = {
90+
"pid": pid, "name": name, "create_time": create_time, "Upload": traffic[0],
91+
"Download": traffic[1],
92+
}
93+
try:
94+
# calculate the upload and download speeds by simply subtracting the old stats from the new stats
95+
process["Upload Speed"] = traffic[0] - global_df.at[pid, "Upload"]
96+
process["Download Speed"] = traffic[1] - global_df.at[pid, "Download"]
97+
except (KeyError, AttributeError):
98+
# If it's the first time running this function, then the speed is the current traffic
99+
# You can think of it as if old traffic is 0
100+
process["Upload Speed"] = traffic[0]
101+
process["Download Speed"] = traffic[1]
102+
# append the process to our processes list
103+
processes.append(process)
104+
# construct our Pandas DataFrame
105+
df = pd.DataFrame(processes)
106+
try:
107+
# set the PID as the index of the dataframe
108+
df = df.set_index("pid")
109+
# sort by column, feel free to edit this column
110+
df.sort_values("Download", inplace=True, ascending=False)
111+
except KeyError as e:
112+
# when dataframe is empty
113+
pass
114+
# make another copy of the dataframe just for fancy printing
115+
printing_df = df.copy()
116+
try:
117+
# apply the function get_size to scale the stats like '532.6KB/s', etc.
118+
printing_df["Download"] = printing_df["Download"].apply(get_size)
119+
printing_df["Upload"] = printing_df["Upload"].apply(get_size)
120+
printing_df["Download Speed"] = printing_df["Download Speed"].apply(get_size).apply(lambda s: f"{s}/s")
121+
printing_df["Upload Speed"] = printing_df["Upload Speed"].apply(get_size).apply(lambda s: f"{s}/s")
122+
except KeyError as e:
123+
# when dataframe is empty again
124+
pass
125+
# clear the screen based on your OS
126+
os.system("cls") if "nt" in os.name else os.system("clear")
127+
# print our dataframe
128+
print(printing_df.to_string())
129+
# update the global df to our dataframe
130+
global_df = df
131+
132+
133+
def print_stats():
134+
"""Simple function that keeps printing the stats"""
135+
while is_program_running:
136+
time.sleep(1)
137+
print_pid2traffic()
138+
139+
140+
141+
if __name__ == "__main__":
142+
# start the printing thread
143+
printing_thread = Thread(target=print_stats)
144+
printing_thread.start()
145+
# start the get_connections() function to update the current connections of this machine
146+
connections_thread = Thread(target=get_connections)
147+
connections_thread.start()
148+
# start sniffing
149+
print("Started sniffing")
150+
sniff(prn=process_packet, store=False)
151+
# setting the global variable to False to exit the program
152+
is_program_running = False
153+
154+
+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
psutil
2+
scapy
3+
pandas

0 commit comments

Comments
 (0)