Python exemplarisch - RPi Tutorial
deutsch     english

KOMMUNIKATION MIT TCP/IP

 

Alle Programme können von hier heruntergeladen werden.


 

Datenübertragung mit TCP/IP

 

Der Informationsaustausch zwischen einem Programm, das auf dem Raspberry Pi und einem Programm, das auf einem entfernten Computer ausgeführt wird, ist vor allem dann wichtig, wenn der Raspberry Pi als Messsystem eingesetzt wird und die Sensordaten auf einem abgesetzten PC verarbeitet werden, oder wenn der Raspberry Pi (bzw. eine daran angeschlossene Roboterhardware) fremdgesteuert wird (sog. Remote Modus).

Die Datenübertragung erfolgt meistens über TCP/IP mit der Client-/Server-Technologie, die auf der Socket-Programmierung basiert. In diesem Kapitel zeigen wir typische Programme für eine einfache TCP-Kommunikation, ohne in Einzelheiten zu gehen. (Bei Unklarheiten ziehen Sie ein spezifisches Tutorials über Socket-Programmierung zu Rate.)

Wichtig und vielleicht etwas unerwartet ist die Tatsache, dass die Kommunikationspartner, Server und Client nicht symmetrisch sind. Zuerst muss das Serverprogramm gestartet werden und erst dann kann der Client eine Verbindung aufbauen. Die beiden Computer identifizieren sich im Internet mit ihrer IP-Adresse und kommunizieren über einen bestimmten TCP-Port (mit einer Portnummer von 0..65 535).

Der Server erstellt nach dem Start unter Verwendung eines bestimmten Ports einen Server-Socket, den man wie einen elektrischen Stecker auffassen kann. Der Client erstellt danach einen Client-Socket (wie das Gegenstück des Steckers) und versucht unter der Verwendung der IP-Adresse des Servers und der Portnummer eine Verbindung zum Server aufzunehmen. Im einfachsten Fall sind beide Geräte mit demselben Router verbunden, befinden sich also auf dem gleichen IP-Segment.
COMM1

 

 

Experiment 1: Programmierung mit einer Low-Level Socket- Bibliothek

 

In einer Client-/Server-Anwendung zwischen einem Raspberry Pi und einem Computer kann der Raspberry Pi (wie im Bild oben) der Server und der PC der Client sein. Je nach Situation können die Rollen aber auch vertauscht werden. Das folgende Beispiel zeigt eine typische Anwendung, wo der Raspberry Pi ein Frontend eines Messsystems ist, der dafür verantwortlich ist, Sensordaten an einen entfernten Computer weiterzuleiten. Das Programm ist unter Verwendung des standardmässigen Python Socket-Moduls geschrieben und dadurch relativ kompliziert.

Ziel:
Auf dem Raspberry Pi läuft ein Socket-Server. Jedesmal, wenn ein PC-Client den Befehl "go" sendet, bestimmt der RPi den Zustand eines GPIO-Eingangs und sendet die Statusmeldung "Button pressed" oder "Button released" an den Client zurück.

Programm:[►]

# DataServer1.py

from threading import Thread
import socket
import time
import RPi.GPIO as GPIO

VERBOSE = False
IP_PORT = 22000
P_BUTTON = 24 # adapt to your wiring

def setup():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(P_BUTTON, GPIO.IN, GPIO.PUD_UP)

def debug(text):
    if VERBOSE:
        print "Debug:---", text

# ---------------------- class SocketHandler ------------------------
class SocketHandler(Thread):
    def __init__(self, conn):
        Thread.__init__(self)
        self.conn = conn

    def run(self):
        global isConnected
        debug("SocketHandler started")
        while True:
            cmd = ""
            try:
                debug("Calling blocking conn.recv()")
                cmd = self.conn.recv(1024)
            except:
                debug("exception in conn.recv()") 
                # happens when connection is reset from the peer
                break
            debug("Received cmd: " + cmd + " len: " + str(len(cmd)))
            if len(cmd) == 0:
                break
            self.executeCommand(cmd)
        conn.close()
        print "Client disconnected. Waiting for next client..."
        isConnected = False
        debug("SocketHandler terminated")

    def executeCommand(self, cmd):
        debug("Calling executeCommand() with  cmd: " + cmd)
        if cmd[:-1] == "go":  # remove trailing "\0"
            if GPIO.input(P_BUTTON) == GPIO.LOW:
                state = "Button pressed"
            else:
                state = "Button released"
            print "Reporting current state:", state
            self.conn.sendall(state + "\0")
