บทนำ: “บนโต๊ะมีคอมพิวเตอร์เครื่องหนึ่ง ด้านหลังมีตัวเข้ารหัส…”
กาลครั้งหนึ่งเพื่อนร่วมชั้นคนหนึ่งของฉันโพสต์ผลการศึกษา Java ของเขาอีกครั้งในรูปแบบของภาพหน้าจอของโปรแกรมใหม่ โปรแกรมนี้เป็นการแชทแบบหลายผู้ใช้ ตอนนั้นฉันเพิ่งเริ่มต้นการเดินทางของตัวเองในการเรียนรู้การเขียนโปรแกรมในภาษานี้ แต่ฉันตั้งข้อสังเกตกับตัวเองอย่างแน่นอนว่า "ฉันต้องการมัน!" เวลาผ่านไปและหลังจากทำงานในโครงการต่อไปเสร็จแล้วโดยเป็นส่วนหนึ่งของการเสริมความรู้ด้านการเขียนโปรแกรม ฉันจำเหตุการณ์นั้นได้และตัดสินใจว่าถึงเวลาแล้ว ฉันเริ่มเจาะลึกหัวข้อนี้ด้วยความอยากรู้ แต่ในหนังสือเรียน Java หลักของฉัน (ซึ่งเป็นคู่มือฉบับสมบูรณ์ของ Schildt) มีเพียง 20 หน้าเท่านั้นที่มีให้สำหรับแพ็คเกจ java.net เป็นเรื่องที่เข้าใจได้ - หนังสือเล่มนี้มีขนาดใหญ่มากแล้ว มีตารางวิธีการและตัวสร้างคลาสหลัก แต่นั่นคือทั้งหมด แน่นอนว่าขั้นตอนต่อไปคือ Google ผู้ยิ่งใหญ่: บทความมากมายมากมายที่นำเสนอสิ่งเดียวกัน - คำสองหรือสามคำเกี่ยวกับซ็อกเก็ตและตัวอย่างสำเร็จรูป แนวทางคลาสสิก (อย่างน้อยก็ในรูปแบบการศึกษาของฉัน) คือการทำความเข้าใจก่อนว่าฉันต้องการอะไรจากเครื่องมือในการทำงาน ว่ามันคืออะไร เหตุใดจึงจำเป็น และจากนั้นเท่านั้น หากวิธีแก้ไขปัญหาไม่ชัดเจน ให้เลือก รายการสำเร็จรูปคลายเกลียวน็อตและสลักเกลียว แต่ฉันรู้แล้วว่าอะไรคืออะไรและในที่สุดก็เขียนแชทแบบหลายผู้ใช้ ภายนอกปรากฎดังนี้:
ที่นี่ฉันจะพยายามให้คุณเข้าใจพื้นฐานของแอปพลิเคชันไคลเอนต์ - เซิร์ฟเวอร์ที่ใช้ซ็อกเก็ต Java โดยใช้ตัวอย่างการออกแบบแชท ในหลักสูตร Javarash คุณจะทำการแชท มันจะอยู่ในระดับที่แตกต่างอย่างสิ้นเชิง สวยงาม ใหญ่ มัลติฟังก์ชั่น แต่ก่อนอื่น คุณต้องวางรากฐานเสมอ ดังนั้นเราจำเป็นต้องค้นหาว่าอะไรเป็นรากฐานของส่วนดังกล่าว (หากคุณพบข้อบกพร่องหรือข้อผิดพลาดใด ๆ เขียนใน PM หรือในความคิดเห็นใต้บทความ) เอาล่ะ. หัวข้อที่หนึ่ง: “บ้านที่…” เพื่ออธิบายว่าการเชื่อมต่อเครือข่ายเกิดขึ้นระหว่างเซิร์ฟเวอร์และไคลเอนต์รายหนึ่งได้อย่างไร มาดูตัวอย่างคลาสสิกของอาคารอพาร์ตเมนต์กันดีกว่า สมมติว่าไคลเอนต์จำเป็นต้องสร้างการเชื่อมต่อกับเซิร์ฟเวอร์เฉพาะ ผู้ค้นหาจำเป็นต้องรู้อะไรบ้างเกี่ยวกับวัตถุการค้นหา ใช่ที่อยู่ เซิร์ฟเวอร์ไม่ใช่เอนทิตีที่มีมนต์ขลังบนคลาวด์ ดังนั้นจึงต้องอยู่บนเครื่องเฉพาะ โดยการอุปมากับบ้านซึ่งควรมีการประชุมสองฝ่ายที่ตกลงกันไว้ และเพื่อที่จะพบกันในอาคารอพาร์ตเมนต์ที่อยู่ของอาคารเดียวไม่เพียงพอคุณต้องระบุหมายเลขอพาร์ทเมนต์ที่จะจัดการประชุม ในทำนองเดียวกัน บนคอมพิวเตอร์เครื่องหนึ่งสามารถมีเซิร์ฟเวอร์ได้หลายเครื่องพร้อมกัน และเพื่อให้ไคลเอนต์ติดต่อกับเครื่องใดเครื่องหนึ่ง เขาจำเป็นต้องระบุหมายเลขพอร์ตที่จะใช้ในการเชื่อมต่อด้วย ดังนั้นที่อยู่และหมายเลขพอร์ต ที่อยู่หมายถึงตัวระบุของเครื่องในพื้นที่อินเทอร์เน็ต อาจเป็นชื่อโดเมน เช่น"javarush.ru"หรือ IP ปกติ ท่าเรือ- หมายเลขเฉพาะที่เกี่ยวข้องกับซ็อกเก็ตเฉพาะ (คำนี้จะกล่าวถึงในภายหลัง) กล่าวอีกนัยหนึ่งคือบริการบางอย่างถูกครอบครองเพื่อให้สามารถใช้เพื่อติดต่อได้ ดังนั้นเพื่อให้วัตถุอย่างน้อยสองชิ้นมาบรรจบกันในอาณาเขตของหนึ่ง (เซิร์ฟเวอร์) เจ้าของพื้นที่ (เซิร์ฟเวอร์) จะต้องครอบครองอพาร์ทเมนต์เฉพาะ (ท่าเรือ) บนนั้น (รถยนต์) และอย่างที่สองจะต้องค้นหาสถานที่นัดพบที่รู้ ที่อยู่ของบ้าน (โดเมนหรือ IP) และหมายเลขอพาร์ตเมนต์ (พอร์ต) หัวเรื่องที่สอง: พบกับ Socket ในบรรดาแนวคิดและคำศัพท์ที่เกี่ยวข้องกับการทำงานบนเครือข่าย สิ่งหนึ่งที่สำคัญมากคือ Socket หมายถึงจุดที่เกิดการเชื่อมต่อ พูดง่ายๆ ก็คือ ซ็อกเก็ตเชื่อมต่อสองโปรแกรมบนเครือข่าย ชั้นเรียน
คลาสนี้ถูกประกาศบนฝั่งไคลเอ็นต์ และเซิร์ฟเวอร์สร้างขึ้นใหม่โดยรับสัญญาณการเชื่อมต่อ นี่คือวิธีการทำงานของการสื่อสารออนไลน์ เริ่มต้นด้วย นี่คือตัวสร้างคลาสที่เป็นไปได้

Socket
นำแนวคิดของซ็อกเก็ตไปใช้ ไคลเอนต์และเซิร์ฟเวอร์จะสื่อสารผ่านช่องสัญญาณอินพุต/เอาท์พุต: 
Socket
:
Socket(String Name_хоста, int порт) throws UnknownHostException, IOException
Socket(InetAddress IP-address, int порт) throws UnknownHostException
“host_name” - หมายถึงโหนดเครือข่ายเฉพาะที่อยู่ IP หากคลาสซ็อกเก็ตไม่สามารถแปลงเป็นที่อยู่จริงที่มีอยู่ได้ ข้อยกเว้นจะถูกส่งออกUnknownHostException
ไป พอร์ตก็คือท่าเรือ หากระบุ 0 เป็นหมายเลขพอร์ต ระบบจะจัดสรรพอร์ตที่ว่างเอง ข้อยกเว้นอาจเกิดขึ้นได้หากการเชื่อมต่อขาดหายIOException
ไป ควรสังเกตว่าประเภทที่อยู่ใน Constructor ตัวที่สองInetAddress
คือ การช่วยเหลือดังกล่าวเกิดขึ้นเมื่อคุณต้องการระบุชื่อโดเมนเป็นที่อยู่ นอกจากนี้ เมื่อโดเมนหมายถึงที่อยู่ IP หลายรายการInetAddress
คุณสามารถใช้โดเมนเพื่อรับอาร์เรย์ได้ อย่างไรก็ตาม มันใช้ได้กับ IP ด้วยเช่นกัน คุณยังสามารถรับชื่อโฮสต์ อาร์เรย์ไบต์ที่ประกอบเป็นที่อยู่ IP เป็นต้น เราจะพูดถึงมันเพิ่มเติมอีกเล็กน้อย แต่คุณจะต้องไปที่เอกสารอย่างเป็นทางการเพื่อดูรายละเอียดทั้งหมด เมื่อวัตถุประเภทถูกเตรียมใช้งานSocket
ไคลเอนต์ที่เป็นเจ้าของจะประกาศบนเครือข่ายว่าต้องการเชื่อมต่อกับเซิร์ฟเวอร์ตามที่อยู่และหมายเลขพอร์ตเฉพาะ ด้านล่างนี้เป็นวิธีการที่ใช้บ่อยที่สุดของคลาสSocket
: InetAddress getInetAddress()
– ส่งคืนอ็อบเจ็กต์ที่มีข้อมูลเกี่ยวกับซ็อกเก็ต หากไม่ได้เชื่อมต่อซ็อกเก็ต - null int getPort()
- ส่งคืนพอร์ตที่มีการเชื่อมต่อกับเซิร์ฟเวอร์ int getLocalPort()
- ส่งคืนพอร์ตที่ซ็อกเก็ตถูกผูกไว้ ความจริงก็คือไคลเอนต์และเซิร์ฟเวอร์สามารถ "สื่อสาร" บนพอร์ตเดียวได้ แต่พอร์ตที่เชื่อมโยงไว้อาจแตกต่างกันโดยสิ้นเชิง boolean isConnected()
- ส่งคืนจริงหากสร้างการเชื่อมต่อ void connect(SocketAddress address)
- ระบุการเชื่อมต่อใหม่ boolean isClosed()
- ส่งคืนจริงหากซ็อกเก็ตปิดอยู่ boolean isBound()
- คืนค่าเป็นจริง หากซ็อกเก็ตเชื่อมโยงกับที่อยู่จริง คลาสSocket
จะใช้อินเทอร์เฟซAutoCloseable
ดังนั้นจึงสามารถใช้ในไฟล์try-with-resources
. อย่างไรก็ตาม คุณยังสามารถปิดซ็อกเก็ตด้วยวิธีคลาสสิกได้โดยใช้ close() หัวที่สาม: และนี่คือ ServerSocket สมมติว่าเราประกาศSocket
คำขอการเชื่อมต่อในฝั่งไคลเอ็นต์ ในรูปแบบของคลาส เซิฟเวอร์จะเดาความปรารถนาของเราได้อย่างไร? สำหรับสิ่งนี้ เซิร์ฟเวอร์มีคลาสเช่น ServerSocket
และเมธอด Accept() อยู่ในนั้น คอนสตรัคเตอร์มีดังต่อไปนี้:
ServerSocket() throws IOException
ServerSocket(int порт) throws IOException
ServerSocket(int порт, int максимум_подключений) throws IOException
ServerSocket(int порт, int максимум_подключений, InetAddress локальный_address) throws IOException
เมื่อประกาศServerSocket
คุณไม่จำเป็นต้องระบุที่อยู่การเชื่อมต่อ เนื่องจากการสื่อสารเกิดขึ้นบนเครื่องเซิร์ฟเวอร์ เฉพาะกับโฮสต์แบบหลายช่องสัญญาณเท่านั้นที่คุณต้องระบุ IP ที่ซ็อกเก็ตเซิร์ฟเวอร์เชื่อมโยงกับ หัวข้อที่สาม หนึ่ง: เซิร์ฟเวอร์ที่บอกว่าไม่ เนื่องจากการจัดหาโปรแกรมที่มีทรัพยากรมากกว่าที่จำเป็นนั้นมีทั้งค่าใช้จ่ายสูงและไม่มีเหตุผล ดังนั้นในตัวสร้างServerSocket
คุณจะถูกขอให้ประกาศการเชื่อมต่อสูงสุดที่เซิร์ฟเวอร์ยอมรับระหว่างการดำเนินการ หากไม่ได้ระบุไว้ โดยค่าเริ่มต้นตัวเลขนี้จะถือว่าเท่ากับ 50 ใช่ ตามทฤษฎีแล้ว เราสามารถสรุปได้ว่าServerSocket
นี่เป็นซ็อกเก็ตเดียวกัน สำหรับเซิร์ฟเวอร์เท่านั้น แต่มันมีบทบาทที่แตกต่างไปจากชั้นเรียนโดยSocket
สิ้นเชิง จำเป็นเฉพาะในขั้นตอนการสร้างการเชื่อมต่อเท่านั้น เมื่อสร้างวัตถุประเภทแล้ว ServerSocket
คุณต้องค้นหาว่ามีคนต้องการเชื่อมต่อกับเซิร์ฟเวอร์ เมธอด Accept() เชื่อมต่ออยู่ที่นี่ เป้าหมายรอจนกว่าจะมีคนต้องการเชื่อมต่อกับเป้าหมาย และเมื่อสิ่งนี้เกิดขึ้น เป้าหมายจะส่งคืนอ็อบเจ็กต์ประเภทSocket
นั่นคือ ซ็อกเก็ตไคลเอ็นต์ที่สร้างขึ้นใหม่ และตอนนี้ซ็อกเก็ตไคลเอ็นต์ได้ถูกสร้างขึ้นบนฝั่งเซิร์ฟเวอร์แล้ว การสื่อสารแบบสองทางจึงสามารถเริ่มต้นได้ การสร้างวัตถุประเภทSocket
บนฝั่งไคลเอ็นต์และสร้างใหม่โดยใช้ServerSocket
ฝั่งเซิร์ฟเวอร์เป็นขั้นต่ำที่จำเป็นสำหรับการเชื่อมต่อ หัวข้อที่สี่: จดหมายถึงซานตาคลอส Вопрос:
ลูกค้าและเซิร์ฟเวอร์สื่อสารกันอย่างไร? Ответ:
ผ่านสตรีม I/O เรามีอะไรอยู่แล้ว? ซ็อกเก็ตที่มีที่อยู่เซิร์ฟเวอร์และหมายเลขพอร์ตของไคลเอ็นต์ และเช่นเดียวกัน ต้องขอบคุณการยอมรับ() ที่ฝั่งเซิร์ฟเวอร์ ดังนั้นจึงสมเหตุสมผลที่จะถือว่าพวกเขาจะสื่อสารผ่านซ็อกเก็ต เมื่อต้องการทำเช่น นี้มีสองวิธีที่ให้สิทธิ์เข้าถึงสตรีมInputStream
และOutputStream
อ็อบเจ็กต์ประเภท Socket
พวกเขาอยู่ที่นี่:
InputStream getInputStream()
OutputStream getOutputStream()
เนื่องจากการอ่านและการเขียนไบต์เปล่าไม่ได้มีประสิทธิภาพมากนัก สตรีมจึงสามารถรวมไว้ในคลาสอะแด็ปเตอร์ จะบัฟเฟอร์หรือไม่ก็ได้ ตัวอย่างเช่น:
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
เพื่อให้การสื่อสารเป็นแบบสองทิศทาง การดำเนินการดังกล่าวจะต้องดำเนินการทั้งสองด้าน ตอนนี้คุณสามารถส่งบางสิ่งโดยใช้เข้า และรับบางอย่างโดยใช้ออกไป และในทางกลับกัน Socket
ที่จริงแล้ว นี่เป็นฟังก์ชันเดียวของคลาส และใช่ อย่าลืมเกี่ยวกับเมธอด flush() BufferedWriter
- มันจะล้างเนื้อหาของบัฟเฟอร์ หากไม่ดำเนินการดังกล่าว ข้อมูลจะไม่ถูกส่ง ดังนั้นจึงไม่ได้รับ เธรดที่ได้รับยังรอตัวบ่งชี้ที่สิ้นสุดบรรทัด – “\n” มิฉะนั้นข้อความจะไม่ได้รับการยอมรับ เนื่องจากในความเป็นจริงแล้ว ข้อความยังไม่เสร็จสมบูรณ์และไม่สมบูรณ์ หากสิ่งนี้ดูเหมือนไม่สะดวกสำหรับคุณ ไม่ต้องกังวล คุณสามารถใช้คลาสได้เสมอPrintWriter
ซึ่งจำเป็นต้องตัดคำ ระบุ true เป็นอาร์กิวเมนต์ที่สอง จากนั้นการแตกจากบัฟเฟอร์จะเกิดขึ้นโดยอัตโนมัติ:
PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())), true);
นอกจากนี้ ไม่จำเป็นต้องระบุจุดสิ้นสุดของบรรทัด ชั้นเรียนนี้ทำเพื่อคุณ แต่สตริง I/O เป็นขีดจำกัดของสิ่งที่ซ็อกเก็ตสามารถทำได้หรือไม่ ไม่ คุณต้องการส่งอ็อบเจ็กต์ผ่านซ็อกเก็ตสตรีมหรือไม่? เพื่อเห็นแก่พระเจ้า ทำให้เป็นอนุกรมและคุณก็พร้อมแล้ว:
ObjectOutputStream out = new ObjectOutputStream(socket.getOutputStream());
ObjectInputStream in = new ObjectInputStream(socket.getInputStream());
หัวข้อที่ห้า: การสื่อสารจริงผ่านอินเทอร์เน็ต เนื่องจากในการเชื่อมต่อผ่านเครือข่ายจริงด้วยที่อยู่ IP จริง คุณจำเป็นต้องมีเซิร์ฟเวอร์ที่มีคุณสมบัติครบถ้วน และเนื่องจาก:
- การสนทนาในอนาคตของเราในฐานะยูทิลิตี้ไม่มีความสามารถดังกล่าว สามารถสร้างการเชื่อมต่อและรับ/ส่งข้อความเท่านั้น นั่นคือมันไม่มีความสามารถเซิร์ฟเวอร์จริง
- เซิร์ฟเวอร์ของเราซึ่งมีเฉพาะข้อมูลซ็อกเก็ตและสตรีม I/O ไม่สามารถทำงานเป็นเว็บหรือเซิร์ฟเวอร์ FTP จริงได้ ดังนั้นเพียงเท่านี้เราก็จะไม่สามารถเชื่อมต่อผ่านอินเทอร์เน็ตได้
Socket
ว่าที่อยู่อยู่ในเครื่อง มี 2 วิธี:
- เขียน “localhost” เป็นอาร์กิวเมนต์ที่อยู่ ซึ่งหมายถึงต้นขั้วท้องถิ่น “127.0.0.1” ก็เหมาะสำหรับสิ่งนี้เช่นกัน - นี่เป็นเพียงรูปแบบดิจิทัลของต้นขั้ว
- การใช้ InetAddress:
InetAddress.getByName(null)
- ชี้ null ไปยัง localhostInetAddress.getByName("localhost")
InetAddress.getByName("127.0.0.1")
import java.io.*;
import java.net.ServerSocket;
import java.net.Socket;
public class Server {
private static Socket clientSocket; // socket for communication
private static ServerSocket server; // server socket
private static BufferedReader in; // socket read stream
private static BufferedWriter out; // socket write stream
public static void main(String[] args) {
try {
try {
server = new ServerSocket(4004); // server socket listening on port 4004
System.out.println("Server is running!"); // server would be nice
// announce your launch
clientSocket = server.accept(); // accept() will wait until
//someone won't want to connect
try { // after establishing a connection and recreating the socket for communication with the client, you can go
// to create I/O streams.
// now we can receive messages
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// and send
out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
String word = in.readLine(); // wait for the client to write something to us
System.out.println(word);
// without hesitation responds to the client
out.write("Hi, this is the Server! I confirm you wrote: " + word + "\n");
out.flush(); // push everything out of the buffer
} finally { // in any case, the socket will be closed
clientSocket.close();
// streams would also be nice to close
in.close();
out.close();
}
} finally {
System.out.println("Server closed!");
server.close();
}
} catch (IOException e) {
System.err.println(e);
}
}
"ไคลเอนต์.java"
import java.io.*;
import java.net.Socket;
public class Client {
private static Socket clientSocket; // socket for communication
private static BufferedReader reader; // we need a reader that reads from the console, otherwise
// do we know what the client wants to say?
private static BufferedReader in; // socket read stream
private static BufferedWriter out; // socket write stream
public static void main(String[] args) {
try {
try {
// address - local host, port - 4004, same as the server
clientSocket = new Socket("localhost", 4004); // with this line we request
// the server has access to the connection
reader = new BufferedReader(new InputStreamReader(System.in));
// read messages from the server
in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
// write there
out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream()));
System.out.println("Did you have something to say? Enter it here:");
// if the connection happened and the threads were successfully created - we can
// work further and offer the client something to enter
// if not, an exception will be thrown
String word = reader.readLine(); // wait for the client to do something
// will not write to the console
out.write(word + "\n"); // send a message to the server
out.flush();
String serverWord = in.readLine(); // wait for the server to say
System.out.println(serverWord); // received - display
} finally { // in any case, you need to close the socket and streams
System.out.println("The client has been closed...");
clientSocket.close();
in.close();
out.close();
}
} catch (IOException e) {
System.err.println(e);
}
}
}
แน่นอน คุณควรเริ่มเซิร์ฟเวอร์ก่อน เพราะเมื่อเริ่มต้นระบบไคลเอ็นต์จะเชื่อมต่อกับอะไรหากไม่มีสิ่งใดที่จะเชื่อมต่อได้ :) ผลลัพธ์จะเป็นดังนี้: /* คุณอยากจะพูดอะไรไหม? ป้อนที่นี่: สวัสดีเซิร์ฟเวอร์? คุณได้ยินฉันไหม? สวัสดี นี่คือเซิร์ฟเวอร์! ฉันยืนยันว่าคุณเขียนว่า: สวัสดีเซิร์ฟเวอร์? คุณได้ยินฉันไหม? ลูกค้าปิดแล้ว... */ ไชโย! เราสอนเซิร์ฟเวอร์ให้สื่อสารกับลูกค้า! ดังนั้นการสื่อสารนั้นจึงไม่ได้อยู่ในสองแบบจำลอง แต่มากเท่าที่คุณต้องการ เพียงแค่ล้อมการอ่านและการเขียนของเธรดในลูปชั่วขณะ (จริง) และระบุถึงทางออกนั้นตามข้อความบางอย่าง เช่น "ทางออก" วงจรถูกขัดจังหวะ และโปรแกรมจะสิ้นสุด หัวหน้าเจ็ด: ผู้ใช้หลายคนดีกว่า การที่เซิร์ฟเวอร์ได้ยินเราเป็นสิ่งที่ดี แต่จะดีกว่ามากหากเราสามารถสื่อสารกับใครสักคนในแบบของเราเอง ฉันจะแนบแหล่งข้อมูลทั้งหมดไว้ท้ายบทความ ดังนั้นที่นี่ฉันจะแสดงโค้ดที่ไม่ใหญ่เสมอไป แต่เป็นส่วนสำคัญที่จะทำให้สามารถสนทนาแบบหลายผู้ใช้ได้ หากใช้อย่างถูกต้อง ดังนั้นเราจึงต้องการสื่อสารกับไคลเอนต์อื่น ๆ ผ่านทางเซิร์ฟเวอร์ ทำอย่างไร? เห็นได้ชัดว่าเนื่องจากโปรแกรมไคลเอนต์มีวิธีการของตัวเองmain
จึงหมายความว่าสามารถเปิดใช้งานแยกจากเซิร์ฟเวอร์และขนานกับไคลเอนต์อื่น ๆ ได้ สิ่งนี้ให้อะไรเราบ้าง? จำเป็นต้องมีการเชื่อมต่อใหม่แต่ละครั้งเซิร์ฟเวอร์จะไม่ไปที่การสื่อสารทันที แต่เขียนการเชื่อมต่อนี้ลงในรายการบางประเภทและดำเนินการรอการเชื่อมต่อใหม่และบริการเสริมบางประเภทมีส่วนร่วมในการสื่อสารกับเฉพาะ ลูกค้า. และไคลเอนต์จะต้องเขียนถึงเซิร์ฟเวอร์และรอการตอบกลับโดยแยกจากกัน กระทู้มาช่วยเหลือ สมมติว่าเรามีคลาสที่รับผิดชอบในการจดจำการเชื่อมต่อใหม่: มันควรจะระบุสิ่งต่อไปนี้:
- หมายเลขพอร์ต.
- รายการที่เขียนการเชื่อมต่อใหม่
- และ
ServerSocket
ในสำเนาเดียว (!)
public class Server {
public static final int PORT = 8080;
public static LinkedList<ServerSomthing> serverList = new LinkedList<>(); // list of all threads
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(PORT);
try {
while (true) {
// Blocks until a new connection is made:
Socket socket = server.accept();
try {
serverList.add(new ServerSomthing(socket)); // add a new connection to the list
} catch (IOException e) {
// If it fails, the socket is closed,
// otherwise, the thread will close it when it exits:
socket.close();
}
}
} finally {
server.close();
}
}
}
โอเค ตอนนี้แต่ละซ็อกเก็ตที่สร้างขึ้นใหม่จะไม่สูญหาย แต่จะถูกเก็บไว้บนเซิร์ฟเวอร์ ไกลออกไป. ลูกค้าทุกคนต้องการใครสักคนที่จะรับฟัง มาสร้างเธรดด้วยฟังก์ชันเซิร์ฟเวอร์จากบทที่แล้วกันดีกว่า
class ServerSomthing extends Thread {
private Socket socket; // socket through which the server communicates with the client,
// except for it - the client and server are not connected in any way
private BufferedReader in; // socket read stream
private BufferedWriter out; // socket write stream
public ServerSomthing(Socket socket) throws IOException {
this.socket = socket;
// если потоку ввода/вывода приведут к генерированию исключения, оно пробросится дальше
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
start(); // call run()
}
@Override
public void run() {
String word;
try {
while (true) {
word = in.readLine();
if(word.equals("stop")) {
break; }
for (ServerSomthing vr : Server.serverList) {
vr.send(word); // send the received message with
// bound client to everyone else including him
}
}
} catch (IOException e) {
}
}
private void send(String msg) {
try {
out.write(msg + "\n");
out.flush();
} catch (IOException ignored) {}
}
}
ดังนั้นในตัวสร้างเธรดของเซิร์ฟเวอร์ ซ็อกเก็ตต้องถูกเตรียมใช้งาน โดยที่เธรดจะสื่อสารกับไคลเอนต์เฉพาะ รวมถึงเธรด I/O และทุกสิ่งที่คุณต้องการเพื่อเริ่มเธรดโดยตรงจากตัวสร้าง โอเค แต่จะเกิดอะไรขึ้นเมื่อเซิร์ฟเวอร์เธรดอ่านข้อความจากไคลเอนต์ ส่งกลับไปยังลูกค้าของคุณเท่านั้น? ไม่ค่อยมีประสิทธิผล เรากำลังทำการแชทแบบหลายผู้ใช้ ดังนั้นเราจึงต้องการให้ไคลเอนต์ที่เชื่อมต่อแต่ละรายเพื่อรับสิ่งที่คนหนึ่งเขียน คุณต้องใช้รายการเธรดเซิร์ฟเวอร์ทั้งหมดที่เกี่ยวข้องกับไคลเอนต์และส่งข้อความแต่ละข้อความที่ส่งไปยังเธรดเฉพาะเพื่อส่งไปยังไคลเอนต์:
for (ServerSomthing vr : Server.serverList) {
vr.send(word); // send the received message
// from the linked client to everyone else, including him
}
private void send(String msg) {
try {
out.write(msg + "\n");
out.flush();
} catch (IOException ignored) {}
}
ตอนนี้ลูกค้าทุกคนจะรู้ว่าหนึ่งในนั้นพูดอะไร! หากคุณไม่ต้องการส่งข้อความถึงบุคคลที่ส่งข้อความนั้น (เขารู้อยู่แล้วว่าเขาเขียนอะไร!) เพียงแค่วนซ้ำเธรดโดยระบุว่าเมื่อประมวลผลวัตถุ ลูปthis
จะย้ายไปยังองค์ประกอบถัดไปโดยไม่ดำเนินการ การกระทำใด ๆ กับมัน หรือหากคุณต้องการ ให้ส่งข้อความถึงลูกค้าโดยระบุว่าได้รับและส่งข้อความเรียบร้อยแล้ว ทุกอย่างชัดเจนกับเซิร์ฟเวอร์แล้ว มาดูลูกค้ากันดีกว่าหรือให้กับลูกค้า! ทุกอย่างจะเหมือนกันที่นั่น โดยการเปรียบเทียบกับไคลเอนต์จากบทที่แล้ว เฉพาะเมื่อสร้างอินสแตนซ์ที่คุณต้องการ ดังที่แสดงในบทนี้กับเซิร์ฟเวอร์ เพื่อสร้างทุกสิ่งที่จำเป็นในตัวสร้าง แต่จะเกิดอะไรขึ้นถ้าตอนที่สร้างลูกค้าเขายังไม่มีเวลาป้อนอะไรเลย แต่มีบางอย่างถูกส่งไปให้เขาแล้ว? (เช่นประวัติการติดต่อของผู้ที่เชื่อมต่อกับแชทก่อนหน้าเขาแล้ว) ดังนั้นรอบที่ข้อความที่ส่งจะถูกประมวลผลจะต้องแยกออกจากรอบที่อ่านข้อความจากคอนโซลและส่งไปยังเซิร์ฟเวอร์เพื่อส่งต่อไปยังผู้อื่น กระทู้มาช่วยเหลืออีกครั้ง ไม่มีประโยชน์ที่จะสร้างไคลเอนต์เป็นเธรด สะดวกกว่าในการสร้างเธรดที่มีการวนซ้ำในวิธีการเรียกใช้ที่อ่านข้อความและโดยการเปรียบเทียบจะเขียนว่า:
// thread reading messages from the server
private class ReadMsg extends Thread {
@Override
public void run() {
String str;
try {
while (true) {
str = in.readLine(); // waiting for a message from the server
if (str.equals("stop")) {
break; // exit the loop if it's "stop"
}
}
} catch (IOException e) {
}
}
}
// thread sending messages coming from the console to the server
public class WriteMsg extends Thread {
@Override
public void run() {
while (true) {
String userWord;
try {
userWord = inputUser.readLine(); // messages from the console
if (userWord.equals("stop")) {
out.write("stop" + "\n");
break; // exit the loop if it's "stop"
} else {
out.write(userWord + "\n"); // send to the server
}
out.flush(); // clean up
} catch (IOException e) {
}
}
}
}
ในตัวสร้างไคลเอ็นต์ คุณเพียงแค่ต้องเริ่มเธรดเหล่านี้ จะปิดทรัพยากรของลูกค้าอย่างถูกต้องได้อย่างไรหากเขาต้องการออก? ฉันจำเป็นต้องปิดทรัพยากรเธรดของเซิร์ฟเวอร์หรือไม่ ในการดำเนินการนี้ คุณจะต้องสร้างวิธีการแยกต่างหากซึ่งจะถูกเรียกเมื่อออกจากลูปข้อความ ที่นั่นคุณจะต้องปิดซ็อกเก็ตและสตรีม I/O สัญญาณสิ้นสุดเซสชันเดียวกันสำหรับไคลเอ็นต์เฉพาะจะต้องถูกส่งไปยังเธรดเซิร์ฟเวอร์ ซึ่งต้องทำเช่นเดียวกันกับซ็อกเก็ตและลบตัวเองออกจากรายการเธรดในคลาสเซิร์ฟเวอร์หลัก หัวหน้าแปด: ความสมบูรณ์แบบไม่มีขีดจำกัด คุณสามารถคิดค้นคุณสมบัติใหม่ๆ เพื่อปรับปรุงโครงการของคุณได้ไม่รู้จบ แต่สิ่งที่ควรโอนไปยังไคลเอนต์ที่เชื่อมต่อใหม่คืออะไร? ฉันคิดว่าสิบเหตุการณ์ล่าสุดเกิดขึ้นก่อนที่เขาจะมาถึง ในการดำเนินการนี้คุณต้องสร้างคลาสที่จะป้อนการดำเนินการสุดท้ายกับเธรดเซิร์ฟเวอร์ใด ๆ ลงในรายการที่ประกาศและหากรายการเต็มแล้ว (นั่นคือมี 10 รายการแล้ว) ให้ลบรายการแรกและเพิ่ม อันสุดท้ายที่มา เพื่อให้เนื้อหาของรายการนี้ได้รับจากการเชื่อมต่อใหม่ เมื่อสร้างเธรดเซิร์ฟเวอร์ คุณต้องส่งเนื้อหาเหล่านั้นไปยังไคลเอนต์ในสตรีมเอาต์พุต ทำอย่างไร? ตัวอย่างเช่นเช่นนี้:
public void printStory(BufferedWriter writer) {
// ...
}
เธรดเซิร์ฟเวอร์ได้สร้างสตรีมแล้วและสามารถส่งกระแสข้อมูลเอาต์พุตเป็นอาร์กิวเมนต์ได้ ถัดไป คุณเพียงแค่ต้องผ่านทุกสิ่งที่ต้องโอนไปยังไคลเอนต์ใหม่ในรอบการค้นหา สรุป: นี่เป็นเพียงพื้นฐาน และมีแนวโน้มว่าสถาปัตยกรรมการแชทนี้จะไม่ทำงานเมื่อสร้างแอปพลิเคชันจริง โปรแกรมนี้ถูกสร้างขึ้นเพื่อการศึกษาและฉันได้แสดงให้เห็นว่าคุณสามารถทำให้ไคลเอนต์สื่อสารกับเซิร์ฟเวอร์ได้อย่างไร (และในทางกลับกัน) วิธีการทำเช่นนี้สำหรับการเชื่อมต่อต่างๆ และแน่นอน วิธีจัดระเบียบสิ่งนี้บนซ็อกเก็ต แหล่งที่มาจะถูกจัดเรียงใหม่ด้านล่าง และแนบซอร์สโค้ดของโปรแกรมที่กำลังวิเคราะห์ด้วย นี่เป็นประสบการณ์ครั้งแรกของฉันในการเขียนบทความ) ขอบคุณสำหรับความสนใจของคุณ :)
- การคิดใน Java Enterprise โดย Bruce Eckel และ อัล. 2546
- Java 8, The Complete Guide, Herbert Schildt, ฉบับที่ 9, 2017 (บทที่ 22)
- การเขียนโปรแกรมซ็อกเก็ตใน บทความ Java เกี่ยวกับซ็อกเก็ต
- ซ็อกเก็ตในเอกสารอย่างเป็นทางการ
- ServerSocket ในเอกสารอย่างเป็นทางการ
- แหล่งที่มาบน GitHub
GO TO FULL VERSION