JavaRush /จาวาบล็อก /Random-TH /คลาส Socket และ ServerSocket หรือ “สวัสดีเซิร์ฟเวอร์? คุณ...
Sergey Simonov
ระดับ
Санкт-Петербург

คลาส Socket และ ServerSocket หรือ “สวัสดีเซิร์ฟเวอร์? คุณได้ยินฉันไหม?"

เผยแพร่ในกลุ่ม
บทนำ: “บนโต๊ะมีคอมพิวเตอร์เครื่องหนึ่ง ด้านหลังมีตัวเข้ารหัส…” คลาส Socket และ ServerSocket หรือ “สวัสดีเซิร์ฟเวอร์?  คุณได้ยินฉันไหม?"  - 1กาลครั้งหนึ่งเพื่อนร่วมชั้นคนหนึ่งของฉันโพสต์ผลการศึกษา Java ของเขาอีกครั้งในรูปแบบของภาพหน้าจอของโปรแกรมใหม่ โปรแกรมนี้เป็นการแชทแบบหลายผู้ใช้ ตอนนั้นฉันเพิ่งเริ่มต้นการเดินทางของตัวเองในการเรียนรู้การเขียนโปรแกรมในภาษานี้ แต่ฉันตั้งข้อสังเกตกับตัวเองอย่างแน่นอนว่า "ฉันต้องการมัน!" เวลาผ่านไปและหลังจากทำงานในโครงการต่อไปเสร็จแล้วโดยเป็นส่วนหนึ่งของการเสริมความรู้ด้านการเขียนโปรแกรม ฉันจำเหตุการณ์นั้นได้และตัดสินใจว่าถึงเวลาแล้ว ฉันเริ่มเจาะลึกหัวข้อนี้ด้วยความอยากรู้ แต่ในหนังสือเรียน Java หลักของฉัน (ซึ่งเป็นคู่มือฉบับสมบูรณ์ของ Schildt) มีเพียง 20 หน้าเท่านั้นที่มีให้สำหรับแพ็คเกจ java.net เป็นเรื่องที่เข้าใจได้ - หนังสือเล่มนี้มีขนาดใหญ่มากแล้ว มีตารางวิธีการและตัวสร้างคลาสหลัก แต่นั่นคือทั้งหมด แน่นอนว่าขั้นตอนต่อไปคือ Google ผู้ยิ่งใหญ่: บทความมากมายมากมายที่นำเสนอสิ่งเดียวกัน - คำสองหรือสามคำเกี่ยวกับซ็อกเก็ตและตัวอย่างสำเร็จรูป แนวทางคลาสสิก (อย่างน้อยก็ในรูปแบบการศึกษาของฉัน) คือการทำความเข้าใจก่อนว่าฉันต้องการอะไรจากเครื่องมือในการทำงาน ว่ามันคืออะไร เหตุใดจึงจำเป็น และจากนั้นเท่านั้น หากวิธีแก้ไขปัญหาไม่ชัดเจน ให้เลือก รายการสำเร็จรูปคลายเกลียวน็อตและสลักเกลียว แต่ฉันรู้แล้วว่าอะไรคืออะไรและในที่สุดก็เขียนแชทแบบหลายผู้ใช้ ภายนอกปรากฎดังนี้: คลาส Socket และ ServerSocket หรือ “สวัสดีเซิร์ฟเวอร์?  คุณได้ยินฉันไหม?"  - 2ที่นี่ฉันจะพยายามให้คุณเข้าใจพื้นฐานของแอปพลิเคชันไคลเอนต์ - เซิร์ฟเวอร์ที่ใช้ซ็อกเก็ต Java โดยใช้ตัวอย่างการออกแบบแชท ในหลักสูตร Javarash คุณจะทำการแชท มันจะอยู่ในระดับที่แตกต่างอย่างสิ้นเชิง สวยงาม ใหญ่ มัลติฟังก์ชั่น แต่ก่อนอื่น คุณต้องวางรากฐานเสมอ ดังนั้นเราจำเป็นต้องค้นหาว่าอะไรเป็นรากฐานของส่วนดังกล่าว (หากคุณพบข้อบกพร่องหรือข้อผิดพลาดใด ๆ เขียนใน PM หรือในความคิดเห็นใต้บทความ) เอาล่ะ. หัวข้อที่หนึ่ง: “บ้านที่…” เพื่ออธิบายว่าการเชื่อมต่อเครือข่ายเกิดขึ้นระหว่างเซิร์ฟเวอร์และไคลเอนต์รายหนึ่งได้อย่างไร มาดูตัวอย่างคลาสสิกของอาคารอพาร์ตเมนต์กันดีกว่า สมมติว่าไคลเอนต์จำเป็นต้องสร้างการเชื่อมต่อกับเซิร์ฟเวอร์เฉพาะ ผู้ค้นหาจำเป็นต้องรู้อะไรบ้างเกี่ยวกับวัตถุการค้นหา ใช่ที่อยู่ เซิร์ฟเวอร์ไม่ใช่เอนทิตีที่มีมนต์ขลังบนคลาวด์ ดังนั้นจึงต้องอยู่บนเครื่องเฉพาะ โดยการอุปมากับบ้านซึ่งควรมีการประชุมสองฝ่ายที่ตกลงกันไว้ และเพื่อที่จะพบกันในอาคารอพาร์ตเมนต์ที่อยู่ของอาคารเดียวไม่เพียงพอคุณต้องระบุหมายเลขอพาร์ทเมนต์ที่จะจัดการประชุม ในทำนองเดียวกัน บนคอมพิวเตอร์เครื่องหนึ่งสามารถมีเซิร์ฟเวอร์ได้หลายเครื่องพร้อมกัน และเพื่อให้ไคลเอนต์ติดต่อกับเครื่องใดเครื่องหนึ่ง เขาจำเป็นต้องระบุหมายเลขพอร์ตที่จะใช้ในการเชื่อมต่อด้วย ดังนั้นที่อยู่และหมายเลขพอร์ต ที่อยู่หมายถึงตัวระบุของเครื่องในพื้นที่อินเทอร์เน็ต อาจเป็นชื่อโดเมน เช่น"javarush.ru"หรือ IP ปกติ ท่าเรือ- หมายเลขเฉพาะที่เกี่ยวข้องกับซ็อกเก็ตเฉพาะ (คำนี้จะกล่าวถึงในภายหลัง) กล่าวอีกนัยหนึ่งคือบริการบางอย่างถูกครอบครองเพื่อให้สามารถใช้เพื่อติดต่อได้ ดังนั้นเพื่อให้วัตถุอย่างน้อยสองชิ้นมาบรรจบกันในอาณาเขตของหนึ่ง (เซิร์ฟเวอร์) เจ้าของพื้นที่ (เซิร์ฟเวอร์) จะต้องครอบครองอพาร์ทเมนต์เฉพาะ (ท่าเรือ) บนนั้น (รถยนต์) และอย่างที่สองจะต้องค้นหาสถานที่นัดพบที่รู้ ที่อยู่ของบ้าน (โดเมนหรือ IP) และหมายเลขอพาร์ตเมนต์ (พอร์ต) หัวเรื่องที่สอง: พบกับ Socket ในบรรดาแนวคิดและคำศัพท์ที่เกี่ยวข้องกับการทำงานบนเครือข่าย สิ่งหนึ่งที่สำคัญมากคือ Socket หมายถึงจุดที่เกิดการเชื่อมต่อ พูดง่ายๆ ก็คือ ซ็อกเก็ตเชื่อมต่อสองโปรแกรมบนเครือข่าย ชั้นเรียนSocketนำแนวคิดของซ็อกเก็ตไปใช้ ไคลเอนต์และเซิร์ฟเวอร์จะสื่อสารผ่านช่องสัญญาณอินพุต/เอาท์พุต: คลาส Socket และ ServerSocket หรือ “สวัสดีเซิร์ฟเวอร์?  คุณได้ยินฉันไหม?"  - 3 คลาสนี้ถูกประกาศบนฝั่งไคลเอ็นต์ และเซิร์ฟเวอร์สร้างขึ้นใหม่โดยรับสัญญาณการเชื่อมต่อ นี่คือวิธีการทำงานของการสื่อสารออนไลน์ เริ่มต้นด้วย นี่คือตัวสร้างคลาสที่เป็นไปได้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 จริง คุณจำเป็นต้องมีเซิร์ฟเวอร์ที่มีคุณสมบัติครบถ้วน และเนื่องจาก:
  1. การสนทนาในอนาคตของเราในฐานะยูทิลิตี้ไม่มีความสามารถดังกล่าว สามารถสร้างการเชื่อมต่อและรับ/ส่งข้อความเท่านั้น นั่นคือมันไม่มีความสามารถเซิร์ฟเวอร์จริง
  2. เซิร์ฟเวอร์ของเราซึ่งมีเฉพาะข้อมูลซ็อกเก็ตและสตรีม I/O ไม่สามารถทำงานเป็นเว็บหรือเซิร์ฟเวอร์ FTP จริงได้ ดังนั้นเพียงเท่านี้เราก็จะไม่สามารถเชื่อมต่อผ่านอินเทอร์เน็ตได้