# ----------------- End of SocketHandler -----------------------

setup()
serverSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# close port when process exits:
serverSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 
debug("Socket created")
HOSTNAME = "" # Symbolic name meaning all available interfaces
try:
    serverSocket.bind((HOSTNAME, IP_PORT))
except socket.error as msg:
    print "Bind failed", msg[0], msg[1]
    sys.exit()
serverSocket.listen(10)

print "Waiting for a connecting client..."
isConnected = False
while True:
    debug("Calling blocking accept()...")
    conn, addr = serverSocket.accept()
    print "Connected with client at " + addr[0]
    isConnected = True
    socketHandler = SocketHandler(conn)
    # necessary to terminate it at program termination:
    socketHandler.setDaemon(True)  
    socketHandler.start()
    t = 0
    while isConnected:
        print "Server connected at", t, "s"
        time.sleep(10)
        t += 10
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Bemerkungen:
Mit dem Aufruf der blockierenden Funktion serverSocket.accept() wird der Server in den Listening-Zustand versetzt. Die Funktion kehrt mit Verbindungsinformationen zurück, sobald eine Verbindung zum Client erstellt ist. Nachfolgend wird der SocketHandler Thread gestartet, der die Kommunikation mit dem Client durchführt. Im Prinzip könnte das Hauptprogramm wieder in den Listening-Zustand gehen und auf einen anderen Client warten. In unserem Programm "hängt" aber der Hauptthread in einer Schleife, ohne etwas Besonderes zu tun.

Es wird empfohlen, ein Flag VERBOSE oder DEBUG zu definieren, um das System in einen "Debug-Modus" zu versetzen, damit zusätzliche Statusmeldungen ausgeschrieben werden. Sobald das Programm korrekt arbeitet, kann man den Debug-Modus abschalten.

Der Client sendet alle 2 Sekunden ein "go" und schreibt die Antwort aus, die er vom Server erhalten hat.

Programm:[►]

# DataClient1.py

from threading import Thread
import socket, time

VERBOSE = False
IP_ADDRESS = "192.168.0.17"
IP_PORT = 22000

def debug(text):
    if VERBOSE:
        print "Debug:---", text

# ------------------------- class Receiver ---------------------------
class Receiver(Thread):
    def run(self):
        debug("Receiver thread started")
        while True:
            try:
                rxData = self.readServerData()
            except:
                debug("Exception in Receiver.run()")
                isReceiverRunning = False
                closeConnection()
                break
        debug("Receiver thread terminated")

    def readServerData(self):
        debug("Calling readResponse")
        bufSize = 4096
        data = ""
        while data[-1:] != "\0": # reply with end-of-message indicator
            try:
                blk = sock.recv(bufSize)
                if blk != None:
                    debug("Received data block from server, len: " + \
                        str(len(blk)))
                else:
                    debug("sock.recv() returned with None")
            except:
                raise Exception("Exception from blocking sock.recv()")
            data += blk
        print "Data received:", data
# ------------------------ End of Receiver ---------------------

def startReceiver():
    debug("Starting Receiver thread")
    receiver = Receiver()
    receiver.start()

def sendCommand(cmd):
    debug("sendCommand() with cmd = " + cmd)
    try:
        # append \0 as end-of-message indicator
        sock.sendall(cmd + "\0")
    except:
        debug("Exception in sendCommand()")
        closeConnection()

def closeConnection():
    global isConnected
    debug("Closing socket")
    sock.close()
    isConnected = False

def connect():
    global sock
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    debug("Connecting...")
    try:
        sock.connect((IP_ADDRESS, IP_PORT))
    except:
        debug("Connection failed.")
        return False
    startReceiver()
    return True

sock = None
isConnected = False

if connect():
    isConnected = True
    print "Connection established"
    time.sleep(1)
    while isConnected:
        print "Sending command: go..."
        sendCommand("go")
        time.sleep(2)
else:
    print "Connection to %s:%d failed" % (IP_ADDRESS, IP_PORT)
print "done"    
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Bemerkungen:
Im Client-Programm wird für die Verarbeitung der empfangenen Daten ein neuer Thread gestartet. In dieser einfachen Situation wäre dies zwar nicht nötig, aber bei komplizierteren Client-/Server-Anwendungen ist dieses Vorgehen üblich.

 

 

ttt

Experiment 2: Viel einfacher mit einer zusätzlichen Software-Schicht

 

