คู่มือ Java Sockets

1. ภาพรวม

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

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

ความแตกต่างหลักระหว่างทั้งสองคือ UDP ไม่มีการเชื่อมต่อซึ่งหมายความว่าไม่มีเซสชันระหว่างไคลเอนต์และเซิร์ฟเวอร์ในขณะที่ TCP มุ่งเน้นการเชื่อมต่อซึ่งหมายความว่าต้องสร้างการเชื่อมต่อพิเศษระหว่างไคลเอนต์และเซิร์ฟเวอร์ก่อนเพื่อให้การสื่อสารเกิดขึ้น

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

2. การตั้งค่าโครงการ

Java จัดเตรียมคอลเลกชันของคลาสและอินเทอร์เฟซที่ดูแลรายละเอียดการสื่อสารระดับต่ำระหว่างไคลเอ็นต์และเซิร์ฟเวอร์

สิ่งเหล่านี้ส่วนใหญ่อยู่ในแพ็คเกจjava.netดังนั้นเราจำเป็นต้องทำการอิมพอร์ตต่อไปนี้:

import java.net.*;

นอกจากนี้เรายังต้องการแพ็คเกจjava.ioซึ่งให้อินพุตและเอาต์พุตสตรีมเพื่อเขียนและอ่านในขณะที่สื่อสาร:

import java.io.*;

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

3. ตัวอย่างง่ายๆ

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

มาสร้างแอ็พพลิเคชันเซิร์ฟเวอร์ในคลาสที่เรียกว่าGreetServer.javaด้วยรหัสต่อไปนี้

เรารวมวิธีการหลักและตัวแปรส่วนกลางเพื่อดึงดูดความสนใจว่าเราจะเรียกใช้เซิร์ฟเวอร์ทั้งหมดในบทความนี้อย่างไร ในตัวอย่างที่เหลือในบทความเราจะละเว้นโค้ดซ้ำ ๆ ประเภทนี้:

public class GreetServer { private ServerSocket serverSocket; private Socket clientSocket; private PrintWriter out; private BufferedReader in; public void start(int port) { serverSocket = new ServerSocket(port); clientSocket = serverSocket.accept(); out = new PrintWriter(clientSocket.getOutputStream(), true); in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); String greeting = in.readLine(); if ("hello server".equals(greeting)) { out.println("hello client"); } else { out.println("unrecognised greeting"); } } public void stop() { in.close(); out.close(); clientSocket.close(); serverSocket.close(); } public static void main(String[] args) { GreetServer server=new GreetServer(); server.start(6666); } }

มาสร้างไคลเอนต์ชื่อGreetClient.javaด้วยรหัสนี้:

public class GreetClient { private Socket clientSocket; private PrintWriter out; private BufferedReader in; public void startConnection(String ip, int port) { clientSocket = new Socket(ip, port); out = new PrintWriter(clientSocket.getOutputStream(), true); in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); } public String sendMessage(String msg) { out.println(msg); String resp = in.readLine(); return resp; } public void stopConnection() { in.close(); out.close(); clientSocket.close(); } }

เริ่มต้นเซิร์ฟเวอร์กันเถอะ ใน IDE ของคุณคุณทำได้โดยเพียงแค่เรียกใช้เป็นแอปพลิเคชัน Java

ตอนนี้เรามาส่งคำทักทายไปยังเซิร์ฟเวอร์โดยใช้การทดสอบหน่วยซึ่งยืนยันว่าเซิร์ฟเวอร์ส่งคำทักทายจริง:

@Test public void givenGreetingClient_whenServerRespondsWhenStarted_thenCorrect() { GreetClient client = new GreetClient(); client.startConnection("127.0.0.1", 6666); String response = client.sendMessage("hello server"); assertEquals("hello client", response); }

อย่ากังวลหากคุณไม่เข้าใจสิ่งที่เกิดขึ้นที่นี่ทั้งหมดเนื่องจากตัวอย่างนี้มีไว้เพื่อให้เรารู้สึกถึงสิ่งที่จะเกิดขึ้นในบทความในภายหลัง

ในส่วนต่อไปนี้เราจะแยกการสื่อสารซ็อกเก็ตโดยใช้ตัวอย่างง่ายๆนี้และเจาะลึกรายละเอียดพร้อมตัวอย่างเพิ่มเติม

4. ซ็อกเก็ตทำงานอย่างไร

เราจะใช้ตัวอย่างข้างต้นเพื่อดูส่วนต่างๆของส่วนนี้

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

4.1. เซิฟเวอร์

โดยปกติเซิร์ฟเวอร์จะทำงานบนคอมพิวเตอร์เฉพาะบนเครือข่ายและมีซ็อกเก็ตที่ผูกไว้กับหมายเลขพอร์ตเฉพาะ ในกรณีของเราเราใช้คอมพิวเตอร์เครื่องเดียวกับไคลเอนต์และเริ่มเซิร์ฟเวอร์บนพอร์ต6666 :

ServerSocket serverSocket = new ServerSocket(6666);

เซิร์ฟเวอร์รอเพียงฟังซ็อกเก็ตเพื่อให้ไคลเอ็นต์ทำการร้องขอการเชื่อมต่อ สิ่งนี้จะเกิดขึ้นในขั้นตอนต่อไป:

Socket clientSocket = serverSocket.accept();

เมื่อรหัสเซิร์ฟเวอร์พบวิธีการยอมรับจะบล็อกจนกว่าไคลเอ็นต์จะร้องขอการเชื่อมต่อ

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

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

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

นับจากนี้เป็นต้นไปเซิร์ฟเวอร์สามารถแลกเปลี่ยนข้อความกับไคลเอนต์ได้อย่างไม่มีที่สิ้นสุดจนกว่าซ็อกเก็ตจะถูกปิดด้วยสตรีม

อย่างไรก็ตามในตัวอย่างของเราเซิร์ฟเวอร์สามารถส่งคำทักทายก่อนที่จะปิดการเชื่อมต่อซึ่งหมายความว่าหากเราทำการทดสอบอีกครั้งการเชื่อมต่อจะถูกปฏิเสธ

เพื่อให้การสื่อสารมีความต่อเนื่องเราจะต้องอ่านจากอินพุตสตรีมภายในลูปwhileและออกก็ต่อเมื่อไคลเอนต์ส่งคำขอยุติการใช้งานเราจะเห็นสิ่งนี้ในการดำเนินการในส่วนต่อไปนี้

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

4.2. ลูกค้า

ไคลเอนต์ต้องทราบชื่อโฮสต์หรือ IP ของเครื่องที่เซิร์ฟเวอร์กำลังทำงานอยู่และหมายเลขพอร์ตที่เซิร์ฟเวอร์กำลังรับฟัง

ในการร้องขอการเชื่อมต่อไคลเอนต์พยายามพบปะกับเซิร์ฟเวอร์บนเครื่องและพอร์ตของเซิร์ฟเวอร์:

Socket clientSocket = new Socket("127.0.0.1", 6666);

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

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

PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true); BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));

อินพุตสตรีมของไคลเอนต์เชื่อมต่อกับสตรีมเอาต์พุตของเซิร์ฟเวอร์เช่นเดียวกับสตรีมอินพุตของเซิร์ฟเวอร์ที่เชื่อมต่อกับสตรีมเอาต์พุตของไคลเอนต์

5. การสื่อสารอย่างต่อเนื่อง

เซิร์ฟเวอร์ปัจจุบันของเราบล็อกจนกว่าไคลเอ็นต์จะเชื่อมต่อแล้วบล็อกอีกครั้งเพื่อฟังข้อความจากไคลเอนต์หลังจากข้อความเดียวเซิร์ฟเวอร์จะปิดการเชื่อมต่อเนื่องจากเราไม่ได้จัดการกับความต่อเนื่อง

So it is only helpful in ping requests, but imagine we would like to implement a chat server, continuous back and forth communication between server and client would definitely be required.

We will have to create a while loop to continuously observe the input stream of the server for incoming messages.

Let's create a new server called EchoServer.java whose sole purpose is to echo back whatever messages it receives from clients:

public class EchoServer { public void start(int port) { serverSocket = new ServerSocket(port); clientSocket = serverSocket.accept(); out = new PrintWriter(clientSocket.getOutputStream(), true); in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); String inputLine; while ((inputLine = in.readLine()) != null) { if (".".equals(inputLine)) { out.println("good bye"); break; } out.println(inputLine); } }

Notice that we have added a termination condition where the while loop exits when we receive a period character.

We will start EchoServer using the main method just as we did for the GreetServer. This time, we start it on another port such as 4444 to avoid confusion.

The EchoClient is similar to GreetClient, so we can duplicate the code. We are separating them for clarity.

In a different test class, we shall create a test to show that multiple requests to the EchoServer will be served without the server closing the socket. This is true as long as we are sending requests from the same client.

Dealing with multiple clients is a different case, which we shall see in a subsequent section.

Let's create a setup method to initiate a connection with the server:

@Before public void setup() { client = new EchoClient(); client.startConnection("127.0.0.1", 4444); }

We will equally create a tearDown method to release all our resources, this is best practice for every case where we use network resources:

@After public void tearDown() { client.stopConnection(); }

Let's then test our echo server with a few requests:

@Test public void givenClient_whenServerEchosMessage_thenCorrect() { String resp1 = client.sendMessage("hello"); String resp2 = client.sendMessage("world"); String resp3 = client.sendMessage("!"); String resp4 = client.sendMessage("."); assertEquals("hello", resp1); assertEquals("world", resp2); assertEquals("!", resp3); assertEquals("good bye", resp4); }

This is an improvement over the initial example, where we would only communicate once before the server closed our connection; now we send a termination signal to tell the server when we're done with the session.

6. Server With Multiple Clients

Much as the previous example was an improvement over the first one, it is still not that great a solution. A server must have the capacity to service many clients and many requests simultaneously.

Handling multiple clients is what we are going to cover in this section.

Another feature we will see here is that the same client could disconnect and reconnect again, without getting a connection refused exception or a connection reset on the server. Previously we were not able to do this.

This means that our server is going to be more robust and resilient across multiple requests from multiple clients.

How we will do this is to create a new socket for every new client and service that client's requests on a different thread. The number of clients being served simultaneously will equal the number of threads running.

The main thread will be running a while loop as it listens for new connections.

Enough talk, let's create another server called EchoMultiServer.java. Inside it, we will create a handler thread class to manage each client's communications on its socket:

public class EchoMultiServer { private ServerSocket serverSocket; public void start(int port) { serverSocket = new ServerSocket(port); while (true) new EchoClientHandler(serverSocket.accept()).start(); } public void stop() { serverSocket.close(); } private static class EchoClientHandler extends Thread { private Socket clientSocket; private PrintWriter out; private BufferedReader in; public EchoClientHandler(Socket socket) { this.clientSocket = socket; } public void run() { out = new PrintWriter(clientSocket.getOutputStream(), true); in = new BufferedReader( new InputStreamReader(clientSocket.getInputStream())); String inputLine; while ((inputLine = in.readLine()) != null) { if (".".equals(inputLine)) { out.println("bye"); break; } out.println(inputLine); } in.close(); out.close(); clientSocket.close(); } }

Notice that we now call accept inside a while loop. Every time the while loop is executed, it blocks on the accept call until a new client connects, then the handler thread, EchoClientHandler, is created for this client.

What happens inside the thread is what we previously did in the EchoServer where we handled only a single client. So the EchoMultiServer delegates this work to EchoClientHandler so that it can keep listening for more clients in the while loop.

เราจะยังคงใช้EchoClientเพื่อทดสอบเซิร์ฟเวอร์ในครั้งนี้เราจะสร้างไคลเอนต์หลายรายแต่ละรายที่ส่งและรับข้อความจากเซิร์ฟเวอร์

ขอเริ่มต้นเซิร์ฟเวอร์ของเราโดยใช้วิธีการหลักในพอร์ต5555

เพื่อความชัดเจนเราจะยังคงทำการทดสอบในชุดใหม่:

@Test public void givenClient1_whenServerResponds_thenCorrect() { EchoClient client1 = new EchoClient(); client1.startConnection("127.0.0.1", 5555); String msg1 = client1.sendMessage("hello"); String msg2 = client1.sendMessage("world"); String terminate = client1.sendMessage("."); assertEquals(msg1, "hello"); assertEquals(msg2, "world"); assertEquals(terminate, "bye"); } @Test public void givenClient2_whenServerResponds_thenCorrect() { EchoClient client2 = new EchoClient(); client2.startConnection("127.0.0.1", 5555); String msg1 = client2.sendMessage("hello"); String msg2 = client2.sendMessage("world"); String terminate = client2.sendMessage("."); assertEquals(msg1, "hello"); assertEquals(msg2, "world"); assertEquals(terminate, "bye"); }

เราสามารถสร้างกรณีทดสอบเหล่านี้ได้มากเท่าที่เราต้องการโดยแต่ละกรณีจะสร้างไคลเอนต์ใหม่และเซิร์ฟเวอร์จะให้บริการทั้งหมด

7. สรุป

ในบทช่วยสอนนี้เราได้มุ่งเน้นไปที่บทนำเกี่ยวกับการเขียนโปรแกรมซ็อกเก็ตผ่าน TCP / IPและเขียนแอปพลิเคชันไคลเอนต์ / เซิร์ฟเวอร์อย่างง่ายใน Java

คุณสามารถพบซอร์สโค้ดฉบับเต็มของบทความได้ตามปกติในโครงการ GitHub