การเขียนโปรแกรม Socket ใน Java: บทช่วยสอน
java.io
แพ็กเกจดั้งเดิม
และ NIO ซึ่งเป็นjava.nio
API ที่ไม่ปิดกั้น 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
มาดูตัวอย่างสั้น ๆ ที่เรียกใช้ HTTP GET กับเซิร์ฟเวอร์ HTTP HTTP มีความซับซ้อนมากกว่าที่ตัวอย่างของเราอนุญาต แต่เราสามารถเขียนโค้ดไคลเอนต์เพื่อจัดการกรณีที่ง่ายที่สุด: ขอทรัพยากรจากเซิร์ฟเวอร์และเซิร์ฟเวอร์จะตอบกลับและปิดสตรีม กรณีนี้ต้องใช้ขั้นตอนต่อไปนี้:
- สร้างซ็อกเก็ตไปยังเว็บเซิร์ฟเวอร์ที่ฟังบนพอร์ต 80
- ขอรับ a
PrintStream
ไปยังเซิร์ฟเวอร์และส่งคำขอGET PATH HTTP/1.0
ซึ่งPATH
ทรัพยากรที่ร้องขอบนเซิร์ฟเวอร์อยู่ที่ไหน/
ตัวอย่างเช่นถ้าเราต้องการที่จะเปิดรากของเว็บไซต์นั้นเส้นทางจะเป็น - รับ
InputStream
ไปยังเซิร์ฟเวอร์ห่อด้วย aBufferedReader
และอ่านการตอบกลับทีละบรรทัด
รายการ 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
เราได้กล่าวถึงฝั่งไคลเอ็นต์แล้วและโชคดีที่การสื่อสารของฝั่งเซิร์ฟเวอร์นั้นง่ายเหมือนกัน จากมุมมองที่เรียบง่ายกระบวนการมีดังนี้:
- สร้าง
ServerSocket
ระบุพอร์ตที่จะรับฟัง - Invoke the
ServerSocket
'saccept()
method to listen on the configured port for a client connection. - When a client connects to the server, the
accept()
method returns aSocket
through which the server can communicate with the client. This is the sameSocket
class that we used for our client, so the process is the same: obtain anInputStream
to read from the client and anOutputStream
write to the client. - 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. - Call the
ServerSocket
'saccept()
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 ServerSocketFactory
s 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(); } } }