int21h

Komunikace přes sítě BNC/TP

Druhý díl seriálu o komunikacích už konečně na úrovni dnešní techniky :-). Pokud je pro Vás komunikace přes sériový port příliš pomalá nebo drahá, použijte programování pro sítě. Dnes již každý počítač obsahuje integrovanou síťovou kartu, takže potřebujete pouze TP kabel (pro starší síťové karty ještě BNC, Téčka a terminátory) v ceně pár (desítek) Kč. Obsluha sítě není nic těžkého a oproti sériovému kabelu přináší několikanásobné navýšení rychlosti.

Dnes máte prakticky dvě možnosti: buď používat IPX protokol (což je také to jediné, co dnes můžete bez dostatečných znalostí použít i v čistém DOSu), který je rychlý, ale nejde použít pro komunikaci přes Internet. Windows jej podporuje také (pokud si ho nainstalujete), a navíc ještě TCP, který je o pár % pomalejší, ale umožňuje komunikaci přes internet. Oba protokoly jsou pak ještě doplněné korekcí chyb. Pokud se rozhodnete pro síť pod čistým DOSem, budete potřebovat ovladač síťové karty (něco na způsob Ne2000.com) a ovladač protokolu (Pdipx.com; nebo něco složitějšího jako Ipxodi.com, lsl.com, pnpodit.com a net.exe). V obou protokolech budeme pracovat s tzv. sockety, což jsou zásuvky, přes které budete posílat svá data. IPX je díky nutnosti používat konvenční paměť vhodný pro Turbo Pascal 7 (DOS nebo DOSové aplikace pod Windows 9X). Free Pascal naopak obsahuje jednotky SOCKETs, které přímo podporují sockety jak pro Linux, tak pro Windows 9X a NT, takže se zde spíše uplatní TCP.

Protokol IPX/SPX

Protože teorii o IPX jsem již popisoval na své homapage, nebudu si třepit ústa zbytečně znovu. Začneme od nejvyšší vrstvy. Abychom mohli posílat data po síti přes packet, budeme si packety muset nadefinovat pro oba směry:

type TPacket = record
		ecb  : ECBType;
		IPX  : IPXheader;
		data : string;	(* zde může být i array of neco *)
	       end;
var Send, Receive : TPacket;  

Typy, které jsme právě použili si musíme samozřejmě také nejprve nadefinovat a to v souladu se standardy (jen upozorňuji, že v IPX je docela nepořádek a spousta dat je v konvenci Motorola, tzv. Big Endián, kdy vyšší nibble wordu jde v paměti jako první, tj. opačně než co známe u našich Intel procesorů):

(* adresa sítě a počítače v síti dle tečkové konvence *)
type	netAddr  = array[1..4] of byte;
	nodeAddr = array[1..6] of byte;
(* ukazatel na data, kde 0 je offset a 1 segment *)
	address  = array[0..1] of word;
(* informace o adrese pro další typ *)
	netAddress = record
			Net : netAddr;
			Node : nodeAddr;
			Socket : word;	(* Velký endián *)
		     end;
(* informace o lokální adrese *)
	localAddrT = record
			Net : netAddr;
			Node : nodeAddr;
		     end;
(* řídící blok události *)
	ECBType = record
		   link : address;
		   ESR : address;
		   inUse : byte;
		   complete : byte;
		   socket : word;
		   IPXwork : array[1..4] of byte;
		   Dwork : array[1..12] of byte;
		   immedAddr : nodeAddr;
		   fragCount : word;
		   fragData : address;
		   fragSize : word;
		  end;
(* IPX hlavička *)
	IPXheader = record
		     check  : word;
		     length : word;	(* velký endián *)
		     tc     : byte;
		     pType  : byte;
		     dest   : netAddress;
		     src    : netAddress;
		    end;  

Je toho docela dost, ale věřte, že většinu údajů nebudete potřebovat znát dopodrobna. Ještě budeme potřebovat nějaké ty informace (číslo socketu, který může u IPX na jednom počítači používat pouze 1 aplikace - v tomto případě ho krademe hře Doom - a vysílací adresa):