นอกจากนี้ เราเพิ่งเริ่มพัฒนาโปรแกรม ซึ่งหมายความว่ามันไม่เสถียรพอที่จะทำงานกับเครือข่ายจริงได้ทันที ดังนั้นเราจะใช้โลคัลโฮสต์เป็นที่อยู่การเชื่อมต่อ ตามทฤษฎีแล้วไคลเอนต์และเซิร์ฟเวอร์จะยังคงไม่เชื่อมต่อกัน แต่อย่างใดยกเว้นผ่านซ็อกเก็ต แต่สำหรับการดีบักโปรแกรมพวกเขาจะอยู่บนเครื่องเดียวกันโดยไม่มีการติดต่อจริงผ่านเครือข่าย เพื่อระบุในตัวสร้างSocketว่าที่อยู่อยู่ในเครื่อง มี 2 วิธี:
  1. เขียน “localhost” เป็นอาร์กิวเมนต์ที่อยู่ ซึ่งหมายถึงต้นขั้วท้องถิ่น “127.0.0.1” ก็เหมาะสำหรับสิ่งนี้เช่นกัน - นี่เป็นเพียงรูปแบบดิจิทัลของต้นขั้ว
  2. การใช้ InetAddress:
    1. InetAddress.getByName(null)- ชี้ null ไปยัง localhost
    2. InetAddress.getByName("localhost")
    3. InetAddress.getByName("127.0.0.1")
