JSON über Socket versenden

Sockets, TCP/IP, (XML-)RPC und ähnliche Themen gehören in dieses Forum
Antworten
exidio
User
Beiträge: 18
Registriert: Dienstag 7. November 2017, 12:18

Guten Tag liebe Community.

Ich möchte mir auf dem Raspberry einen kleinen Server bauen, mit dem ich per 433MHz Funkmodul 4 Funksteckdosen steuern kann. Die Daten sollen per App an den Server geschickt werden, zum Testen und lernen geht es aber erst Client -> Server -> Steckdosen. Die Daten sehen folgendermaßen aus:
Hauscode (zb 10101), Nummer (der Steckdose, 1-4), Status (0-1).
Nun zu meinem Problem: Wie schaffe ich es, JSON-Daten über ein Socket zu schicken?

Auf meinem Server möchte ich folgendes haben:

Code: Alles auswählen

message = json.loads(json_data)			#line 39 der Fehlermeldung später
            hcode = message.get('hcode', None)
            device = message.get('device', None)
            status = message.get('status', None)
Geplant auf dem Client war folgendes:

Code: Alles auswählen

]d = json.dumps({"hcode": 10101, "device": '1', "status": True})
print(json.dumps(d))
s.send(b'd')	#wenn ich nur send(d) schreibe, kommt das: TypeError: a bytes-like object is required, not 'str'
Output Client:
"{\"hcode\": 10101, \"device\": \"1\", \"status\": true}"
data sent
Output Server:
File "D:/Users/xxxx/PycharmProjects/steckdosen/server.py", line 39, in switch_by_json
message = json.loads(json_data)
File "C:\Program Files\Python\Python36-32\lib\json\__init__.py", line 354, in loads
return _default_decoder.decode(s)
File "C:\Program Files\Python\Python36-32\lib\json\decoder.py", line 339, in decode
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
File "C:\Program Files\Python\Python36-32\lib\json\decoder.py", line 357, in raw_decode
raise JSONDecodeError("Expecting value", s, err.value) from None
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Ich bin absolut Neuling was JSON und Sockets angeht, verzeiht mir bitte meine Unwissenheit...
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich finde es ehrlich gesagt verwirrend, das json.dumps einen String und nicht einen Bytestring erzeugt...

Um einen String in ein Bytes-Objekt zu wandeln musst du ihn encodieren, in diesem Fall sollte ascii genug sein:

Code: Alles auswählen

json.dumps(ding).encode("ascii")
Allerdings wirst du dir mit deinem Ansatz frueher oder spaeter in den Fuss schiessen. Es ist ein fundamentaler Fehler anzunehemn das sockets Nachrichten verschicken und empfangen. Sie repraesentieren Byte-Stroeme. Niemand garantiert dir, das du beim lesen aus einem Socket das vollstaendige Datum empfangen hast, das an der anderen Seite reingestopft wurde. Das kann in beliebige Stuecke zerlegt werden.

Darum musst du zwingend ein Protokoll implementieren. Das ist allerdings nicht voellig trivial, und darum stellt sich mir die Frage: warum benutzt du nicht schon existierende Loesungen wie HTTP als Protokoll, mit den diversen Implementierungen fuer clients und server, die es in Python gibt? Oder du setzt auf eine andere Transportschicht, wie zB ZEROMQ, oder nanomsg, statt direkt sockets zu verwenden.
exidio
User
Beiträge: 18
Registriert: Dienstag 7. November 2017, 12:18

Lustigerweise kommt mit dem encode folgender Fehler:
File "C:\Program Files\Python\Python36-32\lib\json\encoder.py", line 180, in default
o.__class__.__name__)
TypeError: Object of type 'bytes' is not JSON serializable
Ich hatte vorher den Fehler
TypeError: a bytes-like object is required, not 'str'
Diese Fehlermeldungen sind für mich doch recht verwirrend.... Erst wills das, dann geht´s nicht damit?! :shock:

Ich komme über das Python3 Buch auf Sockets, was anderes ist mir dazu bisher nicht bekannt.
https://www.amazon.de/Python-umfassende ... XGGBNG949J

Wenn du mir diesbezüglich gute Quellen hast, um wirklich sinnvoll und richtig zu lernen, wäre ich dir dankbar!
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Zeig mal bitte deinen gesamten Code, der da zum Fehler fuehrt. So wie das aussieht versuchst du ein Bytes-Objekt zu JSON zu machen, statt eine Datenstruktur zu JSON das dann zu einem Byte-Objekt gewandelt werden muss.

Und das Buch.... puh. Frueher zumindest waren die Rheinwerk/Galileo Edition fuer Python2 eine ziemliche Katastrophe. Siehe

http://blog.marc.rintsch.de/2012/11/18/ ... nbook.html

