Socketprogrammering in Java: een tutorial

Deze tutorial is een inleiding tot socketprogrammering in Java, beginnend met een eenvoudig client-servervoorbeeld waarin de basisfuncties van Java I / O worden gedemonstreerd. U maakt kennis met zowel het originele  java.io pakket als NIO, de niet-blokkerende I / O ( java.nio) API's die in Java 1.4 zijn geïntroduceerd. Ten slotte ziet u een voorbeeld dat Java-netwerken demonstreert zoals geïmplementeerd vanaf Java 7, in NIO.2.

Socketprogrammering komt neer op twee systemen die met elkaar communiceren. Over het algemeen bestaat netwerkcommunicatie in twee smaken: Transport Control Protocol (TCP) en User Datagram Protocol (UDP). TCP en UDP worden voor verschillende doeleinden gebruikt en beide hebben unieke beperkingen:

  • TCP is een relatief eenvoudig en betrouwbaar protocol waarmee een client verbinding kan maken met een server en de twee systemen kunnen communiceren. In TCP weet elke entiteit dat haar communicatiepayloads zijn ontvangen.
  • UDP is een verbindingsloos protocol en is goed voor scenario's waarbij je niet per se elk pakket nodig hebt om op zijn bestemming aan te komen, zoals mediastreaming.

Om het verschil tussen TCP en UDP te begrijpen, kunt u overwegen wat er zou gebeuren als u video streamt vanaf uw favoriete website en er frames verloren gaan. Wilt u liever dat de klant uw film vertraagt ​​om de ontbrekende frames te ontvangen of wilt u dat de video blijft spelen? Protocollen voor videostreaming maken doorgaans gebruik van UDP. Omdat TCP levering garandeert, is dit het protocol bij uitstek voor HTTP, FTP, SMTP, POP3, enzovoort.

In deze tutorial laat ik je kennismaken met socketprogrammering in Java. Ik presenteer een reeks client-servervoorbeelden die functies demonstreren van het originele Java I / O-framework, en ga vervolgens geleidelijk verder met het gebruik van functies die in NIO.2 zijn geïntroduceerd.

Old-school Java-sockets

In implementaties vóór NIO wordt de Java TCP-client-socketcode afgehandeld door de java.net.Socketklasse. De volgende code opent een verbinding met een server:

 Socket socket = nieuwe socket (server, poort); 

Zodra onze socketinstantie is verbonden met de server, kunnen we beginnen met het verkrijgen van invoer- en uitvoerstromen naar de server. Invoerstromen worden gebruikt om gegevens van de server te lezen, terwijl uitvoerstromen worden gebruikt om gegevens naar de server te schrijven. We kunnen de volgende methoden uitvoeren om invoer- en uitvoerstromen te verkrijgen:

InputStream in = socket.getInputStream (); OutputStream uit = socket.getOutputStream ();

Omdat dit gewone streams zijn, dezelfde streams die we zouden gebruiken om uit een bestand te lezen en ernaar te schrijven, kunnen we ze converteren naar de vorm die het beste aansluit bij ons gebruik. We kunnen bijvoorbeeld een omslag geven OutputStreammet een PrintStreamzodat we gemakkelijk tekst kunnen schrijven met methoden zoals println(). Voor een ander voorbeeld kunnen we de omhullen InputStreammet een BufferedReader, via een InputStreamReader, om gemakkelijk tekst te lezen met methoden zoals readLine().

download Download de broncode Broncode voor "Socketprogrammering in Java: een tutorial." Gemaakt door Steven Haines voor JavaWorld.

Voorbeeld van een Java-socketclient

Laten we eens kijken naar een kort voorbeeld dat een HTTP GET uitvoert op een HTTP-server. HTTP is geavanceerder dan ons voorbeeld toelaat, maar we kunnen clientcode schrijven om het eenvoudigste geval af te handelen: vraag een bron aan bij de server en de server retourneert het antwoord en sluit de stream. Deze case vereist de volgende stappen:

  1. Maak een socket voor de webserver die luistert op poort 80.
  2. Haal een PrintStreamnaar de server en stuur het verzoek GET PATH HTTP/1.0, waar PATHis de gevraagde bron op de server. Als we bijvoorbeeld de root van een website willen openen, is het pad /.
  3. Haal een InputStreamnaar de server, omwikkel het met een BufferedReaderen lees het antwoord regel voor regel.

Listing 1 toont de broncode voor dit voorbeeld.

Lijst 1. SimpleSocketClientExample.java

pakket com.geekcap.javaworld.simplesocketclient; importeer java.io.BufferedReader; importeer java.io.InputStreamReader; importeer java.io.PrintStream; importeer java.net.Socket; openbare klasse SimpleSocketClientExample {openbare statische leegte main (String [] args) {if (args.length <2) {System.out.println ("Gebruik: SimpleSocketClientExample"); System.exit (0); } String server = args [0]; String path = args [1]; System.out.println ("Inhoud van URL laden:" + server); probeer {// Maak verbinding met de server Socket-socket = nieuwe Socket (server, 80); // Maak invoer- en uitvoerstromen om van te lezen en om naar de server te schrijven PrintStream out = new PrintStream (socket.getOutputStream ()); BufferedReader in = nieuwe BufferedReader (nieuwe InputStreamReader (socket.getInputStream ())); // Volg het HTTP-protocol van GET HTTP / 1.0 gevolgd door een lege regel out.println ("GET" + pad + "HTTP / 1.0"); out.println (); // Lees gegevens van de server totdat we klaar zijn met het lezen van het document String line = in.readLine (); while (regel! = null) {System.out.println (regel); line = in.readLine (); } // Sluit onze streams in.close (); out.close (); socket.close (); } catch (uitzondering e) {e.printStackTrace (); }}}

Listing 1 accepteert twee opdrachtregelargumenten: de server waarmee verbinding moet worden gemaakt (ervan uitgaande dat we verbinding maken met de server op poort 80) en de bron die moet worden opgehaald. Het creëert een Socketdie naar de server verwijst en expliciet de poort specificeert 80. Het voert dan het commando uit:

KRIJG PAD HTTP / 1.0 

Bijvoorbeeld:

GET / HTTP / 1.0 

Wat is er zojuist gebeurd?

Wanneer u een webpagina van een webserver ophaalt www.google.com, gebruikt de HTTP-client DNS-servers om het adres van de server te vinden: het begint door de hoofddomeinserver te vragen naar het comdomein waar de gezaghebbende domeinnaamserver voor is www.google.com. Vervolgens vraagt ​​het die domeinnaamserver om het IP-adres (of adressen) voor www.google.com. Vervolgens opent het een socket voor die server op poort 80. (Of, als u een andere poort wilt definiëren, kunt u dit doen door een dubbele punt toe te voegen gevolgd door het poortnummer, bijvoorbeeld:. :8080) Ten slotte voert de HTTP-client uit de gespecificeerde HTTP werkwijze, zoals GET, POST, PUT, DELETE, HEADof OPTI/ONS. Elke methode heeft zijn eigen syntaxis. Zoals getoond in de bovenstaande codefragmenten, GETvereist de methode een pad gevolgd doorHTTP/version numberen een lege regel. Als we HTTP-headers wilden toevoegen, hadden we dit kunnen doen voordat we de nieuwe regel invoeren.

In Listing 1 hebben we een opgehaald OutputStreamen in een verpakt PrintStreamzodat we onze op tekst gebaseerde commando's gemakkelijker konden uitvoeren. Onze code kreeg een InputStream, verpakt dat in een InputStreamReader, die het omzette in a Reader, en vervolgens verpakt in een BufferedReader. We gebruikten de PrintStreamom onze GETmethode uit te voeren en gebruikten vervolgens de BufferedReaderom het antwoord regel voor regel te lezen totdat we een nullantwoord ontvingen , wat aangeeft dat de socket was gesloten.

Voer nu deze klasse uit en geef deze de volgende argumenten door:

java com.geekcap.javaworld.simplesocketclient.SimpleSocketClientExample www.javaworld.com / 

Je zou een output moeten zien die lijkt op wat hieronder staat:

Inhoud van URL laden: www.javaworld.com HTTP / 1.1 200 OK Datum: Sun, 21 Sep 2014 22:20:13 GMT Server: Apache X-Gas_TTL: 10 Cache-Control: max-age = 10 X-GasHost: gas2 .usw X-Cooking-With: Gasoline-Local X-Gasoline-Age: 8 Content-Length: 168 Laatst gewijzigd: Tue, 24 Jan 2012 00:09:09 GMT Etag: "60001b-a8-4b73af4bf3340" Content-Type : text / html Vary: Accept-Encoding Connection: sluit de benzinetestpagina

Succes

Deze output toont een testpagina op de website van JavaWorld. Het antwoordde terug dat het HTTP-versie 1.1 spreekt en het antwoord is 200 OK.

Voorbeeld van een Java-socketserver

We hebben de clientkant behandeld en gelukkig is het communicatieaspect van de serverkant net zo eenvoudig. Vanuit een simplistisch perspectief is het proces als volgt:

  1. Maak een ServerSocket, specificeer een poort om op te luisteren.
  2. Invoke the ServerSocket's accept() method to listen on the configured port for a client connection.
  3. When a client connects to the server, the accept() method returns a Socket through which the server can communicate with the client. This is the same Socket class that we used for our client, so the process is the same: obtain an InputStream to read from the client and an OutputStream write to the client.
  4. If you server needs to be scalable, you will want to pass the Socket to another thread to process so that your server can continue listening for additional connections.
  5. Call the ServerSocket's accept() method again to listen for another connection.

As you'll soon see, NIO's handling of this scenario would be a bit different. For now, though, we can directly create a ServerSocket by passing it a port to listen on (more about ServerSocketFactorys in the next section):

 ServerSocket serverSocket = new ServerSocket( port ); 

And now we can accept incoming connections via the accept() method:

 Socket socket = serverSocket.accept(); // Handle the connection ... 

Multithreaded programming with Java sockets

Listing 2, below, puts all of the server code so far together into a slightly more robust example that uses threads to handle multiple requests. The server shown is an echo server, meaning that it echoes back any message it receives.

While the example in Listing 2 isn't complicated it does anticipate some of what's coming up in the next section on NIO. Pay special attention to the amount of threading code we have to write in order to build a server that can handle multiple simultaneous requests.

Listing 2. SimpleSocketServer.java

package com.geekcap.javaworld.simplesocketclient; import java.io.BufferedReader; import java.io.I/OException; import java.io.InputStreamReader; import java.io.PrintWriter; import java.net.ServerSocket; import java.net.Socket; public class SimpleSocketServer extends Thread { private ServerSocket serverSocket; private int port; private boolean running = false; public SimpleSocketServer( int port ) { this.port = port; } public void startServer() { try { serverSocket = new ServerSocket( port ); this.start(); } catch (I/OException e) { e.printStackTrace(); } } public void stopServer() { running = false; this.interrupt(); } @Override public void run() { running = true; while( running ) { try { System.out.println( "Listening for a connection" ); // Call accept() to receive the next connection Socket socket = serverSocket.accept(); // Pass the socket to the RequestHandler thread for processing RequestHandler requestHandler = new RequestHandler( socket ); requestHandler.start(); } catch (I/OException e) { e.printStackTrace(); } } } public static void main( String[] args ) { if( args.length == 0 ) { System.out.println( "Usage: SimpleSocketServer " ); System.exit( 0 ); } int port = Integer.parseInt( args[ 0 ] ); System.out.println( "Start server on port: " + port ); SimpleSocketServer server = new SimpleSocketServer( port ); server.startServer(); // Automatically shutdown in 1 minute try { Thread.sleep( 60000 ); } catch( Exception e ) { e.printStackTrace(); } server.stopServer(); } } class RequestHandler extends Thread { private Socket socket; RequestHandler( Socket socket ) { this.socket = socket; } @Override public void run() { try { System.out.println( "Received a connection" ); // Get input and output streams BufferedReader in = new BufferedReader( new InputStreamReader( socket.getInputStream() ) ); PrintWriter out = new PrintWriter( socket.getOutputStream() ); // Write out our header to the client out.println( "Echo Server 1.0" ); out.flush(); // Echo lines back to the client until the client closes the connection or we receive an empty line String line = in.readLine(); while( line != null && line.length() > 0 ) { out.println( "Echo: " + line ); out.flush(); line = in.readLine(); } // Close our connection in.close(); out.close(); socket.close(); System.out.println( "Connection closed" ); } catch( Exception e ) { e.printStackTrace(); } } }