การเขียนโปรแกรม Socket ใน Java: บทช่วยสอน

บทช่วยสอนนี้เป็นบทนำเกี่ยวกับการเขียนโปรแกรมซ็อกเก็ตใน Java โดยเริ่มจากตัวอย่างไคลเอนต์เซิร์ฟเวอร์แบบง่ายที่แสดงคุณสมบัติพื้นฐานของ Java I / O คุณจะได้รู้จักกับทั้งjava.io แพ็กเกจดั้งเดิม  และ NIO ซึ่งเป็นjava.nioAPI ที่ไม่ปิดกั้น I / O ( ) ที่เปิดตัวใน Java 1.4 สุดท้ายคุณจะเห็นตัวอย่างที่แสดงให้เห็นถึงเครือข่าย Java ที่ใช้งานจาก Java 7 ไปข้างหน้าใน NIO 2

การเขียนโปรแกรมซ็อกเก็ตจะทำให้ระบบสองระบบสื่อสารกัน โดยทั่วไปการสื่อสารบนเครือข่ายมีสองรสชาติ: Transport Control Protocol (TCP) และ User Datagram Protocol (UDP) TCP และ UDP ใช้เพื่อวัตถุประสงค์ที่แตกต่างกันและทั้งสองมีข้อ จำกัด เฉพาะ:

  • TCP เป็นโปรโตคอลที่ค่อนข้างง่ายและเชื่อถือได้ซึ่งช่วยให้ไคลเอนต์สามารถเชื่อมต่อกับเซิร์ฟเวอร์และทั้งสองระบบเพื่อสื่อสารกัน ใน TCP แต่ละเอนทิตีรู้ว่าได้รับเพย์โหลดการสื่อสารแล้ว
  • UDP เป็นโปรโตคอลที่ไม่มีการเชื่อมต่อและเหมาะสำหรับสถานการณ์ที่คุณไม่จำเป็นต้องให้ทุกแพ็กเก็ตไปถึงปลายทางเช่นการสตรีมสื่อ

หากต้องการชื่นชมความแตกต่างระหว่าง TCP และ UDP ให้พิจารณาว่าจะเกิดอะไรขึ้นหากคุณสตรีมวิดีโอจากเว็บไซต์โปรดของคุณและเฟรมหลุด คุณต้องการให้ไคลเอ็นต์ชะลอภาพยนตร์ของคุณเพื่อรับเฟรมที่ขาดหายไปหรือคุณต้องการให้วิดีโอเล่นต่อไป? โดยทั่วไปแล้วโปรโตคอลการสตรีมวิดีโอจะใช้ประโยชน์จาก UDP เนื่องจาก TCP รับประกันการส่งมอบจึงเป็นโปรโตคอลที่เลือกใช้สำหรับ HTTP, FTP, SMTP, POP3 และอื่น ๆ

ในบทช่วยสอนนี้ฉันแนะนำคุณเกี่ยวกับการเขียนโปรแกรมซ็อกเก็ตใน Java ฉันนำเสนอชุดของตัวอย่างไคลเอ็นต์เซิร์ฟเวอร์ที่แสดงคุณสมบัติจากเฟรมเวิร์ก Java I / O ดั้งเดิมจากนั้นค่อยๆก้าวไปสู่การใช้คุณสมบัติที่แนะนำใน NIO 2

ซ็อกเก็ต Java แบบเก่า

ในการนำไปใช้ก่อน NIO โค้ดซ็อกเก็ตไคลเอ็นต์ Java TCP จะถูกจัดการโดยjava.net.Socketคลาส รหัสต่อไปนี้เปิดการเชื่อมต่อกับเซิร์ฟเวอร์:

 ซ็อกเก็ตซ็อกเก็ต = ซ็อกเก็ตใหม่ (เซิร์ฟเวอร์พอร์ต); 

เมื่อsocketอินสแตนซ์ของเราเชื่อมต่อกับเซิร์ฟเวอร์แล้วเราสามารถเริ่มรับอินพุตและเอาต์พุตสตรีมไปยังเซิร์ฟเวอร์ได้ อินพุตสตรีมใช้เพื่ออ่านข้อมูลจากเซิร์ฟเวอร์ในขณะที่เอาต์พุตสตรีมใช้เพื่อเขียนข้อมูลไปยังเซิร์ฟเวอร์ เราสามารถดำเนินการตามวิธีการต่อไปนี้เพื่อรับอินพุตและเอาต์พุตสตรีม:

