Prickle-Prickle, the 73 day of Confusion in the YOLD 3175
Multi-user chat, SCTP och nätverksbibliotek
En kväll i mars skrev jag ett enkelt chatprogram för flera användare som använder det relativt nya transportprotokollet Stream Control Transmission Protocol (SCTP), definierad i RFC 2960. SCTP har en rad skillnader jämfört med TCP, där jag tycker de viktigaste är:
Det bevarar postgränser: En read() hos mottagaren motsvarar alltid en write() hos avsändaren. TCP fungerar, som bekant, i stället som en byteström och en read() kan motsvara flera write() eller en read() kanske bara ger en del av den data som skrevs med en write(). Det går inte att veta på förhand.
Man måste ofta när man använder TCP lägga till en nivå i sitt protokoll på tillämpningsnivå för att skilja på olika poster. I SCTP slipper man alltså det. Win!
- Inbyggd multihoming: SCTP berättar i uppkopplingsfasen för den andra sidan om alla IP-adresser som noden har. Om en av IP-adresserna blir onåbar kan kommunikationen ändå fortsätta på en annan av nodens IP-adresser. Speciellt i IPv6-fallet, där traditionell multihoming med BGP normalt inte finns, är detta relevant. Big win!
SCTP kan också användas i stället för UDP, om man vill, men jag har hittills bara använt det som TCP-ersättning.
Jag ville med mitt lilla program undersöka hur SCTP fungerade från programmerarens synvinkel och se hur pass komplicerat det var att få det att fungera. Det visade sig att det inte alls är komplicerat eftersom det enda som skiljer mitt program när jag testade med TCP respektive SCTP är en enda rad! Bortsett från det under FreeBSD inte strikt nödvändiga
#include <netinet/sctp.h>
behövde jag bara:
listensock = socket(AF_INET, SOCK_STREAM, IPPROTO_SCTP);
med IPPROTO_SCTP
i stället för IPPROTO_TCP
. Så enkelt är det alltså
att börja använda SCTP.
Nu använde jag visserligen inte de SCTP-specifika funktionerna som finns i det utökade sockets-API:et, utan använde vanliga read() och write(), så på sätt och vis kanske jag fuskade. Hur som helst fungerade det bra.
David Westlund skrev en trivial klient för att fungera med servern och använde för variation poll() i stället för select(). Resultatet finns som
Nätbibliotek
Servern i chatsystemet ovan är som du ser i koden en klassisk select()-snurra. Det är inte helt optimalt i dessa dagar, då det finns sådana systemanrop som det nu närmast allestädes närvarande poll() men också mycket mer effektiva lösningar för samma sak som Linux epoll(), BSD:ernas kqueue()/kevent(), Solaris /dev/poll och allt vad de heter, som alla är antingen mer effektiva än select() eller i alla fall inte har dess begränsningar.
Eftersom skillnaden i effektivitet ofta är så stor finns det en poäng att använda de för systemen unika systemanropen i stället för select() eller ens poll(). Den senares enda fördel framför select() är ju att den inte är på förhand begränsad i hur många fildeskriptorer den kan hantera. Detta ställer förstås till det lite för den stackars programmeraren, som då måste anpassa sitt program för alla plattformar hän vill köra det på.
Jag har flera gånger skrivit bibliotek med nätverksfunktioner för att underlätta konstruktionen av serverprogram. Tyvärr har koden jag skrivit inte kunnat släppas fri och jag har därför blivit tvungen att återimplementera ett motsvarande bibliotek några gånger. Typiskt använde man mina bibliotek ungefär så här:
int parsestuff(void *pcon, short flags) { /* Handle data when it arrives. */ } int main(void) { struct connection *con; con = opensocket("foo.example.com:4711", TCP, parsestuff, 1024); for (;;) { incoming(NULL); } }
Poängen här är alltså att skriva minimalt med kod i stället för select()-snurran och allt det som hör till bindande av adresser, et cetera. I stället finns bara en funktion där jag registrerar en callback-funktion och den funktionen kallas sedan på så fort det finns data att hantera.
Motsvarande på klientsidan skulle se snarlik ut, fast där skulle förstås anropet motsvarande opensocket() aktivt etablera en förbindelse i stället för att passivt lyssna efter nya förbindelser.
Det finns i den fria världen nu för tiden bibliotek som gör nästan det som mina tidigare bibliotek gjort. Två exempel är BSD-licensierade libevent och LGPL-licensierade liboop.
För ett tag sedan slog det mig att jag skulle se om jag kunde använda libevent och skriva något ovanpå som kan vara agnostiskt med avseende på IPv4 och IPv6 och kanske också stödja UDP, TCP och rent av SCTP.
Jag skrev alltså om den multi-user chat jag skriver om ovan, fast använde nu i stället libevent i stället för min select()-snurra. Jag skrev dock bara server-sidan. Se här:
Det finns i den här koden ingen som helst TELNET-förhandling, så om du kopplar upp dig mot event-servern med en telnet-klient så beror det i hög grad på din klient hur det kommer att upplevas. Windows telnet-klient, till exempel, defaultar till tecken-för-tecken, så det betyder att varje tecken kommer att skickas till alla inloggade direkt! Det blir snabbt väldigt förvirrande för alla inblandade, så gör inte det.
Telnet-klienterna i Linux och BSD:erna uppför sig annorlunda, men den är möjligen inte åttabitarsren, så svenska tecken kommer att se skumma ut. Ett bättre sätt att koppla upp sig är kanske att använda netcat eller övertyga sin telnet-klient om att inte göra någon som helst förhandling och vara åttabitarsren. Du får titta i manualen för din telnet-klient hur du gör det.
I koden för event-servern finns raden
/* Undef this for TCP operation. */ /*#define SCTP 1 */
som du kan okommentera för att för kompilera allt med stöd för SCTP i stället för TCP. På min utvecklingsplattform, FreeBSD, fungerar det emellertid av någon anledning inte med SCTP, men jag har ännu inte hittat felet. Kanske kan någon annan göra det?
Jag använder libevent med de automagiska buffrarna, som alltså skrivs ut till mottagaren så fort det råkar finnas något i dem. Det kanske är det som ställer till det i SCTP-fallet?