เพื่อความง่าย เราจะใช้ "localhost" ประเภท String แต่ตัวเลือกอื่น ๆ ทั้งหมดก็สามารถใช้ได้เช่นกัน หัวหน้าที่หก: ถึงเวลาสำหรับการสนทนา ดังนั้นเราจึงมีทุกสิ่งที่จำเป็นในการดำเนินการเซสชันการสนทนากับเซิร์ฟเวอร์แล้ว สิ่งที่เหลืออยู่คือการรวบรวมเข้าด้วยกัน: รายการต่อไปนี้แสดงวิธีที่ไคลเอนต์เชื่อมต่อกับเซิร์ฟเวอร์ ส่งข้อความถึงมัน และเซิร์ฟเวอร์ในทางกลับกันก็ยืนยันว่าได้รับข้อความโดยใช้เป็นอาร์กิวเมนต์ใน: "Server. ชวา"
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จึงหมายความว่าสามารถเปิดใช้งานแยกจากเซิร์ฟเวอร์และขนานกับไคลเอนต์อื่น ๆ ได้ สิ่งนี้ให้อะไรเราบ้าง? จำเป็นต้องมีการเชื่อมต่อใหม่แต่ละครั้งเซิร์ฟเวอร์จะไม่ไปที่การสื่อสารทันที แต่เขียนการเชื่อมต่อนี้ลงในรายการบางประเภทและดำเนินการรอการเชื่อมต่อใหม่และบริการเสริมบางประเภทมีส่วนร่วมในการสื่อสารกับเฉพาะ ลูกค้า. และไคลเอนต์จะต้องเขียนถึงเซิร์ฟเวอร์และรอการตอบกลับโดยแยกจากกัน กระทู้มาช่วยเหลือ สมมติว่าเรามีคลาสที่รับผิดชอบในการจดจำการเชื่อมต่อใหม่: มันควรจะระบุสิ่งต่อไปนี้:
  1. หมายเลขพอร์ต.
  2. รายการที่เขียนการเชื่อมต่อใหม่
  3. และ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) {
// ...
}
เธรดเซิร์ฟเวอร์ได้สร้างสตรีมแล้วและสามารถส่งกระแสข้อมูลเอาต์พุตเป็นอาร์กิวเมนต์ได้ ถัดไป คุณเพียงแค่ต้องผ่านทุกสิ่งที่ต้องโอนไปยังไคลเอนต์ใหม่ในรอบการค้นหา สรุป: นี่เป็นเพียงพื้นฐาน และมีแนวโน้มว่าสถาปัตยกรรมการแชทนี้จะไม่ทำงานเมื่อสร้างแอปพลิเคชันจริง โปรแกรมนี้ถูกสร้างขึ้นเพื่อการศึกษาและฉันได้แสดงให้เห็นว่าคุณสามารถทำให้ไคลเอนต์สื่อสารกับเซิร์ฟเวอร์ได้อย่างไร (และในทางกลับกัน) วิธีการทำเช่นนี้สำหรับการเชื่อมต่อต่างๆ และแน่นอน วิธีจัดระเบียบสิ่งนี้บนซ็อกเก็ต แหล่งที่มาจะถูกจัดเรียงใหม่ด้านล่าง และแนบซอร์สโค้ดของโปรแกรมที่กำลังวิเคราะห์ด้วย นี่เป็นประสบการณ์ครั้งแรกของฉันในการเขียนบทความ) ขอบคุณสำหรับความสนใจของคุณ :)
  1. การคิดใน Java Enterprise โดย Bruce Eckel และ อัล. 2546
  2. Java 8, The Complete Guide, Herbert Schildt, ฉบับที่ 9, 2017 (บทที่ 22)
  3. การเขียนโปรแกรมซ็อกเก็ตใน บทความ Java เกี่ยวกับซ็อกเก็ต
  4. ซ็อกเก็ตในเอกสารอย่างเป็นทางการ
  5. ServerSocket ในเอกสารอย่างเป็นทางการ
  6. แหล่งที่มาบน GitHub
ความคิดเห็น
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION