Implementatie van een HTTP-server mbv sockets Computernetwerken deel 2 2008-2009
Een socket is het eindpunt van een netwerkverbinding. Als tussen twee computers een TCP-verbinding wordt opgezet, kunnen beide systemen een socket gebruiken om via de verbinding te communiceren. Ze lezen van en schrijven naar de socket, net zoals ze dat van en naar een bestand zouden doen. Dit maakt het werken met sockets voor de programmeur relatief eenvoudig.
Een client Een client maakt verbinding met een server op een gekend adres en een gekende poort. In Java kan dit met behulp van de klasse java.net.Socket. In het volgende eenvoudige voorbeeld wordt een verbinding opgezet met een server op het IP-adres 12.34.56.78 en poort 1234. 1 2
try { Socket cSock = new Socket ( " 12.34.56.78 " , 1234) ;
3 4 5 6
BufferedReader in = new BufferedReader ( new I nputSt reamRe ader ( cSock . getInputStream () ) ) ;
7 8 9
PrintWriter out = new PrintWriter ( cSock . getOutputStream () , true ) ;
10 11 12 13
out . println ( " Hallo ! " ) ; String antwoord = in . readLine () ; System . out . println ( " Antwoord : " + antwoord ) ;
14 15 16 17 18 19 20 21 22
in . close () ; out . close () ; cSock . close () ; } catch ( IOException e ) { System . err . println ( " IOException : " + e . getMessage () ) ; System . exit ( -1) ; }
1
Op lijn 2 wordt de socket gecre¨eerd, met als parameters het IP-adres van de server en het poortnummer. In plaats van het IP-adres kan ook een computernaam worden meegegeven. Een Socket-object beschikt over een InputStream en een OutputStream. Via de OutputStream kunnen gegevens naar de server sturen, en de InputStream laat toe om gegevens te ontvangen. Om het werken met de in- en uitvoer van de socket te vereenvoudigen gebruiken we een BufferedReader en een PrintWriter (lijn 5 tot 8). Dit toont de grote overeenkomst tussen werken met bestanden en werken met sockets. Merk op dat we op lijn 8 bij het aanroepen van de PrintWriter-constructor via de boolean true zorgen dat de autoflush-functie aanstaat. Hierdoor wordt de inhoud van de buffer verzonden na het aanroepen van println(), en wordt er niet gewacht tot de buffer vol is. Als autoflush uitstaat wordt er pas data verzonden als de buffer vol is. Dit is effici¨enter, omdat er minder segmenten verstuurd worden, maar je moet opletten dat je programma niet blijft wachten als er geen data meer komt. Als de verbinding niet meer nodig is worden op lijn 14 tot 16 eerst de datastromen gesloten, en daarna de socket.
Een server Een server opent een poort en wacht tot een client de verbinding opzet. Het verschil met de client-code is dat er een ServerSocket gecre¨eerd moet worden, en dat hierbij geen IP-adres moet meegegeven worden. De oproep van de accept() methode blokkeert tot er een verbinding is opgezet, waarna een Socket-object teruggegeven wordt. Met deze socket kunnen we op dezelfde manier werken als bij de client. Zolang er geen verbinding wordt opgezet door een client blijft het server-programma dus wachten op lijn 4. De server in dit voorbeeld leest ´e´en lijn uit de verbinding, en stuurt deze terug naar de client. Hierna wordt de verbinding afgesloten. 1 2
try { ServerSocket sSock = new ServerSocket (1234) ;
3 4
Socket sock = sSock . accept () ;
5 6 7 8 9 10
BufferedReader in = new BufferedReader ( new I nputSt reamRe ader ( sock . getInputStream () ) ) ; PrintWriter out = new PrintWriter ( sock . getOutputStream () , true ) ;
11 12
String input = in . readLine () ;
2
out . println ( input ) ;
13 14
in . close () ; out . close () ; sock . close () ; sSock . close () ;
15 16 17 18 19 20 21 22 23
} catch ( IOException e ) { System . err . println ( " IOException : " + e . getMessage () ) ; System . exit ( -1) ; }
Een bruikbare server mag natuurlijk niet stoppen met werken na het afhandelen van slechts ´e´en verbinding. Een eenvoudige oplossing is de volledige inhoud van het try-blok in een lus zetten. In een meer realistische aanpak wordt het afhandelen van de verbinding in een aparte thread gestart zodat de server onmiddellijk andere verbindingen kan accepteren. Bovenstaande code is geschikt voor het versturen en ontvangen van tekst via de opgezette TCP-verbinding. Om bv. ook zonder problemen andere gegevens te versturen kan je de PrintWriter vervangen door een DataOutputStream, en de print-methode door writeBytes.
Protocollen Als 2 systemen met mekaar willen communiceren moeten ze hetzelfde protocol spreken. Een protocol is een verzameling regels waaraan beide partijen zich moeten houden. Op het internet wordt gebruik gemaakt van het Internet Protocol (IP), en daarboven het Transmission Control Protocol (TCP) of het User Datagram Protocol (UDP). Deze protocollen maken communicatie mogelijk, maar zeggen niets over de inhoud ervan.
Applicatieprotocollen Veel internet-protocollen worden gedefini¨eerd in zogenaamde Request For Comments (RFC’s). RFC’s beschrijven allerlei aspecten van het internet, van informele richtlijnen tot strikte protocolspecificaties1 , en niet alle RFC’s zijn standaarden. Enkele bekende standaarden zijn: Enkele voorbeelden: HTTP HyperText Transfer Protocol (RFC 2616) 1
Er zijn zelfs enkele humoristische RFC’s, de meest bekenden beschrijven het versturen van IP-pakketten m.b.v. postduiven (RFC 1149) en het introduceren van een “Evil Bit” in de IP-hoofding om aan te geven dat een pakket slechte bedoelingen heeft (RFC 3514).
3
ESMTP Extended Simple Mail Transfer Protocol (RFC 2821) DHCP Dynamic Host Configuration Protocol (RFC 2131) Voor meer info en de RFC’s zelf, zie http://www.rfc-editor.org. Niet alle protocollen zijn open standaarden. Microsofts MSN protocol of de specificatie van het Skype protocol worden niet publiek gemaakt. Om software te schrijven die met deze diensten communiceert moet men het protocol analyseren, vaak “reverse engineering” genoemd. Op http://www.hypothetic. org/docs/msn/ vind je een voorbeeld van een analyse van het MSN-protocol. Als we de voorbeeld-server van hierboven op TCP poort 7 zouden starten, hebben we ´e´en van de eenvoudigste internet-standaarden ge¨ımplementeerd: het Echo-protocol uit RFC 862. In deze RFC staat: Echo uses UDP and TCP port 7 and is used as a debgging tool to send any datagrams received from a source, back to that source. The risk with this is that someone who has access to the network can overload devices via the Echo service amounting to a DoS attack. Discard (RFC 863), Chargen (RFC 864) en Daytime (RFC 867) beschrijven andere testhulpmiddelen. Deze protocollen worden niet vaak meer gebruikt, het testen van een netwerk gebeurt nu meestal met programm’s als ping en traceroute.
HyperText Transfer Protocol (HTTP) De meest recente specificatie van het HyperText Transfer Protocol (HTTP) is beschreven in RFC 2616. Hieronder een voorbeeld van een (vereenvoudigde) HTTP-sessie tussen een Firefox Browser en een Apache HTTP server. 1 2 3
4
5 6 7 8 9
GET / rfcs / rfc2616 . html HTTP /1.1 Host : www . faqs . org User - Agent : Mozilla /5.0 ( X11 ; U ; Linux i686 ; en - US ; rv :1.9.0.3) Gecko /2008092816 Iceweasel /3.0.3 ( Debian -3.0.3 -3) Accept : text / html , application / xhtml + xml , application / xml ; q =0.9 ,*/*; q =0.8 Accept - Language : en - us , en ; q =0.5 Accept - Encoding : gzip , deflate Accept - Charset : ISO -8859 -1 , utf -8; q =0.7 ,*; q =0.7 Keep - Alive : 300 Connection : keep - alive
10
4
11 12 13
14 15 16
HTTP /1.1 200 OK Date : Mon , 20 Oct 2008 10:02:44 GMT Server : Apache /2.2.9 ( Unix ) mod_ssl /2.2.9 OpenSSL /0.9.7 a Connection : close Transfer - Encoding : chunked Content - Type : text / html
17 18 19 20 21
22
< HTML > < HEAD > < TITLE > RFC 2616 ( rfc2616 ) - Hypertext Transfer Protocol -- HTTP /1.1 TITLE > ... Voor de details van het protocol verwijzen we naar de RFC, maar in dit voorbeeld zie je van lijn 1 tot 9 de HTTP Request die door de client (browser) naar de server gestuurd wordt. Lijn 1 is de eigenlijke ’request’, de overige lijnen zijn headers met extra informatie. Dmv de lege lijn 10 geeft de client aan dat hij klaar is en wacht op het antwoord van de server. Het antwoord van de server bestaat uit een Response (lijn 11), gevolgd door header-lijnen (12-16), een lege lijn en de response body. In dit geval is de response body de HTML-code van de opgevraagde website.
Opgave: Schrijf een eenvoudige HTTP-server Schrijf een HTTP-server in Java. Hou rekening met de volgende punten: • Alle bestanden en subdirs in een opgegeven directory worden via de HTTPserver ter beschikking gesteld, bijvoorbeeld /tmp/webroot of c:\www • Zowel tekst als afbeeldingen moeten correct doorgegeven worden • Als het verzoek van de client een directory opvraagt moet de server ervan uitgaan dat het om het bestand index.html in die directory gaat • Als een opgevraagd bestand niet bestaat antwoordt de server met een ”404 Not Found”(10.4.5) • Als een opgrevraagd bestand niet toegankelijk is voor de server is het het resultaat een ”403 Forbidden”(10.4.4) • De GET en HEAD methode moeten gemplementeerd worden, op andere verzoeken wordt met een ”501 Unimplemented Method”gereageerd (zie 5.1.1) 5
• De Host-header is verplicht. Als de client die niet meegeeft moet een ’400 Bad Request’ code teruggegeven worden (zie 5.2) • De server moet meerdere requests na mekaar kunnen behandelen, maar niet noodzakelijk persistent (i.e. in dezelfde TCP-verbinding) (zie 8.1) • De Content-length header moet correct ingevuld worden in de response (zie 14.3) • Je mag enkel sockets gebruiken en moet het HTTP-protocol zelf implementeren. Nog enkele tips: • Testen kan met netcat of een browser. In Firefox kan je met o.a. de LiveHttpHeaders-plugin de conversatie tussen de browser en de server bekijken. Voor Internet Explorer bestaan gelijkaardige hulpmiddelen. Merk op dat de laatste versie van IE • Begin met een eenvoudige webserver die steeds hetzelfde antwoord teruggeeft, bv. HTTP /1.1 200 OK Content - length : 112 < html > < head > < title > Test title > head > < body > Hello
World b >! body > html > Als dat lukt kan je beginnen met het laden van de inhoud uit een bestand en het bekijken van de request van de client. • Sluit je sockets af na gebruik, anders krijg je problemen als je het programma opnieuw wilt opstarten (bv. Socket in use) • Als je geen administrator/root bent moet je poorten boven 1024 gebruiken
6