Unser ereignisgesteuertes Modul tcpcom kapselt die Komplexität der Socket-Programmierung (inklusive der Threads) in einer Klassenbibliothek (Informationen unter www.aplu.ch/tcpcom). Laden Sie das Modul tcpcom.py von hier herunter und kopieren Sie es in das gleiche Verzeichnis, in dem sich Ihr Programm befindet.

Bei gleich bleibender Funktionalität wird der Programmcode durch die Verwendung des Moduls wesentlich einfacher und man nimmt gerne eine zusätzliche Software-Schicht in Kauf.

Programm:[►]

# DataServer2.py

from tcpcom import TCPServer
import time
import RPi.GPIO as GPIO

IP_PORT = 22000
P_BUTTON = 24 # adapt to your wiring

def onStateChanged(state, msg):
    if state == "LISTENING":
        print "Server:-- Listening..."
    elif state == "CONNECTED":
        print "Server:-- Connected to", msg
    elif state == "MESSAGE":
        print "Server:-- Message received:", msg
        if msg == "go":
            if GPIO.input(P_BUTTON) == GPIO.LOW:
                server.sendMessage("Button pressed")
            else:
                server.sendMessage("Button released")

def setup():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(P_BUTTON, GPIO.IN, GPIO.PUD_UP)

setup()
server = TCPServer(IP_PORT, stateChanged = onStateChanged)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Bemerkungen:
Sie können den gleichen Client wie oben verwenden, aber unter Verwendung des Moduls tcpcom wird auch der Code des Clients viel einfacher.

Programm:[►]

# DataClient2.py

from tcpcom import TCPClient
import time

IP_ADDRESS = "192.168.0.17"
IP_PORT = 22000

def onStateChanged(state, msg):
    global isConnected
    if state == "CONNECTING":
       print "Client:-- Waiting for connection..."
    elif state == "CONNECTED":
       print "Client:-- Connection estabished."
    elif state == "DISCONNECTED":
       print "Client:-- Connection lost."
       isConnected = False
    elif state == "MESSAGE":
       print "Client:-- Received data:", msg

client = TCPClient(IP_ADDRESS, IP_PORT, stateChanged = onStateChanged)
rc = client.connect()
if rc:
    isConnected = True
    while isConnected:
        print "Client:-- Sending command: go..."
        client.sendMessage("go")
        time.sleep(2)
    print "Done"    
else:
    print "Client:-- Connection failed"      
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

 

 

Experiment 3: Zugriff auf einen lokalen Server über das Internet

 

In diesem Experiment werden die Rollen vertauscht: Der Raspberry Pi ist nun ein Client, der mit einem PC-Server kommuniziert. Dieser ist über ein WLAN- oder Ethernet-Router mit dem Internet verbunden. Client und Server befindet sich damit nicht mehr im gleichen IP-Segment, sondern irgendwo im Internet.

Damit der PC-Server vom Internet aus "sichtbar" ist, muss man auf dem Router das sog. "IP-Forwarding" aktivieren. Dabei geben Sie im Router-Management einen IP-Port (oder einen Bereichs von IP-Ports) und eine IP-Adresse als Destination an (diese Einstellung wird manchmal auch als "virtueller Server" bezeichnet). Jede Internet-Anfrage mit der IP-Adresse des Routers und einer Portnummer im angegebenen Bereich wird dann vom Router automatisch an den PC weitergeleitet.
comm2

Es gibt zwei Probleme: Da Ihr PC die IP-Adresse vom DHCP des Routers erhält, kann sich diese ändern, was dazu führt, dass die Weiterleitung nicht mehr funktioniert. Um dieses Problem zu vermeiden, können Sie auf dem PC statt DHCP eine feste IP-Adresse (im Segment des Routers) verwenden oder Sie binden unter Verwendung der Adress-Reservierung des Routers die IP-Adresse an die feste MAC-Adresse des WLAN- oder Ethernet-Adapters Ihres PCs (falls der Router diese Option unterstützt).

Das zweite Problem entsteht, weil der Router seine IP-Adresse von Ihrem Provider per DHCP erhält. Ändert sich diese ohne Ihr Wissen, so geht der Zugriff von aussen verloren. Dieses Problem kann durch die Verwendung eines Dynamic Update Client (DUC) gelöst werden. Dieses Tools wird von No-IP (noip.com) gratis zur Verfügung gestellt. Der DUC erstellt alle 5 Minuten (oder mit einem von Ihnen gewählten Intervall) eine Verbindung zum No-IP-Server und meldet diesem die aktuelle IP-Adresse des Routers. Von No-IP erhalten Sie zudem ein IP-Alias (eine URL) zugeteilt. Alle Anfragen eines TCP-Clients unter der Verwendung dieser URL werden in der Folge vom No-IP-Server an Ihren Router und von dort an den PC weitergeleitet.

comm3

Ziel:
Lassen Sie auf dem PC einen einfachen Socket-Server laufen, der den Port 22000 verwendet und lediglich Status-Informationen in einer Konsole ausschreibt. Konfigurieren Sie Ihren Router so, dass der Server von aussen sichtbar ist. Ein Client auf dem Raspberry Pi soll eine Verbindung herstellen und jede Sekunde eine Sensor-Information übermitteln (hier lediglich den Zustand eines Tastenschalters).

Wenn Sie die Bibliothek tcpcom verwenden, bleiben die Programme fast gleich. Der PC-Server zeigt in der Konsole die Sensorinformation, die er vom Client erhalten hat.

Programm:[►]

# DataServer3.py

from tcpcom import TCPServer

IP_PORT = 22000

def onStateChanged(state, msg):
    if state == "LISTENING":
        print "Server:-- Listening..."
    elif state == "CONNECTED":
        print "Server:-- Connected to", msg
    elif state == "MESSAGE":
        print "Server:-- Message received:", msg

server = TCPServer(IP_PORT, stateChanged = onStateChanged)
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Auch der Client-Code auf dem Raspberry Pi ist einfach.

(Wenn sich zu Testzwecken Server und Client im gleichen internen IP-Segment befinden, verwenden Sie beim Client die IP-Adresse des Server-PCs, die Sie mit ipconfig unter Windows oder mit ifconfig auf Mac / Linux oder unter Verwendung eines anderen Netzwerk-Tool herausfinden können.)

Programm:[►]

# DataClient3.py

from tcpcom import TCPClient
import time
import RPi.GPIO as GPIO

IP_ADDRESS = "192.168.0.111" # PC on same IP segment
#IP_ADDRESS = "5.149.19.200" # router's WAN address
#IP_ADDRESS = "raplu.zapto.org" # router's no-ip alias
IP_PORT = 22000
P_BUTTON = 24 # adapt to your wiring

def setup():
    GPIO.setmode(GPIO.BOARD)
    GPIO.setup(P_BUTTON, GPIO.IN, GPIO.PUD_UP)

def onStateChanged(state, msg):
    global isConnected
    if state == "CONNECTING":
       print "Client:-- Waiting for connection..."
    elif state == "CONNECTED":
       print "Client:-- Connection estabished."
    elif state == "DISCONNECTED":
       print "Client:-- Connection lost."
       isConnected = False
    elif state == "MESSAGE":
       print "Client:-- Received data:", msg

setup()
client = TCPClient(IP_ADDRESS, IP_PORT, stateChanged = onStateChanged)
rc = client.connect()
if rc:
    isConnected = True
    while isConnected:
        if GPIO.input(P_BUTTON) == GPIO.LOW:
            reply = "Button pressed"
        else:
            reply = "Button released"
        client.sendMessage(reply)
        print "Client:-- Sending message:", reply
        time.sleep(2)
    print "Done"    
else:
    print "Client:-- Connection failed"      
Programmcode markieren (Ctrl+C kopieren, Ctrl+V einfügen)

Bemerkungen:
Nachdem Sie erfolgreich die Kommunikation zwischen dem Server und Client im gleichen Netzwerksegment (am gleichen Router) erstellt haben, konfigurieren Sie Ihren Router durch IP-Forwarding so, dass der Server von aussen sichtbar ist und stellen eine Verbindung vom Raspberry Pi mit der IP-Adresse des Routers her.

Wenn Sie die IP-Adresse des Routers nicht kennen, so können Sie mit einem PC-Browser die Site www.portchecktool.com besuchen, wo die Adresse sichtbar ist. Wie Sie sehen, kann Sie ein externer Server bis zur IP-Adresse des Routers zurückverfolgen, die Sie von Ihrem Provider erhalten haben, der wahrscheinlich auch Ihre Personalien kennt.

Wenn alles funktioniert, können Sie mit dem Raspberry Pi von einem anderen Ort (auch weit entfernt) eine Internetverbindung zum PC-Server aufbauen. Wenn Sie zudem ein Konto bei no-ip.org erwerben und einen DUC (Dynamic Update Client) auf dem PC einrichten, so können Sie sogar eine feste URL verwenden.

Mit Ihrem Know-how ist es nun leicht, die Rollen zu tauschen und Server und DUC auf einem Raspberry Pi einzurichten.