Das die sockets so benutzen, ohne zu erklaeren, was man da fuer Probeleme zu beachten hat, ist fuer mich schon ein Kriterum zu sagen "ist Mist".

Besser beleumundet ist zB das freie Buch "Learn Python the hard way", allerdings fokussiert das glaube ich noch auf Python2. Mehr Quellen habe ich nicht, persoenlich lerne ich durch studieren von Dokumentation und Code. Die Grundlagen sind allerdings auch schon seit fast 30 Jahren drin...

Nochmal die Frage: warum nicht einfach HTTP benutzen? So etwas wie zB das bottle Microrahmenwerk kommt schon mit allem, was du zur Verarbeitung von JSON brauchst - ohne das du dich da kuemmern musst.
Benutzeravatar
noisefloor
User
Beiträge: 3843
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

@__deets__: die dritte Auflage des Buchs ist OK, da sind so gut wie alle Fehler eleminiert. Habe mal eine Rezension für freiesMagazin dazu geschrieben, die sollte auch noch online verfügbar sein.

Lern Python the hard way gibt es für Python 3 leider nicht mehr kostenlos, jedenfalls gibt es keine mir bekannte Quelle. LPTHW kostet jetzt glaube ich ~30,- USD - keine Ahnung, ob es das wert ist.

Zum Problem:
Sockets halt ich hier auch für die schlechteste Lösung - und technisch besteht dazu IMHO auch null Notwendigkeit. Einfach die Daten zwischen Client und Server per AJAX im JSON-Format austauschen und alles ist gut. Das ist auch einfach und robust zu implementieren.

@exido: in dem genannten Buch wird ja auch Django behandelt. Dieses Webframework ist ein paar Nummern zu groß für dein vorhaben. Bottle, Flask oder Hug wären für die Server-Seite die bessere Wahl.

Gruß, noisefloor
exidio
User
Beiträge: 18
Registriert: Dienstag 7. November 2017, 12:18

Puuh.... So krass kann man sich also in einem Buch täuschen und wundert sich dann, warum man alles falsch lernt :cry: :oops:
Dann liest man Berichte in der tiefsten Tiefe, aber weiß nicht, woher sie diese Infos haben, um sich das auch aneignen zu können...

Naja, nochmal zu meinem Projekt, sonst krieg ich noch mehr Kopfschmerzen als von meinem Code:

Client:

Code: Alles auswählen

import socket
import json
import sys
from time import sleep

IP = '127.0.0.1'
PORT = 50000

print('init socket...')

try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((IP, PORT))
    s.settimeout(500)
except socket.error as exc:
    print('Caught exception socket.error: {0}'.format(exc))
    sys.exit(0)
print("init socket successfull")

d = json.dumps({"hcode": 10101, "device": '1', "status": True}).encode("ascii")
print(json.dumps(d))
s.send(d)
print('data sent')
Der Server:

Code: Alles auswählen

from __future__ import unicode_literals
import socket
import json
import traceback
import sys
from pip import commands

IP = '127.0.0.1'
PORT = 50000        #Port und IP überprüfen

print("start server...")
try:
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind((IP, PORT))
    s.listen(1)
except Exception:
    print('an error occured...')
    sys.exit(0)
print("server started")


class Plug:
    def switch(self, hcode, id, status):
        command = ("sudo /home/pi/raspberry-remote/send {0} {1} {2}".format(hcode, id, '1' if status else '0'))
        print(command)
        status_code, result = commands.getstatusoutput(command)
        return status_code == 0


class Plugswitcher:
    def switch_by_json(self, json_data):
        try:
            message = json.loads(json_data)
            hcode = message.get('hcode', None)
            device = message.get('device', None)
            status = message.get('status', None)
            print('switching {0} in housecode {1} {2}'.format(device, hcode, 'on' if status else 'off'))
            if hcode and device and message:
                return Plug.switch(hcode, device, status)
        except:
            traceback.print_exc()
            return False


switcher = Plugswitcher()

try:
    while True:
        komm, addr = s.accept()
        data = komm.recv(1024)
        if not data:
            continue
        else:
            print(data)
            switcher.switch_by_json(data)

finally:
    s.close()

Vor langer langer Zeit, hat mir ein alter Kollege mal ein Grundgerüst dazu gebaut, das kopier ich auch mal rein. Mein Wunsch war es, die Einträge der Steckdosen nicht im Server zu belegen, sondern einfach nur den gesendeten Input zu verarbeiten. So kann man, später in der App, flexibel die Anzahl der Steckdosen festlegen und muss nicht den Code auf dem Server bearbeiten.

Grundgerüst:

Code: Alles auswählen

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from __future__ import unicode_literals
import os
import commands
import socket
import json
import traceback

print("Server wird gestartet...")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(("", 50000))
s.listen(1)

print("Server ist gestartet !")


class Plug:
	def __init__(self, name, house_code, id):
		self.name = name
		self.house_code = house_code
		self.id = id
		
	def switch(self, status):
		command = "sudo /home/pi/raspberry-remote/send {code } {id} {enabled}".format (		code = self.house_code, id = self.id, enabled = '1' if status else '0'	)
		print(command)
		status_code, result = commands.getstatusoutput(command)
		return status_code == 0
		
		
class Plugswitcher:
	def __init__(self):
		self.plugs = [
			Plug('Licht_1', '10101', 1),
			Plug('Licht_2', '10101', 2),
			Plug('Licht_3', '10101', 3),
			Plug('Licht_4', '10101', 4),
		 ]	
	
	def switch_by_json(self, json_data):
		try:
			message = json.loads(json_data)
			device = message.get('device', None)
			status = message.get('status', None)
			print(device, status)
			if device and message:
				for plug in self.plugs:
					return plug.switch(status)
		except:
			traceback.print_exc()
			return False
			
switcher = Plugswitcher()

try:
	while True:
		komm, addr = s.accept()
		data = komm.recv(1024)
		if not data:
			continue
		else: 
			print(data)
			switcher.switch_by_json(data)
			
finally:
	s.close
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

Mir ist noch kein allgemeines Programmierbuch über den Weg gelaufen, wo Sockets richtig erklärt werden, egal welche Programmiersprache. Bücher, die sich auf Netzwerkprogrammierung spezialisiert haben, sind da deutlich empfehlenswerter. Dazu kommt, dass 99% der Beispiele, die man im Netz findet, genauso Schrott sind.

@exidio: Deine Fehlerbehandlung ist sehr zweifelhaft. Die könnte man auch gleich ganz weglassen. Den Server solltest Du nicht an die lokale IP-Adresse binden, das macht Dir sonst später noch Scherereien. Die Klassen die Du da hast, sind eigentlich keine, den Aufwand kannst Du Dir sparen. Du benutzt sie nicht einmal, sondern rufst die Methoden direkt auf. Dank Deiner ungeprüften Formatierung eines sudo-Befehls hast Du mit Deinem Server eine wunderbare Backdoor eingerichtet, womit quasi jeder die komplette Kontrolle übernehmen kann. `pip` sollte man eigentlich in keinem Programm importieren. Nimm subprocess und übergib die geprüften Parameter als Liste. Deine socket-Communikation ist, wie von den anderen Teilnehmern hier schon geschrieben, fehlerhaft. Nimm ein HTTP-Framework für die Kommunikation (brauchst Du eh für Deine App), setzt statt JSON ein Rest-Like-Interface ein (reicht hier völlig).
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Dein Fehler kommt vom doppelten json.dumps. Einmal reicht.

Und ich wiederhol's nochmal: benutz HTTP. Da kannst du ggf. gleich mit REST arbeiten, womit auch die Struktur deines Plugin-Dinges schon fuer dich gegeben ist.
exidio
User
Beiträge: 18
Registriert: Dienstag 7. November 2017, 12:18

Okay okay, ich habs kapiert, völlig falscher Ansatz :lol:

Also für mich zusammengefasst:
Komplett neu anfangen, basierend auf HTTP-Framework (Flask?) und REST.
Das mit dem sudo wird schwierig, da das Script diese Rechte benötigt. Man möge dazu sagen, das wird auf einem lokalen Pi ohne Internetzugriff sein, in meinem eigenen kleinen Netzwerk. Möchtest du mir vielleicht trotzdem kurz erklären, warum das eine Backdoor erstellt?


Bis hierhin schon mal vielen herzlichen Dank an alle!
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@exidio: Du rufst in einer Shell mit root-Rechten ein Programm auf, ohne dass Du die Parameter, die Du übergibst auf Korrektheit prüfst. Sobald jemand in Deinem lokalen Netz ist, wird das gefährlich, aber auch unbeabsichtigt könntest Du Parameter übergeben, die irgendetwas seltsames machen. Daher:
1. keine Shell benutzen, sondern subprocess mit Liste.
2. Parameter auf Korrektheit prüfen
`sudo` kannst Du verwenden und ist auch kein generelles Problem, macht nur das Problem gefährlicher.
exidio
User
Beiträge: 18
Registriert: Dienstag 7. November 2017, 12:18

Vielen Dank für die Info!
Ich werd mir dann mal HTTP und REST anschauen und dann entsprechend was neues aufbauen. Mal schauen ob ich da was gescheites finde...

Danke euch für die große Hilfe!
exidio
User
Beiträge: 18
Registriert: Dienstag 7. November 2017, 12:18

Hallo allerseits,
ich hab einen funktionsfähigen Server hinbekommen. Das ganze geht über Flask ziemlich einfach. Danke für eure Hilfe!
Antworten