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

Inhaltsverzeichnis
Geleitwort
Vorwort
1 Hello iPhone
2 Grundlagen
3 Views und Viewcontroller
4 Alles unter Kontrolle
5 Daten, Tabellen und Controller
6 Models, Layer, Animationen
7 Programmieren, aber sicher
8 Datenserialisierung und Internetzugriff
9 Jahrmarkt der Nützlichkeiten
A Sicherer Entwicklungszyklus
Stichwort

Download:
- ZIP, ca. 49,9 MB
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
Galileo Computing
1000 S., geb., mit DVD
39,90 Euro, ISBN 978-3-8362-1915-0
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 Hier geht die POST ab
Pfeil 8.4.8 Datei-Upload
Pfeil 8.4.9 Ü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

Galileo Computing - Zum Seitenanfang

8.4 Daten, 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 Klient 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: 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.


Galileo Computing - Zum Seitenanfang

8.4.1 Synchrone KommunikationZur nächsten ÜberschriftZur vorigen Überschrift

Sie haben bereits Möglichkeiten aus dem Foundation-Framework für die asynchrone 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 [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.

Auf dem Mac findet Apple FTP schon seit Jahren so wenig relevant, 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 [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 Loginnamen und das Passwort als Klartext vor den Servernamen schreiben. Als Trennzeichen verwenden Sie dabei einen Doppelpunkt beziehungsweise einen Klammeraffen, sodass 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, sodass 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:theURLZeilenumbruch
options:0 error:&theError];

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

Listing 8.39 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: beziehungsweise initWithContentsOfURL:usedEncoding:error: verwenden, bei denen usedEncoding ein Ausgabeparameter ist, was Sie in Listing 8.40 an dem &-Zeichen vor der Variable theEncoding erkennen. Diese Methoden versuchen also die richtige Kodierung zu bestimmen.

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

Listing 8.40 URL-Anfrage mit Bestimmung der Kodierung

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


Galileo Computing - Zum Seitenanfang

8.4.2 Komplexe 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 auch einen Timeout und kann außerdem noch 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 = Zeilenumbruch
[NSURLConnection sendSynchronousRequest:theRequest Zeilenumbruch
returningResponse:&theResponse error:&theError];

Listing 8.41 Synchrone Anfrage mit Anfrage- und Antwortobjekt

Das zurückgelieferte Antwortobjekt enthält verschiedene Metadaten einer Serverantwort. Wie bereits erwähnt wurde, können Sie HTTP-Anfragen noch weitere Daten mitgeben. In Listing 8.42 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 Zeilenumbruch
like Mac OS X) AppleWebKit/534.46 (KHTML, Zeilenumbruch
like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3
Accept: text/html,application/xhtml+xml,Zeilenumbruch
application/xml;q=0.9,*/*;q=0.8
Accept-Language: de-de
Accept-Encoding: gzip, deflate
Connection: keep-alive

Listing 8.42 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 die Anfrage-URI /de/ und das verwendete Protokoll HTTP/1.1. Dabei besteht die 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.13 an einem Beispiel dar, wobei alle Bestandteile bis auf den Server und das Schema optional sind.

Abbildung

Abbildung 8.13 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.42 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. [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.13 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 Offline-Modus.

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. Wenn Sie hingegen noch Header der Anfrage setzen oder Daten an die Anfrage anhängen wollen, 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: können Sie ein Objekt der Klasse NSData an die Anfrage übergeben, um dessen Daten an den Server mitzuschicken.

Der Aufbau einer Antwort ähnelt sehr stark dem Aufbau von Anfragen. Auf die Anfrage aus Listing 8.42 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.43 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 Format HTML, 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; aber dazu später mehr.

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 Hochkommata 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.


Galileo Computing - Zum Seitenanfang

8.4.3 Auf 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 Offline-Betrieb 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.14).

Abbildung

Abbildung 8.14 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 selber 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_2/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 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 launchctl start org.apache.httpd

starten und über

sudo launchctl stop org.apache.httpd

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.44 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]] || Zeilenumbruch
[@"https" isEqualToString:[inURL scheme]]) {
NSMutableURLRequest *theRequest = Zeilenumbruch
[NSMutableURLRequest requestWithURL:inURL Zeilenumbruch
cachePolicy: Zeilenumbruch
NSURLRequestReloadIgnoringLocalCacheData Zeilenumbruch
timeoutInterval:3.0];
NSURLResponse *theResponse = nil;
NSError *theError = nil;

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

Listing 8.45 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 allHeaderFields existiert.

Die Kategorie NSDictionary(HTTPRequest) stellt außerdem noch 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, um diese Werte auch sinnvoll verwenden zu können. Sie können diesen Wert auch über die Methode expectedContentLength direkt aus der Antwort lesen.

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

return [NSDateFormatter dateForRFC1123String:theDate];
}

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

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

return [theLength longLongValue];
}

Listing 8.46 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.

Mithilfe dieser Methoden können Sie nun das oben beschriebene Vorgehen zum Daten-Download 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 = Zeilenumbruch
[NSUserDefaults standardUserDefaults];
NSDate *theUpdateTime = [theDefaults Zeilenumbruch
objectForKey:@"updateTime"];

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

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

Listing 8.47 So wird geprüft, ob ein Daten-Download notwendig ist.

Tipp

Sie können diese Prüfung mit dem Programm SiteSchedule ausprobieren, wenn Sie den Webserver auf Ihrem Mac einschalten (siehe Abbildung 8.14), 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 = Zeilenumbruch
kDefaultDownloadURL;

und ersetzen Sie das Makro auf der rechten Seite durch die URL zu der Datei. Das ist die erste URL in Abbildung 8.14 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 Gerät selber 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.47 startet den Daten-Download 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.47 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 Zeilenumbruch
requestWithURL:inURL];
id theResponse = nil;
NSError *theError = nil;
NSData *theData = [NSURLConnection Zeilenumbruch
sendSynchronousRequest:theRequest Zeilenumbruch
returningResponse:&theResponse error:&theError];

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

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

Listing 8.48 Daten-Download ü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 [Die angeforderte Ressource existiert nicht.]) enthalten die Daten eine Fehlerseite des Servers, jedoch nicht den gewünschten Inhalt.


Galileo Computing - Zum Seitenanfang

8.4.4 Asynchrone 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 Download-Prozesses 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 können Sie in Listing 8.48 die synchrone durch eine asynchrone Anfrage ersetzen.

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

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

Listing 8.49 Ausführen einer einfachen asynchronen Anfrage

Listing 8.49 zeigt den angepassten Code und hebt die Änderungen hervor. Die asynchrone Anfrage initiiert die Klassenmethode sendAsynchronousRequest:queue:completionHandler:, die im zweiten Parameter eine Operation-Queue 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.48. 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.

Operation-Queues

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

Der Code in Listing 8.49 blockiert zwar die Programmausführung nicht, jedoch behebt er nicht die beiden anderen eingangs erwähnten Probleme, da Sie auch hier alle Daten auf einmal in einem Objekt bekommen. Für deren 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 das 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.15 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 Integerwerte, 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.15 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, sodass Sie immer mit Verbindungsabbrüchen rechnen sollten, die Sie auch entsprechend behandeln sollten. Das ist wichtig, damit die Daten in Ihrer App keinen Schaden nehmen können. Ein einfaches Beispiel dafür haben Sie in Listing 8.47 und Listing 8.48: Der Code ändert das Modifikationsdatum in den User-Defaults erst, nach dem er die Daten erfolgreich gesichert hat.

Über die Methode cancel der Klasse NSURLConnection können Sie eine Verbindung auch selber 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 Download-Prozess visualisieren. Die Delegate-Methoden lassen sich darüber folgendermaßen implementieren:

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

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

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

Listing 8.50 Delegate-Implementierung, basierend auf »NSMutableData«

Die Methode connection:didReceiveResponse: in Listing 8.50 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; 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 Zeilenumbruch
updateWithInputStream:inoutStream];
NSUserDefaults *theDefaults = [NSUserDefaults Zeilenumbruch
standardUserDefaults];

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

Listing 8.51 Aktualisierung der Daten und des Zeitstempels

Die Methode updateWithInputStream: in Listing 8.51 entspricht im Wesentlichen Listing 8.21; allerdings initialisiert sie den XML-Parser über die Methode initWithStream: und erzeugt einen Objektkontext für die Verarbeitung.

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

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

Listing 8.52 Verarbeitung der empfangenen XML-Daten


Galileo Computing - Zum Seitenanfang

8.4.5 Groß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, sodass die Datei 1,2 MB groß ist und Sie dadurch den Download-Prozess 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 müssen 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 können Sie ein Objekt der Klasse NSOutputStream erzeugen, das das Delegate in einer Property hält. Anstatt an ein Datenobjekt schreiben Sie nun die empfangenen Daten in einen Ausgabestrom und schreiben sie somit in eine Datei:

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

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

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

[self.outputStream close];

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

Listing 8.53 Delegate-Implementierung, basierend auf einer Datei

Listing 8.53 stellt die Änderungen gegenüber Listing 8.50 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 eine Datei laden, sollten Sie die kompletten Daten nicht mehr 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 verwendet die Methode finishDownload: im Beispielprojekt auch einen Parameter der Klasse NSInputStream.

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 deserialisierten Objekte schon während des Parsings speichert. Nach der erfolgreichen Deserialisierung verwendet dann die App den neuen Store.

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.44), 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 brauchen Sie einfach nur den zusätzlichen Header Range mit der Anfrage zu senden; 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.14 enthält einige Beispiele für gültige Intervalle.

Tabelle 8.14 Beispiele für Bereichsintervalle

Intervall Beschreibung Anwort

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.14 enthält die Bereichsangaben in der Antwort, die zu den Intervallen in der ersten Spalte passen, wenn das Dokument 1000 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 dann ein kompletter Download der Daten notwendig. Der gesamte Download erfordert einige Fallunterscheidungen, die Sie dem Programmablaufplan in Abbildung 8.16. entnehmen können.

Eine Implementierung dieses Verfahrens kann auf Listing 8.53 aufbauen, wobei Sie allerdings den Start der Anfrage anpassen müssen. Das letzte Modifikationsdatum der Serverdaten verwaltet der Code analog zu Listing 8.47 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 = Zeilenumbruch
[NSFileManager defaultManager];
NSError *theError = nil;

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

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

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

Listing 8.54 Hilfsmethoden für den Datei-Download

Abbildung

Abbildung 8.16 Daten-Download mit Wiederaufnahme nach einem Abbruch

Mithilfe der Dateigröße und der Methode downloadRequired aus Listing 8.47 können Sie nun das Anfrageobjekt erstellen. Die Methode startDownload in Listing 8.55 braucht nur noch zu entscheiden, ob sie die kompletten Daten oder nur einen Bereich anfordern soll. Deswegen braucht die Methode nur Bedingungen aus dem grau hinterlegten Teil in Abbildung 8.16 zu 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.55.

- (void)startDownload {
NSURL *theURL = self.downloadURL;
NSMutableURLRequest *theRequest = Zeilenumbruch
[NSMutableURLRequest requestWithURL:theURL Zeilenumbruch
cachePolicy:Zeilenumbruch
NSURLRequestReloadIgnoringLocalCacheDataZeilenumbruch
timeoutInterval:5.0];

NSUInteger theSize = self.downloadFileSize;

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

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

Listing 8.55 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 1000 Bytes abfragen und danach die restlichen Daten, liefert Ihnen die zweite Anfrage trotzdem die ersten 1000 Bytes.

Dieses Problem können Sie auf zwei Wegen umgehen. Für die einfachere Lösung schalten Sie das Caching aus. Alternativ können Sie auch den Cache-Eintrag auslesen und überprüfen, ob er zu den angefragten Daten passt, und ihn bei ungeeigneten Daten aus dem Cache löschen. 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(Zeilenumbruch
NSRange inRange, NSUInteger inSize);
HTTPContentRange HTTPContentRangeMakeFull(Zeilenumbruch
NSUInteger inSize);
HTTPContentRange HTTPContentRangeFromString(Zeilenumbruch
NSString *inString);
NSString *NSStringFromHTTPContentRange(Zeilenumbruch
HTTPContentRange inRange);

Listing 8.56 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 = Zeilenumbruch
[self valueForKey:@"Content-Range"];

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

Listing 8.57 Auslesen des Content-Ranges aus dem Header

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

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

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

Listing 8.58 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.

Galileo Computing - Zum Seitenanfang

8.4.6 PasswortabfragenZur nächsten ÜberschriftZur vorigen Überschrift

In Abschnitt 8.4.1, »Synchrone Kommunikation«, haben Sie bereits eine Möglichkeit kennengelernt, wie Sie HTTP-Login-Daten an den Server übermitteln können. 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 connection:didReceiveAuthenticationChallenge: vor iOS 5 und 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 können Sie die Anfrageverarbeitung auch über cancelAuthenticationChallenge: abbrechen.

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 = Zeilenumbruch
kProtectedDownloadURL;

Die Delegate-Methode öffnet einen Alertview, der den Nutzer nach dem Loginnamen 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 nur 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:Zeilenumbruch
(NSURLAuthenticationChallenge *)inChallenge Zeilenumbruch
forConnection:(NSURLConnection *)inConnection {
if([inChallenge previousFailureCount] < 3) {
NSString *theMethod = [inChallenge.protectionSpace Zeilenumbruch
authenticationMethod];
UIAlertView *theAlert = [[UIAlertView alloc] Zeilenumbruch
initWithTitle:theMethod Zeilenumbruch
message:NSLocalizedString(Zeilenumbruch
@"Please enter your credentials.", @"") Zeilenumbruch
delegate:self cancelButtonTitle: Zeilenumbruch
NSLocalizedString(@"Cancel", @"") Zeilenumbruch
otherButtonTitles: Zeilenumbruch
NSLocalizedString(@"Login", @""), nil];

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

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

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

Listing 8.59 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.59. 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 noch angeben, wie lange die Autorisierungsdaten gültig sind. Hier stellt Ihnen das Foundation-Framework drei Werte zur Verfügung:

Tabelle 8.15 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.

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 Zeilenumbruch
willDismissWithButtonIndex:(NSInteger)inButtonIndex {
if(inButtonIndex == 1) {
NSString *theLogin = Zeilenumbruch
[inAlertView textFieldAtIndex:0].text;
NSString *thePassword = Zeilenumbruch
[inAlertView textFieldAtIndex:1].text;
NSURLCredential *theCredential = Zeilenumbruch
[NSURLCredential credentialWithUser:theLogin Zeilenumbruch
password:thePassword persistence:Zeilenumbruch
NSURLCredentialPersistenceForSession];

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

Listing 8.60 Beantwortung der Autorisierungsanfrage

Dieser Kasten ist keine Sicherheitslücke ...

... obwohl er die Anmeldedaten für das Testdokument im Klartext enthält: Der Loginname 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 können Sie beispielsweise dem Nutzer die Möglichkeit geben, 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 Zeilenumbruch
self.allCredentials) {
NSDictionary *theCredentials = Zeilenumbruch
[self credentialsForProtectionSpace:theSpace];

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

Listing 8.61 Löschen aller Anmeldedaten

Der Simulator vergisst nichts

Wenn Sie die Anmeldedaten löschen, vergisst sie die App auf dem Simulator bis Version 5.1 erst, wenn Sie die Ausführung stoppen. Sie können also noch weitere Anfragen für die bereits authentifizierten Protection Spaces starten, ohne Login-Daten eingeben zu müssen. Unter iOS verhält sich das URL-Loading-System hingegen richtig, und es vergisst die Anmeldedaten ebenfalls.


Galileo Computing - Zum Seitenanfang

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

Sie haben bei der YouTube-Anbindung bereits eine Möglichkeit kennengelernt, um Daten von einem Server abzufragen; dort haben Sie einen Parameter in der URL verändert, um nach unterschiedlichen Begriffen suchen zu können. Die Methode dataWithContentsOfURL: erzeugt aus der URL eine GET-Anfrage wie in Listing 8.42, wobei 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.62 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 Header-Parameter. 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 Zeilenumbruch
like Mac OS X) AppleWebKit/534.46 (KHTML, Zeilenumbruch
like Gecko) Version/5.1 Mobile/9B206 Safari/7534.48.3
Accept: text/html,application/xhtml+xml,Zeilenumbruch
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.62 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.7 erstellen, was jedoch bei großen Parameterlisten leicht zu unübersichtlichen und längeren Codeblöcken führen kann. 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.63 befindet sich im Beispielprojekt SiteSchedule in der Kategorie NSDictionary(HTTPRequest). Sie verwendet für die Kodierung der Parameter die Methode encodedStringForURLWithEncoding: aus Listing 8.8:

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

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

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

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

Listing 8.63 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.63 erlaubt außerdem die Übergabe von mehreren Parametern mit dem gleichen Namen. Dazu fügen Sie diese Werte in eine Sammlung, also beispielsweise ein Array oder eine Menge ein, 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 von dessen Methode description. Diese Methode gibt eine Darstellung des Wertes als Zeichenkette zurück. Wenn der Wert selber 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 können Sie beispielsweise über dieses Dictionary erzeugen:

@{
@"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 ]
}

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

farbe=blau&farbe=gr%C3 %BCn&farbe=gelb&farbe=rot& Zeilenumbruch
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 Zeilenumbruch
parameterStringWithEncoding:NSUTF8StringEncoding];
NSData *theBodyData = [theBodyString Zeilenumbruch
dataUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest *theRequest = [NSMutableURLRequest Zeilenumbruch
requestWithURL:theURL Zeilenumbruch
cachePolicy:NSURLRequestReloadIgnoringLocalCacheData Zeilenumbruch
timeoutInterval:5.0];
NSString *theLength = [NSString stringWithFormat:@"%u", Zeilenumbruch
theBodyData.length];

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

Listing 8.64 Erzeugung der POST-Anfrage

Mit der URL und dem Parameter-Dictionary können Sie nun eine POST-Anfrage wie in Listing 8.64 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 Methode parameterStringWithEncoding: nicht nur für POST-Anfragen, sondern auch für die Erstellung von URLs mit variablen Parametern verwenden.


Galileo Computing - Zum Seitenanfang

8.4.8 Datei-UploadZur nächsten ÜberschriftZur vorigen Überschrift

Icon

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 hierfür 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 in Abschnitt 8.4.3, »Auf dem Webserver nichts Neues«, beschrieben ist. 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 können Sie am einfachsten durch folgende Schritte erreichen:

1. Kopieren Sie die Datei mit cp /etc/apache2/httpd.conf ~/Desktop/ auf Ihren Schreibtisch, und legen Sie am besten 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. [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 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_2/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). 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 Datei-Upload 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 Upload-Funktion 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.17 Zugriffsrechte für den Upload-Ordner einstellen

POST-Anfragen mit Datei-Upload unterscheiden sich im Wesentlichen an zwei Stellen von den bereits besprochenen. Einerseits verwenden sie einen anderen Wert für den Header Content-Type, und andererseits haben die Anfragedaten ein eigenes Format. [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.65 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. [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 = Zeilenumbruch
[inValue dataUsingEncoding:self.encoding];

[self.data appendData:theData];
}

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

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

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

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

Listing 8.66 Erzeugung des Teils für einen einfachen Parameter

Die Property encoding in Listing 8.66 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 der Parametername beliebige Zeichen enthalten darf, verwendet sie 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 noch zusätzlich den Dateinamen, und außerdem muss der Teil noch 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"; Zeilenumbruch
filename="picture.jpg"
Content-Type: image/jpeg

<<Binäre Daten>>

Listing 8.67 Multipart-Teil für Dateien

Listing 8.68 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 sowie Content-Type und, durch zwei Zeilenvorschübe davon getrennt, die angegebenen Daten in die Multipart-Daten. Da auch der Dateiname beliebige Zeichen enthalten darf, verwendet die Methode dessen kodierten Wert.

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

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

Listing 8.68 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:Zeilenumbruch
@"--%@--", self.boundary]];
[self appendNewline];
theData = [theBody copy];
theBody.length = theLength;
return theData;
}

Listing 8.69 Erzeugung der Multpart-Daten für die Anfrage

Die Klasse MIMEMultipartBody besitzt außerdem noch 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:Zeilenumbruch
@"multipart/form-data; boundary=%@", Zeilenumbruch
self.boundary];
}

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

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

Listing 8.70 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 Zeilenumbruch
@"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.14). Außerdem müssen Sie auch 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 Actionmethode upload startet den Datei-Upload, 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 = Zeilenumbruch
[[MIMEMultipartBody alloc] init];
NSData *thePhoto = UIImageJPEGRepresentation(Zeilenumbruch
self.photo, 0.8);
Site *theSite = self.activity.site;
NSString *theFile = [NSString Zeilenumbruch
stringWithFormat:@"%@.jpg", theSite.code];
NSMutableURLRequest *theRequest;

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

Listing 8.71 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.18), 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.

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

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

Listing 8.72 Auswertung der Server-Antwort

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.

Abbildung

Abbildung 8.18 Auf dieser Baustelle ist noch viel zu tun.

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 auch noch nicht mal 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 können Sie die eingehenden Daten auf Port 1234 loggen. 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 diese 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!" Zeilenumbruch
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; Zeilenumbruch
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"; Zeilenumbruch
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.


Galileo Computing - Zum Seitenanfang

8.4.9 Ü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 Verbindung zum Internet.

Sie sollten also, bevor Sie eine Internetverbindung herstellen, überprüfen, welche Verbindungsmöglichkeiten Sie zur Verfügung haben. Das können Sie über das SystemConfiguration-Framework feststellen, das dafür ein C-API anbietet. Die zentrale Funktion in dem Framework 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 mithilfe von vordefinierten Konstanten aus der Bitmaske auslesen. Tabelle 8.16 führt die wichtigsten Konstanten für die Zustandsabfrage auf.

Tabelle 8.16 Konstanten für die Zustandsabfrage

Konstante Beschreibung

kSCNetworkReachabilityFlagsReachable

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

kSCNetworkReachabilityFlagsConnectionRequired

Der gesuchte Server ist erreichbar; es muss aber zunächst eine Einwahlverbindung eingerichtet werden.

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.16. Da die Auswertung recht komplex ist, stellt Abbildung 8.19 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ße 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.

Abbildung

Abbildung 8.19 Berechnung des Verbindungsstatus

Die Funktion SCNetworkReachabilityGetFlags erwartet im ersten Parameter eine Referenz auf das Verbindungziel 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 können Sie über einen Servernamen oder eine IP-Adresse definieren. 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 sie über die Funktion SCNetworkReachabiltyCreateWithName in der Methode initWithHostName:, die Sie in Listing 8.73 sehen:

- (id)initWithHostName:(NSString *)inHostName {
SCNetworkReachabilityRef theReachability = Zeilenumbruch
SCNetworkReachabilityCreateWithName(Zeilenumbruch
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.73 Erzeugung der Zielreferenz

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, um Sie bei Änderungen des Verbindungszustands automatisch zu unterrichten. Sie können das beispielsweise dazu verwenden, die Oberfläche automatisch anzupassen:

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

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

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

Listing 8.74 Starten und Stoppen der automatischen Benachrichtigung

Für die automatische Benachrichtigung legt die Referenz das Verbindungsziel in die Runloop. Die Benachrichtigung erfolgt in Form eines Funktionsaufrufes (auch Callback genannt), und Sie müssen einen Zeiger auf die Funktion über die Funktion SCNetworkReachabilitySetCallback setzen. Die Callback-Funktion im Beispielprojekt sendet schließlich eine Benachrichtigung über das Standard-Notification-Center. 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 Rechability-Objekt enthält.

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

@autoreleasepool {
Reachability *theReachability = theInfo;

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

Listing 8.75 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.



Ihr Kommentar

Wie hat Ihnen das <openbook> gefallen? Wir freuen uns immer über Ihre freundlichen und kritischen Rückmeldungen.

>> Zum Feedback-Formular
<< zurück
  Zum Katalog
Zum Katalog: Apps programmieren für iPhone und iPad

Apps programmieren für iPhone und iPad
Jetzt 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: Apps entwickeln für iPhone und iPad - Videotraining






 Apps entwickeln für
 iPhone und iPad -
 Videotraining


Zum Katalog: Apps mit HTML5 und CSS3






 Apps mit HTML5
 und CSS3


Zum Katalog: iPhone- und iPad-Apps entwickeln






 iPhone- und
 iPad-Apps entwickeln


Zum Katalog: Android 4






 Android 4


Zum Katalog: Android-Apps entwickeln - Videotraining






 Android-Apps
 entwickeln -
 Videotraining


Zum Katalog: Windows Store Apps mit XAML und C#






 Windows Store Apps
 mit XAML und C#


 Shopping
Versandkostenfrei bestellen in Deutschland und Österreich
InfoInfo





Copyright © Galileo Press 2013
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.


[Galileo Computing]

Galileo Press, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, info@galileo-press.de