SO_REUSEADDR và ​​SO_REUSEPORT khác nhau như thế nào?


663

Các man pagestài liệu và lập trình viên cho các tùy chọn ổ cắm SO_REUSEADDRSO_REUSEPORTkhác nhau đối với các hệ điều hành khác nhau và thường rất khó hiểu. Một số hệ điều hành thậm chí không có tùy chọn SO_REUSEPORT. WEB chứa đầy thông tin mâu thuẫn về chủ đề này và thường bạn có thể tìm thấy thông tin chỉ đúng với một ổ cắm của một hệ điều hành cụ thể, thậm chí có thể không được đề cập rõ ràng trong văn bản.

Vậy chính xác thì SO_REUSEADDRkhác như thế nào SO_REUSEPORT?

Là hệ thống mà không SO_REUSEPORTgiới hạn hơn?

Và chính xác hành vi dự kiến ​​là gì nếu tôi sử dụng một trong hai hệ điều hành khác nhau?

Câu trả lời:


1612

Chào mừng bạn đến với thế giới tuyệt vời của tính di động ... hay đúng hơn là thiếu nó. Trước khi chúng tôi bắt đầu phân tích chi tiết hai tùy chọn này và xem xét sâu hơn cách các hệ điều hành khác nhau xử lý chúng, cần lưu ý rằng việc triển khai ổ cắm BSD là mẹ của tất cả các cài đặt ổ cắm. Về cơ bản, tất cả các hệ thống khác đã sao chép việc triển khai ổ cắm BSD tại một số thời điểm (hoặc ít nhất là các giao diện của nó) và sau đó bắt đầu tự phát triển nó. Tất nhiên, việc triển khai ổ cắm BSD cũng được phát triển đồng thời và do đó các hệ thống sao chép nó sau này có các tính năng thiếu trong các hệ thống đã sao chép nó trước đó. Hiểu về triển khai ổ cắm BSD là chìa khóa để hiểu tất cả các cài đặt ổ cắm khác, vì vậy bạn nên đọc về nó ngay cả khi bạn không quan tâm đến việc viết mã cho hệ thống BSD.

Có một vài điều cơ bản bạn nên biết trước khi chúng ta xem xét hai lựa chọn này. Kết nối TCP / UDP được xác định bằng một bộ gồm năm giá trị:

{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}

Bất kỳ kết hợp duy nhất của các giá trị này xác định một kết nối. Kết quả là, không có hai kết nối nào có thể có cùng năm giá trị, nếu không hệ thống sẽ không thể phân biệt các kết nối này nữa.

Giao thức của một socket được thiết lập khi socket được tạo với socket()chức năng. Địa chỉ nguồn và cổng được thiết lập với bind()chức năng. Địa chỉ đích và cổng được đặt với connect()chức năng. Vì UDP là giao thức không kết nối, nên có thể sử dụng ổ cắm UDP mà không cần kết nối chúng. Tuy nhiên, nó được phép kết nối chúng và trong một số trường hợp rất thuận lợi cho mã và thiết kế ứng dụng chung của bạn. Trong chế độ không kết nối, các ổ cắm UDP không bị ràng buộc rõ ràng khi dữ liệu được gửi qua chúng lần đầu tiên thường được hệ thống tự động ràng buộc, vì ổ cắm UDP không liên kết không thể nhận bất kỳ dữ liệu (trả lời) nào. Điều tương tự cũng đúng với một socket TCP không liên kết, nó được tự động ràng buộc trước khi nó được kết nối.

Nếu bạn liên kết rõ ràng một ổ cắm, có thể liên kết nó với cổng 0, có nghĩa là "bất kỳ cổng nào". Do một ổ cắm thực sự không thể bị ràng buộc với tất cả các cổng hiện có, nên hệ thống sẽ phải chọn một cổng cụ thể trong trường hợp đó (thường là từ một phạm vi cổng nguồn cụ thể được xác định trước của hệ điều hành). Một ký tự đại diện tương tự tồn tại cho địa chỉ nguồn, có thể là "bất kỳ địa chỉ" nào ( 0.0.0.0trong trường hợp của IPv4 và::trong trường hợp IPv6). Không giống như trong trường hợp các cổng, một socket thực sự có thể bị ràng buộc với "bất kỳ địa chỉ" nào có nghĩa là "tất cả các địa chỉ IP nguồn của tất cả các giao diện cục bộ". Nếu ổ cắm được kết nối sau này, hệ thống phải chọn một địa chỉ IP nguồn cụ thể, vì ổ cắm không thể được kết nối và đồng thời bị ràng buộc với bất kỳ địa chỉ IP cục bộ nào. Tùy thuộc vào địa chỉ đích và nội dung của bảng định tuyến, hệ thống sẽ chọn một địa chỉ nguồn thích hợp và thay thế ràng buộc "bất kỳ" bằng một ràng buộc với địa chỉ IP nguồn được chọn.

Theo mặc định, không có hai ổ cắm có thể được liên kết với cùng một kết hợp địa chỉ nguồn và cổng nguồn. Miễn là cổng nguồn khác nhau, địa chỉ nguồn thực sự không liên quan. Liên kết socketAđến A:XsocketBđến B:Y, nơi ABlà địa chỉ XYlà cổng, luôn luôn có thể miễn là X != Yđúng. Tuy nhiên, ngay cả khi X == Y, ràng buộc vẫn có thể miễn là A != Bđúng. Ví dụ: socketAthuộc về chương trình máy chủ FTP và bị ràng buộc 192.168.0.1:21socketBthuộc về chương trình máy chủ FTP khác và bị ràng buộc 10.0.0.1:21, cả hai ràng buộc sẽ thành công. Tuy nhiên, hãy nhớ rằng một ổ cắm có thể bị ràng buộc cục bộ với "bất kỳ địa chỉ" nào. Nếu một ổ cắm bị ràng buộc với0.0.0.0:21, nó bị ràng buộc với tất cả các địa chỉ cục bộ hiện tại cùng một lúc và trong trường hợp đó, không có ổ cắm nào khác có thể bị ràng buộc với cổng 21, bất kể địa chỉ IP cụ thể nào mà nó cố gắng liên kết, như 0.0.0.0xung đột với tất cả các địa chỉ IP cục bộ hiện có.

Bất cứ điều gì nói cho đến nay là khá nhiều bằng nhau cho tất cả các hệ điều hành lớn. Mọi thứ bắt đầu để có được hệ điều hành cụ thể khi sử dụng lại địa chỉ. Chúng tôi bắt đầu với BSD, vì như tôi đã nói ở trên, nó là mẹ của tất cả các cài đặt ổ cắm.

BSD

SO_REUSEADDR

Nếu SO_REUSEADDRđược bật trên một ổ cắm trước khi ràng buộc nó, ổ cắm có thể được ràng buộc thành công trừ khi có xung đột với một ổ cắm khác bị ràng buộc chính xác cùng một tổ hợp địa chỉ nguồn và cổng. Bây giờ bạn có thể tự hỏi làm thế nào là khác bất kỳ trước? Từ khóa là "chính xác". SO_REUSEADDRchủ yếu thay đổi cách xử lý các địa chỉ ký tự đại diện ("bất kỳ địa chỉ IP" nào) khi tìm kiếm xung đột.

Nếu không có SO_REUSEADDR, ràng buộc socketAđể 0.0.0.0:21rồi ràng buộc socketBđể 192.168.0.1:21sẽ thất bại (với lỗi EADDRINUSE), vì 0.0.0.0 có nghĩa là "bất kỳ địa chỉ IP cục bộ", do đó tất cả các địa chỉ IP địa phương được coi là sử dụng bởi ổ cắm này và điều này bao gồm 192.168.0.1, quá. Với SO_REUSEADDRnó sẽ thành công, vì 0.0.0.0192.168.0.1không chính xác cùng một địa chỉ, một là một ký tự đại diện cho tất cả các địa chỉ địa phương và một trong những khác là một địa chỉ cục bộ rất cụ thể. Lưu ý rằng tuyên bố trên là đúng bất kể theo thứ tự nào socketAsocketBbị ràng buộc; không có SO_REUSEADDRnó sẽ luôn luôn thất bại, với SO_REUSEADDRnó sẽ luôn luôn thành công.

Để cung cấp cho bạn tổng quan tốt hơn, hãy tạo một bảng ở đây và liệt kê tất cả các kết hợp có thể:

SO_REUSEADDR socketA socketB Kết quả
-------------------------------------------------- -------------------
  BẬT / TẮT 192.168.0.1:21 192.168.0.1:21 Lỗi (EADDRINUSE)
  BẬT / TẮT 192.168.0.1:21 10.0.0.1:21 OK
  BẬT / TẮT 10.0.0.1:21 192.168.0.1:21 OK
   TẮT 0.0.0.0:21 192.168.1.0:21 Lỗi (EADDRINUSE)
   TẮT 192.168.1.0:21 0.0.0.0:21 Lỗi (EADDRINUSE)
   TRÊN 0.0.0.0:21 192.168.1.0:21 OK
   TRÊN 192.168.1.0:21 0.0.0.0:21 OK
  BẬT / TẮT 0.0.0.0:21 0.0.0.0:21 (EADDRINUSE)

Bảng ở trên giả định rằng socketAđã được liên kết thành công với địa chỉ đã cho socketA, sau đó socketBđược tạo, được SO_REUSEADDRđặt hoặc không, và cuối cùng được liên kết với địa chỉ đã cho socketB. Resultlà kết quả của hoạt động liên kết cho socketB. Nếu cột đầu tiên nói ON/OFF, giá trị của SO_REUSEADDRkhông liên quan đến kết quả.

Được rồi, SO_REUSEADDRcó ảnh hưởng đến địa chỉ ký tự đại diện, tốt để biết. Tuy nhiên, đó không phải là hiệu ứng duy nhất. Có một hiệu ứng nổi tiếng khác cũng là lý do tại sao hầu hết mọi người sử dụng SO_REUSEADDRtrong các chương trình máy chủ ở nơi đầu tiên. Đối với việc sử dụng quan trọng khác của tùy chọn này, chúng ta phải xem xét sâu hơn về cách thức hoạt động của giao thức TCP.

Ổ cắm có bộ đệm gửi và nếu cuộc gọi đến send()chức năng thành công, điều đó không có nghĩa là dữ liệu được yêu cầu thực sự đã được gửi đi, điều đó chỉ có nghĩa là dữ liệu đã được thêm vào bộ đệm gửi. Đối với ổ cắm UDP, dữ liệu thường được gửi khá sớm, nếu không phải ngay lập tức, nhưng đối với ổ cắm TCP, có thể có độ trễ tương đối dài giữa việc thêm dữ liệu vào bộ đệm gửi và việc triển khai TCP thực sự gửi dữ liệu đó. Kết quả là, khi bạn đóng ổ cắm TCP, vẫn có thể có dữ liệu đang chờ xử lý trong bộ đệm gửi, chưa được gửi nhưng mã của bạn coi nó là đã gửi, vìsend()gọi thành công. Nếu việc triển khai TCP đã đóng ổ cắm ngay lập tức theo yêu cầu của bạn, tất cả dữ liệu này sẽ bị mất và mã của bạn thậm chí sẽ không biết về điều đó. TCP được cho là một giao thức đáng tin cậy và mất dữ liệu như thế không đáng tin cậy lắm. Đó là lý do tại sao một ổ cắm vẫn còn dữ liệu cần gửi sẽ chuyển sang trạng thái được gọi TIME_WAITkhi bạn đóng nó. Ở trạng thái đó, nó sẽ đợi cho đến khi tất cả dữ liệu đang chờ xử lý được gửi thành công hoặc cho đến khi hết thời gian chờ, trong trường hợp đó, ổ cắm bị đóng mạnh.

Lượng thời gian mà hạt nhân sẽ đợi trước khi nó đóng ổ cắm, bất kể nó có còn dữ liệu trong chuyến bay hay không, được gọi là Thời gian kéo dài . Các Thời gian Linger là cấu hình trên toàn thế giới trên hầu hết các hệ thống và theo mặc định khá dài (hai phút là một giá trị chung bạn sẽ tìm thấy trên nhiều hệ thống). Nó cũng có thể được cấu hình trên mỗi ổ cắm bằng cách sử dụng tùy chọn ổ cắm SO_LINGERcó thể được sử dụng để làm cho thời gian chờ ngắn hơn hoặc dài hơn, và thậm chí để vô hiệu hóa hoàn toàn. Tuy nhiên, vô hiệu hóa nó hoàn toàn là một ý tưởng rất tồi, vì việc đóng một ổ cắm TCP một cách duyên dáng là một quá trình hơi phức tạp và liên quan đến việc gửi đi và gửi lại một vài gói (cũng như gửi lại các gói đó trong trường hợp chúng bị mất) và toàn bộ quá trình đóng này cũng bị giới hạn bởi Linger Time. Nếu bạn vô hiệu hóa kéo dài, ổ cắm của bạn có thể không chỉ mất dữ liệu trong chuyến bay, nó cũng luôn bị đóng mạnh thay vì duyên dáng, điều này thường không được khuyến khích. Các chi tiết về cách kết nối TCP được đóng một cách duyên dáng nằm ngoài phạm vi của câu trả lời này, nếu bạn muốn tìm hiểu thêm, tôi khuyên bạn nên xem trang này . Và ngay cả khi bạn vô hiệu hóa kéo dài SO_LINGER, nếu quy trình của bạn chết mà không đóng ổ cắm một cách rõ ràng, BSD (và có thể các hệ thống khác) vẫn sẽ nán lại, bỏ qua những gì bạn đã cấu hình. Điều này sẽ xảy ra ví dụ nếu mã của bạn chỉ gọiexit()(khá phổ biến đối với các chương trình máy chủ nhỏ, đơn giản) hoặc quá trình bị hủy bởi tín hiệu (bao gồm khả năng nó chỉ gặp sự cố do truy cập bộ nhớ bất hợp pháp). Vì vậy, không có gì bạn có thể làm để đảm bảo một ổ cắm sẽ không bao giờ tồn tại trong mọi trường hợp.

Câu hỏi là, làm thế nào để hệ thống xử lý một ổ cắm ở trạng thái TIME_WAIT? Nếu SO_REUSEADDRkhông được đặt, ổ cắm ở trạng thái TIME_WAITđược coi là vẫn bị ràng buộc với địa chỉ nguồn và cổng và mọi nỗ lực liên kết ổ cắm mới với cùng một địa chỉ và cổng sẽ thất bại cho đến khi ổ cắm thực sự bị đóng, có thể mất nhiều thời gian như Linger Time được cấu hình . Vì vậy, đừng hy vọng rằng bạn có thể đặt lại địa chỉ nguồn của ổ cắm ngay sau khi đóng nó. Trong hầu hết các trường hợp điều này sẽ thất bại. Tuy nhiên, nếu SO_REUSEADDRđược đặt cho ổ cắm mà bạn đang cố gắng liên kết, một ổ cắm khác bị ràng buộc với cùng một địa chỉ và cổng ở trạng tháiTIME_WAITchỉ đơn giản là bị bỏ qua, sau khi tất cả đã "chết một nửa" và ổ cắm của bạn có thể liên kết với cùng một địa chỉ mà không có vấn đề gì. Trong trường hợp đó, không có vai trò nào là ổ cắm khác có thể có cùng địa chỉ và cổng. Lưu ý rằng việc liên kết một ổ cắm với chính xác cùng một địa chỉ và cổng như một ổ cắm đang chết trong TIME_WAITtrạng thái có thể có những tác dụng phụ không mong muốn và thường không mong muốn trong trường hợp ổ cắm kia vẫn "hoạt động", nhưng điều đó nằm ngoài phạm vi của câu trả lời này và may mắn thay, những tác dụng phụ là khá hiếm trong thực tế.

Có một điều cuối cùng bạn nên biết SO_REUSEADDR. Tất cả mọi thứ được viết ở trên sẽ hoạt động miễn là ổ cắm bạn muốn liên kết có bật lại địa chỉ. Không nhất thiết là ổ cắm khác, ổ cắm đã bị ràng buộc hoặc đang ở TIME_WAITtrạng thái, cũng được đặt cờ này khi nó bị ràng buộc. Mã quyết định xem liên kết sẽ thành công hay thất bại chỉ kiểm tra SO_REUSEADDRcờ của ổ cắm được đưa vào bind()cuộc gọi, đối với tất cả các ổ cắm khác được kiểm tra, cờ này thậm chí không được nhìn.

SO_REUSEPORT

SO_REUSEPORTlà những gì hầu hết mọi người mong đợi SO_REUSEADDRđược. Về cơ bản, SO_REUSEPORTcho phép bạn để ràng buộc một số tùy ý các ổ cắm để chính xác địa chỉ cùng một nguồn và cổng miễn là tất cả các ổ cắm trước khi bị ràng buộc cũng đã SO_REUSEPORTthiết lập trước khi họ bị ràng buộc. Nếu ổ cắm đầu tiên bị ràng buộc với một địa chỉ và cổng chưa được SO_REUSEPORTđặt, thì không có ổ cắm nào khác có thể bị ràng buộc với cùng một địa chỉ và cổng đó, bất kể ổ cắm khác này có SO_REUSEPORTđược đặt hay không, cho đến khi ổ cắm đầu tiên phát hành lại liên kết của nó. Không giống như trong trường hợp SO_REUESADDRxử lý mã SO_REUSEPORTsẽ không chỉ xác minh rằng ổ cắm hiện bị ràng buộc đã SO_REUSEPORTđược đặt mà còn xác minh rằng ổ cắm có địa chỉ và cổng xung đột đã được SO_REUSEPORTđặt khi bị ràng buộc.

SO_REUSEPORTkhông ngụ ý SO_REUSEADDR. Điều này có nghĩa là nếu một ổ cắm không được SO_REUSEPORTđặt khi nó bị ràng buộc và một ổ cắm khác đã SO_REUSEPORTđược đặt khi nó bị ràng buộc với cùng một địa chỉ và cổng, thì liên kết sẽ thất bại, nhưng nó cũng thất bại nếu ổ cắm kia đã chết và đang ở trong TIME_WAITtrạng thái Để có thể liên kết một ổ cắm với cùng một địa chỉ và cổng như một ổ cắm khác ở TIME_WAITtrạng thái yêu cầu hoặc SO_REUSEADDRphải được đặt trên ổ cắm đó hoặc SO_REUSEPORTphải được đặt trên cả hai ổ cắm trước khi ràng buộc chúng. Tất nhiên, nó được phép đặt cả hai, SO_REUSEPORTSO_REUSEADDR, trên một ổ cắm.

Không có gì nhiều để nói về SO_REUSEPORTđiều đó ngoài việc nó được thêm vào muộn hơn SO_REUSEADDR, đó là lý do tại sao bạn sẽ không tìm thấy nó trong nhiều triển khai ổ cắm của các hệ thống khác, đã "rẽ nhánh" mã BSD trước khi tùy chọn này được thêm vào và không có cách liên kết hai ổ cắm với chính xác cùng một địa chỉ ổ cắm trong BSD trước tùy chọn này.

Kết nối () Trả lại EADDRINUSE?

Hầu hết mọi người đều biết rằng bind()có thể thất bại với lỗi EADDRINUSE, tuy nhiên, khi bạn bắt đầu chơi xung quanh với việc sử dụng lại địa chỉ, bạn cũng có thể gặp phải tình huống lạ connect()với lỗi đó. Làm sao có thể? Làm thế nào một địa chỉ từ xa, sau tất cả những gì kết nối thêm vào ổ cắm, đã được sử dụng? Kết nối nhiều ổ cắm với chính xác cùng một địa chỉ từ xa chưa bao giờ là vấn đề trước đây, vậy điều gì đang xảy ra ở đây?

Như tôi đã nói ở đầu câu trả lời của tôi, một kết nối được xác định bởi một bộ năm giá trị, nhớ không? Và tôi cũng đã nói, năm giá trị này phải là duy nhất nếu không hệ thống không thể phân biệt hai kết nối nữa, phải không? Vâng, với việc sử dụng lại địa chỉ, bạn có thể liên kết hai ổ cắm của cùng một giao thức với cùng một địa chỉ nguồn và cổng. Điều đó có nghĩa là ba trong số năm giá trị này giống nhau cho hai ổ cắm này. Nếu bây giờ bạn cố gắng kết nối cả hai ổ cắm này với cùng một địa chỉ đích và cổng, bạn sẽ tạo hai ổ cắm được kết nối, có bộ dữ liệu hoàn toàn giống nhau. Điều này không thể hoạt động, ít nhất là không cho các kết nối TCP (dù sao kết nối UDP không có kết nối thực sự). Nếu dữ liệu đến một trong hai kết nối, hệ thống không thể biết dữ liệu đó thuộc về kết nối nào.

Vì vậy, nếu bạn liên kết hai ổ cắm của cùng một giao thức với cùng một địa chỉ nguồn và cổng và cố gắng kết nối cả hai với cùng một địa chỉ đích và cổng, connect()sẽ thực sự thất bại với lỗi EADDRINUSEcho ổ cắm thứ hai mà bạn cố gắng kết nối, điều đó có nghĩa là ổ cắm với một bộ năm giá trị giống hệt nhau đã được kết nối.

Địa chỉ đa tuyến

Hầu hết mọi người bỏ qua thực tế là các địa chỉ multicast tồn tại, nhưng chúng tồn tại. Trong khi các địa chỉ unicast được sử dụng cho giao tiếp một-một, địa chỉ multicast được sử dụng cho giao tiếp một-nhiều. Hầu hết mọi người đều biết về các địa chỉ multicast khi họ tìm hiểu về IPv6 nhưng các địa chỉ multicast cũng tồn tại trong IPv4, mặc dù tính năng này không bao giờ được sử dụng rộng rãi trên Internet công cộng.

Ý nghĩa của các SO_REUSEADDRthay đổi đối với các địa chỉ multicast vì nó cho phép nhiều ổ cắm được liên kết chính xác với cùng một tổ hợp địa chỉ multicast nguồn và cổng. Nói cách khác, đối với các địa chỉ multicast SO_REUSEADDRhoạt động chính xác như SO_REUSEPORTđối với các địa chỉ unicast. Trên thực tế, mã xử lý SO_REUSEADDRSO_REUSEPORTgiống hệt nhau cho các địa chỉ multicast, điều đó có nghĩa là bạn có thể nói rằng SO_REUSEADDRnó ngụ ý SO_REUSEPORTcho tất cả các địa chỉ multicast và ngược lại.


FreeBSD / OpenBSD / NetBSD

Tất cả đều là những nhánh khá muộn của mã BSD gốc, đó là lý do tại sao cả ba đều cung cấp các tùy chọn giống như BSD và chúng cũng hoạt động giống như trong BSD.


macOS (MacOS X)

Về cốt lõi, macOS chỉ đơn giản là một UNIX kiểu BSD có tên là " Darwin ", dựa trên một nhánh rẽ khá muộn của mã BSD (BSD 4.3), sau đó được đồng bộ hóa lại với FreeBSD (tại thời điểm hiện tại) 5 cơ sở mã cho bản phát hành Mac OS 10.3, để Apple có thể đạt được sự tuân thủ POSIX đầy đủ (macOS được chứng nhận POSIX). Mặc dù có nhân vi mô ở lõi (" Mach "), phần còn lại của hạt nhân (" XNU ") về cơ bản chỉ là hạt nhân BSD và đó là lý do tại sao macOS cung cấp các tùy chọn giống như BSD và chúng cũng hoạt động giống như trong BSD .

