移植性の素晴らしい世界へようこそ... というか、移植性の欠如です。これら 2 つのオプションを詳細に分析し、さまざまなオペレーティング システムがそれらをどのように処理するかを詳しく見ていく前に、BSD ソケット実装がすべてのソケット実装の母であることに注意してください。基本的に、他のすべてのシステムは、ある時点で BSD ソケットの実装 (または少なくともそのインターフェイス) をコピーし、それを独自に進化させ始めました。もちろん、BSD ソケットの実装も同時に進化したため、それを後でコピーしたシステムは、以前にコピーしたシステムに欠けていた機能を取得しました。 BSD ソケットの実装を理解することは、他のすべてのソケットの実装を理解するための鍵となるため、BSD システム用のコードを書きたくない場合でも、それについて読む必要があります。
これら 2 つのオプションを検討する前に、知っておくべき基本事項がいくつかあります。 TCP/UDP 接続は、5 つの値のタプルによって識別されます:
{<protocol>, <src addr>, <src port>, <dest addr>, <dest port>}
これらの値の一意の組み合わせにより、接続が識別されます。その結果、2 つの接続が同じ 5 つの値を持つことはできません。そうしないと、システムはこれらの接続を区別できなくなります。
socket()
でソケットを作成すると、ソケットのプロトコルが設定されます。 関数。送信元アドレスとポートは bind()
で設定されます 関数。宛先アドレスとポートは connect()
で設定されます 関数。 UDP はコネクションレス プロトコルであるため、UDP ソケットを接続せずに使用できます。ただし、それらを接続することは許可されており、場合によっては、コードや一般的なアプリケーション設計に非常に有利です.コネクションレス モードでは、データが最初に送信されたときに明示的にバインドされなかった UDP ソケットは、通常、システムによって自動的にバインドされます。これは、バインドされていない UDP ソケットが (応答) データを受信できないためです。バインドされていない TCP ソケットの場合も同様で、接続される前に自動的にバインドされます。
明示的にソケットをバインドすると、ポート 0
にバインドできます 、これは「任意のポート」を意味します。ソケットは既存のすべてのポートに実際にバインドすることはできないため、その場合、システムは特定のポート自体を選択する必要があります (通常は、事前定義された OS 固有のソース ポートの範囲から)。送信元アドレスにも同様のワイルドカードがあり、「任意のアドレス」(0.0.0.0
) にすることができます。 IPv4 と ::
の場合 IPv6の場合)。ポートの場合とは異なり、ソケットは実際には「すべてのローカル インターフェイスのすべてのソース IP アドレス」を意味する「任意のアドレス」にバインドできます。ソケットが後で接続される場合、システムは特定のソース IP アドレスを選択する必要があります。これは、ソケットを接続すると同時にローカル IP アドレスにバインドすることができないためです。宛先アドレスとルーティング テーブルの内容に応じて、システムは適切な送信元アドレスを選択し、「any」バインディングを選択した送信元 IP アドレスへのバインディングに置き換えます。
デフォルトでは、送信元アドレスと送信元ポートの同じ組み合わせに 2 つのソケットをバインドすることはできません。送信元ポートが異なる限り、送信元アドレスは実際には関係ありません。バインド socketA
ipA:portA
へ および socketB
ipB:portB
まで ipA != ipB
の場合は常に可能です portA == portB
の場合でも当てはまります .例えば。 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 アドレスと競合します。
これまでに述べたことは、すべての主要なオペレーティング システムでほぼ同じです。アドレスの再利用が始まると、OS 固有のものになり始めます。上で述べたように、BSD はすべてのソケット実装の母であるため、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 アドレス」を意味するため、すべてのローカル IP アドレスがこのソケットで使用されていると見なされ、これには 192.168.0.1
が含まれます 、 それも。 SO_REUSEADDR
で 0.0.0.0
から成功します と 192.168.0.1
正確ではない 1 つはすべてのローカル アドレスのワイルドカードで、もう 1 つは特定のローカル アドレスです。上記のステートメントは、socketA
の順序に関係なく真であることに注意してください。 と socketB
結合しています; SO_REUSEADDR
なし SO_REUSEADDR
で常に失敗します 必ず成功します。
より良い概要を提供するために、ここに表を作成し、すべての可能な組み合わせをリストしましょう:
SO_REUSEADDR socketA socketB Result --------------------------------------------------------------------- ON/OFF 192.168.0.1:21 192.168.0.1:21 Error (EADDRINUSE) ON/OFF 192.168.0.1:21 10.0.0.1:21 OK ON/OFF 10.0.0.1:21 192.168.0.1:21 OK OFF 0.0.0.0:21 192.168.1.0:21 Error (EADDRINUSE) OFF 192.168.1.0:21 0.0.0.0:21 Error (EADDRINUSE) ON 0.0.0.0:21 192.168.1.0:21 OK ON 192.168.1.0:21 0.0.0.0:21 OK ON/OFF 0.0.0.0:21 0.0.0.0:21 Error (EADDRINUSE)
上記の表では、socketA
を想定しています。 socketA
に指定されたアドレスに既に正常にバインドされています 、次に socketB
SO_REUSEADDR
を取得します。 設定されているかどうかにかかわらず、最終的に socketB
に指定されたアドレスにバインドされます . Result
socketB
のバインド操作の結果です .最初の列が ON/OFF
の場合 、SO_REUSEADDR
の値 結果とは無関係です。
わかりました、SO_REUSEADDR
ワイルドカード アドレスに影響を与えます。効果はそれだけではありません。ほとんどの人が SO_REUSEADDR
を使用する理由でもある、別のよく知られた効果があります。 そもそもサーバープログラムで。このオプションのその他の重要な使用法については、TCP プロトコルの仕組みを詳しく調べる必要があります。
TCP ソケットが閉じられている場合、通常は 3 ウェイ ハンドシェイクが実行されます。シーケンスは FIN-ACK
と呼ばれます .ここでの問題は、そのシーケンスの最後の ACK が反対側に到着したか、到着していない可能性があり、到着した場合にのみ、反対側もソケットが完全に閉じていると見なすことです。アドレスとポートの組み合わせが再利用されるのを防ぐため、システムは最後の ACK
を送信した後、ソケットをすぐに無効と見なしません。 代わりに、ソケットを一般に TIME_WAIT
と呼ばれる状態にします。 .数分間その状態になることがあります (システム依存の設定)。ほとんどのシステムでは、残留を有効にして残留時間をゼロ 1 に設定することでこの状態を回避できますが、これが常に可能であり、システムが常にこの要求を尊重するという保証はありません。リセットによって閉じられるソケット (RST
)、これは必ずしも優れたアイデアではありません。リンガー タイムの詳細については、このトピックに関する私の回答をご覧ください。
問題は、システムが状態 TIME_WAIT
のソケットをどのように扱うかです。 ? SO_REUSEADDR
の場合 が設定されていない、状態 TIME_WAIT
のソケット ソースアドレスとポートにまだバインドされていると見なされ、新しいソケットを同じアドレスとポートにバインドしようとすると、ソケットが実際に閉じられるまで失敗します。したがって、ソケットを閉じた直後にソケットのソース アドレスを再バインドできるとは思わないでください。ほとんどの場合、これは失敗します。ただし、 SO_REUSEADDR
の場合 バインドしようとしているソケットに設定され、別のソケットが同じアドレスと状態のポートにバインドされています TIME_WAIT
すでに「半分死んでいる」ため、単に無視され、ソケットは問題なくまったく同じアドレスにバインドできます。その場合、他のソケットがまったく同じアドレスとポートを持つ可能性があることは何の役割も果たしません。 TIME_WAIT
で死にかけているソケットとまったく同じアドレスとポートにソケットをバインドすることに注意してください。 state は、他のソケットがまだ「動作中」の場合、予期しない、通常は望ましくない副作用を引き起こす可能性がありますが、それはこの回答の範囲を超えており、幸いなことに、これらの副作用は実際にはかなりまれです。
最後に SO_REUSEADDR
について知っておくべきことが 1 つあります。 .バインド先のソケットでアドレスの再利用が有効になっている限り、上記のすべてが機能します。すでにバインドされているか、TIME_WAIT
にある他のソケットは必要ありません。 状態でも、バインド時にこのフラグが設定されていました。バインドが成功するか失敗するかを決定するコードは、SO_REUSEADDR
のみを検査します。 bind()
に供給されるソケットのフラグ 検査される他のすべてのソケットについては、このフラグは調べられません。
SO_REUSEPORT
SO_REUSEPORT
SO_REUSEADDR
はほとんどの人が期待するものです することが。基本的には SO_REUSEPORT
任意の数のソケットを 正確に にバインドできます all である限り、同じ送信元アドレスとポート 以前にバインドされたソケットにも 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 では 2 つのソケットをまったく同じソケットアドレスにバインドする方法がありませんでした。オプション。
Connect() は EADDRINUSE を返しますか?
ほとんどの人は bind()
を知っています エラー EADDRINUSE
で失敗する場合があります ただし、アドレスの再利用をいじり始めると、connect()
という奇妙な状況に遭遇する可能性があります。 そのエラーでも失敗します。どうすればいいの? connect がソケットに追加するものであるにもかかわらず、リモートアドレスがすでに使用されている可能性はありますか?複数のソケットをまったく同じリモート アドレスに接続することは、これまで問題になったことはありませんでした。では、何が問題になっているのでしょうか?
返信の一番上で述べたように、接続は 5 つの値のタプルによって定義されます。覚えていますか?また、これらの 5 つの値は一意である必要があるとも言いました。そうしないと、システムは 2 つの接続を区別できなくなりますよね?アドレスを再利用すると、同じプロトコルの 2 つのソケットを同じ送信元アドレスとポートにバインドできます。つまり、これらの 5 つの値のうち 3 つが、これら 2 つのソケットで既に同じであることを意味します。これらのソケットの両方を同じ宛先アドレスとポートに接続しようとすると、タプルが完全に同一の 2 つの接続されたソケットが作成されます。これは、少なくとも TCP 接続では機能しません (とにかく、UDP 接続は実際の接続ではありません)。 2 つの接続のいずれかのデータが到着した場合、システムはデータがどちらの接続に属しているかを判断できませんでした。着信データがどの接続に属しているかをシステムが問題なく識別できるように、少なくとも宛先アドレスまたは宛先ポートはいずれかの接続で異なる必要があります。
したがって、同じプロトコルの 2 つのソケットを同じソース アドレスとポートにバインドし、両方を同じ宛先アドレスとポートに接続しようとすると、connect()
になります。 実際にはエラー EADDRINUSE
で失敗します 接続しようとする 2 番目のソケットの場合、これは、5 つの値の同一のタプルを持つソケットが既に接続されていることを意味します。
マルチキャスト アドレス
ほとんどの人は、マルチキャスト アドレスが存在するという事実を無視していますが、実際には存在します。ユニキャスト アドレスは 1 対 1 の通信に使用され、マルチキャスト アドレスは 1 対多の通信に使用されます。ほとんどの人は、IPv6 について学んだときにマルチキャスト アドレスに気づきましたが、この機能が公共のインターネットで広く使用されることはありませんでしたが、マルチキャスト アドレスは IPv4 にも存在していました。
SO_REUSEADDR
の意味 複数のソケットをソースマルチキャストアドレスとポートのまったく同じ組み合わせにバインドできるため、マルチキャストアドレスの変更。つまり、マルチキャスト アドレス SO_REUSEADDR
の場合 SO_REUSEPORT
とまったく同じように動作します ユニキャスト アドレス用。実際、コードは SO_REUSEADDR
を扱います と SO_REUSEPORT
マルチキャスト アドレスについても同様です。つまり、SO_REUSEADDR
と言えます。 SO_REUSEPORT
を意味します すべてのマルチキャスト アドレスとその逆。
FreeBSD/OpenBSD/NetBSD
これらはすべて、元の BSD コードの後期フォークであるため、3 つすべてが BSD と同じオプションを提供し、BSD と同じように動作します。
macOS (MacOS X)
基本的に、macOS は「Darwin」という名前の BSD スタイルの UNIX です。 "、BSD コード (BSD 4.3) のかなり遅いフォークに基づいており、後で Mac OS 10.3 リリース用の (当時最新の) FreeBSD 5 コードベースと再同期されたので、Apple は完全な POSIX 準拠 (macOS は POSIX 認定済み) コアにマイクロカーネル ("Mach ")、残りのカーネル ("XNU ") は基本的に単なる BSD カーネルです。そのため、macOS は BSD と同じオプションを提供し、BSD と同じように動作します。
iOS / watchOS / tvOS
iOS は、カーネルがわずかに変更およびトリミングされ、ユーザー空間のツールセットが多少削除され、デフォルトのフレームワーク セットがわずかに異なる macOS フォークにすぎません。 watchOS と tvOS は iOS フォークであり、さらに機能を削ぎ落としています (特に watchOS)。私の知る限り、それらはすべて macOS とまったく同じように動作します。
Linux
Linux <3.9
Linux 3.9 より前では、オプション SO_REUSEADDR
のみ 存在しました。このオプションは、2 つの重要な例外を除いて、一般的に BSD と同じように動作します:
リスニング (サーバー) TCP ソケットが特定のポートにバインドされている限り、SO_REUSEADDR
オプションは、そのポートを対象とするすべてのソケットに対して完全に無視されます。 2 番目のソケットを同じポートにバインドできるのは、SO_REUSEADDR
を持たない BSD でも可能だった場合のみです。 設定。例えば。 SO_REUSEADDR
を設定すると、ワイルドカード アドレスにバインドしてからより具体的なアドレスにバインドしたり、その逆はできません。BSD では両方が可能です。 .できることは、常に許可されているため、同じポートと2つの異なるワイルドカード以外のアドレスにバインドできることです。この点では、Linux は BSD よりも制限的です。
2 番目の例外は、クライアント ソケットの場合、このオプションは SO_REUSEPORT
とまったく同じように動作することです。 BSD では、バインドされる前に両方にこのフラグが設定されている限り。これを許可した理由は、複数のソケットをさまざまなプロトコルの同じ UDP ソケット アドレスに正確にバインドできることが重要であり、以前は SO_REUSEPORT
がなかったからです。 3.9 より前では、SO_REUSEADDR
の動作 そのギャップを埋めるためにそれに応じて変更されました。その点では、Linux は BSD よりも制限が緩いです。
Linux>=3.9
Linux 3.9 ではオプション SO_REUSEPORT
が追加されました Linuxにも。このオプションは、BSD のオプションとまったく同じように動作し、バインドする前にすべてのソケットにこのオプションが設定されている限り、まったく同じアドレスとポート番号にバインドできます。
それでも、SO_REUSEPORT
との違いはまだ 2 つあります。 他のシステム:
「ポート ハイジャック」を防ぐために、特別な制限が 1 つあります。同じアドレスとポートの組み合わせを共有するすべてのソケットは、同じ実効ユーザー ID を共有するプロセスに属している必要があります。 したがって、あるユーザーが別のユーザーのポートを「盗む」ことはできません。これは、欠落している SO_EXCLBIND
をいくらか補うための特別な魔法です。 /SO_EXCLUSIVEADDRUSE
さらに、カーネルは SO_REUSEPORT
に対していくつかの「特別な魔法」を実行します 他のオペレーティング システムには見られないソケット:UDP ソケットの場合、データグラムを均等に分散しようとします。TCP リッスン ソケットの場合、着信接続要求 (accept()
を呼び出すことによって受け入れられるもの) を分散しようとします。 ) 同じアドレスとポートの組み合わせを共有するすべてのソケットで均等に。したがって、アプリケーションは複数の子プロセスで同じポートを簡単に開き、SO_REUSEPORT
を使用できます。 非常に安価な負荷分散を実現します。
アンドロイド
Android システム全体はほとんどの Linux ディストリビューションと多少異なりますが、そのコアはわずかに変更された Linux カーネルで動作するため、Linux に適用されるものはすべて Android にも適用されます。
窓
Windows は SO_REUSEADDR
しか認識しません オプション、SO_REUSEPORT
はありません .設定 SO_REUSEADDR
Windows のソケットでは SO_REUSEPORT
を設定するように動作します と SO_REUSEADDR
BSD のソケットで、1 つの例外を除いて:
Windows 2003 より前では、SO_REUSEADDR
のソケット バインドされたときに他のソケットにこのオプションが設定されていなくても、既にバインドされているソケットとまったく同じソース アドレスとポートに常にバインドできます。 .この動作により、アプリケーションは別のアプリケーションの接続ポートを「盗む」ことができました。言うまでもなく、これはセキュリティに大きな影響を与えます!
Microsoft はそれを認識し、別の重要なソケット オプションを追加しました:SO_EXCLUSIVEADDRUSE
.設定 SO_EXCLUSIVEADDRUSE
ソケット上では、バインディングが成功した場合、送信元アドレスとポートの組み合わせがこのソケットによって排他的に所有され、他のソケットがそれらにバインドできないことを確認します。さえも SO_REUSEADDR
の場合
この既定の動作は、Windows 2003 で最初に変更されました。Microsoft はそれを "Enhanced Socket Security" (他のすべての主要なオペレーティング システムの既定の動作の変な名前) と呼んでいます。詳細については、このページにアクセスしてください。 3 つの表があります:最初の表は従来の動作 (互換モードを使用する場合はまだ使用中です!) を示し、2 番目の表は bind()
の場合の Windows 2003 以降の動作を示します。 呼び出しは同じユーザーによって行われ、3 回目は bind()
呼び出しは異なるユーザーによって行われます。
ソラリス
Solaris は SunOS の後継です。 SunOS はもともと BSD のフォークに基づいており、SunOS 5 以降は SVR4 のフォークに基づいていましたが、SVR4 は BSD、System V、および Xenix のマージであるため、Solaris もある程度までは BSD のフォークであり、かなり初期のもの。その結果、Solaris は SO_REUSEADDR
しか認識しません 、 SO_REUSEPORT
はありません . SO_REUSEADDR
BSD とほぼ同じように動作します。私の知る限り、SO_REUSEPORT
と同じ動作をする方法はありません。 Solaris では、2 つのソケットをまったく同じアドレスとポートにバインドすることはできません。
Windows と同様に、Solaris にはソケットに排他的バインディングを与えるオプションがあります。このオプションの名前は SO_EXCLBIND
です .このオプションがバインド前にソケットに設定されている場合は、SO_REUSEADDR
を設定します。 2 つのソケットでアドレスの競合がテストされている場合、別のソケットでは効果がありません。例えば。 socketA
の場合 ワイルドカード アドレスと socketB
にバインドされています SO_REUSEADDR
を持っています 有効で、ワイルドカード以外のアドレスと socketA
と同じポートにバインドされています socketA
でない限り、このバインドは通常成功します。 SO_EXCLBIND
を持っていた この場合、SO_REUSEADDR
に関係なく失敗します。 socketB
の旗 .
その他のシステム
お使いのシステムが上記のリストにない場合のために、お使いのシステムがこれら 2 つのオプションをどのように処理するかを調べるために使用できる小さなテスト プログラムを作成しました。 また、私の結果が間違っていると思われる場合 、コメントを投稿して虚偽の主張をする前に、まずそのプログラムを実行してください。
コードを構築するために必要なのは、少しの POSIX API (ネットワーク部分用) と C99 コンパイラ (実際には、ほとんどの非 C99 コンパイラは inttypes.h
を提供する限り同様に機能します) です。 と stdbool.h
;例えばgcc
完全な C99 サポートを提供するずっと前に、両方をサポートしていました)。
プログラムを実行するために必要なのは、システム内の少なくとも 1 つのインターフェイス (ローカル インターフェイス以外) に IP アドレスが割り当てられており、そのインターフェイスを使用するデフォルト ルートが設定されていることだけです。プログラムはその IP アドレスを収集し、それを 2 番目の「特定のアドレス」として使用します。
考えられるすべての組み合わせをテストします:
- TCP および UDP プロトコル
- 通常のソケット、リッスン (サーバー) ソケット、マルチキャスト ソケット
SO_REUSEADDR
socket1、socket2、または両方のソケットに設定SO_REUSEPORT
socket1、socket2、または両方のソケットに設定0.0.0.0
から作成できるすべてのアドレスの組み合わせ (ワイルドカード)、127.0.0.1
(特定のアドレス)、およびプライマリ インターフェイスで見つかった 2 番目の特定のアドレス (マルチキャストの場合は224.1.2.3
のみ) すべてのテストで)
結果を素敵な表に出力します。 SO_REUSEPORT
を知らないシステムでも動作します 、その場合、このオプションは単にテストされていません。
プログラムが簡単にテストできないのは、SO_REUSEADDR
の方法です。 TIME_WAIT
のソケットに作用します ソケットをその状態に強制して保持するのは非常に難しいためです。幸いなことに、ほとんどのオペレーティング システムは、ここでは単純に BSD のように動作するようであり、ほとんどの場合、プログラマーはその状態の存在を単純に無視できます。
コードは次のとおりです (ここに含めることはできません。回答にはサイズ制限があり、コードはこの返信を制限を超えてプッシュします)。
Mecki の答えは完全に完璧ですが、FreeBSD も SO_REUSEPORT_LB
をサポートしていることを追加する価値があります。 、Linux の SO_REUSEPORT
を模倣します 動作 - 負荷を分散します。 setsockopt(2) を参照