A máme tady i...OS! - MujMAC.cz - Apple, Mac OS X, Apple iPod

Odběr fotomagazínu

Fotografický magazín "iZIN IDIF" každý týden ve Vašem e-mailu.
Co nového ve světě fotografie!

 

Zadejte Vaši e-mailovou adresu:

Kamarád fotí rád?

Přihlas ho k odběru fotomagazínu!

 

Zadejte e-mailovou adresu kamaráda:

Soutěž

Sponzorem soutěže je:

IDIF

 

Odkud pochází fotografka Anne Erhard?

V dnešní soutěži hrajeme o:

Seriály

Více seriálů



Software

A máme tady i...OS!

8. července 2010, 00.00 | Se správou MujMacu jsme se domluvili, že v našem seriálu "Nastal čas" už nejen na Kakao, ale také na iPhone OS. Nebudeme se zabývat nejzákladnějšími základy sestavování aplikací; budeme se ale věnovat zajímavým oblastem API.

Se správou MujMacu jsme se domluvili, že v našem seriálu "Nastal čas" už nejen na Kakao, ale také na iPhone OS, pardon, OS X, tedy vlastně iOS (Apple k tomu už přistupuje jako Češi ke jménům ulic – např. taková Vinohradská, přednedávnem Stalinova, dříve Schwerinova, předtím Fochova, původně Jungmannova přijde člověku maně na mysl. Ne že by to byla novinka: připomeňme postupné přejmenovávání OpenStepu na všeliké barevné skříňky, než se ustálilo jméno "Cocoa"...).

Nebudeme se zabývat nejzákladnějšími základy sestavování aplikací pro iPhone/iPad; těm bude v nejbližší době věnován samostatný seriál (možná již ve chvíli, kdy tento článek čtete, je jeho prvý díl k dispozici – v době psaní tomu tak ještě nebylo). Stejně ale, jako se postupně díváme na různé zajímavé oblasti API Cocoa, se budeme věnovat také zajímavým oblastem API iOS (tedy v zásadě UIKitu, neboť sám jazyk, runtime či knihovny Foundation jsou oběma platformám společné, a až na některé okrajové výjimky – např. bloky, jež nebyly k dispozici před OS 4, nebo garbage collector, který není v iOS k dispozici vůbec – není třeba při psaní kódu nižší úrovně mezi Cocoa a iOS rozlišovat).

Dohodli jsme se proto, že bude praktičtější prostě rozšířit záběr našeho stávajícího seriálu "Nastal čas", než zavádět seriál nový.

Komunikace mezi telefonem a počítačem

Abychom se k novému tématu odrazili co nejplynuleji, začneme kódem, který funguje stejně dobře v Cocoa (tedy na počítači s Mac OS X), jako na iPhone: ukážeme si, jak implementovat vzájemnou komunikaci typu peer-to-peer mezi dvěma takovými zařízeními.

Proč tedy vůbec hovoříme o iOSu, když by kód, kterým se budeme zabývat, mohl stejně dobře běžet na dvou Macech? Inu, v Cocoa máme k dispozici pro tento účel daleko pohodlnější a silnější aparát, totiž distribuované objekty; ty ale, bohužel, patří mezi API, které v iOS nenalezneme. Musíme tedy sestoupit níže, na úroveň tříd Foundation, které zajišťují přístup k systému Bonjour a ke komunikačním streamům – a dokonce, jak uvidíme, místy je nutno použít i nízkoúrovňové API Carbon pro práci se sockety. Tento kód by se zjevně nikdo neobtěžoval psát pro komunikaci mezi dvěma Macy; pro "domluvu" mezi Macem a iPhonem (nebo dvěma iPhony) prostřednictvím TCP/IP je ale bohužel nutný.

A co Bluetooth?

Moment, "sockety", "TCP/IP"... cožpak by nebyl pro tento typ komunikace daleko šikovnější Bluetooth?

Inu, byl by... kdyby byl. Pro komunikaci mezi dvěma iPhony, jež jsou dostatečně blízko pro spolehlivé spojení Bluetooth, není nic snazšího: pro implementaci můžeme použít velmi pohodlné služby šikovně navrženého a krajně nevhodně pojmenovaného frameworku GameKit (který slouží ke zcela obecné bluetoothové komunikaci mezi zařízeními, a není vůbec nijak omezen na použití pro pouhé hry), a v podstatě "není co řešit".

Chceme-li však komunikovat mezi dvěma vzdálenými iPhony, nezbývá, než sáhnout po Internetu (tedy TCP/IP); podobně je tomu mezi iPhonem a počítačem – na libovolnou vzdálenost. To proto, že bohužel

• GameKit pro Mac OS X k dispozici není (a patrně nebude);

• naopak iOS nenabízí nízkoúrovňový přístup ke službám Bluetooth – ten zde máme k dispozici jen prostřednictvím vysokoúrovňových služeb GameKitu;

• vlastní implementace protokolu GameKitu je netriviální a problematická: protokol není zdokumentován, a i kdybychom jej "vyhackovali", nemáme žádnou záruku, že naše aplikace bude fungovat i s příštím upgradem iOS.

Zkrátka, s bluetoothem máme prozatím – jde-li o komunikaci mezi iPhonem a Macem – smůlu, protože rozumné řešení zde bohužel neexistuje.

Klient

Ačkoli jde v zásadě o komunikaci typu peer-to-peer, stejně má smysl rozlišovat klienta a server: "server" zde je ten, kdo se přihlásí prostřednictvím systému Bonjour k síti, a kdo nabídne komunikační socket. "Klient" je pak ten, kdo v systému Bonjour nalezne server a připojí se k jeho komunikačními socketu. Vlastní komunikace pak již samozřejmě probíhá z obou stran stejně, prostřednictvím streamů a je čistě "peer-to-peerová".

(Pokud bychom naopak potřebovali plnohodnotnou komunikaci typu klient/server, samozřejmě použijeme na Macu docela standardní Apache, případně ve spojení s vhodným aplikačním serverem – nabízejí se samozřejmě WebObjects nebo Ruby on Rails, zdaleka ne tak dobře navržené a výkonné, ale zato používající mnohem lepší jazyk. iPhone pak se k serveru připojí prostřednictvím protokolu HTTP a opět "není co řešit".)

Kód klienta si ukážeme dříve, protože je jednodušší, a na rozdíl od serveru v něm není zapotřebí páchat carbonovské nízkoúrovňové ošklivosti. V podstatě sestává z následujících kroků:

• klient si vyžádá vyhledání serverů prostřednictvím Bonjour;

• k nalezenému serveru se připojí a sestaví komunikační streamy;

• jejich prostřednictvím pak může přijímat i odesílat libovolná data.

My si zde samozřejmě ukážeme kód mírně zjednodušený; oproti mé přednášce na totéž téma na nedávné konferenci iDevcamp, kde se možná někteří z čtenářů tohoto článku mohli s těmito informacemi již setkat, ale přece jen ukážeme alespoň nejzákladnější ošetření chyb i některé mírně pokročilé funkce: na to samozřejmě na přednášce nebyl dostatek místa. Poznamenejme také, že kód vychází z ukázkové aplikace Apple WiTap, kde jsou služby klienta i serveru propojeny dohromady do skutečně "čistého" kódu peer-to-peer.

Budeme předpokládat, že kompletní komunikaci řídí jediný kontrolér; v praxi tomu tak obvykle asi bude, ačkoli samozřejmě – pokud by tomu složitost aplikace odpovídala – rozložit služby do samostatných řídicích objektů, z nichž jeden by se staral pouze o vyhledání serveru, druhý o navázání spojení a třetí o vlastní komunikaci, by nebyl žádný problém.

Ve složeném řídicím objektu můžeme použít kupříkladu následující instanční proměnné:

@interface ClientController:NSObject <...> {
  NSNetServiceBrowser *browser;
  NSNetService *resolving;
  NSString *currentName,*currentDomain;
  NSInputStream *inStream;
  NSOutputStream *outStream;
  NSTimeInterval lastConnectionTime;
}

Zdaleka ne všechny jsou nutně zapotřebí, některé jen usnadňují komunikaci nebo udržují pomocné informace; to uvidíme hned v implementaci odpovídajících metod. Tři tečky v lomených závorkách jsou samozřejmě deklarace protokolů – s novým přístupem Apple, kdy každý delegát má formální protokol, jemuž musí odpovídat, jich obvykle bývá dlouhá řada.

Začneme inicializací; její součástí je ale také uvolnění "starých" objektů – to proto, abychom mohli bezpečně znovu inicializovat komunikaci např. po chybě nebo poté, kdy třeba výpadek na straně providera dočasně přeruší spojení:

@implementation ClientController
...
-(void)releaseStreams {
  [currentName release]; currentName=nil;
  [currentDomain release]; currentDomain=nil;
  [inStream removeFromRunLoop:
    [NSRunLoop currentRunLoop]
    forMode:NSDefaultRunLoopMode];
  [inStream release]; inStream=nil;
  [outStream removeFromRunLoop:
    [NSRunLoop currentRunLoop]
    forMode:NSDefaultRunLoopMode];
  [outStream release]; outStream=nil;
}
-(void)releaseNetSearch {
  [resolving stop];
  [resolving release]; resolving=nil;
  [browser stop];
  [browser release]; browser=nil;
}
-(void)setupCommunication {
  [self releaseStreams];
  [self releaseNetSearch];
  if ((browser=[[NSNetServiceBrowser alloc] init])) {
    [browser setDelegate:self];
    [browser searchForServicesOfType:@"_FOOBAR._tcp."
      inDomain:@"local"];
  } else
    [self networkFailAlert:@"Cannot set up browser"];
}

Služba releaseStreams uvolní pomocné informace a komunikační streamy (k "run loopu" se vrátíme za chvilku, až je budeme inicializovat). Podobně služba releaseNetSearch přeruší vyhledávání serveru – běží-li – a uvolní odpovídající objekty.

Podstatná zde je služba setupCommunication: ta (zavolá obě služby release... a) vytvoří instanci třídy NSNetServiceBrowser; tato třída je standardní systémové API pro vyhledávání serverů (v terminologii Apple "služeb") prostřednictvím systému Bonjour. Inicializace je triviální: řídicí objekt nastaví sám sebe jako delegáta a spustí vyhledávání požadované služby v lokální doméně. My budeme metodu setupCommunication volat ve chvíli, kdy chceme navázat komunikaci – nejspíše tedy hned po spuštění aplikace z metody applicationDidFinishLaunching:, nebo po explicitní aktivaci prostřednictvím libovolného vhodného GUI.

"Typ" hledané služby je právě to, podle čeho se klient a server navzájem poznají. Podtržítka a tečky okolo jsou vyžadovány protokolem Bonjour; "tcp" na konci je identifikace transportní vrstvy. Vlastní jméno služby – v ukázce výše tedy FOOBAR – můžeme v zásadě použít libovolné, ovšem je třeba se vyhnout konfliktům s ostatními vývojáři a jejich službami. Pro jednoznačnost je ideální reversní DNS, jen pozor na omezenou délku (max. 14 znaků) a také na to, že tečka nepatří mezi znaky, jež zde smíme použít: vhodné jméno by mohlo být např. "cz-mujmac-XYZ". Více detailů (včetně odkazu na možnost registrace jména u obecné registrační autority, jež zcela vyloučí případnou možnost konfliktů) lze nalézt v dokumentaci Apple.

V případě chyby nezbývá, než ji ohlásit uživateli (a případně po nějaké době opakovat pokus): konkrétní obsah metody networkFailAlert: je zbytečné uvádět, každý si něco podobného může napsat jinak podle potřeb dané aplikace.

Pokud se v systému Bonjour podaří požadovanou službu nalézt, ohlásí to NSNetServiceBrowser svému delegátovi prostřednictvím standardní zprávy netServiceBrowser:didFindService:moreComing:. Seznam služeb, jež jsou k dispozici, se může dynamicky měnit; browser proto posílá delegátovi také případnou zprávu netServiceBrowser:didRemoveService:moreComing: v případě, že nalezená služba opět zanikne.

V naší jednoduché implementaci po nalezení služby nastavíme náš universální kontrolér jako jejího delegáta a vyžádáme si připojení k odpovídajícímu serveru ("resolve"). Zmizí-li služba, jejímž prostřednictvím se právě připojujeme, akci prostě ukončíme. Pokud by bylo pravděpodobné, že serverů bude více, zde bychom mohli sestavit jejich seznam a nechat uživatele, ať zvolí ten, který chce použít – tento přístup je vidět v ukázkové aplikaci Apple WiTap.

-(void)netServiceBrowser:(NSNetServiceBrowser*)nsb
  didFindService:(NSNetService*)service
  moreComing:(BOOL)more {
  [resolving=[service retain] setDelegate:self];
  [resolving resolveWithTimeout:0];
}
-(void)netServiceBrowser:(NSNetServiceBrowser*)nsb
  didRemoveService:(NSNetService*)service
  moreComing:(BOOL)more {
  if (resolving && [service isEqual:resolving]) {
    [resolving stop];
    [resolving release]; resolving=nil;
  }
}

Připojování k serveru je ovšem déletrvající akce, a je proto samozřejmě asynchronní – po úspěšném připojení služba (tj. instance NSNetService, již jsme dostali jako argument zprávy netServiceBrowser:didFindService:moreComing:) svému delegátu ohlásí výsledek pomocí standardní zprávy netServiceDidResolveAddress: – její implementace by mohla vypadat následovně (pozn.: jelikož jsme nepoužili timeout, nemůže dojít k tomu, že by se připojení nepodařilo – objekt NSNetService to prostě bude zkoušet tak dlouho, dokud je třeba; pokud bychom ovšem timeout nastavili nenulový, bylo by vhodné implementovat také metodu netService:didNotResolve:).

-(void)netServiceDidResolveAddress:(NSNetService*)ns {
  assert(ns==resolving);
  [[resolving autorelease] stop]; resolving=nil;
  if (![ns getInputStream:&inStream
             outputStream:&outStream])
    [self networkFailAlert:@"Failed connecting"];
  else { // no retain!
    currentName=[ns.name copy];
    currentDomain=[ns.domain copy];
    lastConnectionTime=
      [NSDate timeIntervalSinceReferenceDate];
    inStream.delegate=self;
    [inStream scheduleInRunLoop:
      [NSRunLoop currentRunLoop]
      forMode:NSDefaultRunLoopMode];
    [inStream open];
    _outStream.delegate=self;
    [outStream scheduleInRunLoop:
      [NSRunLoop currentRunLoop]
      forMode:NSDefaultRunLoopMode];
    [outStream open];
  }
}

Zde již fakticky navážeme spojení se serverem, vytvoříme a inicializujeme komunikační streamy; kromě toho si v pomocných instančních proměnných zapamatujeme jméno aktuální služby (k tomu, co toto jméno přesně obsahuje, se vrátíme za chvíli, až se budeme zabývat kódem serveru), její doménu a také moment, kdy byla komunikace navázána: tyto údaje mohou být pro uživatele zajímavé a můžeme je proto chtít zobrazit někde v GUI.

Prvá "finta", u níž se na chvilku zdržíme, spočívá v tom, že streamům, jež nám vrátí standardní služba getInputStream:outputStream:, nepošleme zprávu retain. To proto, že Apple má v implementaci metody getInputStream:outputStream: chybu, a ta vrací streamy "neautoreleasované": pokud bychom jim poslali zprávu retain, nebyly by při pozdějším volání metody releaseStreams uvolněny vůbec!

Pak již jen pro oba streamy nastavíme náš universální řídicí objekt jako delegáta, a přidáme je do "run loopu". My se dosud třídou NSRunLoop a jejími službami nezabývali; jejím úkolem je spravovat tzv. "event loop", tj. přijímat události od nejrůznějších zařízení – od myši a klávesnice přes časovač až po komunikační rozhraní – a presentovat je prostřednictvím odpovídajících zpráv. Zatímco pro běžné zdroje událostí (jako je např. klávesnice) se o nic nemusíme starat a vše je zajištěno plně automaticky, některé další zdroje musíme explicitně v aktuální instanci třídy NSRunLoop zaregistrovat, aby je brala v úvahu a – dojde-li na nich k nějaké události – předala naší aplikaci odpovídající zprávu. Právě k tomu slouží standardní metoda scheduleInRunLoop:forMode: (a když už nadále sledovat zdroj nechceme, metoda removeFromRunLoop:forMode:, s níž jsme se setkali výše, jej z "run loopu" opět odstraní).

Poté, co streamy (zaregistrujeme u aktuálního run loopu a) spustíme pomocí zprávy open, můžeme je volně používat ke komunikaci.

Odesílat data budeme nejspíše pomocí synchronní služby xxx; odpovídající metoda by mohla vypadat kupříkladu takto:

-(void)send:(const uint8_t)message {
  if ([outStream hasSpaceAvailable] &&
      [outStream write:&message
             maxLength:sizeof(const uint8_t)]==-1)
    [self networkFailAlert:@"Failed sending"];
}

Pokud bychom ovšem potřebovali vyšší spolehlivost a nemohli si dovolit ztrátu dat v případě, že zrovna náhodou stream není s to další byte přijmout ("![outStream hasSpaceAvailable]"), případně pokud by se to lépe hodilo do konkrétního návrhu naší aplikace, můžeme data odesílat také asynchronně na základě události NSStreamEventHasSpaceAvailable, již si hned ukážeme.

Pro příjem dat samozřejmě užijeme asynchronního přístupu vždy. Pro ten streamy standardně podporují zprávu stream:handleEvent:, již pošlou delegátu, kdykoli na straně streamu nastane odpovídající událost; my bychom ji mohli implementovat kupříkladu takto:

-(void)stream:(NSStream*)stream
  handleEvent:(NSStreamEvent)eventCode {
  switch(eventCode) {
    case NSStreamEventOpenCompleted:
      [self releaseNetSearch];
      break;
    case NSStreamEventHasSpaceAvailable:
      break; // ignored, sending synchronously
    case NSStreamEventHasBytesAvailable: {
      if (stream==inStream) {
        uint8_t b;
        unsigned int len=[inStream read:&b
                              maxLength:sizeof(uint8_t)];
        if (!len) {
          if ([stream streamStatus]!=NSStreamStatusAtEnd)
            [self networkFailAlert:@"Failed reading"];
        } else
          [self proceedReceivedByte:b];
      }
      break;
    case NSStreamEventErrorOccurred: // fall through
    case NSStreamEventEndEncountered:
      [self performSelector:@selector(setupCommunication)
                 withObject:nil afterDelay:1];
  }
}
@end

Jednotlivé události, jež dostaneme od streamu prostřednictvím argumentu eventCode a jež zde zpracováváme, mohou být

NSStreamEventOpenCompleted: stream byl úspěšně otevřen. V podstatě není zapotřebí dělat nic; my jen definitivně ukončíme vyhledávání serveru (uvědomme si, že tuto událost dostaneme od obou streamů – a podívejme se do implementace metody releaseNetSearch, proč to vůbec nevadí);

NSStreamEventHasSpaceAvailable: stream může přijmout odesílaná data; tuto událost dostaneme od výstupního streamu. Zde ji ignorujeme, protože data – jak jsme si ukázali výše – odesíláme synchronně; pokud bychom ale chtěli využít asynchronního přístupu, použili bychom službu write:maxLength: právě zde;

NSStreamEventHasBytesAvailable: stream přijal data; tuto událost dostaneme od vstupního streamu. Data načteme a (po ošetření případné chyby) zpracujeme v metodě proceedReceivedByte:, jejíž konkrétní implementace samozřejmě závisí na dané aplikaci a proto ji neuvádíme;

• konečně události NSStreamEventErrorOccurred – již dostaneme od streamu při chybě – a NSStreamEventEndEncountered – již stream pošle je-li uzavřen – zde zpracujeme týmž způsobem: naplánujeme prostě nové otevření komunikace (to samozřejmě závisí na konkrétních potřebách dané aplikace; mohli bychom také např. chybu či ukončení ohlásit uživateli a počkat na explicitní aktivaci nového připojení).

To je na straně klienta vše; nic jiného není pro základní úspěšnou komunikaci zapotřebí.

Server

Kód serveru je poněkud složitější, protože na jeho straně je pro vytvoření a aktivaci komunikačního socketu zapotřebí sestoupit k nízkoúrovňovému API Carbon. Na druhou stranu je kód zčásti totožný s kódem klienta; nebudeme si proto již podrobně vysvětlovat to, co jsme si ukázali výše (např. registraci streamů v aktuálním "run loopu").

Podobně jako u klienta budeme předpokládat, že kompletní komunikaci serveru řídí jediný kontrolér; ten by mohl mít kupříkladu následující instanční proměnné:

@interface ServerController:NSObject <...> {
  CFSocketRef ipControlSocket;
  NSNetService* ipControlService;
  NSInputStream *ipControlIStream;
  NSOutputStream *ipControlOStream;  
}

I základní struktura kódu bude podobná jako u klienta: definujeme především dvě metody, setupControlListeningPortAndPublish a removeControlListeningPortAndUnpublish; prvou z nich voláme ve chvíli, kdy chceme zahájit komunikaci (např. z applicationDidFinishLaunching:), druhá slouží k "úklidu" např. ve chvíli, kdy je zapotřebí komunikaci po pádu obnovit. Tu si ukážeme nejdříve; vzhledem k tomu, že na straně serveru je toho k uvolňování více, bude delší a malinko složitější:

@implementation ServerController
...
-(void)removeControlListeningPortAndUnpublish {
  if (ipControlService) {
    [ipControlService stop];
    [ipControlService removeFromRunLoop:
      [NSRunLoop currentRunLoop]
      forMode:RUN_LOOP_COMMON_MODES];
    [ipControlService release]; ipControlService=nil;
  }
  if (ipControlSocket) {
    CFSocketInvalidate(ipControlSocket);
    CFRelease(ipControlSocket); ipControlSocket=NULL;
  }
  if (ipControlIStream) {
    [ipControlIStream removeFromRunLoop:
      [NSRunLoop currentRunLoop]
      forMode:RUN_LOOP_COMMON_MODES];
    [ipControlIStream close];
    [ipControlIStream release]; ipControlIStream=nil;
  }
  if (ipControlOStream) {
    [ipControlOStream removeFromRunLoop:
      [NSRunLoop currentRunLoop]
      forMode:RUN_LOOP_COMMON_MODES];
    [ipControlOStream close];
    [ipControlOStream release]; ipControlOStream=nil;
  }
}

Vlastní inicializaci socketu a přihlášení k Bonjour ale prozatím odložíme do další pomocné metody, jejíž obsah si ukážeme později; prozatím ji jen použijeme a pokud zklame, naplánujeme "za chvilku" nový pokus (samozřejmě není naprosto nutné problém řešit takto; je to jen jedna z mnoha možností, jež se náhodou v praxi celkem osvědčila). Metoda setupControlListeningPortAndPublish je proto velmi jednoduchá:

-(void)setupControlListeningPortAndPublish {
  [self removeControlListeningPortAndUnpublish];
  if (![self doSetupCLPortAndPublishSucceeded])
    [self performSelector:_cmd
               withObject:nil afterDelay:30];
}

Naopak metoda doSetupCLPortAndPublishSucceeded, jež skutečně vytváří a publikuje komunikační socket, je poměrně složitá; proto si její kód rozdělíme na několik bloků, a každý z nich si vysvětlíme zvlášť:

-(BOOL)doSetupCLPortAndPublishSucceeded {
  CFSocketContext sctxt={0,self,NULL,NULL,NULL};
  ipControlSocket=CFSocketCreate(kCFAllocatorDefault,
    PF_INET,SOCK_STREAM,IPPROTO_TCP,
    kCFSocketAcceptCallBack,
    (CFSocketCallBack)&TCPSocketCallBack,&sctxt));
  if (!ipControlSocket) return NO;
  int yes=1;
  setsockopt(CFSocketGetNative(ipControlSocket),
    SOL_SOCKET,SO_REUSEADDR,(void *)&yes,sizeof(yes));

Nejprve vytvoříme komunikační socket a nastavíme potřebným způsobem jeho atributy. Detailní vysvětlení všech argumentů lze nalézt v dokumentaci, ale pro nás na naší úrovni není příliš podstatné; opravdu důležité jsou pouze dva momenty, oba v kódu výše označené tučně: předání odkazu na řídicí objekt (self) v kontextu socketu, abychom jej měli k dispozici v callbacku; a adresa vlastního callbacku – zde TCPSocketCallBack: to je jméno funkce, která se zavolá ve chvíli, kdy se k socketu připojí klient. Jelikož užíváme nízkoúrovňové API Carbon, nejde o zprávu, ale o obyčejnou funkci plain C; její obsah si ukážeme za chvilku.
  struct sockaddr_in addr4;
  memset(&addr4,0,sizeof(addr4));
  addr4.sin_len=sizeof(addr4);
  addr4.sin_family=AF_INET;
  addr4.sin_port=0;
  addr4.sin_addr.s_addr=htonl(INADDR_ANY);
  NSData *a4=[NSData dataWithBytes:&addr4
                            length:sizeof(addr4)];
  if (CFSocketSetAddress(ipControlSocket,(CFDataRef)a4)
                                   !=kCFSocketSuccess) {
    CFRelease(ipControlSocket); ipControlSocket=NULL;
    return NO;
  }

Výše uvedený několikařádkový kód je v nízkoúrovňovém API zapotřebí na jedinou drobnost: nastavení čísla portu, k němuž je socket připojen (sin_port). Nastavíme-li je na nulu – jak jsme právě učinili –, přidělí jádro libovolný volný port. To je obecně výhodné (jen pozor na nastavení firewallu), protože nehrozí, abychom se o pevně zvolené číslo portu "poprali" s jinou aplikací; před publikací socketu prostřednictvím Bonjour ovšem musíme přidělené číslo portu opět zjistit:

  NSData *addr=[(NSData*)CFSocketCopyAddress
                         (ipControlSocket) autorelease];
  memcpy(&addr4,[addr bytes],[addr length]);
  uint16_t port=ntohs(addr4.sin_port);

Za zmínku možná stojí, že kopírování dat do proměnné addr4 je docela zbytečné; stejně dobře bychom mohli port načíst pomocí přetypování rovnou z [addr bytes]. Jde ovšem o nepodstatnou drobnost; tento kód je převzatý beze změny z příkladu WiTap, a nestálo za to jej přepisovat ☺

  CFRunLoopSourceRef source=CFSocketCreateRunLoopSource
                (kCFAllocatorDefault,ipControlSocket,0);
  CFRunLoopAddSource(CFRunLoopGetCurrent(),source,
                kCFRunLoopCommonModes);
  CFRelease(source);

"Run loop" již známe; zde je rozdíl jen v tom, že namísto streamů v něm registrujeme socket, a že namísto objektového rozhraní NSRunLoop používáme nižší úroveň, Carbon (carbonovský CFRunLoop je součástí objektového NSRunLoopu, ale není s ním totožný).

  if (!(ipControlService=[[NSNetService alloc]
      initWithDomain:@"local" type:@"_FOOBAR._tcp."
      name:nil port:port])) return NO;
  [ipControlService setDelegate:self];
  [ipControlService scheduleInRunLoop:
    [NSRunLoop currentRunLoop]
    forMode:RUN_LOOP_COMMON_MODES];
  [ipControlService publish];
  return YES;
}

