ยินดีต้อนรับสู่โลกอันแสนวิเศษของการพกพา ... หรือมากกว่านั้น ก่อนที่เราจะเริ่มวิเคราะห์รายละเอียดสองตัวเลือกเหล่านี้และมองลึกลงไปว่าระบบปฏิบัติการที่แตกต่างกันจัดการพวกเขาควรสังเกตว่าการใช้ซ็อกเก็ต BSD เป็นแม่ของการใช้ซ็อกเก็ตทั้งหมด โดยทั่วไประบบอื่น ๆ ทั้งหมดจะคัดลอกการใช้ซ็อกเก็ต BSD ในบางช่วงเวลา (หรืออย่างน้อยก็อินเตอร์เฟส) จากนั้นก็เริ่มพัฒนามันเอง แน่นอนว่าการติดตั้งซ็อกเก็ต BSD นั้นได้รับการพัฒนาเช่นกันในเวลาเดียวกันดังนั้นระบบที่คัดลอกมันในภายหลังนั้นมีคุณสมบัติที่ขาดในระบบที่คัดลอกมาก่อนหน้านี้ การทำความเข้าใจการใช้ซ็อกเก็ต BSD เป็นกุญแจสำคัญในการทำความเข้าใจการใช้ซ็อกเก็ตอื่น ๆ ทั้งหมดดังนั้นคุณควรอ่านเกี่ยวกับเรื่องนี้แม้ว่าคุณจะไม่สนใจเขียนโค้ดสำหรับระบบ BSD ก็ตาม
มีพื้นฐานสองสามข้อที่คุณควรรู้ก่อนที่เราจะดูตัวเลือกทั้งสองนี้ การเชื่อมต่อ TCP / UDP มีการระบุโดย tuple ของห้าค่า:
{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
การรวมกันที่ไม่ซ้ำกันของค่าเหล่านี้จะระบุการเชื่อมต่อ เป็นผลให้ไม่มีการเชื่อมต่อสองรายการที่สามารถมีค่าห้าค่าเดียวกันมิฉะนั้นระบบจะไม่สามารถแยกแยะการเชื่อมต่อเหล่านี้ได้อีกต่อไป
โปรโตคอลของซ็อกเก็ตถูกตั้งค่าเมื่อซ็อกเก็ตถูกสร้างขึ้นด้วยsocket()
ฟังก์ชั่น แหล่งที่อยู่และพอร์ตถูกตั้งค่าด้วยbind()
ฟังก์ชัน ที่อยู่ปลายทางและพอร์ตถูกตั้งค่าด้วยconnect()
ฟังก์ชั่น เนื่องจาก UDP เป็นโปรโตคอลที่ไม่มีการเชื่อมต่อจึงสามารถใช้ซ็อกเก็ต UDP ได้โดยไม่ต้องเชื่อมต่อ แต่ก็สามารถเชื่อมต่อได้และในบางกรณีมีข้อได้เปรียบอย่างมากสำหรับรหัสและการออกแบบแอปพลิเคชันทั่วไปของคุณ ในโหมดการเชื่อมต่อซ็อกเก็ต UDP ที่ไม่ได้ผูกไว้อย่างชัดเจนเมื่อข้อมูลถูกส่งไปเป็นครั้งแรกมักจะถูกผูกไว้โดยอัตโนมัติโดยระบบเนื่องจากซ็อกเก็ต UDP ที่ไม่ถูกผูกไว้ไม่สามารถรับข้อมูลใด ๆ (ตอบกลับ) เช่นเดียวกับซ็อกเก็ต TCP ที่ไม่ได้ผูกไว้จะถูกผูกไว้โดยอัตโนมัติก่อนที่จะเชื่อมต่อ
หากคุณผูกซ็อกเก็ตอย่างชัดเจนเป็นไปได้ที่จะผูกเข้ากับพอร์ต0
ซึ่งหมายถึง "พอร์ตใด ๆ " เนื่องจากซ็อกเก็ตไม่สามารถผูกกับพอร์ตที่มีอยู่ทั้งหมดได้จริงระบบจะต้องเลือกพอร์ตเฉพาะในกรณีนั้น (โดยทั่วไปจะเป็นช่วงที่มาจากช่วงที่กำหนดไว้ล่วงหน้าของระบบปฏิบัติการของพอร์ตต้นทาง) มีไวด์การ์ดที่คล้ายกันสำหรับที่อยู่ต้นทางซึ่งสามารถเป็น "ที่อยู่ใดก็ได้" ( 0.0.0.0
ในกรณีของ IPv4 และ::
ในกรณีของ IPv6) ซึ่งแตกต่างจากในกรณีของพอร์ตซ็อกเก็ตสามารถถูกผูกไว้กับ "ที่อยู่ใด ๆ " ซึ่งหมายถึง "ที่อยู่ IP ของแหล่งที่มาทั้งหมดของการเชื่อมต่อท้องถิ่นทั้งหมด" หากเชื่อมต่อซ็อกเก็ตในภายหลังระบบจะต้องเลือกที่อยู่ IP ของแหล่งที่มาที่เฉพาะเจาะจงเนื่องจากซ็อกเก็ตไม่สามารถเชื่อมต่อและในเวลาเดียวกันถูกผูกไว้กับที่อยู่ IP ท้องถิ่นใด ๆ ขึ้นอยู่กับที่อยู่ปลายทางและเนื้อหาของตารางเส้นทางระบบจะเลือกที่อยู่ต้นทางที่เหมาะสมและแทนที่การผูก "ใด ๆ " ด้วยการผูกกับที่อยู่ IP ต้นทางที่เลือก
ตามค่าเริ่มต้นแล้วไม่มีซ็อกเก็ตสองตัวที่สามารถรวมกันเป็นที่อยู่เดียวกันและแหล่งที่มาของพอร์ตได้ ตราบใดที่พอร์ตต้นทางแตกต่างกันที่อยู่ของแหล่งข้อมูลนั้นไม่เกี่ยวข้องจริง ๆ มีผลผูกพันsocketA
ไปA:X
และsocketB
เพื่อB:Y
ที่A
และB
ที่อยู่และX
และY
มีพอร์ตเป็นไปได้เสมอตราบใดที่X != Y
ถือเป็นจริง อย่างไรก็ตามแม้ว่าX == Y
การเชื่อมโยงยังคงเป็นไปได้ตราบใดที่ยังคงเป็นA != B
จริง เช่นsocketA
เป็นของโปรแกรมเซิร์ฟเวอร์ FTP และถูกผูกไว้192.168.0.1:21
และsocketB
เป็นของโปรแกรมเซิร์ฟเวอร์ FTP อื่นและถูกผูกไว้กับ10.0.0.1:21
การผูกทั้งสองจะประสบความสำเร็จ โปรดจำไว้ว่าซ็อกเก็ตอาจถูกผูกไว้กับ "ที่อยู่ใดก็ได้" ในเครื่อง หากซ็อกเก็ตถูกผูกไว้กับ0.0.0.0:21
มันถูกผูกไว้กับโลคัลแอดเดรสที่มีอยู่ทั้งหมดในเวลาเดียวกันและในกรณีนั้นจะไม่สามารถเชื่อมต่อซ็อกเก็ตอื่นกับพอร์ต21
ไม่ว่าจะใช้ที่อยู่ IP ใดเฉพาะที่มันพยายามผูกไว้เนื่องจาก0.0.0.0
ขัดแย้งกับที่อยู่ IP ท้องถิ่นที่มีอยู่ทั้งหมด
อะไรก็ตามที่บอกว่าเท่ากันสำหรับระบบปฏิบัติการหลักทั้งหมด สิ่งต่าง ๆ เริ่มที่จะได้รับเฉพาะระบบปฏิบัติการเมื่อนำมาใช้ซ้ำที่อยู่เข้ามาเล่น เราเริ่มต้นด้วย BSD เนื่องจากที่ฉันได้กล่าวไว้ข้างต้นเป็นแม่ของการใช้ซ็อกเก็ตทั้งหมด
BSD
SO_REUSEADDR
หากSO_REUSEADDR
เปิดใช้งานบนซ็อกเก็ตก่อนที่จะมีผลผูกพันมันซ็อกเก็ตสามารถจะผูกพันประสบความสำเร็จจนกว่าจะมีความขัดแย้งกับซ็อกเก็ตอีกผูกไว้กับว่าการรวมกันเดียวกันของแหล่งที่อยู่และพอร์ต ตอนนี้คุณอาจสงสัยว่ามันแตกต่างจากครั้งก่อน ๆ อย่างไร? คำหลักคือ "ตรง" SO_REUSEADDR
ส่วนใหญ่จะเปลี่ยนวิธีการจัดการที่อยู่ของสัญลักษณ์แทน ("ที่อยู่ IP ใด ๆ ") เมื่อค้นหาข้อขัดแย้ง
โดยไม่ต้องSO_REUSEADDR
มีผลผูกพันsocketA
ไป0.0.0.0:21
แล้วมีผลผูกพันsocketB
ไป192.168.0.1:21
จะล้มเหลว (ที่มีข้อผิดพลาดEADDRINUSE
) ตั้งแต่ 0.0.0.0 หมายถึง "ใด ๆ ที่อยู่ในท้องถิ่น" จึงอยู่ IP ทั้งหมดในท้องถิ่นได้รับการพิจารณาในการใช้งานโดยซ็อกเก็ตนี้และรวมถึง192.168.0.1
อีกด้วย ด้วยการSO_REUSEADDR
จะประสบความสำเร็จตั้งแต่0.0.0.0
และ192.168.0.1
จะไม่ตรงกับที่อยู่เดียวกันหนึ่งคือสัญลักษณ์แทนสำหรับที่อยู่ภายในทั้งหมดและคนอื่น ๆ ที่เป็นอยู่ในท้องถิ่นที่เฉพาะเจาะจงมาก โปรดทราบว่าข้อความข้างต้นเป็นจริงโดยไม่คำนึงถึงลำดับsocketA
และการsocketB
เชื่อมโยง หากปราศจากSO_REUSEADDR
มันจะล้มเหลวเสมอโดยSO_REUSEADDR
จะประสบความสำเร็จเสมอ
หากต้องการให้ภาพรวมที่ดีขึ้นให้สร้างตารางที่นี่และแสดงรายการชุดค่าผสมที่เป็นไปได้ทั้งหมด:
ซ็อกเก็ต SO_REUSEADDR ซ็อกเก็ต A ผลลัพธ์ B
-------------------------------------------------- -------------------
เปิด / ปิด 192.168.0.1:21 ข้อผิดพลาด 192.168.0.1:21 (EADDRINUSE)
เปิด / ปิด 192.168.0.1:21 10.0.0.1:21 ตกลง
เปิด / ปิด 10.0.0.1:21 192.168.0.1:21 ตกลง
ปิด 0.0.0.0:21 ข้อผิดพลาด 192.168.1.0:21 (EADDRINUSE)
ปิด 192.168.1.0:21 0.0.0.0:21 ข้อผิดพลาด (EADDRINUSE)
ON 0.0.0.0:21 192.168.1.0:21 ตกลง
ON 192.168.1.0:21 0.0.0.0:21 ตกลง
เปิด / ปิด 0.0.0.0:21 0.0.0.0:21 ข้อผิดพลาด (EADDRINUSE)
ตารางด้านบนถือว่าสมมติว่าsocketA
มีการเชื่อมโยงกับที่อยู่ที่กำหนดสำเร็จsocketA
จากนั้นsocketB
จะถูกสร้างขึ้นไม่ว่าจะได้รับการSO_REUSEADDR
ตั้งค่าหรือไม่และในที่สุดก็ถูกผูกไว้กับที่อยู่ที่ให้socketB
ไว้ เป็นผลมาจากการดำเนินการผูกสำหรับResult
socketB
หากคอลัมน์แรกบอกว่าON/OFF
ค่าของSO_REUSEADDR
ไม่เกี่ยวข้องกับผลลัพธ์
โอเคมีSO_REUSEADDR
ผลกับที่อยู่ไวด์การ์ดดีรู้ แต่นั่นไม่ได้เป็นเพียงผลกระทบที่มี มีเอฟเฟกต์อื่นที่รู้จักกันดีซึ่งเป็นเหตุผลว่าทำไมคนส่วนใหญ่ที่ใช้SO_REUSEADDR
ในโปรแกรมเซิร์ฟเวอร์ในตอนแรก สำหรับการใช้งานที่สำคัญอื่น ๆ ของตัวเลือกนี้เราต้องดูให้ลึกซึ้งยิ่งขึ้นเกี่ยวกับวิธีการทำงานของโปรโตคอล TCP
ซ็อกเก็ตมีบัฟเฟอร์การส่งและหากการเรียกใช้send()
ฟังก์ชันสำเร็จไม่ได้หมายความว่าข้อมูลที่ร้องขอได้ถูกส่งออกไปจริงๆแล้วมันหมายถึงข้อมูลที่ถูกเพิ่มเข้ากับบัฟเฟอร์การส่งเท่านั้น สำหรับซ็อกเก็ต UDP ข้อมูลมักจะถูกส่งไปในไม่ช้าถ้าไม่ใช่ในทันที แต่สำหรับซ็อกเก็ต TCP อาจมีความล่าช้าค่อนข้างนานในการเพิ่มข้อมูลลงในบัฟเฟอร์ส่งและการใช้ TCP ส่งข้อมูลนั้นจริงๆ ดังนั้นเมื่อคุณปิดซ็อกเก็ต TCP อาจยังมีข้อมูลที่ค้างอยู่ในบัฟเฟอร์การส่งซึ่งยังไม่ได้ส่ง แต่รหัสของคุณจะพิจารณาว่าเป็นส่งตั้งแต่send()
โทรสำเร็จแล้ว หากการใช้งาน TCP กำลังปิดซ็อกเก็ตทันทีตามคำขอของคุณข้อมูลทั้งหมดนี้จะสูญหายและรหัสของคุณจะไม่ทราบด้วยซ้ำ TCP ถูกกล่าวว่าเป็นโปรโตคอลที่เชื่อถือได้และสูญเสียข้อมูลเช่นเดียวกับที่ไม่น่าเชื่อถือมาก นั่นเป็นสาเหตุที่ซ็อกเก็ตที่ยังมีข้อมูลที่จะส่งจะเข้าสู่สถานะที่เรียกว่าTIME_WAIT
เมื่อคุณปิด ในสถานะนั้นมันจะรอจนกว่าข้อมูลที่รอดำเนินการทั้งหมดได้รับการส่งเรียบร้อยแล้วหรือจนกว่าจะถึงเวลาที่จะหมดในกรณีที่ซ็อกเก็ตถูกปิดอย่างแข็งขัน
จำนวนเวลาเคอร์เนลจะรอก่อนที่จะปิดซ็อกเก็ตโดยไม่คำนึงถึงว่ามันยังคงมีข้อมูลในการบินหรือไม่เรียกว่าเวลา Linger Linger เวลาเป็นทั่วโลกที่กำหนดในระบบส่วนใหญ่และโดยค่าเริ่มต้นค่อนข้างยาว (สองนาทีเป็นค่าทั่วไปคุณจะพบในระบบจำนวนมาก) นอกจากนี้ยังสามารถกำหนดค่าต่อซ็อกเก็ตโดยใช้ตัวเลือกซ็อกเก็ตSO_LINGER
ซึ่งสามารถใช้ในการทำให้การหมดเวลาสั้นลงหรือนานขึ้นและแม้กระทั่งการปิดการใช้งานอย่างสมบูรณ์ การปิดใช้งานนั้นเป็นความคิดที่ไม่ดีนักเนื่องจากการปิดซ็อกเก็ต TCP เป็นกระบวนการที่ซับซ้อนเล็กน้อยและเกี่ยวข้องกับการส่งแพ็กเก็ตกลับมาและกลับมาสองครั้ง (เช่นเดียวกับการส่งแพ็กเก็ตซ้ำในกรณีที่สูญหาย) จะถูก จำกัด ด้วยเวลา Linger. หากคุณปิดการใช้งานซ็อกเก็ตของคุณอาจไม่เพียง แต่สูญเสียข้อมูลในการบินเท่านั้น แต่ยังปิดอย่างแน่นหนาแทนการใช้อย่างสง่างามซึ่งมักไม่แนะนำให้ใช้ รายละเอียดเกี่ยวกับวิธีปิดการเชื่อมต่อ TCP อย่างงดงามเกินขอบเขตของคำตอบนี้หากคุณต้องการเรียนรู้เพิ่มเติมฉันขอแนะนำให้คุณดูที่หน้านี้ และแม้ว่าคุณจะปิดการใช้งาน lingering ด้วยSO_LINGER
หากกระบวนการของคุณตายโดยไม่ปิดซ็อกเก็ตอย่างชัดแจ้ง BSD (และระบบอื่น ๆ ที่เป็นไปได้) จะยังคงอยู่โดยไม่สนใจสิ่งที่คุณได้กำหนดค่าไว้ สิ่งนี้จะเกิดขึ้นเช่นถ้ารหัสของคุณเพิ่งโทรexit()
(โดยทั่วไปมักใช้กับโปรแกรมเซิร์ฟเวอร์ที่เรียบง่ายเล็ก ๆ ) หรือกระบวนการถูกทำลายโดยสัญญาณ (ซึ่งรวมถึงความเป็นไปได้ที่มันจะล่มเนื่องจากการเข้าถึงหน่วยความจำที่ผิดกฎหมาย) ดังนั้นจึงไม่มีอะไรที่คุณสามารถทำได้เพื่อให้แน่ใจว่าซ็อกเก็ตจะไม่อวดอ้างในทุกสถานการณ์
คำถามคือระบบปฏิบัติต่อซ็อกเก็ตในสถานะTIME_WAIT
อย่างไร หากSO_REUSEADDR
ไม่ได้ตั้งค่าซ็อกเก็ตที่อยู่ในสถานะTIME_WAIT
จะถือว่ายังคงถูกผูกไว้กับแหล่งที่อยู่และพอร์ตและความพยายามใด ๆ ที่จะผูกซ็อกเก็ตใหม่ไปยังที่อยู่เดียวกันและพอร์ตจะล้มเหลวจนกว่าจะปิดซ็อกเก็ตจริงซึ่งอาจใช้เวลานาน เป็นLinger Time ที่ตั้งค่าไว้ ดังนั้นอย่าคาดหวังว่าคุณสามารถเชื่อมโยงที่อยู่แหล่งที่มาของซ็อกเก็ตใหม่ได้ทันทีหลังจากปิดมัน ในกรณีส่วนใหญ่สิ่งนี้จะล้มเหลว อย่างไรก็ตามหากSO_REUSEADDR
มีการตั้งค่าสำหรับซ็อกเก็ตที่คุณพยายามเชื่อมต่อซ็อกเก็ตอื่นจะถูกผูกไว้กับที่อยู่และพอร์ตเดียวกันในสถานะTIME_WAIT
จะถูกละเว้นเพียงหลังจากทั้งหมด "ครึ่งตายแล้ว" และซ็อกเก็ตของคุณสามารถผูกกับที่อยู่เดียวกันโดยไม่มีปัญหาใด ๆ ในกรณีนั้นจะไม่มีบทบาทที่ซ็อกเก็ตอื่นอาจมีที่อยู่และพอร์ตเดียวกัน โปรดทราบว่าการเชื่อมต่อซ็อกเก็ตกับที่อยู่และพอร์ตเดียวกันกับซ็อกเก็ตที่กำลังจะตายในTIME_WAIT
สถานะที่ไม่คาดคิดและมักจะไม่พึงประสงค์ผลข้างเคียงในกรณีที่ซ็อกเก็ตอื่นยังคง "ทำงาน" แต่นั่นอยู่นอกเหนือขอบเขตของคำตอบนี้ โชคดีที่ผลข้างเคียงเหล่านั้นค่อนข้างหายากในทางปฏิบัติ
SO_REUSEADDR
มีสิ่งสุดท้ายที่คุณควรรู้เกี่ยวกับการเป็น ทุกอย่างที่เขียนด้านบนจะทำงานได้ตราบใดที่ซ็อกเก็ตที่คุณต้องการผูกไว้กับที่เปิดใช้งานการใช้ที่อยู่ซ้ำ ไม่จำเป็นว่าซ็อกเก็ตอื่นซึ่งถูกผูกไว้แล้วหรืออยู่ในTIME_WAIT
สถานะก็มีการตั้งค่าสถานะนี้เมื่อมันถูกผูกไว้ รหัสที่ตัดสินว่าการโยงจะสำเร็จหรือล้มเหลวจะตรวจสอบเฉพาะSO_REUSEADDR
ธงของซ็อกเก็ตที่ป้อนเข้าสู่การbind()
โทรเท่านั้นสำหรับซ็อกเก็ตอื่น ๆ ทั้งหมดที่ตรวจสอบแล้ว
SO_REUSEPORT
SO_REUSEPORT
เป็นสิ่งที่คนส่วนใหญ่คาดว่าSO_REUSEADDR
จะเป็น โดยทั่วไปSO_REUSEPORT
ช่วยให้คุณสามารถผูกจำนวนข้อของซ็อกเก็ตที่จะตรงกับแหล่งที่อยู่เดียวกันและพอร์ตตราบเท่าที่ทุกซ็อกเก็ตที่ถูกผูกไว้ก่อนนอกจากนี้ยังได้SO_REUSEPORT
ตั้งก่อนที่จะถูกผูกไว้ หากซ็อกเก็ตแรกที่ถูกผูกไว้กับที่อยู่และพอร์ตไม่ได้SO_REUSEPORT
ตั้งค่าจะไม่มีซ็อกเก็ตอื่นที่เชื่อมโยงกับที่อยู่และพอร์ตเดียวกันโดยไม่คำนึงว่าซ็อกเก็ตอื่นนั้นได้SO_REUSEPORT
ตั้งค่าไว้หรือไม่ ไม่เหมือนในกรณีของSO_REUESADDR
การจัดการรหัสSO_REUSEPORT
จะไม่เพียง แต่ตรวจสอบว่าซ็อกเก็ตที่ถูกผูกไว้ในปัจจุบันได้SO_REUSEPORT
ตั้ง แต่มันจะตรวจสอบว่าซ็อกเก็ตที่มีที่อยู่ที่ขัดแย้งและพอร์ตที่SO_REUSEPORT
ตั้งไว้เมื่อมันถูกผูกไว้
SO_REUSEPORT
SO_REUSEADDR
ไม่ได้หมายความถึง ซึ่งหมายความว่าหากซ็อกเก็ตไม่ได้SO_REUSEPORT
ตั้งค่าเมื่อมันถูกผูกไว้และซ็อกเก็ตอื่นได้SO_REUSEPORT
ตั้งค่าเมื่อมันถูกผูกไว้กับที่อยู่เดียวกันและพอร์ตเดียวกันผูกล้มเหลวซึ่งคาดว่า แต่มันก็ล้มเหลวถ้าซ็อกเก็ตอื่นกำลังจะตาย อยู่ในTIME_WAIT
สถานะ เพื่อให้สามารถผูกซ็อกเก็ตไปยังที่อยู่และพอร์ตเดียวกันได้เนื่องจากซ็อกเก็ตอื่นที่อยู่ในTIME_WAIT
สถานะต้องSO_REUSEADDR
ตั้งค่าบนซ็อกเก็ตนั้นหรือSO_REUSEPORT
ต้องตั้งค่าบนซ็อกเก็ตทั้งสองก่อนที่จะผูกเข้า แน่นอนว่ามันได้รับอนุญาตให้ตั้งค่าทั้งสองSO_REUSEPORT
และSO_REUSEADDR
บนซ็อกเก็ต
ไม่มีอะไรจะพูดเกี่ยวกับสิ่งSO_REUSEPORT
อื่นนอกเหนือจากที่เพิ่มเข้ามาในภายหลังSO_REUSEADDR
นั่นเป็นเหตุผลที่คุณจะไม่พบมันในการใช้งานซ็อกเก็ตของระบบอื่น ๆ ซึ่ง "แยก" รหัส BSD ก่อนที่จะเพิ่มตัวเลือกนี้และไม่มี วิธีการผูกสองซ็อกเก็ตไปยังที่อยู่ซ็อกเก็ตเดียวกันใน BSD ก่อนตัวเลือกนี้
เชื่อมต่อ () EADDRINUSE ที่ส่งคืนหรือไม่
คนส่วนใหญ่รู้ว่าbind()
อาจล้มเหลวพร้อมกับข้อผิดพลาดEADDRINUSE
อย่างไรก็ตามเมื่อคุณเริ่มเล่นกับการใช้ที่อยู่ซ้ำคุณอาจพบสถานการณ์แปลก ๆ ที่connect()
ล้มเหลวด้วยข้อผิดพลาดนั้นเช่นกัน สิ่งนี้จะเป็นอย่างไร ที่อยู่ระยะไกลจะทำอย่างไรหลังจากทั้งหมดนั่นคือสิ่งที่การเชื่อมต่อเพิ่มไปยังซ็อกเก็ตใช้งานอยู่แล้ว? การเชื่อมต่อซ็อกเก็ตหลายตัวเข้ากับที่อยู่ระยะไกลเดียวกันไม่เคยมีปัญหามาก่อนดังนั้นสิ่งที่ผิดพลาดที่นี่
ดังที่ฉันได้กล่าวไว้ด้านบนสุดของคำตอบของฉันการเชื่อมต่อนั้นถูกกำหนดโดยค่า tuple ห้าค่าโปรดจำไว้? และฉันก็บอกด้วยว่าค่าทั้งห้านี้ต้องไม่เหมือนกันมิฉะนั้นระบบไม่สามารถแยกการเชื่อมต่อสองสายได้อีกต่อไปใช่ไหม? ด้วยการใช้ที่อยู่ซ้ำคุณสามารถผูกสองซ็อกเก็ตของโปรโตคอลเดียวกันกับแหล่งที่อยู่และพอร์ตเดียวกัน นั่นหมายความว่าค่าสามค่าทั้งห้านั้นเหมือนกันสำหรับซ็อกเก็ตสองตัวนี้ หากคุณพยายามเชื่อมต่อซ็อกเก็ตทั้งสองนี้เข้ากับที่อยู่ปลายทางและพอร์ตเดียวกันคุณจะสร้างซ็อกเก็ตที่เชื่อมต่อสองซ็อกเก็ตซึ่งทูเปิลเหมือนกันทั้งหมด สิ่งนี้ไม่สามารถใช้งานได้อย่างน้อยก็ไม่ใช่สำหรับการเชื่อมต่อ TCP (การเชื่อมต่อ UDP ไม่ใช่การเชื่อมต่อจริง) หากข้อมูลมาถึงการเชื่อมต่อแบบใดแบบหนึ่งจากทั้งสองระบบจะไม่สามารถบอกได้ว่าการเชื่อมต่อนั้นเป็นข้อมูลใด
ดังนั้นถ้าคุณผูกสองซ็อกเก็ตของโปรโตคอลเดียวกันกับที่อยู่แหล่งที่มาและพอร์ตเดียวกันและพยายามเชื่อมต่อพวกเขาทั้งสองไปยังที่อยู่ปลายทางและพอร์ตเดียวกันconnect()
จริง ๆ แล้วจะล้มเหลวด้วยข้อผิดพลาดEADDRINUSE
สำหรับซ็อกเก็ตที่สองที่คุณพยายามเชื่อมต่อ ซ็อกเก็ตที่มี tuple เหมือนกันจากห้าค่าเชื่อมต่อแล้ว
ที่อยู่แบบหลายผู้รับ
คนส่วนใหญ่ไม่สนใจความจริงที่ว่าที่อยู่แบบหลายผู้รับมีอยู่ แต่มีอยู่จริง แม้ว่าที่อยู่ unicast จะใช้สำหรับการสื่อสารแบบหนึ่งต่อหนึ่ง แต่ที่อยู่แบบหลายผู้รับใช้สำหรับการสื่อสารแบบหนึ่งต่อหลายคน คนส่วนใหญ่ทราบที่อยู่แบบหลายผู้รับเมื่อเรียนรู้เกี่ยวกับ IPv6 แต่ที่อยู่แบบหลายผู้รับมีอยู่ใน IPv4 แม้ว่าคุณลักษณะนี้จะไม่ได้ใช้กันอย่างแพร่หลายในอินเทอร์เน็ตสาธารณะ
ความหมายของSO_REUSEADDR
การเปลี่ยนแปลงสำหรับมัลติคาสต์แอดเดรสเนื่องจากอนุญาตให้ซ็อกเก็ตหลายซ็อกเก็ตรวมกันกับที่อยู่และพอร์ตมัลติคาสต์เดียวกัน กล่าวอีกนัยหนึ่งสำหรับที่อยู่แบบหลายผู้รับSO_REUSEADDR
จะทำงานเหมือนกับที่SO_REUSEPORT
อยู่แบบหลายผู้รับ ที่จริงแล้วโค้ดจะปฏิบัติSO_REUSEADDR
และSO_REUSEPORT
เหมือนกันสำหรับที่อยู่แบบหลายผู้รับซึ่งหมายความว่าคุณสามารถพูดได้ว่ามันSO_REUSEADDR
หมายถึงSO_REUSEPORT
ที่อยู่แบบหลายผู้รับและวิธีอื่น ๆ
FreeBSD / OpenBSD / NetBSD
ทั้งหมดนี้ค่อนข้างจะล่าช้าสำหรับรหัส BSD ดั้งเดิมนั่นคือเหตุผลที่พวกเขาทั้งสามเสนอตัวเลือกเดียวกับ BSD และพวกเขาก็ทำงานแบบเดียวกับใน BSD
macOS (MacOS X)
ที่แกนกลาง macOS เป็นเพียงรูปแบบ BSD UNIX ที่ชื่อ " ดาร์วิน " ซึ่งอิงกับทางแยกของรหัส BSD (BSD 4.3) ที่ค่อนข้างช้าซึ่งต่อมาจะมีการซิงโครไนซ์กับ FreeBSD (ในเวลานั้น) รหัส 5 ฐานสำหรับรุ่น Mac OS 10.3 เพื่อให้ Apple สามารถได้รับการปฏิบัติตาม POSIX เต็มรูปแบบ (macOS ได้รับการรับรอง POSIX) แม้จะมี microkernel ที่แกนกลาง (" Mach ") ส่วนที่เหลือของเคอร์เนล (" XNU ") นั้นเป็นเพียงเคอร์เนล BSD และนั่นเป็นเหตุผลที่ว่าทำไม macOS จึงเสนอตัวเลือกเดียวกับ BSD และพวกมันก็ทำงานแบบเดียวกับ BSD .
iOS / watchOS / tvOS
iOS เป็นเพียง MacOS fork ที่มีเคอร์เนลที่ถูกปรับแต่งเล็กน้อยและถูกตัดแต่งบางส่วนทำให้ชุดเครื่องมือของผู้ใช้ว่างเปล่าและชุดเฟรมเวิร์กเริ่มต้นแตกต่างกันเล็กน้อย watchOS และ tvOS เป็นอุปกรณ์ส้อมของ iOS ที่ถูกถอดออกมากยิ่งขึ้น (โดยเฉพาะ watchOS) เพื่อความรู้ที่ดีที่สุดของฉันพวกเขาทุกคนทำงานได้อย่างที่ macOS
ลินุกซ์
Linux <3.9
ก่อนหน้า Linux 3.9 มีเพียงตัวเลือกSO_REUSEADDR
เท่านั้น ตัวเลือกนี้ทำงานโดยทั่วไปเหมือนกับใน BSD โดยมีข้อยกเว้นที่สำคัญสองข้อ:
ตราบใดที่ซ็อกเก็ต TCP กำลังรอรับฟัง (เซิร์ฟเวอร์) ถูกผูกไว้กับพอร์ตเฉพาะตัวSO_REUSEADDR
เลือกนั้นจะถูกละเว้นสำหรับซ็อกเก็ตทั้งหมดที่กำหนดเป้าหมายพอร์ตนั้น การเชื่อมต่อซ็อกเก็ตที่สองเข้ากับพอร์ตเดียวกันจะทำได้ก็ต่อเมื่อมันเป็นไปได้ใน BSD โดยไม่ต้องSO_REUSEADDR
ตั้งค่า เช่นคุณไม่สามารถเชื่อมโยงกับที่อยู่ตัวแทนแล้วหนึ่งที่เฉพาะเจาะจงมากขึ้นหรือรอบทางอื่น ๆ ทั้งที่เป็นไปได้ใน BSD SO_REUSEADDR
ถ้าคุณตั้งค่า สิ่งที่คุณสามารถทำได้คือคุณสามารถผูกเข้ากับพอร์ตเดียวกันและที่อยู่ที่ไม่ใช่ไวด์การ์ดสองที่แตกต่างกันตามที่ได้รับอนุญาตเสมอ ในแง่นี้ลินุกซ์มีข้อ จำกัด มากกว่า BSD
ข้อยกเว้นที่สองคือสำหรับซ็อกเก็ตไคลเอ็นต์ตัวเลือกนี้ทำงานเหมือนกับSO_REUSEPORT
ใน BSD ตราบใดที่ทั้งคู่ตั้งค่าสถานะนี้ไว้ก่อนที่จะถูกผูกไว้ เหตุผลในการอนุญาตให้ใช้งานนั้นเป็นเรื่องสำคัญที่จะต้องผูกหลายซ็อกเก็ตให้ตรงกับที่อยู่ซ็อกเก็ต UDP เดียวกันสำหรับโปรโตคอลต่างๆและอย่างที่เคยเป็นมาSO_REUSEPORT
ก่อนหน้า 3.9 พฤติกรรมของSO_REUSEADDR
การเปลี่ยนแปลงจึงถูกเติมลงในช่องว่างนั้น . ในแง่นั้นลินุกซ์มีข้อ จำกัด น้อยกว่า BSD
Linux> = 3.9
Linux 3.9 เพิ่มตัวเลือกSO_REUSEPORT
ใน Linux เช่นกัน ตัวเลือกนี้ทำงานเหมือนกับตัวเลือกใน BSD และอนุญาตให้รวมที่อยู่และหมายเลขพอร์ตเดียวกันได้ตราบใดที่ซ็อกเก็ตทั้งหมดได้ตั้งค่าตัวเลือกนี้ไว้ก่อนผูกพัน
ทว่าSO_REUSEPORT
ระบบอื่น ๆยังคงมีความแตกต่างกันสองประการ:
เพื่อป้องกัน "การไฮแจ็กพอร์ต" มีข้อ จำกัด พิเศษหนึ่งข้อ: ซ็อกเก็ตทั้งหมดที่ต้องการใช้ที่อยู่เดียวกันและการรวมกันของพอร์ตจะต้องเป็นของกระบวนการที่ใช้ ID ผู้ใช้ที่มีประสิทธิภาพเดียวกัน! ดังนั้นผู้ใช้รายหนึ่งจึงไม่สามารถ "ขโมย" พอร์ตของผู้ใช้รายอื่นได้ นี่เป็นเวทย์มนตร์พิเศษที่ชดเชยความสูญเสียSO_EXCLBIND
/ SO_EXCLUSIVEADDRUSE
ธง
นอกจากเคอร์เนลดำเนินการบางอย่าง "วิเศษพิเศษ" สำหรับSO_REUSEPORT
ซ็อกเก็ตที่ไม่พบในระบบปฏิบัติการอื่น ๆ : สำหรับซ็อกเก็ต UDP ก็พยายามที่จะแจกจ่ายดาต้าแกรมอย่างสม่ำเสมอสำหรับ TCP ฟังซ็อกเก็ตก็พยายามที่จะกระจายการร้องขอการเชื่อมต่อเข้ามา (ผู้ที่ได้รับการยอมรับโดยการเรียกaccept()
) ทั่วซ็อกเก็ตทั้งหมดที่ใช้ที่อยู่เดียวกันและพอร์ตร่วมกัน ดังนั้นแอปพลิเคชันสามารถเปิดพอร์ตเดียวกันในกระบวนการลูกหลายกระบวนการได้อย่างง่ายดายจากนั้นใช้SO_REUSEPORT
เพื่อให้การทำโหลดบาลานซ์มีราคาไม่แพงมาก
Android
แม้ว่าระบบ Android ทั้งหมดจะค่อนข้างแตกต่างจากลีนุกซ์ส่วนใหญ่, แต่ที่แกนทำงานเป็นลีนุกซ์เคอร์เนลที่ถูกปรับเปลี่ยนเล็กน้อย, ดังนั้นทุกอย่างที่ใช้กับลีนุกซ์ควรใช้กับ Android เช่นกัน.
ของ windows
Windows เท่านั้นรู้ว่าตัวเลือกไม่มีSO_REUSEADDR
SO_REUSEPORT
การตั้งค่าSO_REUSEADDR
บนซ็อกเก็ตในพฤติกรรมของ Windows เช่นการตั้งค่าSO_REUSEPORT
และSO_REUSEADDR
ซ็อกเก็ตใน BSD ที่มีข้อยกเว้น: ซ็อกเก็ตที่มีSO_REUSEADDR
สามารถเสมอผูกตรงที่อยู่เดียวกันแหล่งที่มาและพอร์ตเป็นซ็อกเก็ตที่ถูกผูกไว้แล้วแม้ว่าซ็อกเก็ตอื่น ๆ ที่ไม่ได้มีตัวเลือกนี้ ตั้งเมื่อมันถูกผูกไว้ ลักษณะการทำงานนี้ค่อนข้างอันตรายเนื่องจากอนุญาตให้แอปพลิเคชัน "ขโมย" พอร์ตที่เชื่อมต่อของแอปพลิเคชันอื่น จำเป็นต้องพูดสิ่งนี้อาจมีผลกระทบด้านความปลอดภัยที่สำคัญ SO_EXCLUSIVEADDRUSE
ไมโครซอฟท์ตระหนักว่านี่อาจจะมีปัญหาและทำให้เพิ่มตัวเลือกซ็อกเก็ตอีก การตั้งค่าSO_EXCLUSIVEADDRUSE
บนซ็อกเก็ตทำให้แน่ใจว่าหากการเชื่อมสำเร็จการรวมกันของแหล่งที่อยู่และพอร์ตจะเป็นเจ้าของโดยซ็อกเก็ตนี้เท่านั้นและไม่มีซ็อกเก็ตอื่นที่สามารถเชื่อมโยงกับพวกเขาได้แม้ว่าจะได้SO_REUSEADDR
ตั้งค่าไว้ก็ตาม
สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับวิธีการตั้งค่าสถานะSO_REUSEADDR
และการSO_EXCLUSIVEADDRUSE
ทำงานบน Windows วิธีที่พวกเขามีผลต่อการเชื่อมโยง / ผูกมัด Microsoft โปรดระบุตารางที่คล้ายกับตารางของฉันใกล้กับส่วนบนของคำตอบนั้น เพียงไปที่หน้านี้และเลื่อนลงเล็กน้อย อันที่จริงมีสามตารางหนึ่งอันแรกแสดงพฤติกรรมเก่า (ก่อนหน้า Windows 2003), หนึ่งในสองพฤติกรรม (Windows 2003 และสูงกว่า) และหนึ่งในสามแสดงให้เห็นว่าพฤติกรรมการเปลี่ยนแปลงใน Windows 2003 และในภายหลังถ้ามีการbind()
โทร ผู้ใช้ที่แตกต่างกัน
Solaris
Solaris เป็นผู้สืบทอดของ SunOS SunOS มีพื้นฐานมาจากทางแยกของ BSD, SunOS 5 และต่อมาขึ้นอยู่กับทางแยกของ SVR4 อย่างไรก็ตาม SVR4 นั้นเป็นการรวมกันของ BSD, System V, และ Xenix ดังนั้น Solaris ก็ยังเป็น BSD fork และ ค่อนข้างเร็ว เป็นผล Solaris รู้เพียงไม่มีSO_REUSEADDR
พฤติกรรมสวยมากเหมือนกันเช่นเดียวกับใน BSD เท่าที่ฉันรู้ว่าไม่มีวิธีใดที่จะทำให้เกิดพฤติกรรมเช่นเดียวกับใน Solaris นั่นหมายความว่ามันเป็นไปไม่ได้ที่จะผูกสองซ็อกเก็ตเข้ากับที่อยู่และพอร์ตเดียวกันSO_REUSEPORT
SO_REUSEADDR
SO_REUSEPORT
คล้ายกับ Windows, Solaris มีตัวเลือกให้ซ็อกเก็ตมีผลผูกพันเฉพาะ SO_EXCLBIND
ตัวเลือกนี้จะตั้งชื่อ ถ้าตัวเลือกนี้ถูกตั้งค่าบนซ็อกเก็ตก่อนที่จะผูกมันการตั้งค่าSO_REUSEADDR
ในซ็อกเก็ตอื่นจะไม่มีผลกระทบหากทั้งสองซ็อกเก็ตได้รับการทดสอบสำหรับความขัดแย้งที่อยู่ เช่นถ้าsocketA
ถูกผูกไว้กับที่อยู่ตัวแทนและsocketB
ได้SO_REUSEADDR
เปิดใช้งานและถูกผูกไว้กับที่อยู่ที่ไม่ใช่สัญลักษณ์แทนและพอร์ตเดียวกับsocketA
ผูกนี้โดยปกติจะประสบความสำเร็จเว้นแต่socketA
ได้SO_EXCLBIND
เปิดใช้งานในกรณีที่มันจะล้มเหลวโดยไม่คำนึงถึงธงSO_REUSEADDR
socketB
ระบบอื่น ๆ
ในกรณีที่ระบบของคุณไม่อยู่ในรายการด้านบนฉันได้เขียนโปรแกรมทดสอบเล็กน้อยที่คุณสามารถใช้เพื่อค้นหาวิธีที่ระบบของคุณจัดการกับตัวเลือกทั้งสองนี้ นอกจากนี้หากคุณคิดว่าผลลัพธ์ของฉันผิดโปรดเรียกใช้โปรแกรมก่อนที่จะโพสต์ความคิดเห็นใด ๆ และอาจทำการอ้างสิทธิ์ที่ผิด
ทั้งหมดที่รหัสต้องการสร้างเป็นบิต POSIX API (สำหรับชิ้นส่วนเครือข่าย) และคอมไพเลอร์ C99 (อันที่จริงแล้วคอมไพเลอร์ที่ไม่ใช่ C99 ส่วนใหญ่จะทำงานได้ตราบใดที่มีให้inttypes.h
และสนับสนุนstdbool.h
เช่นgcc
ยาวก่อนที่จะรองรับ C99 เต็ม) .
สิ่งที่โปรแกรมต้องใช้คืออย่างน้อยหนึ่งอินเทอร์เฟซในระบบของคุณ (นอกเหนือจากโลคัลอินเทอร์เฟซ) มีการกำหนดที่อยู่ IP และเส้นทางเริ่มต้นถูกตั้งค่าซึ่งใช้อินเตอร์เฟสนั้น โปรแกรมจะรวบรวมที่อยู่ IP นั้นและใช้เป็น "ที่อยู่เฉพาะ" ที่สอง
มันทดสอบชุดค่าผสมที่เป็นไปได้ทั้งหมดที่คุณสามารถคิดได้:
- โปรโตคอล TCP และ UDP
- ซ็อกเก็ตปกติฟัง (เซิร์ฟเวอร์) ซ็อกเก็ตมัลติคาสต์ซ็อกเก็ต
SO_REUSEADDR
ตั้งค่าบน socket1, socket2 หรือทั้งสองซ็อกเก็ต
SO_REUSEPORT
ตั้งค่าบน socket1, socket2 หรือทั้งสองซ็อกเก็ต
- การรวมที่อยู่ทั้งหมดที่คุณสามารถทำได้
0.0.0.0
(wildcard), 127.0.0.1
(ที่อยู่เฉพาะ) และที่อยู่เฉพาะที่สองที่พบในอินเทอร์เฟซหลักของคุณ (สำหรับมัลติคาสต์เป็นเพียง224.1.2.3
การทดสอบทั้งหมด)
และพิมพ์ผลลัพธ์ในตารางที่ดี มันจะทำงานบนระบบที่ไม่ทราบSO_REUSEPORT
ด้วยซึ่งในกรณีนี้ตัวเลือกนี้จะไม่ถูกทดสอบ
สิ่งที่โปรแกรมไม่สามารถทดสอบได้อย่างง่ายดายคือการSO_REUSEADDR
ทำงานของซ็อกเก็ตในTIME_WAIT
สถานะที่เป็นเรื่องยากมากในการบังคับและเก็บซ็อกเก็ตในสถานะนั้น โชคดีที่ระบบปฏิบัติการส่วนใหญ่ดูเหมือนว่าจะทำตัวเหมือน BSD ที่นี่และโปรแกรมเมอร์ส่วนใหญ่สามารถเพิกเฉยต่อการดำรงอยู่ของสถานะนั้นได้
นี่คือรหัส (ฉันไม่สามารถรวมไว้ที่นี่คำตอบมีขนาด จำกัด และรหัสจะผลักดันคำตอบนี้เกินขีด จำกัด )
INADDR_ANY
เชื่อมโยงกับไม่ได้ผูกที่อยู่ท้องถิ่นที่มีอยู่ แต่ที่อยู่ในอนาคตทั้งหมดเช่นกันlisten
สร้างซ็อกเก็ตอย่างแน่นอนด้วยโปรโตคอลที่แน่นอนที่อยู่ในพื้นที่และพอร์ตในพื้นที่เดียวกันแม้ว่าคุณจะบอกว่าไม่สามารถทำได้