iOS / watchOS / tvOS

iOS chỉ là một ngã ba macOS với một hạt nhân được sửa đổi và cắt xén một chút, phần nào tước bỏ bộ công cụ không gian người dùng và một bộ khung mặc định hơi khác. watchOS và tvOS là các nhánh của iOS, được loại bỏ hơn nữa (đặc biệt là watchOS). Theo hiểu biết tốt nhất của tôi, tất cả họ đều hành xử chính xác như macOS.


Linux

Linux <3.9

Trước Linux 3.9, chỉ có tùy chọn SO_REUSEADDRtồn tại. Tùy chọn này hoạt động chung giống như trong BSD với hai ngoại lệ quan trọng:

  1. Miễn là ổ cắm TCP lắng nghe (máy chủ) bị ràng buộc với một cổng cụ thể, SO_REUSEADDRtùy chọn này hoàn toàn bị bỏ qua cho tất cả các ổ cắm nhắm vào cổng đó. Liên kết một ổ cắm thứ hai vào cùng một cổng chỉ có thể nếu nó cũng có thể có trong BSD mà không cần SO_REUSEADDRthiết lập. Ví dụ: bạn không thể liên kết với một địa chỉ ký tự đại diện và sau đó đến một địa chỉ cụ thể hơn hoặc ngược lại, cả hai đều có thể có trong BSD nếu bạn đặt SO_REUSEADDR. Những gì bạn có thể làm là bạn có thể liên kết với cùng một cổng và hai địa chỉ không phải ký tự đại diện khác nhau, như điều đó luôn được cho phép. Ở khía cạnh này, Linux hạn chế hơn BSD.

  2. Ngoại lệ thứ hai là đối với các socket của máy khách, tùy chọn này hoạt động chính xác như SO_REUSEPORTtrong BSD, miễn là cả hai đều đặt cờ này trước khi chúng bị ràng buộc. Lý do cho phép đơn giản là điều quan trọng là có thể liên kết nhiều ổ cắm chính xác với cùng một địa chỉ ổ cắm UDP cho các giao thức khác nhau và vì SO_REUSEPORTtrước đây không có 3.9, hành vi của nó SO_REUSEADDRđã được thay đổi tương ứng để lấp đầy khoảng trống đó . Trong khía cạnh đó, Linux ít hạn chế hơn BSD.

Linux> = 3,9

Linux 3.9 cũng đã thêm tùy chọn SO_REUSEPORTvào Linux. Tùy chọn này hoạt động chính xác như tùy chọn trong BSD và cho phép liên kết chính xác cùng một địa chỉ và số cổng miễn là tất cả các ổ cắm có tùy chọn này được đặt trước khi ràng buộc chúng.

Tuy nhiên, vẫn còn hai điểm khác biệt SO_REUSEPORTtrên các hệ thống khác:

  1. Để ngăn chặn "chiếm quyền điều khiển cổng", có một hạn chế đặc biệt: Tất cả các ổ cắm muốn chia sẻ cùng một địa chỉ và kết hợp cổng phải thuộc về các quy trình có chung ID người dùng hiệu quả! Vì vậy, một người dùng không thể "đánh cắp" cổng của người dùng khác. Đây là một số phép thuật đặc biệt để phần nào bù đắp cho những lá cờ SO_EXCLBIND/ thiếu SO_EXCLUSIVEADDRUSE.

  2. Ngoài ra, hạt nhân thực hiện một số "ma thuật đặc biệt" cho SO_REUSEPORTcác socket không tìm thấy trong các hệ điều hành khác: Đối với các socket UDP, nó cố gắng phân phối các datagram một cách đồng đều, cho các socket nghe TCP, nó cố gắng phân phối các yêu cầu kết nối đến (những người được chấp nhận bằng cách gọi accept()) đồng đều trên tất cả các ổ cắm có chung địa chỉ và kết hợp cổng. Do đó, một ứng dụng có thể dễ dàng mở cùng một cổng trong nhiều tiến trình con và sau đó sử dụng SO_REUSEPORTđể có được sự cân bằng tải rất rẻ.


Android

Mặc dù toàn bộ hệ thống Android hơi khác so với hầu hết các bản phân phối Linux, nhưng cốt lõi của nó hoạt động một nhân Linux được sửa đổi một chút, do đó, mọi thứ áp dụng cho Linux cũng nên áp dụng cho Android.


các cửa sổ

Windows chỉ biết SO_REUSEADDRtùy chọn, không có SO_REUSEPORT. Cài đặt SO_REUSEADDRtrên một ổ cắm trong Windows hoạt động như cài đặt SO_REUSEPORTSO_REUSEADDRtrên một ổ cắm trong BSD, với một ngoại lệ: Một ổ cắm SO_REUSEADDRluôn có thể liên kết với chính xác cùng một địa chỉ nguồn và cổng như một ổ cắm đã bị ràng buộc, ngay cả khi ổ cắm kia không có tùy chọn này thiết lập khi nó bị ràng buộc . Hành vi này hơi nguy hiểm vì nó cho phép một ứng dụng "đánh cắp" cổng được kết nối của ứng dụng khác. Không cần phải nói, điều này có thể có ý nghĩa bảo mật lớn. Microsoft nhận ra rằng đây có thể là một vấn đề và do đó đã thêm một tùy chọn ổ cắm khác SO_EXCLUSIVEADDRUSE. Cài đặtSO_EXCLUSIVEADDRUSEtrên một ổ cắm đảm bảo rằng nếu liên kết thành công, sự kết hợp giữa địa chỉ nguồn và cổng được sở hữu độc quyền bởi ổ cắm này và không có ổ cắm nào khác có thể liên kết với chúng, ngay cả khi nó đã SO_REUSEADDRđược đặt.

Để biết thêm chi tiết về cách các cờ SO_REUSEADDRSO_EXCLUSIVEADDRUSEhoạt động trên Windows, cách chúng ảnh hưởng đến ràng buộc / ràng buộc lại, Microsoft vui lòng cung cấp một bảng tương tự như bảng của tôi ở gần đầu câu trả lời đó. Chỉ cần truy cập trang này và cuộn xuống một chút. Trên thực tế có ba bảng, bảng đầu tiên thể hiện hành vi cũ (trước Windows 2003), bảng thứ hai hành vi (Windows 2003 trở lên) và bảng thứ ba cho thấy hành vi thay đổi trong Windows 2003 và sau đó nếu các bind()cuộc gọi được thực hiện bởi người dùng khác nhau.


Solaris

Solaris là sự kế thừa của SunOS. SunOS ban đầu được dựa trên một nhánh của BSD, SunOS 5 và sau đó dựa trên một ngã ba của SVR4, tuy nhiên SVR4 là sự hợp nhất của BSD, System V và Xenix, do đó, đến một mức độ nào đó, Solaris cũng là một ngã ba BSD và khá sớm. Kết quả là Solaris chỉ biết SO_REUSEADDR, không có SO_REUSEPORT. Các SO_REUSEADDRhành vi khá giống như trong BSD. Theo như tôi biết thì không có cách nào có được hành vi giống như SO_REUSEPORTtrong Solaris, điều đó có nghĩa là không thể liên kết hai ổ cắm với cùng một địa chỉ và cổng.

Tương tự như Windows, Solaris có một tùy chọn để cung cấp cho ổ cắm một ràng buộc độc quyền. Tùy chọn này được đặt tên SO_EXCLBIND. Nếu tùy chọn này được đặt trên một ổ cắm trước khi ràng buộc nó, cài đặt SO_REUSEADDRtrên một ổ cắm khác sẽ không có hiệu lực nếu hai ổ cắm được kiểm tra xung đột địa chỉ. Ví dụ: nếu socketAbị ràng buộc với một địa chỉ ký tự đại diện và socketBđã SO_REUSEADDRđược bật và bị ràng buộc với một địa chỉ không phải ký tự đại diện và cùng một cổng socketA, liên kết này thường sẽ thành công, trừ khi socketAđã SO_EXCLBINDbật, trong trường hợp đó, nó sẽ thất bại bất kể SO_REUSEADDRcờ của socketB.


Hệ thống khác

Trong trường hợp hệ thống của bạn không được liệt kê ở trên, tôi đã viết một chương trình thử nghiệm nhỏ mà bạn có thể sử dụng để tìm hiểu cách hệ thống của bạn xử lý hai tùy chọn này. Ngoài ra nếu bạn nghĩ rằng kết quả của tôi là sai , trước tiên hãy chạy chương trình đó trước khi đăng bất kỳ bình luận nào và có thể đưa ra tuyên bố sai.

Tất cả những gì mã yêu cầu để xây dựng là API POSIX bit (cho các phần mạng) và trình biên dịch C99 (thực ra hầu hết trình biên dịch không phải C99 sẽ hoạt động miễn là chúng cung cấp inttypes.hstdbool.h; ví dụ như gccđược hỗ trợ cả trước khi cung cấp hỗ trợ C99 đầy đủ) .

Tất cả những gì chương trình cần chạy là ít nhất một giao diện trong hệ thống của bạn (không phải giao diện cục bộ) có địa chỉ IP được gán và một tuyến mặc định được đặt sử dụng giao diện đó. Chương trình sẽ thu thập địa chỉ IP đó và sử dụng nó làm "địa chỉ cụ thể" thứ hai.

Nó kiểm tra tất cả các kết hợp có thể bạn có thể nghĩ đến:

  • Giao thức TCP và UDP
  • Ổ cắm thông thường, ổ cắm nghe (máy chủ), ổ cắm phát đa hướng
  • SO_REUSEADDR đặt trên socket1, socket2 hoặc cả hai socket
  • SO_REUSEPORT đặt trên socket1, socket2 hoặc cả hai socket
  • Tất cả các kết hợp địa chỉ bạn có thể tạo ra 0.0.0.0(ký tự đại diện), 127.0.0.1(địa chỉ cụ thể) và địa chỉ cụ thể thứ hai được tìm thấy trong giao diện chính của bạn (đối với phát đa hướng, nó chỉ 224.1.2.3trong tất cả các thử nghiệm)

và in kết quả trong một bảng đẹp. Nó cũng sẽ hoạt động trên các hệ thống không biết SO_REUSEPORT, trong trường hợp này tùy chọn này đơn giản là không được thử nghiệm.

Điều mà chương trình không thể dễ dàng kiểm tra là cách thức SO_REUSEADDRhoạt động của các ổ cắm ở TIME_WAITtrạng thái vì nó rất khó để buộc và giữ một ổ cắm ở trạng thái đó. May mắn thay, hầu hết các hệ điều hành dường như chỉ đơn giản hoạt động như BSD ở đây và hầu hết các lập trình viên có thể đơn giản bỏ qua sự tồn tại của trạng thái đó.

Đây là mã (Tôi không thể bao gồm nó ở đây, câu trả lời có giới hạn kích thước và mã sẽ đẩy câu trả lời này vượt quá giới hạn).


9
Ví dụ: "địa chỉ nguồn" thực sự phải là "địa chỉ cục bộ", ba trường tiếp theo cũng vậy. Liên kết với INADDR_ANYkhông ràng buộc địa chỉ địa phương hiện tại, nhưng tất cả các địa chỉ trong tương lai là tốt. listenchắc chắn tạo ra các socket có cùng giao thức, địa chỉ cục bộ và cổng cục bộ, mặc dù bạn nói rằng điều đó là không thể.
Ben Voigt

9
@Ben Nguồn và Đích là các thuật ngữ chính thức được sử dụng cho địa chỉ IP (mà tôi tham khảo chính). Local và Remote sẽ không có ý nghĩa gì, vì trên thực tế, địa chỉ Remote có thể là địa chỉ "Local" và ngược lại với Destination là Source chứ không phải Local. Tôi không biết vấn đề của bạn là gì INADDR_ANY, tôi chưa bao giờ nói nó sẽ không liên kết với các địa chỉ trong tương lai. Và listenhoàn toàn không tạo ra bất kỳ ổ cắm nào, điều này làm cho toàn bộ câu của bạn hơi lạ.
Mecki

7
@Ben Khi một địa chỉ mới được thêm vào hệ thống, đó cũng là một "địa chỉ cục bộ hiện có", nó mới bắt đầu tồn tại. Tôi không nói "cho tất cả các hiện địa chỉ địa phương hiện có". Trên thực tế tôi thậm chí còn nói rằng ổ cắm trên thực tế thực sự bị ràng buộc với ký tự đại diện , điều đó có nghĩa là ổ cắm bị ràng buộc với bất cứ thứ gì khớp với ký tự đại diện này, bây giờ, ngày mai và trong hàng trăm năm. Tương tự cho nguồn và đích, bạn chỉ cần nitpicking ở đây. Bạn có đóng góp kỹ thuật thực sự nào không?
Mecki

8
@Mecki: Bạn thực sự nghĩ rằng từ hiện tại bao gồm những thứ không tồn tại bây giờ nhưng sẽ trong tương lai? Nguồn và đích không phải là một nitpick. Khi các gói đến được khớp với một ổ cắm, bạn đang nói rằng địa chỉ đích trong gói sẽ được khớp với địa chỉ "nguồn" của ổ cắm? Điều đó sai và bạn biết điều đó, bạn đã nói rằng nguồnđích là đối lập. Các địa phương địa chỉ trên các ổ cắm được khớp với địa chỉ đích của gói tin gửi đến, và được đặt trong các nguồn địa chỉ trên gói tin gửi đi.
Ben Voigt

10
@Mecki: Điều đó có ý nghĩa hơn nhiều nếu bạn nói "Địa chỉ cục bộ của ổ cắm là địa chỉ nguồn của các gói đi và địa chỉ đích của các gói đến". Các gói có địa chỉ nguồn và đích. Máy chủ và ổ cắm trên máy chủ, không. Đối với ổ cắm datagram cả hai đồng đẳng đều bằng nhau. Đối với ổ cắm TCP, do bắt tay ba chiều, có người khởi tạo (máy khách) và máy đáp ứng (máy chủ), nhưng điều đó vẫn không có nghĩa là ổ cắm kết nối hoặc ổ cắm được kết nối có nguồnđích , bởi vì lưu lượng truy cập chảy cả hai chiều.
Ben Voigt
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.