const	MYSOCKET : word = $869c;
	BROADCAST : nodeAddr = ($ff,$ff,$ff,$ff,$ff,$ff);
(* naše místní adresa *)
var	localAddr : localAddrT;

Dobře, ale jak teď budeme obsluhovat naši síť? Naše aplikace, která bude využívat nějakou jednotku (kterou si napíšeme později), bude vypadat nějak takto (pozor, na rozdíl od TCP musí být socket příjemce a odesílatele na obou počítačích stejný!):

begin
(* otevřeme socket; pokud se to zdaří, můžeme jít dál *)
 if IPXopenSocket(0,MYSOCKET)=0 then
 begin
(* inicializujeme IPX ovladač *)
  InitIPX;
(* nastavíme sockety pro vysílání a příjem *)
  with send do
   InitSendPacket(ecb,ipx,sizeof(String),MYSOCKET);
  with receive do
   InitReceivePacket(ecb,ipx,sizeof(String),MYSOCKET);
(* pro opakovou komunikaci provádějte jen to mezi --- *)
(* --------- *)
(* dokud nic nepřišlo, budeme čekat *)
  repeat until receive.ecb.inuse = 0;
(* vypíšeme data, která nám přišla (máme je jako STRING) *)
  writeln(receive.data);
(* řekneme ovladači, aby čekal na další data *)
  if IPXlistenForPacket(receive.ecb)<>0 then halt(1);	(* chyba! *)
(* tady můžeme něco poslat, posíláme všem - adresa $ffff... *)
  send.data:='Odeslaná zpráva do sítě...';
  with send.ecb do
   for i:=1 to 6 do
    ImmedAddr[i]:=$ff;
(* počkáme, dokud nebude ovladač připraven *)
  repeat until send.ecb.inuse = 0;
(* pak odešleme naši zprávu také jako String *)
  IPXsendPacket(send.ecb);
(* --------- *)
(* už nic nepotřebujeme, takže skončíme a zavřeme obsluhu sítě *)
  IPXcloseSocket(MYSOCKET);
 end;
end;

Každý odeslaný packet nám přijde zpět, protože vysílací adresa, kterou nastavujeme při odesílání je $ff, tedy všem. Abychom ušetřili čas ostatních počítačů (pokud jich máte na síti víc), zařiďte si systém, kdy pošlete zprávu všem něco na způsob "kdo bude chtít se mnou mluvit, ať pošle PŘÍJEM". Pak si jen uložte adresy počítačů, které Vám poslali "PŘÍJEM" a další data posílejte už jen jim. Dobře, takto to zatím vypadá jednoduše, ale použité funkce ještě neexistují, takže je musíme napsat. Ovladač IPX socketů hnízdí na přerušení $7a, takže budeme volat standardně toto přerušení.

Nejprve si ukážeme funkci, kterou jsem sice nepoužil, ale hodí se, pokud je Vaše aplikace náročná na čas procesoru. Tou řekneme ovladači, že odteď až do nějaké další akce nebudeme dělat nic náročného, takže si může udělat svoje věci:

procedure Idle; assembler;
asm
	mov	bx,$a
	int	$7a
end;  

Všechny tyto funkce naleznete popsané i v ATHelpu pod daným přerušení. Nyní se podíváme na funkce, které otevřou a zavřou socket. To je potřeba pro jeho používání (resp. ukončení používání):

function IPXopenSocket(trvani : byte; var cisloSock : word) : byte; assembler;
asm
	xor	bx,bx
	mov	al,trvani	(* 0 = do uzavření či konce programu *)
	mov	dx,cislosock
	mov	cx,dx
	xchg	dl,dh
	int	$7a
	or	cx,cx
	jne	@neni
	xchg	dl,dh
@neni:
	les	bx,cislosock
	mov	es:[bx],dx
end;	(* vrací: 0 = OK, $ff - již otevřen, $fe - vyčerpaný počet *)


procedure IPXcloseSocket(Cislo : word); assembler;
asm
	mov	bx,1
	mov	dx,cislo
	xchg	dl,dh
	int	$7a
end;  

Následující funkce zjistí naši lokální adresu a uloží do předpřipravené proměnné:

procedure GetLocalAddress; assembler;
asm
	mov	bx,9
	les	si,localAddr
	int	$7a
end;  

Další dvě funkce zajišťují odeslání packetu, resp. jeho příjem. Je nutné si uvědomit, že funkce pro příjem packet ve skutečnosti nepřijme. Pouze řekne ovladači, aby jakmile přijde packet, nastavil dle obsluhy určitý byte a pak hned skončí. Pokud nějaký packet opravdu přijde, musíte ho zpracovat a pak spustit tuto proceduru znovu. Jinak je příjem dalších packetů zablokován (a budou zahozeny)!

procedure IPXsendPacket(var E : ECBtype); assembler;
asm
	mov	bx,3
	les	si,e
	int	$7a
end;


function IPXlistenForPacket(var E : ECBtype) : byte; assembler;
asm
	mov	bx,4
	les	si,e
	int	$7a
end;	(* vrací <>0 při chybě *)  

Dále budeme potřebovat funkci, která nastaví IPX a zjistí místní uzel:

procedure InitIPX; assembler;
asm
	mov	ax,$7a00
	int	$2f		(* multiplex *)
	cmp	al,255
	jne	@chyba		(* nastala chyba? *)
	call	getlocaladdress
@chyba:
end;  

Jako poslední nám chybí funkce, které nastaví vysílací, resp. přijímací packet, aby se dal použít. Prakticky je stačí jen opsat:

procedure InitSendPacket(var ecb : ecbType; var ipx : ipxHeader; size,sock : word);
begin
 fillChar(ecb, sizeOf(ecb), 0);
 fillChar(ipx, sizeOf(ipx), 0);
 with ecb do
 begin
  socket := swap(sock);
  fragCount := 1;
  fragData[0] := ofs(IPX);
  fragData[1] := seg(IPX);
  fragSize := sizeof(IPX) + size;
  immedAddr := BROADCAST;
 end;
 with ipx do
 begin
  check := $ffff;
  ptype := 0;
  dest.net := localAddr.net;
  dest.node := BROADCAST;
  dest.socket := swap(sock);
  src.net := localAddr.net;
  src.node := localAddr.node;
  src.socket := swap(sock);
 end;
end;


procedure InitReceivePacket(var ecb : ecbType; var ipx : ipxHeader; size,sock : word);
begin
 fillChar(ecb, sizeOf(ecb), 0);
 fillChar(ipx, sizeOf(ipx), 0);
 with ecb do
 begin
  inUse := $1d;
  socket := swap(sock);
  fragCount := 1;
  fragData[0] := ofs(IPX);
  fragData[1] := seg(IPX);
  fragSize := sizeof(IPX) + size;
 end;
 if IPXlistenForPacket(ecb) <> 0 then;
end;  

Pokud byste si je chtěli měnit a neumíte anglicky (SRC = zdrojový, atd.), tak opět odkazuji na ATHelp. No, a věřte nebo ne, v tom je celé to kouzlo. Pokud Vám síť funguje dobře, tak už můžete vesele síťovat. Pokud náte Free Pascal, můžete toto použít také, ale musíte si alokovat část dolní (konvenční paměti). Lepší je ovšem využít TCP (pokud je k dispozici):


Protokol TCP/IP

Tento protokol na rozdíl od IPX, které funguje většinou na principu peer-to-peer, funguje jako klient-server. I když IPX se může chovat stejně (když si ho tak napíšete) a TCP (sockety) disponuje také prostředky jak posílat data i na počítač, ke kterému jsme se nepřipojili, je lepší využívat tuto CS architekturu. Budeme se tedy soustředit na dva programy: klient a server. Vycházím z příkladu, který je uvedený na stránkách Free Pascalu, protože starší z dokumentace SOCKETS, nebo z různých WWW na Internetu, mi z nějakých důvodů nešly zkompilovat (jakoby nefungovalo přetěžování). Příklady jsem upravil tak, aby klient mohl data serveru i posílat, a server je mohl přijímat. Protože FP je multiplaformní, jsou příkazy napsané tak, aby šly přeložit jak pro Windows, tak pro Linux/Unix.

Nejprve, společná část oběma programům:

program SockSer;	(* nebo SockCli *)
(* pro klienta uveďte toto, pokud to bude jen textový program: *)
{$IFDEF DELPHI}
{$APPTYPE CONSOLE}
{$ENDIF}
(* ----- *)
uses Crt, Sockets {$IFDEF WIN32}, Winsock {$ENDIF};
{$IFDEF LINUX}
type sockaddr_in = TSockAddr;
const IPPROTO_TCP = 6;
{$ENDIF}
var	s, c : cardinal;
	addr : sockaddr_in;
	str : string;
	dat : array[1..50] of byte;
	len, t : integer;
(* toto je zase pouze pro server *)
{$IFDEF LINUX}
	dummyInt : integer;
	dummyPtr : pointer;
{$ENDIF}
(* ----- *)  

Jen pro vysvětlení. S a C jsou sockety, které budeme využívat. Vždy, když totiž otevřeme socket, dostaneme číslo, se kterým můžeme pracovat, avšak původní socket zůstane opět volný, takže můžeme připojit libovolný počet počítačů k jednomu socketu. Na jednom počítači může pouze 1 program využívat jeden port (na kterém bude naslouchat), může se k němu ale připojit libovolný počet klientů (socketů), které sami využívají své vlastní čísla na svých počítačích (opět může běžet jen 1 klient s daným číslem portu). Nyní se vrhneme již na samotné programy. Začneme serverem.

begin
(* necháme si vrátit nějaký volný socket *)
 s := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 if s <> 0 then
 begin
(* nastavíme, že se k nám může připojit kdokoliv na portu 40 *)
  FillChar(addr, sizeof(addr), 0);
  addr.sin_addr.S_addr := htonl(INADDR_ANY);
  addr.sin_port := htons(40);
  addr.sin_family := AF_INET;
(* přiřadíme našemu socketu tyto informace *)
 if bind(s, addr, sizeof(addr)) = 0 then
 begin
(* nastavíme socket jako naslouchací, tj. pro připojení *)
(* aby nás to nezahltilo, povolíme jen 10 počítačů současně *)
  if listen(s, 10) = 0 then
  begin
(* připravíme si zprávu, kterou pošleme každému klientovi *)
   str := 'Vítejte na serveru';
   len := length(str);
   move((@str[1])^, (@dat[1])^, len);
(* začneme naslouchat pro prvního klienta *)
   {$IFDEF LINUX}
   dummyInt := 0;
   c := accept(s, dummyPtr, dummyInt);
   {$ELSE}
   c := accept(s, nil, nil);
   {$ENDIF}
(* pokud nás to pustí sem, přijali jsme nějakého klienta *)
(* dostali jsme nový socket C, který je tedy obsazený *)
(* zatímco S může být použit pro další připojení *)
(* teď se zřejmě musí socket povolit, pokud běžíme pod Windows *)
   {$IFDEF WIN32}
   t := 1;
   ioctlsocket(c, FIONBIO, t);
   {$ENDIF}
(* pošleme klientovi uvítací zprávu *)
   send(c, dat, len, 0);
(* tady počkáme, až ji klinet obdrží, a až nám pošle něco on *)
   Delay(5000);
(* pokusíme se něco přijmout *)
   len := recv(c, dat, 50, 0);
   if len <> 0 then
   begin
    Setlength(str, len);
    move((@dat[1])^, (@str[1])^, len);
(* vypíšeme zprávu od klienta *)
    WriteLn(str);
   end;
(* budeme končit, už žádné další klienty nechceme *)
   {$IFDEF WIN32}
   t := 0;
   ioctlsocket(c, FIONBIO, t);
   {$ENDIF}
(* zavřeme jak socket našeho klienta, tak hlavní naslouchací *)
(* pokud bychom chtěli mít současně opravdu 10 klientů, museli
   bychom mít 10x proměnnou C, např. jako array *)
   closesocket(c);
   closesocket(s);
  end;
 end;
end;  

Tak takhle to celé funguje. Jistě jste si všimli, že v daném případě obsluhujeme jen jednoho klienta a jakmile se k nám spojí, tak mu něco pošleme, něco si přečteme od něj a skončíme. Kdybychom chtěli mít současně připojeno několik počítačů, museli bychom se po jeho obsluze vrátit k ACCEPT a čekat na dalšího klienta. Bohužel, současná implementace je trochu zrádná. ACCEPT (podobně jako CONNECT, viz. klient) totiž čeká, dokud se k němu někdo nepřipojí. Program stojí! To se hodí pro prvního klienta, ale ne pro ty další, protože bychom zatím nemohli obsloužit ty již připojené (protože se zde nepoužívají tzv. non-blocking sockety, a zatím jsem nenašel spolehlivý způsob, jak je nastavit).

Jsou dvě řešení: Buď bychom museli mít ACCEPT v hlavním těle programu, nechat si volat nějakou obsluhu přerušení (např. časovač) a v ní číst, zda nám nějací klienti neposlali data; nebo využít vlákna: V tomto případě je vhodné, aby se ACCEPT spustilo jako vlákno a v případě, kdy se k němu někdo připojí, aby se vytvořilo další vlákno pro ACCEPT, které bude paralelně čekat, zatímco my bychom obsluhovali daného klienta (v ideálním případě 1 klient na jedno vlákno; můžeme však ACCEPT otevřít jen jednou, které po připojení jen nastaví, že máme nového klienta, a pak se zase v něm spustí ACCEPT, zatímco jiné vlákno bude obsluhovat všechny již připojené klienty). Jinak totiž můžete přerušit ACCEPT pouze stiskem CTRL+C, čímž ovšem standardně ukončíte také celý program (navíc to není moc vhodné pro server, u kterého nemusí nikdo sedět). Nyní se podíváme na klienta:

begin
 s := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 if s <> 0 then
 begin
  FillChar(addr, sizeof(addr), 0);
(* zadáme adresu serveru, ke kterému se chceme připojit *)
(* můžeme to přečíst i přes funkce READKEY, atd. *)
(* pozor na to, že tady se pracuje s PChar, nikoliv Stringem *)
  {$IFDEF WIN32}
  addr.sin_addr.S_addr := inet_addr('192.168.1.1');
  {$ELSE}
  addr.sin_addr := StrToNetAddr('192.168.1.1');
  {$ENDIF}
(* opět port 40; kdybyste chtěli pro testování mít klienta spuštěného
   na stejném počítači jako server, dejte sem třeba číslo 41 *)
  addr.sin_port := htons(40);
  addr.sin_family := AF_INET;
(* pokusíme se připojit k serveru *)
(* pokud socket funguje, bude čekat, dokud se nespojí! *)
(* zde může být socket S použit jen pro 1 spojení *)
  if connect(s, addr, sizeof(addr)) = 0 then
  begin
(* server nám má poslat data, tak si je přečteme *)
(* čekání není na lokální síti potřeba, ale jistota je jistota *)
   Delay(100);
   len := recv(s, dat, 50, 0);
   if len <> 0 then
   begin
    Setlength(str, len);
    move((@dat[1])^, (@str[1])^, len);
(* a vypíšeme na obrazovku *)
    WriteLn(str);
   end;
(* nyní pošleme na oplátku něco my serveru *)
   str := 'Klient se hlásí...';
   len := length(str);
   move((@str[1])^, (@dat[1])^, len);
   send(s, dat, len, 0);
(* a protože už nic jiného nepotřebujeme, skončíme *)
   closesocket(s);
  end;
 end;
end;  

Toto je vůbec (skoro) nejjednodušší klient. Můžete si samozřejmě server upravit tak, že bude cyklicky číst data ze vstupu (existuje i funkce, která zjistí, jestli tam nějaká jsou), a pokud narazí na nějaký příkaz, např. %stop, tak se teprve ukončí. Jinak Vám může posílat např. všechny Vaše řetězce převedené na velká písmena, atd., prostě co si zamanete. Stejně tak klient by měl po odeslání zase cyklicky číst a pak čekat na vstup od uživatele. Můžete také vložit do "hry" tzv. timeout, tj. pokud nepřijde do určité doby od klienta/serveru něco, tak se prostě odpojíte.

A to je všechno. Šťastnou komunikaci!

2006-11-30 | Martin Lux
Reklamy: