Rheinwerk Computing < openbook > Rheinwerk Computing - Professionelle Bücher. Auch für Einsteiger.
Professionelle Bücher. Auch für Einsteiger.

Inhaltsverzeichnis
Geleitwort
Vorwort
1 Hello iPhone
2 Die Reise nach iOS
3 Sehen und anfassen
4 Alles unter Kontrolle
5 Daten, Tabellen und Controller
6 Models, Layer, Animationen
7 Programmieren, aber sicher
8 Datenserialisierung und Internetzugriff
9 Multimedia
10 Jahrmarkt der Nützlichkeiten
Stichwort

Jetzt Buch bestellen
Ihre Meinung?

Spacer
Apps programmieren für iPhone und iPad von Klaus M. Rodewig, Clemens Wagner
Das umfassende Handbuch
Buch: Apps programmieren für iPhone und iPad

Apps programmieren für iPhone und iPad
Rheinwerk Computing
1172 S., geb., mit DVD
49,90 Euro, ISBN 978-3-8362-2734-6
Pfeil 8 Datenserialisierung und Internetzugriff
Pfeil 8.1 Ich packe meine Texte
Pfeil 8.1.1 Serialisierung von Zeichenketten
Pfeil 8.1.2 Zeichenkodierungen
Pfeil 8.1.3 Unicode und UTF-8
Pfeil 8.1.4 Die Zeichenkodierung erkennen
Pfeil 8.1.5 Zeichenketten konvertieren
Pfeil 8.2 JSON und die URLonauten
Pfeil 8.2.1 Das JSON-Format
Pfeil 8.2.2 Einfacher YouTube-Zugriff mit JSON
Pfeil 8.2.3 URLs erstellen
Pfeil 8.2.4 JSON-Dokumente schreiben
Pfeil 8.2.5 Verwendung des JSONKits als Parser
Pfeil 8.3 XML
Pfeil 8.3.1 XML in Kürze
Pfeil 8.3.2 Property-Listen
Pfeil 8.3.3 SAX
Pfeil 8.3.4 DOM und XPath
Pfeil 8.3.5 Der Tag der Entscheidung
Pfeil 8.4 Daten, Daten, ihr müsst wandern
Pfeil 8.4.1 Synchrone Kommunikation
Pfeil 8.4.2 Komplexe Anfragen
Pfeil 8.4.3 Auf dem Webserver nichts Neues
Pfeil 8.4.4 Asynchrone Kommunikation
Pfeil 8.4.5 Große Datenmengen der Übertragung
Pfeil 8.4.6 Passwortabfragen
Pfeil 8.4.7 Sicher kommunizieren mit TSL (SSL)
Pfeil 8.4.8 Hier geht die POST ab
Pfeil 8.4.9 Dateiupload
Pfeil 8.4.10 Überprüfung der Erreichbarkeit
Pfeil 8.5 Karten
Pfeil 8.5.1 Karten darstellen
Pfeil 8.5.2 Koordinatensysteme
Pfeil 8.5.3 Geokoordinaten bestimmen
Pfeil 8.5.4 Eigene Kartenbeschriftungen
Pfeil 8.5.5 Routen

Rheinwerk Computing - Zum Seitenanfang

8.4Daten, Daten, ihr müsst wandernZur nächsten Überschrift

Sie haben mit JSON und XML zwei Möglichkeiten kennengelernt, Daten so zu verpacken, dass Sie sie über eine Internetverbindung übertragen können. In diesem Abschnitt erfahren Sie, wie Sie die Daten zwischen einem iOS-Gerät und einem Server übertragen können. Bei der Übertragung unterscheidet man zwischen synchronen und asynchronen Anfragen. Bei synchronen Anfragen wartet der Client so lange, bis das vollständige Ergebnis der Anfrage vorliegt, während eine asynchrone Anfrage den Programmlauf nicht nennenswert unterbricht. Bei diesem Vorgehen informiert das Betriebssystem den Prozess über den Fortschritt und das Ergebnis der Anfrage.

Der nächste Abschnitt beschreibt zwar die Möglichkeiten synchroner Anfragen, es gilt jedoch nach wie vor der Hinweis aus Abschnitt 8.2.2, »Einfacher YouTube-Zugriff mit JSON«: Synchrone Anfragen eignen sich allenfalls für kleine Datenmengen und Zugriffe auf schnelle und hochverfügbare Server. Für länger dauernde Anfragen sollten Sie lieber asynchrone Anfragen einsetzen.


Rheinwerk Computing - Zum Seitenanfang

8.4.1Synchrone KommunikationZur nächsten ÜberschriftZur vorigen Überschrift

Sie haben bereits Möglichkeiten aus dem Foundation-Framework für die synchrone Kommunikation kennengelernt. Verschiedene Klassen erlauben die Initialisierung ihrer Objekte durch synchrone Anfragen. Beispielsweise können Sie Objekte der Klasse NSData mit den Daten einer URL-Anfrage initialisieren (siehe Listing 8.2). Diese Möglichkeit steht Ihnen indes nicht nur bei NSData, sondern auch bei einigen anderen Klassen, z. B. NSString, NSArray und NSDictionary sowie deren veränderlichen Verwandten [Anm.: Also NSMutableData, NSMutableString, NSMutableArray und NSMutableDictionary] , zur Verfügung. Dabei erwarten die NSArray- und NSDictionary-Methoden, dass die Anfrage eine entsprechende Property-Liste zurückliefert.

Apple hat eine Unterstützung für die URL-Protokolle HTTP, HTTPS, FTP und FILE in iOS eingebaut, und somit können Sie alle URLs verwenden, die mit den genannten Protokollen beginnen. Dieser Abschnitt beschäftigt sich ausschließlich mit der Übertragung per HTTP(S), da das FILE-Protokoll nur auf lokale Dateien zugreifen kann und FTP aufgrund seiner Unsicherheit nicht empfehlenswert ist.

FTP? Nein, danke

Das File Transfer Protocol (FTP) ist inzwischen vollkommen veraltet, kompliziert und komplett unsicher, da es die Daten unverschlüsselt im Netzwerk überträgt. Sie sollten es also lieber nicht verwenden und stattdessen auf modernere Protokolle wie beispielsweise WebDAV, das auf HTTP basiert, ausweichen. Aber auch da ist Verschlüsselung notwendig, die Sie ganz einfach über die Verwendung von SSL (HTTPS) erreichen können (siehe Abschnitt 8.4.7, »Sicher kommunizieren mit TSL (SSL)«).

Apple findet FTP auf dem Mac schon seit Jahren so irrelevant, dass der Finder von OS X bis heute zwar FTP unterstützt, aber nur den lesenden Zugriff auf Server, nicht den schreibenden.

Außerdem können Sie auf URLs mit Basic-Authentication-Schutz [Anm.: http://de.wikipedia.org/wiki/HTTP-Authentifizierung#Basic_Authentication] zugreifen, indem Sie die Zugangsdaten in der URL mitgeben. Wenn Sie beispielsweise die geschützten Daten hinter der URL http://www.beispiel.de/datei.xml auslesen möchten, können Sie den Login-Namen und das Passwort als Klartext vor den Servernamen schreiben. Als Trennzeichen verwenden Sie dabei einen Doppelpunkt beziehungsweise einen Klammeraffen, so dass die URL die Form http://login:passwort@www.beispiel.de/datei.xml bekommt.

Authentifizierungsinformationen in der URL

Die Angabe von Login-Passwort-Kombinationen in URLs ist natürlich problematisch, wenn Sie diese URL so in Ihren Programmcode schreiben; ein Angreifer kann den Programmcode analysieren und diese Daten auslesen. Jedoch auch ohne Login-Passwort-Kombination in der URL sollten Sie die Basic Authentication allenfalls als rudimentären Schutz Ihrer Inhalte einsetzen, weil sie die Authentifizierungsdaten im Klartext überträgt.

Besser ist hier die Verwendung der Digest Authentication, die anstelle des Passworts Hashwerte verwendet, so dass ein Angreifer das Passwort nicht aus den Anfragen auslesen kann. Allerdings sichern Sie damit auch nur die Authentifizierung ab, für eine sichere Datenübertragung sollten Sie HTTPS verwenden.

Die Klassen NSArray und NSDictionary bieten jeweils nur einen Convenience-Konstruktor (arrayWithContentsOfURL: beziehungsweise dictionaryWithContentsOfURL:) und den Initializer initWithContentsOfURL: für URL-Anfragen an. Diese Methoden liefern im Fehlerfall nil zurück, und eine weitere Beeinflussung oder Auswertung der Anfrage ist nicht vorgesehen. Sie sollten diese Methoden allenfalls einsetzen, wenn Sie keine genauere Fehleranalyse in der Applikation vornehmen möchten.

Die Klasse NSData bietet neben dataWithContentsOfURL: und initWithContentsOfURL: noch jeweils die Alternativen dataWithContentsOfURL:options:error: und initWithContentsOfURL:options:error: an. Dabei ist der options-Parameter zurzeit nur bei Datei-URLs von Bedeutung; bei HTTP(S)-URLs können Sie hierfür den Wert 0 angeben. Der error-Parameter ist wie üblich ein Ausgabeparameter, für den Sie eine Referenz auf einen NSError-Zeiger angeben:

NSURL *theURL = ...;
NSError *theError = nil;
NSData *theData = [NSData dataWithContentsOfURL:theURL
options:0 error:&theError];

if(theData == nil) {
// Fehler auswerten
}
else {
// Geladene Daten verarbeiten
}

Listing 8.40 Synchrone Anfrage mit Fehlerauswertung

Die Klasse NSString besitzt keine einfache Variante, bei der Sie nur die URL angeben. Bei den Methoden stringWithContentsOfURL:encoding:error: beziehungsweise initWithContentsOfURL:encoding:error: können Sie die Zeichenkodierung vorgeben, mit der die Methode die Zeichenkette aus den empfangenen Daten dekodiert. Für den encoding-Parameter verwenden Sie die Konstanten, die Tabelle 8.1 teilweise auflistet.

Bei unbekannter Zeichenkodierung können Sie hingegen eine der Methoden stringWithContentsOfURL:usedEncoding:error: oder initWithContentsOfURL:usedEncoding:error: verwenden, bei denen usedEncoding ein Ausgabeparameter ist, was Sie in Listing 8.41 an dem &-Zeichen vor der Variablen theEncoding erkennen. Diese Methoden versuchen also, die richtige Kodierung zu bestimmen, und liegen dabei in den meisten Fällen auch richtig.

NSURL *theURL = ...;
NSStringEncoding theEncoding = 0;
NSError *theError = nil;
NSString *theString = [NSString stringWithContentsOfURL:
theURL usedEncoding:&theEncoding error:&theError];

Listing 8.41 URL-Anfrage mit Bestimmung der Kodierung

Nach der Anfrage in Listing 8.41 enthält die Variable theEncoding die Zeichenkodierung, die die Methode für die Umwandlung der geladenen Daten in die Zeichenkette verwendet hat.


Rheinwerk Computing - Zum Seitenanfang

8.4.2Komplexe AnfragenZur nächsten ÜberschriftZur vorigen Überschrift

Wenn Sie in der Anfrage bestimmte Header oder Daten mitschicken wollen, reichen die hier vorgestellten Methoden hingegen nicht aus, und auch eine genauere Analyse der Antwort ist damit nicht möglich. Dafür müssen Sie die Klasse NSURLConnection verwenden, die sowohl synchrone als auch asynchrone URL-Anfragen unterstützt. Eine synchrone Anfrage können Sie über die Methode sendSynchronousRequest:returningResponse:error: ausführen, wobei Sie dieser Methode im ersten Parameter ein Objekt übergeben, das die Anfrage beschreibt. Es enthält neben der URL einen Timeout und kann außerdem Daten und Anfrageheader aufnehmen. Im folgenden Listing sehen Sie den generellen Aufbau einer synchronen Anfrage:

NSURLRequest *theRequest = ...;
NSURLResponse *theResponse = nil;
NSError *theError = nil;
NSData *theData =
[NSURLConnection sendSynchronousRequest:theRequest
returningResponse:&theResponse error:&theError];

Listing 8.42 Synchrone Anfrage mit Anfrage- und Antwortobjekt

Das zurückgelieferte Antwortobjekt enthält verschiedene Metadaten einer Serverantwort. Wie wir bereits erwähnt haben, können Sie HTTP-Anfragen noch weitere Daten mitgeben. In Listing 8.43 sehen Sie ein Beispiel für eine Anfrage, wie Safari sie auf dem iPhone unter iOS 5.1.1 beim Aufruf der URL http://www.apple.com/de versendet:

GET /de/ HTTP/1.1
Host: www.apple.com:80
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 5_1_1
like Mac OS X) AppleWebKit/534.46 (KHTML,
like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3
Accept: text/html,application/xhtml+xml,
application/xml;q=0.9,*/*;q=0.8
Accept-Language: de-de
Accept-Encoding: gzip, deflate
Connection: keep-alive

Listing 8.43 HTTP-Anfrage an einen Webserver

Die erste Zeile beginnt mit der verwendeten HTTP-Methode GET, weswegen dieser Anfragetyp auch GET-Anfrage heißt. Darauf folgen der Anfrage-URI /de/ und das verwendete Protokoll HTTP/1.1. Dabei besteht der Anfrage-URI immer aus dem Teil der URL, der auf den Server (und gegebenenfalls den Port) folgt, jedoch ohne eventuell vorhandene Anker. Die Bestandteile einer HTTP(S)-URL stellt Abbildung 8.14 an einem Beispiel dar, wobei alle Bestandteile bis auf den Server und das Schema optional sind.

Abbildung

Abbildung 8.14 Die Bestandteile einer HTTP(S)-URL

Die Angabe von Authentifizierungsdaten der Form login:password@ vor dem Server in der URL gehört nicht zum offiziellen Standard bei HTTP-URLs. Das iOS liest diese Angaben zwar aus und verwendet sie für die Anfrage, jedoch kann nicht jeder Browser damit umgehen. Der Client wertet diese Angaben aus und modifiziert die Anfrage für den Server entsprechend.

Die weiteren Zeilen der HTTP-Anfrage aus Listing 8.43 enthalten Headerparameter, die jeweils immer in genau einer Zeile stehen. Als Zeilentrenner verwendet HTTP die Zeichenfolge CR-LF, das sind die ASCII-Codes 13 und 10 oder die C-Escape-Sequenzen \r\n.

  • Der User-Agent enthält Informationen über den verwendeten Browser und das Betriebssystem der Anfrage. Manche Webseiten nutzen diese Informationen, um browserspezifische Webinhalte bereitzustellen.
  • Der Accept-Header informiert den Server, welche Inhaltstypen er darstellen kann. Dabei erfolgen diese Angaben in Form von MIME-Typen. [Anm.: http://de.wikipedia.org/wiki/MIME-Typ]
  • Über Accept-Language übermittelt der Client dem Server die Sprache und gegebenenfalls das Land des Nutzers. Webauftritte können diese Angaben verwenden, um dem Nutzer automatisch den Inhalt in seiner bevorzugten Sprache anzuzeigen.
  • Um die Datenmenge zwischen Client und Server zu reduzieren, kann der Server die Daten komprimieren, wenn der Client ihm über das Accept-Encoding die Erlaubnis dazu erteilt. Dabei steht gzip für die GZip-Komprimierung, während deflate für die unkomprimierte Datenübertragung steht.
  • Ein Browser kann mehrere Anfragen über eine Verbindung durchführen; dazu muss er vom Server über Connection den Wert Keep-Alive erhalten. Der Standardwert ist Close und bedeutet, dass der Server nach Auslieferung der Antwort die Verbindung unterbricht.

Bis auf den Host-Header in der zweiten Zeile, der den Servernamen und den Port enthält, sind beim HTTP-1.1-Protokoll übrigens alle weiteren Header optional. Ein doppelter Zeilenvorschub beendet den Header der Anfrage, und darauf können beliebige Daten folgen.

Die Klasse NSURLRequest stellt Anfrageobjekte dar und besitzt die beiden Convenience-Konstruktoren requestWithURL: und requestWithURL:cachePolicy:timeoutInterval:. Mit dem cachePolicy-Parameter der zweiten Methode können Sie bestimmen, ob iOS das Ergebnis der Anfrage speichern soll, um es bei einer erneuten Anfrage eventuell wiederzuverwenden.

Tabelle 8.14 Die Regeln für die Cache-Verwendung bei URL-Anfragen

Konstante Beschreibung

NSURLRequestUseProtocolCachePolicy

Verwendet die Vorgaben des Protokolls.

NSURLRequestReloadIgnoringLocalCacheData

Lädt die Daten vom Server.

NSURLRequestReloadIgnoringLocalAndRemoteCacheData

Lädt die Daten vom Server und weist alle zwischengeschalteten Proxys an, dies ebenfalls so zu machen.

NSURLRequestReloadIgnoringCacheData

veraltet; analog zu NSURLRequestReload-IgnoringLocalCacheData

NSURLRequestReturnCacheDataElseLoad

Verwendet die Daten aus dem Cache unabhängig von ihrem Alter oder Verfallsdatum. Nur wenn kein entsprechender Eintrag im Cache vorhanden ist, lädt iOS die Daten vom Server.

NSURLRequestReturnCacheDataDontLoad

Verwendet die Daten aus dem Cache unabhängig von ihrem Alter oder Verfallsdatum. Wenn kein entsprechender Eintrag im Cache vorliegt, lädt iOS die Daten auch nicht vom Server. Diese Konstante eignet sich zur Realisierung eines Offlinemodus.

NSURLRequestReloadRevalidatingCacheData

Verwendet die Daten aus dem Cache nur, wenn der Server, von dem sie stammen, ihre Gültigkeit bestätigt hat. Andernfalls lädt iOS die Daten neu.

Wenn Sie ein Anfrageobjekt über [NSURLRequest requestWithURL:...] erzeugen, können Sie das Objekt danach nicht verändern. Wollen Sie hingegen noch Header der Anfrage setzen oder Daten an die Anfrage anhängen, müssen Sie stattdessen die Klasse NSMutableURLRequest verwenden. Bei den Objekten dieser Klasse können Sie über die Methode setValue:forHTTPHeaderField: einzelne Header setzen, und über setHTTPMethod: lässt sich auch die HTTP-Anfragemethode von GET auf eine andere Methode umstellen. Über die Methode setHTTPBody: übergeben Sie ein Objekt der Klasse NSData an die Anfrage, um seine Daten mit an den Server zu schicken.

Der Aufbau einer Antwort ähnelt sehr stark dem Aufbau von Anfragen. Auf die Anfrage aus Listing 8.43 kann der Server beispielsweise mit der folgenden Antwort reagieren:

HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Server: Apache/2.2.3 (Oracle)
Cache-Control: max-age=195
Expires: Mon, 02 Jul 2012 21:01:53 GMT
Date: Mon, 02 Jul 2012 20:58:38 GMT
Content-Length: 9425
Connection: keep-alive

<!DOCTYPE html>
...

Listing 8.44 Die Antwort eines HTTP-Servers

In der ersten Zeile stehen das Protokoll HTTP/1.1 und der Status der Antwort als Zahl 200 und als Text OK. Darauf folgen wie bei der Anfrage mehrere Zeilen mit Antwortheadern, die Informationen über die Gültigkeitsdauer des Seiteninhalts geben. Der Header Content-Type beschreibt über einen MIME-Typ den Inhalt der Antwort. In dem Beispiel handelt es sich um einen Text im HTML-Format, der die Zeichenkodierung UTF-8 besitzt. Auch bei der Antwort trennt ein doppelter Zeilenvorschub den Header von den Daten; das ist hier der Quelltext der HTML-Seite. Häufig teilt der Server über den Header Content-Length dem Client die Größe der Antwortdaten mit. Diese Information können Sie unter anderem für die Anzeige von Fortschrittsbalken nutzen.

Anfragen simulieren

Mit dem Kommandozeilenprogramm curl können Sie sich die Header der Anfrage und der Antwort ansehen, wenn Sie die Kommandozeilenoption -v verwenden; Beispiel: curl -v 'http://www.apple.de'. Da in einer URL häufig Zeichen vorkommen, die das Terminal beziehungsweise die Shell falsch verstehen, sollten Sie die URL wie im Beispiel immer in einfache Hochkommas setzen.

Wenn Sie sich hingegen nur für den Antwortheader interessieren, können Sie die Option -I verwenden, die anstelle einer GET- eine HEAD-Anfrage an den Server schickt. Der Server sollte auf diese Anfragen ausschließlich mit dem Antwortheader antworten und keine Daten mitschicken.


Rheinwerk Computing - Zum Seitenanfang

8.4.3Auf dem Webserver nichts NeuesZur nächsten ÜberschriftZur vorigen Überschrift

Webbrowser und viele andere Applikationen verwenden HEAD-Anfragen, um den Status von Seiten oder Inhalten abzufragen. Applikationen, die ihre Inhalte aus dem Internet nachladen, sollten dies möglichst nicht bei jedem Start tun. Ansonsten sind sie für den Offlinebetrieb nicht zu gebrauchen, und wahrscheinlich ärgern sich auch die Nutzer über die stets langen Wartezeiten.

Es ist viel sinnvoller, die Inhalte beim ersten Start zu laden und in Dateien abzulegen. Wenn der Nutzer die Applikation erneut startet oder sie aus dem Hintergrund aufweckt, prüft die Applikation zunächst über eine HEAD-Anfrage, ob es neue Daten gibt, und nur in diesem Fall lädt sie die Daten. Gerade bei großen Dateien erweist sich dieses Vorgehen als sehr sinnvoll, und falls die App keine Verbindung zum Server herstellen kann, kann sie immer noch mit den gespeicherten Daten arbeiten.

Dieses Verfahren funktioniert indes nur sinnvoll, wenn der Server den Header Last-Modified mitliefert, der einen Zeitstempel mit der letzten Änderung der angefragten Ressource enthält. Der weitverbreitete Webserver Apache, den Apple beispielsweise auch mit OS X ausliefert, verwendet bei Dateien für diesen Header deren Änderungsdatum. Ob der Server diesen Header für eine spezifische URL mitliefert, können Sie ja ganz einfach über curl ausprobieren.

In OS X vor Mountain Lion (10.8) können Sie den Webserver über die Systemeinstellungen starten. Dazu aktivieren Sie unter Freigaben die Webfreigabe (siehe Abbildung 8.15).

Abbildung

Abbildung 8.15 Aktivieren der Webfreigabe unter Lion

Webserver unter Mountain Lion aktivieren

Unter Mountain Lion hat Apple leider diese bequeme Möglichkeit abgeschafft, obwohl noch die notwendigen Komponenten vorhanden sind. Die Aktivierung können Sie jedoch über das Terminal leicht selbst durchführen. Dazu finden Sie auf der DVD das Tool Code/Tools/configure-apache.sh, das Sie auch aus dem Github-Repository über die URL https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/Tools/configure-apache.sh laden können.

Wenn Sie es im Terminal ausführen, erzeugt es eine Datei, die den Ordner Websites in Ihrem Heimatverzeichnis freigibt. Diese Datei müssen Sie als Rootnutzer in den Ordner /etc/apache2/users kopieren. Das geht am einfachsten über den Befehl sudo cp user.conf /etc/apache2/users. Der genaue Name der Datei hängt dabei von Ihrem Nutzernamen auf Ihrem Rechner ab. Danach können Sie den Webserver über das Kommando

sudo apachectl start

starten und über

sudo apachectl stop

auch wieder stoppen.

Sie können die Funktionsweise Ihres Webservers ganz einfach über die URL http://localhost überprüfen. Wenn Sie in der Kommandozeile den Befehl curl -I 'http://localhost/index.html' ausführen, erhalten Sie als Ergebnis eine Antwort, die einen Content-Length-Header enthält:

Last-Modified: Sat, 08 Oct 2011 08:29:19 GMT
Accept-Ranges: bytes
Content-Length: 44
Content-Type: text/html

Listing 8.45 Ausschnitt aus einer Antwort mit »Content-Length«

Diese Informationen können Sie auch über eine synchrone Anfrage auslesen. Im Beispielprojekt SiteSchedule finden Sie die Kategorie NSDictionary(HTTPRequest) mit der Klassenmethode dictionaryWithHeaderFieldsForURL:. Sie sendet an eine beliebige HTTP(S)-URL eine HEAD-Anfrage und liefert ein Dictionary mit allen Headern der Antwort zurück:

+(id)dictionaryWithHeaderFieldsForURL:(NSURL *)inURL {
NSDictionary *theResult = nil;

if([@"http" isEqualToString:[inURL scheme]] ||
[@"https" isEqualToString:[inURL scheme]]) {
NSMutableURLRequest *theRequest =
[NSMutableURLRequest requestWithURL:inURL
cachePolicy:
NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:3.0];
NSURLResponse *theResponse = nil;
NSError *theError = nil;

[theRequest setHTTPMethod:@"HEAD"];
[NSURLConnection sendSynchronousRequest:theRequest
returningResponse:&theResponse error:&theError];
if(theError == nil && [theResponse
respondsToSelector:@selector(allHeaderFields)]) {
theResult = [(id)theResponse allHeaderFields];
}
}
return theResult;
}

Listing 8.46 Senden und Verarbeiten einer HEAD-Anfrage

Diese Methode verwendet kein Caching, einen Timeout von 3 Sekunden und stellt die Anfragemethode im Anfrageobjekt auf HEAD um. Sie verwendet von der Antwort nur die Antwortheader, die die Methode allHeaderFields liefert. Diese Methode stellt hingegen nicht die Klasse NSURLResponse, sondern deren Unterklasse NSHTTPURLResponse bereit. Aus diesem Grund prüft die Klassenmethode vor dem Zugriff, ob das Antwortobjekt die Methode allHeaderFields besitzt.

Die Kategorie NSDictionary(HTTPRequest) stellt weitere Methoden bereit, um auf die meistverwendeten Header bequem zugreifen zu können. Dabei liefern die Methoden lastModified und contentLength ihre Ergebnisse als Zeitstempel beziehungsweise Zahl zurück, damit Sie diese Werte auch sinnvoll verwenden können. Sie können diesen Wert auch über die Methode expectedContentLength direkt aus der Antwort lesen.

- (NSDate *)lastModified {
NSString *theDate = self[@"Last-Modified"];

return [NSDateFormatter dateForRFC1123String:theDate];
}

- (NSString *)contentType {
return self[@"Content-Type"];
}

- (long long)contentLength {
NSString *theLength = self[@"Content-Length"];

return [theLength longLongValue];
}

Listing 8.47 Zugriffsmethoden für wichtige Antwortheader

Der Wert für das Änderungsdatum im Last-Modified-Header sieht auf den ersten Blick etwas ungewöhnlich aus. Glücklicherweise ist das Format jedoch standardisiert, und jeder Webserver muss sich daran halten. Im Beispielprojekt erzeugt die Klassenmethode dateForRFC1123String: der Kategorie NSDateFormatter(Extensions) zu Zeichenketten in diesem Format die entsprechenden NSDate-Objekte.

Mit Hilfe dieser Methoden können Sie nun das oben beschriebene Vorgehen zum Datendownload umsetzen. Die URL auf die Daten liefert dabei die Methode downloadURL, und das Änderungsdatum der Daten des letzten Downloads können Sie einfach in den User-Defaults speichern. Die folgende Methode überprüft, ob die Applikation die Daten vom Server laden muss.

- (BOOL)downloadRequired {
NSUserDefaults *theDefaults =
[NSUserDefaults standardUserDefaults];
NSDate *theUpdateTime = theDefaults[@"updateTime"];

if(theUpdateTime == nil) {
return YES;
}
else {
NSURL *theURL = self.downloadURL;
NSDictionary *theFields = [NSDictionary
dictionaryWithHeaderFieldsForURL:theURL];
NSDate *thModificationTime = theFields.lastModified;

return [theUpdateTime compare:theModificationTime] <
NSOrderedSame;
}
}

Listing 8.48 So wird geprüft, ob ein Datendownload notwendig ist.

Tipp

Sie können diese Prüfung mit dem Programm SiteSchedule ausprobieren, indem Sie den Webserver auf Ihrem Mac einschalten (siehe oben), die Datei schedule.xml aus dem Projekt in Ihren Websites-Ordner legen und in der Datei DownloadViewController.m die URL anpassen. Suchen Sie dazu die Zeile

static NSString * const kDownloadURL = kDefaultDownloadURL;

und ersetzen Sie das Makro auf der rechten Seite durch die URL zu der Datei. Das ist die erste URL in Abbildung 8.15 mit angehängter Datei; hier also http://10.0.1.27/~clemens/schedule.xml. Auf Ihrem Mac müssen Sie natürlich die URL verwenden, die Ihre Systemeinstellungen anzeigen. Sie sollten hingegen nicht »localhost« als Servernamen verwenden: Der funktioniert zwar wunderbar auf Ihrem Mac, jedoch nicht auf Ihrem iPhone. Dort ist ja »localhost« das iPhone selbst und nicht Ihr Mac.

Sie können dann in der App die Daten über den Button Download erneuern oder die Zeitstempel über den Button prüfen miteinander vergleichen. Dabei können Sie eine Aktualisierung der Daten auf dem Server auch durch den Kommandozeilenbefehl touch ~/Sites/schedule.xml simulieren.

Der Programmcode aus Listing 8.48 startet den Datendownload nur, wenn die User-Defaults kein Aktualisierungsdatum enthalten oder das Änderungsdatum vom Server neuer als das Datum aus den User-Defaults ist. Am Ende des erfolgreichen Downloads muss die Applikation das Modifikationsdatum in den User-Defaults speichern, damit sie bei einer erneuten Ausführung des Codes in Listing 8.48 nicht die bereits geladenen Daten schon wieder lädt.

Mit einer synchronen Anfrage könnte die Methode downloadDataFromURL: beispielsweise so aussehen:

- (void)downloadDataFromURL:(NSURL *)inURL {
NSURLRequest *theRequest = [NSURLRequest
requestWithURL:inURL];
id theResponse = nil;
NSError *theError = nil;
NSData *theData = [NSURLConnection
sendSynchronousRequest:theRequest
returningResponse:&theResponse error:&theError];

if(theError == nil &&
[theResponse isKindOfClass:[NSHTTPURLResponse class]]
&& [theResponse statusCode] == 200) {
NSString *theFile = ...;
NSDictionary *theFields =
[theResponse allHeaderFields];
NSUserDefaults *theDefaults =
[NSUserDefaults standardUserDefaults];

if([theData writeToFile:theFile atomically:YES]) {
[theDefaults setObject:theFields.lastModified
forKey:@"updateTime"];
}
}
}

Listing 8.49 Datendownload über eine synchrone Anfrage

Diese Methode verwendet dabei das Antwortobjekt der Anfrage, um den Zeitstempel in den User-Defaults zu aktualisieren. Bei der Auswertung der Antwort ist es außerdem wichtig, dass Sie den Statuscode der Antwort auswerten. Nur wenn er den Wert 200 hat, ist die Antwort gültig. Bei anderen Werten (z. B. »404/Not Found« [Anm.: Die angeforderte Ressource existiert nicht.] ) enthalten die Daten eine Fehlerseite des Servers, jedoch nicht den gewünschten Inhalt.


Rheinwerk Computing - Zum Seitenanfang

8.4.4Asynchrone KommunikationZur nächsten ÜberschriftZur vorigen Überschrift

Bislang haben Sie nur Möglichkeiten für synchrone Anfragen kennengelernt. Diese Anfragemethode hat indes einige schwere Mängel:

  • Die Anfrage blockiert den Programmablauf.
  • Sie können den Fortschritt des Downloadprozesses nicht beobachten oder die Übertragung abbrechen.
  • Sie können damit keine Daten laden, die größer als der verfügbare Hauptspeicher sind.

Die Mängel treten dabei natürlich umso heftiger zutage, je größer die Datenmenge ist. Sie lassen sich jedoch relativ einfach durch eine asynchrone Kommunikation umgehen. Dabei startet der Client die Kommunikation über eine Methode, die allerdings nicht auf das Ergebnis wartet. Stattdessen benachrichtigt das System die Applikationen über Zustandsänderungen.

Die Klasse NSURLConnection bietet zwei unterschiedliche Methoden für die Ausführung von asynchronen Anfragen. Bei der einfacheren Variante geben Sie einen Block an, der das Ergebnis der Anfrage behandelt, oder Sie implementieren alternativ ein Delegate und erhalten so detailliertere Informationen über den Ladevorgang. Zunächst betrachten wir jedoch die einfachere Möglichkeit; mit ein paar Änderungen ersetzen Sie in Listing 8.49 die synchrone durch eine asynchrone Anfrage:

[NSURLConnection sendAsynchronousRequest:theRequest  
queue:[NSOperationQueue mainQueue]
completionHandler:^(NSURLResponse *inResponse,
id theResponse = inResponse;
NSData *inData, NSError *inError) {
if(inError == nil &&
[theRsponse isKindOfClass:
[NSHTTPURLResponse class]] &&
[theResponse statusCode] == 200) {
NSString *theFile = ...;
NSDictionary *theFields =
[theResponse allHeaderFields];
NSUserDefaults *theDefaults =
[NSUserDefaults standardUserDefaults];

if([inData writeToFile:theFile atomically:YES]) {
[theDefaults setObject:
theFields.lastModified
forKey:@"updateTime"];
}
}
}];

Listing 8.50 Ausführen einer einfachen asynchronen Anfrage

Listing 8.50 zeigt den angepassten Code und hebt die Änderungen hervor. Die asynchrone Anfrage initiiert die Klassenmethode sendAsynchronousRequest:queue:completionHandler:, die im zweiten Parameter eine Operationqueue erhält und im dritten einen Block, den sie am Ende der Anfrage ausführt. Der Inhalt des Blocks entspricht weitgehend der if-Anfrage in Listing 8.49. Von den beiden Parametern des Blocks ist übrigens immer entweder der Daten- oder der Fehlerparameter ungleich nil. Sie erhalten also entweder Daten oder einen Fehler als Ergebnis. Trotzdem müssen Sie den Statuscode der Antwort prüfen, da auch hier die Daten eine Fehlerseite enthalten können.

Operationqueues

Eine Operationqueue hat die Klasse NSOperationQueue und dient zur asynchronen Ausführung von Code. Die Klassenmethode mainQueue liefert die Operationqueue des Hauptthreads. Sie können über [[NSOperationQueue alloc] init] jedoch auch eigene Queues anlegen.

Der Code in Listing 8.50 blockiert zwar die Programmausführung nicht, allerdings behebt er nicht die beiden anderen eingangs erwähnten Probleme, da Sie auch hier alle Daten auf einmal in einem Objekt bekommen. Für ihre Lösung müssen Sie mit der Klassenmethode connectionWithRequest:delegate: und einen Connection-Delegate arbeiten. Das Delegate muss das Protokoll NSURLConnectionDelegate implementieren, das Apple mit iOS 5 eingeführt hat. Allerdings enthält dessen Unterprotokoll NSURLConnectionDataDelegate die für den Datenaustausch notwendigen Methoden. Sie können indes die hier besprochenen Methoden in allen iOS-Versionen verwenden, da sie vor iOS 5 ein informelles Protokoll über die Kategorie NSObject(NSURLConnectionDelegate) beschrieben hat.

Abbildung 8.16 stellt die Aufrufreihenfolge der Delegate-Methoden grafisch dar. Wenn das Anfrageobjekt Daten für die Übertragung enthält, ruft die Connection nach dem Start zunächst die Methode connection:didSendBodyData:totalBytesWritten:totalBytesExpectedToWrite: auf. Dabei erhalten Sie in den letzten drei Parametern ganzzahlige Werte, die die Anzahl der zuletzt gesendeten Bytes, der bereits gesendeten Bytes und der insgesamt zu sendenden Bytes angeben. Sie können diese Methode also nutzen, um dem Nutzer für den Upload von Daten auf den Server eine Rückmeldung zu geben. Nach dem Upload der Daten beziehungsweise nach dem Start, wenn Sie keine Daten übermitteln, ruft die Connection die Methode connection:didReceiveResponse: auf und übergibt Ihnen das Antwortobjekt. Darauf folgen Aufrufe der Methode connection:didReceiveData:, um die Daten aus der Antwort an das Delegate zu übergeben, und ein Aufruf der Methode connectionDidFinishLoading: beendet schließlich die Anfrage.

Abbildung

Abbildung 8.16 Aufrufreihenfolge der Delegate-Methoden

Natürlich kann auch jederzeit während der Anfrage ein Fehler auftreten. In diesem Fall übergibt die Connection ihn an die Delegate-Methode connection:didFailWithError: und beendet die Anfrage.

Verbindungsabbrüche

Verbindungsabbrüche entstehen in der Regel durch Fehler in den beteiligten Hard- und Softwarekomponenten (z. B. Rechner, Router, Kabel, Serversoftware). Das gesamte System vom iOS-Gerät bis zum Server, an den die Anfrage ging, ist recht komplex, so dass Sie immer mit Verbindungsabbrüchen rechnen und diese auch entsprechend behandeln sollten. Das ist wichtig, damit die Daten in Ihrer App keinen Schaden nehmen. Ein einfaches Beispiel dafür haben Sie in Listing 8.48 und Listing 8.49: Der Code ändert das Modifikationsdatum in den User-Defaults erst, nachdem er die Daten erfolgreich gesichert hat.

Über die Methode cancel der Klasse NSURLConnection können Sie eine Verbindung auch selbst abbrechen. Ein Beispiel dafür finden Sie im Beispielprojekt SiteSchedule.

Bei dieser Form der asynchronen Kommunikation bekommt die App die Daten in Blöcken übermittelt, wobei die Größe der einzelnen Blöcke unterschiedlich ist. Wenn die Gesamtgröße der Daten nicht zu hoch ist, können Sie die Daten in einem Objekt der Klasse NSMutableData aufsammeln; bei größeren Datenmengen ist hier gegebenenfalls eine kontinuierliche Speicherung im Dateisystem notwendig.

Die Klasse DownloadViewController im Beispielprojekt SiteSchedule verwendet für die Implementierung ein Datenobjekt in der privaten Property data, und die Property dataLength speichert die Gesamtgröße der Daten. Diesen Wert benötigt sie während des Downloads, um den Anteil der geladenen Daten zu berechnen. Damit kann der Viewcontroller über die Outlet-Property progressView, die auf einen Fortschrittsbalken verweist, den Downloadprozess visualisieren. Die Delegate-Methoden lassen sich darüber folgendermaßen implementieren:

- (void)connection:(NSURLConnection *)inConnection  
didReceiveResponse:(NSURLResponse *)inResponse {
self.dataLength =
(NSUInteger)inResponse.expectedContentLength;
self.data =
[NSMutableData dataWithCapacity:self.dataLength];
self.progressView.progress = 0.0;
}

- (void)connection:(NSURLConnection *)inConnection
didReceiveData:(NSData *)inData {
[self.data appendData:inData];
self.progressView.progress =
(double) self.data.length /
(double) self.dataLength;
}

- (void)connectionDidFinishLoading:
(NSURLConnection *)inConnection {
[self performSelector:@selector(finishDownload:)
withObject:[NSInputStream
inputStreamWithData:self.data]
afterDelay:0.0];
}

Listing 8.51 Delegateimplementierung, basierend auf »NSMutableData«

Die Methode connection:didReceiveResponse: in Listing 8.51 initialisiert die Propertys für den Download. Dazu liest sie die Datengröße aus der Antwort, legt ein neues Datenobjekt an und stellt den Fortschrittsbalken auf den Anfang. Die Methode connection:didReceiveData: hängt die neuen an die bislang empfangenen Daten und aktualisiert den Fortschrittsbalken. Der Fortschrittswert liegt zwischen 0 und 1, und so können Sie diesen Wert aus dem Verhältnis von der empfangenen Datenmenge zu der Gesamtdatengröße berechnen. Beachten Sie hierbei, dass Sie für die Berechnung die beiden Werte zunächst in Fließkommazahlen umwandeln müssen, da bei einer Ganzzahldivision immer 0 herauskommt, wenn der Betrag des Nenners größer als der des Zählers ist; das kennen Sie ja schon vom Wecker.

Die Implementierung von connectionDidFinishLoading: stößt schließlich die Verarbeitung der empfangenen Daten über die Methode finishDownload: an. Sie verwendet dafür jedoch keinen direkten Methodenaufruf, sondern versendet die Nachricht indirekt. Wie Sie ja bereits wissen, kehrt die App bei performSelector:withObject:afterDelay: immer zuerst in die Runloop zurück, bevor sie die Nachricht versendet. Dadurch bekommt Cocoa Touch noch die Möglichkeit, den Zustand des Fortschrittsbalkens zu aktualisieren. Die Methode finishDownload: verarbeitet schließlich das empfangene XML-Dokument.

- (void)finishDownload:(NSInputStream *)inoutStream {
NSError *theError = [self.applicationDelegate
updateWithInputStream:inoutStream];
NSUserDefaults *theDefaults = [NSUserDefaults
standardUserDefaults];

if(theError) {
NSLog(@"updateScheduleWithStream: error = %@",
theError);
}
[theDefaults setObject:[NSDate date]
forKey:@"downloadTime"];
[theDefaults synchronize];
self.data = nil;
}

Listing 8.52 Aktualisierung der Daten und des Zeitstempels

Die Implementierung der Methode updateWithInputStream: in Listing 8.52 entspricht im Wesentlichen dem Code aus Listing 8.22; allerdings initialisiert sie den XML-Parser über die Methode initWithStream: und erzeugt einen Objektkontext für die Verarbeitung.

- (NSError *)updateWithInputStream: 
(NSInputStream *)inoutStream {
NSManagedObjectContext *theContext =
[[NSManagedObjectContext alloc] init];
NSXMLParser *theParser =
[[NSXMLParser alloc] initWithStream:inoutStream];
SiteScheduleParser *theDelegate = [[SiteScheduleParser
alloc] initWithManagedObjectContext:theContext];

theContext.persistentStoreCoordinator =
self.storeCoordinator;
theParser.shouldProcessNamespaces = YES;
theParser.shouldReportNamespacePrefixes = YES;
theParser.delegate = theDelegate;
[theParser parse];
[inoutStream close];
return theDelegate.error;
}

Listing 8.53 Verarbeitung der empfangenen XML-Daten


Rheinwerk Computing - Zum Seitenanfang

8.4.5Große Datenmengen der ÜbertragungZur nächsten ÜberschriftZur vorigen Überschrift

Die Datenmenge der SiteSchedule-App ist relativ klein; die XML-Daten sind nur ca. 3,5 kB groß, was vollkommen unproblematisch ist. Das Dokument auf dem Server enthält am Ende noch Leerzeichen, so dass die Datei 1,2 MB groß ist und Sie dadurch den Downloadprozess auf dem Fortschrittsbalken beobachten können. Allerdings sollte eine App auch diese Datenmenge ohne Speicherwarnung verarbeiten können.

Wenn Ihre App jedoch Daten im zweistelligen Megabyte-Bereich oder darüber laden soll, können Sie diese nicht in einem Datenobjekt im Hauptspeicher halten. Sie sollten sie stattdessen zunächst im Dateisystem des Geräts speichern. Dazu müssen Sie eine neue Datei öffnen und die empfangenen Daten sukzessive an sie anhängen.

Dafür erzeugen Sie ein Objekt der Klasse NSOutputStream, das das Delegate in einer Property hält. Anstatt in ein Datenobjekt schreiben Sie nun die empfangenen Daten in einen Ausgabestrom und schreiben sie somit in eine Datei:

- (void)connection:(NSURLConnection *)inConnection 
didReceiveResponse:(NSURLResponse *)inResponse {
self.dataLength =
(NSUInteger) inResponse.expectedContentLength;
self.outputStream = [NSOutputStream
outputStreamToFileAtPath:self.downloadFile
append:NO];
self.progressView.progress = 0.0;
}

- (void)connection:(NSURLConnection *)inConnection
didReceiveData:(NSData *)inData {
[self.outputStream write:inData.bytes
maxLength:inData.length];
self.progressView.progress =
(double) [self downloadFileSize] /
(double) self.dataLength;
}

- (void)connectionDidFinishLoading:
(NSURLConnection *)inConnection {
if(self.outputStream != nil) {
NSInputStream *theStream;

[self.outputStream
close];
self.outputStream = nil;
theStream = [NSInputStream
inputStreamWithFileAtPath:self.downloadFile];
[self performSelector:@selector(finishDownload:)
withObject:theStream afterDelay:0.0];
}
}

Listing 8.54 Delegateimplementierung, basierend auf einer Datei

Listing 8.54 stellt die Änderungen gegenüber Listing 8.51 hervorgehoben dar. Dabei enthält die Property downloadFile den Namen der Datei für die Ablage der heruntergeladenen Daten. Wenn Sie die Daten aufgrund ihrer Größe in einer Datei speichern, sollten Sie für die Verarbeitung auch nicht die kompletten Daten vollständig in den Hauptspeicher laden. Das könnte ja leicht zu einem Absturz führen; stattdessen sollten Sie die Daten lieber stückweise verarbeiten. Aus diesem Grund übergibt das Beispielprojekt auch über ein Objekt der Klasse NSInputStream mit den Daten an den XML-Parser.

Auf das schwächste Glied in der Kette kommt es an

Während der XML-Parser die Daten durchaus in Stücken verarbeiten kann, lädt die Klasse SiteScheduleParser die kompletten Daten in einen Objektkontext, der sie bis zur endgültigen Sicherung im Speicher hält. Das ist ja nicht weiter schlimm, da die Datenmenge im Kilobytebereich liegt. Bei wesentlich größeren Daten müssten Sie hier allerdings das Vorgehen ändern. Eine Möglichkeit wäre hier beispielsweise die Deserialisierung in einen eigenen Store, wobei die Implementierung die erzeugten Objekte schon während des Parsings speichert. Nach der erfolgreichen Verarbeitung verwendet dann die App den neuen Store und löscht den vorherigen.

Abgebrochene Anfragen wieder aufnehmen

Die Wahrscheinlichkeit eines Verbindungsabbruchs steigt mit zunehmender Dateigröße, und er ist umso ärgerlicher, je später er stattfindet. Viele Webserver unterstützen die Übertragung von Dokumentteilen, was sie im Antwortheader Accept-Ranges mitteilen. Hat dieser Header den Wert bytes (siehe Listing 8.45), dann können Sie die Ressource über Bereichsanfragen auch in Teilen laden. Falls das nicht möglich ist, enthält entweder die Antwort diesen Header nicht, oder er hat den Wert none.

Für eine Bereichsanfrage senden Sie einfach den zusätzlichen Header Range mit der Anfrage; dabei kann ein Bereich aus mehreren Intervallen bestehen. Da in der Regel jedoch die Angabe eines Intervalls ausreicht, beschränken wir uns hier auf diesen Fall. Ein Intervall in einer Bereichsangabe hat die Form Startindex "-" Endindex, wobei Sie einen der beiden Indexwerte auch weglassen können. Tabelle 8.15 enthält einige Beispiele für gültige Intervalle.

Tabelle 8.15 Beispiele für Bereichsintervalle

Intervall Beschreibung Antwort

0-100

die ersten 101 Bytes der Daten

0-100/1000

100-

alle Bytes ab dem Index 100 inklusive

100-999/1000

-100

die letzten 100 Bytes der Daten

900-999/1000

Der Wert des Range-Headers muss vor dem Intervall allerdings noch die Dateneinheit angeben. HTTP/1.1 unterstützt nur die Einheit bytes. Der Range-Header für eine Bereichsanfrage der ersten 100 Bytes hat also die Form Range: bytes=0-99.

Die Antwort auf eine Bereichsanfrage mit einem Intervall enthält den Header Content-Range mit einem Wert in dieser Form:

Einheit " " Startindex "-" Endindex "/" Gesamtgröße der Daten

Die Einheit hat auch hier immer den Wert bytes, und der Start- sowie der Endindex sind immer gesetzt. Der Größenwert entspricht der Anzahl der Bytes im Dokument. Die dritte Spalte in Tabelle 8.15 enthält die Bereichsangaben in der Antwort, die zu den Intervallen in der ersten Spalte passen, wenn das Dokument 1.000 Bytes groß ist.

Für die Wiederaufnahme eines abgebrochenen Daten-Downloads ist es sinnvoll, die Daten zunächst in eine Zwischendatei zu laden und sie erst nach dem erfolgreichen Download in die endgültige Datei zu überführen oder die Daten für die Applikation zu erstellen. Die Zwischendatei dient dabei nicht nur als Ablageort der Daten, sondern nach einem Abbruch auch als Markierung dafür. Wenn die endgültigen Daten aus der heruntergeladenen Datei bestehen, hat das Verfahren mit der Zwischendatei außerdem den Vorteil, dass Sie die Datei mit bereits geladenen, gültigen Daten erst löschen müssen, wenn Sie die neuen Daten vollständig geladen haben.

Bei der Implementierung müssen Sie noch den Sonderfall beachten, dass die Zwischendatei veraltete Daten enthält, weil der Server inzwischen aktuellere Daten anbietet. In diesem Fall ist in der Regel ein kompletter Download der Daten notwendig. Der gesamte Download erfordert einige Fallunterscheidungen, die Sie dem Programmablaufplan in Abbildung 8.17 entnehmen können.

Abbildung

Abbildung 8.17 Datendownload mit Wiederaufnahme nach einem Abbruch

Eine Implementierung dieses Verfahrens kann auf Listing 8.54 aufbauen, wobei Sie allerdings den Start der Anfrage anpassen müssen. Das letzte Modifikationsdatum der Serverdaten verwaltet der Code analog zu Listing 8.48 unter dem Schlüssel updateTime in den User-Defaults. Für die Bereichsanfrage muss der Code außerdem die Dateigröße bestimmen und gegebenenfalls die Zwischendatei löschen können. Dazu implementieren Sie zwei entsprechende Hilfsmethoden auf Basis der Klasse NSFileManager, wobei die Methode downloadFile den Namen der Zwischendatei liefert:

- (void)deleteDownloadFile {
NSFileManager *theManager =
[NSFileManager defaultManager];
NSError *theError = nil;

[theManager removeItemAtPath:self.downloadFile
error:&theError];
if(theError) {
NSLog(@"deleteDownloadFile: %@", theError);
}
}

- (NSUInteger)downloadFileSize {
NSFileManager *theManager =
[NSFileManager defaultManager];

return [theManager
attributesOfItemAtPath:self.downloadFile
error:NULL].fileSize;
}

Listing 8.55 Hilfsmethoden für den Dateidownload

Mit Hilfe der Dateigröße und der Methode downloadRequired aus Listing 8.48 können Sie nun das Anfrageobjekt erstellen. Die Methode startDownload in Listing 8.56 braucht nur noch zu entscheiden, ob sie die kompletten Daten oder nur einen Bereich anfordern soll. Deswegen muss die Methode lediglich Bedingungen aus dem grau hinterlegten Teil in Abbildung 8.17 prüfen. Dabei ist die Zwischendatei vorhanden, wenn sie mehr als 0 Bytes enthält. Sie müssen den Range-Header also nur setzen, wenn die Datei nicht leer und kein Komplettdownload notwendig ist. Das entspricht genau der Bedingung in der if-Abfrage in Listing 8.56.

- (void)startDownload {
NSURL *theURL = self.downloadURL;
NSMutableURLRequest *theRequest =
[NSMutableURLRequest requestWithURL:theURL
cachePolicy:
NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:5.0];
NSUInteger theSize = self.downloadFileSize;

if(theSize > 0 && ![self downloadRequired]) {
NSString *theValue = [NSString
stringWithFormat:@"bytes=%u-", theSize];

[theRequest setValue:theValue
forHTTPHeaderField:@"Range"];
}
self.connection = [NSURLConnection
connectionWithRequest:theRequest
delegate:self];
}

Listing 8.56 Bereichsanfrage erzeugen

Bereichsanfragen und Caching

Das URL-Loading-System legt auch Bereichsanfragen im Cache ab. Wenn Sie eine erneute Anfrage mit einem anderen Bereich an die gleiche URL stellen, erhalten Sie unter Umständen die Daten aus dem Cache. Wenn Sie beispielsweise zuerst die ersten 1.000 Bytes abfragen und danach die restlichen Daten, liefert Ihnen die zweite Anfrage trotzdem die ersten 1.000 Bytes.

Dieses Problem können Sie auf zwei Wegen umgehen: Für die einfachere Lösung schalten Sie das Caching aus. Alternativ lesen Sie den Cache-Eintrag aus und überprüfen, ob er zu den angefragten Daten passt, und löschen ihn bei ungeeigneten Daten aus dem Cache. Das Beispielprojekt SiteSchedule speichert ja bereits die ausgewerteten Daten in Core Data, weswegen das Cachen von Antworten hier nicht notwendig ist.

Beim Empfang der Antwort vom Server müssen Sie unterscheiden, ob der Server die kompletten Daten gesendet hat oder nur einen Teilbereich. Dazu werten Sie den Content-Range-Header über die Methode contentRange der Kategorie NSDictionary(HTTPRequest) aus. Sie liefert die Daten in der C-Struktur HTTPContentRange zurück. In der Headerdatei HTTPContentRange.h finden Sie dazu die folgenden Deklarationen:

typedef struct _HTTPContentRange {
NSRange range;
NSUInteger size;
} HTTPContentRange;

HTTPContentRange HTTPContentRangeMake(
NSRange inRange, NSUInteger inSize);
HTTPContentRange HTTPContentRangeMakeFull(
NSUInteger inSize);
HTTPContentRange HTTPContentRangeFromString(
NSString *inString);
NSString *NSStringFromHTTPContentRange(
HTTPContentRange inRange);

Listing 8.57 Deklarationen für »HTTPContentRange«

Die Struktur enthält das Intervall als Wert vom Typ NSRange und die Gesamtgröße der Daten im Attribut size. Sie können die Funktion HTTPContentRangeFromString für die Implementierung der contentRange-Methode nutzen. Sie gibt entweder den entsprechenden Wert aus dem Header zurück oder den Bereich, der den kompletten Daten entspricht. Diese Bereiche können Sie über die Funktion HTTPContentRangeMakeFull erzeugen.

- (HTTPContentRange)contentRange {
NSString *theRange =
[self valueForKey:@"Content-Range"];

if(theRange == nil) {
return HTTPContentRangeMakeFull(self.contentLength);
}
else {
return HTTPContentRangeFromString(theRange);
}
}

Listing 8.58 Auslesen des Content-Ranges aus dem Header

Sie müssen nur relativ kleine Änderungen an der Implementierung von connection:didReceiveResponse: aus Listing 8.54 vornehmen. Wenn der Bereich nicht bei 0 beginnt, müssen Sie die Daten an den Stream anhängen:

- (void)connection:(NSURLConnection *)inConnection 
didReceiveResponse:(NSURLResponse *)inResponse {
NSDictionary *theFields =
[(id) inResponse allHeaderFields];
HTTPContentRange theRange = theFields.contentRange;
NSUserDefaults *theDefaults =
[NSUserDefaults standardUserDefaults];

self.contentRange
= theRange;
self.outputStream = [NSOutputStream
outputStreamToFileAtPath:self.downloadFile
append:theRange.range.location > 0];
self.progressView.progress = 0.0;
[theDefaults setValue:theFields.lastModified
forKey:@"updateTime"];
}

Listing 8.59 Anhängen der Daten bei Bereichsanfragen

Die Methode aktualisiert außerdem das Änderungsdatum der Daten in den User-Defaults, um einen Vergleich der lokalen Modifikationszeit mit der Zeit auf dem Server nach einem Verbindungsabbruch zu ermöglichen. Der DownloadViewController im Beispielprojekt SiteSchedule kann abgebrochene Anfragen fortführen, wenn Sie am Anfang der Implementierungsdatei das entsprechende Makro auf 1 setzen:

#define RESUMABLE_DOWNLOAD 1.

Rheinwerk Computing - Zum Seitenanfang

8.4.6PasswortabfragenZur nächsten ÜberschriftZur vorigen Überschrift

In Abschnitt 8.4.1, »Synchrone Kommunikation«, haben Sie bereits eine Möglichkeit kennengelernt, HTTP-Login-Daten an den Server zu übermitteln. Das URL-Loading-System bietet auch eine eigene Möglichkeit, diese Daten über das Delegate zu ermitteln und zu verwalten. Das Delegate für die Verwaltung der Login-Daten zu verwenden, hat gegenüber den Daten in der URL einige Vorteile: Es besitzt eine Passwortverwaltung, und es kann die Login-Daten für die Applikationslaufzeit oder dauerhaft speichern. Bei der dauerhaften Speicherung legt es die Login-Daten sicher im Schlüsselbund ab. Außerdem funktioniert es mit allen URL-Protokollen, sogar wenn Sie noch weitere Protokolle (z. B. SFTP, RTSP) adaptieren.

Die zentrale Delegate-Methode für die Abfrage der Login-Daten ist ab iOS 5 connection:willSendRequestForAuthenticationChallenge:. Sie ruft die Verbindung immer dann auf, wenn ein Server Login-Daten anfordert, die das URL-Loading-System noch nicht kennt. Im zweiten Parameter erhalten Sie dabei ein Objekt der Klasse NSURLAuthenticationChallenge, das die Autorisierungsanfrage des Servers kapselt. Für die Verarbeitung dieser Anfrage müssen Sie dem Absender der Anfrage, den Sie über die Property sender erhalten, eine von drei möglichen Nachrichten schicken:

  • Sie können dem Absender über die Methode useCredential:forAuthenticationChallenge: neue Zugangsdaten mitteilen.
  • Über continueWithoutCredentialForAuthenticationChallenge: können Sie die Anfrageverarbeitung ohne Zugangsdaten fortsetzen.
  • Alternativ brechen Sie die Anfrageverarbeitung über cancelAuthenticationChallenge: ab.

Sie haben nicht das Recht, zu schweigen

Sie sollten eine Autorisierungsanfrage immer durch eine der drei genannten Methoden beantworten, weil erst dadurch das System die damit verbundene URL-Anfrage abschließen kann. Das muss allerdings nicht unbedingt in der Delegate-Methode, sondern kann auch später geschehen, wie Sie gleich sehen werden.

Sie können das Beispielprojekt SiteSchedule auf die Verwendung einer geschützten URL umstellen, indem Sie am Anfang der Datei DownloadViewController.m die Zuweisung für die Konstante kDownloadURL folgendermaßen ändern:

static NSString * const kDownloadURL = 
kProtectedDownloadURL;

Die Delegate-Methode öffnet einen Alertview, der den Nutzer nach dem Login-Namen und Passwort für die Verbindung fragt. Seit iOS 5 unterstützt die Klasse UIAlertView auch die Anzeige von bis zu zwei Textfeldern. Sie können die Anzeige durch Änderung des Property-Wertes für alertViewStyle erreichen. Außerdem speichert die Delegate-Methode die Autorisierungsanfrage in der Property challenge ab.

Die App soll den Nutzer indes höchstens dreimal pro Anfrage nach den Zugangsdaten befragen und andernfalls die Anfrage abbrechen. Die Anzahl der fehlgeschlagenen Anfragen können Sie dabei über die Methode previousFailureCount ermitteln.

- (void)sendAuthenticationChallenge:  
(NSURLAuthenticationChallenge *)inChallenge
forConnection:(NSURLConnection *)inConnection {
if([inChallenge previousFailureCount] < 3) {
NSString *theMethod = [inChallenge.protectionSpace
authenticationMethod];
UIAlertView *theAlert = [[UIAlertView alloc]
initWithTitle:theMethod
message:NSLocalizedString(
@"Please enter your credentials.", @"")
delegate:self cancelButtonTitle:
NSLocalizedString(@"Cancel", @"")
otherButtonTitles:
NSLocalizedString(@"Login", @""), nil];

self.challenge = inChallenge;
theAlert.alertViewStyle =
UIAlertViewStyleLoginAndPasswordInput;
[theAlert show];
}
else {
[inChallenge.sender cancelAuthenticationChallenge:
inChallenge];
}
}

- (void)connection:(NSURLConnection *)inConnection
willSendRequestForAuthenticationChallenge:
(NSURLAuthenticationChallenge *)inChallenge {
[self sendAuthenticationChallenge:inChallenge
forConnection:inConnection];
}

- (void)connection:(NSURLConnection *)inConnection
didReceiveAuthenticationChallenge:
(NSURLAuthenticationChallenge *)inChallenge {
[self sendAuthenticationChallenge:inChallenge
forConnection:inConnection];
}

Listing 8.60 Verarbeitung einer Autorisierungsanfrage

Der Viewcontroller implementiert beide Delegate-Methoden über die Hilfsmethode sendAuthenticationChallenge:forConnection:, und die Implementierung der drei Methoden sehen Sie in Listing 8.60. Die Hilfsmethode beantwortet dabei die Autorisierungsanfrage nicht, sondern zeigt stattdessen einen Alertview an. Die Beantwortung der Anfrage findet dann in der Delegate-Methode des Alertviews statt, da ja erst zu deren Ausführungszeitpunkt die Login-Daten bekannt sind. Diese Methode erzeugt ein Objekt der Klasse NSURLCredential, das die Login-Daten aufnimmt. Bei dessen Erzeugung müssen Sie zudem angeben, wie lange die Autorisierungsdaten gültig sind. Hier stellt Ihnen das Foundation-Framework drei Werte zur Verfügung:

Tabelle 8.16 Vom System unterstützte Gültigkeitsdauern für Login-Daten

Konstante Beschreibung

NSURLCredentialPersistenceNone

Das System verwirft die Anmeldedaten nach der Anfrage und fragt sie bei der nächsten Anfrage erneut über das Delegate ab.

NSURLCredentialPersistenceForSession

Das System merkt sich die Anmeldedaten bis zum Sitzungs- beziehungsweise Programmende und fragt sie nicht erneut über das Delegate ab.

NSURLCredentialPersistencePermanent

Das System legt die Anmeldedaten im Schlüsselbund ab und behält sie somit auch über einen Neustart des Programms hinweg.

Die Beispielapplikation speichert die Daten nur so lange, wie sie läuft. Das ist ein Kompromiss zwischen Bequemlichkeit und Sicherheit, da der Nutzer damit die Anmeldedaten nur einmal pro Programmlauf eingeben muss. Andererseits kann er die Applikation stoppen und somit den Datenzugriff explizit sperren. Bei Applikationen mit hohen Sicherheitsanforderungen sollten Sie allerdings generell auf die Speicherung verzichten.

Die Anmeldedaten gelten natürlich immer nur für einen bestimmten Bereich. Wenn eine App beispielsweise Anfragen an URLs mit unterschiedlichen Zugangsdaten stellt, fragt sie auch für jede URL die Zugangsdaten ab. Die Klasse NSURLProtectionSpace stellt einen Bereich mit eigenen Zugangsdaten dar, der die relevanten Bestandteile der URL und die Authentifizierungsmethode enthält.

- (void)alertView:(UIAlertView *)inAlertView 
willDismissWithButtonIndex:(NSInteger)inButtonIndex {
if(inButtonIndex == 1) {
NSString *theLogin =
[inAlertView textFieldAtIndex:0].text;
NSString *thePassword =
[inAlertView textFieldAtIndex:1].text;
NSURLCredential *theCredential =
[NSURLCredential credentialWithUser:theLogin
password:thePassword persistence:
NSURLCredentialPersistenceForSession];

[self.challenge.sender useCredential:theCredential
forAuthenticationChallenge:self.challenge];
}
else {
[self cancelDownload];
}
self.challenge = nil;
}

Listing 8.61 Beantwortung der Autorisierungsanfrage

Dieser Kasten ist keine Sicherheitslücke ...

... obwohl er die Anmeldedaten für das Testdokument im Klartext enthält: Der Login-Name ist »homer«, und das Passwort lautet »dooooh!«.

Bei Applikationen, die ihre Autorisierungsdaten speichern, ist es natürlich auch sinnvoll, die Einträge aus dem Schlüsselbund wieder löschen zu können. Dadurch geben Sie beispielsweise dem Nutzer die Möglichkeit, den Zugriff explizit zu sperren.

Über das Singleton NSURLCredentialStorage haben Sie Zugriff auf die Anmeldedaten der App im Schlüsselbund. Darin sind die Anmeldedaten in NSURLCredential-Objekten abgelegt. Sie erhalten alle Objekte des Singletons über die Methode allCredentials, die ein Dictionary zurückgibt, dessen Schlüssel NSProtectionSpace-Objekte sind und deren Werte wiederum Dictionarys sind. In diesen Dictionarys sind die Anmeldedaten nach den Nutzernamen abgelegt. Da Sie immer nur ein Objekt über die Methode removeCredential:forProtectionSpace: aus dem Schlüsselbund löschen können, brauchen Sie also zwei Schleifen, um alle Objekte zu löschen. Das Beispielprojekt enthält die Methode removeAllCredentials in der Kategorie NSURLCredentialStorage(Extensions), mit der Sie alle Anmeldedaten löschen können.

- (void)removeAllCredentials {
for(NSURLProtectionSpace *theSpace in
self.allCredentials) {
NSDictionary *theCredentials =
[self credentialsForProtectionSpace:theSpace];

for(NSURLCredential *theCredential in
theCredentials.allValues) {
[self removeCredential:theCredential
forProtectionSpace:theSpace];
}
}
}

Listing 8.62 Löschen aller Anmeldedaten


Rheinwerk Computing - Zum Seitenanfang

8.4.7Sicher kommunizieren mit TSL (SSL)Zur nächsten ÜberschriftZur vorigen Überschrift

Wie bereits erwähnt, besteht bei der Verwendung unverschlüsselter Netzwerkkommunikation die Gefahr, dass ein Angreifer die Übertragung mitliest und sich darüber in den Besitz sensibler Daten bringt. Darüber hinaus kann ein gewiefter Angreifer den Netzwerkverkehr so umlenken, dass Ihre App gar nicht mit dem vorgesehenen Server spricht, sondern mit einem ganz anderen, einem, den der Angreifer unter seiner Kontrolle hat.

Sie müssen bei der Übertragung von Daten daher stets bedenken, ob die Vertraulichkeit der Daten und die Identität des Kommunikationspartners relevant sind. Praktischerweise gibt es mit TLS (Transport Layer Security) einen Standard, der beide Aspekte abdeckt. Eine über TLS (früher SSL für Secure Socket Layer) abgesicherte Netzwerkverbindung stellt, bei korrekter Anwendung, durch Verschlüsselung die Vertraulichkeit der Verbindung sicher und ermöglicht überdies über die Verwendung von Zertifikaten die wechselseitige Überprüfung der Identität der beteiligten Kommunikationspartner.

TLS ist ein Protokoll der Transportschicht im TCP/IP-Modell und daher ideal dafür geeignet, (nahezu) beliebige Protokolle der Anwendungsschicht abzusichern. Geläufigster Anwendungsfall für TLS ist die verschlüsselte Verbindung im Browser zu Servern. Klassische Anwendungsfälle für TLS sind Online-Banking, E-Commerce und grundsätzlich alle Dienste, bei denen personenbezogene oder sensible Daten übertragen werden.

Weniger geläufig, wenngleich nicht weniger sinnvoll, ist die Absicherung von Protokollen für den Austausch von Emails (POP3S, IMAPS, SMTPS) oder die Verwendung von TLS zur Absicherung von FTP-Verbindungen (FTPS).

TLS verbindet die Sicherheit und Performanz symmetrischer Verschlüsselung mit der Flexibilität der asymmetrischen Verschlüsselung. Symmetrische Verschlüsselung ist dadurch gekennzeichnet, dass Sender und Empfänger über denselben Schlüssel verfügen müssen. Mit diesem Schlüssel verschlüsselt der Sender die Daten, und der Empfänger entschlüsselt sie. Diese klassische Vorgehensweise der Kryptografie ist im Internet denkbar ungünstig. Wenn sich ein Bankkunde verschlüsselt mit dem Server der Bank verbinden möchte – woher soll er dann den Schlüssel kennen, mit dem der Server die Daten verschlüsselt?

Die Lösung komm in Gestalt der asymmetrischen Kryptografie. Diese zeichnet sich dadurch aus, dass das Verschlüsseln von Daten einen andereren Schlüssel benötigt als das Entschlüsseln. Das Verschlüsseln ist unkritisch und erfolgt mit dem öffentlichen Schlüssel des Empfängers. Jemand, der diesen Schlüssel kennt, kann damit Daten an den Empfänger verschlüsseln, nicht aber entschlüsseln. Dies stellt die Verwendung einer sogenannten Einwegfunktion [Anm.: https://de.wikipedia.org/wiki/Asymmetrische_Verschlüsselung] sicher. Der Empfänger kann die empfangenen Daten mit seinem privaten Schlüssel entschlüsseln. Der private Schlüssel ist daher geheim und nur dem Empfänger bekannt. Von dieser funktionalen Ungleichheit zwischen öffentlichem und privatem Schlüssel hat die asymmetrische Kryptografie ihren Namen.

Nun gibt es kein Licht ohne Schatten, und auch die asymmetrische Kryptografie ist kein Allheilmittel. Der Nachteil, den Sie zusammen mit der Flexibilität erhalten, ist ein erhöhter Ressourcenverbrauch, da die kryptographischen Operationen wesentlich rechenintensiver sind als die der symmetrischen Verschlüsselung. Aus diesem Grund verwendet TLS nur für den Verbindungsaufbau und das Aushandeln eines gemeinsamen Schlüssels zwischen Client und Server asymmetrische Verschlüsselung. Haben der Client und der Server einen Schlüssel für die weitere Kommunikation ausgetauscht, kommt damit die symmetrische Verschlüsselung zum Einsatz und schont somit wertvolle Ressourcen.

Neben der Verschlüsselung bietet TLS die Möglichkeit, die Identität des Kommunikationspartners über X.509-Zertifikate [Anm.: https://de.wikipedia.org/wiki/X.509] zu prüfen. Dazu liefert der Server beim Verbindungsaufbau ein Zertifikat an den Client, mit dem der Client die Identität des Servers überprüfen kann. Optional kann der Server umgekehrt ein Zertifikat beim Client anfordern, über das er den Client identifizieren kann. Das Ausliefern des Serverzertifikates ist Standard, das des Client-Zertifikates kommt nur in Einzelfällen zum Einsatz.

Betrachten können Sie die Identifizierung eines Servers mittels eines X.509-Zertifikates im Safari-Browser. Öffnen Sie das Developer-Portal von Apple (http://developer.apple.com), und beobachten Sie dabei die Adresszeile von Safari. Der Server schaltet automatisch von unverschlüsselter Kommunikation (HTTP) auf die verschlüsselte Kommunikation mit TLS um (HTTPS) und übermittelt sein Serverzertifikat an Safari. Safari zeigt die Verschlüsselung durch das kleine Schloss in der Adresszeile an. Das Betriebssystem übernimmt die Absicherung der Verbindung und prüft gleichzeitig die Identität des Servers. Als Programmierer müssen Sie sich nicht mit Verschlüsselung oder dem Zertifikat abgeben.

Abbildung

Abbildung 8.18 Safari zeigt eine HTTPS-Verbindung.

Durch Klick auf das grün markierte Apple Inc. in der Adresszeile können Sie das Serverzertifikat betrachten.

Safari zeigt zunächst eine allgemeine Information zum Zertifikat an. Darin ist vermerkt, dass der Server die Daten verschlüsselt mit HTTPS überträgt und dass VeriSign Inc. das Zertifikat von Apple Inc. in Cupertino für den Server developer.apple.com bestätigt hat (siehe Abbildung 8.19). Durch Klick auf Zertifikat einblenden sehen Sie sich weitere Informationen zum Zertifikat an (siehe Abbildung 8.20).

Abbildung

Abbildung 8.19 Informationen zum Zertifikat

Abbildung

Abbildung 8.20 Das Zertifikat im Detail

In dieser Detailansicht sehen Sie zwei wichtige Protagonisten bei der Arbeit mit X.509-Zertifikaten: den Server, für den das Zertifikat ausgestellt ist (developer.apple.com), und eine Stelle, die entweder die Glaubwürdigkeit des Zertifikates bestätigt oder das Zertifikat selbst ausgestellt hat (VeriSign). Diese Stelle nennt man Certificate Authority. (CA) Damit Safari oder andere Programme auf dem Mac oder dem iPhone den Aussagen einer CA Glauben schenken, muss das Stammzertifikat einer CA im Schlüsselbund von OS X oder iOS gespeichert sein. Ist es das nicht, meldet Safari einen Fehler.

Safari meldet auch dann einen Fehler, wenn das Zertifikat nicht zum Hostnamen, also dem DNS-Namen des kontaktierten Servers, passt. Die beiden Überprüfungen, ob das Zertifikat zum Server passt und ob eine vertrauenswürdigen Stelle das Zertifikat ausgestellt oder beglaubigt hat, machen die Sicherheit von TLS über die eigentliche Verschlüsselung hinaus aus.

Abbildung

Abbildung 8.21 Ein Zertifikat einer nicht vertrauenswürdigen CA

Abbildung

Abbildung 8.22 Der aufgerufene Hostname passt nicht zum Namen im Zertifikat.

Der Nachteil bei der Verwendung offizieller CAs ist, dass diese für das Ausstellen oder Beglaubigen von Zertifikaten Geld verlangen. Es gibt zwar Anbieter, bei denen man sich ein kostenloses Zertifikat ausstellen lassen kann (z. B. www.startssl.com), der Haken dabei ist aber ... nun ja ... derselbe wie immer, wenn etwas kostenlos ist, das woanders Geld kostet. Wer weiß, womit StartSSL Geld verdient [Anm.: https://de.wikipedia.org/wiki/Aluminiumhut] ?

Die Glaubwürdigkeit von CAs

Nicht erst seit den Veröffentlichungen um PRISM, Tempora und die ganzen anderen Schnüffelprogramme diverser Geheimdienste ist bekannt, dass die CA-Infrastruktur für TLS-Zertifikate sehr labil ist. In der Vergangenheit sind verschiedene Fälle bekannt geworden, in denen Angreifer die Infrastrukturen verschiedener CA-Anbieter kompromittierten und sich über diesen Weg gefälschte, aber natürlich gültige Zertifikate für bekannte Websites und Domains wie microsoft.com oder google.com verschafften. Da Browser und Betriebssysteme jedem von einer glaubwürdigen CA ausgestellten Zertifikat vertraut, solange es sich hinter dem richtigen Hostnamen verbirgt, merkt der Benutzer keinen Unterschied. Das Gleiche gilt natürlich für Eingriffe durch Geheimdienste. Wenn Sie nicht möchten, dass jemand die Netzwerkkommunikation Ihrer App mitliest, vertrauen Sie besser nicht blind jeder CA.

Was helfen diese Erkenntnisse jetzt dem App-Programmierer? Für den »normalen« Anwendungsfall, also die Verschlüsselung der Netzwerkübertragung und die Überprüfung des Servers, ist TLS unter Verwendung offizieller Zertifikate eine gute Wahl. Das Betriebssystem übernimmt dabei den gesamten Verschlüsselungs- und Identifizierungsaufwand, und als Programmierer können Sie sich auf Ihre App konzentrieren.

Bei erhöhtem Schutzbedarf reicht das aber nicht. Zum einen besteht die beschriebene Gefahr, dass über eine kompromittierte CA gefälschte Zertifikate in Umlauf gekommen sind, zum anderen können Dienste mit Zugriff auf CA-Anbieter beliebig in die Kommunikation hineinschauen. Wenn Sie wissen möchten, welchen CAs Ihr OS X vertraut, öffnen Sie den Schlüsselbund und wählen den Schlüsselbund System-Roots aus. Allen Stellen, deren Root-Zertifikat Sie dann sehen, vertraut OS X (siehe Abbildung 8.23). Dazu gehören auch Institutionen wie das CNNIC, die chinesische Vergabestelle der Toplevel-Domain .cn. Im China-Urlaub sollten Sie sich also nicht auf die Sicherheit von TLS verlassen. Als versierter Aluminiumhut-Träger kann man sich aber genauso wenig auf die TLS-Sicherheit verlassen, wenn man unterstellt, dass beliebige Dienste westliche CAs wie die Deutsche Telekom oder eben auch VeriSign kontrollieren, unterwandern oder steuern.

Wenn Sie auf Nummer sicher gehen, aber trotzdem den Komfort von TLS nicht missen möchten, können Sie über die Prüfung des Betriebssystems hinaus in der App selbst das Zertifikat vom Server überprüfen. Ein X.509-Zertifikat [Anm.: https://de.wikipedia.org/wiki/X.509] enthält verschiedene Merkmale zu dem Server, für das es gültig ist und anhand deren Sie es überprüfen können, und iOS stellt ein paar Methoden zur Verfügung, um auf diese Informationen zugreifen zu können.

Abbildung

Abbildung 8.23 Die Root-Zertifikate, denen OS X vertraut, im Schlüsselbund

Sie können das X.509-Zertifikat eines beliebigen Servers im Internet unter OS X mit dem Kommandozeilentool openssl und dem Befehl s_client aus der OpenSSL-Bibliothek betrachten. Öffnen Sie dazu das Terminal, und geben Sie den folgenden Befehl ein:

openssl s_client -connect www.postbank.de:443

Die Angabe 443 am Ende weist s_client an, die Verbindung zum Server auf dem TCP-Port 443 zu öffnen, dem Standard-Port für TLS-Verbindungen. Bei funktionierender Internetverbindung zeigt s_client anschließend das Zertifikat des Servers an:

# openssl s_client -connect www.postbank.de:443
CONNECTED(00000003)
depth=2 C = US, O = "VeriSign, Inc.", OU = VeriSign Trust Network, OU = "(c) 2006 VeriSign, Inc. – For authorized use only", CN = VeriSign Class 3 Public Primary Certification Authority – G5
verify error:num=20:unable to get local issuer certificate
verify return:0
---
Certificate chain
0 s:/1.3.6.1.4.1.311.60.2.1.3=DE/1.3.6.1.4.1.311.60.2.1.1=Bonn/businessCategory=Private Organization/serialNumber=HRB6793/C=DE/postalCode=53113/ST=NRW/L=Bonn/street=Friedrich Ebert Allee 114 126/O=Deutsche Postbank AG/OU=Postbank
Systems AG/CN=postbank.de
i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)06/CN=VeriSign Class 3 Extended Validation SSL SGC CA
1 s:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)06/CN=VeriSign Class 3 Extended Validation SSL SGC CA
i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2006 VeriSign, Inc. – For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority – G5
2 s:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=(c) 2006 VeriSign, Inc. – For authorized use only/CN=VeriSign Class 3 Public Primary Certification Authority – G5
i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
---
Server certificate
-----BEGIN CERTIFICATE-----
MIIGJTCCBQ2gAwIBAgIQHkuGKzx/yk+OqmXtm8NuSDANBgkqhkiG9w0BAQUFADCBvjELMAkGA1UEBh
MCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJpU2lnbiBUcnVzdCBOZXR3
b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2UgYXQgaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYS
AoYykwNjE4MDYGA1UEAxMvVmVyaVNpZ24gQ2xhc3MgMyBFeHRlbmRlZCBWYWxpZGF0aW9uIFNTTCBT
R0MgQ0EwHhcNMTIxMDE3MDAwMDAwWhcNMTQxMDE4MjM1OTU5WjCCARIxEzARBgsrBgEEAYI3PAIBAx
MCREUxFTATBgsrBgEEAYI3PAIBARQEQm9ubjEdMBsGA1UEDxMUUHJpdmF0ZSBPcmdhbml6YXRpb24x
EDAOBgNVBAUTB0hSQjY3OTMxCzAJBgNVBAYTAkRFMQ4wDAYDVQQRFAU1MzExMzEMMAoGA1UECBMDTl
JXMQ0wCwYDVQQHFARCb25uMSYwJAYDVQQJFB1GcmllZHJpY2ggRWJlcnQgQWxsZWUgMTE0IDEyNjEd
MBsGA1UEChQURGV1dHNjaGUgUG9zdGJhbmsgQUcxHDAaBgNVBAsUE1Bvc3RiYW5rIFN5c3RlbXMgQU
cxFDASBgNVBAMUC3Bvc3RiYW5rLmRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Z5l
bbPw9Lp86VXY8H1a9A/WiilojTyS8a2MJ5CgZEHYgTxy0oAT1jYmVCaS0sjH5ekYgtyA3Z1CkqRlNp
xlNDfLtlX3gSIx1xELDlnN2CxwORfNWlz/+z65x1dM4ipPVWCnlPxHdlq6qpcJezEoHIKM9CXBw4Mh
iAtl8/njYpVf6lNZz2QBQjDC8PAeU07eBCLwmDE2wvqlM/ifnxHOn1phvtV6drMmxEe3/LoWLG6ybq
3SlG2Y3n5pOkPbwKRV0bT67tzIrDjiDmI9BRgwUvc2v4HyW53jMBr+Fpti9RVDBG7KQQ2SuaOhdBCQ
F8T+PsBkiEG40p2JvRNVixCeKwIDAQABo4IBxjCCAcIwPQYDVR0RBDYwNIIPd3d3LnBvc3RiYW5rLm
RlghRyZWxhdW5jaC5wb3N0YmFuay5kZYILcG9zdGJhbmsuZGUwCQYDVR0TBAIwADAdBgNVHQ4EFgQU
Snc1hazsN2Fo6Y2U7fR6R+QveiUwDgYDVR0PAQH/BAQDAgWgMD4GA1UdHwQ3MDUwM6AxoC+GLWh0dH
A6Ly9FVkludGwtY3JsLnZlcmlzaWduLmNvbS9FVkludGwyMDA2LmNybDBEBgNVHSAEPTA7MDkGC2CG
SAGG+EUBBxcGMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LnZlcmlzaWduLmNvbS9jcHMwKAYDVR
0lBCEwHwYIKwYBBQUHAwEGCCsGAQUFBwMCBglghkgBhvhCBAEwHwYDVR0jBBgwFoAUTkPIHXbvN1N6
T/JYb5TzOOLVvd8wdgYIKwYBBQUHAQEEajBoMCsGCCsGAQUFBzABhh9odHRwOi8vRVZJbnRsLW9jc3
AudmVyaXNpZ24uY29tMDkGCCsGAQUFBzAChi1odHRwOi8vRVZJbnRsLWFpYS52ZXJpc2lnbi5jb20v
RVZJbnRsMjAwNi5jZXIwDQYJKoZIhvcNAQEFBQADggEBABMUHHvPPx7teNDBYYs//hV568TcHlvWc1
wDBt8xkeIBlGXy7mBwOoxxWdVLwOWmFFBHlcvpTGJKNNV7pUq7yqiOJjXmi0+UuaVitm+jmSi7718n
4Xy+0kErZVq0X3HCk4yJ6+EuxE2kdui2jqjM0qcQYk9a/JZKLi8rvHTCw9dtAIkFhy06wQCq0KaB0S
p0L2/JRYSJq6y1oAfQ6f/TKbxqIoaRR0HhoB7sILxQGobAGBt3DKOLz87FFBY/MLMlNr6QCjC2J/wg
m6pH6tJfBUtUA+w7KvcKQ4BXovqb1kNagmpR+4I7zUWXX9tRD6UBbPiwEMFd8DTsODj8oCLEgM=
-----END CERTIFICATE-----
subject=/1.3.6.1.4.1.311.60.2.1.3=DE/1.3.6.1.4.1.311.60.2.1.1=Bonn/businessCategory=Private Organization/serialNumber=HRB6793/C=DE/postalCode=53113/ST=NRW/L=Bonn/street=Friedrich Ebert Allee 114 126/O=Deutsche Postbank AG/OU=Postbank Systems AG/CN=postbank.de
issuer=/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at
https://www.verisign.com/rpa (c)06/CN=VeriSign Class 3 Extended Validation
SSL SGC CA
---
No client certificate CA names sent
---
SSL handshake has read 4573 bytes and written 636 bytes
---
New, TLSv1/SSLv3, Cipher is RC4-SHA
Server public key is 2048 bit
Secure Renegotiation IS supported
Compression: NONE
Expansion: NONE
SSL-Session:
Protocol : TLSv1.2
Cipher : RC4-SHA
Session-ID: 2FBF24AA83D5726AA24B1997B8389B69399351BC8C60A79367E69BE8C13816D5
Session-ID-ctx:
Master-Key: 798A601DBBEC0DC1FF9B63EF1782181446CBEA6820919A13A68C2592D5BE70071
B01FD30CE16B6821E5B4CBE22AA32CF
Key-Arg : None
PSK identity: None
PSK identity hint: None
SRP username: None
Start Time: 1381737061
Timeout : 300 (sec)
Verify return code: 20 (unable to get local issuer certificate)
---
read:errno=0

Wenn Sie die URL www.postbank.de in Safari öffnen und sich, wie beim Beispiel oben, die Details zum Zertifikat in Safari ansehen, erhalten Sie dieselben Angaben. Der Vorteil der Kommandozeile ist aber, dass Sie dort die für das Überprüfen eines Zertifikates gewünschten Werte zielgenau ausschneiden können. Das sinnvollste Datum eines Zertifikates, anhand dessen sich das Zertifikat als das gewünschte identifizieren lässt, ist der sogenannte Fingerprint. Der Fingerprint ist eine kryptographische Prüfsumme des Zertifikates, erstellt mit einer Hashfunktion. Hashfunktionen haben Sie im letzten Kapitel bereits kennengelernt. Wenn Sie den Hashwert eines Zertifikates kennen, können Sie zusätzlich zur Überprüfung des Zertifikates durch das Betriebssystem den Fingerprint vergleichen, um sicherzugehen, dass es sich bei dem vom Server präsentierten Zertifikat auch um das von Ihnen erwartete handelt. Dazu müssen Sie den Hashwert (den Fingerprint) des Zertifikates in Ihrer App hinterlegen, den Hashwert des vom Server präsentierten Zertifikates auslesen und beide Werte miteinander vergleichen. Unabhängig davon, was iOS zur Gültigkeit des Zertifikates sagt, können Sie auf diese Weise sicherstellen, dass der Server genau das Zertifikat besitzt, das Sie erwarten. Oder eben auch nicht.

Den Hashwert des Zertifikates finden Sie sowohl im GUI über Safari als auch über s_client. Letzteres hat den Charme, dass Sie die Ausgabe so formatieren können, dass Sie den Wert 1 : 1 in den Code der App übernehmen können. The Unix way of working ...

openssl s_client -connect www.postbank.de:443 2>&1 |
openssl x509 -noout -fingerprint| sed 's/\://g'

Die Ausgabe ist der Hashwert des Zertifikates:

SHA1 Fingerprint=68F3DC2E68B6F5BFEDB467ED055E949C369F1089

Um diese Prüfung zu implementieren, erzeugen Sie ein neues Xcode-Projekt (Single View Application) oder verwenden ein bereits vorhandenes Projekt, das Daten über HTTPS mit einem Server austauscht.

Projektinformation

Den Quellcode des folgenden Beispiels finden Sie auf der DVD unter Code/Apps/iOS7/TLS oder im Github-Repository zum Buch im Unterverzeichnis https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/Apps/iOS7/TLS.

Um auf die Daten des vom Server übergebenen Zertifikates zugreifen zu können und um den Hashwert des empfangenen Zertifikates zu prüfen, benötigen Sie die folgenden beiden Importanweisungen:

#import <CommonCrypto/CommonDigest.h>
#import <Security/Security.h>

Die erste importiert die Hashfunktionen von CommonCrypto, die zweite das Security-Framework. Beide Importanweisungen platzieren Sie in der Implementierungsdatei des Viewcontrollers. Darunter definieren Sie eine NSString-Konstante mit dem Hashwert, den Sie vom Serverzertifikat erwarten:

#define SERVER_CERT_HASH 
@"68f3dc2e68b6f5bfedb467ed055e949c369f1089"

Um den Hashwert des Serverzertifikates in hexadezimale Darstellung umzuwandeln, fügen Sie die folgende Methode in den Viewcontroller ein:

- (NSString*)sha1HexDigest:(NSData*)input {
unsigned char result[CC_SHA1_DIGEST_LENGTH];
CC_SHA1([input bytes], (int) [input length], result);

return [NSString stringWithFormat:
@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x
%02x%02x%02x%02x%02x%02x%02x",
result[0], result[1], result[2], result[3],result[4],
result[5], result[6], result[7],result[8], result[9],
result[10], result[11], result[12], result[13],
result[14], result[15],result[16], result[17],
result[18], result[19]];
}

Listing 8.63 Darstellung eines SHA1-Hashes in Hex.

Um nach dem Aufbau einer HTTPS-Verbindung auf die vom Server empfangenen Authentisierungsdaten reagieren zu können, und dazu gehört auch das Serverzertifikat, müssen Sie die Methode connection:canAuthenticateAgainstProtectionSpace: des URL-Connection-Delegates implementieren. Fügen Sie auch diese in die Implementierung des Viewcontrollers ein:

- (BOOL)connection:(NSURLConnection *)connection 
canAuthenticateAgainstProtectionSpace:
(NSURLProtectionSpace *)protectionSpace {
return ([[protectionSpace authenticationMethod]
isEqual:NSURLAuthenticationMethodServerTrust]);
}

Listing 8.64 Abgreifen des Zertifikats

Die eigentliche Überprüfung des Zertifikates erfolgt in der Methode connection:didReceiveAuthenticationChallenge:, die Sie ebenfalls in die Implementierung des Viewcontrollers einfügen:

- (void)connection:(NSURLConnection *)connection 
didReceiveAuthenticationChallenge:
(NSURLAuthenticationChallenge *)challenge {
NSURLProtectionSpace *protectionSpace =
[challenge protectionSpace];
if ([[protectionSpace authenticationMethod]
isEqual:NSURLAuthenticationMethodServerTrust]) {
SecTrustRef theTrust =
[protectionSpace serverTrust];
BOOL trustedCert = NO;

if(theTrust != NULL) {
CFIndex theCertCount =
SecTrustGetCertificateCount(theTrust);

for(CFIndex theCertIndex = 0; theCertIndex <
theCertCount; theCertIndex++) {
SecCertificateRef theCert =
SecTrustGetCertificateAtIndex(theTrust, theCertIndex);
NSData *theData = (__bridge NSData *)
SecCertificateCopyData(theCert);
trustedCert |= [SERVER_CERT_HASH
isEqual:[self sha1HexDigest:theData]];
}
}
if(trustedCert) {
NSURLCredential * credential =
[NSURLCredential credentialForTrust:theTrust];

[[challenge sender] useCredential:credential
forAuthenticationChallenge:challenge];
NSLog(@"Certificate trusted");
}
else {
NSLog(@"Invalid certificate!");
}
}
}

Listing 8.65 Die Überprüfung des Serverzertifikates

Die URL-Connection ruft diese Methode automatisch auf, wenn der Server sein Zertifikat geschickt hat. Die Authentisierungsangaben befinden sich in einem sogenannten Protectionspace, den Sie am Anfang der Methode der Variablen theTrust zuweisen. In der Ausgabe von s_client konnten Sie sehen, dass die Antwort vom Server aus einer Zertifikatskette (Certificate Chain) besteht, also aus verschiedenen Zertifikaten. Das Zertifikat mit dem Index 0 ist das gesuchte Zertifikat des Servers, die folgenden sind die der ausstellenden oder beglaubigenden Stelle. Da man im Voraus nie genau weiß, an welcher Stelle im Index das gesuchte Zertifikat steht, durchläuft die for-Schleife weiter unten in der Methode alle in theTrust befindlichen Zertifikate, liest die Rohdaten jedes Zertifikates über die Funktion SecTrustGetCertificateAtIndex ein. Anschließend kopiert die Funktion SecCertificateCopyData die Daten des gelesenen Zertifikates aus der Zertifikatskette in die Variable theData. Der folgende Aufruf der oben implementierten Methode sha1HexDigest erzeugt dann den SHA1-Hashwert in Hex-Darstellung des Zertifikates. Ist der Hashwert gleich dem in der NSString-Konstanten SERVER_CERT_HASH, hat der Server das korrekte Zertifikat gesendet.

Mit dieser Prüfung können Sie zusätzlich zu der Zertifikatsprüfung durch iOS feststellen, ob es das erwartete Serverzertifikat ist. Damit können Sie die Bequemlichkeit von TLS mit der Sicherheit verbinden, dass die Kommunikation von niemandem manipuliert werden kann. Das bedeutet natürlich auch, dass Sie sich gegebenenfalls das Geld für ein offizielles Zertifikat sparen können. Wenn die Anwendung nur für eine geschlossene Benutzergruppe gedacht ist, z. B. Entwickler auf einem Test-Server, reicht die Prüfung des Hashwertes vollkommen. iOS fragt dann zwar bei der ersten Verbindung, ob der Benutzer dem Zertifikat trauen möchte; da Sie die Überprüfung des Zertifikates aber in der App vornehmen, entsteht kein Sicherheitsverlust dadurch, dass nicht eine offizielle CA das Zertifikat ausgestellt oder beglaubigt hat. Investieren Sie das Geld für ein offizielles Zertifikat daher lieber in Bier oder ähnlich sinnvolle Dinge.

Erledigung durch Zeitablauf

Falls Sie den Hashwert oder weitere eindeutige Merkmale eines Zertifikates in der App prüfen, denken Sie bitte daran, die Gültigkeitsdauer des Zertifikates im Auge zu behalten. Wenn das Zertifikat, dessen Hashwert Sie in der App prüfen, abläuft und durch ein neues ersetzt wird, müssen Sie auch die Prüfung in der App updaten. Weitere Probleme können entstehen, wenn der Kommunikationspartner kein singulärer Server ist, sondern ein über ein Content Delivery Network wie z. B. Akamai verteiltes System. Je nachdem, mit welchem Server sich Ihre App dann verbindet, kann dieser ein anderes Zertifikat für denselben Hostnamen besitzen.


Rheinwerk Computing - Zum Seitenanfang

8.4.8Hier geht die POST abZur nächsten ÜberschriftZur vorigen Überschrift

Sie haben bei der YouTube-Anbindung bereits eine Möglichkeit kennengelernt, Daten von einem Server abzufragen; dort haben Sie einen Parameter in der URL verändert, um nach unterschiedlichen Begriffen zu suchen. Die Methode dataWithContentsOfURL: erzeugt aus der URL eine GET-Anfrage, deren Inhalt wie in Listing 8.43 aussieht, wobei die Parameter auch in der Anfrage-URI stehen. Für die YouTube-URL

http://gdata.youtube.com/feeds/api/videos?orderby=published&alt=json&q=iOS

sieht die erste Zeile der Anfrage beispielsweise so aus:

GET /feeds/api/videos?orderby=published&alt=json&q=iOS HTTP/1.1

Allerdings ist die Länge der URI bei GET-Anfragen beschränkt. Sie sollte mit Pfad und Parametern nicht mehr als 256 Bytes umfassen. Das ist zwar für viele Anfragen ausreichend, damit lassen sich allerdings keine längeren Texte übertragen.

Über POST-Anfragen können Sie diese Einschränkung umgehen, da bei dieser HTTP-Methode der Datenbereich der Anfrage die Parameter enthält. Diese Methode kommt bei Webseiten auch zum Einsatz, wenn der Nutzer die Parameter nicht in der Adressleiste des Browsers sehen soll.

Die Wahl der Methode

Viele Webserver, insbesondere PHP-Seiten, geben die Anfragemethode zwingend vor. Das heißt, Sie müssen die Parameter entweder per GET- oder per POST-Anfrage übermitteln und können nicht die jeweils andere verwenden.

Sie sollten also zunächst herausfinden, welche Methode Sie verwenden dürfen. Sofern Sie das Senden eines HTML-Formulars nachbilden möchten, ist die Sache einfach: Sie senden GET-Anfragen, solange nicht im form-Tag das Attribut method="POST" steht.

In Listing 8.66 sehen Sie ein Beispiel für eine POST-Anfrage. Sie beginnt mit dem Bezeichner POST, der Anfrage-URI und dem Protokoll, und die darauffolgenden Zeilen enthalten Headerparameter. Ein POST-Header hat also die gleiche Struktur wie der Header einer GET-Anfrage. Der Unterschied zwischen beiden Methoden besteht im Datenbereich, wo bei der POST-Anfrage die Parameter stehen können. Dabei sind sie genauso kodiert wie bei einer GET-Anfrage. Sehr wichtig ist dabei allerdings der Content-Type-Header, dessen Wert application/x-www-form-urlencoded festlegt, da es sich um kodierte Formulardaten, also Parameter, handelt.

POST /feeds/api/videos HTTP/1.1
Host: gdata.youtube.com:80
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 5_1_1
like Mac OS X) AppleWebKit/534.46 (KHTML,
like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3
Accept: text/html,application/xhtml+xml,
application/xml;q=0.9,*/*;q=0.8
Content-Length: 32
Content-Type: application/x-www-form-urlencoded
Accept-Language: de-de
Accept-Encoding: gzip, deflate
Connection: keep-alive

orderby=published&alt=json&q=iOS

Listing 8.66 Beispiel für eine POST-Anfrage

POST-Anfragen lassen sich über Objekte der Klasse NSMutableRequest erstellen, indem Sie die Anfragemethode, den Header für Content-Length und natürlich die Parameter setzen. Die Parameter können Sie dabei natürlich analog zu Listing 8.8 erstellen, was jedoch bei großen Parameterlisten leicht zu unübersichtlichen und längeren Codeblöcken führt. Es ist in der Regel praktischer, die Parameter in einem Dictionary zu sammeln und daraus die entsprechende Zeichenkette zu erzeugen. Die Methode parameterStringWithEncoding: aus Listing 8.67 befindet sich im Beispielprojekt SiteSchedule in der Kategorie NSDictionary(HTTPRequest). Sie verwendet für die Kodierung der Parameter die Methode encodedStringForURLWithEncoding: aus Listing 8.9:

- (NSString *)parameterStringWithEncoding:  
(NSStringEncoding)inEncoding {
NSMutableString *theParameters =
[NSMutableString stringWithCapacity:256];
NSString *theSeparator = @"";

for(NSString *theName in self) {
NSString *theEncodedName = [theName
encodedStringForURLWithEncoding:inEncoding];
id theValue = [self valueForKey:theName];

if([theValue conformsToProtocol:
@protocol(NSFastEnumeration)]) {
for(id theCollectionValue in theValue) {
NSString *theItem =
[theCollectionValue description];

[theParameters appendFormat:@"%@%@=%@",
theSeparator, theEncodedName,
[theItem encodedStringForURLWithEncoding:
inEncoding]];
theSeparator = @"&";
}
}
else {
theValue = [theValue description];
[theParameters appendFormat:@"%@%@=%@",
theSeparator, theEncodedName,
[theValue encodedStringForURLWithEncoding:
inEncoding]];
theSeparator = @"&";
}
}
return [theParameters copy];
}

Listing 8.67 Serialisierung der Parameter für Anfragen

Die Implementierung durchläuft alle Schlüssel-Wert-Paare des Dictionarys, kodiert dabei sowohl den Parameternamen als auch dessen Wert und verkettet sie mit den entsprechenden Trennzeichen zu einer langen Zeichenkette. Das HTTP-Protokoll stellt übrigens fast keine Bedingungen an den Inhalt von Parameternamen; sie dürfen alle Zeichen des verwendeten Zeichensatzes, gegebenenfalls kodiert, enthalten. Auch wenn in der Praxis Parameternamen nur aus ASCII-Zeichen bestehen, sollten Sie Parameternamen – zumindest bei solch generischen Implementierungen – auch immer kodieren.

Die Methode aus Listing 8.67 erlaubt außerdem die Übergabe von mehreren Parametern mit dem gleichen Namen. Dazu überführen Sie diese Werte in eine Sammlung – also beispielsweise ein Array oder eine Menge –, die Sie als Parameterwert in das Dictionary einfügen. Die Methode erkennt diese Parameter anhand der if-Abfrage: Wenn die Klasse des Parameterwerts das Protokoll NSFastEnumeration implementiert, iteriert die innere Schleife über alle Werte und hängt sie jeweils mit dem gleichen Namen an die Zeichenkette an.

Umwandlung der Werte in Zeichenketten

Die Methode verwendet für jeden Wert aus dem Dictionary beziehungsweise aus den Sammlungen den Rückgabewert der Methode description. Diese Methode gibt eine Darstellung des Wertes als Zeichenkette zurück. Wenn der Wert selbst bereits eine Zeichenkette ist, liefert description also einfach self zurück und bei Objekten der Klasse NSNumber eine Zeichenkette, die der Zahl entspricht. Durch diesen zusätzlichen Methodenaufruf vermeiden Sie zum einen Laufzeitfehler, und zum anderen wandelt er viele Objekte auch noch in eine passende Darstellung um.

Die Parameter für die YouTube-URL zu Beginn dieses Abschnitts erzeugen Sie beispielsweise über dieses Dictionary:

@{
@"alt" : @"json",
@"orderby" : @"published",
@"q" : @"iOS"
}

Und ein Beispiel für eine Parameterliste, die mehrmals den gleichen Parameternamen enthält, ist:

@{
@"zeichen" : @"&",
@"farbe" : @[ @"blau", @"grün", @"gelb", @"rot" ],
@"zahl" : @[ @2, @4, @8, @2.5 ]
}

Daraus erzeugt die Methode parameterStringWithEncoding: für die UTF-8-Kodierung folgende Zeichenkette:

farbe=blau&farbe=gr%C3 %BCn&farbe=gelb&farbe=rot& 
zahl=2&zahl=4&zahl=8&zahl=2.5&zeichen=%26

Die Methode erzeugt also für jeden Wert in den Arrays für die Parameter farbe und zahl jeweils ein eigenes Schlüssel-Wert-Paar in der Ausgabe. Dabei haben diese Werte genau die gleiche Reihenfolge, die sie jeweils auch im entsprechenden Array haben. Im Gegensatz dazu können Sie die Reihenfolge der Parameter mit unterschiedlichen Namen nicht festlegen. Obwohl der Parameter zeichen als erster in der Definition des Dictionarys steht, erscheint er in der Zeichenkette erst an der letzten Position.

NSString *theBodyString = [theParameters 
parameterStringWithEncoding:NSUTF8StringEncoding];
NSData *theBodyData = [theBodyString
dataUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest *theRequest = [NSMutableURLRequest
requestWithURL:theURL
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:5.0];
NSString *theLength = [NSString stringWithFormat:@"%u",
theBodyData.length];

[theRequest setHTTPMethod:@"POST"];
[theRequest setValue:
@"application/x-www-form-urlencoded; charset=UTF-8"
forHTTPHeaderField:@"Content-Type"];
[theRequest setValue:theLength forHTTPHeaderField:
@"Content-Length"];
[theRequest setHTTPBody:theBodyData];

Listing 8.68 Erzeugung der POST-Anfrage

Mit der URL und dem Parameter-Dictionary können Sie nun eine POST-Anfrage wie in Listing 8.68 erzeugen. Dabei müssen Sie in dem Request-Objekt die Anfragemethode auf POST umstellen und die Header für Content-Type sowie Content-Length setzen. Außerdem wandeln Sie die Zeichenkette mit den Parametern in ein Datenobjekt um und setzen es als Body des Anfrageobjekts.

Übrigens können Sie die Methode parameterStringWithEncoding: nicht nur für POST-Anfragen, sondern auch für die Erstellung von URLs mit variablen Parametern verwenden.


Rheinwerk Computing - Zum Seitenanfang

8.4.9DateiuploadZur nächsten ÜberschriftZur vorigen Überschrift

Sie können zwar mit dem beschriebenen Vorgehen POST-Anfragen an beliebige Server schicken. Allerdings lassen sich damit nur einfache Parameter übertragen. Viele Webseiten erlauben jedoch auch den Upload von Dateien. Dafür reichen einfache POST-Anfragen wie diejenige aus dem letzten Abschnitt allerdings nicht aus, da beim Dateiupload die Anfragedaten einen komplexeren Aufbau haben.

Anhand des Beispielprojekts SiteSchedule sollen Sie nun die notwendigen Schritte kennenlernen, um beliebige Formulardaten über eine POST-Anfrage an einen Server zu schicken. Dazu benötigen Sie zum Testen einen laufenden Webserver mit PHP, wofür Sie die Apache-Installation auf Ihrem Entwicklungsrechner oder auch einen entsprechenden anderen Server nutzen können.

Einrichtung des Webservers

Dieser Kasten erläutert die notwendigen Schritte unter OS X, und Sie sollten Ihren Webserver bereits erfolgreich starten und stoppen können, wie es Abschnitt 8.4.3, »Auf dem Webserver nichts Neues«, beschreibt. Für die PHP-Aktivierung sind allerdings ein paar Unix-Kenntnisse notwendig. Bitte lesen Sie diesen Kasten zunächst einmal komplett durch, bevor Sie loslegen.

Als Erstes aktivieren Sie das PHP-Modul des Apache-Servers, indem Sie in der Datei /etc/apache2/httpd.conf nach der Zeile

#LoadModule php5_module libexec/apache2/libphp5.so

suchen und das Doppelkreuz am Anfang entfernen. Das erreichen Sie am einfachsten durch folgende Schritte:

  1. Kopieren Sie die Datei mit cp /etc/apache2/httpd.conf ~/Desktop/ auf Ihren Schreibtisch, und legen Sie am besten auch direkt eine Sicherheitskopie von der Datei an. Wenn sich bei der Änderung Fehler eingeschlichen haben, können Sie die Datei über die Sicherheitskopie wiederherstellen.
  2. Öffnen Sie die Datei auf dem Schreibtisch mit einem Texteditor, z. B. TextEdit, suchen Sie nach der oben genannten Zeile, und löschen Sie das Kommentarzeichen # am Anfang.
  3. Nach dem Speichern kopieren Sie die geänderte Datei über sudo cp ~/Desktop/httpd.conf /etc/apache2/ zurück an die ursprüngliche Stelle. Der Befehl sudo erlaubt Ihnen die Ausführung von Unix-Kommandos mit Administratorrechten, weswegen er Sie vor der ersten Ausführung nach Ihrem Passwort fragt. [Anm.: Mehr zu dem Befehl erfahren Sie über man sudo oder http://developer.apple.com/library/mac/#documentation/Darwin/Reference/ManPages/man8/sudo.8.html.]
  4. Danach testen Sie über apachctl configtest, ob die geänderte Konfigurationsdatei syntaktisch korrekt ist. Bei der Antwort Syntax OK können Sie den Webserver über das Kommando sudo apachectl restart neu starten. Falls Sie eine Fehlermeldung erhalten, sollten Sie es über die Sicherungskopie erneut probieren.

Um die Funktionsweise des PHP-Moduls zu überprüfen, kopieren Sie das Skript Code/Tools/php/upload.php von der beiliegenden DVD beziehungsweise aus dem Github-Repository https://github.com/Cocoaneheads/iPhone/tree/Auflage_3/Tools/php/upload.php in den Ordner Websites in Ihrem Heimatverzeichnis und rufen in Ihrem Browser die URL http://localhost/~user/upload.php auf. Dabei steht user natürlich für Ihren Login-Kurznamen (siehe Abschnitt 8.4.3, »Auf dem Webserver nichts Neues«). Wenn Sie den HTML- beziehungsweise PHP-Quelltext oder eine Fehlermeldung sehen, hat die Aktivierung nicht funktioniert. Bekommen Sie hingegen ein einfaches Formular mit Eingabefeldern und Buttons angezeigt, dann haben Sie PHP erfolgreich aktiviert.

Damit der Dateiupload auch funktioniert, müssen Sie im Websites-Ordner einen Ordner mit dem Namen uploads anlegen. Öffnen Sie anschließend die Dateiinformation zu diesem Ordner im Finder. Im unteren Bereich können Sie die Zugriffsrechte einstellen. Wählen Sie hier für alle drei Zeilen den Wert Lesen & Schreiben aus.

Danach können Sie die Uploadfunktion des Formulars überprüfen, indem Sie unter Choose a photo to upload: eine beliebige Datei auswählen und das Formular abschicken. Wenn der Upload funktioniert hat, sollte sich eine Kopie dieser Datei in dem Ordner uploads befinden, und die Ergebnisseite zeigt eine kurze Erfolgsmeldung an.

Abbildung

Abbildung 8.24 Zugriffsrechte für den Uploadordner einstellen

POST-Anfragen mit Dateiupload unterscheiden sich im Wesentlichen an zwei Stellen von den bereits besprochenen. Einerseits verwenden sie einen anderen Wert für den Header Content-Type, andererseits haben die Anfragedaten ein eigenes Format. [Anm.: http://www.ietf.org/rfc/rfc1867.txt] Dieses Format nennt sich Multipart-Datenformat, und es unterteilt, wie der Name vermuten lässt, die Daten in mehrere Teile. Dabei enthält jeder Teil entweder einen einfachen Parameter oder eine Datei. Die einzelnen Teile trennt eine frei wählbare Grenzmarke, die Boundary, voneinander. Den Wert der Grenzmarke legen Sie im Content-Type-Header der Anfrage fest, z. B. so:

Content-Type: form-data; boundary=/==MultipartBody==/

Wie gesagt können Sie diese Grenzmarke frei wählen; allerdings darf dieser Wert nicht in den Daten auftreten, die Sie übertragen möchten. Sie sollten also die Marke nicht zu kurz wählen. Jeder Teil beginnt mit zwei Minuszeichen, gefolgt von der Grenzmarke. Der eigentliche Teil beginnt mit einem Header auf den – durch einen doppelten Zeilenvorschub getrennt – die Daten folgen. Für einen einfachen Parameterwert kann der Teil beispielsweise so aussehen:

--/==MultipartBody==/
Content-Disposition: form-data; name="code"

10002

Listing 8.69 Multipart-Teil für einen einfachen Parameter

Dabei muss jeder Teil den Header Content-Disposition enthalten, der den Namen des Parameters festlegt, während der Typ form-data bestimmt, dass es sich um Formulardaten handelt. Der Parameterwert ist eine Zeichenkette, die normalerweise die Kodierung UTF-8 hat. [Anm.: Sie können auch andere Kodierungen verwenden, wenn Sie den Server entsprechend konfigurieren. Allerdings ist es fraglich, ob sich der Aufwand dafür lohnt.]

Das Beispielprojekt SiteSchedule erzeugt Multipart-Daten über die Klasse MIMEMultipartBody. Die Objekte dieser Klasse speichern die Daten in einer privaten Property data der Klasse NSMutableData. Über die Methode appendParameterValue:withData: können Sie einen neuen Parameter an die Multipart-Daten anhängen. Die Implementierung nutzt dafür die Hilfsmethode appendString:, die eine Zeichenkette an die Daten anhängt, und die Hilfsmethode startNewPart, die die Grenzmarke für einen neuen Teil in die Daten schreibt:

- (void)appendString:(NSString *)inValue {
NSData *theData =
[inValue dataUsingEncoding:self.encoding];

[self.data appendData:theData];
}

- (void)appendNewline {
[self appendString:@"\r\n"];
}

- (void)startNewPart {
[self appendString:[NSString stringWithFormat:
@"--%@", self.boundary]];
[self appendNewline];
}

- (void)appendParameterValue:(NSString *)inValue
withName:(NSString *)inName {
NSString *theName = [inName
encodedStringForURLWithEncoding:self.encoding];

[self startNewPart];
[self appendString:[NSString stringWithFormat:
@"Content-Disposition: form-data; name=\"%@\"",
theName]];
[self appendNewline];
[self appendNewline];
[self appendString:inValue];
[self appendNewline];
}

Listing 8.70 Erzeugung des Teils für einen einfachen Parameter

Die Property encoding in Listing 8.70 enthält die Zeichenkodierung für die Zeichenketten, die Property boundary enthält die Grenzmarke, und die Methode appendNewline fügt einfach einen Zeilenvorschub an die Daten an. Der Standard legt hierfür die Zeichenfolge Carriage-Return Line-Feed (CRLF) fest, was den Zeichen mit den ASCII-Codes 14 und 10 beziehungsweise der Escape-Sequenz "\r\n" entspricht.

Die Methode erzeugt den Content-Disposition-Header über die Klassenmethode stringWithFormat:. Da ja der Parametername beliebige Zeichen enthalten darf, verwendet sie hierfür den kodierten Rückgabewert der Methode encodedStringForURLWithEncoding:.

Der Teil für eine Datei hat prinzipiell den gleichen Aufbau wie für einfache Parameter. Allerdings enthält die Content-Disposition zusätzlich den Dateinamen, außerdem muss der Teil einen Content-Type-Header aufweisen. Der Datenblock des Teils enthält die Daten in der Regel in unkodierter Form, also binär.

--/==MultipartBody==/
Content-Disposition: form-data; name="photo";
filename="picture.jpg"
Content-Type: image/jpeg
<<Binäre Daten>>

Listing 8.71 Multipart-Teil für Dateien

Listing 8.72 zeigt die Implementierung der Methode appendData:withName:contentType:filename:, die beliebige Daten als Datei an die Multipart-Daten anhängt. Sie schreibt die beiden Header Content-Disposition und Content-Type sowie, durch zwei Zeilenvorschübe davon getrennt, die angegebenen Daten in die Multipart-Daten. Da auch der Dateiname beliebige Zeichen enthalten darf, verwendet die Methode seinen kodierten Wert.

- (void)appendData:(NSData *)inData 
withName:(NSString *)inName
contentType:(NSString *)inContentType
filename:(NSString *)inFileName {
NSString *theName = [inName
encodedStringForURLWithEncoding:self.encoding];
NSString *theFileName = [inFileName
encodedStringForURLWithEncoding:self.encoding];

[self startNewPart];
[self appendString:[NSString stringWithFormat:
@"Content-Disposition: form-data; name=\"%@\";
filename=\"%@\"", theName, theFileName]];
[self appendNewline];
[self appendString:[NSString stringWithFormat:
@"Content-Type: %@", inContentType]];
[self appendNewline];
[self appendNewline];
[self.data appendData:inData];
[self appendNewline];
}

Listing 8.72 Erzeugung der Multipart-Daten für eine Datei

Mit appendData:withName:contentType:filename: und der Methode appendParameterValue:withName: können Sie nun die Daten für beliebige Formulare erzeugen. Die Grenzmarke, in jeweils zwei Minuszeichen eingeschlossen, beendet schließlich die Parameter in den Multipart-Daten. Dabei erzeugt die Methode body der Klasse MIMEMultipartBody ein Datenobjekt mit dieser Endmarke und liefert es als Ergebnis. Bevor sie allerdings die Endmarke an die Daten anhängt, merkt sie sich die Länge der Daten ohne diese Marke. Wenn die Methode die Daten in ein neues Objekt kopiert hat, stellt sie die alte Länge wieder her, und Sie können die Multipart-Daten um weitere Parameter erweitern.

- (NSData *)body {
NSMutableData *theBody = self.data;
NSUInteger theLength = theBody.length;
NSData *theData;

[self appendString:[NSString stringWithFormat:
@"--%@--", self.boundary]];
[self appendNewline];
theData = [theBody copy];
theBody.length = theLength;
return theData;
}

Listing 8.73 Erzeugung der Multipart-Daten für die Anfrage

Die Klasse MIMEMultipartBody besitzt außerdem die Methode mutableRequestWithURL:timeout:, die eine vorkonfigurierte Anfrage mit den Multipart-Daten erzeugt. Sie erleichtert das Versenden von Multipart-Formulardaten, da sie die notwendigen Header und die Anfragedaten im Anfrageobjekt setzt. Die Hilfsmethode contentType erstellt dabei den Wert inklusive der Grenzmarke für den Content-Type-Header.

- (NSString *)contentType {
return [NSString stringWithFormat:
@"multipart/form-data; boundary=%@",
self.boundary];
}

- (NSMutableURLRequest *)mutableRequestWithURL:
(NSURL *)inURL timeout:(NSTimeInterval)inTimeout {
NSMutableURLRequest *theRequest = [NSMutableURLRequest
requestWithURL:inURL
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData
timeoutInterval:inTimeout];
NSData *theBody = self.body;
NSString *theLength = [NSString stringWithFormat:@"%d",
theBody.length];

[theRequest setHTTPMethod:@"POST"];
[theRequest setValue:self.contentType
forHTTPHeaderField:@"Content-Type"];
[theRequest setValue:theLength
forHTTPHeaderField:@"Content-Length"];
[theRequest setHTTPBody:theBody];
return theRequest;
}

Listing 8.74 Erzeugung einer Anfrage mit Multipart-Daten

Das Beispielprojekt SiteSchedule nutzt die Klasse MIMEMultipartBody, um Fotos von den Baustellen auf den Server zu laden. Den Server simuliert in diesem Fall die Datei upload.php, die Sie zu Beginn dieses Abschnitts auf Ihrem Mac installiert haben. Damit jedoch die App Ihren Server auch findet, müssen Sie die Zeile

#define kUploadURL 
@"http://nostromo.local/~clemens/upload.php"

am Anfang der Datei PhotoUploadViewController.m entsprechend anpassen. Ersetzen Sie hier den Servernamen durch den Namen oder die IP-Adresse Ihres Rechners in Ihrem lokalen Netz. Den Namen sehen Sie in den Systemeinstellung Freigaben im Feld Gerätename (siehe Abbildung 8.15). Außerdem müssen Sie den Nutzernamen hinter der Tilde durch Ihren Login-Kurznamen ersetzen. Falls Sie sich nicht sicher sind, wie Ihrer lautet, geben Sie im Terminal einfach den Befehl echo $USER ein.

Die Action-Methode upload startet den Dateiupload, indem sie die Aufnahme in ein JPG-Bild umwandelt und es mit einem weiteren Parameter in ein Multipart-Datenobjekt einfügt. Danach erzeugt sie über das Datenobjekt eine Anfrage und verschickt diese über die Klasse NSURLConnection.

- (IBAction)upload {
MIMEMultipartBody *theBody =
[[MIMEMultipartBody alloc] init];
NSData *thePhoto = UIImageJPEGRepresentation(
self.photo, 0.8);
Site *theSite = self.activity.site;
NSString *theFile = [NSString
stringWithFormat:@"%@.jpg", theSite.code];
NSMutableURLRequest *theRequest;

[theBody appendParameterValue:theSite.code
withName:@"code"];
[theBody appendData:thePhoto withName:@"photo"
contentType:@"image/jpeg" filename:theFile];
theRequest = [theBody mutableRequestWithURL:
[self uploadURL] timeout:10.0];
[NSURLConnection connectionWithRequest:theRequest
delegate:self];
}

Listing 8.75 Erzeugung und Versand von Multipart-Daten

Wenn der Upload erfolgreich verlaufen ist, zeigt die App unter dem Bild die Meldung aus der Seite upload.php an (siehe Abbildung 8.25), indem das Connection-Delegate analog zu dem in Abschnitt 8.4.4, »Asynchrone Kommunikation«, vorgestellten Vorgehen die Daten aufsammelt. Die Methode connectionDidFinishLoading: wandelt schließlich die Daten in eine Zeichenkette um und extrahiert die Meldung, die innerhalb des Tags <p class='result'>...</p> steht.

Abbildung

Abbildung 8.25 Auf dieser Baustelle ist noch viel zu tun.

- (void)connectionDidFinishLoading:
(NSURLConnection *)inConnection {
NSString *theText =
[[NSString alloc] initWithData:self.responseData
encoding:NSUTF8StringEncoding];
NSRange theRange =
[theText rangeOfString:@"<p class='result'>"];

if(theRange.location != NSNotFound) {
theText = [theText substringFromIndex:
theRange.location + theRange.length];
theRange = [theText rangeOfString:@"</p>"];
if(theRange.location != NSNotFound) {
self.result.text = [theText substringToIndex:
theRange.location];
}
}
self.responseData = nil;
}

Listing 8.76 Auswertung der Serverantwort

Dazu ermittelt sie über die Methode rangeOfString: zunächst die Position des öffnenden Tags. Das Ergebnis vom Typ NSRange enthält im Erfolgsfall die Startposition und die Länge der gesuchten Zeichenkette im Anfrageergebnis. Die Summe dieser beiden Werte entspricht also der Startposition der Meldung; nach dem Aufruf der Methode substringFromIndex: enthält die Variable theText also die Meldung mit dem HTML-Quelltext bis zum Ende der Antwort. Durch einen weiteren Aufruf von rangeOfString: ermittelt die Methode nun den Beginn des schließenden p-Tags und schneidet es über die Methode substringToIndex: zusammen mit dem restlichen HTML-Quelltext ab.

Annahme verweigert

Das Versenden von Anfragen mit Multipart-Daten gehört sicherlich nicht zu den einfachsten Fingerübungen. Schon ein kleiner Fehler in den Multipart-Daten kann dazu führen, dass der Server die Anfrage nicht richtig verarbeiten kann. In vielen Fällen gibt er noch nicht einmal einen dezenten Hinweis, was ihn stört. Bei der Fehlersuche ist die Möglichkeit sehr hilfreich, sich die eingehenden Daten des Servers anzusehen. Das können Sie über das Kommandozeilenprogramm nc erreichen. Der Name dieses Programms ist die Abkürzung für netcat, und über nc -l 1234 loggen Sie die eingehenden Daten auf Port 1234. Da HTTP allerdings den Port 80 verwendet, müssen Sie die URL in der Klasse PhotoUploadViewController anpassen.

Fügen Sie hier die Portnummer, durch einen Doppelpunkt getrennt, hinter den Servernamen ein, also beispielsweise http://nostromo.local:1234/~clemens/upload.php. Wenn Sie mit dieser Änderung eine Anfrage aus der App starten, sendet die App sie nicht an Ihren Webserver, sondern an das Kommandozeilenprogramm, das die eingehenden Daten einfach ausgibt.

Das ist bei den Daten für das Bild allerdings suboptimal, da sie in der Regel Steuerzeichen enthalten, die das Terminal nicht vernünftig anzeigen kann. Um das zu vermeiden, können Sie stattdessen auch eine Zeichenkette senden. Dazu initialisieren Sie die lokale Variable thePhoto in der Methode upload der Klasse PhotoUploadViewController einfach folgendermaßen:

NSData *thePhoto = [@"Hello, world!" 
dataUsingEncoding:NSUTF8StringEncoding];

Nach diesen beiden Änderungen sollte nc ungefähr den folgenden Text für eine Anfrage ausgeben:

POST /~clemens/upload.php HTTP/1.1
Host: nostromo.local:1234
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data;
boundary=----MIMEMultipartBodya3a99b9c
Content-Length: 266
Accept-Language: en-us
Accept: */*
Connection: keep-alive
User-Agent: SiteSchedule/1.0 CFNetwork/609 Darwin/12.2.0

------MIMEMultipartBodya3a99b9c
Content-Disposition: form-data; name="code"

10003
------MIMEMultipartBodya3a99b9c
Content-Disposition: form-data; name="photo";
filename="10003.jpg"
Content-Type: image/jpeg

Hello, world!
------MIMEMultipartBodya3a99b9c—

Sie können im Beispielprojekt über das Makro USE_NETCAT zwischen den beiden Varianten hin- und herschalten, indem Sie es auf 0 beziehungsweise 1 setzen.


Rheinwerk Computing - Zum Seitenanfang

8.4.10Überprüfung der ErreichbarkeitZur vorigen Überschrift

Die Beispielapplikation hat bislang die URL-Verbindungen aufgebaut, ohne sich darum zu kümmern, ob dies überhaupt möglich und sinnvoll ist. Dabei gibt es unter iOS drei mögliche Zustände für die Erreichbarkeit eines Servers:

  • Es besteht keine Verbindung zum Internet, weil sich das Gerät in einem Bereich befindet, in dem es keine Verbindung herstellen kann, oder weil der Nutzer Verbindungen explizit untersagt hat (z. B. aktiver Flugmodus). In diesem Fall macht der Aufbau einer Internetverbindung keinen Sinn.
  • Das Gerät hat eine kostenpflichtige und/oder langsame Verbindung über ein mobiles Datennetz. Hier sollte eine App zumindest bei größeren Downloads den Nutzer warnen beziehungsweise um Erlaubnis fragen.
  • Über eine WLAN-Verbindung besteht eine schnelle und in der Regel kostengünstige Verbindung zum Internet.

Sie sollten also überprüfen, bevor Sie eine Internetverbindung herstellen, welche Verbindungsmöglichkeiten Sie zur Verfügung haben. Das können Sie über das System-Configuration-Framework feststellen, das dafür eine C-API anbietet. Die zentrale Funktion in dem Framework für die Überprüfung der Internetverbindung ist SCNetworkReachabilityGetFlags, die eine Bitmaske mit dem momentanen Verbindungsstatus zu einem Ziel zurückliefert. Dabei kann das Ziel ein bestimmter Server oder das Internet allgemein sein. Die Details über den Zustand können Sie mit Hilfe von vordefinierten Konstanten aus der Bitmaske auslesen. Tabelle 8.17 führt die wichtigsten Konstanten für die Zustandsabfrage auf.

Tabelle 8.17 Konstanten für die Zustandsabfrage

Konstante Beschreibung

kSCNetworkReachabilityFlagsReachable

Wenn dieses Bit gesetzt ist, ist der gesuchte Server erreichbar.

kSCNetworkReachabilityFlagsConnectionRequired

Der gesuchte Server ist erreichbar; das Gerät muss aber zunächst eine Einwahlverbindung einrichten.

kSCNetworkReachabilityFlagsConnectionOnDemand

Durch bestimmte Netzwerkaufrufe kann iOS eine Verbindung zu dem Server herstellen.

kSCNetworkReachabilityFlagsConnectionOnTraffic

Das Gerät stellt beim ersten Datenverkehr automatisch eine Verbindung zum Server her.

kSCNetworkReachabilityFlagsInterventionRequired

Um eine Verbindung zum Server herstellen zu können, ist eine Rückfrage beim Nutzer notwendig.

kSCNetworkReachabilityFlagsIsWWAN

Das Gerät kann den Server über eine mobile Datenverbindung erreichen.

Im Beispielprojekt SiteSchedule übernimmt die Klasse Reachability die Überprüfung des Verbindungszustandes, und die Property currentReachabilityStatus gibt das Ergebnis als Konstanten der Aufzählung ReachabilityStatus zurück. Dabei berechnen sich die drei Zustände über verschiedene Bitkombinationen der Masken aus Tabelle 8.17. Da die Auswertung recht komplex ist, stellt Abbildung 8.26 die möglichen Kombinationen dar, die zu den einzelnen Zuständen führen. Die Abkürzung WWAN steht für Wireless Wide Area Network, das Funknetze mit großen Reichweiten wie GSM, UTMS oder LTE bezeichnet. Dabei passen die Bedingungen in den Verzweigungsknoten jeweils zu den Enden der Konstantennamen, und die Entscheidung des Knotens entspricht der Prüfung auf das jeweilige Bit in der Maske.

Die Funktion SCNetworkReachabilityGetFlags erwartet im ersten Parameter eine Referenz auf das Verbindungsziel vom Typ SCNetworkReachabilityRef und im zweiten Parameter einen Zeiger auf eine Variable vom Typ SCNetworkReachabilityFlags. Über diesen Zeiger liefert die Funktion die Maske zurück. Das Verbindungsziel definieren Sie über einen Servernamen oder eine IP-Adresse. Dabei steht die Adresse 0.0.0.0 für einen beliebigen Server im Internet.

Die Klasse Reachability legt dieses Verbindungsziel in der privaten Property networkReachability ab und initialisiert diese Property über die Funktion SCNetworkReachabiltyCreateWithName in der Methode initWithHostName:, die Sie in Listing 8.77 sehen:

- (id)initWithHostName:(NSString *)inHostName {
SCNetworkReachabilityRef theReachability =
SCNetworkReachabilityCreateWithName(
NULL, [inHostName UTF8String]);

if(theReachability == NULL) {
self = nil;
}
else {
self = [super init];
if(self) {
networkReachability = theReachability;
self.localWiFiReference = NO;
}
else {
CFRelease(theReachability);
}
}
return self;
}

Listing 8.77 Erzeugung der Zielreferenz

Abbildung

Abbildung 8.26 Berechnung des Verbindungsstatus

Sie können natürlich den Verbindungszustand immer dann abfragen, wenn Sie ihn benötigen – also bevor Sie eine Verbindung zu einem Server herstellen wollen. Das SystemConfiguration-Framework bietet Ihnen jedoch auch eine Möglichkeit, Sie bei Änderungen des Verbindungszustands automatisch zu unterrichten. Sie können das beispielsweise dazu verwenden, die Oberfläche automatisch anzupassen:

- (BOOL)startNotifier {
SCNetworkReachabilityContext theContext =
{0, (__bridge void *)self, NULL, NULL, NULL};

return SCNetworkReachabilitySetCallback(
networkReachability, ReachabilityCallback,
&theContext) &&
SCNetworkReachabilityScheduleWithRunLoop(
networkReachability, CFRunLoopGetCurrent(),
kCFRunLoopDefaultMode);
}

- (void)stopNotifier {
if(networkReachability) {
SCNetworkReachabilityUnscheduleFromRunLoop(
networkReachability, CFRunLoopGetCurrent(),
kCFRunLoopDefaultMode);
}
}

Listing 8.78 Starten und Stoppen der automatischen Benachrichtigung

Für die automatische Benachrichtigung legt die Referenz das Verbindungsziel in die Runloop, und die Benachrichtigung erfolgt dann in Form eines Funktionsaufrufes (auch Callback genannt). Den Zeiger auf die Funktion übergeben Sie über die Funktion SCNetworkReachabilitySetCallback. Die Callback-Funktion im Beispielprojekt sendet schließlich eine Benachrichtigung über das Standard-Notificationcenter. Die Direktive #pragma unused (inTarget, inFlags) unterdrückt dabei eine Warnung des Compilers für die beiden nicht benutzten Parameter, da die Funktion nur den dritten Parameter verwendet, der das Reachability-Objekt enthält.

static void ReachabilityCallback( 
SCNetworkReachabilityRef inTarget,
SCNetworkReachabilityFlags inFlags, void *inInfo) {
#pragma unused (inTarget, inFlags)
id theInfo = (__bridge id)inInfo;

@autoreleasepool {
Reachability *theReachability = theInfo;

[[NSNotificationCenter defaultCenter]
postNotificationName:
kReachabilityChangedNotification
object:theReachability];
}
}

Listing 8.79 Versenden der Benachrichtigung

Das Beispielprojekt SiteSchedule wertet diese Benachrichtigungen aus und sperrt oder entsperrt die Buttons für den Download und die Überprüfung der Serverdaten gegebenenfalls.



Ihre Meinung

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de.

<< zurück




Copyright © Rheinwerk Verlag GmbH, Bonn 2014
Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.
Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.


Nutzungsbestimmungen | Datenschutz | Impressum

Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de

Cookie-Einstellungen ändern


  Zum Katalog
Zum Katalog: Apps programmieren für iPhone und iPad






Neuauflage: Apps programmieren für iPhone und iPad
Jetzt Buch bestellen


 Ihre Meinung?
Wie hat Ihnen das Openbook gefallen?
Ihre Meinung

 Buchempfehlungen
Zum Katalog: Einstieg in Objective-C 2.0 und Cocoa






Einstieg in Objective-C 2.0 und Cocoa


Zum Katalog: Spieleprogrammierung mit Android Studio






Spieleprogrammierung mit Android Studio


Zum Katalog: Android 5






Android 5


Zum Katalog: iPhone und iPad-Apps entwickeln






iPhone und iPad-Apps entwickeln


 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und der Schweiz
InfoInfo