Nakonec vytvoříme objekt NSNetService, jehož prostřednictvím server svou službu publikuje tak, aby ji mohli klienti nalézt prostřednictvím protokolu Bonjour. Stran "typu" FOOBAR vizte výše. Jméno "name" je právě místo, v němž můžeme určit, jaké jméno uvidí klient, nahlédne-li do atributu name získané služby; užijeme-li – stejně jako zde – hodnotu nil, automaticky se použije jméno počítače.

Vzhledem k tomu, že socket po připojení klienta volá "callback" (v našem případě funkci TCPSocketCallBack, jejíž kód si ukážeme za chvilku), v podstatě není zapotřebí služeb delegáta; nastavujeme jej pouze proto, abychom mohli zachytit případnou chybu při publikování služby a – pokud by nastala – po krátké pause inicializaci zopakovat (nebo ohlásit chybu, atakdále, atakdále):

-(void)netService:(NSNetService*)sender
  didNotPublish:(NSDictionary*)errorDict {
  [self performSelector:
    @selector(setupControlListeningPortAndPublish)
    withObject:nil afterDelay:30];
}

"Callback" TCPSocketCallBack se automaticky zavolá ve chvíli, kdy se ke komunikačnímu socketu připojí klient. V něm musíme opět – a naštěstí již naposledy – sáhnout po nízkoúrovňovém API Carbon, jehož vinou je kód poměrně komplikovaný, ačkoli se toho vlastně moc neděje: jen vyrobíme pro daný socket potřebné komunikační streamy (na rozdíl od obdobné služby Apple, již používáme hotovou na straně klienta, je nezapomeneme uvolnit ☺), a předáme je inicializační metodě connectInputStream:outputStream: řídicího objektu (jehož adresa byla uložena v kontextu socketu):

static void TCPSocketCallBack(CFSocketRef socket,
  CFSocketCallBackType type, CFDataRef address,
  const void *data, void *info) {
  if (type!=kCFSocketAcceptCallBack) return;
  ServerController *self=(ServerController*)info;
  CFSocketNativeHandle nativeSocketHandle=
    *(CFSocketNativeHandle*)data;
  CFReadStreamRef readStream=NULL;
  CFWriteStreamRef writeStream=NULL;
  CFStreamCreatePairWithSocket(kCFAllocatorDefault,
    nativeSocketHandle,&readStream,&writeStream);
  if (readStream && writeStream) {
    CFReadStreamSetProperty(readStream,
      kCFStreamPropertyShouldCloseNativeSocket,
      kCFBooleanTrue);
    CFWriteStreamSetProperty(writeStream,
      kCFStreamPropertyShouldCloseNativeSocket,
      kCFBooleanTrue);
    [self connectInputStream:(NSInputStream*)readStream
          outputStream:(NSOutputStream*)writeStream];
  } else {
    [self removeControlListeningPortAndUnpublish];
    [self performSelector:
      @selector(setupControlListeningPortAndPublish)
      withObject:nil afterDelay:30];
  }
  if (readStream) CFRelease(readStream);
  if (writeStream) CFRelease(writeStream);
}

Tím jsme v podstatě hotovi; jen pro úplnost si ukážeme inicializační metodu connectInputStream:outputStream: řídicího objektu, a také jeho kód pro vlastní komunikaci prostřednictvím streamů, jež tato metoda dostane (od výše popsané funkce TCPSocketCallBack) v argumentech; popisovat funkci jejich kódu si ale už nebudeme, neboť jde o služby dávno známé z klienta (a v případě, kdy obě části implementujeme v témže kódu, který může sloužit stejně dobře jako server i jako klient – jak tomu je např. v příkladu Apple WiTap –, můžeme jejich kód samozřejmě napsat pouze jednou a šikovně vícekrát využít):

-(void)connectInputStream:(NSInputStream*)istr
             outputStream:(NSOutputStream*)ostr {
  [self removeControlListeningPortAndUnpublish];
  [ipControlIStream=[istr retain] setDelegate:self];
  [ipControlIStream scheduleInRunLoop:
    [NSRunLoop currentRunLoop]
    forMode:NSDefaultRunLoopMode];
  [ipControlIStream open];
  [ipControlOStream=[ostr retain] setDelegate:self];
  [ipControlOStream scheduleInRunLoop:
    [NSRunLoop currentRunLoop]
    forMode:NSDefaultRunLoopMode];
  [ipControlOStream open];
}
-(void)send:(const uint8_t)message {
  if ([ipControlOStream hasSpaceAvailable] &&
      [ipControlOStream write:&message
             maxLength:sizeof(const uint8_t)]==-1)
    [self networkFailAlert:@"Failed sending"];
}

-(void)stream:(NSStream*)stream
  handleEvent:(NSStreamEvent)eventCode {
  switch (eventCode) {
    case NSStreamEventHasBytesAvailable:
      if (stream!=ipControlIStream) break;
      uint8_t b;
      unsigned len=[ipControlIStream read:&b
                         maxLength:sizeof(uint8_t)];
      if (!len) {
        if ([stream streamStatus]!=NSStreamStatusAtEnd)
          [self networkFailAlert:@"Failed reading"];
      } else
          [self proceedReceivedByte:b];
      break;
    case NSStreamEventErrorOccurred:
    case NSStreamEventEndEncountered:
      [self setupControlListeningPortAndPublish];
  }
}
@end

Obsah seriálu (více o seriálu):

Tématické zařazení:

 » Rubriky  » Informace  

 » Rubriky  » Agregator  

 » Rubriky  » Tipy a Triky  

 » Rubriky  » Začínáme s  

 » Rubriky  » Software  

 

 

 

 

Přihlášení k mému účtu

Uživatelské jméno:

Heslo: