Tekniktips

Tekniktips

Så analyserar du mängden nätverksdata

Hur mycket nätverksdata skickas ut och tas emot av din mobiltelefon eller applikation? I det här inlägget redogörs för såväl nyttan som nöjet med att ta reda på det. Det bjuds på kodexempel samt korta sammanfattningar av de verktyg och procedurer som kan behövas för att analysera överföring av nätverksdata.

Det är väldigt lätt att se på en Androidlur, åtminstone med Android Oreo, hur mycket nätverksdata som telefonen eller godtycklig app förbrukar . Öppnar du upp Inställningar -> Anslutning -> Dataanvändning får du sammanställt dataanvändning för både Wi-Fi och mobildata. Genom att trycka på “Användning av mobildata” eller “Wi-Fi-dataanvändning” ser du även vilka appar som drar mest för respektive gränssnitt.

Informationen som presenteras därigenom räcker gott och väl för de flesta användare. Men jobbar du som utvecklare eller testare av androidapplikationer är den informationen lite för tråkig. Hur mycket nätverksdata skickas ut och tas emot av din app? När gör den det som mest? Hur tar jag reda på den här informationen utan att behöva pilla på mobilen? Hur gör jag det automatiserat?

Automatisera

För det första bryr du dig inte om alla andra appars dataanvändning. För det andra vill du se den totala förbrukningen, oberoende av gränssnitt. För det tredje vill du ju självklart använda dig av automatiserade python-skript som utnyttjar Android Debug Bridge (ADB) och Linux shell. Att behöva använda grafiska gränssnitt, manuellt dessutom, är helt enkelt inte tillräckligt nördigt. Dessutom blir huvudräkning tröttsamt och felaktigt ganska lätt.

Lite längre ner kommer ett kodexempel som gör just detta. Utgångspunkten är att vi inte ska behöva pilla med eller i appen för att ta reda på informationen. Användandet av, exempelvis, ConnectivityManager hade ju underlättat en hel del, men kräver något slags interface mellan appen och den testmiljö du sitter i.

Tillämpning

I min roll som testare, eller rättare sagt människa, har jag inte möjlighet att sitta på kontoret mellan kl 22 till klockan 06 dagen efter, då över 100 automatiserade tester körs. Dessutom är jag nyfiken på dataanvändningen per testfall. Att ta screenshots och analysera dem efteråt kommer inte på fråga. Att koda en egen lösning är däremot högst aktuellt och något som jag gjort samt tänkt dela med mig av.

Huruvida värdena är helt korrekta, eller resulterar i något meningsfullt, är för det här inläggets sammanhang irrelevant. Om du är ute efter en idiotsäker metod för att mäta nätverksdata eller bara nyfiken på var värdena som (inte) syns via det grafiska gränssnittet så kommer du nog att finna inlägget intressant – men inte uttömmande!

Verktyg

Notera att följande kod är, på gott och ont, helt frikopplad från själva applikationen. Med andra ord kan den anpassas till att analysera vilken godtycklig applikation som helst på vilken Androidenhet (Version 7.0 eller högre) som helst. Gott, eftersom du inte behöver “smutsa” ner (utvecklings-)kodbasen med testkod. Ont, eftersom vi inte använder några av Androids trevliga och inbyggda API:er som är mycket mer användarvänliga när det kommer till införskaffandet av informationen om nätverksdata. Koden är alltså till för black box-testning.

Allt som behövs är en Android-enhet med USB-debugging aktiverat kopplat, med USB, till en dator med Linux-distro, ADB och (åtminstone) Python 2.7.X installerat. Se till att ha paketnamnet för appen du ska testa samt serienumret för Androidenheten nerskrivet.

Nörderi

Följande kod innehåller allt vi behöver för att, i testmiljö, kommunicera med enheten och läsa av antalet bytes som en specifik applikation skickat och tagit emot. Klassen Adb är ett gränssnitt som använder subprocess för att skicka shell-kommandon och ta emot svar. I den här klassen tillåter vi endast adb-kommandon men koden kan självklart skalas upp och ner för att passa andra ändamål. I klassen finns även ett hjälpkommando för att skicka specifikt “adb shell”-kommandon.

Klassen NetworkDataStats gör allt vi behöver för att ta reda på tre saker:

• Vilket userId har applikationen vi vill testa? Se metoden getUserId().
→ Varje gång en applikation installeras på en Androidenhet så tilldelas den ett userId (förkortat UID). Väldigt förenklat kan man säga att UID:et används av Android för att spåra vad en applikation har för sig. UID:et är unikt för varje paketnamn och installation.

• Hur mycket data har (alla) applikationer sänt på respektive gränssnitt? Se metoden getAllNetworkDataStats().
Varje applikation, eller rättare sagt UID, kan skicka och ta emot nätverksdata på olika gränssnitt. Du kanske skickar data över en VPN-tunnel eller via andra nätverksgränssnitt (både mobildata och Wi-Fi)? Men själva datan i sig kan ju paketeras som UDP-, TCP- eller andra protokoll. Vår metod tar reda på allt, vilket det långa och skräckinjagande reguljära uttrycket i koden påvisar. Vi tittar alltså på den totala datatrafiken, in och ut från enheten via samtliga gränssnitt, först. Därefter tittar vi på datan för applikationen.

• Slutligen, hur många bytes har applikationen skickat och tagit emot totalt? Se metoden summarizeStats().
I metoden bryr vi oss om bara en endaste sak: Totala antalet bytes mottagna och skickade hittills över valda gränssnitt. I exemplet bryr vi oss bara om Wi-Fi och mobildata, men tillåter användaren att via parametrar ställa in vilka gränssnitt som är av intresse. På så sätt summerar vi inte över alla tänkbara gränssnitt såsom ‘Local loopback’-gränssnittet (som dock är intressant att analysera om du skickar kommandon till enheten via sockets).

Kod

Notera att koden nedan är väldigt primitiv i sin nuvarande form. Använd den gärna som utgångspunkt när du ska analysera nätverkstrafik. Tro dock inte att den täcker alla tänkbara fall eller är skriven perfekt, varken ur PEP-8 eller “ultimat pythonisk”-synpunkt.

Exempelvis är det få datapunkter som används i summarizeStats() och en hel del slängs i getAllNetworkDataStats(). Datapunkterna som slängs är endast till för att identifiera gränssnitt, trådar och bak- eller förgrundsdata, något som vi inte är intresserade av att särskilja eller presentera i det givna exemplet. Datapunkterna som inte slängs men returneras från getAllNetworkDataStats() är självklart användbara och med hjälp av lite ändringar eller tillägg i koden kan du få en mycket mer detaljerad representation av dataöverföringarna.


import subprocess
import re
    
class Adb(object):
    """
    Helper class that wraps adb to run commands
    towards an Android device
    """
    
    def __init__(self):
        pass
    
    def run_adb(self, serial, command):
        adb_command = "adb -s %s %s" % (serial, command)
        proc = subprocess.Popen(adb_command, 
                                stdout=subprocess.PIPE,
        shell=True, stderr=subprocess.PIPE)
        (response, err) = proc.communicate()
    
        # Note: adb will exit with return code 0 even if 
        # shell commands fail. Shell command will return 1 
        # if the command does not have a return value or explicit 
        # print out 
    
        if proc.returncode != 0 and proc.returncode != 1:
            raise Exception("Error executing command [%s] on "
                            "%s with Popen, exit code: %d, "
                            "stderr:" 
                            "%s, output: %s" % 
                            (command, serial, 
                             proc.returncode, 
                             err, response))
        return response
    
    def shell(self, serial, shell_command):
        command = "shell \"%s\"" % shell_command
        return self.run_adb(serial, command)
    
class NetworkDataStats:
    PACKAGE = "com.i3tex.example"
    SERIAL = "0c222d33ef7"  # Serial number of Android phone.
    
    def __init__(self):
        self.adb = Adb()


    def getUserId(self):
        package_info = self.adb.shell(self.SERIAL, 
                                      "dumpsys package %s |"
                                      "grep userId=" 
                                      % self.PACKAGE)
    
        match = re.search("userId=(\d+)", package_info)
        if match:
            return match.group(1)
        else:
            # App is not installed or PACKAGE has a typo
            raise Exception("%s is not installed on "
                            "this device!" % self.PACKAGE)

    def getAllNetworkDataStats(self):
        uid = self.getUserId()
        cmd_read_stats = "cat /proc/net/xt_qtaguid/stats |"\
                         " grep %s" % uid
 
        stats_response_table = \
            self.adb.shell(self.SERIAL, 
                           cmd_read_stats).splitlines()
        
        # Will contain data for each interface 
        # e.g. wlan, mobile data
        interfaces = {}

        for row in stats_response_table:
            stats = re.search( "(\d+) " # 1 idx
                               "(\w+) " # 2 iface
                               "(\w+) " # 3 acct_tag_hex
                               "(\d+) " # 4 uid_tag_int
                               "(\d+) " # 5 cnt_set
                               "(\d+) " # 6 rx_bytes (total)
                               "(\d+) " # 7 rx_packets (total)
                               "(\d+) " # 8 tx_bytes (total)
                               "(\d+) " # 9 tx_packets (total)
                               "(\d+) " # 10 rx_tcp_bytes
                               "(\d+) " # 11 rx_tcp_packets
                               "(\d+) " # 12 rx_udp_bytes
                               "(\d+) " # 13 rx_udp_packets
                               "(\d+) " # 14 rx_other_bytes
                               "(\d+) " # 15 rx_other_packets
                               "(\d+) " # 16 tx_tcp_bytes
                               "(\d+) " # 17 tx_tcp_packets
                               "(\d+) " # 18 tx_udp_bytes
                               "(\d+) " # 19 tx_udp_packets
                               "(\d+) " # 20 tx_other_bytes
                               "(\d+)", # 21 tx_other_packets
                               row)

        if stats and stats.group(4) == uid \
           and stats.group(3) == "0x0":
            iface = stats.group(2)
            temp_stats = []
            for i in range(6,22):
            temp_stats.append(int(stats.group(i)))
    
        # Case when interface has been iterated over once before
        if iface in interfaces:
            compound = [interfaces[iface]]
            compound.append(temp_stats)
            stats_sum = [sum(x) for x in zip(*compound)]
            interfaces[iface] = stats_sum
        else:
            interfaces[iface] = temp_stats
        return interfaces

    def summarizeStats(self, ifaces=["wlan", "rmnet"]):
        i_stats = self.getAllNetworkDataStats()
        sum_bytes_rx = 0
        sum_bytes_tx = 0
        for key in i_stats:
            data = i_stats[key]
            for i in ifaces:
                if i in key:
                    sum_bytes_rx += data[0]
                    sum_bytes_tx += data[1]
        return sum_bytes_rx, sum_bytes_tx

Förklaring

Exakt vilka värden som du kan få ut eller kan använda dig utav framgår, om än lite kryptiskt, i kommentarerna till det reguljära uttrycket. Det vi gör är att vi läser värden, eller rättare sagt en tabell, direkt från qtaguid-modulen i Androidenhetens Linuxkärna. Värdena har olika stora betydelser för vår analys. För att förstå exakt vad som händer – eller varför vi slänger vissa värden och fokuserar extra på andra kan du läsa igenom följande stycke där jag kortfattat “översätter” vad respektive värde eller kolumnrubrik innebär:

• idx – Ett unikt ID. Radnumret för ett specifik mätvärde i tabellen.

iface – anger vilket gränssnitt som mätvärdet refererar till. “Wlan0” representerar din telefons primära Wi-Fi-modul. “Rmnet0” är motsvarande för mobildata.

acct_tag_hex – en “tagg” som kan sättas av utvecklare för att enklare se vilken typ av data det är som överförs eller vilken tråd det är som överför datan. Om du inte bryr dig om denna sortens detaljrikedom så räcker det att fokusera på de rader som har taggen “0x0”. Alla andra värden, för samma UID med olika taggar, kommer ändå att summeras i den rad med tagg “0x0”.

uid_tag_int – UID för respektive app.

cnt_set – Är antingen 1 eller 0. En rad i tabellen med cnt_set = 0 sammanställer all bakgrundsdata. Är cnt_set = 1 så sammanställer raden all förgrundsdata. Summan av dessa två rader ger den totala dataöverföringen.

Innebörden av resterande värden framgår av att ta en extra titt på kommentarerna och dra lite egna slutsatser. “Rx” står för mottaget, “Tx” för skickat.

Som nämnt tidigare finns det många saker i koden som kan optimeras. Har du kritik, frågor eller förslag tar jag glatt emot dem!

Alexander Theofanous har studerat Datateknik vid Lunds Tekniska Högskola med en profil inom inbyggda system. Efter studierna fick han anställning hos i3tex i Linköping. Alexanders nuvarande uppdrag är som testingenjör på Sectra Secure Communications där han hjälper till att automatisera tester och utveckla integrationsmiljön. Innan dess har han jobbat med att utveckla Androidapplikationer.

Läs mer om vårt tjänsteområde Mjukvaruutveckling här >>

Postad: 15 november 2018