InputStream ใน = socket.getInputStream (); OutputStream ออก = socket.getOutputStream ();

เนื่องจากสตรีมเหล่านี้เป็นสตรีมธรรมดาซึ่งเป็นสตรีมเดียวกับที่เราจะใช้อ่านและเขียนไปยังไฟล์เราจึงสามารถแปลงให้อยู่ในรูปแบบที่ตอบสนองการใช้งานของเราได้ดีที่สุด ตัวอย่างเช่นเราสามารถห่อOutputStreamด้วย a PrintStreamเพื่อให้เราสามารถเขียนข้อความด้วยวิธีการเช่นprintln(). อีกตัวอย่างหนึ่งเราสามารถปิดInputStreamด้วย a BufferedReaderผ่าน an InputStreamReaderเพื่อให้อ่านข้อความได้ง่ายด้วยวิธีการเช่นreadLine().

ดาวน์โหลดดาวน์โหลดซอร์สโค้ดซอร์สโค้ดสำหรับ "การเขียนโปรแกรมซ็อกเก็ตใน Java: A Tutorial" สร้างโดย Steven Haines สำหรับ JavaWorld

ตัวอย่างไคลเอ็นต์ซ็อกเก็ต Java

มาดูตัวอย่างสั้น ๆ ที่เรียกใช้ HTTP GET กับเซิร์ฟเวอร์ HTTP HTTP มีความซับซ้อนมากกว่าที่ตัวอย่างของเราอนุญาต แต่เราสามารถเขียนโค้ดไคลเอนต์เพื่อจัดการกรณีที่ง่ายที่สุด: ขอทรัพยากรจากเซิร์ฟเวอร์และเซิร์ฟเวอร์จะตอบกลับและปิดสตรีม กรณีนี้ต้องใช้ขั้นตอนต่อไปนี้:

  1. สร้างซ็อกเก็ตไปยังเว็บเซิร์ฟเวอร์ที่ฟังบนพอร์ต 80
  2. ขอรับ a PrintStreamไปยังเซิร์ฟเวอร์และส่งคำขอGET PATH HTTP/1.0ซึ่งPATHทรัพยากรที่ร้องขอบนเซิร์ฟเวอร์อยู่ที่ไหน /ตัวอย่างเช่นถ้าเราต้องการที่จะเปิดรากของเว็บไซต์นั้นเส้นทางจะเป็น
  3. รับInputStreamไปยังเซิร์ฟเวอร์ห่อด้วย a BufferedReaderและอ่านการตอบกลับทีละบรรทัด

รายการ 1 แสดงซอร์สโค้ดสำหรับตัวอย่างนี้

รายการ 1. SimpleSocketClientExample.java

แพ็คเกจ com.geekcap.javaworld.simplesocketclient; นำเข้า java.io.BufferedReader; นำเข้า java.io.InputStreamReader; นำเข้า java.io.PrintStream; นำเข้า java.net.Socket; คลาสสาธารณะ SimpleSocketClientExample {public static void main (String [] args) {if (args.length <2) {System.out.println ("Usage: SimpleSocketClientExample"); System.exit (0); } เซิร์ฟเวอร์สตริง = args [0]; เส้นทางสตริง = args [1]; System.out.println ("กำลังโหลดเนื้อหาของ URL:" + เซิร์ฟเวอร์); ลอง {// เชื่อมต่อกับเซิร์ฟเวอร์ Socket Socket = ซ็อกเก็ตใหม่ (เซิร์ฟเวอร์ 80); // สร้างสตรีมอินพุตและเอาต์พุตเพื่ออ่านและเขียนไปยังเซิร์ฟเวอร์ PrintStream out = PrintStream ใหม่ (socket.getOutputStream ()); BufferedReader ใน = BufferedReader ใหม่ (InputStreamReader ใหม่ (socket.getInputStream ())); // ปฏิบัติตามโปรโตคอล HTTP ของ GET HTTP / 10 ตามด้วยบรรทัดว่าง out.println ("GET" + path + "HTTP / 1.0"); out.println (); // อ่านข้อมูลจากเซิร์ฟเวอร์จนกว่าเราจะอ่านเอกสาร String line = in.readLine (); ในขณะที่ (line! = null) {System.out.println (line); บรรทัด = in.readLine (); } // ปิดสตรีมของเรา in.close (); out.close (); socket.close (); } จับ (ข้อยกเว้นจ) {e.printStackTrace (); }}}

รายการ 1 ยอมรับอาร์กิวเมนต์บรรทัดคำสั่งสองรายการ: เซิร์ฟเวอร์ที่จะเชื่อมต่อ (สมมติว่าเรากำลังเชื่อมต่อกับเซิร์ฟเวอร์บนพอร์ต 80) และทรัพยากรที่จะดึงข้อมูล มันจะสร้างที่จุดไปยังเซิร์ฟเวอร์และชัดเจนระบุพอร์ตSocket 80จากนั้นรันคำสั่ง:

รับเส้นทาง HTTP / 1.0 

ตัวอย่างเช่น:

รับ / HTTP / 1.0 

เกิดอะไรขึ้น?

เมื่อคุณดึงหน้าเว็บจากเว็บเซิร์ฟเวอร์เช่นwww.google.comไคลเอนต์ HTTP ใช้เซิร์ฟเวอร์ DNS เพื่อค้นหาที่อยู่ของเซิร์ฟเวอร์โดยเริ่มต้นด้วยการขอเซิร์ฟเวอร์โดเมนระดับบนสุดสำหรับโดเมนที่เซิร์ฟเวอร์ชื่อcomโดเมนที่เชื่อถือได้ใช้สำหรับwww.google.com. จากนั้นก็จะถามว่าเซิร์ฟเวอร์ชื่อโดเมนสำหรับที่อยู่ IP (หรือที่อยู่) www.google.comสำหรับจากนั้นจะเปิดซ็อกเก็ตไปยังเซิร์ฟเวอร์นั้นบนพอร์ต 80 (หรือหากคุณต้องการกำหนดพอร์ตอื่นคุณสามารถทำได้โดยการเพิ่มโคลอนตามด้วยหมายเลขพอร์ตตัวอย่างเช่น:) :8080สุดท้ายไคลเอ็นต์ HTTP จะดำเนินการ วิธี HTTP ที่ระบุเช่นGET, POST, PUT, DELETE, หรือHEAD OPTI/ONSแต่ละวิธีมีไวยากรณ์ของตัวเอง ดังที่แสดงในสนิปโค้ดด้านบนGETเมธอดต้องใช้เส้นทางตามด้วยHTTP/version numberและบรรทัดว่าง หากเราต้องการเพิ่มส่วนหัว HTTP เราสามารถทำได้ก่อนที่จะเข้าสู่บรรทัดใหม่

ในรายการ 1 เราดึงข้อมูลOutputStreamและรวมเข้าด้วยกันPrintStreamเพื่อให้เราสามารถเรียกใช้คำสั่งแบบข้อความของเราได้ง่ายขึ้น รหัสของเราได้รับ an InputStreamห่อด้วยInputStreamReaderซึ่งแปลงเป็น a Readerแล้วห่อด้วยไฟล์BufferedReader. เราใช้PrintStreamเพื่อดำเนินการตามGETวิธีการของเราจากนั้นใช้BufferedReaderเพื่ออ่านคำตอบทีละบรรทัดจนกว่าเราจะได้รับการnullตอบกลับซึ่งแสดงว่าซ็อกเก็ตถูกปิดแล้ว

ตอนนี้เรียกใช้คลาสนี้และส่งผ่านอาร์กิวเมนต์ต่อไปนี้:

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

คุณควรเห็นผลลัพธ์ที่คล้ายกับด้านล่าง:

กำลังโหลดเนื้อหาของ URL: www.javaworld.com HTTP / 1.1 200 OK Date: 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 Last-Modified: Tue, 24 Jan 2012 00:09:09 GMT Etag: "60001b-a8-4b73af4bf3340" Content-Type : text / html Vary: การเชื่อมต่อที่ยอมรับการเข้ารหัส: ปิดหน้าทดสอบน้ำมันเบนซิน

ประสบความสำเร็จ

ผลลัพธ์นี้แสดงหน้าทดสอบบนเว็บไซต์ของ JavaWorld มันตอบกลับมาว่ามันพูดถึงรุ่นของ HTTP 1.1 200 OKและการตอบสนองเป็น

ตัวอย่างเซิร์ฟเวอร์ซ็อกเก็ต Java

เราได้กล่าวถึงฝั่งไคลเอ็นต์แล้วและโชคดีที่การสื่อสารของฝั่งเซิร์ฟเวอร์นั้นง่ายเหมือนกัน จากมุมมองที่เรียบง่ายกระบวนการมีดังนี้:

  1. สร้างServerSocketระบุพอร์ตที่จะรับฟัง
  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(); } } }